mapbox-gl | 3.1 聚合(上)

简述

聚合效果,在不同的缩放级别下,将临近的点变成一个点,在视觉上能够清晰的知道点分布情况。举一个场景,全国建立了许多充电站,如果我想将所有的充电站都标出来,那么整个地图就会变得十分拥挤,那如果在全国的视角下,可以使用一个点去代表周围有多少充电站数量(大概是一个省一点),如果在市级别,我们也可以采用同样的方案去展示充电站数量。

在通常我们实现聚类效果有两种办法,一种是Marker(html),另一种是纯Symbol(也可以是circle),两者的原理其实是相同的,Marker更适合对复杂图标效果的情景,因为Symbol能力有限,并不能表达所有的图标样式(比如饼图)。本节优先介绍Symbol

聚合有两个例子,先将一个简单例子,然后再讲一个官方的例子

示例1

代码

js 复制代码
  // 添加图片资源
  function addImageSource() {
    const images = [
      png1,
      png2,
      png3,
      png4,
      png5,
      png6,
      greenMarkerPng,
    ];
    const imageNames = ["1", "2", "3", "4", "5", "6", "greenMarker"];
    images.forEach((item, index) => {
      map.loadImage(item, (error, image) => {
        if (error) throw error;
        map.addImage(imageNames[index], image);
      });
    });
  }
  addImageSource();
  // 随机造数据
  const geojson = {
    type: "FeatureCollection",
    features: [],
  };
  for(let i = 0;i<100;i++){
    geojson.features.push({
      type: "Feature",
      properties: {
        type: 1 + Math.floor(Math.random() * 5),
      },
      geometry: {
        type: "Point",
        coordinates: [110 + Math.random()*10, 30 + Math.random()*10],
      },
    });
  }
  // 添加source
  map.addSource("cluster", {
    type: "geojson",
    data: geojson,
    cluster: true,
    clusterRadius: 30,
  });
  // 添加layer
  map.addLayer({
    id: "cluster",
    type: "symbol",
    source: "cluster",
    layout: {
      "icon-size": ["case", ["has", "point_count"], 0.5, 0.3],
      "icon-image": [
        "case",
        ["has", "point_count"],
        "greenMarker",
        ["get", "type"],
      ],
      "text-field": ["get", "point_count_abbreviated"],
      "text-offset": [0.5, -0.1],
      "text-size": 20,
      "icon-ignore-placement": true,
    },
    paint: {
      "text-color": "#F0F0F0",
      "text-halo-width": 0.5,
      "text-halo-color": "#F0F0F0",
    },
  });
  // 添加点击事件
  map.on("click", "cluster", (e) => {
    if (e.features[0].properties.point_count !== undefined) {
      console.log("cluster");
      // 放大地图
      map.flyTo({
        center: e.lngLat,
        zoom: map.getZoom() + 2.5,
      });
    } else {
      console.log("single");
    }
  });
  map.flyTo({
    center: [110, 35],
    zoom: 3,
  });

示例逻辑

  1. 添加需要的图片资源
  2. 生成一些示例数据
  3. 添加source
  4. 添加layer
  5. 添加点击事件

解析

  1. 添加需要的图片资源
js 复制代码
  function addImageSource() {
    const images = [
      png1,
      png2,
      png3,
      png4,
      png5,
      png6,
      greenMarkerPng,
    ];
    const imageNames = ["1", "2", "3", "4", "5", "6", "greenMarker"];
    images.forEach((item, index) => {
      map.loadImage(item, (error, image) => {
        if (error) throw error;
        map.addImage(imageNames[index], image);
      });
    });
  }

这一步使用了api两个方法 loadImageaddImage, 两个方法在1.8中介绍过,不再重复介绍。这一步创建了图片资源和图片名称两个数组,长度相同,随后循环添加图片。

  1. 生成一些示例数据
js 复制代码
  // 随机造数据
  const geojson = {
    type: "FeatureCollection",
    features: [],
  };
  for(let i = 0;i<100;i++){
    geojson.features.push({
      type: "Feature",
      properties: {
        type: 1 + Math.floor(Math.random() * 5),
      },
      geometry: {
        type: "Point",
        coordinates: [110 + Math.random()*10, 30 + Math.random()*10],
      },
    });
  }

创建geojson格式的点数据,生成范围在经度110120,纬度30 40之间,同时生成一个属性type其值为1-6之间的整数

  1. 添加source
js 复制代码
  // 添加source
  map.addSource("cluster", {
    type: "geojson",
    data: geojson,
    cluster: true,
    clusterRadius: 30,
  });

addSource添加上一步生成的随机数据,与以往不同的是,这里设置了两个参数 clusterclusterRadius。这里需要注意的是,虽然聚合效果是在layer上所展现的,但是设置聚合却是在source中设置的。

参数 描述
cluster 是否聚合,默认为false
clusterRadius 聚合半径,在cluster为true时生效,默认为50

示例这一步的效果为:添加名为cluster的数据源,并开启聚合,且聚合半径为30,需要注意的是,这里的聚合半径的单位即不是像素,也不是实际的长度单位。官方解释为:如果聚合半径为512,那么就等于瓦片的宽,也就是和所在瓦片有关(或者说是层级),如果上述解释还是不太理解,当作像素理解也可以。

addSource还可以设置其他一些属性,感兴趣的可以自行查阅官方文档

  1. 添加layer
js 复制代码
  // 添加layer
  map.addLayer({
    id: "cluster",
    type: "symbol",
    source: "cluster",
    layout: {
      "icon-size": ["case", ["has", "point_count"], 0.5, 0.3],
      "icon-image": [
        "case",
        ["has", "point_count"],
        "greenMarker",
        ["get", "type"],
      ],
      "text-field": ["get", "point_count_abbreviated"],
      "text-offset": [0.5, -0.1],
      "text-size": 20,
      "icon-ignore-placement": true,
    },
    paint: {
      "text-color": "#F0F0F0",
      "text-halo-width": 0.5,
      "text-halo-color": "#F0F0F0",
    },
  });

添加一个点图层cluster,数据源设置为cluster, 在上面代码中,首先需要说明的是以下两个

js 复制代码
"icon-size": ["case", ["has", "point_count"], 0.5, 0.3],
"text-field": ["get", "point_count_abbreviated"],

以往的入门文章中,很少去讲点图层的样式,是因为这里面的东西非常多,只能见一个讲一个,不增加理解负担。首先需要知道的是,在mapbox中,点图层包含了图标和文字两个部分,可以是仅图标,可以是仅文字,也可以两者都有(同一图层)。而其他线、面图层都是没有文字的,需额外的点图层,这部分会在2.x中讲解。

icon-sizetext-field分别设置的是图标的大小和展示文字的字段,icon-size 用了case表达式,意思为如果有 point_count字段,设置为0.5,否则是0.3。这里最大疑惑是point_countpoint_count_abbreviated两个字段,在我们生成的数据里,只有type一个属性字段,这两个是哪里来的呢?

在设置聚合后,当多个点聚合成1个聚合点时,这个聚合点的属性中就会出现新的字段:

参数 描述
cluster 是否是聚合点
cluster_id 聚合ID(不重复)
point_count 被聚合的数量
point_count_abbreviated 被聚合的数量(缩写)

根据以上字段,可以区分聚合点和原始点,这样便可以设置各自的样式。

js 复制代码
      "icon-image": [
        "case",
        ["has", "point_count"],
        "greenMarker",
        ["get", "type"],
      ],

icon-image: 当有字段point_count时,图标是greenMarker,否则用type字段值的图片,前面步骤中,造的type字段数据为16的整数,添加的图片名称相应的也是16,这样便匹配上了。

js 复制代码
 "text-offset": [0.5, -0.1], // 文字的偏移,分别是左右偏移(右为正),上下偏移(下为正)
  "text-size": 20, // 文字大小 单位是像素
  "icon-ignore-placement": true, //是否忽略碰撞
  // paint
  "text-color": "#F0F0F0", // 文字颜色
  "text-halo-width": 0.5, // 字体轮廓宽度
  "text-halo-color": "#F0F0F0",// 字体轮廓颜色
  // 上面三个是为了加粗字体,其他加粗字体的方法比较麻烦

到了这一步,我们可以先看一下效果

聚合

放大

再放大

  1. 添加点击事件
js 复制代码
  map.on("click", "cluster", (e) => {
    if (e.features[0].properties.point_count !== undefined) {
      console.log("cluster");
      // 放大地图
      map.flyTo({
        center: e.lngLat,
        zoom: map.getZoom() + 2.5,
      });
    } else {
      console.log("single");
    }
  });

在这一步中,给cluster图层注册点击事件,聚合点的属性字段有point_count,根据这个判断我们点击的是聚合点和原始点,在这步可以打印e.features[0]查看其他的属性。通过判断,对聚合点和原始点执行不同的事件

总结

本节是聚合的第一个例子,整个过程相对简单,只是需要理解聚合多出来的属性和数据源的设置,接下来是官网的一个例子。

如有错误,欢迎指正;如有疑问,评论留言。

相关推荐
ObjectX前端实验室27 分钟前
【react18原理探究实践】异步可中断 & 时间分片
前端·react.js
SoaringHeart30 分钟前
Flutter进阶:自定义一个 json 转 model 工具
前端·flutter·dart
努力打怪升级32 分钟前
Rocky Linux 8 远程管理配置指南(宿主机 VNC + KVM 虚拟机 VNC)
前端·chrome
brzhang1 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang1 小时前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构
reembarkation1 小时前
自定义分页控件,只显示当前页码的前后N页
开发语言·前端·javascript
gerrgwg2 小时前
React Hooks入门
前端·javascript·react.js
ObjectX前端实验室2 小时前
【react18原理探究实践】调度机制之注册任务
前端·react.js
汉字萌萌哒2 小时前
【 HTML基础知识】
前端·javascript·windows
ObjectX前端实验室3 小时前
【React 原理探究实践】root.render 干了啥?——深入 render 函数
前端·react.js