开发

软件开发相关知识

Javascript(turfjs)画等值线图

绘制气象与环境空间领域的等高线图常依托于NCL及Python这类工具,尤其在传统情境下。然而,在以下几种情形里,采取不同路径变得尤为必要:


- 当目标是在网页端迅速实现等值线的简易绘制时,

- 若后台系统主要构建于Node.js平台,

- 对于专注于前端开发的技术人员而言,

- 特别是在需要纯JavaScript解决方案的场合,


本文便探索了一种新途径,旨在指导如何直接运用纯JavaScript技术,基于空间散点数据,动手实现等值线或颜色填充图的绘制过程。


1. 前期准备

   - turf.js:作为核心的空间分析利器,turf.js 能在浏览器环境与Node.js环境中自如运作,专注于GeoJSON格式的数据处理,为地理空间信息分析提供便利。

   - 散点数据集:这是实战中的关键素材,设想为一系列环境监测站点的温度读数或空气质量指数(如PM2.5),每一点都携带特定地理位置的意义。

   - 区域边界GeoJSON:为了聚焦特定区域并制作清晰的色斑图,需定义一个裁剪边界。

   - Mapbox:作为强大的Web地图展示平台,Mapbox助力在网页上展示从原始数据到最终分析结果的全链条,无论是基础底图还是高级数据可视化均能胜任。

   - CodePen:虽然非绘制等值线的直接要求,但此在线前端开发工具为我们的演示添彩,提供了一个实时互动的环境来逐步构建和展示整个编码过程。


2.基本流程

map.addLayer(
turf.intersect(
  turf.isobands(
    turf.interpolate(
	load_data_geojson(data)
    )
  ),boundaries)
)

基本步骤概括为以下序列 —— 首先,对散点(Points)数据实施插值处理;接着,依据插值结果绘制等值线;随后,对生成的等值线执行区域裁剪操作;最后,完成数据的渲染与展示。这一流程深刻体现了turfjs的工作原理,即在整个处理链中,GeoJSON数据作为通用语言,在各个函数间无缝流转。


百度坐标拾取工具 : https://api.map.baidu.com/lbsapi/getpoint/index.html


3.业务数据

一组离散点的业务数据,如下所示:

let data = [  
    {"Lat":39.958087,"Lon":116.338153,"value":3.2},
    {"Lat":39.975783,"Lon":116.501429,"value":1.6},
    {"Lat":39.880615,"Lon":116.477283,"value":3.5},
    {"Lat":39.894345,"Lon":116.318031,"value":4},
    {"Lat":39.939944,"Lon":116.39852,"value":0.8}
]

需要把 data 转为一组 feature。使用 array.map() 就好

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

随机做一些业务数据

let 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()

turf.interpolate() 引入了一种基于IDW(反距离权重)算法的功能,用于将数据平滑地转换到网格系统上。该插值过程的精细程度受第二参数及interpolate_options.units共同调控,其中单位可选degrees、radians、miles或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()

此阶段涉及利用先前插值生成的网格点来创建等值区域图,并为这些区域分配不同的颜色以便于区分。具体实现通过调用turf.isobands()函数完成,它依据属性zProperty对数据进行分层处理,进而得到一系列多边形区域(MultiPolygon),每个多边形代表一个特定的数值范围或等值面。

let isobands_options = {
  zProperty: "value", //z轴的值
  commonProperties: {
    "fill-opacity": 0.7 //显示不透明度
  },
  breaksProperties: [
    {fill: "#111"},
    {fill: "#222"},
    {fill: "#333"},
    {fill: "#444"},
    {fill: "#555"},
    {fill: "#666"},
    {fill: "#777"},
    {fill: "#888"}
  ]
};
let isobands = turf.isobands(
  grid,
  [0, 10, 20, 30, 50, 80, 100],
  isobands_options
);

6.turf.intersect()

在此步骤中,我们将借助已准备好的边界来对整个色彩分布图进行裁剪操作。为了实现这一目的,我们将运用turf.intersect()方法。值得注意的是,根据官方文档指引,该函数要求输入的数据类型为Feature<Polygon>。鉴于我们当前持有的数据结构为MultiPolygon,因此,在执行裁剪操作前,需要先通过flatten()函数将其转换为合适的格式,以确保与turf.intersect()的兼容性。

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

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

let 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);
  }
 });
});
 
let intersection = turf.featureCollection(features);

7.异常处理

在完成等值线的绘制后,有时可能会产生不符合规范的多边形实体,比如在地理信息系统(GeoJSON标准中提及的“洞”内部意外出现了多边形形状,若对此概念不清晰,建议查阅GeoJSON规范以加深理解)。这类不规则多边形在执行turf.intersect()操作时,会导致错误发生。因此,需要在代码中引入了一个错误处理机制作为应对,一个常见策略是对这些多边形应用turf.buffer()方法,通过设置合适的缓冲区大小,可以有效消除那些微小且不合规的多边形碎片,从而净化数据集,确保后续的空间分析能够顺利进行。

8.处理性能

因为程序涉及的计算强度相当大,尤其是在采用高精度边界时,其执行时间可能会超过数据插值步骤,显得尤为耗时。因此,若目标仅仅是展示边界范围内彩色斑点图,一种更高效的策略是借助turf.mask()函数来创建一个覆盖层作为视觉遮罩。该遮罩随后可在WebGIS平台上叠加于彩色斑点图层上,既节省了计算资源,又能准确实现预期的视觉效果。

9.叠加地图

最后阶段,将制备好的等值线GeoJSON数据层添加至地图上,这一过程通过MapBox技术完成。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"
    ]
  }
});

10.总结

我们通过一系列步骤运用turfjs成功地完成了散点数据等值线的绘制及渲染工作,成果展示详见Codepen链接。尽管在此次应用中,JavaScript成功实现了目标,与Python或NCL相比,在专业性上略显不足,尤其是在地理空间分析领域,JavaScript生态系统中高性能的插值算法实现较为稀缺。


就性能角度考量,文中提出的解决方案在浏览器环境下的处理能力仅适用于较小规模的数据集。为进一步提升效率,未来可探索实施空间数据索引策略、限制插值计算范围等优化手段,以增强其处理更大数据集的能力。