15

使用气象、环境类空间数据绘制等值线通常是由 NCL、Python 来做,在一些场景中:

  1. 你只是想在 WEB 端做一些简单的绘制
  2. 你的后端只有 Node.js 环境
  3. 你纯粹是个前端工程师

你也许需要使用纯 Javascript 来做这件事。本文尝试根据空间中的一组散点来绘制等值线图(或色斑图)。

1. 准备工作

  1. turfjs, 空间分析(geospatial analysis)工具包,支持在浏览器和 Node.js 环境中运行,空间数据的输入输出使用 GeoJSON 编码。本文对应的版本是 turfjs@5.1.6 。
  2. 一组散点数据,这是业务数据了,可以是一些观测点的温度或PM2.5。
  3. 一个边界,用于裁剪出一幅针对某一区域干净的色斑图,格式为 GeoJSON。推荐使用 geojson.io 来查看或编辑这类数据,这里给出一个城市的 boundaries 做示例。
  4. Mapbox, 一个 WebGIS 引擎,用来渲染输入、输出以及以一些中间数据。
  5. Codepen ,一个交互式的在线前端 IDE,这不是等值线绘制必备的工具,只是为演示这个过程提供一个载体。

2. 基本流程

workflow Generated by carbon

基本流程就是这样一串操作 —— 散点(Points)经过插值、等值线绘制、裁剪和渲染展示。按照 turfjs 的哲学,各个函数间传递GeoJSON 数据。

3. load_data_to_geojson()

image.png

一般业务数据往往是一个数组,大概是这样

var data = [  
    {"Lat":36.18,"Lon":103.75,"value":1.7},
    {"Lat":36.17,"Lon":103.29,"value":1},
    {"Lat":37.98,"Lon":102.75,"value":4},
    {"Lat":36.59,"Lon":104.91,"value":3},
    {"Lat":36.22,"Lon":107.78,"value":0.1}
]

你需要把 data 转为一组 featurearray.map() 大法好~

var features = data.map(i => {return {
      type: "Feature",
      properties: {
        value: i.value
      },
      geometry: {
        type: "Point",
        coordinates: [i.Lon, i.Lat]
      }
    }
  }
)
var points = turf.featureCollection(features);

你没有业务数据? 那就随机来一些

var points = turf.randomPoint(30, { bbox: turf.bbox(boundaries) });

//再生成些随机数做属性
turf.featureEach(points, function (currentFeature, featureIndex) {
  currentFeature.properties = { value: (Math.random() * 100).toFixed(2) };
});

4. turf.interpolate()

image.png

turf.interpolate() 提供了基于 IDW(反距离权重)算法的将数据插值为格点的方法。插值的精度是由第二个参数与 interpolate_options.units 共同决定的,单位支持 degrees, radians, miles, or kilometers,IDW 要为每个格点计算所有散点的权重,计算规模是 (散点数 * 格点数),所以要在精度与性能间做好平衡。 我们将之前的散点(points)代入

var interpolate_options = {
  gridType: "points",
  property: "value",
  units: "degrees",
  weight: 10
};
var grid = turf.interpolate(points, 0.05, interpolate_options);
// 适当降低插值结果的精度便于显示
grid.features.map((i) => (i.properties.value = i.properties.value.toFixed(2)));

5. turf.isobands()

image.png

这一步基于插值获得的格点绘制等值区域,并为区域配置颜色。turf.isobands() 根据 zProperty 分段,形成一些 MultiPolygon

var isobands_options = {
  zProperty: "value",
  commonProperties: {
    "fill-opacity": 0.8
  },
  breaksProperties: [
    {fill: "#e3e3ff"},
    {fill: "#c6c6ff"},
    {fill: "#a9aaff"},
    {fill: "#8e8eff"},
    {fill: "#7171ff"},
    {fill: "#5554ff"},
    {fill: "#3939ff"},
    {fill: "#1b1cff"}
  ]
};
var isobands = turf.isobands(
  grid,
  [1, 10, 20, 30, 50, 70, 100],
  isobands_options
);

到这步,你就有了覆盖整个格点的色斑图。

6. turf.intersect()

image.png

这一步,我们利用准备的边界来裁剪整个色斑图。这里要用到 turf.intersect(),根据文档,这里输入的参数要 Feature<Polygon\> ,而我们拿到的是 MultiPolygon,需要先 flatten() 处理一下。

boundaries = turf.flatten(boundaries);
isobands = turf.flatten(isobands);

之后对每个 Polygon 做一次 intersect() 操作。

var features = [];

isobands.features.forEach(function (layer1) {
  boundaries.features.forEach(function (layer2) {
    let intersection = null;
    try {
      intersection = turf.intersect(layer1, layer2);
    } catch (e) {
      layer1 = turf.buffer(layer1, 0);
      intersection = turf.intersect(layer1, layer2);
    }
    if (intersection != null) {
      intersection.properties = layer1.properties;
      intersection.id = Math.random() * 100000;
      features.push(intersection);
    }
  });
});

var intersection = turf.featureCollection(features);

6.1 异常处理

image.png

色斑图绘制之后,可能会生成一些非法 Polygon ,例如 在 hole 里存在一些形状(听不懂?去查一下 GeoJSON 的规范),我遇到的一个意外情况大概是这样,这种 Polygon 在做 intersect() 操作的时候会报错,所以在代码中做了个容错操作。解决的方法通常就是做一次 turf.buffer() 操作,这样可以把一些小的碎片 Polygon 清理掉。

6.2 性能

image.png

这个操作的计算量很大,在使用精细边界时,运行耗时甚至超过插值过程,所以如果仅仅是为了渲染一个边界范围内的色斑图,那建议利用 turf.mask() 做一个遮罩,在 WebGIS 引擎里叠加到色斑图层之上,可以达到预期的效果。

7. map.addLayer()

最后一步工作就是形成的色斑 GeoJSON 叠加到地图上了,这里使用 MapBox 来实现,利用其 expressions 功能,可以很便捷的实现一些样式渲染和交互效果。

  map.addSource("intersection", {
    type: "geojson",
    data: intersection
  });  map.addSource("intersection", {
    type: "geojson",
    data: intersection
  });
  map.addLayer({
    id: "intersection",
    type: "fill",
    source: "intersection",
    layout: {},
    paint: {
      "fill-color": ["get", "fill"],
      "fill-opacity": [
        "case",
        ["boolean", ["feature-state", "hover"], false],
        0.8,
        0.5
      ],
      "fill-outline-color": [
        "case",
        ["boolean", ["feature-state", "hover"], false],
        "#000",
        "#fff"
      ]
    }
  });

8. 结论

综上,通过一串操作,我们使用 turfjs 实现了散点数据的等值线绘制和渲染,最终的效果请访问 Codepen 。对比在 Python 或 NCL, Javascript 虽然完成了任务,但略显业余,比如,在 Javascript 生态中,达到业务应用水平的插值的算法实现非常少。性能方面,本文给出的方案在浏览器中表现只能应对非常有限的数据规模,仍可以通过空间数据索引、控制插值计算规模等方法进行优化。


舍瓦温
114 声望35 粉丝

引用和评论

0 条评论