Show list of repeaters alongside route
This commit is contained in:
parent
bdaa17ca38
commit
2b7b7e4824
4 changed files with 53 additions and 23 deletions
55
db.go
55
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.
|
// 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 (0–1) 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 {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue