基於Turf.js教你快速實現地理圍欄的合並拆分


以下內容轉載自totoro的文章《幾何計算-基於Turf.js實現多邊形的拆分及合並》

作者:totoro

鏈接:https://blog.totoroxiao.com/geo-polygon-split-union/

來源:https://blog.totoroxiao.com/

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

JavaScript API GL近期為支持物流行業實現了幾何圖形編輯器,用戶可通過編輯器接口進行點、線、面、圓的繪制和編輯。在物流行業中常見的使用場景是配送區域及地理圍欄的繪制,常會有對已有區域進行拆分或者合並的需要,所以編輯器也提供了相應的功能。本文介紹了如何基於Turf實現多邊形的拆分及合並。

背景介紹

多邊形的拆分合並

多邊形的拆分是指將多邊形沿着線切分為幾個多邊形。如下圖所示,不僅可以沿線一分為二,當線與多邊形有多段相交時也可以分為多份,另外當多邊形帶洞(環多邊形)時也可以在拆分后保持洞的形狀。

多邊形的合並是指將多個多邊形合並為一個多邊形,其前提條件是多邊形之間有交叉區域或者共邊。如下圖所示,完全共邊或者部分共邊都可以合並,當有交叉時會貫通交叉部分。

Turf.js

不難發現,多邊形的拆分合並中會有大量且復雜的幾何計算,包括點、線、面相互之間的相交、包含等計算。不過我們並不需要造輪子,可以使用Turf.js完成大部分的基礎計算。Turf是由mapbox推出的空間幾何計算庫,常用於地理空間內的幾何關系分析,功能非常強大,具體功能可見Turf.js | Advanced geospatial analysis

可是Turf.js目前還沒有提供多邊形的拆分方法,另外多邊形的合並雖然已有union方法,但在實際應用中也無法很好解決部分共邊的多邊形的合並問題,所以只能在Turf的基礎上自行實現符合業務需求的拆分合並功能。

多邊形的拆分

基礎方案

多邊形拆分的核心思想是找到切割點,所以線對面的切割可以簡化為線對線的切割。兩條線互相切割得到子線段,將子線段互相組合形成多邊形。

如上圖所示,待拆分的多邊形記為polygon,切割折線記為splitter。拆分步驟如下:

  • 面化為線:polygon從起點解開可以形成路徑為[p0, p1, p2, p3, p0]的折線pline
  • 線互相切割:Turf提供了lineSplit方法,可以使用點或者線將一條折線切分為幾部分。利用該方法可以將pline與splitter互相切割,得到子線段集合pieceCollection
  • 線組合為多邊形:Turf提供了polygonize方法,將一組折線互相拼接組合成多邊形。利用該方法可以將pieceCollection組合成多個多邊形splitedCollection

這方案看似可行,實則有以下問題:

  • pline與splitter互相切割后得到的切割點不一致,導致polygonize無法將其拼接在一起
  • 切割線在多邊形外的部分會形成外部多邊形,如下圖所示

解決切割點不一致問題

上文所述第一個切割點不一致的問題是指,使用線A切線B得到的切割點與使用線B切線A得到的切割點不同。

可以看看Turf的源碼是如何實現lineSplit的:

function lineSplit(line, splitter) {
    ...

    var lineType = getType(line);
    var splitterType = getType(splitter);

    ...

    // remove excessive decimals from splitter
    // to avoid possible approximation issues in rbush
    var truncatedSplitter = truncate(splitter, {precision: 7});

    switch (splitterType) {
    case 'Point':
        return splitLineWithPoint(line, truncatedSplitter);
    case 'MultiPoint':
        return splitLineWithPoints(line, truncatedSplitter);
    case 'LineString':
    case 'MultiLineString':
    case 'Polygon':
    case 'MultiPolygon':
        return splitLineWithPoints(line, lineIntersect(line, truncatedSplitter));
    }
}

代碼中truncate方法是用於保留指定位數的小數,即splitter被限制了精度,所以pline和splitter交換位置后實際計算中的坐標點就發生了變化,導致了不一致的問題。

如何保證兩者一致?可以發現用線B切線A時,實際上是先計算線B與線A的交點,再使用splitLineWithPoints方法用這些交點對線A進行切割。那么先計算好兩條線的交點,再用交點分別對兩條線進行切割,就可以保證切割點的一致了。實現方法如下:

// truncate
let truncatedSplitter = truncate(splitter, {precision: 7});

// compute intersects of two lines
let intersectCollection = lineIntersect(outerLine, truncatedSplitter);
if (intersectCollection.features.length < 2) {
	return null;
}

// transform FeatureCollection[Point] to Feature[MultiPoint]
let intersectCombined = combine(intersectCollection).features[0];

// split lines with points
let outerPieceCollection = lineSplit(outerLine, intersectCombined);
let splitterPieceCollection = lineSplit(truncatedSplitter, intersectCombined);

// polygonize pieces
let pieceCollection = featureCollection(outerPieceCollection.features.concat(splitterPieceCollection.features));
let polygonCollection = polygonize(pieceCollection);

解決外部多邊形問題

簡單來說只要能篩選出在原大多邊形內部的小多邊形就好了,Turf提供了booleanContains、booleanDisjoint、booleanWithin等方法用於判斷點、線、面的位置關系。但是由於小多邊形的部分頂點是在原多邊形的邊線上計算出來的,且精度有限,位置關系非常微妙,計算時其落在多邊形內外都有可能,所以誤判率極高。

但是多邊形的形心就沒有這個問題了,在當前的場景下,我們無需判斷小多邊形的每個頂點是否都落在原多邊形內,只要其形心落在原多邊形內即可。

實現如下:

// filter polygons in outer poly
let innerPolygons = polygonCollection.features.filter(polygon => {
	let center = centroid(polygon);
	return booleanWithin(center, outerPolygon);
});

環多邊形的拆分

環多邊形是指內部帶“洞”的多邊形,其拆分時有兩種情況,一是拆分線穿過了洞,那么洞就變成了外輪廓,二是拆分線沒有穿過洞,那么洞還整個保留。但是這樣的思考方式容易引導我們去將洞也進行拆分,然后再與外環拆分后的片段進行拼接。

還能有更簡單的做法,將洞作為遮罩。即在拆分時只對外環多邊形進行拆分,在拆分完成之后對小多邊形進行遮罩剔除。如下圖所示。

遮罩的剔除可以使用Turf的difference方法,具體實現如下:

let diffedPolygons = innerPolygons.map(polygon => {
	let diff = polygon;
	featureEach(holeCollection, (hole) => {
		diff = difference(diff, hole);
	});

	return diff;
});

至此即可完成多邊形的拆分功能啦。

多邊形的合並

turf.union

Turf提供union方法可以對有交集的多邊形進行合並,可以處理完全共邊、部分壓蓋、包含的情況,環多邊形也是可以處理的。但是在處理部分共邊的多邊形時,仍然存在點、線關系判定沒有容限的問題,導致點被判定在線外而無法完全合並。

這里先簡單介紹一下判斷點、線段關系的計算方法,用P表示點,S0和S1兩點構成線段,那么首先判斷向量P-S0和S1-S0的叉積(叉積表示其構成平行四邊形的面積)是否為0,然后判斷P是否在S0、S1兩點之間。問題就出在叉積是否為0這一步,由於點坐標都是高精度浮點數,叉積很難嚴格等於0,一般會設定一個較小容限值,只要叉積絕對值小於容限值即可判定為點在線上。

部分共邊多邊形的合並

已定位合並失敗的原因,但是沒辦法直接修改union的源碼,因為Turf在union的實現上其實也使用了外部庫martinez-polygon-clipping。不過可以轉換思維方式,將部分共邊的情況轉換為完全共邊,再交給union進行合並。這個轉換過程我將其稱為點注入,將多邊形B的頂點注入到多邊形A中,即遍歷B的頂點進行判斷,若其在A的某個線段上且不是線段端頭,就將其插入到A的路徑中。A與B互相注入頂點之后,所有部分共邊的邊線都變成完全共邊了。

實現過程如下,其中沒有使用booleanPointOnLine,而是基於其實現了isPointOnLine,一方面在點線關系判斷時加入了容限值,同時排除了所有的端點,另一方面返回值里不僅包含了bool說明點是否在線上,同時還有index屬性說明點在線的哪個線段上,以方便將其插入路徑中:

/**
 * 將點注入到線上
 * @param {Feature[LineString]} line 
 * @param {FeatureCollection} pointCollection 
 */
function injectPointsOnSide(line, pointCollection) {
	let coords = getCoords(line);
	featureEach(pointCollection, (point, index) => {
		let isOnLine = isPointOnLine(point, line, {
			ignoreVertices: true
		});
		if (isOnLine.bool) {
			coords.splice(isOnLine.index + 1, 0, getCoord(point));
		}
	});
}

至此即可完成多邊形的合並功能啦。

產品推廣

在JSAPI GL上實現的圖形編輯器集成了幾何圖形的繪制、編輯、刪除功能,相較於JSAPI v2功能更加完善且便於使用。該功能已上線官網,歡迎使用~
JavaScript API GL | 騰訊位置服務

注:GIF圖片較模糊且閃爍,不代表真實效果。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM