Added uncertainty circle and zipping

This commit is contained in:
beans
2026-10-28 13:43:27 -05:00
parent 5f5be1b8d3
commit 499dc3bd54
10 changed files with 218 additions and 16 deletions
+22 -3
View File
@@ -10,13 +10,32 @@
</head>
<body>
<header><div><h1>Macker</h1><p>Work car · Live location</p></div><button id="follow" type="button">Follow car: on</button></header>
<main><div class="stats"><section class="card"><span class="label">Current speed</span><strong class="value" id="speed">—</strong><small id="metric">Waiting for GPS</small></section><section class="card"><span class="label">Last update</span><strong class="value" id="age">—</strong><small id="time">No location received yet</small></section></div><p id="status" role="status">Waiting for the car’s first GPS fix…</p><div id="map" aria-label="Car location map"></div></main>
<main><div class="stats"><section class="card"><span class="label">Current speed</span><strong class="value" id="speed">—</strong><small id="metric">Waiting for GPS</small></section><section class="card"><span class="label">Last update</span><strong class="value" id="age">—</strong><small id="time">No location received yet</small></section></div><p id="status" role="status">Waiting for the car’s first GPS fix…</p><p id="uncertainty"><small>GPS uncertainty unavailable</small></p><div id="map" aria-label="Car location map with GPS uncertainty circle"></div></main>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const el=id=>document.getElementById(id);
let map, marker, latest, follow=true, failed=false;
let map, marker, uncertaintyCircle, latest, follow=true, failed=false;
if(window.L){map=L.map('map').setView([20,0],2);L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(map);}
el('follow').onclick=()=>{follow=!follow;el('follow').textContent=`Follow car: ${follow?'on':'off'}`;if(follow&&marker)map.panTo(marker.getLatLng());};
// gpsd axis errors are already in metres. Enclose their horizontal error box.
// Zero also represents an omitted estimate in the client's gpsd dependency.
function uncertaintyRadius(point){
const {LatErr,LongErr}=point;
if(!Number.isFinite(LatErr)||!Number.isFinite(LongErr)||LatErr<=0||LongErr<=0)return null;
const radius=Math.hypot(LatErr,LongErr);
return Number.isFinite(radius)?radius:null;
}
function updateUncertainty(point,pos){
const radius=uncertaintyRadius(point);
el('uncertainty').textContent=radius===null?'GPS uncertainty unavailable':`Estimated GPS uncertainty: ${radius.toFixed(1)} m radius · shaded circle`;
if(radius===null){
if(uncertaintyCircle){map.removeLayer(uncertaintyCircle);uncertaintyCircle=null;}
return;
}
if(!uncertaintyCircle){
uncertaintyCircle=L.circle(pos,{radius,color:'#287a57',weight:2,fillColor:'#409b73',fillOpacity:0.16,interactive:false}).addTo(map);
}else{uncertaintyCircle.setLatLng(pos).setRadius(radius);}
}
function render(){
if(!latest){el('status').textContent=failed?'Cannot reach server. Retrying…':'Waiting for the car’s first GPS fix…';return;}
const seconds=Math.max(0,Math.floor((Date.now()-Date.parse(latest.updated_at))/1000));
@@ -28,7 +47,7 @@ function render(){
el('status').textContent=failed?'Connection lost · showing last known location':stale?'No recent GPS fix · showing last known location':`GPS active · ${p.Lat.toFixed(5)}, ${p.Long.toFixed(5)}`;
if(!map)el('status').textContent+=' · Map failed to load; reload to retry';
}
async function poll(){try{const res=await fetch('/api/location',{cache:'no-store',signal:AbortSignal.timeout(8000)});if(!res.ok)throw Error(res.status);latest=await res.json();failed=false;if(latest&&map){const pos=[latest.point.Lat,latest.point.Long];if(!marker){marker=L.marker(pos).addTo(map);map.setView(pos,15);}else{marker.setLatLng(pos);if(follow)map.panTo(pos);}}}catch(e){failed=true;}render();setTimeout(poll,2000);}
async function poll(){try{const res=await fetch('/api/location',{cache:'no-store',signal:AbortSignal.timeout(8000)});if(!res.ok)throw Error(res.status);latest=await res.json();failed=false;if(latest&&map){const pos=[latest.point.Lat,latest.point.Long];updateUncertainty(latest.point,pos);if(!marker){marker=L.marker(pos).addTo(map);map.setView(pos,15);}else{marker.setLatLng(pos);if(follow)map.panTo(pos);}}}catch(e){failed=true;}render();setTimeout(poll,2000);}
setInterval(render,1000);poll();
</script>
</body></html>
+3 -3
View File
@@ -58,12 +58,12 @@ func (t *tracker) receive(c net.Conn) {
defer c.Close()
_ = c.SetReadDeadline(time.Now().Add(10 * time.Second))
var raw []byte
if err := gob.NewDecoder(io.LimitReader(c, 4<<20)).Decode(&raw); err != nil {
if err := gob.NewDecoder(io.LimitReader(c, datapoint.MaxBatchBytes+1024)).Decode(&raw); err != nil {
log.Printf("decode batch: %v", err)
return
}
var points []datapoint.DataPoint
if err := json.Unmarshal(raw, &points); err != nil {
points, err := datapoint.UnzipBatch(raw)
if err != nil {
log.Printf("decode points: %v", err)
return
}
+1 -1
View File
@@ -19,7 +19,7 @@ func TestReceiveAndServeLocation(t *testing.T) {
speed := 12.5
now := time.Now().UTC().Truncate(time.Millisecond)
point := datapoint.DataPoint{Timestamp: uint64(now.UnixMilli()), Lat: 41.8, Long: -87.6, Speed: &speed}
raw, err := json.Marshal([]datapoint.DataPoint{point})
raw, err := datapoint.ZipBatch([]datapoint.DataPoint{point})
if err != nil {
t.Fatal(err)
}
Executable
BIN
View File
Binary file not shown.