feat(gps-map): add phone/user GPS as second marker on robot GPS map
Subscribes to saltybot/phone/gps (JSON: {ts, lat, lon, alt_m,
accuracy_m, speed_ms, bearing_deg, provider}) and renders a blue
Leaflet marker + blue breadcrumb trail alongside the robot's
orange/cyan marker. Status bar now shows PHONE badge with stale
detection. Sidebar adds phone lat/lon/speed/accuracy/provider section.
Clear button resets both trails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
12d457e743
commit
ef76830f83
@ -24,11 +24,14 @@
|
||||
|
||||
<!-- ── Status bar ── -->
|
||||
<div id="status-bar">
|
||||
<span style="color:var(--text-dim);font-size:10px">GPS FIX</span>
|
||||
<span style="color:var(--text-dim);font-size:10px">ROBOT FIX</span>
|
||||
<span class="sys-badge badge-stale" id="badge-fix">STALE</span>
|
||||
<span style="color:var(--text-dim)">│</span>
|
||||
<span style="color:var(--text-dim);font-size:10px">VELOCITY</span>
|
||||
<span class="sys-badge badge-stale" id="badge-vel">STALE</span>
|
||||
<span style="color:var(--text-dim)">│</span>
|
||||
<span style="color:var(--text-dim);font-size:10px">PHONE</span>
|
||||
<span class="sys-badge badge-stale" id="badge-phone">NO DATA</span>
|
||||
<span id="last-update">Awaiting data…</span>
|
||||
</div>
|
||||
|
||||
@ -94,6 +97,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone position -->
|
||||
<div class="stat-section">
|
||||
<div class="section-title" style="color:#3b82f6">📱 Phone / User</div>
|
||||
<div style="margin-bottom:6px">
|
||||
<div class="coord-row">LAT <span class="coord-val" id="phone-lat" style="color:#60a5fa">—</span></div>
|
||||
<div class="coord-row">LON <span class="coord-val" id="phone-lon" style="color:#60a5fa">—</span></div>
|
||||
</div>
|
||||
<div class="kv-grid">
|
||||
<div class="kv-cell">
|
||||
<div class="kv-lbl">SPEED</div>
|
||||
<div class="kv-val" id="phone-spd" style="color:#60a5fa">—</div>
|
||||
</div>
|
||||
<div class="kv-cell">
|
||||
<div class="kv-lbl">ACCURACY</div>
|
||||
<div class="kv-val" id="phone-acc">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coord-row" style="margin-top:6px">SRC <span id="phone-provider" style="color:#6b7280;font-family:monospace">—</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Trail log -->
|
||||
<div class="section-title" style="padding:8px 12px 2px">Trail log</div>
|
||||
<div id="trail-log"></div>
|
||||
@ -105,7 +128,8 @@
|
||||
<div id="bottombar">
|
||||
<span class="bb-lbl">FIXES</span><span id="bb-fixes">0</span>
|
||||
<span class="bb-lbl">TRAIL PTS</span><span id="bb-trail">0</span>
|
||||
<span class="bb-lbl">MSGS</span><span id="bb-msgs">0</span>
|
||||
<span class="bb-lbl">ROBOT MSGS</span><span id="bb-msgs">0</span>
|
||||
<span class="bb-lbl">PHONE MSGS</span><span id="bb-phone">0</span>
|
||||
<span id="last-msg">No data</span>
|
||||
</div>
|
||||
|
||||
|
||||
@ -2,13 +2,15 @@
|
||||
* gps_map_panel.js — Robot GPS Live Map (Issue #709 companion)
|
||||
*
|
||||
* Subscribes via rosbridge to:
|
||||
* saltybot/gps/fix std_msgs/String JSON — {lat, lon, alt, stat, t}
|
||||
* saltybot/gps/vel std_msgs/String JSON — {spd, hdg, t}
|
||||
* saltybot/gps/fix std_msgs/String JSON — {lat, lon, alt, stat, t}
|
||||
* saltybot/gps/vel std_msgs/String JSON — {spd, hdg, t}
|
||||
* saltybot/phone/gps std_msgs/String JSON — {ts, lat, lon, alt_m, accuracy_m, speed_ms, bearing_deg, provider}
|
||||
*
|
||||
* Renders a Leaflet map with:
|
||||
* - Live robot position marker with heading arrow
|
||||
* - Orange breadcrumb trail polyline (up to MAX_TRAIL points)
|
||||
* - Sidebar: speed, altitude, heading compass, fix status, coordinates
|
||||
* - Live robot position marker (cyan/orange) with heading arrow
|
||||
* - Live phone/user position marker (blue) with bearing arrow
|
||||
* - Orange robot trail + blue phone trail (up to MAX_TRAIL points each)
|
||||
* - Sidebar: speed, altitude, heading compass, fix status, coordinates, phone position
|
||||
* - FOLLOW mode (auto-pan) toggled by button or manual drag
|
||||
* - Stale detection at 5 s
|
||||
*/
|
||||
@ -35,7 +37,15 @@ const gps = {
|
||||
tsfix: 0, tsvel: 0,
|
||||
};
|
||||
|
||||
const phone = {
|
||||
lat: null, lon: null, alt: null,
|
||||
spd: null, bearing: null,
|
||||
accuracy: null, provider: null,
|
||||
msgs: 0, ts: 0,
|
||||
};
|
||||
|
||||
let trail = []; // [{lat, lon}]
|
||||
let phoneTrail = []; // [{lat, lon}]
|
||||
let logEntries = [];
|
||||
let followMode = true;
|
||||
|
||||
@ -74,11 +84,35 @@ function makeIcon(hdg) {
|
||||
});
|
||||
}
|
||||
|
||||
// Robot marker (cyan body, orange heading arrow)
|
||||
let marker = null;
|
||||
let polyline = L.polyline([], {
|
||||
color: '#f97316', weight: 2.5, opacity: .8, lineJoin: 'round',
|
||||
}).addTo(map);
|
||||
|
||||
// Phone/user marker (blue body, blue bearing arrow)
|
||||
function makePhoneIcon(bearing) {
|
||||
const rot = (bearing != null && bearing >= 0) ? bearing : 0;
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<svg width="24" height="32" viewBox="0 0 24 32" xmlns="http://www.w3.org/2000/svg"
|
||||
style="transform:rotate(${rot}deg);transform-origin:12px 17px">
|
||||
<!-- body circle -->
|
||||
<circle cx="12" cy="17" r="8" fill="#3b82f6" fill-opacity=".35" stroke="#3b82f6" stroke-width="2"/>
|
||||
<circle cx="12" cy="17" r="3.5" fill="#3b82f6"/>
|
||||
<!-- bearing arrow (points up = north when bearing=0) -->
|
||||
<polygon points="12,2 9,12 15,12" fill="#60a5fa" opacity=".9"/>
|
||||
</svg>`,
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 17],
|
||||
});
|
||||
}
|
||||
|
||||
let phoneMarker = null;
|
||||
let phonePolyline = L.polyline([], {
|
||||
color: '#3b82f6', weight: 2, opacity: .7, lineJoin: 'round',
|
||||
}).addTo(map);
|
||||
|
||||
// ── Canvas compass ────────────────────────────────────────────────────────────
|
||||
|
||||
function drawCompass(hdg) {
|
||||
@ -135,10 +169,13 @@ function render() {
|
||||
const now = Date.now();
|
||||
|
||||
// Stale badges
|
||||
const fixStale = now - gps.tsfix > STALE_MS;
|
||||
const velStale = now - gps.tsvel > STALE_MS;
|
||||
setSysBadge('badge-fix', fixStale ? 'stale' : 'ok', fixStale ? 'STALE' : 'LIVE');
|
||||
setSysBadge('badge-vel', velStale ? 'stale' : 'ok', velStale ? 'STALE' : 'LIVE');
|
||||
const fixStale = now - gps.tsfix > STALE_MS;
|
||||
const velStale = now - gps.tsvel > STALE_MS;
|
||||
const phoneStale = now - phone.ts > STALE_MS;
|
||||
setSysBadge('badge-fix', fixStale ? 'stale' : 'ok', fixStale ? 'STALE' : 'LIVE');
|
||||
setSysBadge('badge-vel', velStale ? 'stale' : 'ok', velStale ? 'STALE' : 'LIVE');
|
||||
setSysBadge('badge-phone', phone.ts === 0 ? 'stale' : phoneStale ? 'stale' : 'ok',
|
||||
phone.ts === 0 ? 'NO DATA' : phoneStale ? 'STALE' : 'LIVE');
|
||||
|
||||
// Speed
|
||||
const spdKmh = gps.spd != null ? gps.spd : null;
|
||||
@ -171,6 +208,15 @@ function render() {
|
||||
// Coordinates
|
||||
$('val-lat').textContent = gps.lat != null ? gps.lat.toFixed(6) : '—';
|
||||
$('val-lon').textContent = gps.lon != null ? gps.lon.toFixed(6) : '—';
|
||||
|
||||
// Phone position
|
||||
$('phone-lat').textContent = phone.lat != null ? phone.lat.toFixed(6) : '—';
|
||||
$('phone-lon').textContent = phone.lon != null ? phone.lon.toFixed(6) : '—';
|
||||
const phoneSpdMs = phone.spd;
|
||||
$('phone-spd').textContent = phoneSpdMs != null ? (phoneSpdMs * 3.6).toFixed(1) + ' km/h' : '—';
|
||||
$('phone-acc').textContent = phone.accuracy != null ? phone.accuracy.toFixed(1) + ' m' : '—';
|
||||
$('phone-provider').textContent = phone.provider || '—';
|
||||
$('bb-phone').textContent = phone.msgs;
|
||||
}
|
||||
|
||||
function updateMap(lat, lon, hdg) {
|
||||
@ -190,6 +236,21 @@ function updateMap(lat, lon, hdg) {
|
||||
polyline.setLatLngs(trail.map(p => [p.lat, p.lon]));
|
||||
}
|
||||
|
||||
function updatePhoneMap(lat, lon, bearing) {
|
||||
const icon = makePhoneIcon(bearing);
|
||||
|
||||
if (!phoneMarker) {
|
||||
phoneMarker = L.marker([lat, lon], { icon }).addTo(map);
|
||||
} else {
|
||||
phoneMarker.setIcon(icon);
|
||||
phoneMarker.setLatLng([lat, lon]);
|
||||
}
|
||||
|
||||
phoneTrail.push({ lat, lon });
|
||||
if (phoneTrail.length > MAX_TRAIL) phoneTrail.shift();
|
||||
phonePolyline.setLatLngs(phoneTrail.map(p => [p.lat, p.lon]));
|
||||
}
|
||||
|
||||
function addLog(lat, lon, spd, t) {
|
||||
const time = new Date((t || Date.now() / 1000) * 1000)
|
||||
.toLocaleTimeString('en-US', { hour12: false });
|
||||
@ -297,6 +358,30 @@ function setupTopics() {
|
||||
render();
|
||||
} catch(_) {}
|
||||
});
|
||||
|
||||
// saltybot/phone/gps — {ts, lat, lon, alt_m, accuracy_m, speed_ms, bearing_deg, provider}
|
||||
new ROSLIB.Topic({
|
||||
ros, name: 'saltybot/phone/gps',
|
||||
messageType: 'std_msgs/String', throttle_rate: 500,
|
||||
}).subscribe(msg => {
|
||||
try {
|
||||
const d = JSON.parse(msg.data);
|
||||
phone.lat = d.lat ?? phone.lat;
|
||||
phone.lon = d.lon ?? phone.lon;
|
||||
phone.alt = d.alt_m ?? phone.alt;
|
||||
phone.accuracy = d.accuracy_m ?? phone.accuracy;
|
||||
phone.spd = d.speed_ms ?? phone.spd;
|
||||
phone.bearing = d.bearing_deg ?? phone.bearing;
|
||||
phone.provider = d.provider ?? phone.provider;
|
||||
phone.msgs++;
|
||||
phone.ts = Date.now();
|
||||
|
||||
if (phone.lat != null && phone.lon != null) {
|
||||
updatePhoneMap(phone.lat, phone.lon, phone.bearing);
|
||||
}
|
||||
render();
|
||||
} catch(_) {}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Stale checker (1 Hz) ──────────────────────────────────────────────────────
|
||||
@ -318,9 +403,11 @@ $('btn-follow').addEventListener('click', () => {
|
||||
});
|
||||
|
||||
$('btn-clear').addEventListener('click', () => {
|
||||
trail = []; logEntries = []; gps.fixes = 0;
|
||||
if (marker) { marker.remove(); marker = null; }
|
||||
trail = []; phoneTrail = []; logEntries = []; gps.fixes = 0;
|
||||
if (marker) { marker.remove(); marker = null; }
|
||||
if (phoneMarker) { phoneMarker.remove(); phoneMarker = null; }
|
||||
polyline.setLatLngs([]);
|
||||
phonePolyline.setLatLngs([]);
|
||||
$('trail-log').innerHTML = '';
|
||||
render();
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user