saltylab-firmware/ui/map_panel.html
sl-webui 5dac6337e6 feat: WebUI map view (Issue #587)
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>
2026-03-14 12:20:04 -04:00

177 lines
5.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>