GIS基础算法目录
GIS算法基础——左转算法拓扑生成
GIS算法基础——矢量数据压缩道格拉斯普克压缩算法(非递归实现)
GIS算法基础——左转算法拓扑生成
基于JavaScript的左转算法拓扑生成
本博文用以梳理课堂及自学内容,转载请标明出处。
本人应用JS中的Canvas对算法结果进行可视化验证,在算法说明及实现中绘制验证部分省略
拓扑相关基本概念:拓扑空间关系是一种对空间结构进行明确定义的数学方法。
•具有拓扑关系的矢量数据结构就是拓扑数据结构。
•拓扑数据结构描述了基本空间目标点、线、面之间的关联、邻接和包含关系
•拓扑空间关系信息是空间分析的基础之一
拓扑生成算法的技术路线
数据预处理–弧段处理,使整幅图形中的所有弧段,除在端点处相交外,没有其他交点,即没有相交或自相交的弧段–结点匹配,建立结点、弧段关系拓扑构建–建立多边形,以左转算法或右转算法跟踪,生成多边形,建立多边形与弧段的拓扑关系–建立多边形与多边形的拓扑关系
弧段预处理
拓扑关系自动建立的第一步就是处理弧段,使得弧段不存在自相交和相交现象,其过程分为三步:
–直线段相交的判断方法
–自相交弧段处理
–弧段相交打断处理
左转算法流程
从组成多边形边界的某一条弧段开始;如果该弧段与x轴正向夹角为最大,则从该弧段的同一结点出发的其他弧段中,方向角最小的弧段是该多边形的后续弧段;如果该弧段的方向角最小或介于同一结点的其他弧段方向角之间,则最小夹角偏差所对应的弧段为多边形的后续弧段
(1)顺序取一个结点作为起始结点,取完为止;取过该结点的方位角最小的未使用过的或仅使用过一次,且使用过的方向与本次相反的弧段作为起始弧段。(2)取这条弧段的另一个结点,找这个结点关联的弧段集合中的本条弧段的下一条弧段,如果本条弧段是最后一条弧段,则取弧段集合的第一条弧段,作为下一条弧段。
(3)判断是否回到起点,如果是则形成了一个多边形,记录下它,并且根据弧段的方向,设置组成该多边形的左右多边形信息。否则转(2)。
(4)取起始点上开始的,刚才所形成多边形的最后一条边作为新的起始弧段,转(2);若这条弧段已经使用过两次,即形成了两个多边形,转(1)。
构建结点、弧段、多边形类
// 结点类
class Nodep extends Point{//结点类继承点类
constructor(id,x,y){
super(x,y);
this.id = id;
this.linkarc = new Array();
}
SortArc(){//将与该结点相关的弧段按方位角排序
for(let i = 0;i < this.linkarc.length;i++){
this.linkarc[i].GetAzimuth(this);
}
this.linkarc.sort(Utils.CompareProp('azimuth'));
}
GetNearArc(aarc){//获取该弧的左转的下一条弧(需使用SortArc排过序)
this.SortArc();
let i = 0;
while(this.linkarc[i] != aarc) ++i;
return this.linkarc[(i+1)%this.linkarc.length];
}
GetuseableArc(){//选择没有被遍历过或被遍历方向与当前不同的弧段
this.SortArc();
let i = 0;
for(i;i<this.linkarc.length;i++){
if(this.linkarc[i].ifergodic!=2){
return this.linkarc[i];
}
}
return 0;
}
}
//弧段类
class Arc{
constructor(id,points){
this.id = id;
this.points = points;
this.sp = null;//起始结点
this.ep = null;//终止结点
this.lpg = null;//左多边形
this.rpg = null;//右多边形
this.azimuth = 0;//方位角
this.ifergodic = 0;//用于判断这条边方向的使用次数
}
Getspep(){//用于在构建多边形中动态获取起点终点
this.sp = this.points[0];
this.ep = this.points[this.points.length-1];
}
Reverse(){//将点倒置使得正常多边形的边均为顺时针
this.points.reverse();
this.Getspep();
}
GetAzimuth(anode){//获取弧段上一个结点的方位角
if(this.points[0]==anode){
this.azimuth = Utils.GetAzimuth(this.points[0],this.points[1]);
}
else{
this.azimuth = Utils.GetAzimuth(this.points[this.points.length-1],this.points[this.points.length-2]);
}
}
GetDirection(anode){//判断这条弧段的遍历方向,如果方向与点相反则逆置points
if(anode == this.points[0]){
this.Getspep();
}
else if(anode == this.points[this.points.length-1]){
this.Reverse();
}
else console.log("点匹配失败获取方向出现问题");
}
GetanotherNode(node){//获得弧上的另一个结点
if(this.sp == node) return this.ep;
return this.sp;
}
}
//多边形类
class Polygon{
constructor(id,arcs){
this.id = id;
this.arcs = arcs;
this.points = new Array();
this.part = new Array();//岛
this.centerpoint = this.GetCp();
this.bbox;
}
GetCp(){//获取多边形中心点
let sumx = 0, sumy = 0, sump =0;
for(let i = 0;i<this.arcs.length;i++){
let pointlist = this.arcs[i].points;
sump+=pointlist.length;
for(let j = 0;j<pointlist.length;j++){
sumx += pointlist[j].x;
sumy += pointlist[j].y;
}
}
let centerP = new Point(sumx/sump,sumy/sump);
return centerP;
}
JudgePart(){//岛只有一个节点的情况判断删除多余重复弧段
if(this.arcs[0]==this.arcs[1]){
this.arcs.splice(1,1);
}
}
GetPoints(){
let i = 0;
this.JudgePart();
for(i ; i < this.arcs.length;i++){
let arc = this.arcs[i];
if(arc.ifergodic==1){
for(let m = 0;m<arc.points.length-1;++m){
this.points.push(arc.points[m]);
}
}
else if(arc.ifergodic==2){
for(let j = arc.points.length-1;j>0;--j){
this.points.push(arc.points[j]);
}
}
else console.log("赋予多边形点信息出错");
}
this.points.push(this.points[0]);
}
ArcSide(){//给多边形内的弧段赋予多边形拓扑信息
for(let arc of this.arcs){
if(arc.ifergodic == 1) arc.rpg = this;
else if(arc.ifergodic == 2) arc.lpg = this;
else console.log("左转出现错误");
}
}
Area(){
let asum = 0;
for(let i = 0;i<this.points.length-1;i++){
let apoint = this.points[i];
asum+=(this.points[i+1].x*apoint.y-apoint.x*this.points[i+1].y);
}
return asum/2;
}
PolygonBBox(){
let ltp = new Point(this.points[0].x,this.points[0].y);//左上
let rbp = new Point(this.points[0].x,this.points[0].y);//右下
for(let point of this.points){
ltp.x = (ltp.x<point.x)?ltp.x:point.x;
ltp.y = (ltp.y>point.y)?ltp.y:point.y;
rbp.x = (rbp.x>point.x)?rbp.x:point.x;
rbp.y = (rbp.y<point.y)?rbp.y:point.y;
}
this.bbox = new BBox(new Array(ltp,rbp));
}
}
左转算法部分
//左转算法
static SubTurnLeft(stnode,starc){//左转构造多边形递归部分
starc.ifergodic++;
let curnode = starc.GetanotherNode(stnode);
let polyarcs = new Array();
polyarcs.push(starc);
let curarc = curnode.GetNearArc(starc);
while(curnode!=stnode){//当没有回到起始边的时候循环找下一条边
if(curarc.ifergodic==0){//没有遍历过的边先确定方向
curarc.GetDirection(curnode);
curnode = curarc.ep;
}
else if(curarc.ifergodic==1) curnode = curarc.sp;//确定了方向的边则一定是选取起始点
else console.log("左转选取下一条边出现问题");
curarc.ifergodic++;
polyarcs.push(curarc);
curarc = curnode.GetNearArc(curarc);
}
let apolygon = new Polygon(polyid,polyarcs);
++polyid;
apolygon.ArcSide();//左右多边形
apolygon.GetPoints();//按顺序连接点
this.JudgeHole(apolygon,polygons,holes);//通过面积判断将多边形分别放入多边形以及岛列表中
starc = apolygon.arcs[apolygon.arcs.length-1];//多边形最后一条边作为起始进行递归
//TODO:似乎有更好的逻辑???尝试过之后会有bug
if(starc.ifergodic!=2) this.SubTurnLeft(stnode,starc);
else return;
}
static Turnleft(nodelist){//左转构建多边形主体以及岛的list
for(let node of nodelist){
node.SortArc();
for(let arc of node.linkarc){//选取可以开始遍历构造多边形的边
if(arc.ifergodic==0){//第一次遍历则为其赋予方向
arc.GetDirection(node);
this.SubTurnLeft(node,arc);
break;
}
else if(arc.ifergodic==1){//第二次遍历判断本次遍历方向与第一次是否相同
if(arc.sp==node) continue;//通过判断第一次遍历的方向起始点判断方向
else{
this.SubTurnLeft(node,arc);
break;
}
}
else if(arc.ifergodic==2) continue;//遍历过两次的边跳过
}
}
}
匹配多边形岛
static PairHoles(polygons,holes){
let flag = 0;
for(let apoly of polygons){
let apolyarea = apoly.Area();
apoly.PolygonBBox();
let polybbox = apoly.bbox;
for(let ahole of holes){
let holearea = ahole.Area();
if(holearea+apolyarea<0) continue;//第一层判断面积
ahole.PolygonBBox();
let holebbox = ahole.bbox;
if(polybbox.Compare(holebbox)) continue;//第二层判断包围盒
for(let apoint of ahole.points){//第三层逐个点判断是否在内部
if(Utils.ptInPolyByCorner(apoint,apoly)==0){
flag = 0;
console.log(apoint);
break;
}
flag = 1;
}
if(flag){
apoly.part.push(ahole);
}
}
}
}
可视化效果
利用Canvas将生成的多边形随机填充为不同的颜色,多边形岛统一不填充(但是他们并非为空而也是多边形)
总结
左转算法逻辑清晰,多读几遍算法过程描述就能理解其逻辑,但是将此逻辑转化为编程语言并不容易,特别是在需要从基础的类开始构建的情况下。
在我进行的过程中,按照自己的思路写了一遍发现总是会有bug,在debug改代码进行了很长时间依然没有效果的情况下,我果断选择全部重写,事实证明是有效的。对于JS这门语言,在很多书中以及博主的教学中,都提及其动态性等导致自己的程序很难debug成功,而花费的时间精力不如从头来过,并且这样能解决大多数的问题。我是真实体验过了。
如果对大家有帮助的话,点赞收藏一下,欢迎回踩,谢谢大家,一起共同进步!