diff --git a/src/lib/amap.ts b/src/lib/amap.ts index aadbcb3..f1a5443 100644 --- a/src/lib/amap.ts +++ b/src/lib/amap.ts @@ -6,7 +6,6 @@ export function loadAMap() { version: '2.0', plugins: [ 'AMap.Scale', - 'AMap.ToolBar', 'AMap.ControlBar', 'AMap.Geolocation', 'AMap.Marker', diff --git a/src/types/amap.d.ts b/src/types/amap.d.ts index dbc6102..2c53705 100644 --- a/src/types/amap.d.ts +++ b/src/types/amap.d.ts @@ -6,7 +6,7 @@ declare namespace AMap { remove(layer: unknown): void destroy(): void setCenter(center: [number, number]): void - setZoom(zoom: number): void + setZoom(zoom: number, immediately?: boolean, duration?: number): void setPitch(pitch: number): void getPitch(): number setRotation(rotation: number): void @@ -14,7 +14,7 @@ declare namespace AMap { on(event: string, callback: (...args: unknown[]) => void): void off(event: string, callback: (...args: unknown[]) => void): void getCenter(): { lng: number; lat: number } - getZoom(): number + getZoom(digits?: number): number getAllOverlays(type?: string): unknown[] addOverlay(overlay: unknown): void removeOverlay(overlay: unknown): void diff --git a/src/views/map/MapView.vue b/src/views/map/MapView.vue index e4b38ed..d55db1f 100644 --- a/src/views/map/MapView.vue +++ b/src/views/map/MapView.vue @@ -8,7 +8,7 @@ import { loadAMap } from '@/lib/amap' import { useAuthStore } from '@/stores/auth' import type { ApiCloud } from '@/types/api' import { NIcon } from 'naive-ui' -import { Adjustments, Calendar, CloudUpload, Refresh, Map, Satellite, X } from '@vicons/tabler' +import { Adjustments, Calendar, CloudUpload, Minus, Plus, Refresh, Map, Satellite, X } from '@vicons/tabler' interface CloudMarkerData { id: string @@ -40,10 +40,15 @@ const archiveMonth = ref(formatMonthInput(new Date())) const currentMinuteOfDay = ref(getMinuteOfDay(new Date())) const selectedMinuteOfDay = ref(currentMinuteOfDay.value) const sliderDragging = ref(false) +const currentZoom = ref(5) const VISIBLE_WINDOW_MS = 2 * 60 * 60 * 1000 const MIN_MARKER_OPACITY = 0.3 const HIDE_HEADER_EVENT = 'opencloud:hide-header' +const MIN_MAP_ZOOM = 4 +const MAX_MAP_ZOOM = 18 +const ZOOM_HOLD_DELAY_MS = 180 +const ZOOM_CONTINUOUS_SPEED = 2.2 let AMapLib: typeof AMap | null = null let mapInst: AMap.Map | null = null @@ -53,6 +58,13 @@ let mks: AMap.Marker[] = [] let hoverIW: AMap.InfoWindow | null = null let allClouds: CloudMarkerData[] = [] let redrawTimer: number | null = null +let zoomHoldDelayTimer: number | null = null +let zoomAnimationFrame: number | null = null +let activeZoomPointerId: number | null = null +let zoomContinuousDirection: -1 | 1 | null = null +let zoomAnimationValue = currentZoom.value +let zoomLastFrameTime = 0 +let zoomHoldStarted = false const rarityColors: Record = { common: '#cbd5e1', @@ -243,6 +255,165 @@ function hideHeader() { window.dispatchEvent(new CustomEvent(HIDE_HEADER_EVENT)) } +function syncCurrentZoom() { + if (!mapInst) return + currentZoom.value = mapInst.getZoom(2) +} + +function changeZoom(direction: -1 | 1, step = 1) { + if (!mapInst) return false + + const zoom = mapInst.getZoom(2) + const nextZoom = Math.min(MAX_MAP_ZOOM, Math.max(MIN_MAP_ZOOM, zoom + direction * step)) + if (Math.abs(nextZoom - zoom) < 0.001) return false + + mapInst.setZoom(nextZoom, true) + currentZoom.value = nextZoom + return true +} + +function stopZoomAnimation() { + if (zoomAnimationFrame !== null) { + window.cancelAnimationFrame(zoomAnimationFrame) + zoomAnimationFrame = null + } + zoomContinuousDirection = null + zoomLastFrameTime = 0 +} + +function clearZoomHoldDelay() { + if (zoomHoldDelayTimer !== null) { + window.clearTimeout(zoomHoldDelayTimer) + zoomHoldDelayTimer = null + } +} + +function clearZoomHold() { + clearZoomHoldDelay() + stopZoomAnimation() + activeZoomPointerId = null + zoomHoldStarted = false +} + +function runContinuousZoomFrame(timestamp: number) { + if (!mapInst || zoomContinuousDirection === null) { + zoomAnimationFrame = null + return + } + + if (zoomLastFrameTime === 0) { + zoomLastFrameTime = timestamp + zoomAnimationFrame = window.requestAnimationFrame(runContinuousZoomFrame) + return + } + + const elapsedSeconds = Math.min((timestamp - zoomLastFrameTime) / 1000, 0.05) + zoomLastFrameTime = timestamp + const nextZoom = Math.min( + MAX_MAP_ZOOM, + Math.max( + MIN_MAP_ZOOM, + zoomAnimationValue + zoomContinuousDirection * ZOOM_CONTINUOUS_SPEED * elapsedSeconds, + ), + ) + + if (Math.abs(nextZoom - zoomAnimationValue) < 0.0001) { + stopZoomAnimation() + return + } + + zoomAnimationValue = nextZoom + mapInst.setZoom(nextZoom, true) + currentZoom.value = nextZoom + + if (nextZoom === MIN_MAP_ZOOM || nextZoom === MAX_MAP_ZOOM) { + stopZoomAnimation() + return + } + zoomAnimationFrame = window.requestAnimationFrame(runContinuousZoomFrame) +} + +function beginContinuousZoom(direction: -1 | 1) { + if (!mapInst) return + + zoomHoldStarted = true + zoomContinuousDirection = direction + zoomAnimationValue = mapInst.getZoom(4) + zoomLastFrameTime = 0 + zoomAnimationFrame = window.requestAnimationFrame(runContinuousZoomFrame) +} + +function startZoomHold(direction: -1 | 1, event: PointerEvent) { + if (event.pointerType === 'mouse' && event.button !== 0) return + if (activeZoomPointerId !== null) return + + event.preventDefault() + clearZoomHold() + activeZoomPointerId = event.pointerId + + const button = event.currentTarget as HTMLButtonElement + button.setPointerCapture?.(event.pointerId) + + const zoom = mapInst?.getZoom(4) + if ( + zoom === undefined + || (direction === 1 && zoom >= MAX_MAP_ZOOM) + || (direction === -1 && zoom <= MIN_MAP_ZOOM) + ) { + activeZoomPointerId = null + if (button.hasPointerCapture?.(event.pointerId)) { + button.releasePointerCapture(event.pointerId) + } + return + } + + zoomHoldDelayTimer = window.setTimeout(() => { + zoomHoldDelayTimer = null + beginContinuousZoom(direction) + }, ZOOM_HOLD_DELAY_MS) +} + +function finishZoomHold(direction: -1 | 1, event: PointerEvent) { + if (activeZoomPointerId === null || event.pointerId !== activeZoomPointerId) return + + const pointerId = activeZoomPointerId + const wasContinuous = zoomHoldStarted + activeZoomPointerId = null + clearZoomHoldDelay() + stopZoomAnimation() + zoomHoldStarted = false + + const button = event.currentTarget as HTMLButtonElement + if (button.hasPointerCapture?.(pointerId)) { + button.releasePointerCapture(pointerId) + } + + if (!wasContinuous) { + changeZoom(direction) + } +} + +function cancelZoomHold(event: PointerEvent) { + if (activeZoomPointerId === null || event.pointerId !== activeZoomPointerId) return + + const pointerId = activeZoomPointerId + activeZoomPointerId = null + clearZoomHoldDelay() + stopZoomAnimation() + zoomHoldStarted = false + + const button = event.currentTarget as HTMLButtonElement + if (button.hasPointerCapture?.(pointerId)) { + button.releasePointerCapture(pointerId) + } +} + +function handleZoomClick(direction: -1 | 1, event: MouseEvent) { + if (event.detail === 0) { + changeZoom(direction) + } +} + function toCloudMarker(row: ApiCloud): CloudMarkerData { return { id: row.id, @@ -423,14 +594,16 @@ async function returnRealtime() { } onMounted(async () => { + window.addEventListener('blur', clearZoomHold) try { AMapLib = await loadAMap() + AMapLib.getConfig().appname = 'amap-jsapi-skill' mapInst = new AMapLib.Map(mapEl.value!, { viewMode: '3D', pitch: 0, rotation: 0, - zoom: 5, - zooms: [4, 18], + zoom: currentZoom.value, + zooms: [MIN_MAP_ZOOM, MAX_MAP_ZOOM], center: [104.07, 30.67], mapStyle: 'amap://styles/normal', features: ['bg', 'road', 'building', 'point'], @@ -440,12 +613,13 @@ onMounted(async () => { } as AMap.MapOptions) mapInst.addControl(new AMapLib.Scale()) - mapInst.addControl(new AMapLib.ToolBar({ position: 'LT' } as Record)) mapInst.addControl(new AMapLib.ControlBar({ position: { right: '50px', bottom: '224px' } } as Record)) mapInst.on('click', () => { previewCloud.value = null; hideHoverCard() }) mapInst.on('zoomstart', hideHeader) + mapInst.on('zoomchange', syncCurrentZoom) mapInst.on('movestart', hideHeader) mapInst.on('dragstart', hideHeader) + syncCurrentZoom() await refresh() startMarkerDecayTimer() } catch (e) { @@ -454,10 +628,13 @@ onMounted(async () => { }) onUnmounted(() => { + window.removeEventListener('blur', clearZoomHold) + clearZoomHold() if (redrawTimer !== null) { window.clearInterval(redrawTimer) redrawTimer = null } + mapInst?.off('zoomchange', syncCurrentZoom) mapInst?.destroy() mapInst = null AMapLib = null @@ -468,6 +645,41 @@ onUnmounted(() => {
+
+ + +
+