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 () {