diff --git a/src/polygonFeature.js b/src/polygonFeature.js index 5ec51875ab..fde1273df6 100644 --- a/src/polygonFeature.js +++ b/src/polygonFeature.js @@ -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. */ diff --git a/src/util/polyops.js b/src/util/polyops.js index 01f15a7e1d..9f81bb91ee 100644 --- a/src/util/polyops.js +++ b/src/util/polyops.js @@ -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.|Array.>|geo.polygonObject| Array.} geo.anyPolygon + * This can be any object with a ``toPolygonList`` and ``fromPolygonList`` + * method. + * + * @typedef {geo.polygonFlat|Array.|Array.>|geo.polygonObject|Array.|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 @@ -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. @@ -59,7 +63,7 @@ const AlternateOpNames = { '&': 'intersect', mul: 'intersect', multiply: 'intersect', - x: 'xor' + x: 'xor', '^': 'xor' }; @@ -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))); @@ -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. * @@ -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]]; @@ -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]})))); } @@ -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'); @@ -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 @@ -332,6 +333,8 @@ 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; @@ -339,18 +342,31 @@ function generalOperationProcess(op, poly1, poly2, opts) { 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})); @@ -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); } /** @@ -380,7 +396,6 @@ function generalOperation(op) { } module.exports = { - polyop: polygonOperation, union: generalOperation('union'), intersect: generalOperation('intersect'), difference: generalOperation('difference'), diff --git a/tests/cases/polyops.js b/tests/cases/polyops.js index e4619827ad..1fec277c95 100644 --- a/tests/cases/polyops.js +++ b/tests/cases/polyops.js @@ -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]], @@ -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(); + }); + }); + }); });