diff --git a/db.go b/db.go index 3132f42..8d784a6 100644 --- a/db.go +++ b/db.go @@ -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. -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 { - return []Repeater{}, nil + return []RepeaterWithDistance{}, nil } // 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 } - // Filter by distance to route segments - var results []Repeater + // Pre-compute cumulative route distances from start + 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 { - if minDistToRoute(r.Lat, r.Lng, points) <= corridorKm { - results = append(results, r) + perpDist, alongDist := routeDistances(r.Lat, r.Lng, points, cumDist) + 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 } @@ -239,22 +251,29 @@ func queryRepeatersInRadius(db *sql.DB, lat, lng, radiusKm float64, band string, return results, nil } -func minDistToRoute(lat, lng float64, points [][2]float64) float64 { - best := math.Inf(1) +// routeDistances returns the perpendicular distance from a point to the nearest +// 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++ { - d := distToSegmentKm(lat, lng, points[i][0], points[i][1], points[i+1][0], points[i+1][1]) - if d < best { - best = d + d, t := distToSegmentWithParam(lat, lng, points[i][0], points[i][1], points[i+1][0], points[i+1][1]) + if d < bestPerp { + bestPerp = d + segLen := cumDist[i+1] - cumDist[i] + bestAlong = cumDist[i] + t*segLen } } 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. -func distToSegmentKm(pLat, pLng, aLat, aLng, bLat, bLng float64) float64 { +// distToSegmentWithParam returns the approximate distance from point P to line +// segment AB in km, and the projection parameter t (0–1) along the segment. +func distToSegmentWithParam(pLat, pLng, aLat, aLng, bLat, bLng float64) (dist, t float64) { cosLat := math.Cos(pLat * math.Pi / 180) // Project to approximate planar coordinates (km) 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 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 { t = 0 } @@ -277,7 +296,7 @@ func distToSegmentKm(pLat, pLng, aLat, aLng, bLat, bLng float64) float64 { dx := px - t*bx 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 { diff --git a/handlers.go b/handlers.go index 4123c26..e68bdd1 100644 --- a/handlers.go +++ b/handlers.go @@ -176,11 +176,11 @@ func handleRouteRepeaters(db *sql.DB) http.HandlerFunc { } if repeaters == nil { - repeaters = []Repeater{} + repeaters = []RepeaterWithDistance{} } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(apiResponse{ + json.NewEncoder(w).Encode(radiusResponse{ Repeaters: repeaters, Count: len(repeaters), }) diff --git a/static/app.js b/static/app.js index 654bb1c..3d184f0 100644 --- a/static/app.js +++ b/static/app.js @@ -52,6 +52,7 @@ var pinRadiusVal = document.getElementById("pin-radius-val"); var pinClearBtn = document.getElementById("pin-clear"); var pinListEl = document.getElementById("pin-list"); + var routeListEl = document.getElementById("route-list"); clearBtn.style.display = "none"; @@ -404,6 +405,7 @@ if (getSelectedNetworks() === "none") { markerLayer.clearLayers(); + routeListEl.innerHTML = ""; showCount(0); return; } @@ -427,6 +429,8 @@ }) .then(function (data) { displayRepeaters(data.repeaters); + renderRepeaterList(routeListEl, data.repeaters); + routeListEl.style.display = ""; console.log("Showing " + data.count + " repeaters along route"); showCount(data.count); }) @@ -568,6 +572,8 @@ isRouteMode = false; clearBtn.style.display = "none"; corridorRow.style.display = "none"; + routeListEl.style.display = "none"; + routeListEl.innerHTML = ""; fromInput.value = ""; toInput.value = ""; fetchRepeaters(); @@ -611,8 +617,8 @@ }); } - function renderPinList(repeaters) { - pinListEl.innerHTML = ""; + function renderRepeaterList(container, repeaters) { + container.innerHTML = ""; repeaters.forEach(function (r) { var item = document.createElement("div"); item.className = "pin-list-item"; @@ -632,10 +638,14 @@ } }); }); - pinListEl.appendChild(item); + container.appendChild(item); }); } + function renderPinList(repeaters) { + renderRepeaterList(pinListEl, repeaters); + } + function placePin(latlng) { // Clear route mode if active if (isRouteMode) clearRoute(); diff --git a/static/index.html b/static/index.html index 08bfe0c..a0adb49 100644 --- a/static/index.html +++ b/static/index.html @@ -48,6 +48,7 @@ 10 km +