diff --git a/examples/annotations/index.jade b/examples/annotations/index.jade index 416b540a4e..5e8a672e19 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.jade @@ -45,6 +45,9 @@ block append mainContent .form-group(annotation-types='point') label(for='edit-radius') Radius input#edit-radius(option='radius', format='positive') + .form-group(annotation-types='point', title='Set to "false" to disable, "true" to use the specified radius at the current zoom, or a zoom level to use the specified radius at that zoom level.') + label(for='edit-scaled') Scale with Zoom + input#edit-radius(option='scaled', format='booleanOrNumber') .form-group(annotation-types='point polygon rectangle') label(for='edit-fill') Fill select#edit-stroke(option='fill', format='boolean') diff --git a/examples/annotations/thumb.jpg b/examples/annotations/thumb.jpg new file mode 100755 index 0000000000..770976d234 Binary files /dev/null and b/examples/annotations/thumb.jpg differ diff --git a/src/annotation.js b/src/annotation.js index 6d4d898c2b..488b37def3 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -690,7 +690,11 @@ registerAnnotation('polygon', polygonAnnotation, polygonRequiredFeatures); * May specify: * style. * radius, fill, fillColor, fillOpacity, stroke, strokeWidth, strokeColor, - * strokeOpacity + * strokeOpacity, scaled + * + * If scaled is false, the point is not scaled with zoom level. If it is true, + * the radius is based on the zoom level at first instantiation. Otherwise, if + * it is a number, the radius is used at that zoom level. */ ///////////////////////////////////////////////////////////////////////////// var pointAnnotation = function (args) { @@ -698,12 +702,15 @@ var pointAnnotation = function (args) { if (!(this instanceof pointAnnotation)) { return new pointAnnotation(args); } + var m_this = this; + args = $.extend(true, {}, { style: { fill: true, fillColor: {r: 0, g: 1, b: 0}, fillOpacity: 0.25, radius: 10, + scaled: false, stroke: true, strokeColor: {r: 0, g: 0, b: 0}, strokeOpacity: 1, @@ -722,17 +729,36 @@ var pointAnnotation = function (args) { this.features = function () { var opt = this.options(), state = this.state(), - features; + features, style, scaleOnZoom; switch (state) { case annotationState.create: features = []; break; default: + style = opt.style; + if (opt.style.scaled || opt.style.scaled === 0) { + if (opt.style.scaled === true) { + opt.style.scaled = this.layer().map().zoom(); + } + style = $.extend({}, style, { + radius: function () { + var radius = opt.style.radius, + zoom = m_this.layer().map().zoom(); + if (util.isFunction(radius)) { + radius = radius.apply(this, arguments); + } + radius *= Math.pow(2, zoom - opt.style.scaled); + return radius; + } + }); + scaleOnZoom = true; + } features = [{ point: { x: opt.position.x, y: opt.position.y, - style: opt.style + style: style, + scaleOnZoom: scaleOnZoom } }]; break; @@ -786,7 +812,7 @@ var pointAnnotation = function (args) { * @return {array} a list of style names to store. */ this._geojsonStyles = function () { - return ['fill', 'fillColor', 'fillOpacity', 'radius', 'stroke', + return ['fill', 'fillColor', 'fillOpacity', 'radius', 'scaled', 'stroke', 'strokeColor', 'strokeOpacity', 'strokeWidth']; }; diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 5584ff2b64..5fdd69e972 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -57,6 +57,7 @@ var annotationLayer = function (args) { 'fillColor': {dataType: 'color', keys: ['fillColor', 'fill-color', 'marker-color', 'fill']}, 'fillOpacity': {dataType: 'opacity', keys: ['fillOpacity', 'fill-opacity']}, 'radius': {dataType: 'positive', keys: ['radius']}, + 'scaled': {dataType: 'booleanOrNumber', keys: ['scaled']}, 'stroke': {dataType: 'boolean', keys: ['stroke']}, 'strokeColor': {dataType: 'color', keys: ['strokeColor', 'stroke-color', 'stroke']}, 'strokeOpacity': {dataType: 'opacity', keys: ['strokeOpacity', 'stroke-opacity']}, @@ -546,8 +547,13 @@ var annotationLayer = function (args) { * 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. + * boolean: a string whose lowercase value is 'false', 'off', or 'no', and + * falsy values are false, all else is true. null and undefined are + * still considered invalid values. + * booleanOrNumber: a string whose lowercase value is 'false', 'off', no', + * '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. * @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 @@ -558,8 +564,18 @@ var annotationLayer = function (args) { return; } switch (dataType) { + case 'booleanOrNumber': + if ((!value && value !== 0) || ['true', 'false', 'off', 'on', 'no', 'yes'].indexOf(('' + value).toLowerCase()) >= 0) { + value = !!value && ['false', 'no', 'off'].indexOf(('' + value).toLowerCase()) < 0; + } else { + value = +value; + if (isNaN(value) || !isFinite(value)) { + return; + } + } + break; case 'boolean': - value = !!value && value !== 'false'; + value = !!value && ['false', 'no', 'off'].indexOf(('' + value).toLowerCase()) < 0; break; case 'color': value = util.convertColor(value); @@ -575,7 +591,7 @@ var annotationLayer = function (args) { break; case 'positive': value = +value; - if (isNaN(value) || value <= 0) { + if (isNaN(value) || !isFinite(value) || value <= 0) { return; } break; @@ -599,6 +615,7 @@ var annotationLayer = function (args) { $.each(m_features, function (idx, featureLevel) { $.each(featureLevel, function (type, feature) { feature.data = []; + delete feature.feature.scaleOnZoom; }); }); $.each(m_annotations, function (annotation_idx, annotation) { @@ -663,6 +680,9 @@ var annotationLayer = function (args) { } /* Collect the data for each feature */ m_features[idx][type].data.push(featureSpec.data || featureSpec); + if (featureSpec.scaleOnZoom) { + m_features[idx][type].feature.scaleOnZoom = true; + } }); }); }); @@ -677,6 +697,19 @@ var annotationLayer = function (args) { s_update.call(m_this, request); }; + /** + * Check if any features are marked that they need to be updated when a zoom + * occurs. If so, mark that feature as modified. + */ + this._handleZoom = function () { + var i, features = m_this.features(); + for (i = 0; i < features.length; i += 1) { + if (features[i].scaleOnZoom) { + features[i].modified(); + } + } + }; + /////////////////////////////////////////////////////////////////////////// /** * Initialize @@ -694,6 +727,8 @@ var annotationLayer = function (args) { m_this.geoOn(geo_event.mouseclick, m_this._handleMouseClick); m_this.geoOn(geo_event.mousemove, m_this._handleMouseMove); + m_this.geoOn(geo_event.zoom, m_this._handleZoom); + return m_this; }; diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index 23a8d2e577..a27a90eb43 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -379,7 +379,7 @@ describe('geo.annotation', function () { }); it('_geojsonStyles', function () { var ann = geo.annotation.pointAnnotation(); - expect(ann._geojsonStyles().length).toBe(8); + expect(ann._geojsonStyles().length).toBe(9); }); it('_geojsonCoordinates', function () { var ann = geo.annotation.pointAnnotation(); @@ -423,6 +423,30 @@ describe('geo.annotation', function () { expect(ann.options('position')).toEqual({x: 10, y: 20}); expect(ann.state()).toBe(geo.annotation.state.done); }); + it('scaled radius', function () { + var map = create_map(); + var layer = map.createLayer('annotation', { + annotations: ['point'] + }); + var ann = geo.annotation.pointAnnotation({ + position: point, layer: layer, style: {scaled: true}}); + var features = ann.features(); + expect(features.length).toBe(1); + expect(features[0].point.x).toEqual(point.x); + expect(features[0].point.style.radius()).toBe(10); + map.zoom(3); + features = ann.features(); + expect(features[0].point.style.radius()).toBe(5); + ann.options().style.radius = function () { + return map.zoom() > 6.5 ? 4 : 10; + }; + map.zoom(6); + features = ann.features(); + expect(features[0].point.style.radius()).toBe(40); + map.zoom(7); + features = ann.features(); + expect(features[0].point.style.radius()).toBe(32); + }); }); describe('annotation registry', function () { diff --git a/tests/cases/annotationLayer.js b/tests/cases/annotationLayer.js index 2e30e4ff64..d49ded4434 100644 --- a/tests/cases/annotationLayer.js +++ b/tests/cases/annotationLayer.js @@ -355,6 +355,20 @@ describe('geo.annotationLayer', function () { }); expect(layer.annotations()[0].options('vertices')[0]).not.toEqual(layer.annotations()[0].options('vertices')[1]); }); + it('_handleZoom', function () { + layer.mode(null); + layer.removeAllAnnotations(); + layer.addAnnotation(point); + layer._update(); + var mod = layer.features()[0].getMTime(); + layer._handleZoom(); + expect(layer.features()[0].getMTime()).toBe(mod); + layer.annotations()[0].options({style: {scaled: true}}); + layer._update(); + mod = layer.features()[0].getMTime(); + layer._handleZoom(); + expect(layer.features()[0].getMTime()).toBeGreaterThan(mod); + }); it('_processSelection', function () { layer.removeAllAnnotations(); layer._processSelection({ @@ -467,7 +481,8 @@ describe('geo.annotationLayer', function () { properties: { radius: -5, fillColor: 'no such color', - fillOpacity: -1 + fillOpacity: -1, + scaled: 'not a number' } }; layer.geojson(badattr, true); @@ -475,6 +490,7 @@ describe('geo.annotationLayer', function () { expect(attr.radius).toBeGreaterThan(0); expect(attr.fillOpacity).toBeGreaterThan(0); expect(attr.fillColor).toBe('#00ff00'); + expect(attr.scaled).toBe(false); var goodattr = { type: 'Feature', geometry: { @@ -484,7 +500,8 @@ describe('geo.annotationLayer', function () { properties: { radius: 3, fillColor: 'indigo', - fillOpacity: 0.3 + fillOpacity: 0.3, + scaled: 'On' } }; layer.geojson(goodattr, true); @@ -492,6 +509,7 @@ describe('geo.annotationLayer', function () { expect(attr.radius).toBe(3); expect(attr.fillOpacity).toBe(0.3); expect(attr.fillColor).toBe('#4b0082'); + expect(attr.scaled).toBe(4); }); }); it('Test destroy layer.', function () {