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
text/typescript
/**
* 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
}
}