Skip to content

Commit

Permalink
Support feature to polygon list.
Browse files Browse the repository at this point in the history
  • Loading branch information
manthey committed May 6, 2022
1 parent fde9713 commit 2a326db
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 68 deletions.
50 changes: 50 additions & 0 deletions src/polygonFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,56 @@ var polygonFeature = function (arg) {
});
};

/**
* Return the polygons as a polygon list: an array of polygons, each of which
* is an array of polylines, each of which is an array of points, each of
* which is a 2-tuple of numbers.
*
* @param {geo.util.polyop.spec} [opts] Ignored.
* @returns {array[]} A list of polygons.
*/
this.toPolygonList = function (opts) {
const polyFunc = m_this.style.get('polygon');
const posFunc = m_this.style.get('position');
return m_this.data().map((d, i) => {
const polygon = polyFunc(d, i);
const outer = polygon.outer || (Array.isArray(polygon) ? polygon : []);
if (outer.length < 3) {
return [];
}
const resp = [outer.map((p, j) => {
const pos = posFunc(p, j, d, i);
return [pos.x, pos.y];
})];
if (polygon.inner) {
polygon.inner.forEach((h) => {
resp.push(h.map((p, j) => {
const pos = posFunc(p, j, d, i);
return [pos.x, pos.y];
}));
});
}
return resp;
});
};

/**
* Set the data, position accessor, and polygon accessor to use a list of
* polygons.
*
* @param {array[]} poly A list of polygons.
* @param {geo.util.polyop.spec} [opts] Ignored.
* @returns {this}
*/
this.fromPolygonList = function (poly, opts) {
m_this.style({
position: (p) => ({x: p[0], y: p[1]}),
polygon: (p) => ({outer: p[0], inner: p.slice(1)})
});
m_this.data(poly);
return m_this;
};

/**
* Destroy.
*/
Expand Down
143 changes: 79 additions & 64 deletions src/util/polyops.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
var PolyBool = require('polybooljs');
var transform = require('../transform');
var geo_map = require('../map');

/**
* A polygon in any of a variety of formats.
*
* @typedef {geo.polygonFlat|Array.<geo.polygonFlat>|Array.<Array.<geo.polygonFlat>>|geo.polygonObject| Array.<geo.polygonObject>} geo.anyPolygon
* This can be any object with a ``toPolygonList`` and ``fromPolygonList``
* method.
*
* @typedef {geo.polygonFlat|Array.<geo.polygonFlat>|Array.<Array.<geo.polygonFlat>>|geo.polygonObject|Array.<geo.polygonObject>|object} geo.anyPolygon
*/

/**
* Object specification for polygon operation options.
*
* TODO: should poly1/poly2 accept polygonFeature and annotationLayer?
* TODO: should poly1/poly2 accept annotationLayer?
* TODO: should epsilon be configurable from the display pixel size?
*
* TODO: add
Expand All @@ -26,8 +29,9 @@ var transform = require('../transform');
* @property {number} [epsilon1] A precision value to use when processing
* ``poly2``. If not specified, this is computed from the range of values in
* ``poly2``.
* @property {string} [style] If specified, the preferred output style. This
* can be (flat|object)[-list[list[-outer[-list]]]].
* @property {string|object} [style] If specified, the preferred output style.
* This can be (flat|object)[-list[list[-outer[-list]]]]. If an object,
* the object must have a method ``fromPolygonList``.
* @property {string|geo.transform} [ingcs] The default coordinate
* system of the source polygon coordinates. If not specified , this is
* taken from the feature first or the map second if either is available.
Expand Down Expand Up @@ -59,7 +63,7 @@ const AlternateOpNames = {
'&': 'intersect',
mul: 'intersect',
multiply: 'intersect',
x: 'xor'
x: 'xor',
'^': 'xor'
};

Expand All @@ -74,7 +78,7 @@ function seglistToPolygonList(seglist) {
// return seglist.map((s) => PolyBool.polygon(s).regions);
const polys = [];
seglist.forEach((s) => {
let geojson = PolyBool.polygonToGeoJSON(PolyBool.polygon(s));
const geojson = PolyBool.polygonToGeoJSON(PolyBool.polygon(s));
if (geojson.type === 'MultiPolygon') {
geojson.coordinates.forEach((p) => {
polys.push(p.map((h) => h.slice(0, h.length - 1)));
Expand All @@ -86,24 +90,6 @@ function seglistToPolygonList(seglist) {
return polys;
}

/**
* Perform an boolean operation on a set of polygons.
*
* @param {string} op One of 'union', 'intersect', or other value PolyBool
* supports.
* @param {number} epsilon Precision for calculations. In degrees, 1e-9 is
* around 0.11 mm in ground distance.
* @param {array[]} polygons A list of polygons. Each polygon is a list of
* lines. Each line is a list of coordinates. Each coordinate is a list
* of [x, y].
* @returns {array[]} A list of polygons.
*/
function polygonOperation(op, epsilon, polygons) {
const seglist = polygons.map(p => PolyBool.segments({regions: p}));
polygonOperationSeglist(op, epsilon, seglist);
return seglistToPolygonList(seglist);
}

/**
* Perform an boolean operation on a seglist from polygons.
*
Expand Down Expand Up @@ -175,36 +161,43 @@ function polygonOperationSeglist(op, epsilon, seglist) {
* 2-item list with minimum values in x, y; max: a 2-item list with
* maximum values in x, y; epsilon: a recommended value for epsilon in other
* functions.
* @param {geo.util.polyop.spec} [opts] Options for the operation. Only used
* if poly is an object with a toPolygonList method.
* @returns {array[]} A list of polygons.
*/
function toPolygonList(poly, mode) {
function toPolygonList(poly, mode, opts) {
mode = mode || {};

mode.style = '';
if (poly.outer) {
mode.style = '-outer';
poly = [[poly.outer, ...(poly.inner || [])]];
} else if (poly[0].outer) {
mode.style = '-outer-list';
poly = poly.map((p) => [p.outer, ...(p.inner || [])]);
}
if (poly[0].x !== undefined) {
mode.style = 'object';
poly = [[poly.map((pt) => [pt.x, pt.y])]];
} else if (poly[0][0].x !== undefined) {
mode.style = 'object-list' + mode.style;
poly = [poly.map((p) => p.map((pt) => [pt.x, pt.y]))];
} else if (poly[0][0][0] === undefined) {
mode.style = 'flat';
poly = [[poly]];
} else if (poly[0][0][0].x !== undefined) {
mode.style = 'object-listlist' + mode.style;
poly = poly.map((p) => p.map((l) => l.map((pt) => [pt.x, pt.y])));
} else if (poly[0][0][0][0] === undefined) {
mode.style = 'flat-list';
poly = [poly];
if (poly.toPolygonList) {
mode.style = poly;
poly = poly.toPolygonList(opts);
} else {
mode.style = 'flat-listlist' + mode.style;
mode.style = '';
if (poly.outer) {
mode.style = '-outer';
poly = [[poly.outer, ...(poly.inner || [])]];
} else if (poly[0].outer) {
mode.style = '-outer-list';
poly = poly.map((p) => [p.outer, ...(p.inner || [])]);
}
if (poly[0].x !== undefined) {
mode.style = 'object';
poly = [[poly.map((pt) => [pt.x, pt.y])]];
} else if (poly[0][0].x !== undefined) {
mode.style = 'object-list' + mode.style;
poly = [poly.map((p) => p.map((pt) => [pt.x, pt.y]))];
} else if (poly[0][0][0] === undefined) {
mode.style = 'flat';
poly = [[poly]];
} else if (poly[0][0][0].x !== undefined) {
mode.style = 'object-listlist' + mode.style;
poly = poly.map((p) => p.map((l) => l.map((pt) => [pt.x, pt.y])));
} else if (poly[0][0][0][0] === undefined) {
mode.style = 'flat-list';
poly = [poly];
} else {
mode.style = 'flat-listlist' + mode.style;
}
}
mode.min = [poly[0][0][0][0], poly[0][0][0][1]];
mode.max = [poly[0][0][0][0], poly[0][0][0][1]];
Expand All @@ -226,9 +219,14 @@ function toPolygonList(poly, mode) {
* conversion. This includes style: the input polygon format; min: a 2-item
* list with minimum values in x, y; max: a 2-item list with maximum values
* in x, y; epsilon: a recommended value for epsilon in other functions.
* @param {geo.util.polyop.spec} [opts] Options for the operation. Only used
* if ``mode.style`` is an object with a fromPolygonList method.
* @returns {geo.anyPolygon} A polygon in one of several formats.
*/
function fromPolygonList(poly, mode) {
function fromPolygonList(poly, mode, opts) {
if (mode.style.fromPolygonList) {
return mode.style.fromPolygonList(poly, opts);
}
if (mode.style.includes('object')) {
poly = poly.map((p) => p.map((h) => h.map((pt) => ({x: pt[0], y: pt[1]}))));
}
Expand All @@ -253,6 +251,9 @@ function fromPolygonList(poly, mode) {
* @returns {string} The preferred style.
*/
function minimumPolygonStyle(polylist, style) {
if (style.fromPolygonList) {
return style;
}
if (polylist.length > 1) {
if (style.includes('outer')) {
return style + (style.endsWith('list') ? '' : '-list');
Expand Down Expand Up @@ -322,7 +323,7 @@ function generateCorrespondence(poly1, poly2, newpoly, results) {
* Perform a general operation of a set of polygons.
*
* @param {string} op The operation to handle. One of union, intersect,
* difference, or xor.i
* difference, or xor.
* @param {geo.anyPolygon|geo.util.polyop.spec} poly1 Either the first polygon
* set or an object containing all parameters for the function.
* @param {geo.anyPolygon} [poly2] If the poly1 parameter is not a complete
Expand All @@ -332,25 +333,40 @@ function generateCorrespondence(poly1, poly2, newpoly, results) {
* @returns {geo.anyPolygon} A polygon set in the same style as poly1.
*/
function generalOperationProcess(op, poly1, poly2, opts) {
var transform = require('../transform');

op = AlternateOpNames[op] || op;
if (poly1.poly1) {
opts = poly1;
poly1 = opts.poly1;
poly2 = opts.poly2;
}
opts = opts || {};
const ingcs1 = opts.ingcs || (
opts.map ? opts.map.ingcs() : (
poly1.gcs ? poly1.gcs() : (
poly1.layer ? poly1.layer().map().ingcs() : (
poly1.map instanceof geo_map ? poly1.map().ingcs() : undefined))));
const ingcs2 = opts.ingcs || (
opts.map ? opts.map.ingcs() : (
poly2.gcs ? poly2.gcs() : (
poly2.layer ? poly2.layer().map().ingcs() : (
poly2.map instanceof geo_map ? poly2.map().ingcs() : ingcs1))));
const gcs = opts.gcs || (
opts.map ? opts.map.gcs() : (
poly1.layer ? poly1.layer().map().gcs() : (
poly1.map instanceof geo_map ? poly1.map().gcs() : undefined)));
const mode1 = {};
const mode2 = {};
// TODO: handle poly1, poly2 if they are features or annotations
poly1 = toPolygonList(poly1, mode1);
poly2 = toPolygonList(poly2, mode2);
poly1 = toPolygonList(poly1, mode1, opts);
poly2 = toPolygonList(poly2, mode2, opts);
mode1.epsilon = opts.epsilon1 || mode1.epsilon;
mode2.epsilon = opts.epsilon2 || mode1.epsilon;
const ingcs = opts.ingcs || (opts.map ? opts.map.ingcs() : undefined);
const gcs = opts.gcs || (opts.map ? opts.map.gcs() : undefined);
if (ingcs && gcs && ingcs !== gcs) {
poly1 = poly1.map((p) => p.map((h) => transform.transformCoordinates(ingcs, gcs, h)));
poly2 = poly2.map((p) => p.map((h) => transform.transformCoordinates(ingcs, gcs, h)));
if (ingcs1 && gcs && ingcs1 !== gcs) {
poly1 = poly1.map((p) => p.map((h) => transform.transformCoordinates(ingcs1, gcs, h)));
}
if (ingcs2 && gcs && ingcs2 !== gcs) {
poly2 = poly2.map((p) => p.map((h) => transform.transformCoordinates(ingcs2, gcs, h)));
}
let seglist1 = poly1.map(p => PolyBool.segments({regions: p}));
let seglist2 = poly2.map(p => PolyBool.segments({regions: p}));
Expand All @@ -361,11 +377,11 @@ function generalOperationProcess(op, poly1, poly2, opts) {
if (opts.correspond) {
generateCorrespondence(poly1, poly2, newpoly, opts.correspond);
}
if (ingcs && gcs && ingcs !== gcs) {
newpoly = newpoly.map((p) => p.map((h) => transform.transformCoordinates(gcs, ingcs, h)));
if (ingcs1 && gcs && ingcs1 !== gcs) {
newpoly = newpoly.map((p) => p.map((h) => transform.transformCoordinates(gcs, ingcs1, h)));
}
const mode = {style: opts.style || minimumPolygonStyle(newpoly, mode1.style)};
return fromPolygonList(newpoly, mode);
return fromPolygonList(newpoly, mode, opts);
}

/**
Expand All @@ -380,7 +396,6 @@ function generalOperation(op) {
}

module.exports = {
polyop: polygonOperation,
union: generalOperation('union'),
intersect: generalOperation('intersect'),
difference: generalOperation('difference'),
Expand Down
62 changes: 58 additions & 4 deletions tests/cases/polyops.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/* global $ */
var $ = require('jquery');
var geo = require('../test-utils').geo;
var createMap = require('../test-utils').createMap;
var destroyMap = require('../test-utils').destroyMap;
var mockWebglRenderer = geo.util.mockWebglRenderer;
var restoreWebglRenderer = geo.util.restoreWebglRenderer;

describe('geo.util.polyops', function () {
'use strict';

var geo = require('../test-utils').geo;

var polytests = {
flat: {
from: [[0, 1], [2, 3], [4, 5]],
Expand Down Expand Up @@ -115,14 +118,65 @@ describe('geo.util.polyops', function () {
opTests.forEach((test) => {
it(JSON.stringify(test.a) + ' and ' + JSON.stringify(test.b), function () {
const opts = {correspond: {}};
const out = geo.util.polyops[op](test.a, test.b, opts);
let out = geo.util.polyops[op](test.a, test.b, opts);
expect(out).toEqual(test[op].out);
expect(opts.correspond.poly1).toEqual(test[op].ca);
expect(opts.correspond.poly2).toEqual(test[op].cb);

opts.poly1 = test.a;
opts.poly2 = test.b;
out = geo.util.polyops[op](opts);
expect(out).toEqual(test[op].out);
});
});
});
});
});

var polygonOps = [{
op: 'union', len: [[8, 4]]
}, {
op: 'difference', len: [[4], [8]]
}, {
op: 'intersect', len: [[8]]
}, {
op: 'xor', len: [[20]]
}];

describe('with polygonFeature', function () {
polygonOps.forEach((test) => {
it(test.op, function () {
mockWebglRenderer();
const map = createMap();
const layer = map.createLayer('feature', {renderer: 'webgl'});
const poly1 = geo.polygonFeature.create(layer);
const poly2 = geo.polygonFeature.create(layer);
const poly3 = geo.polygonFeature.create(layer);

poly1.style({polygon: (d) => ({outer: d[0], inner: d.slice(1)})});
poly1.data([[
[[-1.1, 50.7], [-1.3, 50.7], [-1.3, 50.9], [-1.1, 50.9]]
]]);
poly2.style({polygon: (d) => ({outer: d[0], inner: d.slice(1)})});
poly2.data([[
[[-1.2, 50.75], [-1.4, 50.75], [-1.4, 50.85], [-1.2, 50.85]],
[[-1.25, 50.78], [-1.35, 50.78], [-1.35, 50.82], [-1.25, 50.82]]
]]);

geo.util.polyops[test.op](poly1, poly2, {style: poly3});

const d = poly3.data();
expect(d.length).toEqual(test.len.length);
test.len.forEach((val, idx) => {
expect(d[idx].length).toEqual(val.length);
val.forEach((val2, idx2) => {
expect(d[idx][idx2].length).toEqual(val2);
});
});

destroyMap();
restoreWebglRenderer();
});
});
});
});

0 comments on commit 2a326db

Please sign in to comment.