地图缩放融合
This commit is contained in:
@@ -2,18 +2,24 @@
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import L from 'leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import type { MapNode } from '../types'
|
||||
import type { MapBoundsChangePayload, MapClusterNode, MapNode, MapRenderable } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: MapNode[]
|
||||
const props = withDefaults(defineProps<{
|
||||
items: MapRenderable[]
|
||||
selectedNodeId: string | null
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
autoFit?: boolean
|
||||
loading?: boolean
|
||||
}>(), {
|
||||
autoFit: true,
|
||||
loading: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select-node': [nodeId: string]
|
||||
'clear-node': []
|
||||
'delete-node': [nodeId: string]
|
||||
'bounds-change': [payload: MapBoundsChangePayload]
|
||||
}>()
|
||||
|
||||
const mapEl = ref<HTMLElement | null>(null)
|
||||
@@ -48,8 +54,10 @@ onMounted(async () => {
|
||||
closeNodeMenu()
|
||||
emit('clear-node')
|
||||
})
|
||||
map.on('moveend', emitBoundsChange)
|
||||
markerLayer = L.layerGroup().addTo(map)
|
||||
renderMarkers(true)
|
||||
emitBoundsChange()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -61,7 +69,7 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.nodes, props.selectedNodeId] as const,
|
||||
() => [props.items, props.selectedNodeId] as const,
|
||||
() => renderMarkers(false),
|
||||
{ deep: true },
|
||||
)
|
||||
@@ -95,6 +103,22 @@ function handleKeydown(event: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function emitBoundsChange() {
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
const bounds = map.getBounds()
|
||||
emit('bounds-change', {
|
||||
bounds: {
|
||||
min_lat: clamp(bounds.getSouth(), -90, 90),
|
||||
max_lat: clamp(bounds.getNorth(), -90, 90),
|
||||
min_lng: normalizeLongitude(bounds.getWest()),
|
||||
max_lng: normalizeLongitude(bounds.getEast()),
|
||||
},
|
||||
zoom: map.getZoom(),
|
||||
})
|
||||
}
|
||||
|
||||
function renderMarkers(forceFit: boolean) {
|
||||
if (!map || !markerLayer) {
|
||||
return
|
||||
@@ -102,7 +126,14 @@ function renderMarkers(forceFit: boolean) {
|
||||
markerLayer.clearLayers()
|
||||
const bounds = L.latLngBounds([])
|
||||
|
||||
for (const node of props.nodes) {
|
||||
for (const item of props.items) {
|
||||
if (item.type === 'cluster') {
|
||||
const marker = buildClusterMarker(item)
|
||||
marker.addTo(markerLayer)
|
||||
bounds.extend([item.latitude, item.longitude])
|
||||
continue
|
||||
}
|
||||
const node = item
|
||||
const selected = node.node_id === props.selectedNodeId
|
||||
const marker = L.marker([node.latitude, node.longitude], {
|
||||
icon: L.divIcon({
|
||||
@@ -127,12 +158,33 @@ function renderMarkers(forceFit: boolean) {
|
||||
bounds.extend([node.latitude, node.longitude])
|
||||
}
|
||||
|
||||
if (props.nodes.length > 0 && (forceFit || !hasFitBounds)) {
|
||||
if (props.autoFit && props.items.length > 0 && (forceFit || !hasFitBounds)) {
|
||||
map.fitBounds(bounds, { padding: [24, 24], maxZoom: 13 })
|
||||
hasFitBounds = true
|
||||
}
|
||||
}
|
||||
|
||||
function buildClusterMarker(cluster: MapClusterNode): L.Marker {
|
||||
const size = clusterIconSize(cluster.count)
|
||||
const marker = L.marker([cluster.latitude, cluster.longitude], {
|
||||
icon: L.divIcon({
|
||||
className: `cluster-marker ${clusterClass(cluster.count)}`,
|
||||
html: `<span>${formatCount(cluster.count)}</span>`,
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2],
|
||||
}),
|
||||
title: `${cluster.count} 个坐标`,
|
||||
})
|
||||
marker.bindPopup(buildClusterPopupHTML(cluster), { maxWidth: 260, className: 'node-detail-popup' })
|
||||
marker.on('click', () => {
|
||||
closeNodeMenu()
|
||||
if (map) {
|
||||
map.setView([cluster.latitude, cluster.longitude], Math.min(map.getZoom() + 2, map.getMaxZoom()))
|
||||
}
|
||||
})
|
||||
return marker
|
||||
}
|
||||
|
||||
function buildNodePopupHTML(node: MapNode): string {
|
||||
const info = node.nodeinfo
|
||||
const report = node.map_report
|
||||
@@ -157,6 +209,61 @@ function buildNodePopupHTML(node: MapNode): string {
|
||||
`
|
||||
}
|
||||
|
||||
function buildClusterPopupHTML(cluster: MapClusterNode): string {
|
||||
return `
|
||||
<div class="node-popup">
|
||||
<strong>聚合坐标</strong>
|
||||
<dl>
|
||||
<div><dt>数量</dt><dd>${cluster.count}</dd></div>
|
||||
<div><dt>经度</dt><dd>${cluster.longitude.toFixed(5)}</dd></div>
|
||||
<div><dt>纬度</dt><dd>${cluster.latitude.toFixed(5)}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function clusterIconSize(count: number): number {
|
||||
if (count >= 1000) {
|
||||
return 58
|
||||
}
|
||||
if (count >= 100) {
|
||||
return 50
|
||||
}
|
||||
if (count >= 10) {
|
||||
return 42
|
||||
}
|
||||
return 34
|
||||
}
|
||||
|
||||
function clusterClass(count: number): string {
|
||||
if (count >= 1000) {
|
||||
return 'cluster-large'
|
||||
}
|
||||
if (count >= 100) {
|
||||
return 'cluster-medium'
|
||||
}
|
||||
return 'cluster-small'
|
||||
}
|
||||
|
||||
function formatCount(count: number): string {
|
||||
return count >= 1000 ? `${Math.round(count / 100) / 10}k` : String(count)
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value))
|
||||
}
|
||||
|
||||
function normalizeLongitude(value: number): number {
|
||||
let normalized = value
|
||||
while (normalized < -180) {
|
||||
normalized += 360
|
||||
}
|
||||
while (normalized > 180) {
|
||||
normalized -= 360
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function nodeColor(nodeId: string): string {
|
||||
let hash = 0
|
||||
for (let index = 0; index < nodeId.length; index += 1) {
|
||||
@@ -193,7 +300,8 @@ function escapeHTML(value: string): string {
|
||||
<template>
|
||||
<section class="map-panel panel">
|
||||
<div ref="mapEl" class="map-container"></div>
|
||||
<div v-if="nodes.length === 0" class="map-empty">暂无可显示坐标的节点</div>
|
||||
<div v-if="loading" class="map-empty">正在加载当前区域坐标...</div>
|
||||
<div v-else-if="items.length === 0" class="map-empty">暂无可显示坐标的节点</div>
|
||||
<div
|
||||
v-if="menuNodeId"
|
||||
class="context-menu"
|
||||
|
||||
Reference in New Issue
Block a user