UNPKG

vuepress-plugin-photomap

Version:

VuePress2插件,用于在文章中插入照片地图组件,类似Apple相册的PhotoMap功能

469 lines (468 loc) 21.9 kB
import { ref, onUnmounted } from 'vue'; export function useMapLibre(options = {}) { const mapContainer = ref(); const map = ref(); const markers = ref([]); const isLoaded = ref(false); const error = ref(null); // MapLibre GL 实例 let maplibregl = null; // 防止重复设置的标志 let isTerrainSetupInProgress = false; // 配置地形和卫星样式 - 使用 MapLibre GL JS v5.6.2 的完整API const setupTerrainAndStyle = () => { if (!map.value || isTerrainSetupInProgress) return; isTerrainSetupInProgress = true; try { console.log('MapLibre: 开始设置地形和样式'); // 使用 setStyle 方法设置样式,包含地形、天空和山体阴影 const styleUrl = config.enableSatelliteHybrid ? `https://api.maptiler.com/maps/hybrid/style.json?key=${config.mapTilerApiKey}` : `https://api.maptiler.com/maps/streets/style.json?key=${config.mapTilerApiKey}`; map.value.setStyle(styleUrl, { transformStyle: (previousStyle, nextStyle) => { console.log('MapLibre: 转换样式'); // 根据地图类型设置投影和地形 if (config.mapType === 'satellite' && config.enableTerrain) { nextStyle.projection = { type: 'globe' }; console.log('MapLibre: 设置地球投影'); } // 只有在启用地形时才添加地形相关配置 if (config.enableTerrain) { // 添加地形和山体阴影数据源 nextStyle.sources = { ...nextStyle.sources, terrainSource: { type: 'raster-dem', url: `https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=${config.mapTilerApiKey}`, tileSize: 256 }, hillshadeSource: { type: 'raster-dem', url: `https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=${config.mapTilerApiKey}`, tileSize: 256 } }; console.log('MapLibre: 添加地形和山体阴影数据源'); // 设置地形 nextStyle.terrain = { source: 'terrainSource', exaggeration: config.terrainExaggeration }; console.log('MapLibre: 添加地形配置'); // 不添加地形轮廓线,因为raster-dem数据源只能用于hillshade图层 console.log('MapLibre: 跳过地形轮廓线(避免raster-dem类型错误)'); // 添加山体阴影图层 const hasHillsLayer = nextStyle.layers.some((layer) => layer.id === 'hills'); if (!hasHillsLayer) { nextStyle.layers.push({ id: 'hills', type: 'hillshade', source: 'hillshadeSource', layout: { visibility: 'visible' }, paint: { 'hillshade-shadow-color': config.mapType === 'satellite' ? '#473B24' : '#2D3748', 'hillshade-accent-color': config.mapType === 'satellite' ? '#FFF8DC' : '#E2E8F0', 'hillshade-exaggeration': 0.8 } }); console.log('MapLibre: 添加山体阴影图层配置'); } else { console.log('MapLibre: 山体阴影图层已存在,跳过添加'); } } // 设置天空/大气效果 if (config.mapType === 'satellite' && config.enableAtmosphere) { nextStyle.sky = { 'atmosphere-blend': [ 'interpolate', ['linear'], ['zoom'], 0, 1, 2, 0 ] }; console.log('MapLibre: 添加天空/大气效果配置'); } return nextStyle; } }); console.log('MapLibre: 样式设置完成'); // 监听样式加载完成事件 map.value.once('styledata', () => { try { console.log('MapLibre: 样式加载完成,添加控制器'); // 检查是否已有地形控制器 const existingControls = map.value._controls || []; const hasTerrainControl = existingControls.some((control) => control instanceof maplibregl.TerrainControl); const hasGlobeControl = existingControls.some((control) => control instanceof maplibregl.GlobeControl); // 添加地形控制器 if (!hasTerrainControl && maplibregl.TerrainControl && typeof maplibregl.TerrainControl === 'function' && config.enableTerrain) { map.value.addControl(new maplibregl.TerrainControl({ source: 'terrainSource', exaggeration: config.terrainExaggeration }), 'top-left'); console.log('MapLibre: 添加地形控制器'); } // 添加地球控制器 if (!hasGlobeControl && maplibregl.GlobeControl && typeof maplibregl.GlobeControl === 'function' && config.enableSatelliteHybrid) { map.value.addControl(new maplibregl.GlobeControl(), 'top-left'); console.log('MapLibre: 添加地球控制器'); } } catch (err) { console.warn('MapLibre: 添加控制器失败:', err); } }); } catch (err) { console.error('MapLibre: 设置地形和样式失败:', err); } finally { // 重置标志,允许后续调用(如果需要) setTimeout(() => { isTerrainSetupInProgress = false; }, 2000); } }; // 根据地图类型生成样式URL const getStyleUrl = (mapType = 'satellite', apiKey) => { switch (mapType) { case 'terrain': return `https://api.maptiler.com/maps/outdoor-v2/style.json?key=${apiKey}`; case 'simple': return `https://api.maptiler.com/maps/streets-v2/style.json?key=${apiKey}`; case 'satellite': default: // 使用简单的卫星底图,兼容地形功能 return `https://api.maptiler.com/maps/satellite/style.json?key=${apiKey}`; } }; // 默认配置 const config = { style: options.style || getStyleUrl(options.mapType, options.mapTilerApiKey || 'get_your_own_OpIi9ZULNHzrESv6T2vL'), defaultZoom: options.defaultZoom || 10, maxZoom: options.maxZoom || 18, markerSize: options.markerSize || 40, clusterRadius: options.clusterRadius || 50, enableTerrain: options.enableTerrain ?? true, enableSatelliteHybrid: options.enableSatelliteHybrid ?? true, enableAtmosphere: options.enableAtmosphere !== false, terrainExaggeration: options.terrainExaggeration || 1, mapTilerApiKey: options.mapTilerApiKey || 'get_your_own_OpIi9ZULNHzrESv6T2vL', mapType: options.mapType || 'satellite' }; // 初始化地图 const initMap = async (container) => { try { // 动态导入MapLibre GL if (!maplibregl) { console.log('MapLibre: 开始动态导入maplibre-gl'); try { // 尝试多种不同的导入方式 // 方式1: 标准动态导入 const maplibreModule = await import('maplibre-gl'); console.log('MapLibre: 标准导入结果:', maplibreModule); console.log('MapLibre: 标准导入属性:', Object.keys(maplibreModule)); if (maplibreModule.default && maplibreModule.default.Map) { maplibregl = maplibreModule.default; console.log('MapLibre: 使用标准导入的default'); } else if (maplibreModule.Map) { maplibregl = maplibreModule; console.log('MapLibre: 使用标准导入的命名导出'); } else { // 方式2: 尝试通过 window 对象获取(如果是UMD构建) console.log('MapLibre: 尝试从window对象获取'); if (typeof window !== 'undefined' && window.maplibregl) { maplibregl = window.maplibregl; console.log('MapLibre: 从window获取成功'); } else { // 方式3: 尝试强制导入UMD版本 console.log('MapLibre: 尝试导入UMD版本'); const umdModule = await import('maplibre-gl/dist/maplibre-gl.js'); console.log('MapLibre: UMD导入结果:', umdModule); console.log('MapLibre: UMD导入属性:', Object.keys(umdModule)); if (umdModule.default) { maplibregl = umdModule.default; console.log('MapLibre: 使用UMD的default'); } else if (umdModule.Map) { maplibregl = umdModule; console.log('MapLibre: 使用UMD的命名导出'); } else { console.log('MapLibre: 尝试直接使用UMD模块'); maplibregl = umdModule; } } } console.log('MapLibre: 最终导入结果:', maplibregl); console.log('MapLibre: 最终导入结果的所有属性:', maplibregl ? Object.keys(maplibregl) : '无属性'); } catch (err) { console.error('MapLibre: 导入失败:', err); throw new Error('MapLibre GL 导入失败'); } } mapContainer.value = container; console.log('MapLibre: 准备创建地图实例'); console.log('MapLibre: maplibregl.Map:', maplibregl.Map); console.log('MapLibre: typeof maplibregl.Map:', typeof maplibregl.Map); map.value = new maplibregl.Map({ container: container, style: getStyleUrl(config.mapType, config.mapTilerApiKey), center: [0, 0], // 临时中心点,后续会根据标记自动调整 zoom: config.defaultZoom, maxZoom: config.maxZoom, pitch: config.enableTerrain ? 70 : 0, // 启用地形时设置倾斜角度 maxPitch: config.enableTerrain ? 95 : 60, // 性能优化选项 antialias: true, // 抗锯齿 optimizeForTerrain: config.enableTerrain, // 为地形优化 preserveDrawingBuffer: false, // 不保留绘图缓冲区以节省内存 fadeDuration: 150, // 减少过渡动画时间 refreshExpiredTiles: false, // 不自动刷新过期瓦片 attributionControl: false // 去掉右下角的版权信息 }); // 等待地图加载完成 map.value.on('load', () => { console.log('MapLibre: 地图加载完成,开始配置地形和样式'); // 确保地形设置在地图完全加载后执行 setTimeout(() => { setupTerrainAndStyle(); isLoaded.value = true; }, 100); }); // 监听样式加载完成事件,确保地形功能正确初始化 map.value.on('styledata', () => { if (isLoaded.value && config.enableTerrain) { console.log('MapLibre: 样式数据加载完成,检查地形状态'); // 检查是否有地形,如果没有则重新设置 if (!map.value.getTerrain()) { console.log('MapLibre: 未检测到地形,重新应用地形设置'); setTimeout(() => { setupTerrainAndStyle(); }, 200); } } }); map.value.on('error', (e) => { error.value = '地图加载失败'; console.error('MapLibre错误:', e); }); // 添加地图控件 map.value.addControl(new maplibregl.NavigationControl({ visualizePitch: config.enableTerrain, showZoom: true, showCompass: true }), 'top-right'); map.value.addControl(new maplibregl.ScaleControl({})); } catch (err) { error.value = '初始化地图失败'; console.error('地图初始化错误:', err); } }; // 创建照片标记元素 const createPhotoMarker = (photo, onClick) => { const el = document.createElement('div'); el.className = 'photo-marker'; el.style.width = `${config.markerSize}px`; el.style.height = `${config.markerSize}px`; el.style.borderRadius = '50%'; el.style.cursor = 'pointer'; el.style.border = '2px solid white'; el.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)'; el.style.backgroundImage = `url(${photo.src})`; el.style.backgroundSize = 'cover'; el.style.backgroundPosition = 'center'; // 添加点击事件 el.addEventListener('click', onClick); return el; }; // 添加照片标记到地图 const addPhotoMarkers = (photos, onMarkerClick) => { if (!map.value) return; // 清除现有标记 clearMarkers(); const validPhotos = photos.filter(photo => photo.hasGPS && photo.gps); if (validPhotos.length === 0) { error.value = '没有可显示的照片位置信息'; return; } // 添加新标记 validPhotos.forEach((photo, index) => { if (!photo.gps) return; const markerElement = createPhotoMarker(photo, () => { onMarkerClick(photo, index); }); const marker = new maplibregl.Marker({ element: markerElement }) .setLngLat([photo.gps.longitude, photo.gps.latitude]) .addTo(map.value); markers.value.push(marker); }); // 自动调整地图视图以包含所有标记 fitMapToMarkers(validPhotos); }; // 调整地图视图以包含所有标记 const fitMapToMarkers = (photos) => { if (!map.value || photos.length === 0) return; const coordinates = photos .filter(photo => photo.gps) .map(photo => [photo.gps.longitude, photo.gps.latitude]); if (coordinates.length === 0) return; if (coordinates.length === 1) { // 只有一个标记时,居中显示 map.value.setCenter(coordinates[0]); map.value.setZoom(config.defaultZoom); } else { // 多个标记时,自动适配边界 const bounds = coordinates.reduce((bounds, coord) => { return bounds.extend(coord); }, new maplibregl.LngLatBounds(coordinates[0], coordinates[0])); map.value.fitBounds(bounds, { padding: 50, maxZoom: config.maxZoom - 2 }); } }; // 清除所有标记 const clearMarkers = () => { markers.value.forEach(marker => marker.remove()); markers.value = []; }; // 跳转到指定照片位置 const flyToPhoto = (photo) => { if (!map.value || !photo.gps) return; map.value.flyTo({ center: [photo.gps.longitude, photo.gps.latitude], zoom: Math.max(config.defaultZoom + 2, map.value.getZoom()), duration: 1000 }); }; // 调整地图大小(响应式) const resizeMap = () => { if (map.value) { map.value.resize(); } }; // 更新地图样式 const updateMapStyle = async (newConfig) => { if (!map.value) return; try { console.log('MapLibre: 更新地图样式配置:', newConfig); // 更新配置 Object.assign(config, newConfig); // 生成新的样式URL const newStyleUrl = getStyleUrl(config.mapType, config.mapTilerApiKey); console.log('MapLibre: 新样式URL:', newStyleUrl); // 重置地形设置标志 isTerrainSetupInProgress = false; // 设置新样式 map.value.setStyle(newStyleUrl, { transformStyle: (previousStyle, nextStyle) => { console.log('MapLibre: 动态更新样式转换'); // 根据地图类型设置投影 if (config.mapType === 'satellite' && config.enableTerrain) { nextStyle.projection = { type: 'globe' }; console.log('MapLibre: 设置地球投影'); } // 只有在启用地形时才添加地形相关配置 if (config.enableTerrain) { // 添加地形和山体阴影数据源 nextStyle.sources = { ...nextStyle.sources, terrainSource: { type: 'raster-dem', url: `https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=${config.mapTilerApiKey}`, tileSize: 256 }, hillshadeSource: { type: 'raster-dem', url: `https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=${config.mapTilerApiKey}`, tileSize: 256 } }; // 设置地形 nextStyle.terrain = { source: 'terrainSource', exaggeration: config.terrainExaggeration }; console.log('MapLibre: 添加地形配置'); // 添加山体阴影图层 const hasHillsLayer = nextStyle.layers.some((layer) => layer.id === 'hills'); if (!hasHillsLayer) { nextStyle.layers.push({ id: 'hills', type: 'hillshade', source: 'hillshadeSource', layout: { visibility: 'visible' }, paint: { 'hillshade-shadow-color': '#473B24' } }); console.log('MapLibre: 添加山体阴影图层配置'); } } // 设置天空/大气效果 if (config.mapType === 'satellite' && config.enableAtmosphere) { nextStyle.sky = { 'atmosphere-blend': [ 'interpolate', ['linear'], ['zoom'], 0, 1, 2, 0 ] }; console.log('MapLibre: 添加天空/大气效果配置'); } return nextStyle; } }); } catch (err) { console.error('MapLibre: 更新地图样式失败:', err); } }; // 销毁地图 const destroyMap = () => { if (map.value) { clearMarkers(); map.value.remove(); map.value = undefined; } isLoaded.value = false; error.value = null; }; // 组件销毁时清理资源 onUnmounted(() => { destroyMap(); }); return { mapContainer, map, markers, isLoaded, error, initMap, addPhotoMarkers, clearMarkers, flyToPhoto, resizeMap, destroyMap, updateMapStyle }; }