OpenLayers聚类优化,可基于要素属性聚类

一、概要

OpenLayers自带的Cluster方法可以根据距离将点要素进行聚类,有助于提升地图的可读性,但其不能根据要素属性进行分类聚合

常规的聚类效果:
请添加图片描述
通过阅读源码,发现只需要将Cluster类的方法略微改造即可实现基于要素属性聚类(支持多属性)。


二、完整代码:基于地图缩放级别的点聚类和样式更新

<template>
  <!--地图-->
  <div ref="mapContainer" class="mapContainer" id="mapContainer"></div>
</template>
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue'
import { View, Map as OLMap, Feature } from "ol"
import { fromLonLat } from 'ol/proj'
import TileLayer from 'ol/layer/Tile'
import { defaults as defaultControls } from "ol/control"
import { Vector as VectorLayer } from 'ol/layer'
import VectorSource from 'ol/source/Vector'
import { Circle as CircleStyle, Style, Stroke, Text, Icon } from "ol/style";
import Fill from 'ol/style/Fill'
import GeoJSON from 'ol/format/GeoJSON'
import type { FeatureLike } from 'ol/Feature'
import { Geometry, Point } from 'ol/geom'
import { XYZ } from 'ol/source'
import {
  buffer,
  createEmpty,
  createOrUpdateFromCoordinate,
} from 'ol/extent';
import { add as addCoordinate, scale as scaleCoordinate } from 'ol/coordinate';
import type { Coordinate } from 'ol/coordinate';
import { getUid } from 'ol/util';

// 改为你自己的GeoJson数据地址
const pointDataURL = './geoJson/10000'
// 地图容器
const mapContainer = shallowRef<HTMLDivElement>()
// 地图对象
const map = shallowRef<OLMap>()
// 矢量图层
let vectorLayer: VectorLayer<VectorSource> | null
// 聚类图层
let clusterLayer: VectorLayer<VectorSource> | null
// 聚类结果
let clusterFeatures: Feature<Point>[] = []
// 聚类的属性字段(我这里是color或者src)
const clusterField: string[] = ['color']
// 结束聚类的分辨率层级
const zoomMax = 8
// 触发聚类的最大像素距离
const distance = 50

/**
 * @description 创建地图实例
 * @param {Document | DocumentId} target 地图容器
 * @returns 地图对象
 */

const createMap = function (target: HTMLElement | string,): OLMap {
  // 创建地图
  const map = new OLMap({
    target,
    layers: [
      new TileLayer({
        source: new XYZ({
          url: "http://t0.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=99a8ea4a53c8553f6f3c565f7ffc15ec",
          crossOrigin: 'anonymous',
          wrapX: true
        })
      })
    ],
    controls: defaultControls({
      zoom: false, // 不显示放大放小按钮
      rotate: false, // 不显示指北针控件
      attribution: false, // 不显示右下角的地图信息控件
    }).extend([]),
    view: new View({
      projection: 'EPSG:3857', // 坐标系EPSG:4326或EPSG:3857
      zoom: 4, // 打开页面时默认地图缩放级别
      maxZoom: 20,
      minZoom: 1, // 地图缩放最小级别
      center: fromLonLat([121.5, 25]), // 需要转到墨卡托坐标系
      constrainResolution: true, // 自动缩放到距离最近的一个整数级别
    })
  })
  return map
}

/**
 * @description 新建图层(检测到同名图层直接获取)
 * @param map 地图实例
 * @param layerName 图层名
 * @param getStyle feature样式
 * @returns 
 */

function getVectorLayer(map: OLMap, layerName: string, getStyle: Function): VectorLayer<VectorSource> {
  let vectorLayer = map.getLayers().getArray().find(layer => layer.get('name') === layerName) as VectorLayer<VectorSource>;
  if (!vectorLayer) {
    vectorLayer = new VectorLayer({
      source: new VectorSource({ wrapX: true, features: [] }),
      style: function (feature: FeatureLike) {
        return getStyle(feature)
      }
    });
    vectorLayer.set('name', layerName);
    map.addLayer(vectorLayer);
  }
  return vectorLayer;
}

/**
 * @description >>>>>>>> 添加矢量点(可追加数据)
 * @param { Map } map 地图对象
 * @param { string} layerName 图层名
 * @param { any } pointData 点据集合
 */

function addPointVector(
  map: OLMap,
  layerName: string,
  pointData: any) {
  if (!map || !pointData) { return }
  // 获取矢量图层
  vectorLayer = getVectorLayer(map, layerName, getStyle) as VectorLayer<VectorSource<Point>>
  // 添加数据源
  let features = new GeoJSON({ dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }).readFeatures(pointData)
  let source = new VectorSource({ features: features }) as VectorSource<Point>
  // 按分辨率加载聚类/矢量图层
  trigger(map, source, layerName)
  map.getView().on("change:resolution", () => {
    trigger(map, source, layerName)
  });
}
/**
 * 根据地图缩放级别触发图层更新。
 * @param {OLMap} map - 地图实例
 * @param {VectorSource<Point>} source - 数据源
 * @param {string} layerName - 图层名称
 * @description 根据地图的缩放级别加载相应的图层:低于指定缩放级别时加载聚类图层,高于该级别时加载矢量图层。
 */
function trigger(map: OLMap, source: VectorSource<Point>, layerName: string) {
  const clusterLayerName = `${layerName}_cluster`;
  const zoom = map.getView()!.getZoom()!;

  // 清空现有数据源
  clearSource();

  // 根据缩放级别加载不同的图层
  if (zoom < zoomMax) {
    // 加载聚类图层
    addCluster(source, clusterLayerName);
  } else {
    // 加载矢量图层
    vectorLayer?.getSource()?.addFeatures(source.getFeatures());
  }
}

/**
 * @description 该函数会清空聚类特征数组,并清除矢量图层和聚类图层的数据源。
 */
function clearSource() {
  // 清空存储聚类要素的数组
  clusterFeatures = [];

  // 清除矢量图层的数据源
  vectorLayer?.getSource()?.clear();

  // 清除聚类图层的数据源
  clusterLayer?.getSource()?.clear();
}
/**
* @description 聚类加载
* @param layerName 图层名
*/
function addCluster(source: VectorSource<Point>, layerName: string) {
  // 创建聚类层
  clusterLayer = getVectorLayer(map.value!, layerName + '_cluster', getStyle) as VectorLayer<VectorSource<Point>>
  // 聚类数据源
  const clusterFeatures = getClusterFeatures(map.value!, source)
  if (clusterFeatures) {
    clusterLayer?.getSource()?.addFeatures(clusterFeatures)
  }
}
/**
 * 聚类特征的入口方法。
 * @param {OLMap} map - OpenLayers 地图对象
 * @param {VectorSource<Point>} source - 包含点特征的数据源
 * @return {Feature<Point>[]} - 聚类后的特征列表
 */
function getClusterFeatures(map: OLMap, source: VectorSource<Point>): Feature<Point>[] {

  // 创建一个空范围对象,用于存储特征的空间范围
  const extent = createEmpty();

  // 计算地图上的距离(选项中的距离乘以地图视图的分辨率)
  const mapDistance = distance * map.getView()!.getResolution()!;

  // 获取数据源中的所有特征
  const features = source.getFeatures();
  // 记录已处理的特征的唯一标识符
  const clustered = {} as Record<string, boolean>;

  // 初始化存储聚类特征的数组
  const clusterFeatures: Feature<Point>[] = [];

  // 遍历所有特征
  for (const feature of features) {
    const uid = getUid(feature);

    // 如果特征还未处理
    if (!(uid in clustered)) {
      // 获取特征的几何体
      const geometry = feature.getGeometry();
      if (geometry) {
        // 更新范围对象为特征几何体的坐标
        createOrUpdateFromCoordinate(geometry.getCoordinates(), extent);

        // 扩展范围以包含周围区域
        buffer(extent, mapDistance, extent);

        // 查找在扩展范围内的所有特征,并排除已处理的特征
        const neighbors = source.getFeaturesInExtent(extent).filter(neighbor => {
          // 检查邻近特征是否与当前特征相似
          const isSimilar = clusterField?.every(field => neighbor.get(field) === feature.get(field));

          // 如果相似且未处理过,则标记为已处理
          if (isSimilar && !(getUid(neighbor) in clustered)) {
            clustered[getUid(neighbor)] = true;
            return true;
          }
          return false;
        });

        // 创建聚类特征并添加到列表中
        if (neighbors.length > 0) {
          clusterFeatures.push(createCluster(neighbors));
        }
      }
    }
  }

  // 返回包含聚类特征的数组
  return clusterFeatures;
}
/**
 * 聚合一组特征的几何体为一个新的特征,计算其质心。
 * @param {Array<Feature>} features 输入的特征数组
 * @return {Feature<Point>} 聚类后的特征,包含质心位置
 * @protected
 */
function createCluster(features: Feature[]): Feature<Point> {

  // 初始化质心坐标为 [0, 0]
  const centroid = [0, 0];

  // 从最后一个特征开始遍历,避免在删除特征时影响迭代
  for (let i = features.length - 1; i >= 0; --i) {
    const geometry = features[i].getGeometry() as Point;

    if (geometry) {
      // 将几何体坐标累加到质心坐标中
      addCoordinate(centroid, geometry.getCoordinates());
    } else {
      // 移除没有几何体的特征
      features.splice(i, 1);
    }
  }

  // 计算质心坐标的平均值
  scaleCoordinate(centroid, 1 / features.length);

  // 创建表示质心的点几何体
  const geometry = new Point(centroid);

  if (features.length === 1) {
    // 如果只有一个特征,直接返回该特征
    return new Feature({
      ...features[0].getProperties(),
      geometry
    }) as Feature<Point>;
  } else {
    // 返回包含质心几何体和原始特征的聚类特征
    return new Feature({
      geometry,
      features,
      ...features[0].getProperties()
    });
  }
}

/**
 * @description >>>>>>>> 全局样式函数,用于矢量要素
 * @param { Feature<Geometry> } feature 矢量要素
 * @returns { Array<Style> } 要素样式数组
 */

// 全局样式缓存
const styleCache: Record<string, Array<Style>> = {};

/**
 * 获取给定要素的样式
 * @param { Feature<Geometry> } feature 矢量要素
 * @returns { Array<Style> } 要素样式数组
 */
function getStyle(feature: Feature<Geometry>): Array<Style> {
  // 从要素中获取样式属性,设置默认值
  const color = feature.get('color') ?? 'rgba(255, 255,255,0)'; // 默认颜色为黑色
  const rotation = feature.get('rotation') ?? 0; // 默认旋转角度为0
  const size = feature.get('features') ? (feature.get('features') as Feature<Geometry>[]).length : undefined; // 聚类的个数
  const src = (feature.get('src') ? feature.get('src') : 'defalut.png')
  // 定义样式数组
  const styles: Array<Style> = [];

  // 为单个要素定义圆圈样式
  styles.push(new Style({
    image: new Icon({
      scale: 0.5,
      src,
      color,
      rotation,
      crossOrigin: 'anonymous',
    })
  }));

  // 如果有聚类要素,定义聚类标记样式
  if (size !== undefined) {
    styles.push(new Style({
      geometry: clusterGeometry(feature), // 聚类几何形状
      image: new CircleStyle({
        radius: 8,
        stroke: new Stroke({
          color: 'yellow',
        }),
        fill: new Fill({
          color,
        }),
      }),
      text: new Text({
        font: "12px Calibri, sans-serif", // 设置字体
        text: size.toString(), // 显示聚类个数
        fill: new Fill({
          color: 'yellow',
        }),
      }),
    }));
  }

  // 使用唯一的缓存键存储样式
  const cacheKey = `${color}-${size}`;
  if (!styleCache[cacheKey]) {
    styleCache[cacheKey] = styles;
  }
  return styleCache[cacheKey];
}

/**
 * @description 计算聚类角标在屏幕上的位置
 * @param { Feature<Geometry> } feature 矢量要素
 * @returns { Point } 聚类角标的新位置
 */
const clusterGeometry = function (feature: Feature<Geometry>): Point {
  // 获取几何对象和坐标
  const geometry = feature.getGeometry() as Point;
  let coordinate = geometry.getCoordinates() as Coordinate;
  // 将坐标转换为屏幕像素
  const pixel = map.value?.getPixelFromCoordinate(coordinate);
  // 如果成功获取像素,则从中心向左、下偏移10像素
  if (pixel) {
    // 使用安全访问操作符和默认值避免 undefined 错误
    const [x, y] = pixel;
    const newCoordinate = map.value?.getCoordinateFromPixel([x + 10, y + 10]);
    if (newCoordinate) {
      coordinate = newCoordinate;
    }
  }

  return new Point(coordinate);
}
onMounted(async () => {
  map.value = createMap(mapContainer.value!);
  // 打开GeoJson
  const response = await fetch(`${pointDataURL}.json`);
  const point = await response.json();
  // 加载Point
  addPointVector(map.value, 'pointLayer', point);
});

</script>
<style lang="scss">
#mapContainer {
  position: absolute;
  top: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
}
</style>

聚类最大像素距离50时,基于图标(src字段)的聚类效果:
请添加图片描述

聚类最大像素距离50时,基于颜色(color字段)的聚类效果:

请添加图片描述

三、核心代码:基于距离和要素属性聚类

/**
 * 聚类特征的入口方法。
 * @param {OLMap} map - OpenLayers 地图对象
 * @param {VectorSource<Point>} source - 包含点特征的数据源
 * @return {Feature<Point>[]} - 聚类后的特征列表
 */
function getClusterFeatures(map: OLMap, source: VectorSource<Point>): Feature<Point>[] {

  // 创建一个空范围对象,用于存储特征的空间范围
  const extent = createEmpty();

  // 计算地图上的距离(选项中的距离乘以地图视图的分辨率)
  const mapDistance = distance * map.getView()!.getResolution()!;

  // 获取数据源中的所有特征
  const features = source.getFeatures();
  // 记录已处理的特征的唯一标识符
  const clustered = {} as Record<string, boolean>;

  // 初始化存储聚类特征的数组
  const clusterFeatures: Feature<Point>[] = [];

  // 遍历所有特征
  for (const feature of features) {
    const uid = getUid(feature);

    // 如果特征还未处理
    if (!(uid in clustered)) {
      // 获取特征的几何体
      const geometry = feature.getGeometry();
      if (geometry) {
        // 更新范围对象为特征几何体的坐标
        createOrUpdateFromCoordinate(geometry.getCoordinates(), extent);

        // 扩展范围以包含周围区域
        buffer(extent, mapDistance, extent);

        // 查找在扩展范围内的所有特征,并排除已处理的特征
        const neighbors = source.getFeaturesInExtent(extent).filter(neighbor => {
          // 检查邻近特征是否与当前特征相似
          const isSimilar = clusterField?.every(field => neighbor.get(field) === feature.get(field));

          // 如果相似且未处理过,则标记为已处理
          if (isSimilar && !(getUid(neighbor) in clustered)) {
            clustered[getUid(neighbor)] = true;
            return true;
          }
          return false;
        });

        // 创建聚类特征并添加到列表中
        if (neighbors.length > 0) {
          clusterFeatures.push(createCluster(neighbors));
        }
      }
    }
  }

  // 返回包含聚类特征的数组
  return clusterFeatures;
}
/**
 * 聚合一组特征的几何体为一个新的特征,计算其质心。
 * @param {Array<Feature>} features 输入的特征数组
 * @return {Feature<Point>} 聚类后的特征,包含质心位置
 * @protected
 */
function createCluster(features: Feature[]): Feature<Point> {

  // 初始化质心坐标为 [0, 0]
  const centroid = [0, 0];

  // 从最后一个特征开始遍历,避免在删除特征时影响迭代
  for (let i = features.length - 1; i >= 0; --i) {
    const geometry = features[i].getGeometry() as Point;

    if (geometry) {
      // 将几何体坐标累加到质心坐标中
      addCoordinate(centroid, geometry.getCoordinates());
    } else {
      // 移除没有几何体的特征
      features.splice(i, 1);
    }
  }

  // 计算质心坐标的平均值
  scaleCoordinate(centroid, 1 / features.length);

  // 创建表示质心的点几何体
  const geometry = new Point(centroid);

  if (features.length === 1) {
    // 如果只有一个特征,直接返回该特征
    return new Feature({
      ...features[0].getProperties(),
      geometry
    }) as Feature<Point>;
  } else {
    // 返回包含质心几何体和原始特征的聚类特征
    return new Feature({
      geometry,
      features,
      ...features[0].getProperties()
    });
  }
}

主要思路:

  1. getClusterFeatures 函数:
  • 计算某点的缓冲范围,在范围内查找所有的邻近点
  • 验证特征并记录具有相同特征的邻近点
  • 使用createCluster聚合相同特征的邻近点
  1. createCluster 函数:
  • 计算特征邻近点的中心位置
  • 累加所有点的特征并创建新的聚类特征

四、数据说明

本文使用的矢量点数据为标准的GeoJson数据,数据下方自取,属性字段包括:

  • name:名称/编码,类型为 string | number
  • rotationx:旋转角度,范围0~1
  • src: 图片的路径
  • color:字符串颜色

数据样例:

 {
    "type": "FeatureCollection",
    "features": [
       {
            "type": "Feature",
            "properties": {
                "rotation": 0.97,
                "name": "0700",
                "src": "/img/ship.png",
                "color": "brown"
            },
            "geometry": {
                "coordinates": [
                    140.39957836095004,
                    58.443654693841836
                ],
                "type": "Point"
            }
        },
    ]
 }
 // "src": "/img/ship.png" 
 //	"src": "/img/plane.png",
 //	"src": "/img/city.png",
 //	"src": "/img/car.png",
 //	"src": "/img/house.png"',
 // 图片需要自己找,放在public/img内
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

無可言喻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值