Show list of repeaters alongside route

This commit is contained in:
Marcus Kida 2026-02-10 09:31:27 +01:00
parent bdaa17ca38
commit 2b7b7e4824
4 changed files with 53 additions and 23 deletions

55
db.go
View file

@ -163,9 +163,9 @@ func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band str
} }
// Route corridor query: find repeaters within corridorKm of a polyline. // Route corridor query: find repeaters within corridorKm of a polyline.
func queryRepeatersAlongRoute(db *sql.DB, points [][2]float64, corridorKm float64, band string, networks []string, showHotspots bool, showInactive bool) ([]Repeater, error) { func queryRepeatersAlongRoute(db *sql.DB, points [][2]float64, corridorKm float64, band string, networks []string, showHotspots bool, showInactive bool) ([]RepeaterWithDistance, error) {
if len(points) == 0 { if len(points) == 0 {
return []Repeater{}, nil return []RepeaterWithDistance{}, nil
} }
// Compute bounding box of all route points + corridor padding // Compute bounding box of all route points + corridor padding
@ -199,13 +199,25 @@ func queryRepeatersAlongRoute(db *sql.DB, points [][2]float64, corridorKm float6
return nil, err return nil, err
} }
// Filter by distance to route segments // Pre-compute cumulative route distances from start
var results []Repeater cumDist := make([]float64, len(points))
for i := 1; i < len(points); i++ {
cumDist[i] = cumDist[i-1] + haversineKm(points[i-1][0], points[i-1][1], points[i][0], points[i][1])
}
// Filter by perpendicular distance to route; record along-route distance from start
var results []RepeaterWithDistance
for _, r := range candidates { for _, r := range candidates {
if minDistToRoute(r.Lat, r.Lng, points) <= corridorKm { perpDist, alongDist := routeDistances(r.Lat, r.Lng, points, cumDist)
results = append(results, r) if perpDist <= corridorKm {
results = append(results, RepeaterWithDistance{Repeater: r, Distance: math.Round(alongDist*10) / 10})
} }
} }
sort.Slice(results, func(i, j int) bool {
return results[i].Distance < results[j].Distance
})
return results, nil return results, nil
} }
@ -239,22 +251,29 @@ func queryRepeatersInRadius(db *sql.DB, lat, lng, radiusKm float64, band string,
return results, nil return results, nil
} }
func minDistToRoute(lat, lng float64, points [][2]float64) float64 { // routeDistances returns the perpendicular distance from a point to the nearest
best := math.Inf(1) // route segment, and the along-route distance from the route start to the
// projection point on that segment.
func routeDistances(lat, lng float64, points [][2]float64, cumDist []float64) (perpDist, alongDist float64) {
bestPerp := math.Inf(1)
bestAlong := 0.0
for i := 0; i < len(points)-1; i++ { for i := 0; i < len(points)-1; i++ {
d := distToSegmentKm(lat, lng, points[i][0], points[i][1], points[i+1][0], points[i+1][1]) d, t := distToSegmentWithParam(lat, lng, points[i][0], points[i][1], points[i+1][0], points[i+1][1])
if d < best { if d < bestPerp {
best = d bestPerp = d
segLen := cumDist[i+1] - cumDist[i]
bestAlong = cumDist[i] + t*segLen
} }
} }
if len(points) == 1 { if len(points) == 1 {
best = haversineKm(lat, lng, points[0][0], points[0][1]) bestPerp = haversineKm(lat, lng, points[0][0], points[0][1])
} }
return best return bestPerp, bestAlong
} }
// Approximate distance from point P to line segment AB in km. // distToSegmentWithParam returns the approximate distance from point P to line
func distToSegmentKm(pLat, pLng, aLat, aLng, bLat, bLng float64) float64 { // segment AB in km, and the projection parameter t (01) along the segment.
func distToSegmentWithParam(pLat, pLng, aLat, aLng, bLat, bLng float64) (dist, t float64) {
cosLat := math.Cos(pLat * math.Pi / 180) cosLat := math.Cos(pLat * math.Pi / 180)
// Project to approximate planar coordinates (km) // Project to approximate planar coordinates (km)
px := (pLng - aLng) * cosLat * 111.32 px := (pLng - aLng) * cosLat * 111.32
@ -264,10 +283,10 @@ func distToSegmentKm(pLat, pLng, aLat, aLng, bLat, bLng float64) float64 {
lenSq := bx*bx + by*by lenSq := bx*bx + by*by
if lenSq == 0 { if lenSq == 0 {
return math.Sqrt(px*px + py*py) return math.Sqrt(px*px + py*py), 0
} }
t := (px*bx + py*by) / lenSq t = (px*bx + py*by) / lenSq
if t < 0 { if t < 0 {
t = 0 t = 0
} }
@ -277,7 +296,7 @@ func distToSegmentKm(pLat, pLng, aLat, aLng, bLat, bLng float64) float64 {
dx := px - t*bx dx := px - t*bx
dy := py - t*by dy := py - t*by
return math.Sqrt(dx*dx + dy*dy) return math.Sqrt(dx*dx + dy*dy), t
} }
func haversineKm(lat1, lng1, lat2, lng2 float64) float64 { func haversineKm(lat1, lng1, lat2, lng2 float64) float64 {

View file

@ -176,11 +176,11 @@ func handleRouteRepeaters(db *sql.DB) http.HandlerFunc {
} }
if repeaters == nil { if repeaters == nil {
repeaters = []Repeater{} repeaters = []RepeaterWithDistance{}
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(apiResponse{ json.NewEncoder(w).Encode(radiusResponse{
Repeaters: repeaters, Repeaters: repeaters,
Count: len(repeaters), Count: len(repeaters),
}) })

View file

@ -52,6 +52,7 @@
var pinRadiusVal = document.getElementById("pin-radius-val"); var pinRadiusVal = document.getElementById("pin-radius-val");
var pinClearBtn = document.getElementById("pin-clear"); var pinClearBtn = document.getElementById("pin-clear");
var pinListEl = document.getElementById("pin-list"); var pinListEl = document.getElementById("pin-list");
var routeListEl = document.getElementById("route-list");
clearBtn.style.display = "none"; clearBtn.style.display = "none";
@ -404,6 +405,7 @@
if (getSelectedNetworks() === "none") { if (getSelectedNetworks() === "none") {
markerLayer.clearLayers(); markerLayer.clearLayers();
routeListEl.innerHTML = "";
showCount(0); showCount(0);
return; return;
} }
@ -427,6 +429,8 @@
}) })
.then(function (data) { .then(function (data) {
displayRepeaters(data.repeaters); displayRepeaters(data.repeaters);
renderRepeaterList(routeListEl, data.repeaters);
routeListEl.style.display = "";
console.log("Showing " + data.count + " repeaters along route"); console.log("Showing " + data.count + " repeaters along route");
showCount(data.count); showCount(data.count);
}) })
@ -568,6 +572,8 @@
isRouteMode = false; isRouteMode = false;
clearBtn.style.display = "none"; clearBtn.style.display = "none";
corridorRow.style.display = "none"; corridorRow.style.display = "none";
routeListEl.style.display = "none";
routeListEl.innerHTML = "";
fromInput.value = ""; fromInput.value = "";
toInput.value = ""; toInput.value = "";
fetchRepeaters(); fetchRepeaters();
@ -611,8 +617,8 @@
}); });
} }
function renderPinList(repeaters) { function renderRepeaterList(container, repeaters) {
pinListEl.innerHTML = ""; container.innerHTML = "";
repeaters.forEach(function (r) { repeaters.forEach(function (r) {
var item = document.createElement("div"); var item = document.createElement("div");
item.className = "pin-list-item"; item.className = "pin-list-item";
@ -632,10 +638,14 @@
} }
}); });
}); });
pinListEl.appendChild(item); container.appendChild(item);
}); });
} }
function renderPinList(repeaters) {
renderRepeaterList(pinListEl, repeaters);
}
function placePin(latlng) { function placePin(latlng) {
// Clear route mode if active // Clear route mode if active
if (isRouteMode) clearRoute(); if (isRouteMode) clearRoute();

View file

@ -48,6 +48,7 @@
<span id="corridor-val">10</span> km <span id="corridor-val">10</span> km
</label> </label>
</div> </div>
<div id="route-list" class="pin-list" style="display:none"></div>
<div id="pin-controls" class="pin-controls" style="display:none"> <div id="pin-controls" class="pin-controls" style="display:none">
<div class="controls-row pin-header"> <div class="controls-row pin-header">
<label class="pin-radius-label">Radius <label class="pin-radius-label">Radius