WebGIS + OpenLayers + GeoServer + PostGIS #量算 #点聚合

前言

前两篇我们搞定了地图初始化和基础交互,但真实的业务地图数据都在后端:几千条管线、地块、监测点位存在数据库里,前端怎么高效拿到、怎么渲染、怎么做空间查询?

本篇讲透一套最经典的开源全栈方案:

复制代码
前端 Vue + OpenLayers  →  HTTP  →  GeoServer(发布服务)→ PostgreSQL + PostGIS(空间数据库)

外加两个高频组件完整代码:

  • ol-ext 量算(测距、测面积、面积单位)
  • 点聚合 Cluster(上万 POI 不卡顿)

工程化能直接抄,适合水利、管线、自然资源、应急项目。

一、全栈架构图

复制代码
┌─────────────────────────────┐
│  前端 Vue3 + OpenLayers      │
│  ┌─ WMS 底图/影像 ────────┐  │
│  ├─ WFS 矢量(点击/查询) ──┤  │  HTTP/JSON
│  ├─ ol-ext 量算          │  │ ────────────▶ GeoServer(8080)
│  └─ Cluster 点聚合        │  │                │ JDBC
└─────────────────────────────┘                │
                                     PostgreSQL + PostGIS
                                     (管线/地块/监测点 空间表)

分工一句话:

  • PostGIS :存空间数据,做复杂空间计算(ST_Intersects、ST_Buffer)
  • GeoServer:把空间表发布成标准 OGC 服务(WMS 瓦片图、WFS 矢量)
  • OpenLayers:前端渲染 + 交互 + 轻量计算

大数据空间分析必须放 PostGIS,前端只做小数据量的 Turf。

二、后端准备

1. 安装 PostgreSQL + PostGIS

复制代码
# Ubuntu/Debian
sudo apt install postgresql postgis
# 创建空间数据库
sudo -u postgres psql -c "CREATE DATABASE gisdb;"
sudo -u postgres psql -d gisdb -c "CREATE EXTENSION postgis;"

2. 建空间表(管线示例)

复制代码
-- 开启空间扩展
CREATE EXTENSION IF NOT EXISTS postgis;

-- 管线段表
CREATE TABLE pipe_line (
  id SERIAL PRIMARY KEY,
  name VARCHAR(50),
  code VARCHAR(20),
  diameter NUMERIC(10,2),      -- 管径mm
  geom geometry(LineString, 4326)   -- 空间字段,WGS84
);

-- 监测点位表
CREATE TABLE monitor_point (
  id SERIAL PRIMARY KEY,
  name VARCHAR(50),
  status INT DEFAULT 1,
  geom geometry(Point, 4326)
);

-- 插入测试数据
INSERT INTO pipe_line (name, code, diameter, geom) VALUES
('主管道A', 'P-001', 800, ST_GeomFromText('LINESTRING(108.95 34.27, 108.97 34.28, 108.99 34.26)', 4326)),
('支管B',   'P-002', 400, ST_GeomFromText('LINESTRING(108.96 34.25, 108.98 34.24)', 4326));

INSERT INTO monitor_point (name, status, geom) VALUES
('站点1', 1, ST_SetSRID(ST_MakePoint(108.95, 34.27), 4326)),
('站点2', 0, ST_SetSRID(ST_MakePoint(108.99, 34.26), 4326)),
('站点3', 1, ST_SetSRID(ST_MakePoint(108.97, 34.28), 4326));

空间字段建议都用 4326 存原始经纬度,发布时再投影,避免坐标系混乱。

3. 空间查询(PostGIS 示例,后端接口用)

复制代码
-- 找某点周围500米内的监测点(注意投影换算)
SELECT name
FROM monitor_point
WHERE ST_DWithin(
  geom::geography,
  ST_SetSRID(ST_MakePoint(108.96, 34.26), 4326)::geography,
  500
);

-- 找与某管线相交的所有管线
SELECT a.name, b.name
FROM pipe_line a, pipe_line b
WHERE a.id = 1 AND ST_Intersects(a.geom, b.geom);

-- 算面积、算长度
SELECT ST_Area(geom::geography) FROM some_polygon;
SELECT ST_Length(geom::geography) FROM pipe_line WHERE id=1;

4. GeoServer 发布

  • 下载 GeoServer,默认 8080,admin/geoserver
  • 新建工作区 workspace → 新建数据源 → PostgreSQL/PostGIS(填库名 / 账号)
  • 图层里对每个表新建图层,设置 SRS 为 EPSG:4326,配置样式
  • 预览时即可拿到 WMS/WFS 服务地址

重点:GeoServer 默认可能跨域,用 CORS Filter 配置放行,否则前端报跨域。

三、前端全栈对接(Vue3 完整代码)

复制代码
<template>
  <div class="map-wrap">
    <div id="map"></div>
    <div class="bar">
      <button @click="loadWms">加载WMS底图</button>
      <button @click="loadWfs">加载WFS管线</button>
      <button @click="queryByClick">点击查询</button>
      <button @click="clearVector">清除矢量</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import Map from 'ol/Map'
import View from 'ol/View'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { TileWMS, Vector as VectorSource, Cluster } from 'ol/source'
import { Style, Fill, Stroke, Circle as CircleStyle, Text } from 'ol/style'
import { fromLonLat } from 'ol/proj'
import GeoJSON from 'ol/format/GeoJSON'
import { bbox as bboxStrategy } from 'ol/loadingstrategy'
import 'ol/ol.css'

let map, vectorSource

// ========= 1. 初始化地图 =========
const initMap = () => {
  map = new Map({
    target: 'map',
    layers: [],
    view: new View({
      center: fromLonLat([108.96, 34.26]),
      zoom: 11,
      projection: 'EPSG:3857'
    })
  })
}

// ========= 2. 加载 WMS(GeoServer发布的底图/影像)=========
const loadWms = () => {
  const wmsLayer = new TileLayer({
    source: new TileWMS({
      url: 'http://localhost:8080/geoserver/gisdb/wms',
      params: {
        LAYERS: 'gisdb:pipe_line',  // workspace:layer
        TILED: true
      },
      serverType: 'geoserver'
    })
  })
  map.addLayer(wmsLayer)
}

// ========= 3. 加载 WFS 矢量(管线,视口内按需加载)=========
const loadWfs = () => {
  vectorSource = new VectorSource({
    url: (extent) =>
      `http://localhost:8080/geoserver/gisdb/ows?service=WFS&version=1.1.0&request=GetFeature` +
      `&typeName=gisdb:pipe_line&outputFormat=application/json&srsname=EPSG:3857` +
      `&bbox=${extent.join(',')},EPSG:3857`,
    format: new GeoJSON(),
    strategy: bboxStrategy   // 只加载当前视口,平移再按需请求
  })

  const vectorLayer = new VectorLayer({
    source: vectorSource,
    style: new Style({
      stroke: new Stroke({ color: '#0a6cff', width: 3 })
    })
  })
  map.addLayer(vectorLayer)
}

// ========= 4. 点击查询 WFS 要素属性 =========
const queryByClick = () => {
  map.on('singleclick', (evt) => {
    const feature = map.forEachFeatureAtPixel(evt.pixel, f => f)
    if (feature) {
      const p = feature.getProperties()
      console.log('管线属性:', { name: p.name, code: p.code, diameter: p.diameter })
      alert(`管段:${p.name}\n编码:${p.code}\n管径:${p.diameter}mm`)
    }
  })
}

const clearVector = () => { vectorSource?.clear() }

onMounted(() => {
  initMap()
  loadWms()
  loadWfs()
})
onUnmounted(() => { map?.dispose() })
</script>

关键点:WFS 用 bbox 策略 + srsname=EPSG:3857,前端只拿当前屏幕的数据,平移再自动请求,几万条也不会卡。

四、ol-ext 量算(测距 + 测面积)完整代码

原生 OpenLayers 没有量算控件,业界标准是用 ol-ext(法国团队开源,插件生态好)。

复制代码
npm install ol-ext

<template>
  <div class="map-wrap">
    <div id="map"></div>
    <div class="bar">
      <button @click="startMeasure(1)">测距</button>
      <button @click="startMeasure(2)">测面积</button>
      <button @click="clearMeasure">清除</button>
    </div>
  </div>
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue'
import Map from 'ol/Map'
import View from 'ol/View'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import { fromLonLat } from 'ol/proj'
import 'ol/ol.css'
// ol-ext 量算控件(按需引入)
import Measure from 'ol-ext/control/Measure'
import 'ol-ext/dist/ol-ext.min.css'

let map, measureControl, measureSource

const initMap = () => {
  measureSource = new VectorSource()
  map = new Map({
    target: 'map',
    layers: [
      // 这里放你的底图(天地图/GeoServer WMS)
      new VectorLayer({ source: measureSource })  // 量算图层
    ],
    view: new View({
      center: fromLonLat([108.96, 34.26]),
      zoom: 11
    })
  })

  // 创建量算控件
  measureControl = new Measure({
    source: measureSource,          // 测量结果画到哪个图层
    maxLength: 200000,              // 最大测距(米)
    maxArea: 100000000,             // 最大测面积(㎡)
    geodesic: true,                 // 球面测距(精确,推荐)
    continueMeasure: true,          // 连续测量
    units: {
      length: { 'm': 1, 'km': 1000 },
      area: { 'm²': 1, 'km²': 1000000, '亩': 666.667 }
    }
  })
  map.addControl(measureControl)
}

// type: 1测距离, 2测面积
const startMeasure = (type) => {
  const isLength = type === 1
  measureControl.getInteraction(true)   // 重置当前测量
  measureControl.set('type', isLength ? 'length' : 'area')
  measureControl.activate()
}

const clearMeasure = () => {
  measureControl.deactivate()
  measureSource.clear()
  measureControl.reset()
}

onMounted(initMap)
onUnmounted(() => { map?.dispose() })
</script>

说明:ol-ext 的 Measure 支持距离 / 面积,自带标签实时显示数值和单位,geodesic:true 用球面距离(WGS84)比平面更准,量算单位里可以加 "亩",国内项目常用。

五、点聚合 Cluster 完整代码(上万 POI 不卡顿)

点位几千上万时直接渲染会掉帧,用 OpenLayers 自带的 Cluster 聚合:近的点合成一个带数字的圆,放大自动散开。

复制代码
<script setup>
import { ref, onMounted } from 'vue'
import Map from 'ol/Map'
import View from 'ol/View'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import Cluster from 'ol/source/Cluster'
import { Style, Fill, Stroke, Circle as CircleStyle, Text } from 'ol/style'
import { Point } from 'ol/geom'
import Feature from 'ol/Feature'
import { fromLonLat } from 'ol/proj'

let map, clusterSource, vectorLayer

const initCluster = () => {
  // 原始点位数据源(可以来自WFS/接口/GeoJSON)
  const pointSource = new VectorSource()

  // 生成5000个随机测试点(模拟真实POI)
  for (let i = 0; i < 5000; i++) {
    const lng = 108.9 + Math.random() * 0.12
    const lat = 34.2 + Math.random() * 0.12
    const f = new Feature({
      geometry: new Point(fromLonLat([lng, lat])),
      name: `点位${i}`
    })
    pointSource.addFeature(f)
  }

  // Cluster 聚合数据源:distance=像素距离,在此范围内合并
  clusterSource = new Cluster({
    source: pointSource,
    distance: 40   // 40像素内的点聚合
  })

  vectorLayer = new VectorLayer({
    source: clusterSource,
    style: (feature) => {
      const size = feature.get('features').length  // 聚合点数
      let color = '#1e88e5'
      if (size >= 100) color = '#e53935'
      else if (size >= 30) color = '#fb8c00'
      return new Style({
        image: new CircleStyle({
          radius: size >= 100 ? 26 : size >= 30 ? 20 : 14,
          fill: new Fill({ color }),
          stroke: new Stroke({ color: '#fff', width: 2 })
        }),
        text: new Text({
          text: size.toString(),   // 显示聚合数量
          fill: new Fill({ color: '#fff' }),
          font: 'bold 12px sans-serif'
        })
      })
    }
  })

  map.addLayer(vectorLayer)

  // 点击聚合点 → 弹层显示内部点位(可选)
  map.on('singleclick', (evt) => {
    const feature = map.forEachFeatureAtPixel(evt.pixel, f => f)
    if (feature) {
      const feats = feature.get('features')
      if (feats && feats.length > 1) {
        console.log('该聚合含', feats.length, '个点位', feats.map(f => f.get('name')))
      }
    }
  })
}
</script>

说明:Cluster 返回的每个 feature 里带 features 数组,就是聚合进去的子点位。样式根据数量分色分级,一眼看出密度。真实项目点位从 WFS / 接口拿,替换掉随机生成那段即可。

六、空间计算怎么选?前端 Turf vs 后端 PostGIS

表格

场景 推荐 原因
两个点 / 线算距离 Turf.js 前端零请求,秒回
鼠标实时绘制算面积 Turf/ol-ext 交互期,后端来不及
点是否在面内(单个) Turf 轻量
几千条要素做缓冲区 / 相交 PostGIS 前端会卡死,SQL 秒出
全库范围检索、空间索引 PostGIS 数据量大必须后端

铁律:前端只算用户能一眼看到、量级小的;批量、全量、需要索引的交给 PostGIS,前端通过接口拿结果。

七、全栈踩坑清单(血泪)

  1. 坐标系三层不一致:库 4326、GeoServer 发布的 SRS、前端 3857,任何一层没对上就偏移。建议库和 GeoServer 统一 4326,前端投影到 3857
  2. GeoServer 跨域:默认 CORS 可能没开,前端请求直接失败。配好 CORS Filter
  3. WFS 一次性加载全库:几万条 addFeatures 直接白屏 → 用 bbox策略 视口加载
  4. WMS 和 WFS 搞混:WMS 返回图片适合底图 / 影像;WFS 返回矢量适合查询 / 点击。图层多了别都用 WMS
  5. ol-ext 样式未引入:ol-ext.min.css 不引,量算控件样式全乱
  6. Cluster 聚合 distance 调太小:聚合不明显;调太大:缩放时点位分布失真,一般 30-50
  7. 量算 geodesic:false:平面量算在大范围时误差大,跨省测距误差可能到几公里,务必开 geodesic:true
  8. 内存泄漏:路由切换 / 页面销毁必须 map.dispose()、移除交互和监听,否则切几次页面就卡

八、小结

全栈 WebGIS 的真正难点不在前端画地图,而在空间数据的存取、发布和计算分层:

  • 存储 / 大数据计算 → PostGIS
  • 服务发布 → GeoServer
  • 渲染交互 → OpenLayers
  • 轻量量算 → ol-ext / Turf.js
相关推荐
不可能片场2 小时前
Electron 发布 cookie 库先查明文再写解密
前端·electron
郭小李2 小时前
springmvc 拦截器
java·开发语言·前端
web打印社区2 小时前
Chrome-kiosk-printing能上生产吗
开发语言·前端·javascript·chrome·websocket
白帽攻防录2 小时前
某pc客户端app从xss到rce实战案例
前端·网络安全·黑客·xss·rce·src漏洞·app漏洞
志尊宝2 小时前
AI原生前端开发(041):AI辅助的状态管理(Zustand/Pinia)
前端·javascript·ai-native
修炼的dance2 小时前
pngquant 两档质量后接 oxipng,每步省多少花几秒
前端
费曼学习法2 小时前
无人值守AI内容矩阵实战(10):Windows 无 systemd,进程存活率 99% 怎么做
前端
小兔子2 小时前
Integer 的 == 在 100 时是 true,在 1000 时是 false
前端
尘世中一位迷途小书童3 小时前
和 AI 打交道会撞见的一整串词:从大模型到 Agent、RAG、MCP、Skill,一次串清
前端·人工智能