mirror of
https://github.com/dpup/meshstream.git
synced 2026-03-28 17:42:37 +01:00
Fix zooming functionality
This commit is contained in:
@@ -34,7 +34,7 @@ export const GatewayList: React.FC = () => {
|
||||
{gatewayArray.length === 1 ? "gateway" : "gateways"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 gap-2">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 gap-2">
|
||||
{gatewayArray.length === 0 ? (
|
||||
<div className="bg-neutral-800/50 hover:bg-neutral-800 p-2 rounded-lg flex items-center">
|
||||
<div className="p-1.5 rounded-full bg-neutral-700/30 text-neutral-500 mr-2">
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import React, { useRef, useEffect, useState, useCallback } from "react";
|
||||
import { useAppSelector } from "../../hooks";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { NodeData, GatewayData } from "../../store/slices/aggregatorSlice";
|
||||
import { Position } from "../../lib/types";
|
||||
import { Button } from "../ui/Button";
|
||||
import { Locate } from "lucide-react";
|
||||
|
||||
interface NetworkMapProps {
|
||||
/** Height of the map in CSS units */
|
||||
height?: string;
|
||||
/** Callback for when auto-zoom state changes */
|
||||
onAutoZoomChange?: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* NetworkMap displays all nodes with position data on a Google Map
|
||||
*/
|
||||
export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
||||
export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, NetworkMapProps>(
|
||||
({ height = "600px", onAutoZoomChange }, ref) => {
|
||||
const navigate = useNavigate();
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const mapInstanceRef = useRef<google.maps.Map | null>(null);
|
||||
@@ -22,6 +27,9 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
||||
const boundsRef = useRef<google.maps.LatLngBounds>(new google.maps.LatLngBounds());
|
||||
const [nodesWithPosition, setNodesWithPosition] = useState<MapNode[]>([]);
|
||||
const animatingNodesRef = useRef<Record<string, number>>({});
|
||||
const [autoZoomEnabled, setAutoZoomEnabled] = useState(true);
|
||||
// Using any for the event listener since TypeScript can't find the MapsEventListener interface
|
||||
const zoomListenerRef = useRef<any>(null);
|
||||
|
||||
// Get nodes data from the store
|
||||
const { nodes, gateways } = useAppSelector((state) => state.aggregator);
|
||||
@@ -30,6 +38,86 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
||||
const latestPacket = useAppSelector((state) =>
|
||||
state.packets.packets.length > 0 ? state.packets.packets[0] : null
|
||||
);
|
||||
|
||||
// Expose the resetAutoZoom function via ref
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
resetAutoZoom: () => {
|
||||
resetAutoZoom();
|
||||
}
|
||||
}));
|
||||
|
||||
// Reset auto-zoom behavior
|
||||
const resetAutoZoom = useCallback(() => {
|
||||
setAutoZoomEnabled(true);
|
||||
|
||||
// Notify parent component of auto-zoom state change
|
||||
if (onAutoZoomChange) {
|
||||
onAutoZoomChange(true);
|
||||
}
|
||||
|
||||
if (mapInstanceRef.current && nodesWithPosition.length > 0) {
|
||||
fitMapToBounds();
|
||||
}
|
||||
}, [nodesWithPosition, onAutoZoomChange]);
|
||||
|
||||
// Function to fit map to bounds
|
||||
const fitMapToBounds = useCallback(() => {
|
||||
if (!mapInstanceRef.current) return;
|
||||
|
||||
// Clear the bounds for recalculation
|
||||
boundsRef.current = new google.maps.LatLngBounds();
|
||||
|
||||
// Extend bounds for each node
|
||||
nodesWithPosition.forEach(node => {
|
||||
const lat = node.position.latitudeI / 10000000;
|
||||
const lng = node.position.longitudeI / 10000000;
|
||||
boundsRef.current.extend({ lat, lng });
|
||||
});
|
||||
|
||||
// Fit the bounds to see all nodes
|
||||
mapInstanceRef.current.fitBounds(boundsRef.current);
|
||||
|
||||
// If we only have one node, ensure we're not too zoomed in
|
||||
if (nodesWithPosition.length === 1) {
|
||||
setTimeout(() => {
|
||||
if (mapInstanceRef.current) {
|
||||
const currentZoom = mapInstanceRef.current.getZoom() || 15;
|
||||
mapInstanceRef.current.setZoom(Math.min(currentZoom, 15));
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [nodesWithPosition]);
|
||||
|
||||
// Setup zoom change listener
|
||||
const setupZoomListener = useCallback(() => {
|
||||
if (!mapInstanceRef.current || !window.google || !window.google.maps) return;
|
||||
|
||||
// Remove previous listener if it exists
|
||||
if (zoomListenerRef.current) {
|
||||
// Use google.maps.event.removeListener for better compatibility
|
||||
window.google.maps.event.removeListener(zoomListenerRef.current);
|
||||
zoomListenerRef.current = null;
|
||||
}
|
||||
|
||||
// Console log to debug
|
||||
console.log("Setting up zoom change listener");
|
||||
|
||||
// Add new listener - using google.maps.event.addListener directly
|
||||
zoomListenerRef.current = window.google.maps.event.addListener(
|
||||
mapInstanceRef.current,
|
||||
'zoom_changed',
|
||||
() => {
|
||||
console.log("Zoom changed detected");
|
||||
// Disable auto-zoom when user manually zooms
|
||||
setAutoZoomEnabled(false);
|
||||
|
||||
// Notify parent component of auto-zoom state change
|
||||
if (onAutoZoomChange) {
|
||||
onAutoZoomChange(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [onAutoZoomChange]);
|
||||
|
||||
// Effect to build the list of nodes with position data
|
||||
useEffect(() => {
|
||||
@@ -54,7 +142,21 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
||||
// Update markers and fit the map
|
||||
updateNodeMarkers(nodesWithPosition, navigate);
|
||||
|
||||
}, [nodesWithPosition, navigate]);
|
||||
}, [nodesWithPosition, navigate, setupZoomListener]);
|
||||
|
||||
// Setup zoom listener when map is initialized
|
||||
useEffect(() => {
|
||||
if (mapInstanceRef.current && window.google && window.google.maps) {
|
||||
setupZoomListener();
|
||||
}
|
||||
}, [setupZoomListener, mapInstanceRef.current]);
|
||||
|
||||
// Update parent component when auto-zoom state changes
|
||||
useEffect(() => {
|
||||
if (onAutoZoomChange) {
|
||||
onAutoZoomChange(autoZoomEnabled);
|
||||
}
|
||||
}, [autoZoomEnabled, onAutoZoomChange]);
|
||||
|
||||
// Effect to detect when a node receives a packet and trigger animation
|
||||
useEffect(() => {
|
||||
@@ -95,6 +197,12 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clean up zoom listener
|
||||
if (zoomListenerRef.current && window.google && window.google.maps) {
|
||||
window.google.maps.event.removeListener(zoomListenerRef.current);
|
||||
zoomListenerRef.current = null;
|
||||
}
|
||||
|
||||
// Clean up markers
|
||||
Object.values(markersRef.current).forEach(marker => marker.setMap(null));
|
||||
|
||||
@@ -208,20 +316,9 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// If we have nodes, fit the map to show all of them
|
||||
if (nodes.length > 0) {
|
||||
// Fit the bounds to see all nodes
|
||||
mapInstanceRef.current?.fitBounds(boundsRef.current);
|
||||
|
||||
// If we only have one node, ensure we're not too zoomed in
|
||||
if (nodes.length === 1 && mapInstanceRef.current) {
|
||||
setTimeout(() => {
|
||||
if (mapInstanceRef.current) {
|
||||
const currentZoom = mapInstanceRef.current.getZoom() || 15;
|
||||
mapInstanceRef.current.setZoom(Math.min(currentZoom, 15));
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
// If auto-zoom is enabled and we have nodes, fit the map to show all of them
|
||||
if (autoZoomEnabled && nodes.length > 0) {
|
||||
fitMapToBounds();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,13 +375,13 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
||||
${nodeName}
|
||||
</h3>
|
||||
<div style="font-size: 12px; color: #555; margin-bottom: 8px; font-weight: 500;">
|
||||
${node.isGateway ? 'Gateway' : 'Node'} · ID: ${node.id.toString(16)}
|
||||
${node.isGateway ? 'Gateway' : 'Node'} · !${node.id.toString(16)}
|
||||
</div>
|
||||
<div style="font-size: 12px; margin-bottom: 4px; color: #333;">
|
||||
Last seen: ${lastSeenText}
|
||||
</div>
|
||||
<div style="font-size: 12px; margin-bottom: 8px; color: #333;">
|
||||
Messages: ${node.messageCount || 0} · Text: ${node.textMessageCount || 0}
|
||||
Packets: ${node.messageCount || 0} · Text: ${node.textMessageCount || 0}
|
||||
</div>
|
||||
<a href="javascript:void(0);"
|
||||
id="view-node-${node.id}"
|
||||
@@ -309,13 +406,17 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={mapRef}
|
||||
className="w-full overflow-hidden effect-inset rounded-lg"
|
||||
style={{ height }}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<div
|
||||
ref={mapRef}
|
||||
className="w-full overflow-hidden effect-inset rounded-lg relative"
|
||||
style={{ height }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
NetworkMap.displayName = "NetworkMap";
|
||||
|
||||
// Define interface for nodes with position data for map display
|
||||
interface MapNode {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const NodeList: React.FC = () => {
|
||||
{nodeArray.length} {nodeArray.length === 1 ? "node" : "nodes"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 gap-2">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 gap-2">
|
||||
{nodeArray.length === 0 ? (
|
||||
<div className="bg-neutral-800/50 hover:bg-neutral-800 p-2 rounded-lg flex items-center">
|
||||
<div className="p-1.5 rounded-full bg-neutral-700/30 text-neutral-500 mr-2">
|
||||
|
||||
@@ -57,21 +57,21 @@ export const PositionPacket: React.FC<PositionPacketProps> = ({ packet }) => {
|
||||
monospace
|
||||
/>
|
||||
)}
|
||||
{position.time && (
|
||||
{!!position.time && (
|
||||
<KeyValuePair
|
||||
label="Time"
|
||||
value={formattedTime}
|
||||
vertical
|
||||
/>
|
||||
)}
|
||||
{position.locationSource && (
|
||||
{!!position.locationSource && (
|
||||
<KeyValuePair
|
||||
label="Source"
|
||||
value={position.locationSource.replace('LOC_', '')}
|
||||
vertical
|
||||
/>
|
||||
)}
|
||||
{position.satsInView && (
|
||||
{!!position.satsInView && (
|
||||
<KeyValuePair
|
||||
label="Satellites"
|
||||
value={position.satsInView}
|
||||
|
||||
@@ -2,27 +2,58 @@ import React from "react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { PageWrapper } from "../components";
|
||||
import { NetworkMap } from "../components/dashboard";
|
||||
import { Button } from "../components/ui";
|
||||
import { Locate } from "lucide-react";
|
||||
|
||||
export const Route = createFileRoute("/map")({
|
||||
component: MapPage,
|
||||
});
|
||||
|
||||
function MapPage() {
|
||||
// State to track if auto-zoom is enabled (forwarded from the NetworkMap component)
|
||||
const [autoZoomEnabled, setAutoZoomEnabled] = React.useState(true);
|
||||
const mapRef = React.useRef<{ resetAutoZoom?: () => void }>({});
|
||||
|
||||
// Function to reset auto-zoom, will be called by the button
|
||||
const handleResetZoom = () => {
|
||||
if (mapRef.current.resetAutoZoom) {
|
||||
mapRef.current.resetAutoZoom();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<div className="max-w-6xl">
|
||||
<div>
|
||||
<NetworkMap height="600px" />
|
||||
<NetworkMap
|
||||
height="600px"
|
||||
ref={mapRef as any}
|
||||
onAutoZoomChange={setAutoZoomEnabled}
|
||||
/>
|
||||
|
||||
<div className="mt-2 bg-neutral-800/50 rounded-lg p-2 text-xs text-neutral-400 effect-inset">
|
||||
<span className="inline-flex items-center mx-2 px-2 py-0.5 rounded text-green-500 bg-green-900/30">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full mr-1.5"></span>
|
||||
Nodes
|
||||
</span>
|
||||
<span className="inline-flex items-center mx-2 px-2 py-0.5 rounded text-amber-500 bg-amber-900/30">
|
||||
<span className="w-2 h-2 bg-amber-500 rounded-full mr-1.5"></span>
|
||||
Gateways
|
||||
</span>
|
||||
<div className="mt-2 bg-neutral-800/50 rounded-lg p-2 text-xs flex items-center justify-between effect-inset">
|
||||
<div>
|
||||
<span className="inline-flex items-center mx-2 px-2 py-0.5 rounded text-green-500 bg-green-900/30">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full mr-1.5"></span>
|
||||
Nodes
|
||||
</span>
|
||||
<span className="inline-flex items-center mx-2 px-2 py-0.5 rounded text-amber-500 bg-amber-900/30">
|
||||
<span className="w-2 h-2 bg-amber-500 rounded-full mr-1.5"></span>
|
||||
Gateways
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Always show the button, but disable it when auto-zoom is enabled */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={handleResetZoom}
|
||||
icon={Locate}
|
||||
disabled={autoZoomEnabled}
|
||||
title={autoZoomEnabled ? "Auto-zoom is already enabled" : "Enable auto-zoom to fit all nodes"}
|
||||
>
|
||||
Auto-zoom
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
25
web/src/types/google-maps.d.ts
vendored
25
web/src/types/google-maps.d.ts
vendored
@@ -9,6 +9,7 @@ declare namespace google {
|
||||
setZoom(zoom: number): void;
|
||||
getZoom(): number | undefined;
|
||||
fitBounds(bounds: LatLngBounds): void;
|
||||
addListener(event: string, handler: Function): MapsEventListener;
|
||||
}
|
||||
|
||||
class Marker {
|
||||
@@ -16,7 +17,7 @@ declare namespace google {
|
||||
setMap(map: Map | null): void;
|
||||
setPosition(position: LatLngLiteral): void;
|
||||
setIcon(icon: any): void;
|
||||
addListener(event: string, handler: Function): void;
|
||||
addListener(event: string, handler: Function): MapsEventListener;
|
||||
}
|
||||
|
||||
class Circle {
|
||||
@@ -76,6 +77,28 @@ declare namespace google {
|
||||
position?: LatLngLiteral;
|
||||
}
|
||||
|
||||
// Event-related functionality
|
||||
const event: {
|
||||
/**
|
||||
* Removes the given listener, which should have been returned by
|
||||
* google.maps.event.addListener.
|
||||
*/
|
||||
removeListener(listener: MapsEventListener): void;
|
||||
/**
|
||||
* Removes all listeners for all events for the given instance.
|
||||
*/
|
||||
clearInstanceListeners(instance: Object): void;
|
||||
};
|
||||
|
||||
// Maps Event Listener
|
||||
interface MapsEventListener {
|
||||
/**
|
||||
* Removes the listener.
|
||||
* Equivalent to calling google.maps.event.removeListener(listener).
|
||||
*/
|
||||
remove(): void;
|
||||
}
|
||||
|
||||
const MapTypeId: {
|
||||
ROADMAP: string;
|
||||
SATELLITE: string;
|
||||
|
||||
Reference in New Issue
Block a user