mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-03-28 17:42:58 +01:00
Store map lat/lng, fix state handling
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { Suspense } from "react";
|
||||
import { useConfig } from "@/components/ConfigContext";
|
||||
import ChatBox from "@/components/ChatBox";
|
||||
|
||||
@@ -12,7 +13,9 @@ export default function MessagesPage() {
|
||||
{/* ChatBox component with all messages tab enabled and expanded behavior */}
|
||||
<div className="flex-1 flex justify-center items-start p-4">
|
||||
<div className="w-full max-w-6xl h-full">
|
||||
<ChatBox showAllMessagesTab={true} startExpanded={true} className="w-full h-full" />
|
||||
<Suspense fallback={<div className="w-full h-full flex items-center justify-center">Loading...</div>}>
|
||||
<ChatBox showAllMessagesTab={true} startExpanded={true} className="w-full h-full" />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { renderToString } from "react-dom/server";
|
||||
import { buildApiUrl } from "../lib/api";
|
||||
import { NodePosition } from "../types/map";
|
||||
import { useNeighbors, type Neighbor } from "../hooks/useNeighbors";
|
||||
import { useQueryParams } from "../hooks/useQueryParams";
|
||||
|
||||
const DEFAULT = {
|
||||
lat: 46.56, // Center between Seattle and Portland
|
||||
@@ -21,6 +22,12 @@ const DEFAULT = {
|
||||
zoom: 7, // Zoom level to show both cities
|
||||
};
|
||||
|
||||
interface MapQuery {
|
||||
lat?: number;
|
||||
lng?: number;
|
||||
zoom?: number;
|
||||
}
|
||||
|
||||
|
||||
type ClusteredMarkersProps = {
|
||||
nodes: NodePosition[];
|
||||
@@ -30,7 +37,7 @@ type ClusteredMarkersProps = {
|
||||
};
|
||||
|
||||
// Individual marker component
|
||||
function IndividualMarker({
|
||||
const IndividualMarker = React.memo(function IndividualMarker({
|
||||
node,
|
||||
showNodeNames,
|
||||
selectedNodeId,
|
||||
@@ -89,17 +96,13 @@ function IndividualMarker({
|
||||
map.removeLayer(markerRef.current);
|
||||
}
|
||||
};
|
||||
}, [map, node, showNodeNames, selectedNodeId, isLoadingNeighbors]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally omitting selectedNodeId, showNodeNames, isLoadingNeighbors to prevent marker recreation
|
||||
}, [map, node.node_id, node.latitude, node.longitude, node.type]);
|
||||
|
||||
// Update marker when node data changes
|
||||
// Update marker when visual properties change (but don't recreate marker)
|
||||
useEffect(() => {
|
||||
if (markerRef.current) {
|
||||
const currentPos = markerRef.current.getLatLng();
|
||||
if (currentPos.lat !== node.latitude || currentPos.lng !== node.longitude) {
|
||||
markerRef.current.setLatLng([node.latitude, node.longitude]);
|
||||
}
|
||||
|
||||
// Update icon and popup
|
||||
// Update icon and popup content only
|
||||
const isSelected = selectedNodeId === node.node_id;
|
||||
const icon = L.divIcon({
|
||||
className: 'custom-node-marker-container',
|
||||
@@ -119,11 +122,21 @@ function IndividualMarker({
|
||||
}
|
||||
}, [node, showNodeNames, selectedNodeId, isLoadingNeighbors]);
|
||||
|
||||
// Handle position updates separately to avoid recreating marker
|
||||
useEffect(() => {
|
||||
if (markerRef.current) {
|
||||
const currentPos = markerRef.current.getLatLng();
|
||||
if (currentPos.lat !== node.latitude || currentPos.lng !== node.longitude) {
|
||||
markerRef.current.setLatLng([node.latitude, node.longitude]);
|
||||
}
|
||||
}
|
||||
}, [node.latitude, node.longitude]);
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Clustered markers component
|
||||
function ClusteredMarkersGroup({
|
||||
const ClusteredMarkersGroup = React.memo(function ClusteredMarkersGroup({
|
||||
nodes,
|
||||
showNodeNames,
|
||||
selectedNodeId,
|
||||
@@ -145,6 +158,7 @@ function ClusteredMarkersGroup({
|
||||
onNodeClickRef.current = onNodeClick;
|
||||
}, [onNodeClick]);
|
||||
|
||||
// Create cluster group only when map or nodes array changes
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
@@ -201,12 +215,40 @@ function ClusteredMarkersGroup({
|
||||
map.removeLayer(clusterGroupRef.current);
|
||||
}
|
||||
};
|
||||
}, [map, nodes, showNodeNames, selectedNodeId, isLoadingNeighbors]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally omitting selectedNodeId, showNodeNames, isLoadingNeighbors to prevent cluster recreation
|
||||
}, [map, nodes]);
|
||||
|
||||
// Update marker appearances when visual properties change
|
||||
useEffect(() => {
|
||||
if (!clusterGroupRef.current) return;
|
||||
|
||||
clusterGroupRef.current.eachLayer((marker: any) => {
|
||||
const nodeData = marker.options.nodeData;
|
||||
if (nodeData) {
|
||||
const isSelected = selectedNodeId === nodeData.node_id;
|
||||
const icon = L.divIcon({
|
||||
className: 'custom-node-marker-container',
|
||||
iconSize: [16, 32],
|
||||
iconAnchor: [8, 8],
|
||||
html: renderToString(
|
||||
<NodeMarker
|
||||
node={nodeData}
|
||||
showNodeNames={showNodeNames}
|
||||
isSelected={isSelected}
|
||||
isLoadingNeighbors={isSelected && isLoadingNeighbors}
|
||||
/>
|
||||
),
|
||||
});
|
||||
marker.setIcon(icon);
|
||||
marker.getPopup()?.setContent(renderToString(<PopupContent node={nodeData} />));
|
||||
}
|
||||
});
|
||||
}, [showNodeNames, selectedNodeId, isLoadingNeighbors]);
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
function ClusteredMarkers({ nodes, selectedNodeId, onNodeClick, isLoadingNeighbors = false }: ClusteredMarkersProps) {
|
||||
const ClusteredMarkers = React.memo(function ClusteredMarkers({ nodes, selectedNodeId, onNodeClick, isLoadingNeighbors = false }: ClusteredMarkersProps) {
|
||||
const configResult = useConfig();
|
||||
const config = configResult?.config;
|
||||
const showNodeNames = config?.showNodeNames !== false;
|
||||
@@ -239,7 +281,7 @@ function ClusteredMarkers({ nodes, selectedNodeId, onNodeClick, isLoadingNeighbo
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Component to render neighbor lines with directional arrows
|
||||
function NeighborLines({
|
||||
@@ -314,6 +356,16 @@ export default function MapView() {
|
||||
const configResult = useConfig();
|
||||
const config = configResult?.config;
|
||||
|
||||
// Use query params to persist map position
|
||||
const { query: mapQuery, updateQuery: updateMapQuery } = useQueryParams<MapQuery>({
|
||||
lat: DEFAULT.lat,
|
||||
lng: DEFAULT.lng,
|
||||
zoom: DEFAULT.zoom,
|
||||
});
|
||||
|
||||
const mapCenter: [number, number] = [mapQuery.lat ?? DEFAULT.lat, mapQuery.lng ?? DEFAULT.lng];
|
||||
const mapZoom = mapQuery.zoom ?? DEFAULT.zoom;
|
||||
|
||||
// Neighbor-related state
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
|
||||
@@ -409,7 +461,18 @@ export default function MapView() {
|
||||
function MapEventCatcher() {
|
||||
useMapEvents({
|
||||
moveend: (e) => {
|
||||
const b = e.target.getBounds();
|
||||
const map = e.target;
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
||||
// Update URL with new map position
|
||||
updateMapQuery({
|
||||
lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
|
||||
lng: Math.round(center.lng * 100000) / 100000,
|
||||
zoom: zoom
|
||||
});
|
||||
|
||||
const b = map.getBounds();
|
||||
const buffer = 0.2; // 20% buffer
|
||||
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
|
||||
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
|
||||
@@ -433,7 +496,18 @@ export default function MapView() {
|
||||
}
|
||||
},
|
||||
zoomend: (e) => {
|
||||
const b = e.target.getBounds();
|
||||
const map = e.target;
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
||||
// Update URL with new map position
|
||||
updateMapQuery({
|
||||
lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
|
||||
lng: Math.round(center.lng * 100000) / 100000,
|
||||
zoom: zoom
|
||||
});
|
||||
|
||||
const b = map.getBounds();
|
||||
const buffer = 0.2; // 20% buffer
|
||||
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
|
||||
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
|
||||
@@ -502,9 +576,9 @@ export default function MapView() {
|
||||
ariaLabel="Refresh map nodes"
|
||||
/>
|
||||
</div>
|
||||
<MapContainer
|
||||
center={[DEFAULT.lat, DEFAULT.lng]}
|
||||
zoom={DEFAULT.zoom}
|
||||
<MapContainer
|
||||
center={mapCenter}
|
||||
zoom={mapZoom}
|
||||
style={{ width: "100%", height: "100%", zIndex: 1 }}
|
||||
className="bg-gray-200"
|
||||
>
|
||||
@@ -529,7 +603,6 @@ export default function MapView() {
|
||||
/>
|
||||
)}
|
||||
<ClusteredMarkers
|
||||
key={`clustering-${config?.clustering}-${config?.showNodeNames}`}
|
||||
nodes={nodePositions}
|
||||
selectedNodeId={selectedNodeId}
|
||||
onNodeClick={handleNodeClick}
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function PathVisualization({
|
||||
}, [searchResults, uniquePrefixes]);
|
||||
|
||||
// Fixed node spacing optimized for ~3 lines of text
|
||||
const fixedNodeSize = { x: 140, y: 100 };
|
||||
const fixedNodeSize = useMemo(() => ({ x: 140, y: 100 }), []);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setExpanded(prev => !prev);
|
||||
@@ -319,7 +319,7 @@ export default function PathVisualization({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [showGraph, pathsCount, treeData, graphFullscreen, handleFullscreenToggle, renderCustomNodeElement]);
|
||||
}, [showGraph, pathsCount, treeData, graphFullscreen, handleFullscreenToggle, renderCustomNodeElement, fixedNodeSize]);
|
||||
|
||||
if (!showDropdown) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user