Files
smart-crop-ui/src/lib/gisMapEngine.ts

594 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
}
}