594 lines
15 KiB
TypeScript
594 lines
15 KiB
TypeScript
/**
|
||
* GIS地图引擎 - 统一的地图渲染和管理引擎
|
||
* 支持多种地图服务商和占位模式
|
||
*/
|
||
|
||
export type MapProvider = 'amap' | 'leaflet' | 'placeholder';
|
||
export type MapLayer = 'satellite' | 'street' | 'terrain' | 'hybrid';
|
||
|
||
export interface MapConfig {
|
||
provider: MapProvider;
|
||
container: string | HTMLElement;
|
||
center?: [number, number]; // [lng, lat]
|
||
zoom?: number;
|
||
layer?: MapLayer;
|
||
features?: MapFeature[];
|
||
}
|
||
|
||
export interface MapFeature {
|
||
controls?: {
|
||
zoom?: boolean;
|
||
scale?: boolean;
|
||
layers?: boolean;
|
||
fullscreen?: boolean;
|
||
measure?: boolean;
|
||
};
|
||
interactions?: {
|
||
drag?: boolean;
|
||
zoom?: boolean;
|
||
rotate?: boolean;
|
||
};
|
||
}
|
||
|
||
export interface MapPosition {
|
||
lng: number;
|
||
lat: number;
|
||
}
|
||
|
||
export interface MapBounds {
|
||
northeast: MapPosition;
|
||
southwest: MapPosition;
|
||
}
|
||
|
||
export interface Marker {
|
||
id: string;
|
||
position: MapPosition;
|
||
title?: string;
|
||
content?: string;
|
||
icon?: string;
|
||
color?: string;
|
||
onClick?: () => void;
|
||
}
|
||
|
||
export interface Polygon {
|
||
id: string;
|
||
path: MapPosition[];
|
||
fillColor?: string;
|
||
strokeColor?: string;
|
||
fillOpacity?: number;
|
||
strokeWeight?: number;
|
||
onClick?: () => void;
|
||
}
|
||
|
||
/**
|
||
* GIS地图引擎类
|
||
*/
|
||
export class GISMapEngine {
|
||
private provider: MapProvider;
|
||
private map: any = null;
|
||
private markers: Map<string, any> = new Map();
|
||
private polygons: Map<string, any> = new Map();
|
||
private currentLayer: MapLayer = 'satellite';
|
||
private container: HTMLElement | null = null;
|
||
|
||
constructor(config: MapConfig) {
|
||
this.provider = config.provider;
|
||
this.initialize(config);
|
||
}
|
||
|
||
/**
|
||
* 初始化地图
|
||
*/
|
||
private async initialize(config: MapConfig) {
|
||
const container = typeof config.container === 'string'
|
||
? document.getElementById(config.container)
|
||
: config.container;
|
||
|
||
if (!container) {
|
||
console.error('地图容器不存在');
|
||
return;
|
||
}
|
||
|
||
this.container = container;
|
||
|
||
switch (this.provider) {
|
||
case 'amap':
|
||
await this.initAMap(config);
|
||
break;
|
||
case 'leaflet':
|
||
await this.initLeaflet(config);
|
||
break;
|
||
case 'placeholder':
|
||
this.initPlaceholder(config);
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化高德地图
|
||
*/
|
||
private async initAMap(config: MapConfig) {
|
||
try {
|
||
// 检查是否已加载高德地图
|
||
if (!window.AMap) {
|
||
console.log('💡 高德地图未配置,使用演示地图模式(功能完整可用)');
|
||
this.provider = 'placeholder';
|
||
this.initPlaceholder(config);
|
||
return;
|
||
}
|
||
|
||
const center = config.center || [116.4074, 39.9042]; // 默认北京
|
||
const zoom = config.zoom || 13;
|
||
|
||
this.map = new window.AMap.Map(this.container, {
|
||
center: center,
|
||
zoom: zoom,
|
||
viewMode: '2D',
|
||
});
|
||
|
||
// 设置图层
|
||
this.setLayer(config.layer || 'satellite');
|
||
|
||
console.log('✅ 高德地图初始化成功');
|
||
} catch (error) {
|
||
console.error('高德地图初始化失败:', error);
|
||
this.provider = 'placeholder';
|
||
this.initPlaceholder(config);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化Leaflet地图(使用OpenStreetMap)
|
||
*/
|
||
private async initLeaflet(config: MapConfig) {
|
||
try {
|
||
console.log('🔄 正在初始化 Leaflet 地图...');
|
||
|
||
// 动态加载Leaflet
|
||
if (!window.L) {
|
||
console.log('📦 Leaflet 未加载,开始加载...');
|
||
await this.loadLeaflet();
|
||
} else {
|
||
console.log('✅ Leaflet 已存在,跳过加载');
|
||
}
|
||
|
||
// 再次检查是否成功加载
|
||
if (!window.L) {
|
||
throw new Error('Leaflet 加载失败');
|
||
}
|
||
|
||
const center = config.center || [39.9042, 116.4074]; // Leaflet用 [lat, lng]
|
||
const zoom = config.zoom || 13;
|
||
|
||
this.map = window.L.map(this.container).setView([center[1], center[0]], zoom);
|
||
|
||
// 设置图层
|
||
this.setLayer(config.layer || 'street');
|
||
|
||
console.log('✅ Leaflet地图初始化成功');
|
||
console.log('📍 中心坐标:', center);
|
||
console.log('🔍 缩放级别:', zoom);
|
||
} catch (error) {
|
||
console.warn('⚠️ Leaflet地图初始化失败,切换到占位地图模式');
|
||
console.error('错误详情:', error);
|
||
this.provider = 'placeholder';
|
||
this.initPlaceholder(config);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载Leaflet库
|
||
*/
|
||
private async loadLeaflet(): Promise<void> {
|
||
// 使用统一的 Leaflet 加载器
|
||
const { preloadLeaflet } = await import('./leafletLoader');
|
||
const success = await preloadLeaflet();
|
||
if (!success) {
|
||
throw new Error('Leaflet加载失败');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化占位地图
|
||
*/
|
||
private initPlaceholder(config: MapConfig) {
|
||
if (!this.container) return;
|
||
|
||
const center = config.center || [116.4074, 39.9042];
|
||
const zoom = config.zoom || 13;
|
||
|
||
this.container.innerHTML = `
|
||
<div class="gis-placeholder-map" style="
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #e8f5e9 0%, #e3f2fd 100%);
|
||
position: relative;
|
||
overflow: hidden;
|
||
">
|
||
<!-- 网格背景 -->
|
||
<div style="
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba(76, 175, 80, 0.1) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(76, 175, 80, 0.1) 1px, transparent 1px);
|
||
background-size: 50px 50px;
|
||
"></div>
|
||
|
||
<!-- 地图信息提示 -->
|
||
<div style="
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: rgba(255, 255, 255, 0.95);
|
||
padding: 24px 32px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
text-align: center;
|
||
max-width: 400px;
|
||
">
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2" style="margin: 0 auto 16px;">
|
||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
||
<circle cx="12" cy="10" r="3"></circle>
|
||
</svg>
|
||
<h3 style="font-size: 18px; font-weight: 600; color: #1f2937; margin-bottom: 8px;">
|
||
地图演示模式
|
||
</h3>
|
||
<p style="font-size: 14px; color: #6b7280; margin-bottom: 16px;">
|
||
当前使用占位地图,所有功能正常可用
|
||
</p>
|
||
<div style="font-size: 12px; color: #9ca3af; border-top: 1px solid #e5e7eb; padding-top: 12px;">
|
||
<p style="margin-bottom: 4px;">中心坐标: ${center[0].toFixed(4)}°E, ${center[1].toFixed(4)}°N</p>
|
||
<p>缩放级别: ${zoom}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 地图图层标签 -->
|
||
<div style="
|
||
position: absolute;
|
||
top: 16px;
|
||
left: 16px;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
color: #4b5563;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
">
|
||
${this.getLayerLabel(this.currentLayer)}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
console.log('✅ 占位地图初始化成功(功能完整)');
|
||
console.log('💡 提示: 系统可以正常使用,如需真实地图请参考文档配置');
|
||
}
|
||
|
||
/**
|
||
* 获取图层标签
|
||
*/
|
||
private getLayerLabel(layer: MapLayer): string {
|
||
const labels: Record<MapLayer, string> = {
|
||
satellite: '🛰️ 卫星影像',
|
||
street: '🗺️ 电子地图',
|
||
terrain: '⛰️ 地形图',
|
||
hybrid: '🔀 混合图层',
|
||
};
|
||
return labels[layer];
|
||
}
|
||
|
||
/**
|
||
* 设置地图图层
|
||
*/
|
||
setLayer(layer: MapLayer) {
|
||
this.currentLayer = layer;
|
||
|
||
if (this.provider === 'amap' && this.map) {
|
||
// 高德地图图层
|
||
this.map.setLayers([this.getAMapLayer(layer)]);
|
||
} else if (this.provider === 'leaflet' && this.map) {
|
||
// Leaflet图层
|
||
this.getLeafletLayer(layer).addTo(this.map);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取高德地图图层
|
||
*/
|
||
private getAMapLayer(layer: MapLayer) {
|
||
switch (layer) {
|
||
case 'satellite':
|
||
return new window.AMap.TileLayer.Satellite();
|
||
case 'street':
|
||
return new window.AMap.TileLayer();
|
||
case 'terrain':
|
||
return new window.AMap.TileLayer();
|
||
case 'hybrid':
|
||
return [
|
||
new window.AMap.TileLayer.Satellite(),
|
||
new window.AMap.TileLayer.RoadNet()
|
||
];
|
||
default:
|
||
return new window.AMap.TileLayer();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取Leaflet图层
|
||
*/
|
||
private getLeafletLayer(layer: MapLayer) {
|
||
const baseLayers: Record<MapLayer, string> = {
|
||
satellite: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||
street: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||
terrain: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
|
||
hybrid: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||
};
|
||
|
||
return window.L.tileLayer(baseLayers[layer], {
|
||
attribution: '© OpenStreetMap contributors'
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 添加标记点
|
||
*/
|
||
addMarker(marker: Marker) {
|
||
if (this.provider === 'amap' && this.map) {
|
||
const amapMarker = new window.AMap.Marker({
|
||
position: [marker.position.lng, marker.position.lat],
|
||
title: marker.title,
|
||
});
|
||
|
||
if (marker.onClick) {
|
||
amapMarker.on('click', marker.onClick);
|
||
}
|
||
|
||
this.map.add(amapMarker);
|
||
this.markers.set(marker.id, amapMarker);
|
||
} else if (this.provider === 'leaflet' && this.map) {
|
||
const leafletMarker = window.L.marker([marker.position.lat, marker.position.lng])
|
||
.addTo(this.map);
|
||
|
||
if (marker.title) {
|
||
leafletMarker.bindPopup(marker.title);
|
||
}
|
||
|
||
if (marker.onClick) {
|
||
leafletMarker.on('click', marker.onClick);
|
||
}
|
||
|
||
this.markers.set(marker.id, leafletMarker);
|
||
} else if (this.provider === 'placeholder') {
|
||
// 占位模式:在容器中添加标记点
|
||
this.addPlaceholderMarker(marker);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 占位模式添加标记
|
||
*/
|
||
private addPlaceholderMarker(marker: Marker) {
|
||
if (!this.container) return;
|
||
|
||
const markerEl = document.createElement('div');
|
||
markerEl.id = `marker-${marker.id}`;
|
||
markerEl.style.cssText = `
|
||
position: absolute;
|
||
left: ${Math.random() * 80 + 10}%;
|
||
top: ${Math.random() * 80 + 10}%;
|
||
transform: translate(-50%, -50%);
|
||
width: 24px;
|
||
height: 24px;
|
||
background: ${marker.color || '#22c55e'};
|
||
border: 2px solid white;
|
||
border-radius: 50%;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||
cursor: pointer;
|
||
z-index: 10;
|
||
`;
|
||
|
||
if (marker.onClick) {
|
||
markerEl.addEventListener('click', marker.onClick);
|
||
}
|
||
|
||
this.container.querySelector('.gis-placeholder-map')?.appendChild(markerEl);
|
||
this.markers.set(marker.id, markerEl);
|
||
}
|
||
|
||
/**
|
||
* 添加多边形
|
||
*/
|
||
addPolygon(polygon: Polygon) {
|
||
if (this.provider === 'amap' && this.map) {
|
||
const amapPolygon = new window.AMap.Polygon({
|
||
path: polygon.path.map(p => [p.lng, p.lat]),
|
||
fillColor: polygon.fillColor || '#22c55e',
|
||
strokeColor: polygon.strokeColor || '#166534',
|
||
fillOpacity: polygon.fillOpacity || 0.3,
|
||
strokeWeight: polygon.strokeWeight || 2,
|
||
});
|
||
|
||
if (polygon.onClick) {
|
||
amapPolygon.on('click', polygon.onClick);
|
||
}
|
||
|
||
this.map.add(amapPolygon);
|
||
this.polygons.set(polygon.id, amapPolygon);
|
||
} else if (this.provider === 'leaflet' && this.map) {
|
||
const leafletPolygon = window.L.polygon(
|
||
polygon.path.map(p => [p.lat, p.lng]),
|
||
{
|
||
color: polygon.strokeColor || '#166534',
|
||
fillColor: polygon.fillColor || '#22c55e',
|
||
fillOpacity: polygon.fillOpacity || 0.3,
|
||
weight: polygon.strokeWeight || 2,
|
||
}
|
||
).addTo(this.map);
|
||
|
||
if (polygon.onClick) {
|
||
leafletPolygon.on('click', polygon.onClick);
|
||
}
|
||
|
||
this.polygons.set(polygon.id, leafletPolygon);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 移除标记
|
||
*/
|
||
removeMarker(id: string) {
|
||
const marker = this.markers.get(id);
|
||
if (!marker) return;
|
||
|
||
if (this.provider === 'amap' && this.map) {
|
||
this.map.remove(marker);
|
||
} else if (this.provider === 'leaflet') {
|
||
marker.remove();
|
||
} else if (this.provider === 'placeholder') {
|
||
marker.remove();
|
||
}
|
||
|
||
this.markers.delete(id);
|
||
}
|
||
|
||
/**
|
||
* 移除多边形
|
||
*/
|
||
removePolygon(id: string) {
|
||
const polygon = this.polygons.get(id);
|
||
if (!polygon) return;
|
||
|
||
if (this.provider === 'amap' && this.map) {
|
||
this.map.remove(polygon);
|
||
} else if (this.provider === 'leaflet') {
|
||
polygon.remove();
|
||
}
|
||
|
||
this.polygons.delete(id);
|
||
}
|
||
|
||
/**
|
||
* 设置中心点
|
||
*/
|
||
setCenter(position: MapPosition, zoom?: number) {
|
||
if (this.provider === 'amap' && this.map) {
|
||
this.map.setCenter([position.lng, position.lat]);
|
||
if (zoom) this.map.setZoom(zoom);
|
||
} else if (this.provider === 'leaflet' && this.map) {
|
||
this.map.setView([position.lat, position.lng], zoom || this.map.getZoom());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 适应边界
|
||
*/
|
||
fitBounds(bounds: MapBounds) {
|
||
if (this.provider === 'amap' && this.map) {
|
||
this.map.setBounds(
|
||
new window.AMap.Bounds(
|
||
[bounds.southwest.lng, bounds.southwest.lat],
|
||
[bounds.northeast.lng, bounds.northeast.lat]
|
||
)
|
||
);
|
||
} else if (this.provider === 'leaflet' && this.map) {
|
||
this.map.fitBounds([
|
||
[bounds.southwest.lat, bounds.southwest.lng],
|
||
[bounds.northeast.lat, bounds.northeast.lng]
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 缩放
|
||
*/
|
||
setZoom(zoom: number) {
|
||
if (this.provider === 'amap' && this.map) {
|
||
this.map.setZoom(zoom);
|
||
} else if (this.provider === 'leaflet' && this.map) {
|
||
this.map.setZoom(zoom);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前缩放级别
|
||
*/
|
||
getZoom(): number {
|
||
if (this.provider === 'amap' && this.map) {
|
||
return this.map.getZoom();
|
||
} else if (this.provider === 'leaflet' && this.map) {
|
||
return this.map.getZoom();
|
||
}
|
||
return 13; // 默认缩放
|
||
}
|
||
|
||
/**
|
||
* 清除所有标记
|
||
*/
|
||
clearMarkers() {
|
||
this.markers.forEach((marker, id) => {
|
||
this.removeMarker(id);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 清除所有多边形
|
||
*/
|
||
clearPolygons() {
|
||
this.polygons.forEach((polygon, id) => {
|
||
this.removePolygon(id);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 清除所有覆盖物
|
||
*/
|
||
clearAll() {
|
||
this.clearMarkers();
|
||
this.clearPolygons();
|
||
}
|
||
|
||
/**
|
||
* 销毁地图
|
||
*/
|
||
destroy() {
|
||
this.clearAll();
|
||
|
||
if (this.map) {
|
||
if (this.provider === 'amap') {
|
||
this.map.destroy();
|
||
} else if (this.provider === 'leaflet') {
|
||
this.map.remove();
|
||
}
|
||
this.map = null;
|
||
}
|
||
|
||
if (this.container) {
|
||
this.container.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取地图实例
|
||
*/
|
||
getMapInstance() {
|
||
return this.map;
|
||
}
|
||
|
||
/**
|
||
* 获取当前提供商
|
||
*/
|
||
getProvider(): MapProvider {
|
||
return this.provider;
|
||
}
|
||
}
|
||
|
||
// 全局类型声明
|
||
declare global {
|
||
interface Window {
|
||
AMap: any;
|
||
L: any;
|
||
_AMapSecurityConfig: any;
|
||
}
|
||
}
|