2024-4-22日早9:12分,办公桌上的茶杯正向下吐出它多余的烟雾,旁边手机屏幕上倒映出它安逸的表情。
嘟嘟嘟,嘟————
我拾起响着电话铃声的手机,“嘿,清风啊,我们那个地图的页面不是有个标绘时区域冲突检测的功能嘛,我想在标绘的时候,两个区域只有一条边重合的情况不算做冲突,这个你们那边可以改一下吗?”
“啊?为什么要这么做?”这人一直都很会来事。
“那个,因为有些区域是挨在一起的嘛,标绘区域的时候,不小心就会和另一个区域边角上有点重合,然后就提示冲突了。”
“画的时候可以稍微分开一点吧!这个场景是有3套地图可以切换使用的,一修改,3个地图都要支持。”这不是一个重要功能,但改动起来可能有些麻烦,我想找点理由推辞他这个需求。
“可以的话要不你先看看能不能做?”又来了,这是他的惯用方法,用委婉的语气让你打消回绝的心思。
“嗯...,我先看看能不能实现吧。”谁让人家是客户呢。
01. 4月22记
我画了一些此功能需要达到的效果,我想,先明确要满足的情况,我才能知道怎么做。
即:在以前区域冲突检测功能的基础上,支持边界可以重合。
之前的区域冲突检测功能是3个地图分别用不同的方法实现的,我得先各自去看看他们各自有没有这样的支持。
- 高德地图:判断两经纬度是否相交的api
doesRingRingIntersect()并没有移除边重合判定的这种参数,不可行。 - 客户方的地图:未找到相关支持,而且文档不全...
leaflet.js:本身并没有这种计算库,使用的是之前从网上找到的一个碰撞检测算法,但边的重合会被算做冲突!
另外turf.js, clipper.js中我也未找到比较直接的支持。这些第三方库似乎都不会直接提供这种奇怪的功能,我只能自己在算法上改造了吗?
02. 4月23记
考虑直接在leaflet.js那套碰撞检测算法基础上做修改实现,它是我在网上找到的示例。
我们在地图上标绘的区域都不足一个机场大小,可以直接使用区域的经纬度坐标当做平面直角坐标系中那样在欧式空间中计算,误差在1cm以内,可以满足我们的需求。
此外,还可以将原本其它两个地图的冲突检测计算方法都改用此法完成,还方便维护。我之前就应该这么做了,当时为什么没这么想呢?太久,忘了!
我重新审视,梳理出它的判断步骤是这样的:
- 步骤一:用
isCross()计算两个多边形区域是否有边的交叉,有则判为冲突,无则进行步骤二。 - 步骤二:用
isPointInPolygon()计算区域1的所有顶点是否都在区域2中,在则是区域2包含区域1,判为冲突,否则进行步骤三。 - 步骤三:同步骤二,但是计算区域2的所有顶点是否都在区域1中。
/***判断两个线段是否相交
* L1:(x1,y1) (x2,y2)
* L2:(x3,y3) (x4,y4)
*/
function isCross(x1, y1, x2, y2, x3, y3, x4, y4) {
const term1 = Math.min(x1, x2) <= Math.max(x3, x4);
const term2 = Math.min(y3, y4) <= Math.max(y1, y2);
const term3 = Math.min(x3, x4) <= Math.max(x1, x2);
const term4 = Math.min(y1, y2) <= Math.max(y3, y4);
const expression = 0.00000000000000001;
// 判断两线段矩形区域,不相交时返回false
if (!(term1 && term2 && term3 && term4)) return false;
var u, v, w, z;
u = (x3 - x1) * (y2 - y1) - (x2 - x1) * (y3 - y1);
v = (x4 - x1) * (y2 - y1) - (x2 - x1) * (y4 - y1);
w = (x1 - x3) * (y4 - y3) - (x4 - x3) * (y1 - y3);
z = (x2 - x3) * (y4 - y3) - (x4 - x3) * (y2 - y3);
return (u * v <= expression && w * z <= expression);
}
/***引射线法判断1个点是否在一个平面内
* @param {{x:1,y:2}} point
* @param {Array} polygon [{x:11,y:23},{x:34,y:45},...]
*/
function isPointInPolygon(point, polygon) {
var N = polygon.length;
var boundOrVertex = true; //如果点位于多边形的顶点或边上,也算做点在多边形内,直接返回true
var intersectCount = 0; //cross points count of x
var precision = 2e-10; //浮点类型计算时候与0比较时候的容差
var p1, p2; //neighbour bound vertices
var p = point; //测试点
p1 = polygon[0]; //left vertex
for (var i = 1; i <= N; ++i) { //check all rays
if (p.x == p1.x && p.y == p1.y) {
return boundOrVertex; //p is an vertex
}
p2 = polygon[i % N]; //right vertex
if (p.y < Math.min(p1.y, p2.y) || p.y > Math.max(p1.y, p2.y)) {
p1 = p2;
continue; //next ray left point
}
if (p.y > Math.min(p1.y, p2.y) && p.y < Math.max(p1.y, p2.y)) {
if (p.x <= Math.max(p1.x, p2.x)) {
if (p1.y == p2.y && p.x >= Math.min(p1.x, p2.x)) {
return boundOrVertex;
}
if (p1.x == p2.x) {
if (p1.x == p.x) {
return boundOrVertex;
} else { //before ray
++intersectCount;
}
} else {
var xinters = (p.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y) + p1.x; //cross point of x
if (Math.abs(p.x - xinters) < precision) {
return boundOrVertex;
}
if (p.x < xinters) {
++intersectCount;
}
}
}
} else {
if (p.y == p2.y && p.x <= p2.x) {
var p3 = polygon[(i + 1) % N]; //next vertex
if (p.y >= Math.min(p1.y, p3.y) && p.y <= Math.max(p1.y, p3.y)) {
++intersectCount;
} else {
intersectCount += 2;
}
}
}
p1 = p2; //next ray left point
}
if (intersectCount % 2 == 0) {
//偶数在多边形外
return false;
} else {
//奇数在多边形内
return true;
}
}
/*****检测两个多边形区域是否冲突
* @param {Array} polygon1 [{ x: 10, y: 10 }]
* @param {Array} polygon2 [{x,y}]
*/
function isCollision(polygon1,polygon2) {
const len1 = polygon1.length;
const len2 = polygon2.length;
let exitCollis = false;
let _i, _j;
// 判断两多边形是否存在相交的边
for (let i = 0; i < len1; i++) {
_i = (i+1) % len1;
if (exitCollis) break;
for (let j = 0; j < len2; j++) {
_j = (j+1) % len2;
exitCollis = isCross(polygon1[i].x, polygon1[i].y, polygon1[_i].x, polygon1[_i].y,
polygon2[j].x, polygon2[j].y, polygon2[_j].x, polygon2[_j].y);
if (exitCollis) break;
}
}
if (exitCollis) return true;
// 判断多边形1 是否在 多边形2 中
for (let k = 0; k < len1; k++){
exitCollis = isPointInPolygon(polygon1[k], polygon2);
if (!exitCollis) break;
}
if (exitCollis) return true;
// 判断多边形2 是否在 多边形1 中
for (let k = 0; k < len2; k++){
exitCollis = isPointInPolygon(polygon2[k], polygon1);
if (!exitCollis) break;
}
return exitCollis;
}
我发现只需修改步骤一,两条线段判断是否相交时,如果出现有一个端点在另一条线段上,则不算它们相交,否则继续按原逻辑计算。这样即使出现边重合的情况也会判定为未相交。它不需要我付出太大的代价,值得试试。
但我该怎么判断一个点是否在一个线段上呢?计算一个点到直线的最小距离,我想到了SDF技术,用在这里再合适不过了。
03. 2d line SDF
SDF(Signed Distance Function 有向距离函数)将当前点传入一个几何体的SDF表示,返回距离这个几何体的最小距离,在几何体外为正值,在几何体内为负值,在几何体边界上为0。
我在iq大神网站上找到了他写的代码。
这是一个用glsl es(一种着色器语言,webgl即使用的它)写成的线段的SDF,因为大佬是用他来做图形渲染研究的。我们将其用js改写一下即可。
// 向量的点积
function dot(v1,v2){
return v1.x*v2.x + v1.y*v2.y;
}
// 将val收敛到 min~max之间
function clamp(val,min,max){
return Math.min(Math.max(val,min),max);
}
// 计算向量的模
function length(v){
return Math.sqrt(dot(v,v));
}
/**线段的 2D SDF
* @param {{x:Number,y:Number}} p 判断的点
* @param {{x:Number,y:Number}} a 线段的一端
* @param {{x:Number,y:Number}} b 线段的一端
*/
function sdSegment(p,a,b){
// 向量的相减,改写为js我们得x,y分开相减。
var pa = {x:p.x-a.x, y:p.y-a.y}, ba = {x:b.x-a.x, y:b.y-a.y};
// dot(ba,ba)为 ba模的平方
// dot(pa,ba)/dot(ba,ba) 则得到 |pa|*cosθ/|ba| θ为pa与ba的夹角
// 相当于pa在ba上的投影长度与ba长度的比值
// 用clamp将其收敛到0~1(作为一个权重度量使用)
var h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
ba.x = ba.x*h;
ba.y = ba.y*h;
return length( {x:pa.x-ba.x,y:pa.y-ba.y} );
}
在判断线段相交的isCross()函数中加上是否有一点在另一条线段上的验证逻辑即可。
export function isCross(x1, y1, x2, y2, x3, y3, x4, y4) {
const term1 = Math.min(x1, x2) <= Math.max(x3, x4);
const term2 = Math.min(y3, y4) <= Math.max(y1, y2);
const term3 = Math.min(x3, x4) <= Math.max(x1, x2);
const term4 = Math.min(y1, y2) <= Math.max(y3, y4);
const expression = 0.00000000000000001;
// 判断两线段矩形区域,不相交时返回false
if (!(term1 && term2 && term3 && term4)) return false;
const p1sdf = sdSegment({ x: x1, y: y1 }, { x: x3, y: y3 }, { x: x4, y: y4 });
const p2sdf = sdSegment({ x: x2, y: y2 }, { x: x3, y: y3 }, { x: x4, y: y4 });
const p3sdf = sdSegment({ x: x3, y: y3 }, { x: x1, y: y1 }, { x: x2, y: y2 });
const p4sdf = sdSegment({ x: x4, y: y4 }, { x: x1, y: y1 }, { x: x2, y: y2 });
// 各端点离对方线段最短距离
if(Math.min(p1sdf,p2sdf,p3sdf,p4sdf) < 0.000006) return false;
...
}
如此便算是完成了他的要求。