diff --git a/frontend/src/api/ecoFlow/index.ts b/frontend/src/api/ecoFlow/index.ts index b8673bc3..97e7771b 100644 --- a/frontend/src/api/ecoFlow/index.ts +++ b/frontend/src/api/ecoFlow/index.ts @@ -1,22 +1,12 @@ import request from '@/utils/request'; import type { EcoFlowStandard, EcoFlowQueryParams } from './types'; -/** - * 获取生态流量达标情况数据 - * @param params 查询参数 - */ -export function getEcoFlowStandardData(params?: EcoFlowQueryParams): Promise<{ data: EcoFlowStandard[] }> { - return request({ - url: '/api/eco-flow/standard', - method: 'get', - params - }); -} - -export function getQgcStaticData(params?: EcoFlowQueryParams): Promise<{ data: EcoFlowStandard[] }> { - return request({ - url: '/eng/eq/interval/qgc/getQgcStaticData', - method: 'post', - data:params - }); +export function getQgcStaticData( + params?: EcoFlowQueryParams +): Promise<{ data: EcoFlowStandard[] }> { + return request({ + url: '/eng/eq/interval/qgc/getQgcStaticData', + method: 'post', + data: params + }); } diff --git a/frontend/src/api/home/index.ts b/frontend/src/api/home/index.ts index 6809e3f2..1f2ac831 100644 --- a/frontend/src/api/home/index.ts +++ b/frontend/src/api/home/index.ts @@ -88,30 +88,3 @@ export function baseMsstbprptGetKendoList(data: any) { data: data }); } - -// 降水量实时数据 -export function getBaseStbprpDataPPTNR(data: any) { - return request({ - url: '/api/dec-lygk-base-server/base/stbprpData/GetKendoListCust?tbCode=PPTN_R', - method: 'post', - data: data - }); -} - -// 气温气压实时数据 -export function getBaseStbprpDataTMPR(data: any) { - return request({ - url: '/api/dec-lygk-base-server/base/stbprpData/GetKendoListCust?tbCode=TMP_R', - method: 'post', - data: data - }); -} - -// 风速风向实时数据 -export function getBaseStbprpDataWNDR(data: any) { - return request({ - url: '/api/dec-lygk-base-server/base/stbprpData/GetKendoListCust?tbCode=WND_R', - method: 'post', - data: data - }); -} \ No newline at end of file diff --git a/frontend/src/api/map.ts b/frontend/src/api/map.ts index e3ac73e6..35ad43ea 100644 --- a/frontend/src/api/map.ts +++ b/frontend/src/api/map.ts @@ -3,9 +3,7 @@ import request from '@/utils/request'; // 获取图例列表 export function getModuleMapLegendList(params?: { moduleId?: string }) { const url = params?.moduleId - ? // ? `/api/wmp-sys-server/mapLegend/getModuleMapLegendList?moduleId=${params.moduleId}` - // : '/api/wmp-sys-server/mapLegend/getModuleMapLegendList'; - `/mapLegend/getModuleMapLegendList?moduleId=${params.moduleId}` + ? `/mapLegend/getModuleMapLegendList?moduleId=${params.moduleId}` : '/mapLegend/getModuleMapLegendList'; return request({ url, @@ -15,7 +13,6 @@ export function getModuleMapLegendList(params?: { moduleId?: string }) { // 获取地图配置列表 export function getMapList(data: any) { return request({ - // url: '/api/wmp-sys-server/mapmodule/getMapData', url: '/mapmodule/getMapData', method: 'post', data @@ -24,7 +21,7 @@ export function getMapList(data: any) { // 获取梯级流域地图 export function getQgcRvcd(data: any) { return request({ - url: '/api/wmp-eng-server/eng/rsvrcscdb/getQgcRvcd', + url: '/eng/base/rsvrcscdb/getQgcRvcd', method: 'post', data }); @@ -32,7 +29,7 @@ export function getQgcRvcd(data: any) { // 获取梯级流域下拉框列表 export function getRvcdList(data: any) { return request({ - url: '/api/wmp-eng-server/eng/rsvrcscdb/rvcd', + url: '/eng/base/rsvrcscdb/rvcd', method: 'post', data }); @@ -41,7 +38,7 @@ export function getRvcdList(data: any) { // 获取梯级流域下拉框图表数据 export function getKendoList(data: any) { return request({ - url: '/api/wmp-eng-server/eng/rsvrcscdb/GetKendoList', + url: '/eng/base/rsvrcscdb/GetKendoList', method: 'post', data }); diff --git a/frontend/src/api/yldc/index.ts b/frontend/src/api/yldc/index.ts index 3282a2ac..dd5a998c 100644 --- a/frontend/src/api/yldc/index.ts +++ b/frontend/src/api/yldc/index.ts @@ -38,7 +38,7 @@ export function getFprdDataInfo(data: any) { data }); } -// +// export function sdFprdRGetKendoListCust(data: any) { return request({ url: '/fpr/sdFprdR/GetKendoListCust', @@ -46,7 +46,7 @@ export function sdFprdRGetKendoListCust(data: any) { data }); } -//装置名称 /api/dec-lygk-base-server/base/vmsstbprpt/GetKendoList +//装置名称 export function vmsstbprptGetKendoList(data: any) { return request({ url: '/fpr/vmsstbprpt/GetKendoList', @@ -54,18 +54,21 @@ export function vmsstbprptGetKendoList(data: any) { data }); } -//鱼种类 /api/wmp-env-server/sdFprdR/getFishInfo +//鱼种类 export function sdFprdRgetFishInfo() { return request({ url: '/fpr/sdFprdR/getFishInfo', - method: 'post', + method: 'post' }); } //获取鱼类变化信息 -export function sdFprdRGetFtpChangeInfo(data: { baseId: string; year: string }) { +export function sdFprdRGetFtpChangeInfo(data: { + baseId: string; + year: string; +}) { return request({ url: '/fpr/sdFprdR/getFtpChangeInfo', method: 'post', data }); -} \ No newline at end of file +} diff --git a/frontend/src/api/zngj/index.ts b/frontend/src/api/zngj/index.ts index b4780180..bc8833c0 100644 --- a/frontend/src/api/zngj/index.ts +++ b/frontend/src/api/zngj/index.ts @@ -8,8 +8,8 @@ export function getAisbdbyxList(data: any) { data }); } - -// AI识别大坝环境-获取饼图+表格数据 /api/wmp-env-server/env/ai/com/GetKendoListCust + +// AI识别大坝环境-获取饼图+表格数据 export function getAiChartTable(data: any) { return request({ url: '/warn/ai/com/GetKendoListCust', @@ -18,7 +18,7 @@ export function getAiChartTable(data: any) { }); } -// AI识别大坝环境-获取电站列表 +// AI识别大坝环境-获取电站列表 export function getAiRstcd(data: any) { return request({ url: '/warn/ai/com/qgc/aiRstcd/GetKendoListCust', diff --git a/frontend/src/api/zxdzw/index.ts b/frontend/src/api/zxdzw/index.ts index 2780d6f6..310ee4a9 100644 --- a/frontend/src/api/zxdzw/index.ts +++ b/frontend/src/api/zxdzw/index.ts @@ -1,6 +1,5 @@ import request from '@/utils/request'; - export function vpcGetKendoListCust(data: any) { return request({ url: '/vap/vpc/GetKendoListCust', @@ -17,7 +16,7 @@ export function vmsstbprptGetKendoList(data: any) { data }); } -// 动物救助情况 +// 动物救助情况 export function wvaGetKendoListCust(data: any) { return request({ url: '/vap/wva/GetKendoListCust', @@ -25,11 +24,3 @@ export function wvaGetKendoListCust(data: any) { data }); } -//植物园情况介绍 -// export function vpcGetKendoListCust(data: any) { -// return request({ -// url: '/api/wmp-env-server/env/vpc/GetKendoListCust', -// method: 'post', -// data -// }); -// } \ No newline at end of file diff --git a/frontend/src/components/gis/GisView.vue b/frontend/src/components/gis/GisView.vue index 07de2437..60ea9a73 100644 --- a/frontend/src/components/gis/GisView.vue +++ b/frontend/src/components/gis/GisView.vue @@ -80,7 +80,6 @@ const init = async () => { // 备注:地图控制器只分发简单视图切换命令,避免在页面入口堆叠复杂业务逻辑。 const handleMapController = async (e: any, mapType: any) => { - console.log(e, mapType); switch (e) { case 'dim': await mapClass.switchView(mapType); @@ -106,7 +105,6 @@ const fetchTjData = async () => { for (let i = 0; i < res.data.data.length; i++) { datas.push(res.data.data[i].rvcd); } - console.log(servers.Tertiarybasin); mapClass.addTertiarybasinLayer( servers.Tertiarybasin, '#4DFFDD', @@ -119,9 +117,9 @@ const fetchTjData = async () => { // 备注:页面切换后仅触发编排器重载页面,不在组件内继续拼装业务联动。 watch( () => pageKey.value, - async newVal => { + newVal => { if (newVal && mapIsInited.value) { - await mapOrchestrator.handlePageChange(newVal, isShuiDianKaiFaMenu.value); + void mapOrchestrator.handlePageChange(newVal, isShuiDianKaiFaMenu.value); } } ); diff --git a/frontend/src/components/gis/gisUtils.ts b/frontend/src/components/gis/gisUtils.ts index f93131a6..1776cc85 100644 --- a/frontend/src/components/gis/gisUtils.ts +++ b/frontend/src/components/gis/gisUtils.ts @@ -19,7 +19,6 @@ declare global { __mapMode?: string; // 建议同时声明代码中用到的其他全局变量 } } -console.log(import.meta.env.VITE_APP_ATTACHMENT_URL); /** * 根据镜头高度获取地图级别 @@ -531,9 +530,7 @@ export const mapOutPut = (imageUrl: string) => { downloadElement.click(); }; }) - .catch((e: any) => { - console.log('e', e); - }); + .catch(() => {}); } else { downloadElement.href = canvas.toDataURL('image/png'); downloadElement.download = 'download'; diff --git a/frontend/src/components/gis/map.cesium.ts b/frontend/src/components/gis/map.cesium.ts index 073a64c5..2229d24a 100644 --- a/frontend/src/components/gis/map.cesium.ts +++ b/frontend/src/components/gis/map.cesium.ts @@ -196,7 +196,6 @@ export class MapCesium implements MapInterface { // 清理临时元素 document.body.removeChild(link); - console.log('Map image exported successfully:', fileName); } catch (error) { console.error('Failed to export map image:', error); if (error instanceof DOMException && error.name === 'SecurityError') { diff --git a/frontend/src/components/gis/map.class.ts b/frontend/src/components/gis/map.class.ts index be4caa56..c0eada73 100644 --- a/frontend/src/components/gis/map.class.ts +++ b/frontend/src/components/gis/map.class.ts @@ -33,7 +33,6 @@ export class MapClass implements MapClassInterface { this.service = new MapOl(); } static getInstance(): MapClass { - console.log('getInstance'); if (!this.instance) { this.instance = new MapClass(); } @@ -41,7 +40,6 @@ export class MapClass implements MapClassInterface { } // 地图初始化 init(container: HTMLElement, rectangle?: any): Promise { - console.log(container, rectangle); return this.service.init(container, rectangle).then(map => { this.view = map; return map; @@ -176,7 +174,6 @@ export class MapClass implements MapClassInterface { // 2. 根据类型初始化新的地图服务 if (type === '2D') { - console.log('Switching to 2D (OpenLayers)'); this.service = new MapOl(); this.service @@ -187,7 +184,6 @@ export class MapClass implements MapClassInterface { }) .catch(err => reject(err)); } else if (type === '3D') { - console.log('Switching to 3D (Cesium)'); this.service = new MapCesium(); this.service @@ -261,12 +257,7 @@ export class MapClass implements MapClassInterface { // }, 5000) // }) // } - } catch (e) { - console.log( - '%c switchView: ', - 'color: MidnightBlue; background: Aquamarine; font-size: 20px;', - e - ); + } catch { reject(); } }); diff --git a/frontend/src/components/gis/map.ol.ts b/frontend/src/components/gis/map.ol.ts index eb752d40..c5aa5d7b 100644 --- a/frontend/src/components/gis/map.ol.ts +++ b/frontend/src/components/gis/map.ol.ts @@ -60,6 +60,7 @@ export class MapOl implements MapInterface { view: View | null = null; private layerRegistry: Map = new Map(); private iconLoadState = new Map(); + private pointStyleCache = new Map(); private baseLayerConfig: any | null = null; private hydropBaseConfig: any | null = null; private REGISTRY_KEY = 'customBaseLayer'; @@ -79,7 +80,9 @@ export class MapOl implements MapInterface { private regionMaskManager: RegionMaskManager; private isBatchPopupMode = false; private batchPopupRefreshFrameId: number | null = null; + private pointLayerStyleRefreshFrameId: number | null = null; private labelVisibilityRefreshFrameId: number | null = null; + private labelVisibilityDebounceTimerId: number | null = null; constructor() { this.pointLayerManager = new PointLayerManager({ map: null, @@ -201,9 +204,9 @@ export class MapOl implements MapInterface { } }); this.map.on('moveend', () => { - this.requestRefreshPointLabelVisibility(true); + this.requestRefreshPointLabelVisibility(false, 60); if (this.isBatchPopupMode) { - this.requestUpdateBatchPopups(true); + this.requestUpdateBatchPopups(); } }); @@ -233,7 +236,7 @@ export class MapOl implements MapInterface { const currentZoom = this.view ? this.view.getZoom() : INITIAL_ZOOM; this.pointLayerManager.updateNearbyFeatureLayout(currentZoom); - this.requestRefreshPointLabelVisibility(true); + this.requestRefreshPointLabelVisibility(false, 80); } /** * 初始化 Popup Overlay @@ -334,10 +337,15 @@ export class MapOl implements MapInterface { } const currentZoom: any = this.view ? this.view.getZoom() : 4.5; + const cachedDensityVisible = feature.get('_densityVisible'); + const densityVisible = + typeof cachedDensityVisible === 'boolean' + ? cachedDensityVisible + : currentZoom >= this.getFeatureDensityMinZoom(feature); const iconTargetVisible = legendVisible !== false && regionVisible !== false && - this.shouldRenderFeatureByDensity(feature, currentZoom) && + densityVisible && this.shouldRenderNearbyFeature(feature, currentZoom); const dynamicScale = this.getDynamicPointScale(currentZoom); @@ -361,8 +369,20 @@ export class MapOl implements MapInterface { return null; } - const styles: Style[] = []; - styles.push( + const styleCacheKey = [ + iconUrl, + dynamicScale.toFixed(2), + fontSize, + labelOffsetY, + labelTargetVisible ? formattedLabelText : '', + labelTargetVisible ? 1 : 0 + ].join('|'); + const cachedStyles = this.pointStyleCache.get(styleCacheKey); + if (cachedStyles) { + return cachedStyles; + } + + const styles: Style[] = [ new Style({ image: new Icon({ src: iconUrl, @@ -372,7 +392,7 @@ export class MapOl implements MapInterface { declutterMode: 'none' }) }) - ); + ]; if (formattedLabelText && labelTargetVisible) { styles.push( @@ -394,9 +414,170 @@ export class MapOl implements MapInterface { ); } + this.pointStyleCache.set(styleCacheKey, styles); return styles; } + private markFeatureVisibilityDirty( + feature: Feature, + key: '_densityVisible' | '_iconCollisionVisible' | '_labelCollisionVisible', + visible: boolean, + dirtyLayerKeys?: Set + ) { + if (feature.get(key) === visible) { + return; + } + + feature.set(key, visible, true); + const layerKey = feature.get('_layerKey'); + if (dirtyLayerKeys && layerKey) { + dirtyLayerKeys.add(String(layerKey)); + } + } + + private flushDirtyPointLayers(dirtyLayerKeys: Set) { + dirtyLayerKeys.forEach(layerKey => { + const layer = this.pointLayerManager.getLayer(layerKey); + layer?.changed(); + }); + } + + private syncFeatureDensityVisible( + feature: Feature, + visible: boolean, + dirtyLayerKeys?: Set + ) { + this.markFeatureVisibilityDirty( + feature, + '_densityVisible', + visible, + dirtyLayerKeys + ); + } + + private getFeaturePixelGridKey( + layerKey: string, + pixelX: number, + pixelY: number, + cellSize: number + ) { + return `${layerKey}:${Math.floor(pixelX / cellSize)}:${Math.floor( + pixelY / cellSize + )}`; + } + + private hasNearbyViewportFeature( + target: { + feature: Feature; + layerKey: string; + pixelX: number; + pixelY: number; + }, + pixelGrid: Map< + string, + Array<{ + feature: Feature; + layerKey: string; + pixelX: number; + pixelY: number; + }> + >, + threshold: number + ): boolean { + const cellX = Math.floor(target.pixelX / threshold); + const cellY = Math.floor(target.pixelY / threshold); + + for (let offsetX = -1; offsetX <= 1; offsetX += 1) { + for (let offsetY = -1; offsetY <= 1; offsetY += 1) { + const bucket = pixelGrid.get( + `${target.layerKey}:${cellX + offsetX}:${cellY + offsetY}` + ); + if (!bucket?.length) { + continue; + } + + for (const candidate of bucket) { + if (candidate.feature === target.feature) { + continue; + } + if ( + Math.hypot( + candidate.pixelX - target.pixelX, + candidate.pixelY - target.pixelY + ) <= threshold + ) { + return true; + } + } + } + } + + return false; + } + + private getCollisionGridKeys( + rect: { left: number; right: number; top: number; bottom: number }, + cellSize: number + ): string[] { + const startX = Math.floor(rect.left / cellSize); + const endX = Math.floor(rect.right / cellSize); + const startY = Math.floor(rect.top / cellSize); + const endY = Math.floor(rect.bottom / cellSize); + const keys: string[] = []; + + for (let x = startX; x <= endX; x += 1) { + for (let y = startY; y <= endY; y += 1) { + keys.push(`${x}:${y}`); + } + } + + return keys; + } + + private addRectToCollisionGrid< + T extends { + left: number; + right: number; + top: number; + bottom: number; + } + >(rect: T, grid: Map, cellSize: number) { + this.getCollisionGridKeys(rect, cellSize).forEach(key => { + const bucket = grid.get(key) || []; + bucket.push(rect); + grid.set(key, bucket); + }); + } + + private findCollidingRectInGrid< + T extends { + left: number; + right: number; + top: number; + bottom: number; + } + >( + rect: { left: number; right: number; top: number; bottom: number }, + grid: Map, + cellSize: number, + isCollision: (candidate: T) => boolean + ): T | undefined { + for (const key of this.getCollisionGridKeys(rect, cellSize)) { + const bucket = grid.get(key); + if (!bucket?.length) { + continue; + } + + for (const candidate of bucket) { + if (isCollision(candidate)) { + return candidate; + } + } + } + + return undefined; + } + private refreshPointLabelVisibility() { if (!this.map || !this.view) return; @@ -442,64 +623,185 @@ export class MapOl implements MapInterface { }> = []; const viewportFeatures = this.pointLayerManager.getFeaturesInViewport(); + const dirtyLayerKeys = new Set(); + const densityIsolationThreshold = 56; + const densityPixelGrid = new Map< + string, + Array<{ + feature: Feature; + layerKey: string; + pixelX: number; + pixelY: number; + }> + >(); + const viewportEntries: Array<{ + feature: Feature; + layerKey: string; + iconUrl: string; + legendVisible: boolean; + regionVisible: boolean; + densityPriority: number; + nearbyVisible: boolean; + labelText: string; + pixelX?: number; + pixelY?: number; + }> = []; + + viewportFeatures.forEach(feature => { + const geometry = feature.getGeometry(); + const pixel = + geometry && geometry.getType() === 'Point' + ? this.map?.getPixelFromCoordinate( + (geometry as Point).getCoordinates() + ) + : null; + const layerKey = String(feature.get('_layerKey') || ''); + const entry = { + feature, + layerKey, + iconUrl: String(feature.get('_iconUrl') || ''), + legendVisible: feature.get('_legendVisible') !== false, + regionVisible: feature.get('_regionVisible') !== false, + densityPriority: this.getFeatureDensityPriority(feature), + nearbyVisible: this.shouldRenderNearbyFeature(feature, currentZoom), + labelText: this.formatPointLabelText( + feature.get('_labelText') as string + ), + pixelX: pixel?.[0], + pixelY: pixel?.[1] + }; + viewportEntries.push(entry); + + if ( + pixel && + entry.layerKey && + entry.legendVisible && + entry.regionVisible + ) { + const gridKey = this.getFeaturePixelGridKey( + entry.layerKey, + pixel[0], + pixel[1], + densityIsolationThreshold + ); + const bucket = densityPixelGrid.get(gridKey) || []; + bucket.push({ + feature, + layerKey: entry.layerKey, + pixelX: pixel[0], + pixelY: pixel[1] + }); + densityPixelGrid.set(gridKey, bucket); + } + }); if (currentZoom >= FULL_DISPLAY_NO_COLLISION_ZOOM) { - viewportFeatures.forEach(feature => { - const iconUrl = feature.get('_iconUrl') as string; - const shouldRenderIcon = - !!iconUrl && - this.isIconReady(iconUrl) && - feature.get('_legendVisible') !== false && - feature.get('_regionVisible') !== false && - this.shouldRenderFeatureByDensity(feature, currentZoom) && - this.shouldRenderNearbyFeature(feature, currentZoom); - const hasLabel = !!this.formatPointLabelText( - feature.get('_labelText') as string + viewportEntries.forEach(entry => { + const minZoom = this.getFeatureDensityMinZoom(entry.feature); + const densityVisible = + currentZoom >= minZoom || + (entry.legendVisible && + entry.regionVisible && + !!entry.layerKey && + entry.pixelX !== undefined && + entry.pixelY !== undefined && + !this.hasNearbyViewportFeature( + { + feature: entry.feature, + layerKey: entry.layerKey, + pixelX: entry.pixelX, + pixelY: entry.pixelY + }, + densityPixelGrid, + densityIsolationThreshold + )); + this.syncFeatureDensityVisible( + entry.feature, + densityVisible, + dirtyLayerKeys ); + const shouldRenderIcon = + !!entry.iconUrl && + this.isIconReady(entry.iconUrl) && + entry.legendVisible && + entry.regionVisible && + densityVisible && + entry.nearbyVisible; + const hasLabel = !!entry.labelText; - this.syncFeatureIconCollisionVisible(feature, shouldRenderIcon); + this.syncFeatureIconCollisionVisible( + entry.feature, + shouldRenderIcon, + dirtyLayerKeys + ); this.syncFeatureLabelCollisionVisible( - feature, - shouldRenderIcon && hasLabel + entry.feature, + shouldRenderIcon && hasLabel, + dirtyLayerKeys ); }); + this.flushDirtyPointLayers(dirtyLayerKeys); return; } - viewportFeatures.forEach(feature => { - const iconUrl = feature.get('_iconUrl') as string; + viewportEntries.forEach(entry => { + const minZoom = this.getFeatureDensityMinZoom(entry.feature); + const densityVisible = + currentZoom >= minZoom || + (entry.legendVisible && + entry.regionVisible && + !!entry.layerKey && + entry.pixelX !== undefined && + entry.pixelY !== undefined && + !this.hasNearbyViewportFeature( + { + feature: entry.feature, + layerKey: entry.layerKey, + pixelX: entry.pixelX, + pixelY: entry.pixelY + }, + densityPixelGrid, + densityIsolationThreshold + )); + this.syncFeatureDensityVisible( + entry.feature, + densityVisible, + dirtyLayerKeys + ); + const shouldRenderIcon = - !!iconUrl && - this.isIconReady(iconUrl) && - feature.get('_legendVisible') !== false && - feature.get('_regionVisible') !== false && - this.shouldRenderFeatureByDensity(feature, currentZoom) && - this.shouldRenderNearbyFeature(feature, currentZoom); + !!entry.iconUrl && + this.isIconReady(entry.iconUrl) && + entry.legendVisible && + entry.regionVisible && + densityVisible && + entry.nearbyVisible; if (!shouldRenderIcon) { - this.syncFeatureIconCollisionVisible(feature, false); - this.syncFeatureLabelCollisionVisible(feature, false); + this.syncFeatureIconCollisionVisible( + entry.feature, + false, + dirtyLayerKeys + ); + this.syncFeatureLabelCollisionVisible( + entry.feature, + false, + dirtyLayerKeys + ); return; } - const geometry = feature.getGeometry(); - if (!geometry || geometry.getType() !== 'Point') { - this.syncFeatureIconCollisionVisible(feature, false); - this.syncFeatureLabelCollisionVisible(feature, false); - return; - } - - const coordinates = (geometry as any).getCoordinates?.(); - if (!coordinates) { - this.syncFeatureIconCollisionVisible(feature, false); - this.syncFeatureLabelCollisionVisible(feature, false); - return; - } - - const pixel = this.map?.getPixelFromCoordinate(coordinates); - if (!pixel) { - this.syncFeatureIconCollisionVisible(feature, false); - this.syncFeatureLabelCollisionVisible(feature, false); + if (entry.pixelX === undefined || entry.pixelY === undefined) { + this.syncFeatureIconCollisionVisible( + entry.feature, + false, + dirtyLayerKeys + ); + this.syncFeatureLabelCollisionVisible( + entry.feature, + false, + dirtyLayerKeys + ); return; } @@ -508,13 +810,11 @@ export class MapOl implements MapInterface { // 避免像“小浪底/三门峡”这类近点被过早裁掉成只剩一个点。 const iconCollisionSize = Math.max(12, 16 * dynamicScale); const iconPadding = 0; - const iconLeft = pixel[0] - iconCollisionSize / 2 - iconPadding; - const iconRight = pixel[0] + iconCollisionSize / 2 + iconPadding; - const iconTop = pixel[1] - iconCollisionSize / 2 - iconPadding; - const iconBottom = pixel[1] + iconCollisionSize / 2 + iconPadding; - const labelText = this.formatPointLabelText( - feature.get('_labelText') as string - ); + const iconLeft = entry.pixelX - iconCollisionSize / 2 - iconPadding; + const iconRight = entry.pixelX + iconCollisionSize / 2 + iconPadding; + const iconTop = entry.pixelY - iconCollisionSize / 2 - iconPadding; + const iconBottom = entry.pixelY + iconCollisionSize / 2 + iconPadding; + const labelText = entry.labelText; const fontSize = this.getPointFontSize(dynamicScale); const lines = labelText ? labelText.split('\n') : ['']; const maxLineLength = Math.max(...lines.map(line => line.length), 1); @@ -525,22 +825,22 @@ export class MapOl implements MapInterface { ); const estimatedWidth = maxLineLength * fontSize * 0.6 + 16; const estimatedHeight = labelLineCount * (fontSize + 4) + 8; - const centerX = pixel[0]; - const centerY = pixel[1] + labelOffsetY; + const centerX = entry.pixelX; + const centerY = entry.pixelY + labelOffsetY; const candidateId = String( - feature.getId?.() || feature.get('stcd') || '' + entry.feature.getId?.() || entry.feature.get('stcd') || '' ); const dedupeKey = candidateId || [ - feature.get('_layerKey') || '', - pixel[0].toFixed(2), - pixel[1].toFixed(2), + entry.feature.get('_layerKey') || '', + entry.pixelX.toFixed(2), + entry.pixelY.toFixed(2), labelText ].join('|'); candidateMap.set(dedupeKey, { - feature, + feature: entry.feature, iconLeft, iconRight, iconTop, @@ -550,11 +850,11 @@ export class MapOl implements MapInterface { right: centerX + estimatedWidth / 2, top: centerY - estimatedHeight / 2, bottom: centerY + estimatedHeight / 2, - priority: Number(feature.get('_nearbyPriority') || 9999), - densityPriority: this.getFeatureDensityPriority(feature), + priority: Number(entry.feature.get('_nearbyPriority') || 9999), + densityPriority: entry.densityPriority, id: candidateId, - pixelX: pixel[0], - pixelY: pixel[1] + pixelX: entry.pixelX, + pixelY: entry.pixelY }); }); @@ -582,6 +882,16 @@ export class MapOl implements MapInterface { top: number; bottom: number; }> = []; + const placedIconGrid = new Map< + string, + Array<{ + featureId: string; + left: number; + right: number; + top: number; + bottom: number; + }> + >(); const placedLabelRects: Array<{ featureId: string; left: number; @@ -589,9 +899,17 @@ export class MapOl implements MapInterface { top: number; bottom: number; }> = []; - let hiddenByIconCollisionCount = 0; - let hiddenByLabelCollisionCount = 0; - let hiddenByIconHiddenCount = 0; + const placedLabelGrid = new Map< + string, + Array<{ + featureId: string; + left: number; + right: number; + top: number; + bottom: number; + }> + >(); + const collisionGridCellSize = 96; candidates.forEach(candidate => { const featureId = String( @@ -603,11 +921,18 @@ export class MapOl implements MapInterface { top: candidate.iconTop, bottom: candidate.iconBottom }; - const iconHasCollision = placedIconRects.some(rect => - this.checkLabelCollision(rect, iconRect) + const iconHasCollision = !!this.findCollidingRectInGrid( + iconRect, + placedIconGrid, + collisionGridCellSize, + rect => this.checkLabelCollision(rect, iconRect) ); const iconVisible = !iconHasCollision; - this.syncFeatureIconCollisionVisible(candidate.feature, iconVisible); + this.syncFeatureIconCollisionVisible( + candidate.feature, + iconVisible, + dirtyLayerKeys + ); if (iconVisible) { placedIconRects.push({ @@ -617,6 +942,11 @@ export class MapOl implements MapInterface { top: iconRect.top, bottom: iconRect.bottom }); + this.addRectToCollisionGrid( + placedIconRects[placedIconRects.length - 1], + placedIconGrid, + collisionGridCellSize + ); } }); @@ -640,37 +970,45 @@ export class MapOl implements MapInterface { const iconVisible = candidate.feature.get('_iconCollisionVisible') !== false; if (!candidate.hasLabel) { - this.syncFeatureLabelCollisionVisible(candidate.feature, false); + this.syncFeatureLabelCollisionVisible( + candidate.feature, + false, + dirtyLayerKeys + ); return; } if (!iconVisible) { - hiddenByIconHiddenCount += 1; - this.syncFeatureLabelCollisionVisible(candidate.feature, false); + this.syncFeatureLabelCollisionVisible( + candidate.feature, + false, + dirtyLayerKeys + ); return; } - const collidingIconRect = placedIconRects.find(rect => { - if (rect.featureId === featureId) { - return false; - } - return this.checkLabelCollisionWithIcon(rect, candidate); - }); + const collidingIconRect = this.findCollidingRectInGrid( + candidate, + placedIconGrid, + collisionGridCellSize, + rect => + rect.featureId !== featureId && + this.checkLabelCollisionWithIcon(rect, candidate) + ); const hasCollisionWithIcon = !!collidingIconRect; - const collidingLabelRect = placedLabelRects.find(rect => - this.checkLabelCollision(rect, candidate) + const collidingLabelRect = this.findCollidingRectInGrid( + candidate, + placedLabelGrid, + collisionGridCellSize, + rect => this.checkLabelCollision(rect, candidate) ); const hasCollisionWithLabel = !!collidingLabelRect; const visible = !hasCollisionWithIcon && !hasCollisionWithLabel; - if (!visible) { - if (hasCollisionWithIcon) { - hiddenByIconCollisionCount += 1; - } else { - hiddenByLabelCollisionCount += 1; - } - } - - this.syncFeatureLabelCollisionVisible(candidate.feature, visible); + this.syncFeatureLabelCollisionVisible( + candidate.feature, + visible, + dirtyLayerKeys + ); if (visible) { placedLabelRects.push({ @@ -680,11 +1018,25 @@ export class MapOl implements MapInterface { top: candidate.top, bottom: candidate.bottom }); + this.addRectToCollisionGrid( + placedLabelRects[placedLabelRects.length - 1], + placedLabelGrid, + collisionGridCellSize + ); } }); + this.flushDirtyPointLayers(dirtyLayerKeys); } - private requestRefreshPointLabelVisibility(immediate = false) { + private requestRefreshPointLabelVisibility( + immediate = false, + debounceMs = 0 + ) { + if (this.labelVisibilityDebounceTimerId !== null) { + window.clearTimeout(this.labelVisibilityDebounceTimerId); + this.labelVisibilityDebounceTimerId = null; + } + if (immediate) { if (this.labelVisibilityRefreshFrameId !== null) { cancelAnimationFrame(this.labelVisibilityRefreshFrameId); @@ -694,6 +1046,14 @@ export class MapOl implements MapInterface { return; } + if (debounceMs > 0) { + this.labelVisibilityDebounceTimerId = window.setTimeout(() => { + this.labelVisibilityDebounceTimerId = null; + this.requestRefreshPointLabelVisibility(); + }, debounceMs); + return; + } + if (this.labelVisibilityRefreshFrameId !== null) { return; } @@ -704,18 +1064,30 @@ export class MapOl implements MapInterface { }); } - private syncFeatureIconCollisionVisible(feature: Feature, visible: boolean) { - if (feature.get('_iconCollisionVisible') !== visible) { - feature.set('_iconCollisionVisible', visible); - feature.changed(); - } + private syncFeatureIconCollisionVisible( + feature: Feature, + visible: boolean, + dirtyLayerKeys?: Set + ) { + this.markFeatureVisibilityDirty( + feature, + '_iconCollisionVisible', + visible, + dirtyLayerKeys + ); } - private syncFeatureLabelCollisionVisible(feature: Feature, visible: boolean) { - if (feature.get('_labelCollisionVisible') !== visible) { - feature.set('_labelCollisionVisible', visible); - feature.changed(); - } + private syncFeatureLabelCollisionVisible( + feature: Feature, + visible: boolean, + dirtyLayerKeys?: Set + ) { + this.markFeatureVisibilityDirty( + feature, + '_labelCollisionVisible', + visible, + dirtyLayerKeys + ); } private isIconReady(iconUrl: string): boolean { @@ -740,11 +1112,11 @@ export class MapOl implements MapInterface { image.crossOrigin = 'anonymous'; image.onload = () => { this.iconLoadState.set(iconUrl, 'loaded'); - this.refreshPointLayerStyles(); + this.requestRefreshPointLayerStyles(); }; image.onerror = () => { this.iconLoadState.set(iconUrl, 'error'); - this.refreshPointLayerStyles(); + this.requestRefreshPointLayerStyles(); }; image.src = iconUrl; @@ -756,13 +1128,24 @@ export class MapOl implements MapInterface { return false; } + private requestRefreshPointLayerStyles() { + if (this.pointLayerStyleRefreshFrameId !== null) { + return; + } + + this.pointLayerStyleRefreshFrameId = window.requestAnimationFrame(() => { + this.pointLayerStyleRefreshFrameId = null; + this.refreshPointLayerStyles(); + }); + } + private refreshPointLayerStyles() { this.pointLayerManager.forEachLayer(layer => { layer.changed(); }); - this.requestRefreshPointLabelVisibility(true); + this.requestRefreshPointLabelVisibility(); if (this.isBatchPopupMode) { - this.requestUpdateBatchPopups(true); + this.requestUpdateBatchPopups(); } } @@ -1063,72 +1446,6 @@ export class MapOl implements MapInterface { return priority === 1; } - private createPointStyle1(feature: Feature): Style { - const color = (feature.get('iconCode') as string) || '#3399CC'; - const labelText = feature.get('titleName') as string; - const buildType = (feature.get('BuildType') as number) ?? 1; - - const currentZoom = this.view ? this.view.getZoom() : 4.5; - let dynamicScale = 0.7 + (currentZoom - 4.5) * 0.08; - dynamicScale = Math.max(0.5, Math.min(3.0, dynamicScale)); - const fontSize = Math.max(16, Math.min(24, 12 * dynamicScale)); - const radius = Math.max(6, Math.min(18, 6 * dynamicScale)); - - let imageStyle; - if (buildType === 0) { - const canvas = this.createSemiCircleCanvas(radius, color); - imageStyle = new Icon({ - img: canvas, - imgSize: [canvas.width, canvas.height], - anchor: [0.5, 0.5] - }); - } else { - imageStyle = new Circle({ - radius: radius, - fill: new Fill({ color: color }), - stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.5)', width: 1 }) - }); - } - - const styleOptions: any = { - image: imageStyle - }; - - if (labelText) { - let offsetY = -20 * dynamicScale; - const specialLabels = [ - '枕头坝二级', - '老鹰岩二级', - '长洲', - '丹巴', - '玛尔挡', - '班多', - '彭水', - '李家峡', - 'ML', - 'BDa', - '乌东德', - '里底', - '大华桥', - - '巴塘' - ]; - if (specialLabels.includes(labelText)) { - offsetY = 20 * dynamicScale; - } - styleOptions.text = new Text({ - text: labelText, - offsetY: offsetY, - font: `${fontSize}px sans-serif`, - fill: new Fill({ color: '#fff' }), - stroke: new Stroke({ color: 'rgba(0, 0, 0, .9)', width: 2 }), - textAlign: 'center', - declutterMode: 'declutter' - }); - } - - return new Style(styleOptions); - } /** * 初始化加载基础图层 * @param layer 图层配置对象 @@ -2167,10 +2484,18 @@ export class MapOl implements MapInterface { * 移除地图对象,释放资源 */ destroy(): void { + if (this.pointLayerStyleRefreshFrameId !== null) { + cancelAnimationFrame(this.pointLayerStyleRefreshFrameId); + this.pointLayerStyleRefreshFrameId = null; + } if (this.labelVisibilityRefreshFrameId !== null) { cancelAnimationFrame(this.labelVisibilityRefreshFrameId); this.labelVisibilityRefreshFrameId = null; } + if (this.labelVisibilityDebounceTimerId !== null) { + window.clearTimeout(this.labelVisibilityDebounceTimerId); + this.labelVisibilityDebounceTimerId = null; + } this.removeQueryLayer(); this.regionMaskManager.destroy(); diff --git a/frontend/src/components/gis/ol/point-layer-manager.ts b/frontend/src/components/gis/ol/point-layer-manager.ts index 19e5e151..95489733 100644 --- a/frontend/src/components/gis/ol/point-layer-manager.ts +++ b/frontend/src/components/gis/ol/point-layer-manager.ts @@ -81,7 +81,11 @@ export class PointLayerManager { if (!this.map) return; const resolution = this.map.getView().getResolution(); - if (currentZoom === undefined || !Number.isFinite(currentZoom) || !resolution) { + if ( + currentZoom === undefined || + !Number.isFinite(currentZoom) || + !resolution + ) { return; } const nearbyPointConfig = getNearbyPointConfig(); @@ -109,7 +113,10 @@ export class PointLayerManager { return; } const sortedFeatures = [...features].sort((left, right) => { - return Number(left.get('_nearbyPriority') || 0) - Number(right.get('_nearbyPriority') || 0); + return ( + Number(left.get('_nearbyPriority') || 0) - + Number(right.get('_nearbyPriority') || 0) + ); }); const secondaryFeatures = sortedFeatures.filter(feature => { const priority = Number(feature.get('_nearbyPriority')); @@ -132,7 +139,8 @@ export class PointLayerManager { const groupSecondaryCount = secondaryFeatures.length; const baseOffsetPx = - Number(feature.get('_nearbyExpandOffsetPx')) || autoRule.expandOffsetPx; + Number(feature.get('_nearbyExpandOffsetPx')) || + autoRule.expandOffsetPx; const ringSize = Math.max(1, layoutRule.ringSize); const ringIndex = Math.floor(index / ringSize); const ringOffsetIndex = index % ringSize; @@ -144,10 +152,10 @@ export class PointLayerManager { currentRingCount <= 1 ? 0 : currentRingCount <= 2 - ? layoutRule.fanAngleForTwoDeg - : currentRingCount <= 4 - ? layoutRule.fanAngleForFourDeg - : layoutRule.fanAngleForManyDeg; + ? layoutRule.fanAngleForTwoDeg + : currentRingCount <= 4 + ? layoutRule.fanAngleForFourDeg + : layoutRule.fanAngleForManyDeg; const angleStepDeg = currentRingCount <= 1 ? 0 : fanAngleDeg / (currentRingCount - 1); const startAngleDeg = -90 - fanAngleDeg / 2; @@ -156,7 +164,10 @@ export class PointLayerManager { const expandOffsetPx = baseOffsetPx + ringIndex * - Math.max(layoutRule.ringGapPx, baseOffsetPx * layoutRule.ringGapFactor); + Math.max( + layoutRule.ringGapPx, + baseOffsetPx * layoutRule.ringGapFactor + ); const offsetX = Math.cos(expandAngleRad) * expandOffsetPx * resolution; const offsetY = Math.sin(expandAngleRad) * expandOffsetPx * resolution; @@ -452,7 +463,9 @@ export class PointLayerManager { // 备注:先恢复要素原始坐标,再按缩放阶段决定是否做展开偏移,避免多次缩放后偏移累计。 private resetFeatureToBaseCoordinate(feature: Feature): void { - const baseCoordinates = feature.get('_baseCoordinates') as number[] | undefined; + const baseCoordinates = feature.get('_baseCoordinates') as + | number[] + | undefined; const geometry = feature.getGeometry(); if ( !baseCoordinates || @@ -463,6 +476,9 @@ export class PointLayerManager { return; } - (geometry as Point).setCoordinates([baseCoordinates[0], baseCoordinates[1]]); + (geometry as Point).setCoordinates([ + baseCoordinates[0], + baseCoordinates[1] + ]); } } diff --git a/frontend/src/components/mapFilter/index.vue b/frontend/src/components/mapFilter/index.vue index 2f3733b6..626038af 100644 --- a/frontend/src/components/mapFilter/index.vue +++ b/frontend/src/components/mapFilter/index.vue @@ -203,7 +203,6 @@ watch( () => route.path, () => { mapOrchestrator.resetFilterState(); - mapOrchestrator.changeEngPointCapacity('all'); searchTimeRange.value = [ dayjs(mapViewStore.searchTimeRange[0]).format('YYYY-MM-DD HH:mm:ss'), dayjs(mapViewStore.searchTimeRange[1]).format('YYYY-MM-DD HH:mm:ss') @@ -219,7 +218,6 @@ watch( // 备注:时间选择只通过编排器分发,由编排器决定运行态更新和数据重载。 const triggerManualValuesChange = async (val: any) => { - console.log('triggerManualValuesChange', val); const [startTime, endTime] = getTimeRangeByValue(val); if (!startTime || !endTime) { diff --git a/frontend/src/components/mapLegend/index.vue b/frontend/src/components/mapLegend/index.vue index 83754b94..64cc3e4d 100644 --- a/frontend/src/components/mapLegend/index.vue +++ b/frontend/src/components/mapLegend/index.vue @@ -87,7 +87,6 @@ const hasEnvFac = computed(() => ); const legendItemClick = (item: any) => { - console.log(item); if (item.parentName == '环保设施') return; if (item.canBeChecked == 0) { return; diff --git a/frontend/src/layout/components/AppMain.vue b/frontend/src/layout/components/AppMain.vue index 07fbe486..ec8921f5 100644 --- a/frontend/src/layout/components/AppMain.vue +++ b/frontend/src/layout/components/AppMain.vue @@ -8,9 +8,7 @@ import { useModelStore } from '@/store/modules/model'; import GisView from '@/components/gis/GisView.vue'; const modelStore = useModelStore(); - const tagsViewStore = useTagsViewStore(); - const router = useRoute(); const routeKey = computed(() => router.path + Math.random()); diff --git a/frontend/src/layout/components/Sidebar/index.vue b/frontend/src/layout/components/Sidebar/index.vue index 2e60c917..ac637235 100644 --- a/frontend/src/layout/components/Sidebar/index.vue +++ b/frontend/src/layout/components/Sidebar/index.vue @@ -1,17 +1,28 @@