Added uncertainty circle and zipping
This commit is contained in:
+22
-3
@@ -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:'© <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
@@ -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
@@ -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
Binary file not shown.
Reference in New Issue
Block a user