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> <option value="hybrid">混合地形图</option>
</select> </select>
<label class="text-sm text-gray-600">预报源</label> <label class="text-sm text-gray-600">预报源</label>
<select class="px-2 py-1 border rounded text-sm" [(ngModel)]="provider"> <select class="px-2 py-1 border rounded text-sm" [(ngModel)]="provider">
<option value="">不显示预报</option> <option value="">不显示预报</option>
@ -91,6 +92,17 @@
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" style="position:absolute;top:10px;right:10px;z-index:1001;border-radius:4px;padding:5px 10px;font-size:12px;font-weight:bold;" (click)="toggleMap()">{{ isMapCollapsed ? '展开地图' : '折叠地图' }}</button> <button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" style="position:absolute;top:10px;right:10px;z-index:1001;border-radius:4px;padding:5px 10px;font-size:12px;font-weight:bold;" (click)="toggleMap()">{{ isMapCollapsed ? '展开地图' : '折叠地图' }}</button>
<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 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> </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 *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> <div class="font-bold mb-5 mt-5 text-center">{{ selectedTitle }}</div>

View File

@ -55,6 +55,8 @@ export class AppComponent implements OnInit, AfterViewInit {
private clusterSource: any; private clusterSource: any;
private stationLayer: any; private stationLayer: any;
private clusterLayer: any; private clusterLayer: any;
private kmlLayer: any;
private kmlOverlay: any;
private CLUSTER_THRESHOLD = 10; private CLUSTER_THRESHOLD = 10;
private tileOverlayGroup: any; private tileOverlayGroup: any;
private tileLastList: any[] = []; private tileLastList: any[] = [];
@ -66,6 +68,9 @@ export class AppComponent implements OnInit, AfterViewInit {
tileDt = ''; tileDt = '';
tileProduct: 'none'|'radar'|'rain' = 'radar'; tileProduct: 'none'|'radar'|'rain' = 'radar';
isMapCollapsed = false; isMapCollapsed = false;
kmlInfoTitle = '';
kmlInfoHtml = '';
isKmlDialogOpen = false;
async ngOnInit() { async ngOnInit() {
await Promise.all([this.loadStatus(), this.loadStations()]); 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.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 }); 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.map = new ol.Map({ target: 'map', layers: [
this.layers.satellite, this.layers.satellite,
this.layers.vector, this.layers.vector,
this.layers.terrain, this.layers.terrain,
this.layers.hybrid, this.layers.hybrid,
this.kmlLayer,
this.tileOverlayGroup, this.tileOverlayGroup,
this.clusterLayer, this.clusterLayer,
this.stationLayer this.stationLayer
], view: new ol.View({ center: ol.proj.fromLonLat([108, 35]), zoom: 5, minZoom: 3, maxZoom: 18 }) }); ], view: new ol.View({ center: ol.proj.fromLonLat([108, 35]), zoom: 5, minZoom: 3, maxZoom: 18 }) });
// 使用全屏遮罩的页面级弹窗显示 KML 详情
this.map.getView().on('change:resolution', () => { this.map.getView().on('change:resolution', () => {
const z = this.map.getView().getZoom(); const z = this.map.getView().getZoom();
this.updateClusterDistance(z); this.updateClusterDistance(z);
@ -157,6 +174,29 @@ export class AppComponent implements OnInit, AfterViewInit {
if (this.stations?.length) this.updateStationsOnMap(); 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) { switchLayer(layerType: string) {
const layers = this.layers; if (!layers) return; const layers = this.layers; if (!layers) return;
Object.keys(layers).forEach(key => { if (layers[key].setVisible) layers[key].setVisible(key === layerType); }); 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 // click to select
if (this.map && !this.mapEventsBound) { if (this.map && !this.mapEventsBound) {
this.map.on('singleclick', async (evt:any) => { 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 olAny: any = (window as any).ol;
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 });
if (!features || features.length===0) return; if (!features || features.length===0) return;
@ -244,7 +298,6 @@ export class AppComponent implements OnInit, AfterViewInit {
return; return;
} }
const sid = f.get('stationId'); const sid = f.get('stationId');
const loc = f.get('location') || '';
if (!sid) return; if (!sid) return;
const hex = String(sid).slice(-6).toUpperCase(); const hex = String(sid).slice(-6).toUpperCase();
this.decimalId = hex; this.decimalId = hex;
@ -252,7 +305,7 @@ export class AppComponent implements OnInit, AfterViewInit {
this.scrollToChart(); this.scrollToChart();
}); });
this.map.on('pointermove', (evt:any) => { 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(); const el = this.map.getTargetElement();
if (el) el.style.cursor = (features && features.length>0) ? 'pointer' : ''; if (el) el.style.cursor = (features && features.length>0) ? 'pointer' : '';
this.showTileTooltip(evt); this.showTileTooltip(evt);

File diff suppressed because one or more lines are too long