Skip to content

Commit

Permalink
Showing 7 changed files with 75 additions and 24 deletions.
29 changes: 20 additions & 9 deletions src/clipping/mapshaper-polygon-clipping.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { reversePath } from '../paths/mapshaper-path-utils';
import { forEachShapePart } from '../paths/mapshaper-shape-utils';
import { closeArcRoutes, openArcRoutes, getPathFinder, setBits } from '../paths/mapshaper-pathfinder';
import {
closeArcRoutes,
openArcRoutes,
getPathFinder,
setBits,
markPathsAsUsed } from '../paths/mapshaper-pathfinder';
import { getPolygonDissolver } from '../dissolve/mapshaper-polygon-dissolver';
import { PathIndex } from '../paths/mapshaper-path-index';
import { absArcId } from '../paths/mapshaper-arc-utils';
@@ -16,7 +21,7 @@ export function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
var clipArcTouches = 0;
var clipArcUses = 0;
var usedClipArcs = [];
var dividePath = getPathFinder(nodes, useRoute, routeIsActive);
var findPath = getPathFinder(nodes, useRoute, routeIsActive);
var dissolvePolygon = getPolygonDissolver(nodes);

// The following cleanup step is a performance bottleneck (it often takes longer than
@@ -49,12 +54,16 @@ export function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
return null;
});

markPathsAsUsed(clippedShapes, routeFlags); // to help us find unused paths later


// add clip/erase polygons that are fully contained in a target polygon
// need to index only non-intersecting clip shapes
// (Intersecting shapes have one or more arcs that have been scanned)

// first, find shapes that do not intersect the target layer
// (these could be inside or outside the target polygons)

var undividedClipShapes = findUndividedClipShapes(clipShapes);

closeArcRoutes(clipShapes, arcs, routeFlags, true, true); // not needed?
@@ -83,7 +92,7 @@ export function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
for (var i=0, n=ids.length; i<n; i++) {
clipArcTouches = 0;
clipArcUses = 0;
path = dividePath(ids[i]);
path = findPath(ids[i]);
if (path) {
// if ring doesn't touch/intersect a clip/erase polygon, check if it is contained
// if (clipArcTouches === 0) {
@@ -102,6 +111,7 @@ export function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
}
});


// Clear pathways of current target shape to hidden/closed
closeArcRoutes(shape, arcs, routeFlags, true, true, true);
// Also clear pathways of any clip arcs that were used
@@ -176,8 +186,9 @@ export function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
return usable;
}

// Filter a collection of shapes to exclude paths that contain clip/erase arcs
// and paths that are hidden (e.g. internal boundaries)

// Filter a collection of shapes to exclude paths that incorporate parts of
// clip/erase polygons and paths that are hidden (e.g. internal boundaries)
function findUndividedClipShapes(clipShapes) {
return clipShapes.map(function(shape) {
var usableParts = [];
@@ -201,12 +212,12 @@ export function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
});
}

// Test if arc is unused in both directions
// (not testing open/closed or visible/hidden)

function arcIsUnused(id, flags) {
var abs = absArcId(id),
flag = flags[abs];
return (flag & 0x44) === 0;
return (flag & 0x88) === 0;
// return id < 0 ? (flag & 0x80) === 0 : (flag & 0x8) === 0;
}

function arcIsVisible(id, flags) {
@@ -229,7 +240,7 @@ export function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
enclosedPaths.forEach(function(ids) {
var path;
for (var j=0; j<ids.length; j++) {
path = dividePath(ids[j]);
path = findPath(ids[j]);
if (path) {
dissolvedPaths.push(path);
}
13 changes: 11 additions & 2 deletions src/paths/mapshaper-path-index.mjs
Original file line number Diff line number Diff line change
@@ -121,15 +121,24 @@ export function PathIndex(shapes, arcs) {
}
cands.forEach(function(cand) {
var p = getTestPoint(cand.ids);
var isEnclosed = b.containsPoint(p[0], p[1]) && (index ?
index.pointInPolygon(p[0], p[1]) : geom.testPointInRing(p[0], p[1], pathIds, arcs));
var isEnclosed = b.containsPoint(p[0], p[1]) &&
// added a bounds-in-bounds test to handle a case where the test point
// fell along the shared boundary of two rings, but the rings did no overlap
// (this gave a false positive for the enclosure test)
// (for speed, the midpoint of an arc is used as the test point; this
// works well in the typical case where rings to not share an edge.
// Finding an internal test point would be better, we just need a fast
// function to find internal points)
b.contains(cand.bounds) &&
(index ? index.pointInPolygon(p[0], p[1]) : geom.testPointInRing(p[0], p[1], pathIds, arcs));
if (isEnclosed) {
paths.push(cand.ids);
}
});
return paths.length > 0 ? paths : null;
};

// return array of indexed paths within a given shape
this.findPathsInsideShape = function(shape) {
var paths = []; // list of enclosed paths
shape.forEach(function(ids) {
4 changes: 1 addition & 3 deletions src/paths/mapshaper-pathfinder-utils.mjs
Original file line number Diff line number Diff line change
@@ -18,7 +18,6 @@ export function getRightmostArc(fromArcId, nodes, filter) {
yy = coords.yy,
ids = nodes.getConnectedArcs(fromArcId),
toArcId = fromArcId; // initialize to fromArcId -- an error condition

if (filter) {
ids = ids.filter(filter);
}
@@ -47,21 +46,20 @@ export function getRightmostArc(fromArcId, nodes, filter) {
continue;
}
icand = arcs.indexOfVertex(candId, -2);

if (toArcId == fromArcId) {
// first valid candidate
ito = icand;
toArcId = candId;
continue;
}

code = chooseRighthandPath(fromX, fromY, nodeX, nodeY, xx[ito], yy[ito], xx[icand], yy[icand]);
if (code == 2) {
ito = icand;
toArcId = candId;
}
}


if (toArcId == fromArcId) {
// This shouldn't occur, assuming that other arcs are present
error("Pathfinder error");
36 changes: 26 additions & 10 deletions src/paths/mapshaper-pathfinder.mjs
Original file line number Diff line number Diff line change
@@ -8,25 +8,31 @@ import { debug } from '../utils/mapshaper-logging';
// Functions for redrawing polygons for clipping / erasing / flattening / division
// These functions use 8 bit codes to control forward and reverse traversal of each arc.
//
// Function of path bits 0-7:
// 0: is fwd path hidden or visible? (0=hidden, 1=visible)
// 1: is fwd path open or closed for traversal? (0=closed, 1=open)
// 2: unused
// 3: unused
// 4: is rev path hidden or visible?
// 5: is rev path open or closed for traversal?
// 6: unused
// 7: unused
// Function of arc bits 0-7:
// 0-3 FWD direction
// 0: is fwd arc hidden or visible? (0=hidden, 1=visible)
// 1: is fwd arc open or closed for traversal? (0=closed, 1=open)
// 2: was fwd arc visited in the traversal?
// 3: was fwd arc used in output shape?
// 4-7 REV direction
// 4: is rev arc hidden or visible?
// 5: is rev arc open or closed for traversal?
// 6: was rev arc visited in the traversal?
// 7: was rev arc used in output shape?
//
// Example codes:
// 0x3 (3): forward path is visible and open, reverse path is hidden and closed
// 0x10 (16): forward path is hidden and closed, reverse path is visible and closed
//

var FWD_VISIBLE = 0x1;
var FWD_OPEN = 0x2;
var REV_VISIBLE = 0x10;
var FWD_OPEN = 0x2;
var REV_OPEN = 0x20;
var FWD_VISITED = 0x4;
var REV_VISITED = 0x40;
var FWD_USED = 0x8;
var REV_USED = 0x80;

export function setBits(bits, arcBits, mask) {
return (bits & ~mask) | (arcBits & mask);
@@ -55,6 +61,16 @@ export function getRouteBits(arcId, routesArr) {
return bits & 7;
}

export function markPathsAsUsed(paths, routesArr) {
forEachArcId(paths, function(arcId) {
if (arcId < 0) {
routesArr[~arcId] |= REV_USED;
} else {
routesArr[arcId] |= FWD_USED;
}
});
}

// Open arc pathways in a single shape or array of shapes
//
export function openArcRoutes(paths, arcColl, routesArr, fwd, rev, dissolve, orBits) {
11 changes: 11 additions & 0 deletions test/clip-issues-test.mjs
Original file line number Diff line number Diff line change
@@ -6,6 +6,17 @@ describe('mapshaper-clip-erase.js', function () {

describe('Misc. clipping issues', function () {

describe('Bug fix: clipping polygon is enclosed within target polygon and touches at one vertex', function() {
it('test', async function() {
var clipFile = 'test/data/issues/clip_error/clip_shape.json';
var targetFile = 'test/data/issues/clip_error/original_shape.json';
var cmd = `-i ${targetFile} -clip ${clipFile} -o clipped.json`;
var out = await api.applyCommands(cmd);
var clipped = JSON.parse(out['clipped.json']);
assert.equal(clipped.features.length, 1);
});
});

describe('Bug fix: using -clip command with no-replace and name= options', function() {
it('should not duplicate input layer', function(done) {
// Tests a fix for a bug affecting the -clip command when using '+ name=' arguments
3 changes: 3 additions & 0 deletions test/data/issues/clip_error/clip_shape.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"type":"FeatureCollection", "features": [
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-96.51,40.2],[-96.42,41.23],[-97.04,42.22],[-97.91,42.8],[-99.07,43.65],[-100.51,43.81],[-101.24,43.36],[-101.79,42.86],[-101.78,42.1],[-100.75,41.11],[-99.49,40.59],[-96.51,40.2]]]},"properties":{"DN":10,"VALID":"202307111200","EXPIRE":"202307121200","ISSUE":"202307101730","LABEL":"SIGN","LABEL2":"10% Significant Hail Risk","stroke":"#000000","fill":"#888888"}}
]}
3 changes: 3 additions & 0 deletions test/data/issues/clip_error/original_shape.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"type":"FeatureCollection", "features": [
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-101.78,42.1],[-100.82,41],[-99.33,40.45],[-95.07,39.86],[-92.51,40.64],[-93.17,42.57],[-95.95,43.23],[-98.59,43.75],[-101.18,44.4],[-102.33,44.3],[-102.7,43.98],[-102.3,43.12],[-101.78,42.1]]]},"properties":{"DN":15,"VALID":"202307111200","EXPIRE":"202307121200","ISSUE":"202307101730","LABEL":"0.15","LABEL2":"15% Hail Risk","stroke":"#DDAA00","fill":"#FFE066","significant":null}}
]}

0 comments on commit 5713789

Please sign in to comment.