前言
最近项目有一个需求,要求使用前端生成等值线图数据,并且因为甲方使用的自己的三维地图显示sdk,所以,我不得不离开舒适的cesium去研究纯js的geojson生成,总而言之对于我这种地理信息处理半桶水还是有点难度的,但是好在最后还是没出啥问题的搞出来了,于是记录一下
获取geojson
总体上来讲,分为三步
- 第一步,使用kriging.train生成variogram对象
- 第二步,使用kriging.grid生成栅格化数据,
推导每个栅格的值是多少,并将值在一个区间内的栅格放到一个多边形内
1. 拿到的数据
我们手头能拿到的数据有点位数据,即经纬度加上数值,大概是这样
[ { lat: 'xxx', lng: 'xxx', value: 'xxx' }, ...]
还有地图的边界数据,即渲染等值线的总区域的边界线数据,用于生成合适大小的矩形等值线图和最后裁剪
2. 获取kriging.train()的参数
有了这两个数据之后,我们首先处理点位数据,krigingjs的train方法要求传入经度数组
这个是前端基本功,通过for循环分别将value,lat,lng的值 push到 values/lats/lngs 三个数组中,代码我就不写了
3. 生成variogram对象
没啥好说的,直接照着写
// 参数照着写就行,我也不懂
let params = {
//model可选'gaussian(高斯模型)','spherical(球形模型)',exponential(指数模型)
krigingModel: 'exponential',
krigingSigma2: 0,
krigingAlpha: 100,
canvasAlpha: 0.75,//canvas图层透明度
};
let variogram = kriging.train(
values,
lons,
lats,
params.krigingModel,
params.krigingSigma2,
params.krigingAlpha
);
console.log('variogram', variogram);
4. 获取kriging.grid()的参数,并生成grid对象
需要polygons/variogram/width三个参数
- polygons代表生成的等值线图的矩形边界
- variogram就是处理过的点位数据
- width是栅格的宽度
width看着填就行,也有算出来的,但我觉得没啥必要,所以现在只需要获取polygons,使用turf.bbox()方法获取
import boundaries from '@/assets/js/haishu1.json'
let polygons = [];
const extent = turf.bbox(boundaries)
polygons.push([
[extent[0], extent[1]], [extent[0], extent[3]],
[extent[2], extent[3]], [extent[2], extent[1]]
]);
let grid = kriging.fastGrid(polygons, variogram, 0.0015);
5. 利用网格计算点集
计算栅格的点对应的经纬度和值
const gridFeatureCollection = function (grid, xlim, ylim) {
var range = grid.zlim[1] - grid.zlim[0];
var i, j, x, y, z;
var point
var n = grid.length;//列数
var m = grid[0].length;//行数
var pointArray = [];
for (i = 0; i < n ; i++)
for (j = 0; j < m ; j++) {
x = (i) * grid.width + grid.xlim[0];
y = (j) * grid.width + grid.ylim[0];
z = +grid[i][j];
const isNaN = Number.isNaN(z)
if(isNaN) {
// 有时候z可能是NaN,应该是数据本身有问题,这里我看缺的部分周围的值,给了一个中间值
z = 1.2494884579461856
}
point = turf.point([x, y], { value: z })
pointArray.push(point);
}
return pointArray;
}
let fc = gridFeatureCollection(grid);
6.利用turf.isobands()生成等值面的多边形
需要的参数 :
- collection : 点集,生成等值线图的数据支撑
- scaleList : 等值面颜色
- 其他设置,下面使用的有
- zProperty: 取features的properties中的哪个属性为值
- commonProperties: 通用设置,fill-opacity为透明度设置
- breaksProperties: 设置等值的区间,同一区间内的值视为等值
a. collection的获取
利用turf画等值面,collection参数要处理成geojson类型,将点集处理一下
var collection = turf.featureCollection(fc);
console.log('collection', collection);
b. scaleList的获取
项目要求有常规展示和随值展示两种,常规展示为固定区间,随值展示会根据数据中值的大小获取区间
总而言之,固定值叫产品给你,随值问产品需要多少个刻度然后最大之间最小值除一下push到数组里就行
// 结果大概是这种
let scaleList = [-10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45]
c. 其他设置
var isobands_options = {
zProperty: "value",
commonProperties: {
"fill-opacity": 0.8 //设置通用属性
},
breaksProperties: [
{fill:'#40a4c0'},
{fill:'#6ec2bf'},
{fill:'#92c1a5'},
{fill:'#bab37c'},
{fill:'#c3af71'},
{fill:'#f29d40'},
{fill:'#ef6c31'},
{fill:'#ee6932'},
{fill:'#e65231'},
{fill:'#af444b'},
] // 设置每个区间的颜色
};
d. 生成等值线图的geojson,并优化
var isobands = turf.isobands(
collection,
opt.scaleList, // 设置值的颜色区间
isobands_options
);
console.log('isobands', isobands);
//按照面积对图层进行排序,规避turf的一个bug
function sortArea(a,b) {
return turf.area(b)-turf.area(a);
}
isobands.features.sort(sortArea)
// turf.flatten 扁平化数据,减少嵌套层级
boundaries = turf.flatten(boundaries);
isobands = turf.flatten(isobands);
e. 对于超出边界的区域进行裁剪
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);
得到的 intersection 就是我们要的最终的等值线的geojson了,渲染的话,直接通过cesium的api加载就行,如果你和我一样用的sdk没有这么方便的功能,那么你需要根据 intersection 中的多边形数组一个一个渲染,还要注意有孔的和多面的多边形,总之对于不熟悉geojson中属性带变得含义的人来说还是很麻烦的,以防万一,我也把我处理的函数写下来
顺便放一下geojson属性的解释# GeoJSON 对象
注 : earth参数是地图sdk的实例,不需要太关注,data是上面做好的等值线图的geojson
export function main(earth, data) {
for(let i = 0; i<data.features.length; i++) {
const item = data.features[i];
item.geometry.coordinates.forEach(item1 => {
let {points,interPoints} = getPoint(item1)
console.log('points =======> ',points);
const opt = {...item.properties}
createElementPolygon(earth, {points,interPoints}, opt)
})
}
}
function getPoint(item1){
let points = [],interPoints=[]
// 面
let point = item1[0];
// 有孔
if(point.length > 2){
item1.forEach((item2,index) => {
if(index === 0){
item2.forEach(item3 => {
let [x,y] = item3
points.push(StampGis.Cartesian3.fromDegrees(x, y, 0))
})
}else{
let interPoint = []
item2.forEach(item3 => {
let [x,y] = item3
interPoint.push(StampGis.Cartesian3.fromDegrees(x, y, 0))
})
interPoints.push(interPoint)
}
})
}
else{
// 无孔
item1.forEach(item2 => {
let [x,y] = item2
points.push(StampGis.Cartesian3.fromDegrees(x, y, 0))
})
}
return {points,interPoints}
}
BUG
在使用的过程中,我发现数据多数处于一个区间内的时候,该区间可能会生成大量空隙,能看到turf.isobands()拿到的geojson中feature.geometry下coordinates是空数组,暂时不知道是什么原因,所以也没解决,所以有大佬看到这篇文章有兴趣能指点一下真的拜谢了!
目前我使用的方法是通过计算所有值的平均值所在的区间,将该区间的颜色作为底图添加在等值线图的最底层,算是没办法的办法,方法我写在下面了
function drawIso() {
//...之前的代码需要将colorList单独拿出来定义一个变量
const scaleList = this.getScaleList(max,min)
const colorList = [
{fill:'#04007a'},
{fill:'#121aac'},
{fill:'#253fd1'},
{fill:'#2569dc'},
{fill:'#3084ed'},
{fill:'#58b9f4'},
{fill:'#91d8ff'},
{fill:'#b8eff1'},
]
console.log('scaleList', scaleList);
const isoData = isoLine({
colorList,
scaleList,
krikingOpt: {
lons,lats,values
},
})
console.log('isoData',isoData);
let colorIndex
const average = values.reduce((pre, item) => {
return pre + +item
},0) / values.length
scaleList.forEach((item,index) => {
if(+item >= average && +(scaleList[index+1]) <= max) colorIndex = index
})
main(this.stampAPI.usearth, isoData, colorList[colorIndex].fill)
}
在之前写的main函数中最开始调用isoBaseLayer()函数
export function main(earth, data) {
isoBaseLayer(earth, isoBaseLayerFill)
// ....剩下的
}
最后是添加底图的方法
function isoBaseLayer(earth,fill) {
if(!fill) return
const data = boundaries1.features[0].geometry.coordinates[0].map(item => {
return StampGis.Cartesian3.fromDegrees(item[0], item[1], 50)
})
// createElementPolygon就是接收多边形的笛卡尔3数组,创建多边形,使用你自己的创建多边形函数
createElementPolygon(earth, {points:data}, {
'fill-opacity': 0.5, fill: fill
})
}