通常,提到GIS的空间分析,我们会联系到ArcGIS,QGIS等这些GIS软件。这些工具软件,在空间处理能力方面,非常强大,是我们处理空间数据的非常有力的武器,也是一个GISer入门时很有必要掌握的关键技能。但是,这些软件,作为桌面软件时代的产品,其空间处理功能,作为软件的基本功能,并不能脱离软件独立运行,也很难满足我们的一些定制化需求。而基于这些软件进行的二次开发,虽然能一定程度上满足的我们的定制要求,但是产出通常桌面软件,不符合现今这个互联网时代的基本诉求。
在现今互联网时代,在前后端技术框架下,数据通常都是动态的,我们遇到一些空间分析的需求,通常我们可能会想到把空间分析需求放到后台去处理。这样做的好处是:1、不占用客户端资源,所有计算由服务器完成,充分发货服务器性能;2、技术路线相对比较成熟,有许多可供选择的工具包,比较Geotools、geoserver、PostGIS、Geopandas等等,参考资料也比较多。
然而,有时候关于空间分析的需求,可能只是一些功能相对较为简单的需求,为了这些需求而去引入相对复杂的工具包,似乎有些大材小用。
随着开发技术的进步,个人计算机性能的提升,以及浏览器的进化,当一个前端开发人员,再次拿到GIS空间分析的问题时,开始思考,有没有可能在浏览器端来处理这个问题?
答案是肯定的。
哈哈,废话这么多,其实就是为了引出本文的两个主角JSTS以及turf。
jsts是一个javascript包,可用于处理和分析简单的空间几何。它是Java包JTS通过源码转换而来,并保留了原JTS的API。
另外,它的io模块支持与WKT、GeoJSON以及openlayers3+进化数据互转。这个功能会非常友好。
turf是mapbox出品的用javascript写的模块化空间分析引擎,它使用geojson数据格式来进行空间处理。它包含传统的空间操作,用于创建GeoJSON数据的辅助函数以及数据分类和统计工具。
这两个包都可以在浏览器端或者在node中运行。
<script src="https://unpkg.com/jsts/dist/jsts.min.js">script>
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'
<script src="https://unpkg.com/@turf/turf/turf.min.js">script>
<script src="https://www.bootcdn.cn/Turf.js/">script>
下载安装,可以一次安装所有模块或者只安装部分模块
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';
这里举几个常见的判断空间位置关系的例子,分别使用jsts和turf进行判断
// 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
// 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
// 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
// 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维空间上的计算,欢迎大家去研究挖掘。
先说体积,对于项目体积比较苛刻的同仁,可能会比较关注这点。由于功能强大,所以包体积相对来说也较大。以下是两个包最新完整版的体积大小。
项目 | 版本 | 源码体积 | 压缩后 *.min.js |
---|---|---|---|
jsts | 2.6.1 | 914k | 475k |
turf | 6.3.0 | – | 590k |
如果不经常做前端性能优化,对体积大小不是很敏感,这里提供一个参考数据, openlayers6,压缩后的代码体积在接近1M,约是这两个的两倍。
如果我们的项目是前端工程化开发,你有特别在乎流量问题,那可以通过按需引入来减少项目最终的打包体积。
在性能方面,以判断空间包含关系为例,笔者分别在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里确实耗费了很多时间成本。
起初,笔者并没有对比两者的意思,估计两者计算速度应该相当。出于测试结果完整性考虑,将两个包都拿来跑一下。
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/