mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
add tracker
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
APIProvider,
|
||||
InfoWindow,
|
||||
Map as GoogleMap,
|
||||
Marker,
|
||||
useMap,
|
||||
@@ -76,6 +77,51 @@ function FitBounds({ points }: { points: LatLng[] }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Lazily reverse-geocode a coordinate to a human address. */
|
||||
function useAddress(lat: number, lng: number): string | null {
|
||||
const [addr, setAddr] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (typeof google === "undefined") return;
|
||||
setAddr(null);
|
||||
let cancelled = false;
|
||||
new google.maps.Geocoder().geocode({ location: { lat, lng } }, (res, status) => {
|
||||
if (cancelled) return;
|
||||
setAddr(status === "OK" && res?.[0] ? res[0].formatted_address : "Unknown location");
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [lat, lng]);
|
||||
return addr;
|
||||
}
|
||||
|
||||
/** Hover popup: label, coordinates, speed/course, and the reverse-geocoded place. */
|
||||
function HoverInfo({
|
||||
device,
|
||||
lat,
|
||||
lng,
|
||||
onClose,
|
||||
}: {
|
||||
device: GpsDevice;
|
||||
lat: number;
|
||||
lng: number;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const address = useAddress(lat, lng);
|
||||
return (
|
||||
<InfoWindow position={{ lat, lng }} pixelOffset={[0, -46]} onCloseClick={onClose}>
|
||||
<div style={{ minWidth: 190, fontSize: 13 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 2 }}>{deviceLabel(device)}</div>
|
||||
<div style={{ fontFamily: "monospace" }}>
|
||||
{lat.toFixed(5)}, {lng.toFixed(5)}
|
||||
</div>
|
||||
<div style={{ color: "#555" }}>
|
||||
{toNum(device.lastSpeed) ?? 0} km/h · {device.lastCourse ?? 0}°
|
||||
</div>
|
||||
<div style={{ color: "#777", marginTop: 4 }}>{address ?? "Locating…"}</div>
|
||||
</div>
|
||||
</InfoWindow>
|
||||
);
|
||||
}
|
||||
|
||||
/** Draw the selected vehicle's recent path as a polyline. */
|
||||
function RouteTrail({ path }: { path: LatLng[] }) {
|
||||
const map = useMap();
|
||||
@@ -97,6 +143,7 @@ export function TrackingPage() {
|
||||
const { toast } = useToast();
|
||||
const qc = useQueryClient();
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [hoverId, setHoverId] = useState<string | null>(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editDevice, setEditDevice] = useState<GpsDevice | null>(null);
|
||||
const [form, setForm] = useState({ imei: "", name: "", vehicleId: "" });
|
||||
@@ -154,17 +201,25 @@ export function TrackingPage() {
|
||||
[history],
|
||||
);
|
||||
|
||||
const markerIcon = (d: GpsDevice, selectedFlag: boolean) => {
|
||||
// Teardrop pin colored by state with a white truck glyph inside.
|
||||
const markerIcon = (d: GpsDevice, selectedFlag: boolean): google.maps.Icon | undefined => {
|
||||
if (typeof google === "undefined") return undefined;
|
||||
const color = selectedFlag ? freightBrand.primary : d.online ? "#2f80ed" : "#95a5a6";
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="48" viewBox="0 0 40 48">
|
||||
<path d="M20 2C10 2 2 10 2 20c0 12 18 26 18 26s18-14 18-26C38 10 30 2 20 2Z" fill="${color}" stroke="#ffffff" stroke-width="1.5"/>
|
||||
<g transform="translate(8,7)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 18V6a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h1"/>
|
||||
<path d="M15 18H9"/>
|
||||
<path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/>
|
||||
<circle cx="7" cy="18" r="2"/>
|
||||
<circle cx="17" cy="18" r="2"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
return {
|
||||
path: "M 0,-9 L 6,9 L 0,4 L -6,9 Z", // arrow
|
||||
rotation: d.lastCourse ?? 0,
|
||||
fillColor: selectedFlag ? freightBrand.primary : d.online ? "#2f80ed" : "#95a5a6",
|
||||
fillOpacity: 1,
|
||||
strokeColor: "#ffffff",
|
||||
strokeWeight: 1.5,
|
||||
scale: 1.4,
|
||||
} as google.maps.Symbol;
|
||||
url: `data:image/svg+xml,${encodeURIComponent(svg)}`,
|
||||
scaledSize: new google.maps.Size(40, 48),
|
||||
anchor: new google.maps.Point(20, 48),
|
||||
};
|
||||
};
|
||||
|
||||
const saveMutation = useMutation({
|
||||
@@ -253,11 +308,18 @@ export function TrackingPage() {
|
||||
<Marker
|
||||
key={d.id}
|
||||
position={{ lat, lng }}
|
||||
title={deviceLabel(d)}
|
||||
title={`${deviceLabel(d)}\n${lat.toFixed(5)}, ${lng.toFixed(5)}`}
|
||||
icon={markerIcon(d, d.id === selectedId)}
|
||||
onClick={() => setSelectedId(d.id)}
|
||||
onMouseOver={() => setHoverId(d.id)}
|
||||
/>
|
||||
))}
|
||||
{(() => {
|
||||
const h = positioned.find((p) => p.d.id === hoverId);
|
||||
return h ? (
|
||||
<HoverInfo device={h.d} lat={h.lat} lng={h.lng} onClose={() => setHoverId(null)} />
|
||||
) : null;
|
||||
})()}
|
||||
<FitBounds points={positioned.map((p) => ({ lat: p.lat, lng: p.lng }))} />
|
||||
{selected?.vehicleId && trail.length > 1 && <RouteTrail path={trail} />}
|
||||
</GoogleMap>
|
||||
|
||||
Reference in New Issue
Block a user