基于Leaflet的自助标绘源码解析-其它对象解析
目录
前言
一、整体类图介绍
1、整体类图
二、进攻方向类对象标绘实现
1、基础配置
2、各组成部分的绘制
三、集结地对象的标绘实现
1、对象图形绘制
四、钳击对象的标绘实现
1、基础配置
2、各部分标绘
五、总结
前言
在之前的自助标绘相关博文中,我们对一些战斗对象的标绘有了进一步的介绍和说明。原来的博文列表如下表,感兴趣的朋友可以点击看一下:
序号 | 博客列表 |
1 | 基于Leaflet和天地图的直箭头标绘实战-源码分析 |
2 | 基于Leaflet和天地图的细直箭头和突击方向标绘实战 |
在之前的这两篇博客中,我们已经对直箭头、细直箭头和突击方向这三种对象的绘制进行了详细的介绍。通过这些系列博客,对其主要的类图、源码以及绘制的原理进行深入的剖析,在一些警用或者历史类战斗还原时,除了之前提到过的三种对象,其实还有一些常见的其它对象,比如战斗队形,带尾部的战斗队形,钳击箭头以及聚集地。具体的效果如下所示:
在上图中的示例应用中就将一些基本的标绘对象进行了展示。本文将继续围绕着自助标绘场景来进行深度讲解,首先介绍剩余对象的类继承关系,然后针对不同的对象分别介绍具体对象的绘制方法,最后给出整体的自助标绘的实例成果。如果看博客的您对自助标绘感兴趣的话,不妨来这里看看,欢迎大家评论区留言交流。
一、整体类图介绍
在进行相关的标绘方法的介绍之前,首先我们来梳理一下标绘对象的继承关系。通过梳理标绘对象,大家会发现。这些对象之间其实是有内在的关联的,不同的是加入了一些单独的处理。通过继承等父子关系,可以理顺这些类的逻辑关系。通过个性化的自定义代码讲解,让大家对独立性的标绘对象有了更深入的认识。
1、整体类图
关于标绘对象的整体类图,除了之前介绍的标绘对象不再单独设置,感兴趣的朋友可以翻阅之前的相关博文。这里分享根据源码整理出来的类图,如下所示:
通过上图可以看到, 除了聚居地和钳击箭头是独立的标绘对象外,其它的进攻箭头(带尾)、分队战斗箭头(箭尾和平尾)这三类箭头其实都是进攻方向这个对象的子类。因此这里着重讲解其父类即进攻方向、聚集地、钳击箭头这三种对象。针对进攻方向,只要掌握了其中一种,剩下的掌握起来也是比较简单的。
二、进攻方向类对象标绘实现
在了解了整体的类图设计之后,我们来分别对进攻方向以及其子类标绘对象的实现进行说明。让大家掌握进攻类标绘对象的绘制防范。 进攻方向跟之前介绍的箭头类对象相比,生成的方法较为复杂。
1、基础配置
进攻方向的基础配置参数不少,主要是包含以下的基本参数:
headHeightFactor: 0.18,//头部高度系数
headWidthFactor: 0.3,//头部宽度系数
neckHeightFactor: 0.85,//颈部高度系数
neckWidthFactor: 0.15,//颈部宽度系数
headTailFactor: 0.8
绘制进攻方向在地图上标绘时,至少需要三个点才能确定。而标绘对象由三部分组成,分别是:箭头、箭尾和箭身。通过分别计算三个部分的信息,然后将所有的关键点连接起来,整合成我们的目标进攻方向对象。因此对箭头、箭尾和箭身的计算,之前其它的对象绘制的时候均不要这么复杂。而有一些带尾部效果的对象还需要进行尾部的处理。因此显得更加复杂。
2、各组成部分的绘制
这里将重点介绍如何对进攻方向的箭头、箭身、箭尾来进行介绍。可以在源码中找到generate的方法。而与generate平级都生成三个组成部分的计算方法。下面我们来一一解答。最先进行计算的是箭尾,计算箭尾需要先确定坐标点的方向,可以通过PlotUtils中的isClockWise判断,关键代码如下:
var pnts = this._proPoints;
// 计算箭尾
var tailLeft = pnts[0];
var tailRight = pnts[1];
if (L.PlotUtils.isClockWise(pnts[0], pnts[1], pnts[2])) {tailLeft = pnts[1];tailRight = pnts[0];
}
通过传入的点来判断箭尾的顺序是顺时针还是逆时针。如果是顺时针,就需要对坐标点进行互换,互换后对tailLeft和tailRight进行赋值。:得到这两个值之后,再来计算中间点。如下所示,计算两个点的终点的方法也在下面实例代码当中:
var midTail = L.PlotUtils.mid(tailLeft, tailRight);var bonePnts = [midTail].concat(pnts.slice(2));//计算中点
L.PlotUtils.mid = function (pnt1, pnt2) {return [(pnt1[0] + pnt2[0]) / 2, (pnt1[1] + pnt2[1]) / 2];
};
接下来就是计算箭头的坐标,计算箭头的坐标跟之前计算直线箭头的方式有点类似,通过坐标点和旋转的方向还有相关的系数其实就可以明确箭头相关的点。
//获取箭头部分的点
getArrowHeadPoints: function (points, tailLeft, tailRight) {var len = L.PlotUtils.getBaseLength(points);var headHeight = len * this.options.headHeightFactor;var headPnt = points[points.length - 1];len = L.PlotUtils.distance(headPnt, points[points.length - 2]);var tailWidth = L.PlotUtils.distance(tailLeft, tailRight);if (headHeight > tailWidth * this.options.headTailFactor) {headHeight = tailWidth * this.options.headTailFactor;}var headWidth = headHeight * this.options.headWidthFactor;var neckWidth = headHeight * this.options.neckWidthFactor;headHeight = headHeight > len ? len : headHeight;var neckHeight = headHeight * this.options.neckHeightFactor;var headEndPnt = L.PlotUtils.getThirdPoint(points[points.length - 2], headPnt, 0, headHeight, true);//L.marker(L.PlotUtils.unProPoint(headEndPnt)).bindPopup("headEndPnt").addTo(map)var neckEndPnt = L.PlotUtils.getThirdPoint(points[points.length - 2], headPnt, 0, neckHeight, true);//L.marker(L.PlotUtils.unProPoint(neckEndPnt)).bindPopup("neckEndPnt").addTo(map)var headLeft = L.PlotUtils.getThirdPoint(headPnt, headEndPnt, L.PlotConstants.HALF_PI, headWidth, false);var headRight = L.PlotUtils.getThirdPoint(headPnt, headEndPnt, L.PlotConstants.HALF_PI, headWidth, true);var neckLeft = L.PlotUtils.getThirdPoint(headPnt, neckEndPnt, L.PlotConstants.HALF_PI, neckWidth, false);var neckRight = L.PlotUtils.getThirdPoint(headPnt, neckEndPnt, L.PlotConstants.HALF_PI, neckWidth, true);return [neckLeft, headLeft, headPnt, headRight, neckRight];
}
通过偏转方向和相应的距离和角度系数,最终可以获取箭头各坐标点的坐标。从而组成箭头这个对象相应的坐标。最后来计算箭身,箭身的计算过程是复杂一点,比如tailWidthFactor需要根据箭头的坐标和长度信息来进行动态生成,而不是像前面的常数配置参数一样。
// 计算箭头
var headPnts = this.getArrowHeadPoints(bonePnts, tailLeft, tailRight);
var neckLeft = headPnts[0];
var neckRight = headPnts[4];
var tailWidthFactor = L.PlotUtils.distance(tailLeft, tailRight) / L.PlotUtils.getBaseLength(bonePnts);
通过两个距离的求解来计算相应的系数。最后需要将这些系数传入到箭身的计算方法当中。关键代码如下所示:
//获取箭身部分的点
getArrowBodyPoints: function (points, neckLeft, neckRight, tailWidthFactor) {var allLen = L.PlotUtils.wholeDistance(points);var len = L.PlotUtils.getBaseLength(points);var tailWidth = len * tailWidthFactor;var neckWidth = L.PlotUtils.distance(neckLeft, neckRight);var widthDif = (tailWidth - neckWidth) / 2;var tempLen = 0, leftBodyPnts = [], rightBodyPnts = [];for (var i = 1; i < points.length - 1; i++) {var angle = L.PlotUtils.getAngleOfThreePoints(points[i - 1], points[i], points[i + 1]) / 2;tempLen += L.PlotUtils.distance(points[i - 1], points[i]);var w = (tailWidth / 2 - tempLen / allLen * widthDif) / Math.sin(angle);var left = L.PlotUtils.getThirdPoint(points[i - 1], points[i], Math.PI - angle, w, true);//L.marker(L.PlotUtils.unProPoint(left)).bindPopup("left").addTo(map)var right = L.PlotUtils.getThirdPoint(points[i - 1], points[i], angle, w, false);//L.marker(L.PlotUtils.unProPoint(right)).bindPopup("right").addTo(map)leftBodyPnts.push(left);rightBodyPnts.push(right);}return leftBodyPnts.concat(rightBodyPnts);
}
来看具体坐标点的生成然后从下标为1的点开始循环至倒数第二个点,每次循环计算当前点和前一个点和后一个点组成的角度的一半angle,以及当前的位置相较于tailWidth和neckWidth的宽度w,然后根据当前点的位置、angle和w计算出一组left和right。在生成坐标点,为了保持对象的平滑,这里采用贝塞尔曲线的方式来生成。贝塞尔曲线的生成方法如下:
//获取二次贝塞尔曲线点
L.PlotUtils.getQBSplinePoints = function (points) {if (points.length <= 2)return points;var n = 2;var bSplinePoints = [];var m = points.length - n - 1;bSplinePoints.push(points[0]);for (var i = 0; i <= m; i++) {for (var t = 0; t <= 1; t += 0.05) {var x = y = 0;for (var k = 0; k <= n; k++) {var factor = L.PlotUtils.getQuadricBSplineFactor(k, t);x += factor * points[i + k][0];y += factor * points[i + k][1];}bSplinePoints.push([x, y]);}}bSplinePoints.push(points[points.length - 1]);return bSplinePoints;
};
通过以上的计算后,我们将所有的坐标点连起来,就可以形成一个进攻方向。在地图上绘制后如下图所示:
注意,带了尾部效果的计算与之前的进攻方向不一样的是,带尾的计算过程与平尾的有所区别。其关键的区别在于以下的代码:
var len = allLen * this.options.tailWidthFactor * this.options.swallowTailFactor;
this.swallowTailPnt = L.PlotUtils.getThirdPoint(bonePnts[1], bonePnts[0], 0, len, true);
var factor = tailWidth / allLen;
三、集结地对象的标绘实现
说完了进攻方向,接下来介绍一下集结地对象的介绍。集结地是单独的对象,直接集成自Polot对象。
1、对象图形绘制
集结地对象的基础配置比较简单,这是一个由很多个点组成的蚕豆形状的多边形,只需要三个点就可以计算得出一个聚集地图形。在进行计算时,如果传入的点的长度等于2的话,会重新计算坐标。
if (this.getPointCount() == 2) {var mid = L.PlotUtils.mid(pnts[0], pnts[1]);var d = L.PlotUtils.distance(pnts[0], mid) / 0.9;var pnt = L.PlotUtils.getThirdPoint(pnts[0], mid, L.PlotConstants.HALF_PI, d, true);pnts = [pnts[0], pnt, pnts[1]];
}
在绘制集结地图形过程当中,需要基于前面计算出来的三个点来求解相应的法线,同一个for循环来进行循环生成。
var mid = L.PlotUtils.mid(pnts[0], pnts[2]);pnts.push(mid, pnts[0], pnts[1]);var normals = [];for (var i = 0; i < pnts.length - 2; i++) {var pnt1 = pnts[i];var pnt2 = pnts[i + 1];var pnt3 = pnts[i + 2];var normalPoints = L.PlotUtils.getBisectorNormals(this.options.t, pnt1, pnt2, pnt3);normals = normals.concat(normalPoints);
}
求解法线的方法如下:
//获取平分线的法线(pnt1 pnt2 pnt3顺次连接组成的以pnt2为原点的夹角的角平分线的垂线)
L.PlotUtils.getBisectorNormals = function (t, pnt1, pnt2, pnt3) {var normal = L.PlotUtils.getNormal(pnt1, pnt2, pnt3);// L.marker(L.PlotUtils.unProPoint(normal)).addTo(tempGuidelinesLy)var dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1]);var uX = normal[0] / dist;var uY = normal[1] / dist;var d1 = L.PlotUtils.distance(pnt1, pnt2);var d2 = L.PlotUtils.distance(pnt2, pnt3);if (dist > L.PlotConstants.ZERO_TOLERANCE) {if (L.PlotUtils.isClockWise(pnt1, pnt2, pnt3)) {var dt = t * d1;var x = pnt2[0] - dt * uY;var y = pnt2[1] + dt * uX;var bisectorNormalRight = [x, y];dt = t * d2;x = pnt2[0] + dt * uY;y = pnt2[1] - dt * uX;var bisectorNormalLeft = [x, y];}else {dt = t * d1;x = pnt2[0] + dt * uY;y = pnt2[1] - dt * uX;bisectorNormalRight = [x, y];dt = t * d2;x = pnt2[0] - dt * uY;y = pnt2[1] + dt * uX;bisectorNormalLeft = [x, y];}}else {x = pnt2[0] + t * (pnt1[0] - pnt2[0]);y = pnt2[1] + t * (pnt1[1] - pnt2[1]);bisectorNormalRight = [x, y];x = pnt2[0] + t * (pnt3[0] - pnt2[0]);y = pnt2[1] + t * (pnt3[1] - pnt2[1]);bisectorNormalLeft = [x, y];}return [bisectorNormalRight, bisectorNormalLeft];
};
最后需要求解三次贝塞尔曲线点坐标。在将这些点连接整合起来得到一个完成的集结地图形。
//获取三次贝塞尔曲线点的坐标
L.PlotUtils.getCubicValue = function (t, startPnt, cPnt1, cPnt2, endPnt) {t = Math.max(Math.min(t, 1), 0);var tp = 1 - t;var t2 = t * t;var t3 = t2 * t;var tp2 = tp * tp;var tp3 = tp2 * tp;var x = (tp3 * startPnt[0]) + (3 * tp2 * t * cPnt1[0]) + (3 * tp * t2 * cPnt2[0]) + (t3 * endPnt[0]);var y = (tp3 * startPnt[1]) + (3 * tp2 * t * cPnt1[1]) + (3 * tp * t2 * cPnt2[1]) + (t3 * endPnt[1]);return [x, y];
};
绘制出来的集结地图形如下所示:
四、钳击对象的标绘实现
钳击对象一般用来表示同时发起攻击,因此有两个箭头。通过两个箭头来表示进攻的方向。绘制钳击对象需要使用四个点,分别是尾部的两点以及两个箭头的坐标。这里来详细介绍钳击箭头的属性和绘制过程。
1、基础配置
与其它的标绘对象一致,钳击箭头的属性如下表所示:
headHeightFactor: 0.25,//头部高度倍数
headWidthFactor: 0.3,//头部宽度倍数
neckHeightFactor: 0.85,//颈部高度倍数
neckWidthFactor: 0.15,//颈部宽度倍数
fixPointCount: 4
如前文所述,钳击箭头包含两个箭头,因此同样需要计算不同箭头的坐标点,以及定义箭头对象的高度和高度的倍数。
2、各部分标绘
首先根据给出的坐标点计算顺序,计算方法是之前介绍过的判断点的顺时针和逆时针方向。以此来分别计算左右两个箭头。
var leftArrowPnts, rightArrowPnts;
if (L.PlotUtils.isClockWise(pnt1, pnt2, pnt3)) {leftArrowPnts = this.getArrowPoints(pnt1, this.connPoint, this.tempPoint4, false);rightArrowPnts = this.getArrowPoints(this.connPoint, pnt2, pnt3, true);} else {leftArrowPnts = this.getArrowPoints(pnt2, this.connPoint, pnt3, false);rightArrowPnts = this.getArrowPoints(this.connPoint, pnt1, this.tempPoint4, true);
}
得到左右两边的钳击箭头的方法为:
getArrowPoints: function (pnt1, pnt2, pnt3, clockWise) {var midPnt = L.PlotUtils.mid(pnt1, pnt2);var len = L.PlotUtils.distance(midPnt, pnt3);var midPnt1 = L.PlotUtils.getThirdPoint(pnt3, midPnt, 0, len * 0.3, true);var midPnt2 = L.PlotUtils.getThirdPoint(pnt3, midPnt, 0, len * 0.5, true);//var midPnt3=PlotUtils.getThirdPoint(pnt3, midPnt, 0, len * 0.7, true);midPnt1 = L.PlotUtils.getThirdPoint(midPnt, midPnt1, L.PlotConstants.HALF_PI, len / 5, clockWise);midPnt2 = L.PlotUtils.getThirdPoint(midPnt, midPnt2, L.PlotConstants.HALF_PI, len / 4, clockWise);//midPnt3=PlotUtils.getThirdPoint(midPnt, midPnt3, Constants.HALF_PI, len / 5, clockWise);var points = [midPnt, midPnt1, midPnt2, pnt3];// 计算箭头部分var arrowPnts = this.getArrowHeadPoints(points, this.options.headHeightFactor, this.options.headWidthFactor, this.options.neckHeightFactor, this.options.neckWidthFactor);var neckLeftPoint = arrowPnts[0];var neckRightPoint = arrowPnts[4];// 计算箭身部分var tailWidthFactor = L.PlotUtils.distance(pnt1, pnt2) / L.PlotUtils.getBaseLength(points) / 2;var bodyPnts = this.getArrowBodyPoints(points, neckLeftPoint, neckRightPoint, tailWidthFactor);var n = bodyPnts.length;var lPoints = bodyPnts.slice(0, n / 2);var rPoints = bodyPnts.slice(n / 2, n);lPoints.push(neckLeftPoint);rPoints.push(neckRightPoint);lPoints = lPoints.reverse();lPoints.push(pnt2);rPoints = rPoints.reverse();rPoints.push(pnt1);return lPoints.reverse().concat(arrowPnts, rPoints);}
其它的计算过程跟其它箭头类的计算相似,在此不再赘述。生成钳击箭头的实际效果如下所示:
到此, 进攻方向机器扩展对象、集结地、钳击对象的基础配置以及具体的绘制过程说明就进行了具体的源码解析。最后将所有的对象在地图上进行标绘的可视化展示结果如下:
五、总结
以上就是本文的主要内容,本文将继续围绕着自助标绘场景来进行深度讲解,首先介绍剩余对象的类继承关系,然后针对不同的对象分别介绍具体对象的绘制方法,最后给出整体的自助标绘的实例成果。自助标绘非常重要,掌握自助标绘对于以后进行战斗的还原,演习斗争的还原等具有非常重要的作用。为下一步进行动态标绘奠定绘制的基础。行文仓促,难免有许多不足支持,针对不足,还请各位专家博主朋友在评论区留下真知灼见,不胜感激。