diff --git a/umdloop_gui_web/__pycache__/ros_bridge.cpython-310.pyc b/umdloop_gui_web/__pycache__/ros_bridge.cpython-310.pyc index 38fc514..fc82c40 100644 Binary files a/umdloop_gui_web/__pycache__/ros_bridge.cpython-310.pyc and b/umdloop_gui_web/__pycache__/ros_bridge.cpython-310.pyc differ diff --git a/umdloop_gui_web/app/GUI functions/MapView.js b/umdloop_gui_web/app/GUI functions/MapView.js index cfd5642..3545726 100644 --- a/umdloop_gui_web/app/GUI functions/MapView.js +++ b/umdloop_gui_web/app/GUI functions/MapView.js @@ -1,10 +1,29 @@ "use client"; -import React, { useState, useRef, useCallback, useEffect } from "react"; -import { Map, Marker } from "react-map-gl/maplibre"; +import React, { useState, useRef, useCallback, useEffect, useMemo } from "react"; +import { Map, Marker, Source, Layer } from "react-map-gl/maplibre"; import "maplibre-gl/dist/maplibre-gl.css"; import { useLocalTiles } from "../config"; +const PLAN_LINE_LAYER = { + id: "global-plan-line", + type: "line", + paint: { + "line-color": "#3b82f6", + "line-width": 3, + "line-dasharray": [4, 2], + }, +}; + +const DRIVEN_PATH_LAYER = { + id: "driven-path-line", + type: "line", + paint: { + "line-color": "#f59e0b", + "line-width": 2, + }, +}; + export default function MapView({ selectedSubsystem, titleOverride }) { const [viewState, setViewState] = useState({ longitude: -76.9378, @@ -15,9 +34,18 @@ export default function MapView({ selectedSubsystem, titleOverride }) { const [roverPosition, setRoverPosition] = useState(null); const [rosStatus, setRosStatus] = useState("no fix"); const [followRover, setFollowRover] = useState(false); + + // Global plan: raw coords held in a ref; planVersion bumps only when plan actually changes + const globalPlanRef = useRef(null); + const [planVersion, setPlanVersion] = useState(0); + + // Driven path history — [[lon, lat], ...] + const [drivenPath, setDrivenPath] = useState([]); + const lastDrivenPoint = useRef(null); + const mapRef = useRef(); - // Poll Flask backend for latest /gps/fix data (sourced from ROS via ros_bridge.py) + // Poll Flask backend for latest /gps/fix and accumulate driven path useEffect(() => { const poll = async () => { try { @@ -27,7 +55,18 @@ export default function MapView({ selectedSubsystem, titleOverride }) { const pos = { latitude: data.latitude, longitude: data.longitude }; setRoverPosition(pos); setRosStatus("fix"); - // Keep map centered on rover when follow mode is active + + // Accumulate driven path; only append if moved more than ~0.5 m + const lon = data.longitude; + const lat = data.latitude; + const last = lastDrivenPoint.current; + const MIN_DELTA = 0.000005; // ~0.5 m in degrees + if (!last || Math.abs(lon - last[0]) > MIN_DELTA || Math.abs(lat - last[1]) > MIN_DELTA) { + const point = [lon, lat]; + lastDrivenPoint.current = point; + setDrivenPath((prev) => [...prev, point]); + } + setFollowRover((prev) => { if (prev) { setViewState((vs) => ({ ...vs, latitude: pos.latitude, longitude: pos.longitude })); @@ -47,6 +86,53 @@ export default function MapView({ selectedSubsystem, titleOverride }) { return () => clearInterval(id); }, []); + // Poll Flask backend for latest /plan (global plan from Nav2). + // Only bump planVersion (triggering re-render) when the plan actually changes. + useEffect(() => { + const fetchPlan = async () => { + try { + const res = await fetch("http://127.0.0.1:5000/navigation/plan"); + const data = await res.json(); + if (data.available && data.coordinates?.length >= 2) { + const coords = data.coordinates; + const prev = globalPlanRef.current; + const changed = + !prev || + prev.length !== coords.length || + prev[0][0] !== coords[0][0] || + prev[0][1] !== coords[0][1]; + if (changed) { + globalPlanRef.current = coords; + setPlanVersion((v) => v + 1); + } + } + } catch { + // silently ignore; plan display is optional + } + }; + + fetchPlan(); + const id = setInterval(fetchPlan, 2000); + return () => clearInterval(id); + }, []); + + // Stable GeoJSON objects — only recreated when underlying data actually changes + const planGeoJSON = useMemo( + () => + globalPlanRef.current?.length >= 2 + ? { type: "Feature", geometry: { type: "LineString", coordinates: globalPlanRef.current } } + : null, + [planVersion] + ); + + const drivenGeoJSON = useMemo( + () => + drivenPath.length >= 2 + ? { type: "Feature", geometry: { type: "LineString", coordinates: drivenPath } } + : null, + [drivenPath] + ); + // Snap to rover and enable follow mode const centerOnRover = () => { if (!roverPosition) return; @@ -80,6 +166,11 @@ export default function MapView({ selectedSubsystem, titleOverride }) { const deleteAllWaypoints = () => setWaypoints([]); + const clearDrivenPath = () => { + setDrivenPath([]); + lastDrivenPoint.current = null; + }; + const MAPTILER_KEY = "DDQqKsPBfdOZOVxgcoy5"; const tileUrl = useLocalTiles() ? "/tiles/{z}/{x}/{y}.jpg" @@ -143,8 +234,41 @@ export default function MapView({ selectedSubsystem, titleOverride }) { {followRover ? "Following Rover" : "Center on Rover"} - {/* Waypoint controls */} + {/* Legend */} +
+ {planGeoJSON && ( + + + Global Plan + + )} + {drivenPath.length >= 2 && ( + + + Driven Path + + )} +
+ + {/* Controls */}
+ {drivenPath.length >= 2 && ( + + )} Waypoints: {waypoints.length} {waypoints.length > 0 && (