简述
多边形分割是常用的数据编辑形式之一,turf.js 实现了对线的分割与共边面的合并,通过turf能够实现对面的分割,但是在实现的过程中,我们不得不对各种问题进行解决,才能实现对面分割的效果。
参考
turf.js:实现多边形分割引用的主要计算库
分割合并的流程:这篇文章对使用turf实现分割合并提供了主要思路,并对出现的问题给出了解决方案。时间允许可以看一下这篇文章。
说明
当前实现的分割只是基础版本,有以下限制:
- 仅支持操作规则多边形,不支持环形多边形
- 仅支持将一条分割线将面分割成两个面,不支持一刀多切
以上所说的缺陷可以在后续有实际需求时实现
分割逻辑
- 面转线获得pLine
- 获取pLine与splitLine的交点
- 用交点分别对两条线进行分割
- 获取分割线
- 获取被分割的pLine
- 将被分割的pLine合并成两条线
- 将两条pLine与分割合并成面,即被分割的两个面
- 增加检测判断
步骤详解
1-2
将面转成线直接使用turf.polygonToLine便能达到效果,再使用turf.lineIntersect获得两者的初步交点
const pLine = turf.polygonToLine(polygon);
// 相交点
const points = turf.lineIntersect(polygon, line);
3-6
在1-2时获取到交点,当交点数量为2时,便符合分割线的一般逻辑,然后获取分割线和被分割的面
最初,我获取上述两者使用的是turf.lineSplit,但是发现这个方法在一些情况下有问题,于是自己重写了lineSplit(仅能使用点分割),之后便获得了被两个点分割的三条线,判断这三条线哪一条是真正的分割线(起点与终点都在线上)。 用同样的方式又获得面被分割的三条线,这时候将三条线拼成两条线,便成功获取到两条面的被分割线。
7
我们拿到分割线与面的两条线,接下来需要将他们拼成面,一开始我使用的是turf.polygonize,但因为精度问题,导致无法顺利的拼接成面,于是便写了一个方法,将原本相连的两条线段合并成一条线段,这样便形成一个闭环的线段,最后生成面即可。
8
在实现分割的基础逻辑后,需要对各种分割线情况进行判断并作出处理,以下是我所做的处理:
- 检测分割线是否符合逻辑,必须要保证只有两个交点,如果交点少,或者没有交点,便反馈为不正确分割线。
- 在1-2中,我将用turf.lineIntersect所获取的交点称为初步交点,这是因为这里获取的交点,如果直接使用,由于turf分割有精度省略的问题,导致分割不成功,不过后面换成了自己写的分割,这一步检测可以省去
- 如果分割线本身的起点和终点在面的线上,获取的初步交点有可能是缺失的,需要对这类情况进行补点
- 对获取的初步交点进行校正,通过turf.nearestPointOnLine重新获取交点,对交点进行校正
问与答
turf分割中最大的问题
- 实现分割的主要依托为turf,但turf本身很多方法有精度问题,使是同一个点,点在线上的情况没有判断出来,导致结果出现问题。目前已知有精度问题的有:lineIntersect、polygonize、lineSplit
获取分割线
- 一条线被两个点分割成三条线,已知两个点的坐标,用这两个点与三条线的始末两点进行比较,符合条件的是分割线
getSplitLine(line, points, precision) {
const lineA = this.lineSplitByPoint(line, points.features[0], precision);
let lineB = this.lineSplitByPoint(
lineA.features[0],
points.features[1],
precision
);
if (lineB.features.length !== 2) {
lineB = this.lineSplitByPoint(
lineA.features[1],
points.features[1],
precision
);
}
// 找到分割线
const firstPoint = lineB.features[0].geometry.coordinates[0];
const endPoint =
lineB.features[0].geometry.coordinates[
lineB.features[0].geometry.coordinates.length - 1
];
const flag1 =
isSamePoint(firstPoint, points.features[0], precision) &&
isSamePoint(endPoint, points.features[1], precision);
const flag2 =
isSamePoint(firstPoint, points.features[1], precision) &&
isSamePoint(endPoint, points.features[0], precision);
if (flag1 || flag2) {
return featureToCoordinate(lineB.features[0]);
} else {
return featureToCoordinate(lineB.features[1]);
}
}
获取需要合并两条线
- 面转线后,它同样被两个点分割成三条线,我们需要把其中两条合成一条,同获取分割线,用同样的方式能获取到需要合并的两条线
合并线的思路
- 合并有公共点的两条线,关键在于找到线的顺序,所以需要拿两条线的始末点分别进行比较,找到他们的公共点的位置,根据比较结果将坐标数组反转,去除一个共同点,最后将数组合并。
// 获取两条线的坐标数组
const A = featureToCoordinate(lineA);
const B = featureToCoordinate(lineB);
// 找到两条线段的共同点,并返回两条线段的头与尾
// A0-----A1-B1-----B0
// 如上,发现A1-B1是共同点,所以将A0与B0返回
const AB = new Map([
[isSamePoint(A[0], B[0], precision), [A[A.length - 1], B[B.length - 1]]],
[isSamePoint(A[0], B[B.length - 1], precision), [A[A.length - 1], B[0]]],
[isSamePoint(A[A.length - 1], B[0], precision), [A[0], B[B.length - 1]]],
[isSamePoint(A[A.length - 1], B[B.length - 1], precision), [A[0], B[0]]],
]);
const arr = AB.get(true);
// 因为当前A[0]并不一定是A0,他的顺序可能是反着的,所以要进行判断
// 使注入坐标点时,顺序不会错
let aLine = A;
let bLine = B;
if (A[0] !== arr[0]) {
aLine.reverse();
}
if (B[0] === arr[1]) {
bLine.reverse();
}
bLine.shift();
aLine.push(...bLine);
return aLine;
用点分割
- 由于turf的精度问题,于是重新实现了用点分割线,实现逻辑比较简单,循环获取线上每一个线段,在获取分割点到线上最近的点的位置(nearestPointOnLine),比较分割点与最近点是否是同一个点,如果是则记录一下当前线段的序列,按当前序列将线的坐标数组分割成两个(slice),这样便实现了用点分割线
lineSplitByPoint(line, point, precision = 6) {
// 为避免直接用turf.splitLine分割失败,将取一次最近点,并增加精度测试,增加容错率
// const nearPoint = turf.nearestPointOnLine(line, point);
const coords = featureToCoordinate(line);
const pointCoords = featureToCoordinate(point);
let splitIndex = -1;
for (let i = 0; i < coords.length - 1; i++) {
const line = turf.lineString([coords[i], coords[i + 1]]);
const nearPoint = turf.nearestPointOnLine(line, point);
if (isSamePoint(point, nearPoint, precision)) {
splitIndex = i;
break;
}
}
if (splitIndex >= 0) {
const line1 = coords.slice(0, splitIndex + 1);
line1.push(pointCoords);
const line2 = coords.slice(splitIndex + 1);
line2.unshift(pointCoords);
return {
type: "FeatureCollection",
features: [coordinateToFeature(line1), coordinateToFeature(line2)],
};
} else {
return {
type: "FeatureCollection",
features: [],
};
}
}
引入精度
- 如上文所说,很多问题都是以为turf的精度问题所造成的,所以改分割类添加了一个参数——精度,可以在不同的场景下,传入不同精度,例如,默认精度是6,也就是经纬度小数位到六位,在判断点是否是同一个点时,只要小数位六位(包括六位)之前的是一样的,便确保是同一个点。添加这样一个参数,便可以将很多不可控的问题便为可控。
总结
当前实现的分割比较基础,但好在增加了很多判断,让程序出错或判断错误的情况降低,如果业务需要,也可以在此基础上实现“一刀多切”的分割、及环形多边形的分割以及多边形挖洞这些功能。
与裁剪相对的功能——合并,虽然turf的合并同样有一些问题,不过问题并不像分割这么多,稍作简单判断就能使用,后面业务有实际需求时,可以再考虑重写合并。
2021.8.18 PS
完成绘制功能是在年初的时候,写这篇文章是在七月份,最近逛GitHub的时候,偶然发现一位伊朗的开发者早在去年11月就开源了裁剪与多边形挖洞,功能更加完善,在今年四月份被mapboxgldraw选为推荐的绘制模式,如果业务需求需要你完成这类功能,我推荐直接使用这个库,当然,也可以通过阅读他的源码进行学习。
以下是相关仓库地址: