优化重合节点显示机制

This commit is contained in:
2026-06-06 03:00:21 +08:00
parent 35823507a0
commit 63465f13e6
+91 -5
View File
@@ -38,6 +38,8 @@ let map: L.Map | null = null
let tileLayer: L.TileLayer | null = null let tileLayer: L.TileLayer | null = null
let markerLayer: L.LayerGroup | null = null let markerLayer: L.LayerGroup | null = null
const markersByKey = new Map<string, L.Marker>() const markersByKey = new Map<string, L.Marker>()
const overlapShuffleOrders = new Map<string, string[]>()
const shuffledSelectedNodeIds = new Set<string>()
let hasFitBounds = false let hasFitBounds = false
const minMapZoom = 3 const minMapZoom = 3
@@ -65,6 +67,8 @@ onMounted(async () => {
applyTileLayer() applyTileLayer()
map.on('click', () => { map.on('click', () => {
closeNodeMenu() closeNodeMenu()
overlapShuffleOrders.clear()
shuffledSelectedNodeIds.clear()
emit('clear-node') emit('clear-node')
}) })
map.on('moveend', emitBoundsChange) map.on('moveend', emitBoundsChange)
@@ -81,6 +85,8 @@ onBeforeUnmount(() => {
tileLayer = null tileLayer = null
markerLayer = null markerLayer = null
markersByKey.clear() markersByKey.clear()
overlapShuffleOrders.clear()
shuffledSelectedNodeIds.clear()
}) })
watch( watch(
@@ -177,6 +183,7 @@ function renderMarkers(forceFit: boolean) {
} }
const bounds = L.latLngBounds([]) const bounds = L.latLngBounds([])
const visibleMarkerKeys = new Set<string>() const visibleMarkerKeys = new Set<string>()
const overlapGroups = buildOverlapGroups(props.items)
for (const item of props.items) { for (const item of props.items) {
const markerKey = mapMarkerKey(item) const markerKey = mapMarkerKey(item)
@@ -195,7 +202,12 @@ function renderMarkers(forceFit: boolean) {
const node = item const node = item
const selected = node.node_id === props.selectedNodeId 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({ const nodeIcon = L.divIcon({
className: `node-marker${selected ? ' selected' : ''}`, className: `node-marker${selected ? ' selected' : ''}`,
html: `<span style="--node-color: ${nodeColor(node.node_id)}">${escapeHTML(node.label || 'N')}</span>`, html: `<span style="--node-color: ${nodeColor(node.node_id)}">${escapeHTML(node.label || 'N')}</span>`,
@@ -208,7 +220,7 @@ function renderMarkers(forceFit: boolean) {
marker = L.marker([node.latitude, node.longitude], { marker = L.marker([node.latitude, node.longitude], {
icon: nodeIcon, icon: nodeIcon,
title: node.label, title: node.label,
zIndexOffset: raised ? 1000 : 0, zIndexOffset,
}) })
marker.bindPopup(buildNodePopupHTML(node), { maxWidth: 320, className: 'node-detail-popup' }) marker.bindPopup(buildNodePopupHTML(node), { maxWidth: 320, className: 'node-detail-popup' })
marker.addTo(markerLayer) marker.addTo(markerLayer)
@@ -216,7 +228,7 @@ function renderMarkers(forceFit: boolean) {
} else { } else {
marker.setLatLng([node.latitude, node.longitude]) marker.setLatLng([node.latitude, node.longitude])
marker.setIcon(nodeIcon) marker.setIcon(nodeIcon)
marker.setZIndexOffset(raised ? 1000 : 0) marker.setZIndexOffset(zIndexOffset)
marker.options.title = node.label marker.options.title = node.label
marker.getElement()?.setAttribute('title', node.label) marker.getElement()?.setAttribute('title', node.label)
const popup = marker.getPopup() const popup = marker.getPopup()
@@ -231,13 +243,22 @@ function renderMarkers(forceFit: boolean) {
marker.off('contextmenu') marker.off('contextmenu')
marker.on('click', (event) => { marker.on('click', (event) => {
L.DomEvent.stopPropagation(event) L.DomEvent.stopPropagation(event)
lastRaisedNodeId.value = node.node_id
closeNodeMenu() 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) emit('select-node', node.node_id)
}) })
marker.on('contextmenu', (event) => openNodeMenu(node, event)) marker.on('contextmenu', (event) => openNodeMenu(node, event))
if (selected && !marker.getPopup()?.isOpen()) { if (selected && !shuffledSelected && !marker.getPopup()?.isOpen()) {
marker.openPopup() marker.openPopup()
} }
bounds.extend([node.latitude, node.longitude]) bounds.extend([node.latitude, node.longitude])
@@ -263,6 +284,71 @@ function mapMarkerKey(item: MapRenderable): string {
return `node:${item.node_id}` return `node:${item.node_id}`
} }
function buildOverlapGroups(items: MapRenderable[]): Map<string, string[]> {
const groups = new Map<string, string[]>()
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<string, string[]>): 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 { function buildClusterMarker(cluster: MapClusterNode): L.Marker {
const size = clusterIconSize(cluster.count) const size = clusterIconSize(cluster.count)
const marker = L.marker([cluster.latitude, cluster.longitude], { const marker = L.marker([cluster.latitude, cluster.longitude], {