Standalone 3-file 2D map panel (ui/map_panel.{html,js,css}).
No build step. Open directly or serve ui/ directory.
Canvas layers (drawn every animation frame):
- Grid lines (1m spacing) + world-origin axis cross + 1m scale bar
- RPLIDAR scan dots (/scan, green, cbor-compressed, 100ms throttle)
- Safety zone rings: danger 0.30m (red dashed) + warn 1.00m (amber dashed)
- 100-position breadcrumb trail with fading cyan polyline + dots every 5 pts
- UWB anchor markers (amber diamond + label, user-configured)
- Robot marker: circle + forward arrow, red when e-stopped
Interactions:
- Mouse wheel zoom (zooms around cursor)
- Click+drag pan
- Pinch-to-zoom (touch, two-finger)
- Auto-center toggle (robot stays centered when on)
- Zoom +/- buttons, Reset view button
- Clear trail button
- Mouse hover shows world coords (m) in bottom-left HUD
ROS topics:
SUB /saltybot/pose/fused geometry_msgs/PoseStamped 50ms throttle
SUB /scan sensor_msgs/LaserScan 100ms + cbor
SUB /saltybot/safety_zone/status std_msgs/String (JSON) 200ms throttle
Sidebar:
- Robot position (x, y m) + heading (°)
- Safety zone: forward zone (CLEAR/WARN/DANGER), closest obstacle (m), e-stop
- UWB anchor manager: add/remove anchors with x/y/label, persisted localStorage
- Topic reference
E-stop banner: pulsing red overlay when /saltybot/safety_zone/status estop_active=true
Mobile-responsive: sidebar hidden on <700px, canvas fills viewport.
WS URL persisted in localStorage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
5.6 KiB
HTML
177 lines
5.6 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||
<title>Saltybot — Map View</title>
|
||
<link rel="stylesheet" href="map_panel.css">
|
||
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ── Header ── -->
|
||
<div id="header">
|
||
<div class="logo">⚡ SALTYBOT — MAP</div>
|
||
|
||
<div id="conn-dot"></div>
|
||
<input id="ws-input" type="text" value="ws://localhost:9090" placeholder="ws://robot-ip:9090" />
|
||
<button id="btn-connect" class="hbtn">CONNECT</button>
|
||
<span id="conn-label" style="color:#4b5563;font-size:10px">Not connected</span>
|
||
</div>
|
||
|
||
<!-- ── Toolbar ── -->
|
||
<div id="toolbar">
|
||
<button id="btn-zoom-in" class="hbtn">+</button>
|
||
<button id="btn-zoom-out" class="hbtn">-</button>
|
||
<span id="zoom-display">1.00x</span>
|
||
|
||
<div class="tsep"></div>
|
||
|
||
<button id="btn-center" class="hbtn on">⊙ AUTO-CENTER</button>
|
||
<button id="btn-reset" class="hbtn">RESET VIEW</button>
|
||
|
||
<div class="tsep"></div>
|
||
|
||
<button id="btn-clear-trail" class="hbtn">CLEAR TRAIL</button>
|
||
|
||
<div class="tsep"></div>
|
||
|
||
<!-- Legend -->
|
||
<div style="display:flex;flex-direction:column;gap:2px">
|
||
<div class="legend-row">
|
||
<div class="legend-swatch" style="background:#22c55e"></div>
|
||
<span>LIDAR scan</span>
|
||
</div>
|
||
<div class="legend-row">
|
||
<div class="legend-swatch" style="background:rgba(239,68,68,.6)"></div>
|
||
<span>Danger zone (0.30m)</span>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:2px">
|
||
<div class="legend-row">
|
||
<div class="legend-swatch" style="background:rgba(245,158,11,.5)"></div>
|
||
<span>Warn zone (1.00m)</span>
|
||
</div>
|
||
<div class="legend-row">
|
||
<div class="legend-swatch" style="background:#06b6d4"></div>
|
||
<span>Trail (100 pts)</span>
|
||
</div>
|
||
</div>
|
||
<div class="legend-row">
|
||
<div class="legend-swatch" style="background:#f59e0b;height:8px;width:8px;border-radius:0;transform:rotate(45deg);flex-shrink:0"></div>
|
||
<span>UWB anchor</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Main ── -->
|
||
<div id="main">
|
||
|
||
<!-- Map canvas -->
|
||
<div id="map-wrap">
|
||
<canvas id="map-canvas"></canvas>
|
||
|
||
<!-- No signal -->
|
||
<div id="no-signal">
|
||
<div class="icon">🗺️</div>
|
||
<div>Connect to rosbridge to view map</div>
|
||
<div style="font-size:10px;color:#374151">
|
||
/saltybot/pose/fused · /scan · /saltybot/safety_zone/status
|
||
</div>
|
||
</div>
|
||
|
||
<!-- E-stop banner -->
|
||
<div id="estop-overlay">🛑 E-STOP ACTIVE</div>
|
||
|
||
<!-- Mouse coords -->
|
||
<div id="coords-hud">(0.00, 0.00) m</div>
|
||
</div>
|
||
|
||
<!-- Sidebar -->
|
||
<aside id="sidebar">
|
||
|
||
<!-- Robot status -->
|
||
<div class="sb-card">
|
||
<div class="sb-title">Robot Position</div>
|
||
<div class="sb-row">
|
||
<span class="sb-lbl">Position</span>
|
||
<span class="sb-val" id="sb-pos">—</span>
|
||
</div>
|
||
<div class="sb-row">
|
||
<span class="sb-lbl">Heading</span>
|
||
<span class="sb-val" id="sb-hdg">—</span>
|
||
</div>
|
||
<div class="sb-row">
|
||
<span class="sb-lbl">Trail</span>
|
||
<span class="sb-val" id="sb-trail">0 pts</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Safety status -->
|
||
<div class="sb-card">
|
||
<div class="sb-title">Safety Zone</div>
|
||
<div class="sb-row">
|
||
<span class="sb-lbl"><span class="sdot gray" id="sb-zone-dot"></span>Fwd zone</span>
|
||
<span class="sb-val" id="sb-fwd">—</span>
|
||
</div>
|
||
<div class="sb-row">
|
||
<span class="sb-lbl">Closest</span>
|
||
<span class="sb-val" id="sb-closest">—</span>
|
||
</div>
|
||
<div class="sb-row">
|
||
<span class="sb-lbl">E-stop</span>
|
||
<span class="sb-val" id="sb-estop" style="color:#6b7280">—</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- UWB anchors -->
|
||
<div class="sb-card">
|
||
<div class="sb-title">UWB Anchors</div>
|
||
<div id="anchor-list"></div>
|
||
|
||
<!-- Add anchor form -->
|
||
<div style="margin-top:8px;font-size:9px;color:#6b7280;margin-bottom:4px">
|
||
ADD ANCHOR
|
||
</div>
|
||
<div class="anchor-inputs">
|
||
<input id="anc-x" type="number" step="0.1" placeholder="X (m)" />
|
||
<input id="anc-y" type="number" step="0.1" placeholder="Y (m)" />
|
||
<input id="anc-lbl" type="text" placeholder="Label" />
|
||
</div>
|
||
<button id="btn-add-anchor" class="hbtn" id="anchor-add"
|
||
style="width:100%;margin-top:6px;text-align:center">
|
||
+ ADD ANCHOR
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Legend / topics -->
|
||
<div class="sb-card">
|
||
<div class="sb-title">Topics</div>
|
||
<div style="font-size:9px;color:#374151;line-height:1.9">
|
||
<div>SUB <code style="color:#4b5563">/saltybot/pose/fused</code></div>
|
||
<div style="color:#1e3a5f;padding-left:8px">geometry_msgs/PoseStamped</div>
|
||
<div>SUB <code style="color:#4b5563">/scan</code></div>
|
||
<div style="color:#1e3a5f;padding-left:8px">sensor_msgs/LaserScan</div>
|
||
<div>SUB <code style="color:#4b5563">/saltybot/safety_zone/status</code></div>
|
||
<div style="color:#1e3a5f;padding-left:8px">std_msgs/String (JSON)</div>
|
||
</div>
|
||
</div>
|
||
|
||
</aside>
|
||
</div>
|
||
|
||
<!-- ── Footer ── -->
|
||
<div id="footer">
|
||
<span>wheel=zoom · drag=pan · pinch=zoom (touch)</span>
|
||
<span>rosbridge: <code id="footer-ws">ws://localhost:9090</code></span>
|
||
<span>map view — issue #587</span>
|
||
</div>
|
||
|
||
<script src="map_panel.js"></script>
|
||
<script>
|
||
document.getElementById('ws-input').addEventListener('input', (e) => {
|
||
document.getElementById('footer-ws').textContent = e.target.value;
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|