UNPKG

atchain-mapbox-vue

Version:

A Vue 3 MapBox component library with subway lines, stations, markers and polygons support. Zero dependencies except Vue 3 and Mapbox GL JS.

500 lines (422 loc) 15 kB
/** * MapBox 图层管理 */ import type { GeoJSONSource } from 'mapbox-gl' import type { Position } from 'geojson' import { useMapBoxCore } from './useMapBoxCore' import { useMapBoxData, type SubwayFeatureCollection, type StationFeatureCollection } from './useMapBoxData' import { haversineDistanceKm } from './useMapBoxUtils' import { SUBWAY_SOURCE_ID, SUBWAY_BASE_LAYER_ID, SUBWAY_ANIMATED_LAYER_ID, STATION_SOURCE_ID, STATION_LAYER_ID, STATION_LABEL_LAYER_ID, DEFAULT_STYLES, ANIMATION_CONFIG } from './useMapBoxConstants' // 动画帧引用 let subwayAnimationFrame: number | null = null /** * 使用图层管理 */ export const useMapBoxLayers = () => { const { mapInstance, removeLayer, removeSource } = useMapBoxCore() const { subwayData, stationData } = useMapBoxData() /** * 停止地铁动画 */ const stopSubwayAnimation = () => { if (subwayAnimationFrame) { cancelAnimationFrame(subwayAnimationFrame) subwayAnimationFrame = null } } /** * 清理地铁图层 */ const cleanupSubwayLayers = () => { if (!mapInstance.value) return stopSubwayAnimation() removeLayer(SUBWAY_ANIMATED_LAYER_ID) removeLayer(SUBWAY_BASE_LAYER_ID) removeSource(SUBWAY_SOURCE_ID) } /** * 清理站点图层 */ const cleanupStationLayers = () => { if (!mapInstance.value) return removeLayer(STATION_LABEL_LAYER_ID) removeLayer(STATION_LAYER_ID) removeSource(STATION_SOURCE_ID) } /** * 过滤地铁要素(在指定范围内) */ const filterSubwayFeatures = ( collection: SubwayFeatureCollection, center?: Position, radiusKm = 8 ) => { if (!center) return collection.features const filtered = collection.features.filter((feature) => { if (feature.geometry?.type !== 'LineString') return false const coords = feature.geometry.coordinates return coords.some((coord) => haversineDistanceKm(coord, center) <= radiusKm) }) return filtered.length ? filtered : collection.features } /** * 过滤站点要素(在指定范围内) */ const filterStationFeatures = ( collection: StationFeatureCollection, center?: Position, radiusKm = 15 ) => { if (!center) return collection.features const filtered = collection.features.filter((feature) => { if (feature.geometry?.type !== 'Point') return false return haversineDistanceKm(feature.geometry.coordinates, center) <= radiusKm }) // 返回过滤后的结果,即使为空数组也返回 return filtered } /** * 渲染地铁线路 */ const renderSubwayLines = (_center?: Position, _radiusKm = 8) => { if (!mapInstance.value || !subwayData.value) { console.warn('Map or subway data not ready when trying to render subway lines') return } // 不对地铁数据做额外拼接或过滤,直接使用 setSubwayData 提供的 GeoJSON const geojson: SubwayFeatureCollection = subwayData.value const existingSource = mapInstance.value.getSource(SUBWAY_SOURCE_ID) as GeoJSONSource | undefined if (existingSource) { // 更新已有数据源,保持动画与图层配置不变 existingSource.setData(geojson) return } cleanupSubwayLayers() mapInstance.value.addSource(SUBWAY_SOURCE_ID, { type: 'geojson', data: geojson }) // 背景线:填补虚线之间的间隙 mapInstance.value.addLayer({ id: SUBWAY_BASE_LAYER_ID, type: 'line', source: SUBWAY_SOURCE_ID, paint: { 'line-color': ['coalesce', ['get', 'color'], '#ff5722'], 'line-width': DEFAULT_STYLES.subway.baseLineWidth, 'line-opacity': DEFAULT_STYLES.subway.baseLineOpacity } }) // 蚂蚁动画线:使用 line-dasharray 做流动效果 mapInstance.value.addLayer({ id: SUBWAY_ANIMATED_LAYER_ID, type: 'line', source: SUBWAY_SOURCE_ID, paint: { 'line-color': ['coalesce', ['get', 'color'], '#ff5722'], 'line-width': DEFAULT_STYLES.subway.animatedLineWidth, 'line-dasharray': DEFAULT_STYLES.subway.animatedLineDashArray, 'line-emissive-strength': DEFAULT_STYLES.subway.animatedLineEmissiveStrength } }) let step = 0 const animateDashArray = (timestamp: number) => { if (!mapInstance.value) return const newStep = Math.floor((timestamp / ANIMATION_CONFIG.frameInterval) % ANIMATION_CONFIG.dashArraySequence.length) if (newStep !== step) { mapInstance.value.setPaintProperty( SUBWAY_ANIMATED_LAYER_ID, 'line-dasharray', ANIMATION_CONFIG.dashArraySequence[newStep] ) step = newStep } subwayAnimationFrame = requestAnimationFrame(animateDashArray) } subwayAnimationFrame = requestAnimationFrame(animateDashArray) // 确保站点图层显示在地铁线路图层之上 setTimeout(() => { if (mapInstance.value) { if (mapInstance.value.getLayer(STATION_LAYER_ID)) { mapInstance.value.moveLayer(STATION_LAYER_ID) if (mapInstance.value.getLayer(STATION_LABEL_LAYER_ID)) { mapInstance.value.moveLayer(STATION_LABEL_LAYER_ID) } } } }, ANIMATION_CONFIG.layerAdjustDelay) } /** * 渲染地铁站点 */ const renderStations = (center?: Position, radiusKm = 15, showLabels = true) => { if (!mapInstance.value || !stationData.value) { console.warn('Map or station data not ready when trying to render stations') return } // 过滤在指定范围内的站点 const filteredFeatures = filterStationFeatures(stationData.value, center, radiusKm) const geojson: StationFeatureCollection = { type: 'FeatureCollection', features: filteredFeatures } const existingSource = mapInstance.value.getSource(STATION_SOURCE_ID) as GeoJSONSource | undefined if (existingSource) { existingSource.setData(geojson) return } cleanupStationLayers() mapInstance.value.addSource(STATION_SOURCE_ID, { type: 'geojson', data: geojson }) // 站点圆圈 mapInstance.value.addLayer({ id: STATION_LAYER_ID, type: 'circle', source: STATION_SOURCE_ID, paint: { 'circle-radius': [ 'case', ['get', 'interchange'], DEFAULT_STYLES.station.interchangeRadius, DEFAULT_STYLES.station.normalRadius ], 'circle-color': ['coalesce', ['get', 'color'], DEFAULT_STYLES.station.defaultColor], 'circle-stroke-width': DEFAULT_STYLES.station.strokeWidth, 'circle-stroke-color': DEFAULT_STYLES.station.strokeColor, 'circle-opacity': DEFAULT_STYLES.station.opacity } }) // 站点名称标签 if (showLabels) { mapInstance.value.addLayer({ id: STATION_LABEL_LAYER_ID, type: 'symbol', source: STATION_SOURCE_ID, layout: { 'text-field': ['get', 'name'], 'text-font': DEFAULT_STYLES.stationLabel.fonts, 'text-size': [ 'case', ['get', 'interchange'], DEFAULT_STYLES.stationLabel.interchangeFontSize, DEFAULT_STYLES.stationLabel.normalFontSize ], 'text-offset': DEFAULT_STYLES.stationLabel.textOffset, 'text-anchor': DEFAULT_STYLES.stationLabel.textAnchor, 'text-allow-overlap': false, 'text-ignore-placement': false }, paint: { 'text-color': DEFAULT_STYLES.stationLabel.textColor, 'text-halo-color': DEFAULT_STYLES.stationLabel.haloColor, 'text-halo-width': DEFAULT_STYLES.stationLabel.haloWidth, 'text-opacity': DEFAULT_STYLES.stationLabel.textOpacity } }) } // 确保站点图层显示在地铁线路图层之上 setTimeout(() => { if (mapInstance.value) { // 如果地铁线路图层存在,将站点图层移动到其上方 if (mapInstance.value.getLayer(SUBWAY_ANIMATED_LAYER_ID)) { mapInstance.value.moveLayer(STATION_LAYER_ID) if (showLabels && mapInstance.value.getLayer(STATION_LABEL_LAYER_ID)) { mapInstance.value.moveLayer(STATION_LABEL_LAYER_ID) } } } }, ANIMATION_CONFIG.stationLayerAdjustDelay) } /** * 添加标记点 */ const addMarker = (markerRef: any, id: string, customData?: any) => { if (!mapInstance.value || !markerRef.value) return // 使用传入的数据或全局数据 let dataSource = customData if (!dataSource) { const { mapData } = useMapBoxData() dataSource = mapData.value } // 从数据中查找对应的点要素 const feature = dataSource?.features?.find((f: any) => f.id === id) if (!feature || feature.geometry?.type !== 'Point') { console.warn(`未找到 ID 为 ${id} 的点要素`) return } const coordinates = feature.geometry.coordinates as [number, number] // 将地理坐标转换为屏幕坐标 const point = mapInstance.value.project(coordinates) // 设置 marker 的位置 const markerElement = markerRef.value as HTMLElement markerElement.style.left = `${point.x}px` markerElement.style.top = `${point.y}px` markerElement.style.transform = 'translate(-50%, -50%)' // 居中对齐 // Marker positioned successfully // 监听地图移动,更新标记位置 const updateMarkerPosition = () => { if (!mapInstance.value || !markerRef.value) return const newPoint = mapInstance.value.project(coordinates) const element = markerRef.value as HTMLElement element.style.left = `${newPoint.x}px` element.style.top = `${newPoint.y}px` } // 绑定地图移动事件 mapInstance.value.on('move', updateMarkerPosition) mapInstance.value.on('zoom', updateMarkerPosition) // 初始定位 updateMarkerPosition() // 添加交互功能 addMarkerInteractions(markerElement, feature) } /** * 添加多边形 */ const addPolygon = (id: string, customData?: any) => { if (!mapInstance.value) return // 使用传入的数据或全局数据 let dataSource = customData if (!dataSource) { const { mapData } = useMapBoxData() dataSource = mapData.value } // 从数据中查找对应的多边形要素 const feature = dataSource?.features?.find((f: any) => f.id === id) if (!feature || feature.geometry?.type !== 'Polygon') { console.warn(`未找到 ID 为 ${id} 的多边形要素`) return } const sourceId = `polygon-source-${id}` const fillLayerId = `polygon-fill-${id}` const strokeLayerId = `polygon-stroke-${id}` // 创建多边形的 GeoJSON 数据 const polygonGeoJSON = { type: 'FeatureCollection' as const, features: [feature] } // 添加数据源 if (!mapInstance.value.getSource(sourceId)) { mapInstance.value.addSource(sourceId, { type: 'geojson', data: polygonGeoJSON }) } // 添加填充图层 if (!mapInstance.value.getLayer(fillLayerId)) { mapInstance.value.addLayer({ id: fillLayerId, type: 'fill', source: sourceId, paint: { 'fill-color': '#088', 'fill-opacity': 0.3 } }) } // 添加边框图层 if (!mapInstance.value.getLayer(strokeLayerId)) { mapInstance.value.addLayer({ id: strokeLayerId, type: 'line', source: sourceId, paint: { 'line-color': '#088', 'line-width': 2, 'line-opacity': 0.8 } }) } // 添加交互功能 addPolygonInteractions(fillLayerId, feature) // Polygon added successfully } /** * 添加多边形交互功能 */ const addPolygonInteractions = (layerId: string, feature: any) => { if (!mapInstance.value) return // 点击事件 mapInstance.value.on('click', layerId, (_e) => { // 可以在这里添加弹窗或其他交互逻辑 if (feature.properties?.polygon) { console.log(`点击了多边形: ${feature.properties.polygon}`) } }) // 鼠标进入事件 - 高亮显示 mapInstance.value.on('mouseenter', layerId, (_e) => { // 改变鼠标样式 mapInstance.value!.getCanvas().style.cursor = 'pointer' // 高亮效果 - 改变填充颜色 mapInstance.value!.setPaintProperty(layerId, 'fill-opacity', 0.6) mapInstance.value!.setPaintProperty(layerId, 'fill-color', '#ff6600') }) // 鼠标离开事件 - 移除高亮 mapInstance.value.on('mouseleave', layerId, (_e) => { // 恢复鼠标样式 mapInstance.value!.getCanvas().style.cursor = '' // 恢复原始样式 mapInstance.value!.setPaintProperty(layerId, 'fill-opacity', 0.3) mapInstance.value!.setPaintProperty(layerId, 'fill-color', '#088') }) } /** * 添加标记点交互功能 */ const addMarkerInteractions = (markerElement: HTMLElement, feature: any) => { // 点击事件 markerElement.addEventListener('click', (e) => { e.stopPropagation() if (feature.properties?.point) { console.log(`点击了标记点: ${feature.properties.point}`) // 可以在这里添加弹窗或其他交互逻辑 } }) // 鼠标进入事件 markerElement.addEventListener('mouseenter', (_e) => { markerElement.style.transform = 'translate(-50%, -50%) scale(1.1)' markerElement.style.zIndex = '1000' }) // 鼠标离开事件 markerElement.addEventListener('mouseleave', (_e) => { markerElement.style.transform = 'translate(-50%, -50%) scale(1)' markerElement.style.zIndex = 'auto' }) } /** * 清理所有图层 */ const cleanupAllLayers = () => { cleanupSubwayLayers() cleanupStationLayers() } return { // 地铁线路相关 renderSubwayLines, cleanupSubwayLayers, stopSubwayAnimation, // 站点相关 renderStations, cleanupStationLayers, // 其他图层 addMarker, addPolygon, // 交互功能 addPolygonInteractions, addMarkerInteractions, // 工具方法 filterSubwayFeatures, filterStationFeatures, cleanupAllLayers } }