From 63465f13e67baac4d627a3125963d66b666d13f1 Mon Sep 17 00:00:00 2001 From: kevin Date: Sat, 6 Jun 2026 03:00:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=87=8D=E5=90=88=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=98=BE=E7=A4=BA=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- meshmap_frontend/src/components/MeshMap.vue | 96 +++++++++++++++++++-- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/meshmap_frontend/src/components/MeshMap.vue b/meshmap_frontend/src/components/MeshMap.vue index 649ee9d..9021a0f 100644 --- a/meshmap_frontend/src/components/MeshMap.vue +++ b/meshmap_frontend/src/components/MeshMap.vue @@ -38,6 +38,8 @@ let map: L.Map | null = null let tileLayer: L.TileLayer | null = null let markerLayer: L.LayerGroup | null = null const markersByKey = new Map() +const overlapShuffleOrders = new Map() +const shuffledSelectedNodeIds = new Set() let hasFitBounds = false const minMapZoom = 3 @@ -65,6 +67,8 @@ onMounted(async () => { applyTileLayer() map.on('click', () => { closeNodeMenu() + overlapShuffleOrders.clear() + shuffledSelectedNodeIds.clear() emit('clear-node') }) map.on('moveend', emitBoundsChange) @@ -81,6 +85,8 @@ onBeforeUnmount(() => { tileLayer = null markerLayer = null markersByKey.clear() + overlapShuffleOrders.clear() + shuffledSelectedNodeIds.clear() }) watch( @@ -177,6 +183,7 @@ function renderMarkers(forceFit: boolean) { } const bounds = L.latLngBounds([]) const visibleMarkerKeys = new Set() + const overlapGroups = buildOverlapGroups(props.items) for (const item of props.items) { const markerKey = mapMarkerKey(item) @@ -195,7 +202,12 @@ function renderMarkers(forceFit: boolean) { const node = item const selected = node.node_id === props.selectedNodeId - const raised = selected || node.node_id === lastRaisedNodeId.value + const overlapGroupKey = nodeOverlapGroupKey(node) + const overlapGroup = overlapGroupKey ? overlapGroups.get(overlapGroupKey) : undefined + const overlapIndex = overlapGroup ? nodeOverlapIndex(node, overlapGroup) : 0 + const shuffledSelected = selected && shuffledSelectedNodeIds.has(node.node_id) + const raised = !shuffledSelected && (selected || node.node_id === lastRaisedNodeId.value) + const zIndexOffset = raised ? 1000 : overlapIndex const nodeIcon = L.divIcon({ className: `node-marker${selected ? ' selected' : ''}`, html: `${escapeHTML(node.label || 'N')}`, @@ -208,7 +220,7 @@ function renderMarkers(forceFit: boolean) { marker = L.marker([node.latitude, node.longitude], { icon: nodeIcon, title: node.label, - zIndexOffset: raised ? 1000 : 0, + zIndexOffset, }) marker.bindPopup(buildNodePopupHTML(node), { maxWidth: 320, className: 'node-detail-popup' }) marker.addTo(markerLayer) @@ -216,7 +228,7 @@ function renderMarkers(forceFit: boolean) { } else { marker.setLatLng([node.latitude, node.longitude]) marker.setIcon(nodeIcon) - marker.setZIndexOffset(raised ? 1000 : 0) + marker.setZIndexOffset(zIndexOffset) marker.options.title = node.label marker.getElement()?.setAttribute('title', node.label) const popup = marker.getPopup() @@ -231,13 +243,22 @@ function renderMarkers(forceFit: boolean) { marker.off('contextmenu') marker.on('click', (event) => { L.DomEvent.stopPropagation(event) - lastRaisedNodeId.value = node.node_id closeNodeMenu() + if (node.node_id === props.selectedNodeId) { + if (moveSelectedNodeBehindOverlap(node, overlapGroups)) { + shuffledSelectedNodeIds.add(node.node_id) + marker?.closePopup() + renderMarkers(false) + } + return + } + shuffledSelectedNodeIds.clear() + lastRaisedNodeId.value = node.node_id emit('select-node', node.node_id) }) marker.on('contextmenu', (event) => openNodeMenu(node, event)) - if (selected && !marker.getPopup()?.isOpen()) { + if (selected && !shuffledSelected && !marker.getPopup()?.isOpen()) { marker.openPopup() } bounds.extend([node.latitude, node.longitude]) @@ -263,6 +284,71 @@ function mapMarkerKey(item: MapRenderable): string { return `node:${item.node_id}` } +function buildOverlapGroups(items: MapRenderable[]): Map { + const groups = new Map() + for (const item of items) { + if (item.type === 'cluster') { + continue + } + const key = nodeOverlapGroupKey(item) + const group = groups.get(key) + if (group) { + group.push(item.node_id) + } else { + groups.set(key, [item.node_id]) + } + } + + for (const [key, nodeIds] of groups) { + if (nodeIds.length < 2) { + groups.delete(key) + overlapShuffleOrders.delete(key) + continue + } + + const existingOrder = overlapShuffleOrders.get(key) ?? [] + const activeIds = new Set(nodeIds) + const ordered = existingOrder.filter((nodeId) => activeIds.has(nodeId)) + for (const nodeId of nodeIds) { + if (!ordered.includes(nodeId)) { + ordered.push(nodeId) + } + } + overlapShuffleOrders.set(key, ordered) + groups.set(key, ordered) + } + + for (const key of overlapShuffleOrders.keys()) { + if (!groups.has(key)) { + overlapShuffleOrders.delete(key) + } + } + + return groups +} + +function nodeOverlapGroupKey(node: MapNode): string { + return `${node.latitude.toFixed(6)}:${node.longitude.toFixed(6)}` +} + +function nodeOverlapIndex(node: MapNode, group: string[]): number { + const index = group.indexOf(node.node_id) + return index === -1 ? 0 : index +} + +function moveSelectedNodeBehindOverlap(node: MapNode, overlapGroups: Map): boolean { + const groupKey = nodeOverlapGroupKey(node) + const group = overlapGroups.get(groupKey) + if (!group || group.length < 2) { + return false + } + + const nextOrder = [node.node_id, ...group.filter((nodeId) => nodeId !== node.node_id)] + overlapShuffleOrders.set(groupKey, nextOrder) + lastRaisedNodeId.value = null + return true +} + function buildClusterMarker(cluster: MapClusterNode): L.Marker { const size = clusterIconSize(cluster.count) const marker = L.marker([cluster.latitude, cluster.longitude], {