diff --git a/src/annotation.js b/src/annotation.js index 35f14c9556..4f26bcaa01 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -215,7 +215,7 @@ var annotation = function (type, args) { */ this._rotateHandlePosition = function (offset, rotation) { var map = m_this.layer().map(), - coor = m_this._coordinates(), + coord = m_this._coordinates(), center = util.centerFromPerimeter(m_this._coordinates()), dispCenter = center ? map.gcsToDisplay(center, null) : undefined, i, pos, maxr2 = 0, r; @@ -224,8 +224,9 @@ var annotation = function (type, args) { } offset = offset || 0; rotation = rotation || 0; - for (i = 0; i < coor.length; i += 1) { - pos = map.gcsToDisplay(coor[i], null); + coord = coord.outer ? coord.outer : coord; + for (i = 0; i < coord.length; i += 1) { + pos = map.gcsToDisplay(coord[i], null); maxr2 = Math.max(maxr2, Math.pow(pos.x - dispCenter.x, 2) + Math.pow(pos.y - dispCenter.y, 2)); } r = Math.sqrt(maxr2) + offset; @@ -390,6 +391,22 @@ var annotation = function (type, args) { } }; + /** + * Return a copy of the _coordinates or a geo.polygon record so that it + * doesn't share memory with the original. + * + * @param {geo.polygon} [coord] if specified, return a copy of this object. + * Otherwise, return a copy of this._coordinates. + * @returns {geo.polygon} + */ + this._copyOfCoordinates = function (coord) { + coord = coord || m_this._coordinates(); + if (!coord.outer) { + return coord.slice(); + } + return {outer: coord.outer.slice(), inner: (coord.inner || []).map((h) => h.slice())}; + }; + /** * When an edit handle is selected or deselected (for instance, by moving the * mouse on or off of it), mark if it is selected and record the current @@ -409,7 +426,7 @@ var annotation = function (type, args) { var amountRotated = (m_this._editHandle || {}).amountRotated || 0; m_this._editHandle = { handle: handle, - startCoordinates: m_this._coordinates().slice(), + startCoordinates: m_this._copyOfCoordinates(), center: util.centerFromPerimeter(m_this._coordinates()), rotatePosition: m_this._rotateHandlePosition( handle.style.rotateHandleOffset, handle.style.rotateHandleRotation + amountRotated), @@ -465,9 +482,9 @@ var annotation = function (type, args) { m_options[arg1] = arg2; } if (m_options.coordinates) { - var coor = m_options.coordinates; + var coord = m_options.coordinates; delete m_options.coordinates; - m_this._coordinates(coor); + m_this._coordinates(coord); } if (m_options.name !== undefined) { var name = m_options.name; @@ -676,7 +693,7 @@ var annotation = function (type, args) { gcs = (gcs === null ? map.gcs() : ( gcs === undefined ? map.ingcs() : gcs)); if (gcs !== map.gcs()) { - coord = transform.transformCoordinates(map.gcs(), gcs, coord); + coord = m_this._convertCoordinates(map.gcs(), gcs, coord); } } return coord; @@ -752,20 +769,20 @@ var annotation = function (type, args) { * should not be represented (for instance, while it is being created). */ this.geojson = function (gcs, includeCrs) { - var coor = m_this._geojsonCoordinates(gcs), + var coord = m_this._geojsonCoordinates(gcs), geotype = m_this._geojsonGeometryType(), styles = m_this._geojsonStyles(), objStyle = m_this.options('style') || {}, objLabelStyle = m_this.labelStyle() || {}, i, key, value; - if (!coor || !coor.length || !geotype) { + if (!coord || !coord.length || !geotype) { return; } var obj = { type: 'Feature', geometry: { type: geotype, - coordinates: coor + coordinates: coord }, properties: { annotationType: m_type, @@ -850,22 +867,25 @@ var annotation = function (type, args) { features[editHandleFeatureLevel] = {point: []}; } editPoints = features[editHandleFeatureLevel].point; - vertices.forEach(function (pt, idx) { - if (opts.vertex !== false) { - editPoints.push($.extend({}, pt, {type: 'vertex', index: idx, style: style, editHandle: true})); - } - if (opts.edge !== false && idx !== vertices.length - 1 && (pt.x !== vertices[idx + 1].x || pt.y !== vertices[idx + 1].y)) { - editPoints.push($.extend({ - x: (pt.x + vertices[idx + 1].x) / 2, - y: (pt.y + vertices[idx + 1].y) / 2 - }, {type: 'edge', index: idx, style: style, editHandle: true})); - } - if (opts.edge !== false && !isOpen && idx === vertices.length - 1 && (pt.x !== vertices[0].x || pt.y !== vertices[0].y)) { - editPoints.push($.extend({ - x: (pt.x + vertices[0].x) / 2, - y: (pt.y + vertices[0].y) / 2 - }, {type: 'edge', index: idx, style: style, editHandle: true})); - } + const vertexList = vertices.outer ? [vertices.outer].concat(vertices.inner || []) : [vertices]; + vertexList.forEach((vert, vidx) => { + vert.forEach(function (pt, idx) { + if (opts.vertex !== false) { + editPoints.push($.extend({}, pt, {type: 'vertex', index: idx, vindex: vidx, style: style, editHandle: true})); + } + if (opts.edge !== false && idx !== vert.length - 1 && (pt.x !== vert[idx + 1].x || pt.y !== vert[idx + 1].y)) { + editPoints.push($.extend({ + x: (pt.x + vert[idx + 1].x) / 2, + y: (pt.y + vert[idx + 1].y) / 2 + }, {type: 'edge', index: idx, vindex: vidx, style: style, editHandle: true})); + } + if (opts.edge !== false && !isOpen && idx === vert.length - 1 && (pt.x !== vert[0].x || pt.y !== vert[0].y)) { + editPoints.push($.extend({ + x: (pt.x + vert[0].x) / 2, + y: (pt.y + vert[0].y) / 2 + }, {type: 'edge', index: idx, vindex: vidx, style: style, editHandle: true})); + } + }); }); if (opts.center !== false) { editPoints.push($.extend({}, util.centerFromPerimeter(m_this._coordinates()), {type: 'center', style: style, editHandle: true})); @@ -887,13 +907,43 @@ var annotation = function (type, args) { } if (selected) { editPoints.forEach(function (pt) { - if (pt.type === selected.type && pt.index === selected.index) { + if (pt.type === selected.type && pt.index === selected.index && pt.vindex === selected.vindex) { pt.selected = true; } }); } }; + /** + * Apply a map function of a geo.polygon. + * + * @param {geo.polygon} coord The polygon to apply the function to. + * @param {function} func The function to apply. + * @returns {array} The map results. + */ + this._coordinatesMapFunc = function (coord, func) { + if (!coord.outer) { + return coord.map(func); + } + return { + outer: coord.outer.map(func), + inner: (coord.inner || []).map((h) => h.map(func)) + }; + }; + + /** + * Check if two geo.polygons differ in their first point. + * + * @param {geo.polygon} coord1 One polygon to compare. + * @param {geo.polygon} coord2 A second polygon to compare. + * @returns {boolean} true if the first point matches. + */ + this._firstPointDifferent = function (coord1, coord2) { + coord1 = coord1.outer ? coord1.outer : coord1; + coord2 = coord2.outer ? coord2.outer : coord2; + return (coord1[0].x !== coord2[0].x || coord1[0].y !== coord2[0].y); + }; + /** * Process the edit center action for a general annotation. * @@ -908,10 +958,10 @@ var annotation = function (type, args) { y: evt.mouse.mapgcs.y - evt.state.origin.mapgcs.y }, curPts = m_this._coordinates(); - var pts = start.map(function (elem) { + var pts = m_this._coordinatesMapFunc(start, function (elem) { return {x: elem.x + delta.x, y: elem.y + delta.y}; }); - if (pts[0].x !== curPts[0].x || pts[0].y !== curPts[0].y) { + if (m_this._firstPointDifferent(pts, curPts)) { m_this._coordinates(pts); return true; } @@ -940,14 +990,14 @@ var annotation = function (type, args) { handle.rotatePosition.x + delta.x - handle.center.x), ang = ang2 - ang1, curPts = m_this._coordinates(); - var pts = start.map(function (elem) { + var pts = m_this._coordinatesMapFunc(start, function (elem) { var delta = {x: elem.x - handle.center.x, y: elem.y - handle.center.y}; return { x: delta.x * Math.cos(ang) - delta.y * Math.sin(ang) + handle.center.x, y: delta.x * Math.sin(ang) + delta.y * Math.cos(ang) + handle.center.y }; }); - if (pts[0].x !== curPts[0].x || pts[0].y !== curPts[0].y) { + if (m_this._firstPointDifferent(pts, curPts)) { m_this._coordinates(pts); handle.amountRotated = handle.startAmountRotated + ang; return true; @@ -985,13 +1035,13 @@ var annotation = function (type, args) { curPts = m_this._coordinates(); if (d02 && d01) { var scale = d02 / d01; - var pts = start.map(function (elem) { + var pts = m_this._coordinatesMapFunc(start, function (elem) { return { x: (elem.x - handle.center.x) * scale + handle.center.x, y: (elem.y - handle.center.y) * scale + handle.center.y }; }); - if (pts[0].x !== curPts[0].x || pts[0].y !== curPts[0].y) { + if (m_this._firstPointDifferent(pts, curPts)) { m_this._coordinates(pts); return true; } @@ -1009,11 +1059,17 @@ var annotation = function (type, args) { this._processEditActionEdge = function (evt) { var handle = m_this._editHandle, index = handle.handle.index, + vindex = handle.handle.vindex, curPts = m_this._coordinates(); - curPts.splice(index + 1, 0, {x: handle.handle.x, y: handle.handle.y}); + if (!curPts.outer) { + curPts.splice(index + 1, 0, {x: handle.handle.x, y: handle.handle.y}); + } else { + const loop = vindex ? curPts.inner[vindex - 1] : curPts.outer; + loop.splice(index + 1, 0, {x: handle.handle.x, y: handle.handle.y}); + } handle.handle.type = 'vertex'; handle.handle.index += 1; - handle.startCoordinates = curPts.slice(); + handle.startCoordinates = m_this._copyOfCoordinates(curPts); m_this.modified(); return true; }; @@ -1031,8 +1087,10 @@ var annotation = function (type, args) { this._processEditActionVertex = function (evt, canClose) { var handle = m_this._editHandle, index = handle.handle.index, + vindex = handle.handle.vindex, start = handle.startCoordinates, - curPts = m_this._coordinates(), + ptsRef = m_this._coordinates(), + curPts = ptsRef.outer ? (vindex ? ptsRef.inner[vindex - 1] : ptsRef.outer) : ptsRef, origLen = curPts.length, origPt = curPts[index], delta = { @@ -1043,6 +1101,9 @@ var annotation = function (type, args) { aPP = layer.options('adjacentPointProximity'), near, atEnd; + if (start.outer) { + start = vindex ? start.inner[vindex - 1] : start.outer; + } curPts[index] = { x: start[index].x + delta.x, y: start[index].y + delta.y @@ -1078,9 +1139,37 @@ var annotation = function (type, args) { curPts[index].x === origPt.x && curPts[index].y === origPt.y) { return false; } - m_this._coordinates(curPts); + + m_this._coordinates(ptsRef); return true; }; + + /** + * Transform the annotations coordinates from one gcs to another. + * + * @param {string|geo.transform} oldgcs The current gcs. + * @param {string|geo.transform} newgcs The new gcs. + * @param {geo.polygon} [coord] If not specified, convert the coordinates in + * place. If specified, convert these coordinates and return them (don't + * alter the existing values). + * @returns {geo.polygon} + */ + this._convertCoordinates = function (oldgcs, newgcs, coord) { + const store = !coord; + coord = coord || m_this._coordinates(); + if (!coord.outer) { + coord = transform.transformCoordinates(oldgcs, newgcs, coord); + } else { + coord = { + outer: transform.transformCoordinates(oldgcs, newgcs, coord.outer), + inner: (coord.inner || []).map((h) => transform.transformCoordinates(oldgcs, newgcs, h)) + }; + } + if (store) { + m_this._coordinates(coord); + } + return coord; + }; }; /* Functions used by multiple annotations */ @@ -1368,12 +1457,12 @@ var rectangleAnnotation = function (args, annotationName) { if (!src || m_this.state() === annotationState.create || src.length < 4) { return; } - var coor = []; + var coord = []; for (var i = 0; i < 4; i += 1) { - coor.push([src[i].x, src[i].y]); + coord.push([src[i].x, src[i].y]); } - coor.push([src[0].x, src[0].y]); - return [coor]; + coord.push([src[0].x, src[0].y]); + return [coord]; }; /** @@ -1745,12 +1834,16 @@ var polygonAnnotation = function (args) { fillColor: {r: 0.3, g: 0.3, b: 0.3}, fillOpacity: 0.25, line: function (d) { + const coord = m_this._coordinates(); /* Return an array that has the same number of items as we have * vertices. */ - return Array.apply(null, Array(m_this.options('vertices').length)).map( + return Array.apply(null, Array((coord.outer || coord).length)).map( function () { return d; }); }, position: function (d, i) { + if (d.x !== undefined) { + return d.x; + } return m_this.options('vertices')[i]; }, stroke: false, @@ -1779,7 +1872,7 @@ var polygonAnnotation = function (args) { switch (state) { case annotationState.create: features = []; - if (opt.vertices && opt.vertices.length >= 3) { + if (opt.vertices && (opt.vertices.outer || opt.vertices.length >= 3)) { features[1] = { polygon: { polygon: opt.vertices, @@ -1939,15 +2032,23 @@ var polygonAnnotation = function (args) { */ this._geojsonCoordinates = function (gcs) { var src = m_this.coordinates(gcs); - if (!src || src.length < 3 || m_this.state() === annotationState.create) { + if (!src || (!src.outer && src.length < 3) || m_this.state() === annotationState.create) { return; } - var coor = []; - for (var i = 0; i < src.length; i += 1) { - coor.push([src[i].x, src[i].y]); + var coord = []; + if (!src.outer) { + coord = [src.map((pt) => [pt.x, pt.y])]; + coord[0].push(coord[0][0].slice()); + } else { + coord = [src.outer.map((pt) => [pt.x, pt.y])]; + coord[0].push(coord[0][0].slice()); + (src.inner || []).forEach((h) => { + const poly = h.map((pt) => [pt.x, pt.y]); + poly.push(poly[0].slice()); + coord.push(poly); + }); } - coor.push([src[0].x, src[0].y]); - return [coor]; + return coord; }; /** @@ -2220,11 +2321,11 @@ var lineAnnotation = function (args) { if (!src || src.length < 2 || m_this.state() === annotationState.create) { return; } - var coor = []; + var coord = []; for (var i = 0; i < src.length; i += 1) { - coor.push([src[i].x, src[i].y]); + coord.push([src[i].x, src[i].y]); } - return coor; + return coord; }; /** diff --git a/src/annotationLayer.js b/src/annotationLayer.js index a7c16bb2ec..93ee0a40d8 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -417,8 +417,7 @@ var annotationLayer = function (arg) { gcs = (gcs === null ? map.gcs() : ( gcs === undefined ? map.ingcs() : gcs)); if (gcs !== map.gcs()) { - annotation._coordinates(transform.transformCoordinates( - gcs, map.gcs(), annotation._coordinates())); + annotation._convertCoordinates(gcs, map.gcs()); } if (update !== false) { m_this.modified(); diff --git a/src/util/common.js b/src/util/common.js index 331dc8bd90..52faa0f6ff 100644 --- a/src/util/common.js +++ b/src/util/common.js @@ -603,13 +603,18 @@ var util = { * this is the same as the average of all the points of the lines of the * perimeter. * - * @param {geo.geoPosition[]} coor An array of coordinates. + * @param {geo.geoPolygon} coor An array of coordinates. This can also be + * a polygon object with an ``outer`` element with an array of + * coordinates. * @returns {geo.geoPosition|undefined} The position for the center, or * `undefined` if no such position exists. * @memberof geo.util */ centerFromPerimeter: function (coor) { var position, p0, p1, w, sumw, i; + if (coor && coor.outer) { + coor = coor.outer; + } if (!coor || !coor.length) { return; } diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index cf1b0690b6..0820e8256a 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -414,7 +414,7 @@ describe('geo.annotation', function () { undefined, undefined, undefined]); // add handles with an edge selected handles.splice(0, handles.length); - ann._editHandle = {handle: {selected: true, type: 'edge', index: 1}}; + ann._editHandle = {handle: {selected: true, type: 'edge', index: 1, vindex: 0}}; ann._addEditHandles(features, ann._coordinates()); expect(handles.map(function (h) { return h.selected; })).toEqual([ undefined, undefined, undefined, true, undefined, undefined, @@ -1052,6 +1052,132 @@ describe('geo.annotation', function () { }); }); + describe('geo.annotation.polygonAnnotation with holes', function () { + var vertices = { + outer: [{x: 30, y: 0}, {x: 50, y: 0}, {x: 40, y: 20}, {x: 30, y: 10}], + inner: [[{x: 25, y: 5}, {x: 45, y: 5}, {x: 35, y: 15}, {x: 25, y: 10}]] + }; + var vertices2 = [{x: 30, y: 10}, {x: 50, y: 10}, {x: 40, y: 30}]; + it('features', function () { + var map = createMap(); + var layer = map.createLayer('annotation', { + annotations: ['polygon'] + }); + var ann = geo.annotation.polygonAnnotation({layer: layer, vertices: vertices}); + var features = ann.features(); + expect(features.length).toBe(1); + expect(features[0].polygon.polygon).toEqual(vertices); + expect(features[0].polygon.style.polygon({polygon: 'a'})).toBe('a'); + ann.state(geo.annotation.state.edit); + features = ann.features(); + expect(features.length).toBe(4); + ann.state(geo.annotation.state.create); + features = ann.features(); + expect(features.length).toBe(2); + expect(features[0]).toBe(undefined); + expect(features[1].polygon.polygon).toEqual(vertices); + expect(features[1].polygon.style.polygon({polygon: 'a'})).toBe('a'); + ann.options('vertices', [{x: 3, y: 0}, {x: 5, y: 0}]); + features = ann.features(); + expect(features.length).toBe(3); + expect(features[0]).toBe(undefined); + expect(features[1]).toBe(undefined); + expect(features[2].line.line.length).toBe(2); + }); + it('_coordinates', function () { + var ann = geo.annotation.polygonAnnotation({vertices: vertices}); + expect(ann._coordinates()).toEqual(vertices); + ann._coordinates(vertices2); + expect(ann._coordinates()).toEqual(vertices2); + }); + it('_geojsonCoordinates', function () { + var ann = geo.annotation.polygonAnnotation(); + expect(ann._geojsonCoordinates()).toBe(undefined); + ann._coordinates(vertices); + var coor = ann._geojsonCoordinates(); + expect(coor[0].length).toBe(5); + expect(coor[1].length).toBe(5); + }); + it('_geojsonGeometryType', function () { + var ann = geo.annotation.polygonAnnotation(); + expect(ann._geojsonGeometryType()).toBe('Polygon'); + }); + it('geojson', function () { + var ann = geo.annotation.polygonAnnotation({vertices: vertices}); + var geojson = ann.geojson(); + expect(geojson.type).toBe('Feature'); + expect(geojson.geometry.type).toBe('Polygon'); + expect(geojson.geometry.coordinates.length).toBe(2); + expect(geojson.geometry.coordinates[0].length).toBe(5); + expect(geojson.geometry.coordinates[1].length).toBe(5); + expect(geojson.geometry.coordinates[0][2][1]).toBeCloseTo(20); + expect(geojson.geometry.coordinates[1][2][1]).toBeCloseTo(15); + }); + it('mouseMove', function () { + var ann = geo.annotation.polygonAnnotation({vertices: vertices}); + expect(ann.mouseMove({mapgcs: {x: 6, y: 4}})).toBe(undefined); + expect(ann.options('vertices')).toEqual(vertices); + }); + it('mouseClick', function () { + var map = createMap(); + var layer = map.createLayer('annotation', { + annotations: ['polygon'] + }); + var ann = geo.annotation.polygonAnnotation({layer: layer}); + var time = Date.now(); + expect(ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: {x: 10, y: 20}, + mapgcs: map.displayToGcs({x: 10, y: 20}, null) + })).toBe(undefined); + ann.state(geo.annotation.state.create); + expect(ann.mouseClick({ + buttonsDown: {middle: true}, + time: time, + map: vertices.outer[0], + mapgcs: map.displayToGcs(vertices.outer[0], null) + })).toBe(undefined); + expect(ann.options('vertices').length).toBe(0); + expect(ann.mouseClick({ + buttonsDown: {right: true}, + time: time, + map: vertices.outer[0], + mapgcs: map.displayToGcs(vertices.outer[0], null) + })).toBe(undefined); + expect(ann.options('vertices').length).toBe(0); + expect(ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: vertices.outer[0], + mapgcs: map.displayToGcs(vertices.outer[0], null) + })).toBe(true); + expect(ann.options('vertices').length).toBe(2); + ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: vertices.outer[1], + mapgcs: map.displayToGcs(vertices.outer[1], null) + }); + expect(ann.options('vertices').length).toBe(3); + ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: vertices.outer[2], + mapgcs: map.displayToGcs(vertices.outer[2], null) + }); + expect(ann.options('vertices').length).toBe(4); + expect(ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: {x: vertices.outer[0].x + 1, y: vertices.outer[0].y}, + mapgcs: map.displayToGcs({x: vertices.outer[0].x + 1, y: vertices.outer[0].y}, null) + })).toBe('done'); + expect(ann.options('vertices').length).toBe(3); + expect(ann.state()).toBe(geo.annotation.state.done); + }); + }); + describe('geo.annotation.pointAnnotation', function () { var point = {x: 30, y: 25}; var point2 = {x: 50, y: 35}; diff --git a/tests/cases/util.js b/tests/cases/util.js index 62b8ec97ee..5259edf68d 100644 --- a/tests/cases/util.js +++ b/tests/cases/util.js @@ -44,6 +44,7 @@ describe('geo.util', function () { expect(util.centerFromPerimeter([ {x: 1, y: 1}, {x: 3, y: 1}, {x: 5, y: 1}, {x: 5, y: 3}, {x: 1, y: 3} ])).toEqual({x: 3, y: 2}); + expect(util.centerFromPerimeter({outer: [{x: 1, y: 1}]})).toEqual({x: 1, y: 1}); }); it('rdpLineSimplify', function () {