Fix zooming functionality

This commit is contained in:
Daniel Pupius
2025-04-29 12:47:44 -07:00
parent b2b94d7204
commit 2c37282dab
6 changed files with 196 additions and 41 deletions

View File

@@ -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">

View File

@@ -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 {

View File

@@ -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">

View File

@@ -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}

View File

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

View File

@@ -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;