From 8112e5a9c6074da8de288af7074a7cb0a3c7597e Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 23 Sep 2016 16:35:24 -0400 Subject: [PATCH 1/7] Export annotations in a geojson compatible object. --- examples/annotations/main.js | 9 +- src/annotation.js | 181 ++++++++++++++++++++++++++++++++++- src/annotationLayer.js | 40 ++++++++ src/util/init.js | 15 +++ tests/cases/annotation.js | 2 +- 5 files changed, 235 insertions(+), 12 deletions(-) diff --git a/examples/annotations/main.js b/examples/annotations/main.js index 655d39dde6..250ef588ba 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -266,14 +266,7 @@ $(function () { value = opt.style[key]; switch (format) { case 'color': - value = geo.util.convertColor(value); - if (!value.r && !value.g && !value.b) { - value = '#000000'; - } else { - value = '#' + ((1 << 24) + (Math.round(value.r * 255) << 16) + - (Math.round(value.g * 255) << 8) + - Math.round(value.b * 255)).toString(16).slice(1); - } + value = geo.util.convertColorToHex(value); break; } $('[option]', ctl).val('' + value); diff --git a/src/annotation.js b/src/annotation.js index c00d75dc04..ca236308f1 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -2,6 +2,7 @@ var $ = require('jquery'); var inherit = require('./inherit'); var geo_event = require('./event'); var transform = require('./transform'); +var util = require('./util'); var registerAnnotation = require('./registry').registerAnnotation; var annotationId = 0; @@ -241,10 +242,88 @@ var annotation = function (type, args) { }; /** - * TODO: return the annotation as a geojson object + * 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.geojson = function () { - return 'not implemented'; + this._geojsonStyles = function () { + return ['fill', 'fillColor', 'fillOpacity', 'stroke', 'strokeColor', + 'strokeOpacity', 'strokeWidth']; + }; + + /** + * 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) { + }; + + /** + * Return the geometry type that is used to store this annotation in geojson. + * + * @return {string} a geojson geometry type. + */ + this._geojsonGeometryType = function () { + }; + + /** + * Return the annotation as a geojson object. + * + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @param {boolean} includeCrs: if true, include the coordinate system. + * @return {object} the annotation as a geojson object, or undefined if it + * should not be represented (for instance, while it is being created). + */ + this.geojson = function (gcs, includeCrs) { + var coor = this._geojsonCoordinates(gcs), + geotype = this._geojsonGeometryType(), + styles = this._geojsonStyles(), + objStyle = this.options('style'), + i, key, value; + if (!coor || !coor.length || !geotype) { + return; + } + var obj = { + type: 'Feature', + geometry: { + type: geotype, + coordinates: coor + }, + properties: { + annotationType: m_type, + name: this.name(), + style: {} + } + }; + for (i = 0; i < styles.length; i += 1) { + key = styles[i]; + value = util.ensureFunction(objStyle[key])(); + if (value !== undefined) { + if (key.toLowerCase().match(/color$/)) { + value = util.convertColorToHex(value); + } + obj.properties.style[key] = value; + } + } + if (includeCrs) { + var map = this.layer().map(); + gcs = (gcs === null ? map.gcs() : ( + gcs === undefined ? map.ingcs() : gcs)); + obj.crs = { + type: 'name', + properties: { + type: 'proj4', + name: gcs + } + }; + } + return obj; }; }; @@ -308,6 +387,36 @@ var rectangleAnnotation = function (args) { this._coordinates = function () { return this.options('corners'); }; + + /** + * 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.length < 4) { + return; + } + var coor = []; + for (var i = 0; i < 4; i += 1) { + coor.push([src[i].x, src[i].y]); + } + coor.push([src[0].x, src[0].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 'Polygon'; + }; }; inherit(rectangleAnnotation, annotation); @@ -505,6 +614,36 @@ var polygonAnnotation = function (args) { } return (end || !skip); }; + + /** + * 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.length < 3 || this.state() === annotationState.create) { + return; + } + var coor = []; + for (var i = 0; i < src.length; i += 1) { + coor.push([src[i].x, src[i].y]); + } + coor.push([src[0].x, src[0].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 'Polygon'; + }; }; inherit(polygonAnnotation, annotation); @@ -602,6 +741,42 @@ var pointAnnotation = function (args) { this.state(annotationState.done); return 'done'; }; + + /** + * 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', 'radius', 'stroke', + 'strokeColor', 'strokeOpacity', 'strokeWidth']; + }; + + /** + * 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 (this.state() === annotationState.create || src.length < 1) { + return; + } + return [src[0].x, src[0].y]; + }; + + /** + * Return the geometry type that is used to store this annotation in geojson. + * + * @return {string} a geojson geometry type. + */ + this._geojsonGeometryType = function () { + return 'Point'; + }; }; inherit(pointAnnotation, annotation); diff --git a/src/annotationLayer.js b/src/annotationLayer.js index aabcc7fadf..40897ea807 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -351,6 +351,46 @@ var annotationLayer = function (args) { return this; }; + /** + * Return the current set of annotations as a geojson object. Alternately, + * add a set of annotations from a geojson object. + * + * @param {object} geojson: if present, add annotations based on the given + * geojson object. If undefined, return the current annotations as + * geojson. This may bei either a JSON string or a javascript object. + * @param {boolean} clear: if true, when adding objects, first remove all + * existing objects. + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @param {boolean} includeCrs: if true, include the coordinate system in the + * output. + * @return {object} the current annotations as a javascript object that + * can be converted to geojson using JSON.stringify. + */ + this.geojson = function (geojson, clear, gcs, includeCrs) { + if (geojson !== undefined) { + if (clear) { + this.removeAllAnnotations(); + } + //DWM:: + } + geojson = null; + var features = []; + $.each(m_annotations, function (annotation_idx, annotation) { + var obj = annotation.geojson(gcs, includeCrs); + if (obj) { + features.push(obj); + } + }); + if (features.length) { + geojson = { + type: 'FeatureCollection', + features: features + }; + } + return geojson; + }; + /////////////////////////////////////////////////////////////////////////// /** * Update layer diff --git a/src/util/init.js b/src/util/init.js index 2cfd1b9591..32150e03cf 100644 --- a/src/util/init.js +++ b/src/util/init.js @@ -140,6 +140,21 @@ return color; }, + /** + * Convert a color to a six digit hex value prefixed with #. + */ + convertColorToHex: function (color) { + var value = geo.util.convertColor(color); + if (!value.r && !value.g && !value.b) { + value = '#000000'; + } else { + value = '#' + ((1 << 24) + (Math.round(value.r * 255) << 16) + + (Math.round(value.g * 255) << 8) + + Math.round(value.b * 255)).toString(16).slice(1); + } + return value; + }, + /** * Normalize a coordinate object into {x: ..., y: ..., z: ... } form. * Accepts 2-3d arrays, diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index 496c1395f8..d2819faf23 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -41,7 +41,7 @@ describe('geo.annotation', function () { expect(ann.mouseClick()).toBe(undefined); expect(ann.mouseMove()).toBe(undefined); expect(ann._coordinates()).toEqual([]); - expect(ann.geojson()).toBe('not implemented'); + expect(ann.geojson()).toBe(undefined); map = create_map(); layer = map.createLayer('annotation', { annotations: geo.listAnnotations() From 05347333d1042059da8f75e5911bb40d3d92426f Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 26 Sep 2016 11:04:14 -0400 Subject: [PATCH 2/7] Import geojson into annotations, where possible. The order is not guaranteed to be the same as the exported order; points will end up before rectangles and polygons due to grouping by the geojson reader. --- examples/annotations/main.js | 3 + src/annotation.js | 42 ++++++++-- src/annotationLayer.js | 155 +++++++++++++++++++++++++++++++++-- src/event.js | 10 +++ src/jsonReader.js | 2 +- src/lineFeature.js | 2 + src/pointFeature.js | 2 + src/polygonFeature.js | 2 + src/registry.js | 19 +++++ 9 files changed, 224 insertions(+), 13 deletions(-) diff --git a/examples/annotations/main.js b/examples/annotations/main.js index 250ef588ba..a0bfb32496 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -52,6 +52,7 @@ $(function () { layer.geoOn(geo.event.mouseclick, mouseClickToStart); layer.geoOn(geo.event.annotation.mode, handleModeChange); layer.geoOn(geo.event.annotation.add, handleAnnotationChange); + layer.geoOn(geo.event.annotation.update, handleAnnotationChange); layer.geoOn(geo.event.annotation.remove, handleAnnotationChange); layer.geoOn(geo.event.annotation.state, handleAnnotationChange); @@ -261,8 +262,10 @@ $(function () { format = $('[option]', ctl).attr('format'), value; if (!ctl.attr('annotation-types').match(typeMatch)) { + ctl.hide(); return; } + ctl.show(); value = opt.style[key]; switch (format) { case 'color': diff --git a/src/annotation.js b/src/annotation.js index ca236308f1..3f647a072d 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -141,6 +141,16 @@ var annotation = function (type, args) { } else { m_options[arg1] = arg2; } + if (m_options.coordinates) { + var coor = m_options.coordinates; + delete m_options.coordinates; + this._coordinates(coor); + } + if (m_options.name !== undefined) { + var name = m_options.name; + delete m_options.name; + this.name(name); + } this.modified(); return this; }; @@ -193,9 +203,10 @@ var annotation = function (type, args) { * 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 () { + this._coordinates = function (coodinates) { return []; }; @@ -298,7 +309,7 @@ var annotation = function (type, args) { properties: { annotationType: m_type, name: this.name(), - style: {} + annotationId: this.id() } }; for (i = 0; i < styles.length; i += 1) { @@ -308,7 +319,7 @@ var annotation = function (type, args) { if (key.toLowerCase().match(/color$/)) { value = util.convertColorToHex(value); } - obj.properties.style[key] = value; + obj.properties[key] = value; } } if (includeCrs) { @@ -361,6 +372,8 @@ var rectangleAnnotation = function (args) { uniformPolygon: true } }, args || {}); + args.corners = args.corners || args.coordinates; + delete args.coordinates; annotation.call(this, 'rectangle', args); /** @@ -382,9 +395,13 @@ var rectangleAnnotation = function (args) { * 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 () { + this._coordinates = function (coordinates) { + if (coordinates && coordinates.length > 1) { + this.options('corners', coordinates[0]); + } return this.options('corners'); }; @@ -449,7 +466,6 @@ var polygonAnnotation = function (args) { var m_this = this; args = $.extend(true, {}, { - vertices: [], style: { fill: true, fillColor: {r: 0, g: 1, b: 0}, @@ -483,6 +499,8 @@ var polygonAnnotation = function (args) { uniformPolygon: true } }, args || {}); + args.vertices = args.vertices || args.coordinates || []; + delete args.coordinates; annotation.call(this, 'polygon', args); /** @@ -532,9 +550,13 @@ var polygonAnnotation = function (args) { * 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 () { + this._coordinates = function (coordinates) { + if (coordinates) { + this.options('vertices', coordinates); + } return this.options('vertices'); }; @@ -679,6 +701,8 @@ var pointAnnotation = function (args) { strokeWidth: 3 } }, args || {}); + args.position = args.position || (args.coordinates ? args.coordinates[0] : undefined); + delete args.coordinates; annotation.call(this, 'point', args); /** @@ -711,9 +735,13 @@ var pointAnnotation = function (args) { * 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 () { + this._coordinates = function (coordinates) { + if (coordinates) { + this.options('vertices', coordinates); + } if (this.state() === annotationState.create) { return []; } diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 40897ea807..e628354e92 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -4,6 +4,7 @@ var geo_action = require('./action'); var geo_annotation = require('./annotation'); var geo_event = require('./event'); var registry = require('./registry'); +var transform = require('./transform'); var $ = require('jquery'); var Mousetrap = require('mousetrap'); @@ -51,6 +52,17 @@ var annotationLayer = function (args) { m_annotations = [], m_features = []; + var geojsonStyleProperties = { + 'fill': {dataType: 'boolean', keys: ['fill']}, + 'fillColor': {dataType: 'color', keys: ['fillColor', 'fill-color', 'marker-color', 'fill']}, + 'fillOpacity': {dataType: 'opacity', keys: ['fillOpacity', 'fill-opacity']}, + 'radius': {dataType: 'positive', keys: ['radius']}, + 'stroke': {dataType: 'boolean', keys: ['stroke']}, + 'strokeColor': {dataType: 'color', keys: ['strokeColor', 'stroke-color', 'stroke']}, + 'strokeOpacity': {dataType: 'opacity', keys: ['strokeOpacity', 'stroke-opacity']}, + 'strokeWidth': {dataType: 'positive', keys: ['strokeWidth', 'stroke-width']} + }; + m_options = $.extend(true, {}, { dblClickTime: 300, adjacentPointProximity: 5, // in pixels, 0 is exact @@ -248,9 +260,11 @@ var annotationLayer = function (args) { * * @param {boolean} skipCreating: if true, don't remove annotations that are * in the create state. + * @param {boolean} update if false, don't update the layer after removing + * the annotation. * @returns {number} the number of annotations that were removed. */ - this.removeAllAnnotations = function (skipCreating) { + this.removeAllAnnotations = function (skipCreating, update) { var removed = 0, annotation, pos = 0; while (pos < m_annotations.length) { annotation = m_annotations[pos]; @@ -261,7 +275,7 @@ var annotationLayer = function (args) { this.removeAnnotation(annotation, false); removed += 1; } - if (removed) { + if (removed && update !== false) { this.modified(); this._update(); this.draw(); @@ -357,7 +371,8 @@ var annotationLayer = function (args) { * * @param {object} geojson: if present, add annotations based on the given * geojson object. If undefined, return the current annotations as - * geojson. This may bei either a JSON string or a javascript object. + * geojson. This may be a JSON string, a javascript object, or a File + * object. * @param {boolean} clear: if true, when adding objects, first remove all * existing objects. * @param {string|geo.transform} [gcs] undefined to use the interface gcs, @@ -370,9 +385,19 @@ var annotationLayer = function (args) { this.geojson = function (geojson, clear, gcs, includeCrs) { if (geojson !== undefined) { if (clear) { - this.removeAllAnnotations(); + this.removeAllAnnotations(true, false); } - //DWM:: + var reader = registry.createFileReader('jsonReader', {layer: this}); + reader.read(geojson, function (features) { + $.each(features.slice(), function (feature_idx, feature) { + m_this._geojsonFeatureToAnnotation(feature, gcs); + m_this.deleteFeature(feature); + }); + }); + this.modified(); + this._update(); + this.draw(); + return this; } geojson = null; var features = []; @@ -391,6 +416,126 @@ var annotationLayer = function (args) { return geojson; }; + /** + * Convert a feature as parsed by the geojson reader into one or more + * annotations. + * + * @param {geo.feature} feature: the feature to convert. + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + */ + this._geojsonFeatureToAnnotation = function (feature, gcs) { + var dataList = feature.data(), + annotationList = registry.listAnnotations(); + $.each(dataList, function (data_idx, data) { + var type = (data.properties || {}).annotationType || feature.featureType, + options = $.extend({}, data.properties || {}), + position, datagcs, i, existing; + if ($.inArray(type, annotationList) < 0) { + return; + } + if (!options.style) { + options.style = {}; + } + delete options.annotationType; + switch (feature.featureType) { + case 'polygon': + position = feature.polygon()(data, data_idx); + if (!position || !position.outer || position.outer.length < 3) { + return; + } + position = position.outer; + if (position[position.length - 1][0] === position[0][0] && + position[position.length - 1][1] === position[0][1]) { + position.splice(position.length - 1, 1); + } + break; + 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]); + } + datagcs = ((data.crs && data.crs.type === 'name' && data.crs.properties && + data.crs.properties.type === 'proj4' && + data.crs.properties.name) ? data.crs.properties.name : gcs); + if (datagcs !== m_this.map().gcs()) { + position = transform.transformCoordinates(datagcs, m_this.map().gcs(), position); + } + options.coordinates = position; + /* For each style listed in the geojsonStyleProperties object, check if + * is given under any of the variety of keys as a valid instance of the + * required data type. If not, use the property from the feature. */ + $.each(geojsonStyleProperties, function (key, prop) { + var value; + $.each(prop.keys, function (idx, altkey) { + if (value !== undefined) { + return; + } + value = options[altkey]; + if (value === undefined || value === null) { + value = undefined; + return; + } + switch (prop.dataType) { + case 'color': + value = util.convertColor(value); + if (value === undefined || value.r === undefined) { + value = undefined; + } + break; + case 'positive': + value = +value; + if (isNaN(value) || value <= 0) { + value = undefined; + } + break; + case 'opacity': + value = +value; + if (isNaN(value) || value < 0 || value > 1) { + value = undefined; + } + break; + case 'boolean': + value = value && value !== 'false'; + break; + } + }); + if (value === undefined) { + value = feature.style.get(key)(data, data_idx); + } + if (value !== undefined) { + options.style[key] = value; + } + }); + /* Delete property keys we have used */ + $.each(geojsonStyleProperties, function (key, prop) { + $.each(prop.keys, function (idx, altkey) { + delete options[altkey]; + }); + }); + if (options.annotationId !== undefined) { + existing = m_this.annotationById(options.annotationId); + delete options.annotationId; + } + if (existing && existing.type() === type) { + delete options.state; + delete options.layer; + existing.options(options); + m_this.geoTrigger(geo_event.annotation.update, { + annotation: existing + }); + } else { + options.state = geo_annotation.state.done; + options.layer = m_this; + m_this.addAnnotation(registry.createAnnotation(type, options)); + } + }); + }; + /////////////////////////////////////////////////////////////////////////// /** * Update layer diff --git a/src/event.js b/src/event.js index d9f323c092..8e5daf8477 100644 --- a/src/event.js +++ b/src/event.js @@ -454,6 +454,16 @@ geo_event.annotation.add = 'geo_annotation_add'; ////////////////////////////////////////////////////////////////////////////// geo_event.annotation.add_before = 'geo_annotation_add_before'; +////////////////////////////////////////////////////////////////////////////// +/** + * Triggered when an annotation has been altered. This is currently only + * triggered when updating existing annotations via the geojson function. + * + * @property {geo.annotation} annotation The annotation that was altered. + */ +////////////////////////////////////////////////////////////////////////////// +geo_event.annotation.update = 'geo_annotation_update'; + ////////////////////////////////////////////////////////////////////////////// /** * Triggered when an annotation has been removed. diff --git a/src/jsonReader.js b/src/jsonReader.js index c7aa3dad3e..f9cd466927 100644 --- a/src/jsonReader.js +++ b/src/jsonReader.js @@ -203,7 +203,7 @@ var jsonReader = function (arg) { _default = convert(_default); return function (d, i, e, j) { var p; - if (spec) { + if (spec && j !== undefined && spec[j] !== undefined) { p = spec[j].properties; } else { p = d.properties; diff --git a/src/lineFeature.js b/src/lineFeature.js index 0d352daf80..550bbe2509 100644 --- a/src/lineFeature.js +++ b/src/lineFeature.js @@ -29,6 +29,8 @@ var lineFeature = function (arg) { var m_this = this, s_init = this._init; + this.featureType = 'line'; + //////////////////////////////////////////////////////////////////////////// /** * Get/Set line accessor diff --git a/src/pointFeature.js b/src/pointFeature.js index c92b8c097b..1bbb1e0ab5 100644 --- a/src/pointFeature.js +++ b/src/pointFeature.js @@ -44,6 +44,8 @@ var pointFeature = function (arg) { m_lastZoom = null, m_ignoreData = false; // flag to ignore data() calls made locally + this.featureType = 'point'; + //////////////////////////////////////////////////////////////////////////// /** * Get/Set clustering option diff --git a/src/polygonFeature.js b/src/polygonFeature.js index c89f3aa630..9387e35efd 100644 --- a/src/polygonFeature.js +++ b/src/polygonFeature.js @@ -67,6 +67,8 @@ var polygonFeature = function (arg) { s_style = this.style, m_coordinates = []; + this.featureType = 'polygon'; + //////////////////////////////////////////////////////////////////////////// /** * Get/set data. diff --git a/src/registry.js b/src/registry.js index 72a4f55c52..175397dafa 100644 --- a/src/registry.js +++ b/src/registry.js @@ -353,6 +353,25 @@ util.registerAnnotation = function (name, func, features) { return old; }; +////////////////////////////////////////////////////////////////////////////// +/** + * Create an annotation based on a registered type. + * + * @param {string} name The annotation name + * @param {object} options The options for the annotation. + * @returns {object} the new annotation. + */ +////////////////////////////////////////////////////////////////////////////// +util.createAnnotation = function (name, options) { + if (!annotations[name]) { + console.warn('The ' + name + ' annotation is not registered'); + return; + } + var annotation = annotations[name].func(options); + console.log('create', name, options, annotation); //DWM:: + return annotation; +}; + ////////////////////////////////////////////////////////////////////////////// /** * Get a list of registered annotation types. From b876b37ab287871f363df9ce5bfd714a7dbc9f3d Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 26 Sep 2016 13:46:16 -0400 Subject: [PATCH 3/7] Show geojson in the annotations example. You can edit the geojson live. --- examples/annotations/index.jade | 4 ++- examples/annotations/main.css | 4 +++ examples/annotations/main.js | 57 ++++++++++++++++++++++++++++++--- examples/common/js/examples.js | 2 +- src/annotationLayer.js | 41 +++++++++++++++++++----- src/registry.js | 1 - 6 files changed, 94 insertions(+), 15 deletions(-) diff --git a/examples/annotations/index.jade b/examples/annotations/index.jade index bd78a8a2d5..416b540a4e 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.jade @@ -28,7 +28,9 @@ block append mainContent span.entry-name Sample a.entry-edit(action='edit', title='Edit name and properties') ✎ a.entry-remove(action='remove', title='Delete this annotation') ✖ - // add buttons to copy annotations as geojson and to paste geojson to annotations + .form-group + textarea#geojson(type='textarea', rows=15, autocomplete='off', + autocorrect='off', autocapitalize='off', spellcheck='false') #editdialog.modal.fade .modal-dialog diff --git a/examples/annotations/main.css b/examples/annotations/main.css index c8eeb444fb..c4c93a6201 100644 --- a/examples/annotations/main.css +++ b/examples/annotations/main.css @@ -99,3 +99,7 @@ color: red; font-weight: bold; } +#geojson { + width: 100%; + resize: vertical; +} diff --git a/examples/annotations/main.js b/examples/annotations/main.js index a0bfb32496..a745065858 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -6,7 +6,7 @@ var annotationDebug = {}; $(function () { 'use strict'; - var layer, fromButtonSelect; + var layer, fromButtonSelect, fromGeojsonUpdate; // get the query parameters and set controls appropriately var query = utils.getQuery(); @@ -16,14 +16,21 @@ $(function () { $('.annotationtype button').removeClass('lastused'); $('.annotationtype button#' + query.lastannotation).addClass('lastused'); } + // You can set the intiial annotations via a query parameter. If the query + // parameter 'save=true' is specified, the query will be updated with the + // geojson. This can become too long for some browsers. + var initialGeoJSON = query.geojson; + // respond to changes in our controls $('#controls').on('change', change_controls); + $('#geojson[type=textarea]').on('input propertychange', change_geojson); $('#controls').on('click', 'a', select_control); $('.annotationtype button').on('click', select_annotation); $('#editdialog').on('submit', edit_update); $('#controls').toggleClass('no-controls', query.controls === 'false'); + // start the map near Fresno unless the query parameters say to do otherwise var map = geo.map({ node: '#map', center: { @@ -33,8 +40,7 @@ $(function () { zoom: query.zoom ? +query.zoom : 8, rotation: query.rotation ? +query.rotation * Math.PI / 180 : 0 }); - // allow some query parameters without controls to specify what map we will - // show + // allow some query parameters to specify what map we will show if (query.map !== 'false') { if (query.map !== 'satellite') { annotationDebug.mapLayer = map.createLayer('osm'); @@ -68,13 +74,18 @@ $(function () { } } + // if we have geojson as a query parameter, populate our annotations + if (initialGeoJSON) { + layer.geojson(initialGeoJSON, true); + } + // expose some internal parameters so you can examine them from the console annotationDebug.map = map; annotationDebug.layer = layer; annotationDebug.query = query; /** - * When the mouse is clicked, switch adding an annotation if appropriate. + * When the mouse is clicked, switch to adding an annotation if appropriate. * * @param {geo.event} evt geojs event. */ @@ -116,9 +127,32 @@ $(function () { value === ctl.attr('placeholder'))) { delete query[param]; } + // update our query parameters, os when you reload the page it is in the + // same state utils.setQuery(query); } + /** + * Handle changes to the geojson. + * + * @param evt jquery evt that triggered this call. + */ + function change_geojson(evt) { + var ctl = $(evt.target), + value = ctl.val(); + // when we update the geojson from the textarea control, raise a flag so we + // (a) ignore bad geojson, and (b) don't replace the user's geojson with + // the auto-generated geojson + fromGeojsonUpdate = true; + var result = layer.geojson(value, 'update'); + if (query.save && result !== undefined) { + var geojson = layer.geojson(); + query.geojson = geojson ? JSON.stringify(geojson) : undefined; + utils.setQuery(query); + } + fromGeojsonUpdate = false; + } + /** * Handle selecting an annotation button. * @@ -209,6 +243,15 @@ $(function () { }); $('#annotationheader').css( 'display', $('#annotationlist .entry').length <= 1 ? 'none' : 'block'); + if (!fromGeojsonUpdate) { + // update the geojson textarea + var geojson = layer.geojson(); + $('#geojson').val(geojson ? JSON.stringify(geojson, undefined, 2) : ''); + if (query.save) { + query.geojson = geojson ? JSON.stringify(geojson) : undefined; + utils.setQuery(query); + } + } } /** @@ -256,12 +299,15 @@ $(function () { dlg.attr('annotation-id', id); dlg.attr('annotation-type', type); $('[option="name"]', dlg).val(annotation.name()); + // populate each control with the current value of the annotation $('.form-group[annotation-types]').each(function () { var ctl = $(this), key = $('[option]', ctl).attr('option'), format = $('[option]', ctl).attr('format'), value; if (!ctl.attr('annotation-types').match(typeMatch)) { + // if a property doesn't exist for the current annotation's type, hide + // the control ctl.hide(); return; } @@ -269,6 +315,7 @@ $(function () { value = opt.style[key]; switch (format) { case 'color': + // always show colors as hex values value = geo.util.convertColorToHex(value); break; } @@ -295,6 +342,7 @@ $(function () { error, newopt = {}; + // validate form values $('.form-group[annotation-types]').each(function () { var ctl = $(this), key = $('[option]', ctl).attr('option'), @@ -333,6 +381,7 @@ $(function () { annotation.options({style: newopt}).draw(); dlg.modal('hide'); + // refresh the annotation list handleAnnotationChange(); } }); diff --git a/examples/common/js/examples.js b/examples/common/js/examples.js index ae01d7e14c..7198b4d257 100644 --- a/examples/common/js/examples.js +++ b/examples/common/js/examples.js @@ -8,7 +8,7 @@ var exampleUtils = { '&').map(function (n) { n = n.split('='); if (n[0]) { - this[decodeURIComponent(n[0])] = decodeURIComponent(n[1]); + this[decodeURIComponent(n[0].replace(/\+/g, '%20'))] = decodeURIComponent(n[1].replace(/\+/g, '%20')); } return this; }.bind({}))[0]; diff --git a/src/annotationLayer.js b/src/annotationLayer.js index e628354e92..a2e1a1e47d 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -373,31 +373,52 @@ var annotationLayer = function (args) { * geojson object. If undefined, return the current annotations as * geojson. This may be a JSON string, a javascript object, or a File * object. - * @param {boolean} clear: if true, when adding objects, first remove all - * existing objects. + * @param {boolean} clear: if true, when adding annotations, first remove all + * existing objects. If 'update', update existing annotations and remove + * annotations that no longer exit, If false, update existing + * annotations and leave unchanged annotations. * @param {string|geo.transform} [gcs] undefined to use the interface gcs, * null to use the map gcs, or any other transform. * @param {boolean} includeCrs: if true, include the coordinate system in the * output. - * @return {object} the current annotations as a javascript object that - * can be converted to geojson using JSON.stringify. + * @return {object|number|undefined} if geojson was undefined, the current + * annotations as a javascript object that can be converted to geojson + * using JSON.stringify. If geojson is specified, either the number of + * annotations now present upon success, or undefined if the value in + * geojson was not able to be parsed. */ this.geojson = function (geojson, clear, gcs, includeCrs) { if (geojson !== undefined) { - if (clear) { + var reader = registry.createFileReader('jsonReader', {layer: this}); + if (!reader.canRead(geojson)) { + return; + } + if (clear === true) { this.removeAllAnnotations(true, false); } - var reader = registry.createFileReader('jsonReader', {layer: this}); + if (clear === 'update') { + $.each(this.annotations(), function (idx, annotation) { + annotation.options('updated', false); + }); + } reader.read(geojson, function (features) { $.each(features.slice(), function (feature_idx, feature) { m_this._geojsonFeatureToAnnotation(feature, gcs); m_this.deleteFeature(feature); }); }); + if (clear === 'update') { + $.each(this.annotations(), function (idx, annotation) { + if (annotation.options('updated') === false && + annotation.state() === geo_annotation.state.done) { + m_this.removeAnnotation(annotation, false); + } + }); + } this.modified(); this._update(); this.draw(); - return this; + return m_annotations.length; } geojson = null; var features = []; @@ -521,9 +542,12 @@ var annotationLayer = function (args) { existing = m_this.annotationById(options.annotationId); delete options.annotationId; } - if (existing && existing.type() === type) { + if (existing && existing.type() === type && existing.state() === geo_annotation.state.done && existing.options('updated') === false) { + /* We could change the state of the existing annotation if it differs + * from done. */ delete options.state; delete options.layer; + options.updated = true; existing.options(options); m_this.geoTrigger(geo_event.annotation.update, { annotation: existing @@ -531,6 +555,7 @@ var annotationLayer = function (args) { } else { options.state = geo_annotation.state.done; options.layer = m_this; + options.updated = 'new'; m_this.addAnnotation(registry.createAnnotation(type, options)); } }); diff --git a/src/registry.js b/src/registry.js index 175397dafa..b227586f73 100644 --- a/src/registry.js +++ b/src/registry.js @@ -368,7 +368,6 @@ util.createAnnotation = function (name, options) { return; } var annotation = annotations[name].func(options); - console.log('create', name, options, annotation); //DWM:: return annotation; }; From c284d0587cf9068cd49ade2ae87e974315e1d804 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 26 Sep 2016 16:29:59 -0400 Subject: [PATCH 4/7] Start adding tests. --- tests/cases/annotation.js | 9 +++- tests/cases/colors.js | 89 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index d2819faf23..05a951313d 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -305,6 +305,7 @@ describe('geo.annotation', function () { }); describe('annotation registry', function () { + var newshapeCount = 0; it('listAnnotations', function () { var list = geo.listAnnotations(); expect($.inArray('rectangle', list) >= 0).toBe(true); @@ -313,13 +314,19 @@ describe('geo.annotation', function () { expect($.inArray('unknown', list) >= 0).toBe(false); }); it('registerAnnotation', function () { - var func = function () {}; + var func = function () { newshapeCount += 1; return 'newshape return'; }; expect($.inArray('newshape', geo.listAnnotations()) >= 0).toBe(false); expect(geo.registerAnnotation('newshape', func)).toBe(undefined); expect($.inArray('newshape', geo.listAnnotations()) >= 0).toBe(true); expect(geo.registerAnnotation('newshape', func).func).toBe(func); expect($.inArray('newshape', geo.listAnnotations()) >= 0).toBe(true); }); + it('createAnnotation', function () { + expect(geo.createAnnotation('unknown')).toBe(undefined); + expect(newshapeCount).toBe(0); + expect(geo.createAnnotation('newshape')).toBe('newshape return'); + expect(newshapeCount).toBe(1); + }); it('featuresForAnnotations', function () { var features = geo.featuresForAnnotations(['polygon']); expect($.inArray('polygon', features) >= 0).toBe(true); diff --git a/tests/cases/colors.js b/tests/cases/colors.js index 69bb9f3e2d..bf233f43d5 100644 --- a/tests/cases/colors.js +++ b/tests/cases/colors.js @@ -56,7 +56,7 @@ describe('geo.util.convertColor', function () { }); }); describe('From hex value', function () { - it('#000000', function () { + it('0x000000', function () { var c = geo.util.convertColor(0x000000); expect(c).toEqual({ r: 0, @@ -64,7 +64,7 @@ describe('geo.util.convertColor', function () { b: 0 }); }); - it('#ffffff', function () { + it('0xffffff', function () { var c = geo.util.convertColor(0xffffff); expect(c).toEqual({ r: 1, @@ -72,7 +72,7 @@ describe('geo.util.convertColor', function () { b: 1 }); }); - it('#1256ab', function () { + it('0x1256ab', function () { var c = geo.util.convertColor(0x1256ab); expect(c).toEqual({ r: 18 / 255, @@ -134,3 +134,86 @@ describe('geo.util.convertColor', function () { }); }); }); + +describe('geo.util.convertColorToHex', function () { + 'use strict'; + + var geo = require('../test-utils').geo; + + describe('From hex string', function () { + it('#000000', function () { + var c = geo.util.convertColorToHex('#000000'); + expect(c).toEqual('#000000'); + }); + it('#ffffff', function () { + var c = geo.util.convertColorToHex('#ffffff'); + expect(c).toEqual('#ffffff'); + }); + it('#1256aB', function () { + var c = geo.util.convertColorToHex('#1256aB'); + expect(c).toEqual('#1256ab'); + }); + }); + describe('From short hex string', function () { + it('#000', function () { + var c = geo.util.convertColorToHex('#000'); + expect(c).toEqual('#000000'); + }); + it('#fff', function () { + var c = geo.util.convertColorToHex('#fff'); + expect(c).toEqual('#ffffff'); + }); + it('#26b', function () { + var c = geo.util.convertColorToHex('#26b'); + expect(c).toEqual('#2266bb'); + }); + }); + describe('From hex value', function () { + it('0x000000', function () { + var c = geo.util.convertColorToHex(0x000000); + expect(c).toEqual('#000000'); + }); + it('0xffffff', function () { + var c = geo.util.convertColorToHex(0xffffff); + expect(c).toEqual('#ffffff'); + }); + it('0x1256ab', function () { + var c = geo.util.convertColorToHex(0x1256ab); + expect(c).toEqual('#1256ab'); + }); + }); + describe('From css name', function () { + it('red', function () { + var c = geo.util.convertColorToHex('red'); + expect(c).toEqual('#ff0000'); + }); + it('green', function () { + var c = geo.util.convertColorToHex('green'); + expect(c).toEqual('#008000'); + }); + it('blue', function () { + var c = geo.util.convertColorToHex('blue'); + expect(c).toEqual('#0000ff'); + }); + it('steelblue', function () { + var c = geo.util.convertColorToHex('steelblue'); + expect(c).toEqual('#4682b4'); + }); + }); + describe('From rgb triplet', function () { + it('object', function () { + var c = geo.util.convertColorToHex({ + r: 0, + g: 1, + b: 1 + }); + expect(c).toEqual('#00ffff'); + }); + }); + describe('Pass through unknown colors', function () { + it('none', function () { + var c = geo.util.convertColorToHex('none'); + expect(c).toEqual('#000000'); + }); + }); +}); From e91cf946207e6d25c67c1abe8554e63f0b18a542 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 27 Sep 2016 08:23:00 -0400 Subject: [PATCH 5/7] Fix updating points and rectangles. --- examples/annotations/main.js | 6 +++--- src/annotation.js | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/annotations/main.js b/examples/annotations/main.js index a745065858..94f5b38021 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -34,10 +34,10 @@ $(function () { var map = geo.map({ node: '#map', center: { - x: query.x ? +query.x : -119.5420833, - y: query.y ? +query.y : 37.4958333 + x: query.x ? +query.x : -119.150, + y: query.y ? +query.y : 36.712 }, - zoom: query.zoom ? +query.zoom : 8, + zoom: query.zoom ? +query.zoom : 10, rotation: query.rotation ? +query.rotation * Math.PI / 180 : 0 }); // allow some query parameters to specify what map we will show diff --git a/src/annotation.js b/src/annotation.js index 3f647a072d..d8b9f2e453 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -144,6 +144,7 @@ var annotation = function (type, args) { if (m_options.coordinates) { var coor = m_options.coordinates; delete m_options.coordinates; + console.log(coor); //DWM:: this._coordinates(coor); } if (m_options.name !== undefined) { @@ -399,8 +400,10 @@ var rectangleAnnotation = function (args) { * @returns {array} an array of coordinates. */ this._coordinates = function (coordinates) { - if (coordinates && coordinates.length > 1) { - this.options('corners', coordinates[0]); + if (coordinates && coordinates.length >= 4) { + this.options('corners', coordinates.slice(0, 4)); + /* Should we ensure that the four points form a rectangle in the current + * projection, though this might not be rectangular in another gcs? */ } return this.options('corners'); }; @@ -739,8 +742,8 @@ var pointAnnotation = function (args) { * @returns {array} an array of coordinates. */ this._coordinates = function (coordinates) { - if (coordinates) { - this.options('vertices', coordinates); + if (coordinates && coordinates.length >= 1) { + this.options('position', coordinates[0]); } if (this.state() === annotationState.create) { return []; From 43e9f8405b732ce28bd84f1eb998e985576d8541 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 29 Sep 2016 08:58:22 -0400 Subject: [PATCH 6/7] Add tests for annotation geojson functions. --- src/annotation.js | 11 ++-- src/mapInteractor.js | 2 +- tests/cases/annotation.js | 125 +++++++++++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 9 deletions(-) diff --git a/src/annotation.js b/src/annotation.js index e45f1ef58f..6d4d898c2b 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -147,7 +147,6 @@ var annotation = function (type, args) { if (m_options.coordinates) { var coor = m_options.coordinates; delete m_options.coordinates; - console.log(coor); //DWM:: this._coordinates(coor); } if (m_options.name !== undefined) { @@ -210,7 +209,7 @@ var annotation = function (type, args) { * @param {array} coordinates: an optional array of coordinates to set. * @returns {array} an array of coordinates. */ - this._coordinates = function (coodinates) { + this._coordinates = function (coordinates) { return []; }; @@ -230,8 +229,8 @@ var annotation = function (type, args) { if (gcs !== map.gcs()) { coord = transform.transformCoordinates(map.gcs(), gcs, coord); } - return coord; } + return coord; }; /** @@ -421,7 +420,7 @@ var rectangleAnnotation = function (args) { */ this._geojsonCoordinates = function (gcs) { var src = this.coordinates(gcs); - if (src.length < 4) { + if (!src || src.length < 4) { return; } var coor = []; @@ -655,7 +654,7 @@ var polygonAnnotation = function (args) { */ this._geojsonCoordinates = function (gcs) { var src = this.coordinates(gcs); - if (src.length < 3 || this.state() === annotationState.create) { + if (!src || src.length < 3 || this.state() === annotationState.create) { return; } var coor = []; @@ -801,7 +800,7 @@ var pointAnnotation = function (args) { */ this._geojsonCoordinates = function (gcs) { var src = this.coordinates(gcs); - if (this.state() === annotationState.create || src.length < 1) { + if (!src || this.state() === annotationState.create || src.length < 1 || src[0] === undefined) { return; } return [src[0].x, src[0].y]; diff --git a/src/mapInteractor.js b/src/mapInteractor.js index ae4803ac52..3da06d7b7d 100644 --- a/src/mapInteractor.js +++ b/src/mapInteractor.js @@ -921,7 +921,7 @@ var mapInteractor = function (args) { //////////////////////////////////////////////////////////////////////////// /** - * Based on the screen coodinates of a selection, zoom or unzoom and + * Based on the screen coordinates of a selection, zoom or unzoom and * recenter. * * @private diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index 05a951313d..23a8d2e577 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -37,7 +37,7 @@ describe('geo.annotation', function () { expect(ann.name()).toBe('Test ' + ann.id()); expect(ann.layer()).toBe(undefined); expect(ann.features()).toEqual([]); - expect(ann.coordinates()).toBe(undefined); + expect(ann.coordinates()).toEqual([]); expect(ann.mouseClick()).toBe(undefined); expect(ann.mouseMove()).toBe(undefined); expect(ann._coordinates()).toEqual([]); @@ -101,6 +101,22 @@ describe('geo.annotation', function () { expect(ann.options().testopt).toBe(40); expect(ann.options({testopt: 30})).toBe(ann); expect(ann.options().testopt).toBe(30); + /* name and coordinates are handled specially */ + ann.options('name', 'newname'); + expect(ann.options().name).toBe(undefined); + expect(ann.name()).toBe('newname'); + var coord = null, testval = [[1, 2], [3, 4]]; + ann._coordinates = function (arg) { + if (arg !== undefined) { + coord = arg; + return ann; + } + return coord; + }; + expect(ann._coordinates()).toBe(null); + ann.options('coordinates', testval); + expect(ann.options().coordinates).toBe(undefined); + expect(ann._coordinates()).toBe(testval); }); it('coordinates', function () { var ann = geo.annotation.annotation('test', {layer: layer}); @@ -128,10 +144,44 @@ describe('geo.annotation', function () { expect(drawCalled).toBe(1); layer.draw = oldDraw; }); + it('geojson', function () { + var ann = geo.annotation.annotation('test', { + layer: layer, + style: {fillColor: 'red'}, + name: 'testAnnotation' + }); + expect(ann.geojson()).toBe(undefined); + ann._coordinates = function () { + return geo.transform.transformCoordinates(map.ingcs(), map.gcs(), [ + -73.757222, 42.849776]); + }; + ann._geojsonCoordinates = function (gcs) { + return this.coordinates(gcs); + }; + ann._geojsonGeometryType = function () { + return 'Point'; + }; + var geojson = ann.geojson(); + expect(geojson.type).toBe('Feature'); + expect(geojson.geometry.type).toBe('Point'); + expect(geojson.geometry.coordinates.length).toBe(2); + expect(geojson.geometry.coordinates[1]).toBeCloseTo(42.849775); + expect(geojson.properties.name).toBe('testAnnotation'); + expect(geojson.properties.fillColor).toBe('#ff0000'); + expect(geojson.crs).toBe(undefined); + geojson = ann.geojson('EPSG:3857'); + expect(geojson.geometry.coordinates[1]).toBeCloseTo(5289134.103576); + expect(geojson.crs).toBe(undefined); + geojson = ann.geojson('EPSG:3857', true); + expect(geojson.crs.properties.name).toBe('EPSG:3857'); + geojson = ann.geojson(undefined, true); + expect(geojson.crs.properties.name).toBe('EPSG:4326'); + }); }); describe('geo.annotation.rectangleAnnotation', function () { - var corners = [{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}]; + var corners = [{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}], + corners2 = [{x: 2, y: 0}, {x: 3, y: 0}, {x: 3, y: 1}, {x: 2, y: 1}]; it('create', function () { var ann = geo.annotation.rectangleAnnotation(); expect(ann instanceof geo.annotation.rectangleAnnotation); @@ -147,11 +197,34 @@ describe('geo.annotation', function () { it('_coordinates', function () { var ann = geo.annotation.rectangleAnnotation({corners: corners}); expect(ann._coordinates()).toEqual(corners); + ann._coordinates(corners2); + expect(ann._coordinates()).toEqual(corners2); + }); + it('_geojsonCoordinates', function () { + var ann = geo.annotation.rectangleAnnotation(); + expect(ann._geojsonCoordinates()).toBe(undefined); + ann._coordinates(corners); + var coor = ann._geojsonCoordinates(); + expect(coor[0].length).toBe(5); + }); + it('_geojsonGeometryType', function () { + var ann = geo.annotation.rectangleAnnotation(); + expect(ann._geojsonGeometryType()).toBe('Polygon'); + }); + it('geojson', function () { + var ann = geo.annotation.rectangleAnnotation({corners: corners}); + var geojson = ann.geojson(); + expect(geojson.type).toBe('Feature'); + expect(geojson.geometry.type).toBe('Polygon'); + expect(geojson.geometry.coordinates.length).toBe(1); + expect(geojson.geometry.coordinates[0].length).toBe(5); + expect(geojson.geometry.coordinates[0][2][1]).toBeCloseTo(1); }); }); describe('geo.annotation.polygonAnnotation', 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.polygonAnnotation(); expect(ann instanceof geo.annotation.polygonAnnotation); @@ -186,6 +259,28 @@ describe('geo.annotation', function () { it('_coordinates', function () { var ann = geo.annotation.polygonAnnotation({vertices: vertices}); expect(ann._coordinates()).toEqual(vertices); + ann._coordinates(vertices2); + expect(ann._coordinates()).toEqual(vertices2); + }); + it('_geojsonCoordinates', function () { + var ann = geo.annotation.polygonAnnotation(); + expect(ann._geojsonCoordinates()).toBe(undefined); + ann._coordinates(vertices2); + var coor = ann._geojsonCoordinates(); + expect(coor[0].length).toBe(4); + }); + it('_geojsonGeometryType', function () { + var ann = geo.annotation.polygonAnnotation(); + expect(ann._geojsonGeometryType()).toBe('Polygon'); + }); + it('geojson', function () { + var ann = geo.annotation.polygonAnnotation({vertices: vertices2}); + var geojson = ann.geojson(); + expect(geojson.type).toBe('Feature'); + expect(geojson.geometry.type).toBe('Polygon'); + expect(geojson.geometry.coordinates.length).toBe(1); + expect(geojson.geometry.coordinates[0].length).toBe(4); + expect(geojson.geometry.coordinates[0][2][1]).toBeCloseTo(30); }); it('mouseMove', function () { var ann = geo.annotation.polygonAnnotation({vertices: vertices}); @@ -257,6 +352,7 @@ describe('geo.annotation', function () { describe('geo.annotation.pointAnnotation', function () { var point = {x: 30, y: 25}; + var point2 = {x: 50, y: 35}; it('create', function () { var ann = geo.annotation.pointAnnotation(); @@ -276,9 +372,34 @@ describe('geo.annotation', function () { it('_coordinates', function () { var ann = geo.annotation.pointAnnotation({position: point}); expect(ann._coordinates()).toEqual([point]); + ann._coordinates([point2]); + expect(ann._coordinates()).toEqual([point2]); ann.state(geo.annotation.state.create); expect(ann._coordinates()).toEqual([]); }); + it('_geojsonStyles', function () { + var ann = geo.annotation.pointAnnotation(); + expect(ann._geojsonStyles().length).toBe(8); + }); + it('_geojsonCoordinates', function () { + var ann = geo.annotation.pointAnnotation(); + expect(ann._geojsonCoordinates()).toBe(undefined); + ann._coordinates([point]); + var coor = ann._geojsonCoordinates(); + expect(coor.length).toBe(2); + }); + it('_geojsonGeometryType', function () { + var ann = geo.annotation.pointAnnotation(); + expect(ann._geojsonGeometryType()).toBe('Point'); + }); + it('geojson', function () { + var ann = geo.annotation.pointAnnotation({position: point}); + var geojson = ann.geojson(); + expect(geojson.type).toBe('Feature'); + expect(geojson.geometry.type).toBe('Point'); + expect(geojson.geometry.coordinates.length).toBe(2); + expect(geojson.geometry.coordinates[1]).toBeCloseTo(25); + }); it('mouseClick', function () { var ann = geo.annotation.pointAnnotation(); expect(ann.mouseClick({ From a065cafe3bb50bc6a8b680f74de9aa1d351b0c11 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 29 Sep 2016 11:15:42 -0400 Subject: [PATCH 7/7] Add tests for the geojson functions in the annotation layer. --- examples/annotations/main.js | 27 ++--- src/annotationLayer.js | 82 +++++++++------ src/jsonReader.js | 2 +- tests/cases/annotationLayer.js | 180 +++++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 52 deletions(-) diff --git a/examples/annotations/main.js b/examples/annotations/main.js index 94f5b38021..5a005462a5 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -350,28 +350,13 @@ $(function () { if (!ctl.attr('annotation-types').match(typeMatch)) { return; } - value = $('[option]', ctl).val(); - switch ($('[option]', ctl).attr('format')) { - case 'boolean': - value = ('' + value).toLowerCase() === 'true'; - break; - case 'color': - value = geo.util.convertColor(value); - break; - case 'opacity': - value = +value; - if (value < 0 || value > 1 || isNaN(value)) { - error = $('label', ctl).text() + ' must be a between 0 and 1, inclusive.'; - } - break; - case 'positive': - value = +value; - if (value <= 0 || isNaN(value)) { - error = $('label', ctl).text() + ' must be a positive number.'; - } - break; + value = layer.validateAttribute($('[option]', ctl).val(), + $('[option]', ctl).attr('format')); + if (value === undefined) { + error = $('label', ctl).text() + ' is not a valid value'; + } else { + newopt[key] = value; } - newopt[key] = value; }); if (error) { $('#edit-validation-error', dlg).text(error); diff --git a/src/annotationLayer.js b/src/annotationLayer.js index a2e1a1e47d..5584ff2b64 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -469,6 +469,9 @@ var annotationLayer = function (args) { if (position[position.length - 1][0] === position[0][0] && position[position.length - 1][1] === position[0][1]) { position.splice(position.length - 1, 1); + if (position.length < 3) { + return; + } } break; case 'point': @@ -493,40 +496,14 @@ var annotationLayer = function (args) { $.each(geojsonStyleProperties, function (key, prop) { var value; $.each(prop.keys, function (idx, altkey) { - if (value !== undefined) { - return; - } - value = options[altkey]; - if (value === undefined || value === null) { - value = undefined; + if (value === undefined) { + value = m_this.validateAttribute(options[altkey], prop.dataType); return; } - switch (prop.dataType) { - case 'color': - value = util.convertColor(value); - if (value === undefined || value.r === undefined) { - value = undefined; - } - break; - case 'positive': - value = +value; - if (isNaN(value) || value <= 0) { - value = undefined; - } - break; - case 'opacity': - value = +value; - if (isNaN(value) || value < 0 || value > 1) { - value = undefined; - } - break; - case 'boolean': - value = value && value !== 'false'; - break; - } }); if (value === undefined) { - value = feature.style.get(key)(data, data_idx); + value = m_this.validateAttribute( + feature.style.get(key)(data, data_idx), prop.dataType); } if (value !== undefined) { options.style[key] = value; @@ -561,6 +538,51 @@ var annotationLayer = function (args) { }); }; + /** + * Validate a value for an attribute based on a specified data type. This + * returns a sanitized value or undefined if the value was invalid. Data + * types include: + * color: a css string, #rrggbb hex string, #rgb hex string, number, or + * object with r, g, b properties in the range of [0-1]. + * opacity: a floating point number in the range [0, 1]. + * positive: a floating point number greater than zero. + * boolean: the string 'false' and falsy values are false, all else is + * true. null and undefined are still considered invalid values. + * @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 + * undefined. + */ + this.validateAttribute = function (value, dataType) { + if (value === undefined || value === null) { + return; + } + switch (dataType) { + case 'boolean': + value = !!value && value !== 'false'; + break; + case 'color': + value = util.convertColor(value); + if (value === undefined || value.r === undefined) { + return; + } + break; + case 'opacity': + value = +value; + if (isNaN(value) || value < 0 || value > 1) { + return; + } + break; + case 'positive': + value = +value; + if (isNaN(value) || value <= 0) { + return; + } + break; + } + return value; + }; + /////////////////////////////////////////////////////////////////////////// /** * Update layer diff --git a/src/jsonReader.js b/src/jsonReader.js index f9cd466927..8a9a5249f1 100644 --- a/src/jsonReader.js +++ b/src/jsonReader.js @@ -208,7 +208,7 @@ var jsonReader = function (arg) { } else { p = d.properties; } - if (p.hasOwnProperty(prop)) { + if (p !== undefined && p.hasOwnProperty(prop)) { return convert(p[prop]); } return _default; diff --git a/tests/cases/annotationLayer.js b/tests/cases/annotationLayer.js index 2628a68fac..2e30e4ff64 100644 --- a/tests/cases/annotationLayer.js +++ b/tests/cases/annotationLayer.js @@ -190,6 +190,69 @@ describe('geo.annotationLayer', function () { expect(layer.displayDistance(c3, null, c1, 'display')).toBeCloseTo(10.63, 2); expect(layer.displayDistance(c3, null, c2)).toBeCloseTo(4.47, 2); }); + it('geojson', function () { + layer.removeAllAnnotations(); + layer.addAnnotation(poly); + layer.addAnnotation(rect); + var geojson = layer.geojson(); + expect(geojson.type).toBe('FeatureCollection'); + expect(geojson.features.length).toBe(1); + expect(geojson.features[0].crs).toBe(undefined); + geojson = layer.geojson(undefined, undefined, 'EPSG:4326', true); + expect(geojson.features[0].crs.properties.name).toBe('EPSG:4326'); + layer.removeAllAnnotations(); + expect(layer.geojson()).toBe(null); + /* test setting via geojson */ + layer.addAnnotation(poly); + layer.addAnnotation(rect); + expect(layer.geojson('not geojson')).toBe(undefined); + expect(layer.geojson('not geojson', true)).toBe(undefined); + expect(layer.annotations().length).toBe(2); + var sampleGeojson = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-118.872649, 36.696299] + } + }; + expect(layer.geojson(sampleGeojson)).toBe(3); + expect(layer.geojson(JSON.stringify(sampleGeojson))).toBe(4); + sampleGeojson.properties = {annotationId: layer.annotations()[3].id()}; + expect(layer.geojson(sampleGeojson)).toBe(5); + expect(layer.geojson(sampleGeojson, 'update')).toBe(2); + expect(layer.geojson(sampleGeojson, true)).toBe(2); + }); + it('validateAttribute', function () { + expect(layer.validateAttribute(undefined, 'other')).toBe(undefined); + expect(layer.validateAttribute(null, 'other')).toBe(undefined); + + expect(layer.validateAttribute('value', 'other')).toBe('value'); + expect(layer.validateAttribute('value', 'boolean')).toBe(true); + expect(layer.validateAttribute(true, 'boolean')).toBe(true); + expect(layer.validateAttribute(0, 'boolean')).toBe(false); + expect(layer.validateAttribute('false', 'boolean')).toBe(false); + + expect(layer.validateAttribute('not a color', 'color')).toBe(undefined); + expect(layer.validateAttribute('yellow', 'color')).toEqual({r: 1, g: 1, b: 0}); + expect(layer.validateAttribute('#ffff00', 'color')).toEqual({r: 1, g: 1, b: 0}); + expect(layer.validateAttribute('#ff0', 'color')).toEqual({r: 1, g: 1, b: 0}); + expect(layer.validateAttribute({r: 1, g: 1, b: 0}, 'color')).toEqual({r: 1, g: 1, b: 0}); + + expect(layer.validateAttribute(0.5, 'opacity')).toBe(0.5); + expect(layer.validateAttribute('0.5', 'opacity')).toBe(0.5); + expect(layer.validateAttribute(0, 'opacity')).toBe(0); + expect(layer.validateAttribute(-1, 'opacity')).toBe(undefined); + expect(layer.validateAttribute(1, 'opacity')).toBe(1); + expect(layer.validateAttribute(1.2, 'opacity')).toBe(undefined); + expect(layer.validateAttribute('value', 'opacity')).toBe(undefined); + + expect(layer.validateAttribute(0.5, 'positive')).toBe(0.5); + expect(layer.validateAttribute('0.5', 'positive')).toBe(0.5); + expect(layer.validateAttribute(0, 'positive')).toBe(undefined); + expect(layer.validateAttribute(-1, 'positive')).toBe(undefined); + expect(layer.validateAttribute(1.2, 'positive')).toBe(1.2); + expect(layer.validateAttribute('value', 'positive')).toBe(undefined); + }); }); describe('Private utility functions', function () { var map, layer, point, rect, rect2; @@ -313,6 +376,123 @@ describe('geo.annotationLayer', function () { expect(layer.annotations().length).toBe(1); expect(layer.annotations()[0].type()).toBe('rectangle'); }); + it('_geojsonFeatureToAnnotation', function () { + map.deleteLayer(layer); + layer = map.createLayer('annotation'); /* use the vgl variant */ + /* This is tested through the layer.geojson function */ + var lineString = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [[-73.759202, 42.849643], [-73.756799, 42.849572]] + } + }; + expect(layer.geojson(lineString)).toBe(0); + lineString.properties.annotationType = 'polygon'; + expect(layer.geojson(lineString)).toBe(0); + var sample = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-118.872649, 36.696298] + }, + properties: { + radius: 6.7 + } + }, { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [-118.915853, 36.606894], + [-118.531332, 36.587048], + [-118.280020, 36.600279], + [-118.355551, 36.436937], + [-118.565664, 36.302031], + [-118.764791, 36.488847], + [-118.915853, 36.606894] + ]] + }, + properties: { + name: 'Sequoia', + fill: true, + fillColor: '#00ff00', + fillOpacity: 0.25, + stroke: true, + strokeColor: 'black', + strokeOpacity: 1, + strokeWidth: 4 + } + }, { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [-118.897685, 36.773031], + [-118.897685, 36.915198], + [-118.692647, 36.915198], + [-118.692647, 36.773031], + [-118.897685, 36.773031] + ]] + }, + properties: { + annotationType: 'rectangle' + } + }] + }; + expect(layer.geojson(sample)).toBe(3); + var badpoly = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [-118.915853, 36.606894], + [-118.531332, 36.587048], + [-118.915853, 36.606894] + ]] + } + }; + expect(layer.geojson(badpoly, true)).toBe(0); + badpoly.geometry.coordinates.splice(2, 1); + expect(layer.geojson(badpoly, true)).toBe(0); + var badattr = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-118.872649, 36.696298] + }, + properties: { + radius: -5, + fillColor: 'no such color', + fillOpacity: -1 + } + }; + layer.geojson(badattr, true); + var attr = layer.geojson().features[0].properties; + expect(attr.radius).toBeGreaterThan(0); + expect(attr.fillOpacity).toBeGreaterThan(0); + expect(attr.fillColor).toBe('#00ff00'); + var goodattr = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-118.872649, 36.696298] + }, + properties: { + radius: 3, + fillColor: 'indigo', + fillOpacity: 0.3 + } + }; + layer.geojson(goodattr, true); + attr = layer.geojson().features[0].properties; + expect(attr.radius).toBe(3); + expect(attr.fillOpacity).toBe(0.3); + expect(attr.fillColor).toBe('#4b0082'); + }); }); it('Test destroy layer.', function () { var map = create_map();