From 94f4179030ed1dd68a8a83cfa16da592cdadffbe Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 27 Mar 2017 10:13:09 -0400 Subject: [PATCH] Added line annotations. Rectangle annotations can be created by clicking opposite points rather than dragging the outline. This allows them to be created on a touch device. Added style and editstyle convenience functions to annotations -- rather than have to ask for annotation.options('style'), you can do annotation.style(). Fix an issue with the touch handler that produced an offset to the touch event. Fix an issue in the line feature that didn't recompute the search data when just styles were changed. Fix an issue with the lines demo where the tooltip wouldn't always show. --- examples/annotations/example.json | 2 +- examples/annotations/index.jade | 30 +- examples/annotations/main.css | 1 + examples/lines/main.js | 8 +- src/action.js | 1 + src/annotation.js | 466 +++++++++++++++++++++++++++++- src/annotationLayer.js | 47 ++- src/lineFeature.js | 3 +- src/mapInteractor.js | 5 +- tests/cases/annotation.js | 274 ++++++++++++++++++ tests/cases/annotationLayer.js | 67 ++++- 11 files changed, 871 insertions(+), 33 deletions(-) diff --git a/examples/annotations/example.json b/examples/annotations/example.json index 11d603ae4b..c8ee61f208 100644 --- a/examples/annotations/example.json +++ b/examples/annotations/example.json @@ -4,6 +4,6 @@ "exampleCss": ["main.css"], "exampleJs": ["main.js"], "about": { - "text": "This example shows how to add annotations, such as marked rectangles, to a map. Left click to add a polygon, right click to add a rectangle." + "text": "This example shows how to add annotations, such as marked rectangles, to a map." } } diff --git a/examples/annotations/index.jade b/examples/annotations/index.jade index 5e8a672e19..83fee41329 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.jade @@ -6,13 +6,15 @@ block append mainContent .shortlabel Add button#rectangle.lastused(next='polygon') Rectangle button#polygon(next='point') Polygon - button#point(next='rectangle') Point + button#point(next='line') Point + button#line(next='rectangle') Line .form-group #instructions(annotation='none') .annotation.none .annotation.polygon Left-click points in the polygon. Double click, right click, or click the starting point to close the polygon. - .annotation.rectangle Left click-and-drag to draw a rectangle. + .annotation.rectangle Left click-and-drag or left click opposite corners to draw a rectangle. .annotation.point Left click to create a point. + .annotation.line Left-click points in the line. Double click, right click, or click the starting point to end the line. .form-group(title='If enabled, left-click to add another annotation, and right-click to switch annotation type. Otherwise, you must click a button above.') label(for='clickadd') Click to add annotation input#clickadd(param-name='clickadd', type='checkbox', placeholder='true', checked='checked') @@ -64,15 +66,33 @@ block append mainContent select#edit-stroke(option='stroke', format='boolean') option(value='true') Yes option(value='false') No - .form-group(annotation-types='point polygon rectangle') + .form-group(annotation-types='point polygon rectangle line') label(for='edit-strokeWidth') Stroke Width input#edit-strokeWidth(option='strokeWidth', format='positive') - .form-group(annotation-types='point polygon rectangle') + .form-group(annotation-types='point polygon rectangle line') label(for='edit-strokeColor') Stroke Color input#edit-strokeColor(option='strokeColor', format='color') - .form-group(annotation-types='point polygon rectangle') + .form-group(annotation-types='point polygon rectangle line') label(for='edit-strokeOpacity') Stroke Opacity input#edit-strokeOpacity(option='strokeOpacity', format='opacity') + .form-group(annotation-types='line') + label(for='edit-closed') Closed + select#edit-closed(option='closed', format='boolean') + option(value='true') Yes + option(value='false') No + .form-group(annotation-types='line') + label(for='edit-lineCap') Line End Caps + select#edit-lineCap(option='lineCap', format='text') + option(value='butt') Butt + option(value='round') Round + option(value='square') Square + .form-group(annotation-types='line') + label(for='edit-lineJoin') Line Joins + select#edit-lineJoin(option='lineJoin', format='text') + option(value='miter') Miter + option(value='bevel') Bevel + option(value='round') Round + option(value='miter-clip') Miter-Clip .form-group #edit-validation-error .modal-footer diff --git a/examples/annotations/main.css b/examples/annotations/main.css index c4c93a6201..5beccb9fc4 100644 --- a/examples/annotations/main.css +++ b/examples/annotations/main.css @@ -54,6 +54,7 @@ #instructions[annotation="polygon"] .annotation.polygon, #instructions[annotation="rectangle"] .annotation.rectangle, #instructions[annotation="point"] .annotation.point, +#instructions[annotation="line"] .annotation.line, #instructions[annotation="none"] .annotation.none { display: block; } diff --git a/examples/lines/main.js b/examples/lines/main.js index 8be77cdae0..eddcde5c4b 100644 --- a/examples/lines/main.js +++ b/examples/lines/main.js @@ -347,8 +347,10 @@ $(function () { .position(function (d) { return {x: d[0], y: d[1]}; }) - // add hover events - .geoOn(geo.event.feature.mouseover, function (evt) { + // add hover events -- use mouseon and mouseoff, since we only show one + // tootip. If we showed one tooltip per item we were over, use mouseover + // and mouseout. + .geoOn(geo.event.feature.mouseon, function (evt) { var text = (evt.data.name ? evt.data.name : '') + (evt.data.highway ? ' (' + evt.data.highway + ')' : ''); if (text) { @@ -357,7 +359,7 @@ $(function () { } tooltipElem.toggleClass('hidden', !text); }) - .geoOn(geo.event.feature.mouseout, function (evt) { + .geoOn(geo.event.feature.mouseoff, function (evt) { tooltipElem.addClass('hidden'); }); diff --git a/src/action.js b/src/action.js index af8b34efc8..a8435e6bbe 100644 --- a/src/action.js +++ b/src/action.js @@ -15,6 +15,7 @@ var geo_action = { zoomselect: 'geo_action_zoomselect', // annotation actions + annotation_line: 'geo_annotation_line', annotation_polygon: 'geo_annotation_polygon', annotation_rectangle: 'geo_annotation_rectangle' }; diff --git a/src/annotation.js b/src/annotation.js index 0b86097165..62bb8078cc 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -183,6 +183,60 @@ var annotation = function (type, args) { return this; }; + /** + * Set or get style. + * + * @param {string|object} arg1 if undefined, return the options.style object. + * If a string, either set or return the style of that name. If an + * object, update the style with the object's values. + * @param {object} arg2 if arg1 is a string and this is defined, set the + * style to this value. + * @returns {object|this} if styles are set, return the layer, otherwise + * return the requested style or the set of styles. + */ + this.style = function (arg1, arg2) { + if (arg1 === undefined) { + return m_options.style; + } + if (typeof arg1 === 'string' && arg2 === undefined) { + return m_options.style[arg1]; + } + if (arg2 === undefined) { + m_options.style = $.extend(true, m_options.style, arg1); + } else { + m_options.style[arg1] = arg2; + } + this.modified(); + return this; + }; + + /** + * Set or get edit style. + * + * @param {string|object} arg1 if undefined, return the options.editstyle + * object. If a string, either set or return the style of that name. If + * an object, update the style with the object's values. + * @param {object} arg2 if arg1 is a string and this is defined, set the + * style to this value. + * @returns {object|this} if styles are set, return the layer, otherwise + * return the requested style or the set of styles. + */ + this.editstyle = function (arg1, arg2) { + if (arg1 === undefined) { + return m_options.editstyle; + } + if (typeof arg1 === 'string' && arg2 === undefined) { + return m_options.editstyle[arg1]; + } + if (arg2 === undefined) { + m_options.editstyle = $.extend(true, m_options.editstyle, arg1); + } else { + m_options.editstyle[arg1] = arg2; + } + this.modified(); + return this; + }; + /** * Get the type of this annotation. * @@ -287,8 +341,10 @@ var annotation = function (type, args) { * @return {array} a list of style names to store. */ this._geojsonStyles = function () { - return ['fill', 'fillColor', 'fillOpacity', 'stroke', 'strokeColor', - 'strokeOpacity', 'strokeWidth']; + return [ + 'closed', 'fill', 'fillColor', 'fillOpacity', 'lineCap', 'lineJoin', + 'radius', 'stroke', 'strokeColor', 'strokeOffset', 'strokeOpacity', + 'strokeWidth']; }; /** @@ -387,6 +443,7 @@ var rectangleAnnotation = function (args) { if (!(this instanceof rectangleAnnotation)) { return new rectangleAnnotation(args); } + args = $.extend(true, {}, { style: { fill: true, @@ -398,9 +455,20 @@ var rectangleAnnotation = function (args) { strokeOpacity: 1, strokeWidth: 3, uniformPolygon: true + }, + editstyle: { + fill: true, + fillColor: {r: 0.3, g: 0.3, b: 0.3}, + fillOpacity: 0.25, + polygon: function (d) { return d.polygon; }, + stroke: true, + strokeColor: {r: 0, g: 0, b: 1}, + strokeOpacity: 1, + strokeWidth: 3, + uniformPolygon: true } }, args || {}); - args.corners = args.corners || args.coordinates; + args.corners = args.corners || args.coordinates || []; delete args.coordinates; annotation.call(this, 'rectangle', args); @@ -468,6 +536,14 @@ var rectangleAnnotation = function (args) { switch (state) { case annotationState.create: features = []; + if (opt.corners && opt.corners.length >= 4) { + features = [{ + polygon: { + polygon: opt.corners, + style: opt.editstyle + } + }]; + } break; default: features = [{ @@ -526,6 +602,77 @@ var rectangleAnnotation = function (args) { this._geojsonGeometryType = function () { return 'Polygon'; }; + + /** + * Return a list of styles that should be preserved in a geojson + * representation of the annotation. + * + * @return {array} a list of style names to store. + */ + this._geojsonStyles = function () { + return [ + 'fill', 'fillColor', 'fillOpacity', 'lineCap', 'lineJoin', 'stroke', + 'strokeColor', 'strokeOffset', 'strokeOpacity', 'strokeWidth']; + }; + + /** + * Handle a mouse move on this annotation. + * + * @param {geo.event} evt the mouse move event. + * @returns {boolean|string} true to update the annotation, falsy to not + * update anything. + */ + this.mouseMove = function (evt) { + if (this.state() !== annotationState.create) { + return; + } + var corners = this.options('corners'); + if (corners.length) { + corners[2] = $.extend({}, evt.mapgcs); + corners[1].x = evt.mapgcs.x; + corners[3].y = evt.mapgcs.y; + return true; + } + }; + + /** + * Handle a mouse click on this annotation. If the event is processed, + * evt.handled should be set to true to prevent further processing. + * + * @param {geo.event} evt the mouse click 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.mouseClick = function (evt) { + var layer = this.layer(); + if (this.state() !== annotationState.create || !layer) { + return; + } + if (!evt.buttonsDown.left && !evt.buttonsDown.right) { + return; + } + var corners = this.options('corners'); + if (evt.buttonsDown.right && !corners.length) { + return; + } + evt.handled = true; + if (corners.length) { + corners[2] = $.extend({}, evt.mapgcs); + corners[1].x = evt.mapgcs.x; + corners[3].y = evt.mapgcs.y; + this.state(annotationState.done); + return 'done'; + } + if (evt.buttonsDown.left) { + corners.push($.extend({}, evt.mapgcs)); + corners.push($.extend({}, evt.mapgcs)); + corners.push($.extend({}, evt.mapgcs)); + corners.push($.extend({}, evt.mapgcs)); + return true; + } + }; + }; inherit(rectangleAnnotation, annotation); @@ -760,6 +907,18 @@ var polygonAnnotation = function (args) { this._geojsonGeometryType = function () { return 'Polygon'; }; + + /** + * Return a list of styles that should be preserved in a geojson + * representation of the annotation. + * + * @return {array} a list of style names to store. + */ + this._geojsonStyles = function () { + return [ + 'fill', 'fillColor', 'fillOpacity', 'lineCap', 'lineJoin', 'stroke', + 'strokeColor', 'strokeOffset', 'strokeOpacity', 'strokeWidth']; + }; }; inherit(polygonAnnotation, annotation); @@ -768,6 +927,301 @@ polygonRequiredFeatures[polygonFeature.capabilities.feature] = true; polygonRequiredFeatures[lineFeature.capabilities.basic] = [annotationState.create]; registerAnnotation('polygon', polygonAnnotation, polygonRequiredFeatures); +///////////////////////////////////////////////////////////////////////////// +/** + * Line annotation class + * + * Must specify: + * vertices: a list of vertices {x: x, y: y} in map gcs coordinates. + * May specify: + * style. + * strokeWidth, strokeColor, strokeOpacity, strokeOffset, closed, lineCap, + * lineJoin + * editstyle. + * strokeWidth, strokeColor, strokeOpacity, strokeOffset, closed, lineCap, + * lineJoin + */ +///////////////////////////////////////////////////////////////////////////// +var lineAnnotation = function (args) { + 'use strict'; + if (!(this instanceof lineAnnotation)) { + return new lineAnnotation(args); + } + + var m_this = this; + + args = $.extend(true, {}, { + style: { + line: function (d) { + /* 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( + function () { return d; }); + }, + position: function (d, i) { + return m_this.options('vertices')[i]; + }, + strokeColor: {r: 0, g: 0, b: 0}, + strokeOpacity: 1, + strokeWidth: 3, + closed: false, + lineCap: 'butt', + lineJoin: 'miter' + }, + editstyle: { + line: function (d) { + /* 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( + function () { return d; }); + }, + position: function (d, i) { + return m_this.options('vertices')[i]; + }, + strokeColor: {r: 0, g: 0, b: 1}, + strokeOpacity: 1, + strokeWidth: 3, + closed: false, + lineCap: 'butt', + lineJoin: 'miter' + } + }, args || {}); + args.vertices = args.vertices || args.coordinates || []; + delete args.coordinates; + annotation.call(this, 'line', args); + + /** + * Get a list of renderable features for this annotation. + * + * @returns {array} an array of features. + */ + this.features = function () { + var opt = this.options(), + state = this.state(), + features; + switch (state) { + case annotationState.create: + features = [{ + line: { + line: opt.vertices, + style: opt.editstyle + } + }]; + break; + default: + features = [{ + line: { + line: opt.vertices, + style: opt.style + } + }]; + break; + } + return features; + }; + + /** + * Get coordinates associated with this annotation in the map gcs coordinate + * system. + * + * @param {array} coordinates: an optional array of coordinates to set. + * @returns {array} an array of coordinates. + */ + this._coordinates = function (coordinates) { + if (coordinates) { + this.options('vertices', coordinates); + } + return this.options('vertices'); + }; + + /** + * Handle a mouse move on this annotation. + * + * @param {geo.event} evt the mouse move event. + * @returns {boolean|string} true to update the annotation, falsy to not + * update anything. + */ + this.mouseMove = function (evt) { + if (this.state() !== annotationState.create) { + return; + } + var vertices = this.options('vertices'); + if (vertices.length) { + vertices[vertices.length - 1] = evt.mapgcs; + return true; + } + }; + + /** + * Handle a mouse click on this annotation. If the event is processed, + * evt.handled should be set to true to prevent further processing. + * + * @param {geo.event} evt the mouse click 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.mouseClick = function (evt) { + var layer = this.layer(); + if (this.state() !== annotationState.create || !layer) { + return; + } + var end = !!evt.buttonsDown.right, skip; + if (!evt.buttonsDown.left && !evt.buttonsDown.right) { + return; + } + var vertices = this.options('vertices'); + if (evt.buttonsDown.right && !vertices.length) { + return; + } + evt.handled = true; + if (evt.buttonsDown.left) { + if (vertices.length) { + if (vertices.length >= 2 && layer.displayDistance( + vertices[vertices.length - 2], null, evt.map, 'display') <= + layer.options('adjacentPointProximity')) { + skip = true; + if (this.lastClick && + evt.time - this.lastClick < layer.options('dblClickTime')) { + end = true; + } + } else if (vertices.length >= 2 && layer.displayDistance( + vertices[0], null, evt.map, 'display') <= + layer.options('finalPointProximity')) { + end = 'close'; + } else { + vertices[vertices.length - 1] = evt.mapgcs; + } + } else { + vertices.push(evt.mapgcs); + } + if (!end && !skip) { + vertices.push(evt.mapgcs); + } + this.lastClick = evt.time; + } + if (end) { + if (vertices.length < 3) { + return 'remove'; + } + vertices.pop(); + this.options('style').closed = end === 'close'; + this.state(annotationState.done); + return 'done'; + } + return (end || !skip); + }; + + /** + * 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_line, + name: 'line create', + owner: annotationActionOwner, + input: 'left', + modifiers: {shift: false, ctrl: false} + }, { + action: geo_action.annotation_line, + name: 'line create', + owner: annotationActionOwner, + input: 'pan' + }]; + 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_line) { + return; + } + var cpp = layer.options('continuousPointProximity'); + if (cpp || cpp === 0) { + var vertices = this.options('vertices'); + if (!vertices.length) { + vertices.push(evt.mouse.mapgcs); + vertices.push(evt.mouse.mapgcs); + return true; + } + var dist = layer.displayDistance( + vertices[vertices.length - 2], null, evt.mouse.map, 'display'); + if (dist && dist > cpp) { + // we should combine nearly colinear points, but we don't + vertices[vertices.length - 1] = evt.mouse.mapgcs; + vertices.push(evt.mouse.mapgcs); + return true; + } + } + }; + + /** + * Return the coordinates to be stored in a geojson geometery object. + * + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @return {array} an array of flattened coordinates in the ingcs coordinate + * system. Undefined if this annotation is incompelte. + */ + this._geojsonCoordinates = function (gcs) { + var src = this.coordinates(gcs); + if (!src || src.length < 2 || this.state() === annotationState.create) { + return; + } + var coor = []; + for (var i = 0; i < src.length; i += 1) { + coor.push([src[i].x, src[i].y]); + } + return coor; + }; + + /** + * Return the geometry type that is used to store this annotation in geojson. + * + * @return {string} a geojson geometry type. + */ + this._geojsonGeometryType = function () { + return 'LineString'; + }; + + /** + * Return a list of styles that should be preserved in a geojson + * representation of the annotation. + * + * @return {array} a list of style names to store. + */ + this._geojsonStyles = function () { + return [ + 'closed', 'lineCap', 'lineJoin', 'strokeColor', 'strokeOffset', + 'strokeOpacity', 'strokeWidth']; + }; +}; +inherit(lineAnnotation, annotation); + +var lineRequiredFeatures = {}; +lineRequiredFeatures[lineFeature.capabilities.basic] = [annotationState.create]; +registerAnnotation('line', lineAnnotation, lineRequiredFeatures); + ///////////////////////////////////////////////////////////////////////////// /** * Point annotation class @@ -899,8 +1353,9 @@ var pointAnnotation = function (args) { * @return {array} a list of style names to store. */ this._geojsonStyles = function () { - return ['fill', 'fillColor', 'fillOpacity', 'radius', 'scaled', 'stroke', - 'strokeColor', 'strokeOpacity', 'strokeWidth']; + return [ + 'fill', 'fillColor', 'fillOpacity', 'radius', 'scaled', 'stroke', + 'strokeColor', 'strokeOpacity', 'strokeWidth']; }; /** @@ -938,6 +1393,7 @@ module.exports = { state: annotationState, actionOwner: annotationActionOwner, annotation: annotation, + lineAnnotation: lineAnnotation, pointAnnotation: pointAnnotation, polygonAnnotation: polygonAnnotation, rectangleAnnotation: rectangleAnnotation diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 9f4558fbdf..8ca08f1253 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -51,13 +51,17 @@ var annotationLayer = function (args) { m_features = []; var geojsonStyleProperties = { + 'closed': {dataType: 'boolean', keys: ['closed', 'close']}, 'fill': {dataType: 'boolean', keys: ['fill']}, 'fillColor': {dataType: 'color', keys: ['fillColor', 'fill-color', 'marker-color', 'fill']}, 'fillOpacity': {dataType: 'opacity', keys: ['fillOpacity', 'fill-opacity']}, + 'lineCap': {dataType: 'text', keys: ['lineCap', 'line-cap']}, + 'lineJoin': {dataType: 'text', keys: ['lineJoin', 'line-join']}, 'radius': {dataType: 'positive', keys: ['radius']}, 'scaled': {dataType: 'booleanOrNumber', keys: ['scaled']}, 'stroke': {dataType: 'boolean', keys: ['stroke']}, 'strokeColor': {dataType: 'color', keys: ['strokeColor', 'stroke-color', 'stroke']}, + 'strokeOffset': {dataType: 'number', keys: ['strokeOffset', 'stroke-offset']}, 'strokeOpacity': {dataType: 'opacity', keys: ['strokeOpacity', 'stroke-opacity']}, 'strokeWidth': {dataType: 'positive', keys: ['strokeWidth', 'stroke-width']} }; @@ -65,16 +69,19 @@ var annotationLayer = function (args) { m_options = $.extend(true, {}, { dblClickTime: 300, adjacentPointProximity: 5, // in pixels, 0 is exact + // in pixels; set to continuousPointProximity to false to disable + // continuous drawing modes. + continuousPointProximity: 5, finalPointProximity: 10 // in pixels, 0 is exact }, args); /** - * Process a selection event. If we are in rectangle-creation mode, this + * Process an action event. If we are in rectangle-creation mode, this * creates a rectangle. * * @param {geo.event} evt the selection event. */ - this._processSelection = function (evt) { + this._processAction = function (evt) { var update; if (evt.state && evt.state.actionRecord && evt.state.actionRecord.owner === geo_annotation.actionOwner && @@ -341,6 +348,9 @@ var annotationLayer = function (args) { this.currentAnnotation = null; } switch (m_mode) { + case 'line': + createAnnotation = geo_annotation.lineAnnotation; + break; case 'point': createAnnotation = geo_annotation.pointAnnotation; break; @@ -464,7 +474,14 @@ var annotationLayer = function (args) { options.style = {}; } delete options.annotationType; + // the geoJSON reader can only emit line, polygon, and point switch (feature.featureType) { + case 'line': + position = feature.line()(data, data_idx); + if (!position || position.length < 2) { + return; + } + break; case 'polygon': position = feature.polygon()(data, data_idx); if (!position || !position.outer || position.outer.length < 3) { @@ -482,8 +499,6 @@ var annotationLayer = function (args) { case 'point': position = [feature.position()(data, data_idx)]; break; - default: - return; } for (i = 0; i < position.length; i += 1) { position[i] = util.normalizeCoordinates(position[i]); @@ -558,6 +573,8 @@ var annotationLayer = function (args) { * 'true', 'on', or 'yes', falsy values that aren't 0, and true are * handled as booleans. Otherwise, a floating point number that isn't * NaN or an infinity. + * number: a floating point number that isn't NaN or an infinity. + * text: any text string. * @param {number|string|object|boolean} value: the value to validate. * @param {string} dataType: the data type for validation. * @returns {number|string|object|boolean|undefined} the sanitized value or @@ -587,6 +604,12 @@ var annotationLayer = function (args) { return; } break; + case 'number': + value = +value; + if (isNaN(value) || !isFinite(value)) { + return; + } + break; case 'opacity': value = +value; if (isNaN(value) || value < 0 || value > 1) { @@ -599,6 +622,9 @@ var annotationLayer = function (args) { return; } break; + case 'text': + value = '' + value; + break; } return value; }; @@ -652,10 +678,12 @@ var annotationLayer = function (args) { * hasn't been tested. */ var style = {}; - $.each(['fill', 'fillColor', 'fillOpacity', 'line', 'polygon', - 'position', 'radius', 'stroke', 'strokeColor', - 'strokeOpacity', 'strokeWidth', 'uniformPolygon' - ], function (keyidx, key) { + $.each([ + 'closed', 'fill', 'fillColor', 'fillOpacity', 'line', + 'lineCap', 'lineJoin', 'polygon', 'position', 'radius', + 'stroke', 'strokeColor', 'strokeOffset', 'strokeOpacity', + 'strokeWidth', 'uniformPolygon' + ], function (keyidx, key) { var origFunc; if (feature.style()[key] !== undefined) { origFunc = feature.style.get(key); @@ -725,7 +753,8 @@ var annotationLayer = function (args) { if (!m_this.map().interactor()) { m_this.map().interactor(mapInteractor({actions: []})); } - m_this.geoOn(geo_event.actionselection, m_this._processSelection); + m_this.geoOn(geo_event.actionselection, m_this._processAction); + m_this.geoOn(geo_event.actionmove, m_this._processAction); m_this.geoOn(geo_event.mouseclick, m_this._handleMouseClick); m_this.geoOn(geo_event.mousemove, m_this._handleMouseMove); diff --git a/src/lineFeature.js b/src/lineFeature.js index 57513bab13..62e3236b64 100644 --- a/src/lineFeature.js +++ b/src/lineFeature.js @@ -115,7 +115,8 @@ var lineFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._updatePointSearchInfo = function () { - if (m_pointSearchTime.getMTime() >= m_this.dataTime().getMTime()) { + if (m_pointSearchTime.getMTime() >= m_this.dataTime().getMTime() && + m_pointSearchTime.getMTime() >= m_this.getMTime()) { return; } m_pointSearchTime.modified(); diff --git a/src/mapInteractor.js b/src/mapInteractor.js index 22c6ce8b01..61320c9e82 100644 --- a/src/mapInteractor.js +++ b/src/mapInteractor.js @@ -652,10 +652,9 @@ var mapInteractor = function (args) { m_touchHandler.lastEventType = evt.which; m_touchHandler.lastEvent = evt; /* convert touch events to have page locations */ - var offset = $node.offset(); if (evt.pageX === undefined && evt.center !== undefined && evt.center.x !== undefined) { - evt.pageX = evt.center.x + offset.left; - evt.pageY = evt.center.y + offset.top; + evt.pageX = evt.center.x; + evt.pageY = evt.center.y; } /* start events should occur *before* the triggering delta. By using the * mouse handlers, we get all of the action properties we expect (and diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index 252343d0fb..6e6b9065f9 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -120,6 +120,24 @@ describe('geo.annotation', function () { expect(ann.options().coordinates).toBe(undefined); expect(ann._coordinates()).toBe(testval); }); + it('style and editstyle', function () { + var ann = geo.annotation.annotation('test', { + layer: layer, style: {testopt: 30}, editstyle: {testopt: 50}}); + expect(ann.options('style').testopt).toBe(30); + expect(ann.style().testopt).toBe(30); + expect(ann.style('testopt')).toBe(30); + expect(ann.style('testopt', 40)).toBe(ann); + expect(ann.style().testopt).toBe(40); + expect(ann.style({testopt: 30})).toBe(ann); + expect(ann.style().testopt).toBe(30); + expect(ann.options('editstyle').testopt).toBe(50); + expect(ann.editstyle().testopt).toBe(50); + expect(ann.editstyle('testopt')).toBe(50); + expect(ann.editstyle('testopt', 60)).toBe(ann); + expect(ann.editstyle().testopt).toBe(60); + expect(ann.editstyle({testopt: 50})).toBe(ann); + expect(ann.editstyle().testopt).toBe(50); + }); it('coordinates', function () { var ann = geo.annotation.annotation('test', {layer: layer}); var coord = [{x: 10, y: 30}, {x: 20, y: 25}]; @@ -195,8 +213,17 @@ 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); + expect(features[0].polygon.style.fillColor.g).toBe(1); + expect(features[0].polygon.style.polygon({polygon: 'a'})).toBe('a'); ann.state(geo.annotation.state.create); features = ann.features(); + expect(features.length).toBe(1); + expect(features[0].polygon.polygon).toEqual(corners); + expect(features[0].polygon.style.fillOpacity).toBe(0.25); + expect(features[0].polygon.style.fillColor.g).toBe(0.3); + expect(features[0].polygon.style.polygon({polygon: 'a'})).toBe('a'); + ann.options('corners', []); + features = ann.features(); expect(features.length).toBe(0); }); it('actions', function () { @@ -260,6 +287,57 @@ describe('geo.annotation', function () { expect(geojson.geometry.coordinates[0].length).toBe(5); expect(geojson.geometry.coordinates[0][2][1]).toBeCloseTo(1); }); + it('mouseMove', function () { + var ann = geo.annotation.rectangleAnnotation({corners: corners}); + expect(ann.mouseMove({mapgcs: {x: 6, y: 4}})).toBe(undefined); + expect(ann.options('corners')).toEqual(corners); + ann.state(geo.annotation.state.create); + expect(ann.mouseMove({mapgcs: {x: 6, y: 4}})).toBe(true); + expect(ann.options('corners')).not.toEqual(corners); + }); + it('mouseClick', function () { + var map = create_map(); + var layer = map.createLayer('annotation', { + annotations: ['rectangle'] + }); + var ann = geo.annotation.rectangleAnnotation({layer: layer}); + var time = new Date().getTime(); + 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: corners[0], + mapgcs: map.displayToGcs(corners[0], null) + })).toBe(undefined); + expect(ann.mouseClick({ + buttonsDown: {right: true}, + time: time, + map: corners[0], + mapgcs: map.displayToGcs(corners[0], null) + })).toBe(undefined); + expect(ann.options('corners').length).toBe(0); + expect(ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: corners[0], + mapgcs: map.displayToGcs(corners[0], null) + })).toBe(true); + expect(ann.options('corners').length).toBe(4); + ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: corners[2], + mapgcs: map.displayToGcs(corners[2], null) + }); + expect(ann.options('corners').length).toBe(4); + expect(ann.state()).toBe(geo.annotation.state.done); + }); }); describe('geo.annotation.polygonAnnotation', function () { @@ -489,6 +567,201 @@ describe('geo.annotation', function () { }); }); + describe('geo.annotation.lineAnnotation', function () { + var vertices = [{x: 30, y: 0}, {x: 50, y: 0}, {x: 40, y: 20}, {x: 30, y: 10}]; + var vertices2 = [{x: 30, y: 10}, {x: 50, y: 10}, {x: 40, y: 30}]; + it('create', function () { + var ann = geo.annotation.lineAnnotation(); + expect(ann instanceof geo.annotation.lineAnnotation); + expect(ann.type()).toBe('line'); + }); + it('features', function () { + var ann = geo.annotation.lineAnnotation({vertices: vertices}); + var features = ann.features(); + expect(features.length).toBe(1); + expect(features[0].line.line).toEqual(vertices); + expect(features[0].line.style.strokeOpacity).toBe(1); + expect(features[0].line.style.strokeColor.b).toBe(0); + expect(features[0].line.style.line().length).toBe(vertices.length); + expect(features[0].line.style.position(0, 1)).toEqual(vertices[1]); + ann.state(geo.annotation.state.create); + features = ann.features(); + expect(features.length).toBe(1); + expect(features[0].line.line).toEqual(vertices); + expect(features[0].line.style.strokeOpacity).toBe(1); + expect(features[0].line.style.strokeColor.b).toBe(1); + expect(features[0].line.style.line().length).toBe(vertices.length); + expect(features[0].line.style.position(0, 1)).toEqual(vertices[1]); + ann.options('vertices', [{x: 3, y: 0}, {x: 5, y: 0}]); + features = ann.features(); + expect(features.length).toBe(1); + expect(features[0].line.line.length).toBe(2); + }); + it('_coordinates', function () { + var ann = geo.annotation.lineAnnotation({vertices: vertices}); + expect(ann._coordinates()).toEqual(vertices); + ann._coordinates(vertices2); + expect(ann._coordinates()).toEqual(vertices2); + }); + it('_geojsonCoordinates', function () { + var ann = geo.annotation.lineAnnotation(); + expect(ann._geojsonCoordinates()).toBe(undefined); + ann._coordinates(vertices2); + var coor = ann._geojsonCoordinates(); + expect(coor.length).toBe(3); + }); + it('_geojsonGeometryType', function () { + var ann = geo.annotation.lineAnnotation(); + expect(ann._geojsonGeometryType()).toBe('LineString'); + }); + it('geojson', function () { + var ann = geo.annotation.lineAnnotation({vertices: vertices2}); + var geojson = ann.geojson(); + expect(geojson.type).toBe('Feature'); + expect(geojson.geometry.type).toBe('LineString'); + expect(geojson.geometry.coordinates.length).toBe(3); + expect(geojson.geometry.coordinates[2][1]).toBeCloseTo(30); + }); + it('mouseMove', function () { + var ann = geo.annotation.lineAnnotation({vertices: vertices}); + expect(ann.mouseMove({mapgcs: {x: 6, y: 4}})).toBe(undefined); + expect(ann.options('vertices')).toEqual(vertices); + ann.state(geo.annotation.state.create); + expect(ann.mouseMove({mapgcs: {x: 6, y: 4}})).toBe(true); + expect(ann.options('vertices')).not.toEqual(vertices); + }); + it('mouseClick', function () { + var map = create_map(); + var layer = map.createLayer('annotation', { + annotations: ['line'] + }); + var ann = geo.annotation.lineAnnotation({layer: layer}); + var time = new Date().getTime(); + 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[0], + mapgcs: map.displayToGcs(vertices[0], null) + })).toBe(undefined); + expect(ann.options('vertices').length).toBe(0); + expect(ann.mouseClick({ + buttonsDown: {right: true}, + time: time, + map: vertices[0], + mapgcs: map.displayToGcs(vertices[0], null) + })).toBe(undefined); + expect(ann.options('vertices').length).toBe(0); + expect(ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: vertices[0], + mapgcs: map.displayToGcs(vertices[0], null) + })).toBe(true); + expect(ann.options('vertices').length).toBe(2); + ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: vertices[1], + mapgcs: map.displayToGcs(vertices[1], null) + }); + expect(ann.options('vertices').length).toBe(3); + ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: vertices[2], + mapgcs: map.displayToGcs(vertices[2], null) + }); + expect(ann.options('vertices').length).toBe(4); + expect(ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: {x: vertices[0].x + 1, y: vertices[0].y}, + mapgcs: map.displayToGcs({x: vertices[0].x + 1, y: vertices[0].y}, null) + })).toBe('done'); + expect(ann.options('vertices').length).toBe(3); + expect(ann.state()).toBe(geo.annotation.state.done); + + // test double click + ann.state(geo.annotation.state.create); + ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: vertices[2], + mapgcs: map.displayToGcs(vertices[2], null) + }); + expect(ann.options('vertices').length).toBe(4); + ann.mouseClick({ + buttonsDown: {left: true}, + time: time, + map: vertices[2], + mapgcs: map.displayToGcs(vertices[2], null) + }); + expect(ann.options('vertices').length).toBe(3); + expect(ann.state()).toBe(geo.annotation.state.done); + + // test right-click with only one fixed vertex + ann.state(geo.annotation.state.create); + ann.options('vertices', [{x: 3, y: 0}, {x: 5, y: 0}]); + expect(ann.mouseClick({ + buttonsDown: {right: true}, + time: time, + map: vertices[0], + mapgcs: map.displayToGcs(vertices[0], null) + })).toBe('remove'); + }); + it('actions', function () { + var ann = geo.annotation.lineAnnotation({vertices: vertices}); + var actions = ann.actions(); + expect(actions.length).toBe(0); + actions = ann.actions(geo.annotation.state.create); + expect(actions.length).toBe(2); + expect(actions[0].name).toEqual('line create'); + ann.state(geo.annotation.state.create); + actions = ann.actions(); + expect(actions.length).toBe(2); + expect(actions[0].name).toEqual('line 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: ['line'] + }); + var ann = geo.annotation.lineAnnotation({layer: layer, vertices: vertices}); + expect(ann.processAction({state: null})).toBe(undefined); + ann.options('vertices', []); + ann.state(geo.annotation.state.create); + var evt = { + state: {action: geo.geo_action.annotation_line}, + mouse: { + map: vertices[0], + mapgcs: map.displayToGcs(vertices[0], null) + } + }; + expect(ann.processAction(evt)).toBe(true); + expect(ann.options('vertices').length).toBe(2); + evt = { + state: {action: geo.geo_action.annotation_line}, + mouse: { + map: vertices[1], + mapgcs: map.displayToGcs(vertices[1], null) + } + }; + expect(ann.processAction(evt)).toBe(true); + expect(ann.options('vertices').length).toBe(3); + expect(ann.processAction(evt)).not.toBe(true); + expect(ann.options('vertices').length).toBe(3); + }); + }); + describe('annotation registry', function () { var newshapeCount = 0; it('listAnnotations', function () { @@ -496,6 +769,7 @@ describe('geo.annotation', function () { expect($.inArray('rectangle', list) >= 0).toBe(true); expect($.inArray('polygon', list) >= 0).toBe(true); expect($.inArray('point', list) >= 0).toBe(true); + expect($.inArray('line', list) >= 0).toBe(true); expect($.inArray('unknown', list) >= 0).toBe(false); }); it('registerAnnotation', function () { diff --git a/tests/cases/annotationLayer.js b/tests/cases/annotationLayer.js index d408820aa9..6ba1cadc57 100644 --- a/tests/cases/annotationLayer.js +++ b/tests/cases/annotationLayer.js @@ -82,6 +82,12 @@ describe('geo.annotationLayer', function () { expect(layer.mode(null)).toBe(layer); expect(layer.mode()).toBe(null); expect(map.interactor().hasAction(undefined, undefined, geo.annotation.actionOwner)).toBeNull(); + expect(layer.mode('line')).toBe(layer); + expect(layer.mode()).toBe('line'); + 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, geo.annotation.actionOwner)).toBeNull(); }); it('annotations', function () { var poly = geo.annotation.polygonAnnotation({ @@ -376,9 +382,9 @@ describe('geo.annotationLayer', function () { layer._handleZoom(); expect(layer.features()[0].getMTime()).toBeGreaterThan(mod); }); - it('_processSelection', function () { + it('_processAction', function () { layer.removeAllAnnotations(); - layer._processSelection({ + layer._processAction({ state: {action: geo.geo_action.annotation_rectangle}, lowerLeft: {x: 10, y: 10}, lowerRight: {x: 20, y: 10}, @@ -388,7 +394,7 @@ 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({ + layer._processAction({ state: { action: geo.geo_action.annotation_rectangle, actionRecord: {owner: geo.annotation.actionOwner} @@ -406,17 +412,35 @@ describe('geo.annotationLayer', function () { map.deleteLayer(layer); layer = map.createLayer('annotation'); /* use the vgl variant */ /* This is tested through the layer.geojson function */ + var unknownFeature = { + type: 'Feature', + properties: {annotationType: 'unknown'}, + geometry: { + type: 'LineString', + coordinates: [[-73.75, 42.84], [-73.79, 42.84]] + } + }; + expect(layer.geojson(unknownFeature)).toBe(0); var lineString = { type: 'Feature', properties: {}, geometry: { type: 'LineString', - coordinates: [[-73.759202, 42.849643], [-73.756799, 42.849572]] + coordinates: [[-73.75, 42.84]] } }; expect(layer.geojson(lineString)).toBe(0); + lineString = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [[-73.75, 42.84], [-73.79, 42.84]] + } + }; + expect(layer.geojson(lineString)).toBe(1); lineString.properties.annotationType = 'polygon'; - expect(layer.geojson(lineString)).toBe(0); + expect(layer.geojson(lineString)).toBe(2); var sample = { type: 'FeatureCollection', features: [{ @@ -469,7 +493,7 @@ describe('geo.annotationLayer', function () { } }] }; - expect(layer.geojson(sample)).toBe(3); + expect(layer.geojson(sample)).toBe(5); var badpoly = { type: 'Feature', geometry: { @@ -522,6 +546,37 @@ describe('geo.annotationLayer', function () { expect(attr.fillOpacity).toBe(0.3); expect(attr.fillColor).toBe('#4b0082'); expect(attr.scaled).toBe(4); + + badattr = { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [[-118, 36], [-119, 36]] + }, + properties: { + strokeOffset: 'not a number', + lineCap: 'any text is allowed' + } + }; + layer.geojson(badattr, true); + attr = layer.geojson().features[0].properties; + expect(attr.strokeOffset).toBe(undefined); + expect(attr.lineCap).toBe('any text is allowed'); + goodattr = { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [[-118, 36], [-119, 36]] + }, + properties: { + strokeOffset: '0.5', + lineCap: 'round' + } + }; + layer.geojson(goodattr, true); + attr = layer.geojson().features[0].properties; + expect(attr.strokeOffset).toBe(0.5); + expect(attr.lineCap).toBe('round'); }); }); it('Test destroy layer.', function () {