feat: 新增地图边界

This commit is contained in:
yarnom 2025-11-14 00:30:52 +08:00
parent 1cc2d5f519
commit 97c0df46bf
3 changed files with 1272 additions and 2 deletions

View File

@ -30,6 +30,7 @@
<option value="hybrid">混合地形图</option>
</select>
<label class="text-sm text-gray-600">预报源</label>
<select class="px-2 py-1 border rounded text-sm" [(ngModel)]="provider">
<option value="">不显示预报</option>
@ -92,6 +93,17 @@
<div id="tileValueTooltip" style="position:absolute;pointer-events:none;z-index:1003;display:none;background:rgba(0,0,0,0.65);color:#fff;font-size:12px;padding:4px 6px;border-radius:4px;"></div>
</div>
<!-- 全屏弹窗KML 详情 -->
<div *ngIf="isKmlDialogOpen" class="fixed inset-0 z-50" style="background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;">
<div class="bg-white rounded shadow-lg" style="width: min(92vw, 720px); max-height: 80vh; overflow: hidden; border: 1px solid #ddd;">
<div class="px-4 py-2 border-b flex items-center justify-between" style="border-color:#eee;">
<div class="font-semibold text-base">{{kmlInfoTitle}}</div>
<button class="text-sm text-gray-600 hover:text-gray-900" (click)="closeKmlPopup()">关闭</button>
</div>
<div class="px-4 py-3 text-sm leading-6" style="overflow:auto; max-height: calc(80vh - 48px);" [innerHTML]="kmlInfoHtml"></div>
</div>
</div>
<div *ngIf="showPanels" id="chartSection" class="border rounded p-3 mb-4" style="border-color:#ddd;">
<div class="font-bold mb-5 mt-5 text-center">{{ selectedTitle }}</div>
<chart-panel [history]="history" [forecast]="forecast" [legendMode]="legendMode"></chart-panel>

View File

@ -55,6 +55,8 @@ export class AppComponent implements OnInit, AfterViewInit {
private clusterSource: any;
private stationLayer: any;
private clusterLayer: any;
private kmlLayer: any;
private kmlOverlay: any;
private CLUSTER_THRESHOLD = 10;
private tileOverlayGroup: any;
private tileLastList: any[] = [];
@ -66,6 +68,9 @@ export class AppComponent implements OnInit, AfterViewInit {
tileDt = '';
tileProduct: 'none'|'radar'|'rain' = 'radar';
isMapCollapsed = false;
kmlInfoTitle = '';
kmlInfoHtml = '';
isKmlDialogOpen = false;
async ngOnInit() {
await Promise.all([this.loadStatus(), this.loadStations()]);
@ -138,16 +143,28 @@ export class AppComponent implements OnInit, AfterViewInit {
this.stationLayer = new ol.layer.Vector({ source: this.stationSource, visible: false, style: (f:any)=> this.createStationStyle(f) });
this.tileOverlayGroup = new ol.layer.Group({ layers: [], zIndex: 999, visible: true });
// Load KML overlay from /static/kml/selected_polygons.kml
try {
const kmlSource = new ol.source.Vector({
url: '/static/kml/selected_polygons.kml',
format: new ol.format.KML({ extractStyles: true })
});
this.kmlLayer = new ol.layer.Vector({ source: kmlSource, zIndex: 800, visible: true });
} catch {}
this.map = new ol.Map({ target: 'map', layers: [
this.layers.satellite,
this.layers.vector,
this.layers.terrain,
this.layers.hybrid,
this.kmlLayer,
this.tileOverlayGroup,
this.clusterLayer,
this.stationLayer
], view: new ol.View({ center: ol.proj.fromLonLat([108, 35]), zoom: 5, minZoom: 3, maxZoom: 18 }) });
// 使用全屏遮罩的页面级弹窗显示 KML 详情
this.map.getView().on('change:resolution', () => {
const z = this.map.getView().getZoom();
this.updateClusterDistance(z);
@ -157,6 +174,29 @@ export class AppComponent implements OnInit, AfterViewInit {
if (this.stations?.length) this.updateStationsOnMap();
}
openKmlPopup(feature: any, coordinate: any) {
try {
const name = feature?.get ? (feature.get('name') || '') : '';
let desc = feature?.get ? (feature.get('description') || '') : '';
// Cleanup KML-wrapped CDATA and decode HTML entities
try {
desc = String(desc);
desc = desc.replace(/^<!\[CDATA\[/, '').replace(/\]\]>$/, '');
const ta = document.createElement('textarea'); ta.innerHTML = desc; desc = ta.value;
} catch {}
this.kmlInfoTitle = String(name || '详情');
this.kmlInfoHtml = String(desc || '');
// 使用页面级模态对话框显示
this.isKmlDialogOpen = true;
} catch {}
}
closeKmlPopup() {
try {
this.isKmlDialogOpen = false;
} catch {}
}
switchLayer(layerType: string) {
const layers = this.layers; if (!layers) return;
Object.keys(layers).forEach(key => { if (layers[key].setVisible) layers[key].setVisible(key === layerType); });
@ -229,6 +269,20 @@ export class AppComponent implements OnInit, AfterViewInit {
// click to select
if (this.map && !this.mapEventsBound) {
this.map.on('singleclick', async (evt:any) => {
// 先尝试命中 KML 要素
try {
let handledKml = false;
this.map.forEachFeatureAtPixel(evt.pixel, (f:any, layer:any) => {
if (this.kmlLayer && layer === this.kmlLayer) {
this.openKmlPopup(f, evt.coordinate);
handledKml = true;
return true;
}
return false;
}, { layerFilter: (l:any)=> l===this.kmlLayer, hitTolerance: 6 });
if (handledKml) return;
} catch {}
// 再处理站点/聚合点击
const olAny: any = (window as any).ol;
const features = this.map.getFeaturesAtPixel(evt.pixel, { layerFilter: (l:any)=> l===this.stationLayer || l===this.clusterLayer });
if (!features || features.length===0) return;
@ -244,7 +298,6 @@ export class AppComponent implements OnInit, AfterViewInit {
return;
}
const sid = f.get('stationId');
const loc = f.get('location') || '';
if (!sid) return;
const hex = String(sid).slice(-6).toUpperCase();
this.decimalId = hex;
@ -252,7 +305,7 @@ export class AppComponent implements OnInit, AfterViewInit {
this.scrollToChart();
});
this.map.on('pointermove', (evt:any) => {
const features = this.map.getFeaturesAtPixel(evt.pixel, { layerFilter: (l:any)=> l===this.stationLayer || l===this.clusterLayer });
const features = this.map.getFeaturesAtPixel(evt.pixel, { layerFilter: (l:any)=> l===this.stationLayer || l===this.clusterLayer || l===this.kmlLayer });
const el = this.map.getTargetElement();
if (el) el.style.cursor = (features && features.length>0) ? 'pointer' : '';
this.showTileTooltip(evt);

File diff suppressed because one or more lines are too long