vuepress-plugin-photomap
Version:
VuePress2插件,用于在文章中插入照片地图组件,类似Apple相册的PhotoMap功能
469 lines (468 loc) • 21.9 kB
JavaScript
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
};
}