openlayers+天地图+geoserver实现对某区域高亮,加载瓦片或自定义点位并显示详细信息

150 阅读11分钟

image.png

上图使用对江阴区域geojson数据做反转,对江阴以外的区域加遮罩。核心代码:

// 创建遮罩层
const maskLayer = createMaskLayer()

function createMaskLayer() {
  // 世界范围坐标 (EPSG:3857)
  const worldExtent = [-20037508.34, -20037508.34, 20037508.34, 20037508.34]

  // 创建世界范围多边形
  const worldPolygon = new Polygon([[
    [worldExtent[0], worldExtent[1]],
    [worldExtent[2], worldExtent[1]],
    [worldExtent[2], worldExtent[3]],
    [worldExtent[0], worldExtent[3]],
    [worldExtent[0], worldExtent[1]]
  ]])

  // 解析江阴区域GeoJSON
  const jiangyinFeatures = new GeoJSON().readFeatures(jiangyinArea.jiangyinBoundery, {
    featureProjection: 'EPSG:3857'
  })

  // 获取江阴区域坐标(只取第一个多边形)
  const jiangyinCoords = jiangyinFeatures[0].getGeometry().getCoordinates()

  // 创建遮罩Feature:在世界多边形中"挖空"江阴区域
  const maskFeature = new Feature({
    geometry: new Polygon([
      worldPolygon.getCoordinates()[0], // 外环:世界范围
      ...jiangyinCoords.map(ring => ring.slice().reverse()) // 内环:江阴区域(坐标需反转)
    ])
  })

  // 创建矢量图层
  return new VectorLayer({
    source: new VectorSource({ features: [maskFeature] }),
    style: new Style({
      fill: new Fill({
        color: 'rgba(0, 0, 0, 0.5)' // 半透明黑色遮罩
      }),
      stroke: new Stroke({
        color: 'transparent' // 隐藏边界线
      })
    }),
    zIndex: 1 // 确保在底图之上
  })
}

image.png

上图使用geoserver加载管线数据瓦片。这样加载出来的瓦片图层非常流畅,可以任意拖拽、放大缩小,几乎没有卡顿。核心代码:

// GeoServer WMS 图层 ---供水管网
let curheight = 300;
const wmsSource = new TileWMS({
  url: '/geoserver/work1/wms', // 通过代理转发
  params: {
    'SERVICE': 'WMS',
    'VERSION': '1.1.0',
    'REQUEST': 'GetMap',
    'LAYERS': 'work1:pipe_network',
    'STYLES': 'line_green',
    'FORMAT': 'image/png', // 必须用image/png或image/jpeg
    // 'FORMAT': 'application/json;type=utfgrid',
    'SRS': 'EPSG:3857',    // 1.1.1用SRS,1.3.0用CRS
    'TILED': false, // 必须为false,否则GeoServer可能返回空图
    'TRANSPARENT': true,
    // 'CQL_FILTER' 动态设置,初始值为300
    'CQL_FILTER': `height > ${curheight}`,
    // 'maxFeatures': 5000, //控制显示密度,随机过滤,而不是密度过滤
    // 'format_options': 'cluster:true;cluster_distance:60;cluster_max:5',  //仅适用于 application/vnd.mapbox-vector-tile 等矢量格式,对 PNG/JPG 栅格格式无效
    // 'CQL_FILTER': 'ST_Area(geom) > 10', // 过滤小面积要素,小短线,小面积图形
    // 'CQL_FILTER': 'Length(geom) >= 110', // 过滤小短线
    // BBOX参数由OpenLayers自动生成并拼接,无需手动设置
  },

  serverType: 'geoserver',
  tileGrid: new TileGrid({
    extent: [13356624.394745126, 3722943.2809553347, 13424618.821758036, 3757885.6374882166],  // EPSG:3857 全球范围
    resolutions: [
      611.49622628141,     // 级别 8
      305.748113140705,    // 级别 9
      152.8740565703525,   // 级别 10
      76.43702828517625,   // 级别 11
      38.21851414258813,   // 级别 12
      19.109257071294063,  // 级别 13
      9.554628535647032,    // 级别 14
      4.777314267823516,    // 级别 15
      2.388657133911758,    // 级别 16
      1.194328566955879,    // 级别 17
      0.5971642834779395,   // 级别 18
      0.29858214173896974,  // 级别 19
      0.14929107086948487,  // 级别 20
      0.0746455354243517,   // 级别 21
      0.0373227677121758,  // 级别 22
      0.0186613838560879, // 级别 23
    ]
  }),
  crossOrigin: 'anonymous'
  // projection: 'EPSG:3857', // OpenLayers 6+不需要此项,view的projection决定
})
wmsLayer = new TileLayer({
  source: wmsSource,
  opacity: 1,
  cacheSize: 256, // 缓存256个WMS瓦片
  visible: false // 初始状态显示
})
wmsLayer.set('layerName', 'layerName01')

image.png 上图展示根据经纬度得到的点位,撒点并展示详细信息。

所有代码(天地图key需自行申请):

<template>
  <div class="tiandi-map-container">
    <div id="ol-map" class="ol-map-full"></div>
<!--    管线点击信息-->
    <div v-if="popupVisible" :style="popupStyle" class="ol-popup">
      <div>
        <strong>管线高度:</strong>
        <span>{{ markerInfo.height }}</span>
      </div>
      <button @click="popupVisible = false" style="margin-top: 8px;">关闭</button>
    </div>
    <!-- 右上角专题按钮区 -->
    <div class="map-topic-panel" ref="typePanel">
      <div v-for="topic in topicData" :key="topic.title" class="topic-row">
        <div class="topic-title">{{ topic.title }}</div>
        <div class="topic-btns-cols">
          <div v-for="(btnList,index) in topic.buttons" :key="index" class="topic-btns" >
            <div v-for="btn in btnList" :key="btn.label" class="topic-btn"
                 :class="{ 'topic-btn-active': activeButtons.includes(btn.label) }" @click="handleButtonClick(btn.label)">
              <div class="topic-btn-icon">
                <img :src="btn.icon" :alt="btn.label" />
              </div>
              <div class="topic-btn-label">
                <span v-for="(line, index) in btn.labelLines" :key="index">{{ line }}</span>
              </div>
            </div>
          </div>

        </div>
      </div>
    </div>

  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import 'ol/ol.css'
import Map from 'ol/Map'
import View from 'ol/View'
import { defaults as defaultInteractions } from 'ol/interaction';
import TileLayer from 'ol/layer/Tile'
import TileWMS from 'ol/source/TileWMS'
import TileGrid from 'ol/tilegrid/TileGrid'
import { fromLonLat } from 'ol/proj'
import { transformExtent } from 'ol/proj';
import XYZ from 'ol/source/XYZ'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import GeoJSON from 'ol/format/GeoJSON'
import { Fill, Stroke, Style, Text, Icon } from 'ol/style'
import jiangyinArea from '@/utils/jiangyinArea'
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import {normalMarkers} from "../../../utils/markerImages";
import {searchAlarmList} from "../../../api/screen/map";
import {Polygon} from "ol/geom";

// coordinate变量放在外部,方便调用
let coordinate = null

//状态框位置
const rightPosition = ref(false)
const typePanel = ref(null)

//自定义点标记数据
let autoMarkerList = {
  '电导率':[]
}
// 自定义点标记图层
let autoMarkerLayerList = {
  '电导率':null
}


// 管线WMS图层变量(全局访问)
let wmsLayer = null
let wmsLayer2 = null
let wmsLayer3 = null

//geoserver图层
let markerList = {
  '雨水井':null,
  '污水井':null,
  '阀门':null,
  '排水户':null,
  '消防栓':null,
  '污水泵站':null,
  '加压站':null,
}

// 地图变量
let map = null


function changeStatusPositon(isCloseTop,isCloseSides){
  typePanel.value.style.transform = `translateX(${(isCloseTop&&isCloseSides) ? '900px' : '0'})`;
}

function getDianDaoLvStatusDict(status){
  if([0,'0'].includes(status)){
    return '离线'
  }else if([1,'1'].includes(status)){
    return '在线'
  }else if([2,'2'].includes(status)){
    return '维护'
  }else if([3,'3'].includes(status)){
    return '报警'
  }
}

// 按钮点击处理函数
async function handleButtonClick(buttonLabel) {
  const index = activeButtons.value.indexOf(buttonLabel)
  if (index > -1) {
    // 如果按钮已激活,则取消激活
    activeButtons.value.splice(index, 1)
  } else {
    // 如果按钮未激活,则添加激活状态
    activeButtons.value.push(buttonLabel)
  }

  //自定义点标记
  if (buttonLabel === '电导率') {
    if (activeButtons.value.includes(buttonLabel)) {
      let response = await searchAlarmList()
      let res = response.data
      for(let i=0;i<res.length;i++){
        let obj = {
          markerImg: normalMarkers.diandaolv,
          latLng:res[i].latLng,
          deviceName:res[i].deviceName,
          status_dict:getDianDaoLvStatusDict(res[i].status),
          conductivityLowerLimit:res[i].conductivityLowerLimit,
          conductivityUpperLimit:res[i].conductivityUpperLimit,
        }
        autoMarkerList['电导率'].push(obj)
      }
    } else {
      autoMarkerList['电导率'] = []
    }
    updateAutoMarkerLayerList()
    return;
  }

  //geoserver点标记
  if (!['供水管网', '污水管网', '雨水管网'].includes(buttonLabel)) {
    markerList[buttonLabel].setVisible(activeButtons.value.includes(buttonLabel))
    return
  }

// 控制管线WMS图层显示/隐藏
  if (buttonLabel === '供水管网') {
    wmsLayer.setVisible(activeButtons.value.includes('供水管网'))
    return
  }
  if (buttonLabel === '污水管网') {
    wmsLayer2.setVisible(activeButtons.value.includes('污水管网'))
    return
  }
  if (buttonLabel === '雨水管网') {
    wmsLayer3.setVisible(activeButtons.value.includes('雨水管网'))
  }
}





// 当前激活的按钮数组
const activeButtons = ref([])

// 专题按钮数据
const topicData = ref([
  {
    title: '供水专题',
    buttons: [
     [ {
        label: '阀门',
        icon: new URL('@/assets/images/mapImgs/btn_icons/btn_famen.png', import.meta.url).href,
        labelLines: ['阀门']
      },
      {
        label: '供水管网',
        icon: new URL('@/assets/images/mapImgs/btn_icons/btn_gongshui.png', import.meta.url).href,
        labelLines: ['供水管网']
      },{
       label: '消防栓',
       icon: new URL('@/assets/images/mapImgs/btn_icons/btn_xiaofangshuan.png', import.meta.url).href,
       labelLines: ['消防栓']
     },{
       label: '加压站',
       icon: new URL('@/assets/images/mapImgs/btn_icons/btn_jiayazhan.png', import.meta.url).href,
       labelLines: ['加压站']
     },],
      // [ {
      //   label: '水厂',
      //   icon: new URL('@/assets/images/mapImgs/btn_icons/btn_famen.png', import.meta.url).href,
      //   labelLines: ['水厂']
      // },
      //   {
      //     label: '水源地',
      //     icon: new URL('@/assets/images/mapImgs/btn_icons/btn_gongshui.png', import.meta.url).href,
      //     labelLines: ['水源地']
      //   }]
    ]
  },
  {
    title: '排水专题',
    buttons: [
     [ {
        label: '雨水井',
        icon: new URL('@/assets/images/mapImgs/btn_icons/btn_yushuijing.png', import.meta.url).href,
        labelLines: ['雨水井']
      },
       {
         label: '污水井',
         icon: new URL('@/assets/images/mapImgs/btn_icons/btn_wushuijing.png', import.meta.url).href,
         labelLines: ['污水井']
       },
      {
        label: '污水管网',
        icon: new URL('@/assets/images/mapImgs/btn_icons/btn_wushui.png', import.meta.url).href,
        labelLines: ['污水管网']
      },{
       label: '雨水管网',
       icon: new URL('@/assets/images/mapImgs/btn_icons/btn_yushui.png', import.meta.url).href,
       labelLines: ['雨水管网']
     },],
        [{
          label: '排水户',
          icon: new URL('@/assets/images/mapImgs/btn_icons/btn_paishuihu.png', import.meta.url).href,
          labelLines: ['排水户']
        }, {
          label: '污水泵站',
          icon: new URL('@/assets/images/mapImgs/btn_icons/btn_wushuibengzhan.png', import.meta.url).href,
          labelLines: ['污水泵站']
        },]
    ]
  },
  {
    title: '报警专题',
    buttons: [
      //   [
      //     {
      //       label: '高品质水',
      //       icon: new URL('@/assets/images/mapImgs/btn_icons/btn_gaopinzhishui.png', import.meta.url).href,
      //       labelLines: ['高品质水']
      //     },
      //     {
      //       label: '测压点',
      //       icon: new URL('@/assets/images/mapImgs/btn_icons/btn_ceyadian.png', import.meta.url).href,
      //       labelLines: ['测压点']
      //     },
      //     {
      //       label: '泵房',
      //       icon: new URL('@/assets/images/mapImgs/btn_icons/btn_bengfang.png', import.meta.url).href,
      //       labelLines: ['泵房']
      //     },
      //   ],
      // [  {
      //   label: '施工',
      //   icon: new URL('@/assets/images/mapImgs/btn_icons/btn_shigong.png', import.meta.url).href,
      //   labelLines: ['施工']
      // },
      //   {
      //     label: '工单',
      //     icon: new URL('@/assets/images/mapImgs/btn_icons/btn_gongdan.png', import.meta.url).href,
      //     labelLines: ['工单']
      //   },
      //   {
      //     label: '重大设备故障',
      //     icon: new URL('@/assets/images/mapImgs/btn_icons/btn_shebeiguzhang.png', import.meta.url).href,
      //     labelLines: ['重大设备', '故障']
      //   },
      //   {
      //     label: '重大工程',
      //     icon: new URL('@/assets/images/mapImgs/btn_icons/btn_zhongdagongcheng.png', import.meta.url).href,
      //     labelLines: ['重大工程']
      //   },
      //  ],
      [ {
        label: '电导率',
        icon: new URL('@/assets/images/mapImgs/btn_icons/btn_diandaolv.png', import.meta.url).href,
        labelLines: ['电导率']
      }]
    ]
  }
])

// 弹窗相关响应式变量
const popupVisible = ref(false)
const markerInfo = ref({
  height: ''
})
const popupStyle = ref({
  position: 'absolute',
  left: '0px',
  top: '0px',
  background: 'white',
  border: '1px solid #ccc',
  padding: '8px',
  borderRadius: '4px',
  zIndex: 1000,
  minWidth: '120px',
  boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
})

/*
  天地图默认的坐标系为 EPSG:4326(WGS84,经纬度坐标系),
  OpenLayers 默认是 EPSG:3857。
  解决方法:确保WMS参数SRS/CRS为EPSG:3857,FORMAT为image/png,且GeoServer图层已正确投影和切片。
*/
const lnglatCenter = [120.284794, 31.841642]
const MIN_ZOOM = 11.49
const MAX_ZOOM = 18

// 节流函数 - 用于WMS参数更新
function throttle(func, limit) {
  let inThrottle
  return function () {
    const args = arguments
    const context = this
    if (!inThrottle) {
      func.apply(context, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}
// 创建带文本标注的样式函数
function createBoundaryStyle(feature) {
  const name = feature.get('name') || feature.get('properties.name')
  return new Style({
    fill: new Fill({
      color: 'rgba(185,224,242, 0.1)' // 半透明填充
    }),
    stroke: new Stroke({
      color: 'rgba(176,225,254, 0.8)', // 边框
      width: 2
    }),
    text: new Text({
      text: name,
      font: '14px Arial',
      fill: new Fill({
        color: '#FFF'
      }),
      offsetY: 0,
      placement: 'point'
    })
  })
}

function initMap() {
  // 天地图key轮换 zq
  const tdtKeys = [
    
  ];
  // 计数器用localStorage
  let tdtKeyIndex = parseInt(localStorage.getItem('tdtKeyIndex'), 10);
  if (isNaN(tdtKeyIndex)) {
    tdtKeyIndex = 0;
  } else {
    tdtKeyIndex = (tdtKeyIndex + 1) % tdtKeys.length;
  }
  localStorage.setItem('tdtKeyIndex', tdtKeyIndex);
  const tdtKey = tdtKeys[tdtKeyIndex];
  // 天地图矢量底图服务URL,随机选择天地图二级域名 t0-t7
  const tdtSubdomain = Math.floor(Math.random() * 8)
  const tdtUrl = `https://t${tdtSubdomain}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${tdtKey}`
  const tdtLayer = new TileLayer({
    source: new XYZ({
      url: tdtUrl,
      minZoom: MIN_ZOOM,
      maxZoom: MAX_ZOOM,
      crossOrigin: 'anonymous', // 避免跨域导致瓦片加载异常
    }),
    cacheSize: 512 // 缓存512个瓦片
  })

  // 江阴边界填充图层
  const boundarySource = new VectorSource({
    features: new GeoJSON().readFeatures(jiangyinArea.jiangyinArea, {
      featureProjection: 'EPSG:3857'
    })
  })

  const boundaryLayer = new VectorLayer({
    source: boundarySource,
    style: createBoundaryStyle,
    zIndex: 1 // 确保在底图之上,WMS图层之下
  })

  // 创建遮罩层
  const maskLayer = createMaskLayer()

  function createMaskLayer() {
    // 世界范围坐标 (EPSG:3857)
    const worldExtent = [-20037508.34, -20037508.34, 20037508.34, 20037508.34]

    // 创建世界范围多边形
    const worldPolygon = new Polygon([[
      [worldExtent[0], worldExtent[1]],
      [worldExtent[2], worldExtent[1]],
      [worldExtent[2], worldExtent[3]],
      [worldExtent[0], worldExtent[3]],
      [worldExtent[0], worldExtent[1]]
    ]])

    // 解析江阴区域GeoJSON
    const jiangyinFeatures = new GeoJSON().readFeatures(jiangyinArea.jiangyinBoundery, {
      featureProjection: 'EPSG:3857'
    })

    // 获取江阴区域坐标(只取第一个多边形)
    const jiangyinCoords = jiangyinFeatures[0].getGeometry().getCoordinates()

    // 创建遮罩Feature:在世界多边形中"挖空"江阴区域
    const maskFeature = new Feature({
      geometry: new Polygon([
        worldPolygon.getCoordinates()[0], // 外环:世界范围
        ...jiangyinCoords.map(ring => ring.slice().reverse()) // 内环:江阴区域(坐标需反转)
      ])
    })

    // 创建矢量图层
    return new VectorLayer({
      source: new VectorSource({ features: [maskFeature] }),
      style: new Style({
        fill: new Fill({
          color: 'rgba(0, 0, 0, 0.5)' // 半透明黑色遮罩
        }),
        stroke: new Stroke({
          color: 'transparent' // 隐藏边界线
        })
      }),
      zIndex: 1 // 确保在底图之上
    })
  }


    // GeoServer WMS 图层 ---供水管网
  let curheight = 300;
  const wmsSource = new TileWMS({
    url: '/geoserver/work1/wms', // 通过代理转发
    params: {
      'SERVICE': 'WMS',
      'VERSION': '1.1.0',
      'REQUEST': 'GetMap',
      'LAYERS': 'work1:pipe_network',
      'STYLES': 'line_green',
      'FORMAT': 'image/png', // 必须用image/png或image/jpeg
      // 'FORMAT': 'application/json;type=utfgrid',
      'SRS': 'EPSG:3857',    // 1.1.1用SRS,1.3.0用CRS
      'TILED': false, // 必须为false,否则GeoServer可能返回空图
      'TRANSPARENT': true,
      // 'CQL_FILTER' 动态设置,初始值为300
      'CQL_FILTER': `height > ${curheight}`,
      // 'maxFeatures': 5000, //控制显示密度,随机过滤,而不是密度过滤
      // 'format_options': 'cluster:true;cluster_distance:60;cluster_max:5',  //仅适用于 application/vnd.mapbox-vector-tile 等矢量格式,对 PNG/JPG 栅格格式无效
      // 'CQL_FILTER': 'ST_Area(geom) > 10', // 过滤小面积要素,小短线,小面积图形
      // 'CQL_FILTER': 'Length(geom) >= 110', // 过滤小短线
      // BBOX参数由OpenLayers自动生成并拼接,无需手动设置
    },

    serverType: 'geoserver',
    tileGrid: new TileGrid({
      extent: [13356624.394745126, 3722943.2809553347, 13424618.821758036, 3757885.6374882166],  // EPSG:3857 全球范围
      resolutions: [
        611.49622628141,     // 级别 8
        305.748113140705,    // 级别 9
        152.8740565703525,   // 级别 10
        76.43702828517625,   // 级别 11
        38.21851414258813,   // 级别 12
        19.109257071294063,  // 级别 13
        9.554628535647032,    // 级别 14
        4.777314267823516,    // 级别 15
        2.388657133911758,    // 级别 16
        1.194328566955879,    // 级别 17
        0.5971642834779395,   // 级别 18
        0.29858214173896974,  // 级别 19
        0.14929107086948487,  // 级别 20
        0.0746455354243517,   // 级别 21
        0.0373227677121758,  // 级别 22
        0.0186613838560879, // 级别 23
      ]
    }),
    crossOrigin: 'anonymous'
    // projection: 'EPSG:3857', // OpenLayers 6+不需要此项,view的projection决定
  })
  // GeoServer WMS 图层 ---污水管网
  const wmsSource2 = new TileWMS({
    url: '/geoserver/work1/wms', // 通过代理转发
    params: {
      'SERVICE': 'WMS',
      'VERSION': '1.1.0',
      'REQUEST': 'GetMap',
      'LAYERS': 'work1:pipe_dirty',
      'STYLES': 'line_orange',
      'FORMAT': 'image/png', // 必须用image/png或image/jpeg
      // 'FORMAT': 'application/json;type=utfgrid',
      'SRS': 'EPSG:3857',    // 1.1.1用SRS,1.3.0用CRS
      'TILED': false, // 必须为false,否则GeoServer可能返回空图
      'TRANSPARENT': true,
      // 'CQL_FILTER' 动态设置,初始值为300
      'CQL_FILTER': `height > ${curheight} AND pipe_type = '污水管网'`,
      // 'maxFeatures': 5000, //控制显示密度,随机过滤,而不是密度过滤
      // 'format_options': 'cluster:true;cluster_distance:60;cluster_max:5',  //仅适用于 application/vnd.mapbox-vector-tile 等矢量格式,对 PNG/JPG 栅格格式无效
      // 'CQL_FILTER': 'ST_Area(geom) > 10', // 过滤小面积要素,小短线,小面积图形
      // 'CQL_FILTER': 'Length(geom) >= 110', // 过滤小短线
      // BBOX参数由OpenLayers自动生成并拼接,无需手动设置
    },

    serverType: 'geoserver',
    tileGrid: new TileGrid({
      extent: [13356624.394745126, 3722943.2809553347, 13424618.821758036, 3757885.6374882166],  // EPSG:3857 全球范围
      resolutions: [
        611.49622628141,     // 级别 8
        305.748113140705,    // 级别 9
        152.8740565703525,   // 级别 10
        76.43702828517625,   // 级别 11
        38.21851414258813,   // 级别 12
        19.109257071294063,  // 级别 13
        9.554628535647032,    // 级别 14
        4.777314267823516,    // 级别 15
        2.388657133911758,    // 级别 16
        1.194328566955879,    // 级别 17
        0.5971642834779395,   // 级别 18
        0.29858214173896974,  // 级别 19
        0.14929107086948487,  // 级别 20
        0.0746455354243517,   // 级别 21
        0.0373227677121758,  // 级别 22
        0.0186613838560879, // 级别 23
      ]
    }),
    crossOrigin: 'anonymous'
    // projection: 'EPSG:3857', // OpenLayers 6+不需要此项,view的projection决定
  })

  // GeoServer WMS 图层 ---污水管网
  const wmsSource3 = new TileWMS({
    url: '/geoserver/work1/wms', // 通过代理转发
    params: {
      'SERVICE': 'WMS',
      'VERSION': '1.1.0',
      'REQUEST': 'GetMap',
      'LAYERS': 'work1:pipe_dirty',
      'STYLES': 'line_blue',
      'FORMAT': 'image/png', // 必须用image/png或image/jpeg
      // 'FORMAT': 'application/json;type=utfgrid',
      'SRS': 'EPSG:3857',    // 1.1.1用SRS,1.3.0用CRS
      'TILED': false, // 必须为false,否则GeoServer可能返回空图
      'TRANSPARENT': true,
      // 'CQL_FILTER' 动态设置,初始值为300
      'CQL_FILTER': `height > ${curheight} AND pipe_type = '雨水管网'`,
      // 'maxFeatures': 5000, //控制显示密度,随机过滤,而不是密度过滤
      // 'format_options': 'cluster:true;cluster_distance:60;cluster_max:5',  //仅适用于 application/vnd.mapbox-vector-tile 等矢量格式,对 PNG/JPG 栅格格式无效
      // 'CQL_FILTER': 'ST_Area(geom) > 10', // 过滤小面积要素,小短线,小面积图形
      // 'CQL_FILTER': 'Length(geom) >= 110', // 过滤小短线
      // BBOX参数由OpenLayers自动生成并拼接,无需手动设置
    },

    serverType: 'geoserver',
    tileGrid: new TileGrid({
      extent: [13356624.394745126, 3722943.2809553347, 13424618.821758036, 3757885.6374882166],  // EPSG:3857 全球范围
      resolutions: [
        611.49622628141,     // 级别 8
        305.748113140705,    // 级别 9
        152.8740565703525,   // 级别 10
        76.43702828517625,   // 级别 11
        38.21851414258813,   // 级别 12
        19.109257071294063,  // 级别 13
        9.554628535647032,    // 级别 14
        4.777314267823516,    // 级别 15
        2.388657133911758,    // 级别 16
        1.194328566955879,    // 级别 17
        0.5971642834779395,   // 级别 18
        0.29858214173896974,  // 级别 19
        0.14929107086948487,  // 级别 20
        0.0746455354243517,   // 级别 21
        0.0373227677121758,  // 级别 22
        0.0186613838560879, // 级别 23
      ]
    }),
    crossOrigin: 'anonymous'
    // projection: 'EPSG:3857', // OpenLayers 6+不需要此项,view的projection决定
  })


  wmsLayer = new TileLayer({
    source: wmsSource,
    opacity: 1,
    cacheSize: 256, // 缓存256个WMS瓦片
    visible: false // 初始状态显示
  })
  wmsLayer.set('layerName', 'layerName01')

  wmsLayer2 = new TileLayer({
    source: wmsSource2,
    opacity: 1,
    cacheSize: 256, // 缓存256个WMS瓦片
    visible: false // 初始状态显示
  })
  wmsLayer2.set('layerName', 'layerName02')

  wmsLayer3 = new TileLayer({
    source: wmsSource3,
    opacity: 1,
    cacheSize: 256, // 缓存256个WMS瓦片
    visible: false // 初始状态显示
  })
  wmsLayer3.set('layerName', 'layerName03')

  //点位
  let markerSourceList = {
    '雨水井':null,
    '污水井':null,
    '阀门':null,
    '排水户':null,
    '消防栓':null,
    '污水泵站':null,
    '加压站':null,
  }

  function getMarkerSource(CQL_FILTER,STYLES,LAYERS){
    let params = {
      'SERVICE': 'WMS',
      'VERSION': '1.1.0',
      'REQUEST': 'GetMap',
      'FORMAT': 'image/png', // 必须用image/png或image/jpeg
      // 'FORMAT': 'application/json;type=utfgrid',
      'SRS': 'EPSG:3857',    // 1.1.1用SRS,1.3.0用CRS
      'TILED': false, // 必须为false,否则GeoServer可能返回空图
      'TRANSPARENT': true,
      // 'CQL_FILTER' 动态设置,初始值为300
      // 'maxFeatures': 5000, //控制显示密度,随机过滤,而不是密度过滤
      // 'format_options': 'cluster:true;cluster_distance:60;cluster_max:5',  //仅适用于 application/vnd.mapbox-vector-tile 等矢量格式,对 PNG/JPG 栅格格式无效
      // 'CQL_FILTER': 'ST_Area(geom) > 10', // 过滤小面积要素,小短线,小面积图形
      // 'CQL_FILTER': 'Length(geom) >= 110', // 过滤小短线
      // BBOX参数由OpenLayers自动生成并拼接,无需手动设置
    }
    if(CQL_FILTER){
      params.CQL_FILTER = CQL_FILTER
    }
    if(STYLES){
      params.STYLES = STYLES
    }
    if(LAYERS){
      params.LAYERS = LAYERS
    }
    return new TileWMS({
      url: '/geoserver/work1/wms', // 通过代理转发
      params: params,

      serverType: 'geoserver',
      tileGrid: new TileGrid({
        extent: [13356624.394745126, 3722943.2809553347, 13424618.821758036, 3757885.6374882166],  // EPSG:3857 全球范围
        resolutions: [
          611.49622628141,     // 级别 8
          305.748113140705,    // 级别 9
          152.8740565703525,   // 级别 10
          76.43702828517625,   // 级别 11
          38.21851414258813,   // 级别 12
          19.109257071294063,  // 级别 13
          9.554628535647032,    // 级别 14
          4.777314267823516,    // 级别 15
          2.388657133911758,    // 级别 16
          1.194328566955879,    // 级别 17
          0.5971642834779395,   // 级别 18
          0.29858214173896974,  // 级别 19
          0.14929107086948487,  // 级别 20
          0.0746455354243517,   // 级别 21
          0.0373227677121758,  // 级别 22
          0.0186613838560879, // 级别 23
        ]
      }),
      crossOrigin: 'anonymous'
      // projection: 'EPSG:3857', // OpenLayers 6+不需要此项,view的projection决定
    })
  }

  markerSourceList['雨水井'] =getMarkerSource('dno=10','point_rain','work1:dirty_point')
  markerSourceList['污水井'] =getMarkerSource('dno=13','point_sewage_well','work1:dirty_point')
  markerSourceList['阀门'] =getMarkerSource('','point_valve','work1:control')
  markerSourceList['排水户'] =getMarkerSource('','point_psh','work1:psh')
  markerSourceList['消防栓'] =getMarkerSource('','point_fire_hyd','work1:fire_hyd')
  markerSourceList['污水泵站'] =getMarkerSource('SUBTYPE=2','point_dirty_pump','work1:ps_pump')
  markerSourceList['加压站'] =getMarkerSource('SUBTYPE=1','point_boosting_station','work1:boosting_station')


  function getMarkerLayer(source) {
    return new TileLayer({
      source: source,
      opacity: 1,
      cacheSize: 256, // 缓存256个WMS瓦片
      visible: false // 初始状态显示
    })
  }

  markerList['雨水井'] =getMarkerLayer(markerSourceList['雨水井'])
  markerList['污水井'] =getMarkerLayer(markerSourceList['污水井'])
  markerList['阀门'] =getMarkerLayer(markerSourceList['阀门'])
  markerList['排水户'] =getMarkerLayer(markerSourceList['排水户'])
  markerList['消防栓'] =getMarkerLayer(markerSourceList['消防栓'])
  markerList['污水泵站'] =getMarkerLayer(markerSourceList['污水泵站'])
  markerList['加压站'] =getMarkerLayer(markerSourceList['加压站'])


  markerList['雨水井'].set('layerName', '雨水井')
  markerList['污水井'].set('layerName', '污水井')
  markerList['阀门'].set('layerName', '阀门')
  markerList['排水户'].set('layerName', '排水户')
  markerList['消防栓'].set('layerName', '消防栓')
  markerList['污水泵站'].set('layerName', '污水泵站')
  markerList['加压站'].set('layerName', '加压站')

  autoMarkerLayerList['电导率'] = new VectorLayer({
    source: new VectorSource(),
  })

  const extent4326 = [119.75487990670948, 31.606645090235276, 120.86752346270671, 32.08937971257144];
  const extent3857 = transformExtent(extent4326, 'EPSG:4326', 'EPSG:3857');

  //创建地图
  map = new Map({
    target: 'ol-map',
    layers: [
      tdtLayer,
      boundaryLayer, // 添加边界图层
      maskLayer,//江阴以外的遮罩
      wmsLayer,
      wmsLayer2,
      wmsLayer3,
      markerList['雨水井'],
      markerList['污水井'],
      markerList['阀门'],
      markerList['排水户'],
      markerList['消防栓'],
      markerList['污水泵站'],
      markerList['加压站'],
      autoMarkerLayerList['电导率']
    ],
    view: new View({
      center: fromLonLat(lnglatCenter), // fromLonLat 默认转换为 EPSG:3857
      zoom: MIN_ZOOM,
      minZoom: MIN_ZOOM,
      maxZoom: MAX_ZOOM,
      projection: 'EPSG:3857',
      // 添加以下属性限制拖动范围
      extent: extent3857,
      constrainOnlyCenter: true, // 只限制中心点,不限制整个视图
      constrainResolution: false, // 可选:限制分辨率不能超出范围
      smoothExtentConstraint: true // 平滑约束
    }),
    interactions: defaultInteractions({
      // 启用以鼠标位置为中心的缩放
      mouseWheelZoom: {
        duration: 250,
        constrainResolution: false, // 允许中间缩放级别
        onFocusOnly: false // 确保不需要聚焦也能缩放
      }
    })
  })

  // 节流处理的WMS参数更新函数
  const updateWmsParams = throttle((zoom) => {
    try {
      let curheight = 300;
      if (zoom >= 12 && zoom < 13) curheight = 250;
      else if (zoom >= 13 && zoom < 14) curheight = 200;
      else if (zoom >= 14 && zoom < 15) curheight = 150;
      else if (zoom >= 15 && zoom < 16) curheight = 100;
      else if (zoom >= 16 && zoom < 17) curheight = 50;
      else if (zoom >= 17 && zoom <= 18) curheight = 0;
      else if (zoom < 12) curheight = 300;

      // 确保WMS源存在且有效
      if (wmsSource && typeof wmsSource.updateParams === 'function') {
        wmsSource.updateParams({
          'CQL_FILTER': `height > ${curheight}`
        });
      }
      if (wmsSource2 && typeof wmsSource2.updateParams === 'function') {
        wmsSource2.updateParams({
          'CQL_FILTER': `height > ${curheight} AND pipe_type = '污水管网'`
        });
      }
      if (wmsSource3 && typeof wmsSource3.updateParams === 'function') {
        wmsSource3.updateParams({
          'CQL_FILTER': `height > ${curheight} AND pipe_type = '雨水管网'`
        });
      }
    } catch (error) {
      console.warn('WMS参数更新失败:', error);
    }
  }, 50); // 50ms节流,确保及时更新

  // 弹窗位置更新函数,外部可调用
  function updatePopupPosition(coord) {
    const mapDiv = document.getElementById('ol-map');
    if (mapDiv && coord) {
      const pixel = map.getPixelFromCoordinate(coord);
      // 使弹窗不超出地图容器
      let left = pixel[0] + 10;
      let top = pixel[1] - 10;
      // 限制弹窗在地图容器内
      const popupWidth = 180;
      const popupHeight = 60;
      if (left + popupWidth > mapDiv.offsetWidth) {
        left = mapDiv.offsetWidth - popupWidth - 10;
      }
      if (top + popupHeight > mapDiv.offsetHeight) {
        top = mapDiv.offsetHeight - popupHeight - 10;
      }
      if (left < 0) left = 10;
      if (top < 0) top = 10;
      popupStyle.value = {
        ...popupStyle.value,
        left: left + 'px',
        top: top + 'px'
      }
    }
  }

  // 合并的事件监听器:处理缩放限制和WMS参数更新,更新管线详情弹框位置
  map.getView().on('change:resolution', function () {
    try {
      const view = map.getView()
      const zoom = view.getZoom()
      updateAutoMarkerLayerList()

      // 节流更新WMS参数
      updateWmsParams(zoom)

      if (popupVisible.value && coordinate) {
        updatePopupPosition(coordinate);
      }
    } catch (error) {
      console.error('地图缩放事件处理失败:', error);
    }
  })

  // 地图点击事件,获取WMS要素详情
  // 修复:使用async/await确保能正确获取source字段,且log能触发
  const handleMapClick = throttle(function (evt) {
    // 如果WMS图层不可见,则不处理点击事件
    if (!wmsLayer.getVisible() && !wmsLayer2.getVisible() && !wmsLayer3.getVisible() && !activeButtons.value.includes('加压站')) {
      return;
    }
    const viewResolution = map.getView().getResolution();
    coordinate = evt.coordinate; // 赋值到外部变量
    // 构造GetFeatureInfo请求URL
    function getUrl(wmsSourceType,QUERY_LAYERS){
      return wmsSourceType.getFeatureInfoUrl(
          coordinate,
          viewResolution,
          'EPSG:3857',
          {
            'REQUEST': 'GetFeatureInfo',
            'INFO_FORMAT': 'application/json',
            'QUERY_LAYERS': QUERY_LAYERS,
            'FEATURE_COUNT': 1
          }
      );
    }

    let urls = []
    let markerList = []
    if(wmsLayer.getVisible()){
      const url1 = getUrl(wmsSource,'work1:pipe_network')
      urls.push(url1)
    }
    if(wmsLayer2.getVisible()){
      const url2 = getUrl(wmsSource2,'work1:pipe_dirty')
      urls.push(url2)
    }
    if(wmsLayer3.getVisible()){
      const url3 = getUrl(wmsSource3,'work1:pipe_dirty')
      urls.push(url3)
    }
    // if(activeButtons.value.includes('加压站')){
    //   const markerListJiaya = getUrl(markerSourceList['加压站'],'work1:boosting_station')
    //   markerList.push(markerListJiaya)
    // }
    handlerUrl(urls,markerList)
  }, 50); // 50ms防抖

  const handlerUrl = async (urls,markerList) => {
    try {
      // 遍历所有管线
      for (const url of urls) {
        if (!url) continue; // 跳过无效URL
        const response = await fetch(url);
        const data = await response.json();
        // 检查是否有要素信息
        if (data && data.features && data.features.length > 0) {
          const feature = data.features[0];

          // 显示弹窗(使用对应图层的属性)
          markerInfo.value.height = feature.properties?.height ?? '--';
          updatePopupPosition(coordinate); // 更新弹窗位置
          popupVisible.value = true;

          return
        }
      }
      //遍历标记
      for (const marker of markerList) {
        if (!marker) continue; // 跳过无效URL
        const response = await fetch(marker);
        const data = await response.json();
        // 检查是否有要素信息
        if (data && data.features && data.features.length > 0) {
          const feature = data.features[0];
// console.log(feature)

        }
      }
      // 所有图层都无结果时关闭弹窗
      popupVisible.value = false;
    } catch (error) {
      popupVisible.value = false;
    }
  }

  map.on('singleclick', handleMapClick);

// 在点击地图其他地方关闭弹窗时也清除高亮
  map.on('pointerdown', function (evt) {
    // 如果不是单击(即不是singleclick事件),关闭弹窗并清除高亮
    if (!evt.originalEvent || evt.originalEvent.type !== 'click') {
      popupVisible.value = false;
    }
  });

  updateAutoMarkerLayerList()

  // 点击地图输出经纬度坐标
  // map.on('singleclick', function (evt) {
  //   // evt.coordinate 是投影坐标(EPSG:3857),需要转换为经纬度
  //   const lonLat = toLonLat(evt.coordinate);
  //   console.log('点击位置经纬度:', lonLat);
  // });
}

function updateAutoMarkerLayerList(){
  for(let i=0;i<Object.keys(autoMarkerList).length;i++){
    let buttonLabel = Object.keys(autoMarkerList)[i]
    //自定义点标记图层
    if(!autoMarkerList[buttonLabel]){
      return
    }
    autoMarkerLayerList[buttonLabel].getSource().clear()
    if(autoMarkerList[buttonLabel].length>0){
      // 添加标记点到图层
      autoMarkerList[buttonLabel].forEach(item => {
        if(!item.latLng || !item.latLng.includes(',')){
          return
        }
        const show = map.getView().getZoom() >= 15;
        // 创建 Feature 时,geometry 单独传递
        let latlng = item.latLng.split(',')
        const marker = new Feature({
          geometry: new Point(fromLonLat([Number(latlng[0]),Number(latlng[1])])), // 经度, 纬度记得转换
        });
        // 设置标记样式
        marker.setStyle(new Style({
          image: new Icon({
            anchor: [0.5, 0.5], // 图标中心对齐坐标点,必须这样,否则偏移巨大
            src: item.markerImg, // 图标URL
            // size: [200, 200],  // 指明图片原始大小,不一致直接不能正常显示
            scale: 0.3,
          }),
          text: show ? new Text({
            text: getDetailText(buttonLabel,item),
            offsetY: -120,
            font: '14px Microsoft YaHei',
            fill: new Fill({ color: '#000' }),
            stroke: new Stroke({ color: '#fff', width: 2 }),
            backgroundFill: new Fill({ color: 'rgba(255,255,255,0.8)' }),
            padding: [8, 10, 8, 10],
            textBaseline: 'top',
            overflow: true,
            maxLines: 5,
            lineHeight: 1.4
          }) : null
        }));
        autoMarkerLayerList[buttonLabel].getSource().addFeature(marker);
      });
  }

  }
}

// // 辅助函数生成格式化文本
function getDetailText(buttonLabel,f) {
  if(buttonLabel === '电导率'){
    return [
      `【${f.deviceName}】`,
      `状态: ${f.status_dict}`,
      `下限: ${f.conductivityLowerLimit}`,
      `上限: ${f.conductivityUpperLimit}`,
    ].join('\n');
  }
}



onMounted(() => {
  // 确保DOM元素完全加载
  setTimeout(() => {
    initMap();
  }, 100);
})



defineExpose({
  changeStatusPositon
})

</script>

<style>
.tiandi-map-container {
  width: 100%;
  height: 100%;
  position: relative;
}

.ol-map-full {
  width: 100%;
  height: 100%;
  position: relative;
}

.ol-popup {
  pointer-events: auto;
}

.map-topic-panel {
  position: absolute;
  top: 0px;
  right: 1100px;
  width: 338px;
  height: 340px;
  background: linear-gradient(90deg, rgba(10, 41, 88, 0.3) 0%, rgba(20, 49, 90, 0.8) 52%, rgba(14, 33, 61, 0.3) 100%);
  border-radius: 12px;
  overflow-y: auto;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
  z-index: 2000;
  padding: 16px 12px 12px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: all 0.6s;
}

.map-topic-panel::-webkit-scrollbar {
  width: 4px;
  background: transparent;
}

.map-topic-panel::-webkit-scrollbar-thumb {
  background: rgba(25, 78, 108, 0.5);
  border-radius: 2px;
}

.topic-row {
  margin-bottom: 6px;
}

.topic-title {
  color: #fff;
  font-size: 15px;
  font-weight: bold;
  margin-bottom: 8px;
  letter-spacing: 1px;
}

.topic-btns-cols{
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.topic-btns {
  display: flex;
  flex-direction: row;
  gap: 8px;
}

.topic-btn {
  width: 72px;
  height: 74px;
  background: rgba(15, 53, 75, 0.3);
  border-radius: 7px;
  border: 1px solid rgba(25, 78, 108, 0.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: box-shadow 0.2s;
}

.topic-btn:hover {
  box-shadow: 0 0 8px 2px rgba(20, 49, 90, 0.18);
}

.topic-btn-active {
  background: rgba(0, 50, 108, 0.8) !important;
  border-radius: 7px;
  border: 1px solid #00CEFF !important;
}

.topic-btn-icon {
  width: 28px;
  height: 28px;
  background: rgba(53, 155, 255, 0.5);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 6px;
}

.topic-btn img {
  width: 16px;
  height: 16px;
  display: block;
}

.topic-btn-label {
  color: #fff;
  font-size: 14px;
  text-align: center;
  line-height: 1.2;
  min-height: 28px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* 保证多行和单行按钮视觉对齐 */
}

.topic-btn-label span {
  display: block;
  margin: 0;
  padding: 0;
}

/* 状态筛选区域样式 */
.status-filter-panel {
  position: absolute;
  top: 316px;
  right: 1100px;
  width: 338px;
  height: 100px;
  background: linear-gradient(90deg, rgba(10, 41, 88, 0.3) 0%, rgba(20, 49, 90, 0.8) 52%, rgba(14, 33, 61, 0.3) 100%);
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
  z-index: 2000;
  padding: 16px 12px 12px 12px;
  transition: all 0.6s;
}

.status-title {
  color: #fff;
  font-size: 15px;
  font-weight: bold;
  margin-bottom: 12px;
  letter-spacing: 1px;
}

.status-checkboxes {
  display: flex;
  flex-direction: row;
  gap: 8px;
}

.status-checkbox {
  display: flex;
  align-items: center;
  cursor: pointer;
  color: #fff;
  font-size: 14px;
}

.status-checkbox input[type="checkbox"] {
  margin-right: 8px;
  width: 16px;
  height: 16px;
  accent-color: rgba(53, 155, 255, 0.5);
}

.status-label {
  user-select: none;
}
</style>