开源的前端GIS空间分析库介绍 (一)jsts与turf

1 前言

通常,提到GIS的空间分析,我们会联系到ArcGIS,QGIS等这些GIS软件。这些工具软件,在空间处理能力方面,非常强大,是我们处理空间数据的非常有力的武器,也是一个GISer入门时很有必要掌握的关键技能。但是,这些软件,作为桌面软件时代的产品,其空间处理功能,作为软件的基本功能,并不能脱离软件独立运行,也很难满足我们的一些定制化需求。而基于这些软件进行的二次开发,虽然能一定程度上满足的我们的定制要求,但是产出通常桌面软件,不符合现今这个互联网时代的基本诉求。

在现今互联网时代,在前后端技术框架下,数据通常都是动态的,我们遇到一些空间分析的需求,通常我们可能会想到把空间分析需求放到后台去处理。这样做的好处是:1、不占用客户端资源,所有计算由服务器完成,充分发货服务器性能;2、技术路线相对比较成熟,有许多可供选择的工具包,比较Geotools、geoserver、PostGIS、Geopandas等等,参考资料也比较多。

然而,有时候关于空间分析的需求,可能只是一些功能相对较为简单的需求,为了这些需求而去引入相对复杂的工具包,似乎有些大材小用。

随着开发技术的进步,个人计算机性能的提升,以及浏览器的进化,当一个前端开发人员,再次拿到GIS空间分析的问题时,开始思考,有没有可能在浏览器端来处理这个问题?

答案是肯定的。
哈哈,废话这么多,其实就是为了引出本文的两个主角JSTS以及turf。

2 JSTS

jsts是一个javascript包,可用于处理和分析简单的空间几何。它是Java包JTS通过源码转换而来,并保留了原JTS的API。
另外,它的io模块支持与WKT、GeoJSON以及openlayers3+进化数据互转。这个功能会非常友好。

3 turf

turf是mapbox出品的用javascript写的模块化空间分析引擎,它使用geojson数据格式来进行空间处理。它包含传统的空间操作,用于创建GeoJSON数据的辅助函数以及数据分类和统计工具。

这两个包都可以在浏览器端或者在node中运行。

4 安装使用

4.1 jsts

4.1.1 直接引入

<script src="https://unpkg.com/jsts/dist/jsts.min.js"></script>

4.1.2 NPM

1) 安装

npm install jsts

2) 引入-node环境

const jsts = require('jsts')

3) 引入-esModule
这里要稍微注意下,直接 import jsts from ‘jsts’ 并不能正确引入jsts包。这里需要按需引入具体的模块,根路径是’jsts/org/locationtech/jts’。比如引入jsts的overlayOp模块

import OverlayOp from 'jsts/org/locationtech/jts/operation/overlay/OverlayOp'

4.2 turf

4.1.1 直接引入

<!-- 使用unpkg -->
<script src="https://unpkg.com/@turf/turf/turf.min.js"></script>

<!-- 在BootCDN上下载指定版本 -->
<script src="https://www.bootcdn.cn/Turf.js/"></script>

4.1.2 NPM

下载安装,可以一次安装所有模块或者只安装部分模块

1) 安装所有

安装

$ npm install @turf/turf

一次引入所有模块

import * as turf from '@turf/turf'

或者单独引入

import {
    
     lineString, along } from '@turf/turf'

node环境引入

const turf = require('@turf/turf')

2) 独立安装

安装

$ npm install @turf/collect

引入

import collect from '@turf/collect';

5 空间分析

这里举几个常见的判断空间位置关系的例子,分别使用jsts和turf进行判断

5.1 判断是否包含

// turf
const line = turf.lineString([[1, 1], [1, 2], [1, 3], [1, 4]]);
const point = turf.point([1, 2]);
console.log('turf运算结果:', turf.booleanContains(line, point))

// jsts
const reader = new jsts.io.WKTReader()
const jstsLine = reader.read('LINESTRING (1 1, 1 2, 1 3, 1 4)')
const jstsPoint = reader.read('POINT (1 2)')
console.log('jsts运算结果:', jsts.operation.relate.RelateOp.contains(jstsLine, jstsPoint))

运算结果

turf运算结果: true
jsts运算结果: true

5.2 判断是否交叉

// turf
const line1 = turf.lineString([[-2, 2], [4, 2]]);
const line2 = turf.lineString([[1, 1], [1, 2], [1, 3], [1, 4]]);
console.log('turf运算结果:', turf.booleanCrosses(line1, line2))

// jsts
const reader = new jsts.io.WKTReader()
const jstsLine1 = reader.read('LINESTRING (-2 2, 4 2)')
const jstsLine2 = reader.read('LINESTRING (1 1, 1 2, 1 3, 1 4)')
console.log('jsts运算结果:',jsts.operation.relate.RelateOp.crosses(jstsLine1, jstsLine2))

运算结果

turf运算结果: true
jsts运算结果: true

5.3 判断是否重叠

// turf
var poly1 = turf.polygon([[[0,0],[0,5],[5,5],[5,0],[0,0]]]);
var poly2 = turf.polygon([[[1,1],[1,6],[6,6],[6,1],[1,1]]])
console.log('turf运算结果:', turf.booleanOverlap(poly1, poly2))

// jsts
var reader = new jsts.io.WKTReader()
var jstsPoly1 = reader.read('POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))')
var jstsPoly2= reader.read('POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1))')
console.log('jsts运算结果:',jsts.operation.relate.RelateOp.overlaps(jstsPoly1, jstsPoly2))

运算结果

turf运算结果: true
jsts运算结果: true

5.4 缓冲

// turf
const point = turf.point([-90.548630, 14.616599]);
const buffered = turf.buffer(point, 500, {
    
    units: 'miles'});
console.log('turf运算结果:', buffered)

// jsts
const reader = new jsts.io.WKTReader()
const jstsPoint = reader.read('POINT (-90.548630 14.616599)')
const jstsBuffer = jsts.operation.buffer.BufferOp.bufferOp(jstsPoint, 500)
console.log('jsts运算结果:', jstsBuffer)

运算结果

turf运算结果: {type: 'Feature', properties: {…}, geometry: {…}}
jsts运算结果: it {_shell: ut, _holes: Array(0), _envelope: null, _userData: null, _factory: Ct, …}

以上只是抛砖引玉,如果你以为这两个包只能干这点事,那就大错特错了。实际上,这两个包的功能都非常强大,里面有非常多的GIS相关的计算操作,jsts甚至支持3维空间上的计算,欢迎大家去研究挖掘。

6 体积和性能

6.1 体积

先说体积,对于项目体积比较苛刻的同仁,可能会比较关注这点。由于功能强大,所以包体积相对来说也较大。以下是两个包最新完整版的体积大小。

项目 版本 源码体积 压缩后 *.min.js
jsts 2.6.1 914k 475k
turf 6.3.0 590k

如果不经常做前端性能优化,对体积大小不是很敏感,这里提供一个参考数据, openlayers6,压缩后的代码体积在接近1M,约是这两个的两倍。
如果我们的项目是前端工程化开发,你有特别在乎流量问题,那可以通过按需引入来减少项目最终的打包体积。

6.2 性能

6.2.1 jsts在node端

在性能方面,以判断空间包含关系为例,笔者分别在node环境和浏览器环境中,做了一些测试,查看它们的耗时情况。以下是拿jsts包在node端的测试部分代码:

var i = 0
var count = 1000000

var time1 = new Date().getTime()
var reader = new jsts.io.WKTReader()
var jstsPolygon = reader.read('POLYGON((1 1, 35 1, 35 84, 1 84, 1 1))')
while (i < count) {
    
    
  var x = Math.random() * 100
  var y = Math.random() * 100
  var p = reader.read(`POINT(${
      
      x} ${
      
      y})`)
  jsts.operation.relate.RelateOp.contains(jstsPolygon, p)
  i++
}
var time2 = new Date().getTime()
console.log(`jsts test done, ${
      
      time2 - time1}ms`)

jtst在node端,计算1万次空间包含关系,耗时91ms。

jsts test done, 91ms

如果总数设置为100w,大约需要3s。

jsts test done, 3042ms

这个耗时,似乎有点长。
不过上面代码有个问题,用了wktreader把数据翻译成jsts的几何对象。在循环次数大的情况下,这个可能会造成比较大的误差。如果只单纯考虑判断空间关系的耗时,再对比看一看。对代码稍微做调整。

const reader = new jsts.io.WKTReader()
const jstsPolygon = reader.read('POLYGON ((1 1, 35 1, 35 84, 1 84, 1 1))')
let jstsPoints = []
let i = 0
let max = 10000

while (i < max) {
    
    
  let x = Math.random() * 100
  let y = Math.random() * 100
  var jstsPoint = reader.read(`POINT (${
      
      x} ${
      
      y})`)
  jstsPoints.push(jstsPoint)
  i++
}

const time1 = new Date().getTime()
jstsPoints.forEach((p) => {
    
    
  var r = jsts.operation.relate.RelateOp.contains(jstsPolygon, p)
})
const time2 = new Date().getTime()
console.log(`jsts test done, ${
      
      time2 - time1}ms`)

好,在来跑一次看下。
1万次19ms。

jsts test done, 19ms

100万次233ms

jsts test done, 233ms

1万次从91ms变成19ms,100次从3042ms变成233ms。看来在WKTReader里确实耗费了很多时间成本。

6.2.2 jsts和turf对比

起初,笔者并没有对比两者的意思,估计两者计算速度应该相当。出于测试结果完整性考虑,将两个包都拿来跑一下。

function testJsts (count) {
    
    
  const reader = new jsts.io.WKTReader()
  const jstsPolygon = reader.read('POLYGON ((1 1, 35 1, 35 84, 1 84, 1 1))')
  let jstsPoints = []
  
  let i = 0
  while (i < count) {
    
    
    let x = Math.random() * 100
    let y = Math.random() * 100
    var jstsPoint = reader.read(`POINT (${
      
      x} ${
      
      y})`)
    jstsPoints.push(jstsPoint)
    i++
  }

  const time1 = new Date().getTime()
  jstsPoints.forEach((p) => {
    
    
    var r = jsts.operation.relate.RelateOp.contains(jstsPolygon, p)
  })
  const time2 = new Date().getTime()
  console.log(`jsts test done, ${
      
      time2 - time1}ms`)
}

testJsts(10000)
function testTurf (count) {
    
    
  const turfPoints = []
  const polygon = turf.polygon([[
    [1, 1],
    [35, 1],
    [35, 84],
    [1, 84],
    [1, 1]
  ]])
  
  let i = 0
  while (i < count) {
    
    
    let x = Math.random() * 100
    let y = Math.random() * 100
    turfPoints.push(turf.point([x, y]))
    i++
  }
  const time1 = new Date().getTime()
  turfPoints.forEach((p) => {
    
    
    turf.booleanContains(polygon, p)
  })
  const time2 = new Date().getTime()
  console.log(`turf test done, ${
      
      time2 - time1}ms`)
}
testTurf(10000)

在个人笔记本上分别运行上面代码,count分别取10000,1000000, 10000000,每个count跑三次并取结果中位数。
结果,意料之外…

node环境下

10000次 1000000次 10000000次
jsts 20 251 3395
turf 9 137 1257

浏览器环境下

10000次 1000000次 10000000次
jsts 19 447 4074
turf 10 97 797

横向看貌似turf计算效率更高,纵向看似乎turf在浏览器端表现更好于node环境,而jsts似乎更适应node环境。不过,这里只是用了一个案例做对比,不能以点概全。

由于在后端,GIS空间分析方面有很多路可以选,所以在node中的运用,机会相对来说的比较少。这里我们更多考虑在前端开始中应用。

两个包都非常优秀好用,可以弥补像openlayers、leaflet等这些前端GIS可视化库的不足,让我们处理GIS数据和进行空间分析,不再依赖于后端。

本文参考
1、Turf.js中文网 https://turfjs.fenxianglu.cn/
2、https://www.npmjs.com/package/jsts
3、http://bjornharrtell.github.io/jsts/
4、https://docs.mapbox.com/help/glossary/turf/

猜你喜欢

转载自blog.csdn.net/u012413551/article/details/116233450