From e2821f691097349aa6ec0d39c536f96ef3ae6147 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 6 May 2022 13:21:09 -0400 Subject: [PATCH] Convert annotations to polygon lists. --- src/annotation.js | 77 ++++++++++++++++++++++++++++++++++++++++++ src/annotationLayer.js | 21 ++++++++++++ src/util/polyops.js | 15 ++++---- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/annotation.js b/src/annotation.js index 4f26bcaa01..57c9987dbe 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -1100,6 +1100,7 @@ var annotation = function (type, args) { layer = m_this.layer(), aPP = layer.options('adjacentPointProximity'), near, atEnd; + // DWM:: more complex if (start.outer) { start = vindex ? start.inner[vindex - 1] : start.outer; @@ -1425,6 +1426,19 @@ var rectangleAnnotation = function (args, annotationName) { return features; }; + /** + * Return this annotation as a polygon list. + * + * @param {geo.util.polyop.spec} [opts] Ignored. + * @returns {array[]} A list of polygons. + */ + this.toPolygonList = function (opts) { + if (m_this._coordinates().length < 3) { + return []; + } + return [[m_this._coordinates().map((pt) => [pt.x, pt.y])]]; + }; + /** * Get and optionally set coordinates associated with this annotation in the * map gcs coordinate system. @@ -1746,6 +1760,50 @@ var ellipseAnnotation = function (args, annotationName) { } return features; }; + + /** + * Return this annotation as a polygon list. + * + * @param {geo.util.polyop.spec} [opts] The ``tolerance`` and + * ``pixelTolerance`` parameters are used if set. Otherwise, a polygon is + * approximated to 1/10th of a pixel at the map's current maximum zoom + * level. + * @returns {array[]} A list of polygons. + */ + this.toPolygonList = function (opts) { + const coord = m_this._coorddinates(); + if (coord.length < 3) { + return []; + } + let tolerance = (opts && opts.tolerance) || 0; + if (!tolerance) { + const map = m_this.layer().map(); + if (opts && opts.pixelTolerance) { + tolerance = map.unitsPerPixel(map.zoom()) * opts.pixelTolerance; + } else { + tolerance = map.unitsPerPixel(map.zoomRange().max) * 0.1; + } + } + const w = ((coord[0].x - coord[1].x) ** 2 + (coord[0].y - coord[1].y) ** 2) ** 0.5; + const h = ((coord[0].x - coord[3].x) ** 2 + (coord[0].y - coord[3].y) ** 2) ** 0.5; + const cx = (coord[0].x + coord[2].x) / 2; + const cy = (coord[0].y - coord[2].y) / 2; + const radius = Math.max(w, h) / 2; + const rotation = -Math.atan2(coord[1].y - coord[0].y, coord[1].x - coord[0].x); + const sides = Math.max(12, Math.ceil(Math.PI / Math.acos((radius - tolerance) / (radius + tolerance)))); + const a = w / 2 * (1 + (1 - Math.cos(Math.PI / sides)) / 2); + const b = h / 2 * (1 + (1 - Math.cos(Math.PI / sides)) / 2); + const poly = []; + const cosr = Math.cos(rotation), sinr = Math.sin(rotation); + for (let s = 0; s < sides; s += 1) { + const sa = Math.PI * 2 * s / sides; + const cosa = Math.cos(sa), sina = Math.sin(sa); + const x = cx + a * cosr * cosa - b * sinr * sina; + const y = cy + a * sinr * cosa + b * cosr * sina; + poly.push([x, y]); + } + return [[poly]]; + }; }; inherit(ellipseAnnotation, rectangleAnnotation); @@ -1904,6 +1962,25 @@ var polygonAnnotation = function (args) { return features; }; + /** + * Return this annotation as a polygon list. + * + * @param {geo.util.polyop.spec} [opts] Ignored. + * @returns {array[]} A list of polygons. + */ + this.toPolygonList = function (opts) { + const coord = m_this._coordinates(); + if (coord.outer) { + const result = [[coord.outer.map((pt) => [pt.x, pt.y])]]; + (coord.inner || []).forEach((h) => result.push(h.map((pt) => [pt.x, pt.y]))); + return result; + } + if (coord.length < 3) { + return []; + } + return [[coord.map((pt) => [pt.x, pt.y])]]; + }; + /** * Get and optionally set coordinates associated with this annotation in the * map gcs coordinate system. diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 93ee0a40d8..d95c611e2e 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -1140,6 +1140,27 @@ var annotationLayer = function (arg) { return m_this; }; + /** + * Return any annotation that has area 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 poly = []; + opts = opts || {}; + opts.annotationIndices = []; + m_annotations.forEach((annotation, idx) => { + if (annotation.toPolygonList) { + annotation.toPolygonList(opts).forEach((p) => poly.push(p)); + opts.annotationIndices.push(idx); + } + }); + return poly; + }; + /** * Initialize. * diff --git a/src/util/polyops.js b/src/util/polyops.js index 9f81bb91ee..932cc94896 100644 --- a/src/util/polyops.js +++ b/src/util/polyops.js @@ -13,13 +13,6 @@ var geo_map = require('../map'); /** * Object specification for polygon operation options. * - * TODO: should poly1/poly2 accept annotationLayer? - * TODO: should epsilon be configurable from the display pixel size? - * - * TODO: add - * rdp: threshold - * ellipseToPoly: threshold (only if we input features) -- use rdp - * * @typedef {object} geo.util.polyop.spec * @property {geo.anyPolygon} [poly1] The first polygon set to operate on. * @property {geo.anyPolygon} [poly2] The second polygon set to operate on. @@ -49,6 +42,14 @@ var geo_map = require('../map'); * array is the length of the input polygon array and each entry is either * undefined or contains a list of indices of corresponding output polygons. * Multiple inputs can refer to the same output. + * @property {number} [tolerance] A tolerance value to pass to features, + * annotations, or other object-based polygon generators to specify how + * closely non-polygons are converted to polygons. This is in the feature's + * gcs coordinate system. + * @property {number} [pixelTolerance] A tolerance value to pass to features, + * annotations, or other object-based polygon generators to specify how + * closely non-polygons are converted to polygons. This is interpreted in + * the feature's map's display coordinates. */ const AlternateOpNames = {