From d754129a7731a053dbae6ee193265943a06d1c64 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 8 Nov 2016 11:13:22 -0500 Subject: [PATCH] Make annotation states more consistent. Prior to this, rectangles didn't enter create mode. Now they do, and the concept of actions has become more general. --- src/annotation.js | 106 ++++++++++++++++++++++++++++++--- src/annotationLayer.js | 88 +++++++++++++-------------- tests/cases/annotation.js | 40 +++++++++++++ tests/cases/annotationLayer.js | 13 ++-- 4 files changed, 187 insertions(+), 60 deletions(-) diff --git a/src/annotation.js b/src/annotation.js index 488b37def3..0b86097165 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -1,6 +1,7 @@ var $ = require('jquery'); var inherit = require('./inherit'); var geo_event = require('./event'); +var geo_action = require('./action'); var transform = require('./transform'); var util = require('./util'); var registerAnnotation = require('./registry').registerAnnotation; @@ -16,6 +17,8 @@ var annotationState = { edit: 'edit' }; +var annotationActionOwner = 'annotationAction'; + ///////////////////////////////////////////////////////////////////////////// /** * Base annotation class @@ -121,6 +124,28 @@ var annotation = function (type, args) { return this; }; + /** + * Return actions needed for the specified state of this annotation. + * + * @param {string} state: the state to return actions for. Defaults to + * the current state. + * @returns {array}: a list of actions. + */ + this.actions = function () { + return []; + }; + + /** + * Process any actions for this annotation. + * + * @param {object} evt: the action event. + * @returns {boolean|string} true to update the annotation, 'done' if the + * annotation was completed (changed from create to done state), 'remove' + * if the annotation should be removed, falsy to not update anything. + */ + this.processAction = function () { + }; + /** * Set or get options. * @@ -221,7 +246,7 @@ var annotation = function (type, args) { * @returns {array} an array of coordinates. */ this.coordinates = function (gcs) { - var coord = this._coordinates(); + var coord = this._coordinates() || []; if (this.layer()) { var map = this.layer().map(); gcs = (gcs === null ? map.gcs() : ( @@ -379,19 +404,81 @@ var rectangleAnnotation = function (args) { delete args.coordinates; annotation.call(this, 'rectangle', args); + /** + * Return actions needed for the specified state of this annotation. + * + * @param {string} state: the state to return actions for. Defaults to + * the current state. + * @returns {array}: a list of actions. + */ + this.actions = function (state) { + if (!state) { + state = this.state(); + } + switch (state) { + case annotationState.create: + return [{ + action: geo_action.annotation_rectangle, + name: 'rectangle create', + owner: annotationActionOwner, + input: 'left', + modifiers: {shift: false, ctrl: false}, + selectionRectangle: true + }]; + default: + return []; + } + }; + + /** + * Process any actions for this annotation. + * + * @param {object} evt: the action event. + * @returns {boolean|string} true to update the annotation, 'done' if the + * annotation was completed (changed from create to done state), 'remove' + * if the annotation should be removed, falsy to not update anything. + */ + this.processAction = function (evt) { + var layer = this.layer(); + if (this.state() !== annotationState.create || !layer || + evt.state.action !== geo_action.annotation_rectangle) { + return; + } + var map = layer.map(); + this.options('corners', [ + /* Keep in map gcs, not interface gcs to avoid wrapping issues */ + map.displayToGcs({x: evt.lowerLeft.x, y: evt.lowerLeft.y}, null), + map.displayToGcs({x: evt.lowerLeft.x, y: evt.upperRight.y}, null), + map.displayToGcs({x: evt.upperRight.x, y: evt.upperRight.y}, null), + map.displayToGcs({x: evt.upperRight.x, y: evt.lowerLeft.y}, null) + ]); + this.state(annotationState.done); + return 'done'; + }; + /** * Get a list of renderable features for this annotation. * * @returns {array} an array of features. */ this.features = function () { - var opt = this.options(); - return [{ - polygon: { - polygon: opt.corners, - style: opt.style - } - }]; + var opt = this.options(), + state = this.state(), + features; + switch (state) { + case annotationState.create: + features = []; + break; + default: + features = [{ + polygon: { + polygon: opt.corners, + style: opt.style + } + }]; + break; + } + return features; }; /** @@ -420,7 +507,7 @@ var rectangleAnnotation = function (args) { */ this._geojsonCoordinates = function (gcs) { var src = this.coordinates(gcs); - if (!src || src.length < 4) { + if (!src || this.state() === annotationState.create || src.length < 4) { return; } var coor = []; @@ -849,6 +936,7 @@ registerAnnotation('point', pointAnnotation, pointRequiredFeatures); module.exports = { state: annotationState, + actionOwner: annotationActionOwner, annotation: annotation, pointAnnotation: pointAnnotation, polygonAnnotation: polygonAnnotation, diff --git a/src/annotationLayer.js b/src/annotationLayer.js index d62a63003a..1a89891932 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -1,6 +1,5 @@ var inherit = require('./inherit'); var featureLayer = require('./featureLayer'); -var geo_action = require('./action'); var geo_annotation = require('./annotation'); var geo_event = require('./event'); var registry = require('./registry'); @@ -47,7 +46,6 @@ var annotationLayer = function (args) { s_update = this._update, m_buildTime = timestamp(), m_options, - m_actions, m_mode = null, m_annotations = [], m_features = []; @@ -70,16 +68,6 @@ var annotationLayer = function (args) { finalPointProximity: 10 // in pixels, 0 is exact }, args); - m_actions = { - rectangle: { - action: geo_action.annotation_rectangle, - owner: 'annotationLayer', - input: 'left', - modifiers: {shift: false, ctrl: false}, - selectionRectangle: true - } - }; - /** * Process a selection event. If we are in rectangle-creation mode, this * creates a rectangle. @@ -87,22 +75,37 @@ var annotationLayer = function (args) { * @param {geo.event} evt the selection event. */ this._processSelection = function (evt) { - if (m_this.mode() === 'rectangle') { - m_this.mode(null); - if (evt.state.action === geo_action.annotation_rectangle) { - var map = m_this.map(); - var params = { - corners: [ - /* Keep in map gcs, not interface gcs to avoid wrapping issues */ - map.displayToGcs({x: evt.lowerLeft.x, y: evt.lowerLeft.y}, null), - map.displayToGcs({x: evt.lowerLeft.x, y: evt.upperRight.y}, null), - map.displayToGcs({x: evt.upperRight.x, y: evt.upperRight.y}, null), - map.displayToGcs({x: evt.upperRight.x, y: evt.lowerLeft.y}, null) - ], - layer: this - }; - this.addAnnotation(geo_annotation.rectangleAnnotation(params)); - } + var update; + if (evt.state && evt.state.actionRecord && + evt.state.actionRecord.owner === geo_annotation.actionOwner && + this.currentAnnotation) { + update = this.currentAnnotation.processAction(evt); + } + this._updateFromEvent(update); + }; + + /** + * Handle updating the current annotation based on an update state. + * + * @param {string|undefined} update: truthy to update. 'done' if the + * annotation was completed and the mode should return to null. 'remove' + * to remove the current annotation and set the mode to null. Falsy to do + * nothing. + */ + this._updateFromEvent = function (update) { + switch (update) { + case 'remove': + m_this.removeAnnotation(m_this.currentAnnotation, false); + m_this.mode(null); + break; + case 'done': + m_this.mode(null); + break; + } + if (update) { + m_this.modified(); + m_this._update(); + m_this.draw(); } }; @@ -132,20 +135,7 @@ var annotationLayer = function (args) { this._handleMouseClick = function (evt) { if (this.mode() && this.currentAnnotation) { var update = this.currentAnnotation.mouseClick(evt); - switch (update) { - case 'remove': - m_this.removeAnnotation(m_this.currentAnnotation, false); - m_this.mode(null); - break; - case 'done': - m_this.mode(null); - break; - } - if (update) { - m_this.modified(); - m_this._update(); - m_this.draw(); - } + this._updateFromEvent(update); } }; @@ -323,7 +313,8 @@ var annotationLayer = function (args) { return m_mode; } if (arg !== m_mode) { - var createAnnotation, mapNode = m_this.map().node(), oldMode = m_mode; + var createAnnotation, actions, + mapNode = m_this.map().node(), oldMode = m_mode; m_mode = arg; mapNode.css('cursor', m_mode ? 'crosshair' : ''); if (m_mode) { @@ -347,18 +338,21 @@ var annotationLayer = function (args) { createAnnotation = geo_annotation.polygonAnnotation; break; case 'rectangle': - m_this.map().interactor().addAction(m_actions.rectangle); + createAnnotation = geo_annotation.rectangleAnnotation; break; } + m_this.map().interactor().removeAction( + undefined, undefined, geo_annotation.actionOwner); if (createAnnotation) { this.currentAnnotation = createAnnotation({ state: geo_annotation.state.create, layer: this }); this.addAnnotation(m_this.currentAnnotation); - } - if (m_mode !== 'rectangle') { - m_this.map().interactor().removeAction(m_actions.rectangle); + actions = this.currentAnnotation.actions(geo_annotation.state.create); + $.each(actions, function (idx, action) { + m_this.map().interactor().addAction(action); + }); } m_this.geoTrigger(geo_event.annotation.mode, { mode: m_mode, oldMode: oldMode}); diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index a27a90eb43..252343d0fb 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -38,6 +38,8 @@ describe('geo.annotation', function () { expect(ann.layer()).toBe(undefined); expect(ann.features()).toEqual([]); expect(ann.coordinates()).toEqual([]); + expect(ann.actions()).toEqual([]); + expect(ann.processAction()).toBe(undefined); expect(ann.mouseClick()).toBe(undefined); expect(ann.mouseMove()).toBe(undefined); expect(ann._coordinates()).toEqual([]); @@ -193,6 +195,44 @@ describe('geo.annotation', function () { expect(features.length).toBe(1); expect(features[0].polygon.polygon).toEqual(corners); expect(features[0].polygon.style.fillOpacity).toBe(0.25); + ann.state(geo.annotation.state.create); + features = ann.features(); + expect(features.length).toBe(0); + }); + it('actions', function () { + var ann = geo.annotation.rectangleAnnotation({corners: corners}); + var actions = ann.actions(); + expect(actions.length).toBe(0); + actions = ann.actions(geo.annotation.state.create); + expect(actions.length).toBe(1); + expect(actions[0].name).toEqual('rectangle create'); + ann.state(geo.annotation.state.create); + actions = ann.actions(); + expect(actions.length).toBe(1); + expect(actions[0].name).toEqual('rectangle create'); + actions = ann.actions(geo.annotation.state.done); + expect(actions.length).toBe(0); + }); + it('processAction', function () { + var map = create_map(); + var layer = map.createLayer('annotation', { + annotations: ['rectangle'] + }); + var ann = geo.annotation.rectangleAnnotation({layer: layer, corners: corners}); + expect(ann.processAction({state: null})).toBe(undefined); + ann.state(geo.annotation.state.create); + var evt = { + state: {action: geo.geo_action.annotation_rectangle}, + lowerLeft: {x: 10, y: 65}, + upperRight: {x: 90, y: 5} + }; + expect(ann.processAction(evt)).toBe('done'); + expect(ann.state()).toBe(geo.annotation.state.done); + var coor = ann.coordinates(); + expect(coor[0].x).toBeCloseTo(-27.246); + expect(coor[0].y).toBeCloseTo(10.055); + expect(coor[2].x).toBeCloseTo(-20.215); + expect(coor[2].y).toBeCloseTo(15.199); }); it('_coordinates', function () { var ann = geo.annotation.rectangleAnnotation({corners: corners}); diff --git a/tests/cases/annotationLayer.js b/tests/cases/annotationLayer.js index d49ded4434..7f70f2e969 100644 --- a/tests/cases/annotationLayer.js +++ b/tests/cases/annotationLayer.js @@ -75,13 +75,13 @@ describe('geo.annotationLayer', function () { expect(layer.mode()).toBe('polygon'); expect(layer.annotations().length).toBe(1); expect(layer.annotations()[0].id()).not.toBe(id); - expect(map.interactor().hasAction(undefined, undefined, 'annotationLayer')).toBeNull(); + expect(map.interactor().hasAction(undefined, undefined, geo.annotation.actionOwner)).toBeNull(); expect(layer.mode('rectangle')).toBe(layer); expect(layer.mode()).toBe('rectangle'); - expect(map.interactor().hasAction(undefined, undefined, 'annotationLayer')).not.toBeNull(); + expect(map.interactor().hasAction(undefined, undefined, geo.annotation.actionOwner)).not.toBeNull(); expect(layer.mode(null)).toBe(layer); expect(layer.mode()).toBe(null); - expect(map.interactor().hasAction(undefined, undefined, 'annotationLayer')).toBeNull(); + expect(map.interactor().hasAction(undefined, undefined, geo.annotation.actionOwner)).toBeNull(); }); it('annotations', function () { var poly = geo.annotation.polygonAnnotation({ @@ -380,8 +380,12 @@ describe('geo.annotationLayer', function () { }); expect(layer.annotations().length).toBe(0); layer.mode('rectangle'); + expect(layer.annotations()[0].state()).toBe(geo.annotation.state.create); layer._processSelection({ - state: {action: geo.geo_action.annotation_rectangle}, + state: { + action: geo.geo_action.annotation_rectangle, + actionRecord: {owner: geo.annotation.actionOwner} + }, lowerLeft: {x: 10, y: 10}, lowerRight: {x: 20, y: 10}, upperLeft: {x: 10, y: 5}, @@ -389,6 +393,7 @@ describe('geo.annotationLayer', function () { }); expect(layer.annotations().length).toBe(1); expect(layer.annotations()[0].type()).toBe('rectangle'); + expect(layer.annotations()[0].state()).toBe(geo.annotation.state.done); }); it('_geojsonFeatureToAnnotation', function () { map.deleteLayer(layer);