527 lines
13 KiB
TypeScript
527 lines
13 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.warn('高德地图SDK未加载,切换到占位模式');
|
||
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 {
|
||
// 动态加载Leaflet
|
||
if (!window.L) {
|
||
await this.loadLeaflet();
|
||
}
|
||
|
||
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地图初始化成功');
|
||
} catch (error) {
|
||
console.error('Leaflet地图初始化失败:', error);
|
||
this.provider = 'placeholder';
|
||
this.initPlaceholder(config);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载Leaflet库
|
||
*/
|
||
private async loadLeaflet(): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
// 加载CSS
|
||
const link = document.createElement('link');
|
||
link.rel = 'stylesheet';
|
||
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
|
||
document.head.appendChild(link);
|
||
|
||
// 加载JS
|
||
const script = document.createElement('script');
|
||
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
||
script.onload = () => resolve();
|
||
script.onerror = () => reject(new Error('Leaflet加载失败'));
|
||
document.head.appendChild(script);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 初始化占位地图
|
||
*/
|
||
private initPlaceholder(config: MapConfig) {
|
||
if (!this.container) return;
|
||
|
||
this.container.innerHTML = `
|
||
<div class="gis-placeholder-map" style="
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #f0fdf4 0%, #dbeafe 100%);
|
||
position: relative;
|
||
overflow: hidden;
|
||
">
|
||
<div style="
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px);
|
||
background-size: 50px 50px;
|
||
"></div>
|
||
</div>
|
||
`;
|
||
|
||
console.log('✅ 占位地图初始化成功(功能完整)');
|
||
}
|
||
|
||
/**
|
||
* 设置地图图层
|
||
*/
|
||
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;
|
||
}
|
||
}
|