diff --git a/examples/annotations/main.js b/examples/annotations/main.js index 4b9a900dc1..655d39dde6 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -46,7 +46,7 @@ $(function () { // create an annotation layer layer = map.createLayer('annotation', { renderer: query.renderer ? (query.renderer === 'html' ? null : query.renderer) : undefined, - features: query.renderer ? undefined : ['polygon', 'line', 'point'] + annotations: query.renderer ? undefined : geo.listAnnotations() }); // bind to the mouse click and annotation mode events layer.geoOn(geo.event.mouseclick, mouseClickToStart); diff --git a/src/annotation.js b/src/annotation.js index bc377a7247..5e9c6e0fcb 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -2,6 +2,10 @@ var $ = require('jquery'); var inherit = require('./inherit'); var geo_event = require('./event'); var transform = require('./transform'); +var registerAnnotation = require('./registry').registerAnnotation; +var lineFeature = require('./lineFeature'); +var pointFeature = require('./pointFeature'); +var polygonFeature = require('./polygonFeature'); var annotationId = 0; @@ -16,7 +20,8 @@ var annotationState = { * Base annotation class * * @class geo.annotation - * @param {string} type the type of annotation. + * @param {string} type the type of annotation. These should be registered + * with utils.registerAnnotation and can be listed with same function. * @param {object?} options Inidividual annotations have additional options. * @param {string} [options.name] A name for the annotation. This defaults to * the type with a unique ID suffixed to it. @@ -309,6 +314,10 @@ var rectangleAnnotation = function (args) { }; inherit(rectangleAnnotation, annotation); +var rectangleRequiredFeatures = {}; +rectangleRequiredFeatures[polygonFeature.capabilities.feature] = true; +registerAnnotation('rectangle', rectangleAnnotation, rectangleRequiredFeatures); + ///////////////////////////////////////////////////////////////////////////// /** * Polygon annotation class @@ -504,6 +513,11 @@ var polygonAnnotation = function (args) { }; inherit(polygonAnnotation, annotation); +var polygonRequiredFeatures = {}; +polygonRequiredFeatures[polygonFeature.capabilities.feature] = true; +polygonRequiredFeatures[lineFeature.capabilities.basic] = [annotationState.create]; +registerAnnotation('polygon', polygonAnnotation, polygonRequiredFeatures); + ///////////////////////////////////////////////////////////////////////////// /** * Point annotation class @@ -598,6 +612,10 @@ var pointAnnotation = function (args) { }; inherit(pointAnnotation, annotation); +var pointRequiredFeatures = {}; +pointRequiredFeatures[pointFeature.capabilities.feature] = true; +registerAnnotation('point', pointAnnotation, pointRequiredFeatures); + module.exports = { state: annotationState, annotation: annotation, diff --git a/src/layer.js b/src/layer.js index 4f0d962114..c8f22c4197 100644 --- a/src/layer.js +++ b/src/layer.js @@ -3,6 +3,7 @@ var sceneObject = require('./sceneObject'); var feature = require('./feature'); var checkRenderer = require('./registry').checkRenderer; var rendererForFeatures = require('./registry').rendererForFeatures; +var rendererForAnnotations = require('./registry').rendererForAnnotations; ////////////////////////////////////////////////////////////////////////////// /** @@ -45,7 +46,9 @@ var layer = function (arg) { m_canvas = null, m_renderer = null, m_initialized = false, - m_rendererName = arg.renderer === undefined ? rendererForFeatures(arg.features) : arg.renderer, + m_rendererName = arg.renderer !== undefined ? arg.renderer : ( + arg.annotations ? rendererForAnnotations(arg.annotations) : + rendererForFeatures(arg.features)), m_dataTime = timestamp(), m_updateTime = timestamp(), m_sticky = arg.sticky === undefined ? true : arg.sticky, diff --git a/src/lineFeature.js b/src/lineFeature.js index 0d352daf80..008a6bb91a 100644 --- a/src/lineFeature.js +++ b/src/lineFeature.js @@ -260,6 +260,8 @@ lineFeature.create = function (layer, spec) { }; lineFeature.capabilities = { + /* core feature name -- support in any manner */ + feature: 'line', /* support for solid-colored, constant-width lines */ basic: 'line.basic', /* support for lines that vary in width and color */ diff --git a/src/pointFeature.js b/src/pointFeature.js index c92b8c097b..c065c59546 100644 --- a/src/pointFeature.js +++ b/src/pointFeature.js @@ -435,5 +435,10 @@ pointFeature.create = function (layer, renderer, spec) { return feature.create(layer, spec); }; +pointFeature.capabilities = { + /* core feature name -- support in any manner */ + feature: 'point' +}; + inherit(pointFeature, feature); module.exports = pointFeature; diff --git a/src/polygonFeature.js b/src/polygonFeature.js index c89f3aa630..d9f6ba53dd 100644 --- a/src/polygonFeature.js +++ b/src/polygonFeature.js @@ -406,5 +406,10 @@ polygonFeature.create = function (layer, spec) { return feature.create(layer, spec); }; +polygonFeature.capabilities = { + /* core feature name -- support in any manner */ + feature: 'polygon' +}; + inherit(polygonFeature, feature); module.exports = polygonFeature; diff --git a/src/registry.js b/src/registry.js index 0310552f10..72a4f55c52 100644 --- a/src/registry.js +++ b/src/registry.js @@ -9,6 +9,7 @@ var features = {}; var featureCapabilities = {}; var fileReaders = {}; var rendererLayerAdjustments = {}; +var annotations = {}; var util = {}; ////////////////////////////////////////////////////////////////////////////// @@ -65,7 +66,7 @@ util.createRenderer = function (name, layer, canvas, options) { * that would support those features. * * @params {string|null} name name of the desired renderer - * @params {boolean} noFallack if true, don't recommend a fallback + * @params {boolean} noFallback if true, don't recommend a fallback * @return {string|null|false} the name of the renderer that should be used * or false if no valid renderer can be determined. */ @@ -168,6 +169,8 @@ util.rendererForFeatures = function (featureList) { * and image quads that support full transformations. The capabailities * should be defined in the base feature in a capabilities object so that * they can be referenced by that rather than an explicit string. + * @returns {object} if this feature replaces an existing one, this was the + * feature that was replaced. In this case, a warning is issued. */ ////////////////////////////////////////////////////////////////////////////// util.registerFeature = function (category, name, func, capabilities) { @@ -176,9 +179,13 @@ util.registerFeature = function (category, name, func, capabilities) { featureCapabilities[category] = {}; } - // TODO Add warning if the name already exists + var old = features[category][name]; + if (old) { + console.warn('The ' + category + '.' + name + ' feature is already registered'); + } features[category][name] = func; featureCapabilities[category][name] = capabilities; + return old; }; ////////////////////////////////////////////////////////////////////////////// @@ -207,6 +214,9 @@ util.createFeature = function (name, layer, renderer, arg) { ////////////////////////////////////////////////////////////////////////////// /** * Register a layer adjustment. + * + * @returns {object} if this layer adjustment replaces an existing one, this + * was the value that was replaced. In this case, a warning is issued. */ ////////////////////////////////////////////////////////////////////////////// util.registerLayerAdjustment = function (category, name, func) { @@ -214,8 +224,12 @@ util.registerLayerAdjustment = function (category, name, func) { rendererLayerAdjustments[category] = {}; } - // TODO Add warning if the name already exists + var old = rendererLayerAdjustments[category][name]; + if (old) { + console.warn('The ' + category + '.' + name + ' layer adjustment is already registered'); + } rendererLayerAdjustments[category][name] = func; + return old; }; ////////////////////////////////////////////////////////////////////////////// @@ -276,6 +290,9 @@ util.createLayer = function (name, map, arg) { ////////////////////////////////////////////////////////////////////////////// /** * Register a new widget type + * + * @returns {object} if this widget replaces an existing one, this was the + * value that was replaced. In this case, a warning is issued. */ ////////////////////////////////////////////////////////////////////////////// util.registerWidget = function (category, name, func) { @@ -283,8 +300,12 @@ util.registerWidget = function (category, name, func) { widgets[category] = {}; } - // TODO Add warning if the name already exists + var old = widgets[category][name]; + if (old) { + console.warn('The ' + category + '.' + name + ' widget is already registered'); + } widgets[category][name] = func; + return old; }; ////////////////////////////////////////////////////////////////////////////// @@ -308,4 +329,97 @@ util.createWidget = function (name, layer, arg) { throw new Error('Cannot create unknown widget ' + name); }; +////////////////////////////////////////////////////////////////////////////// +/** + * Register a new annotation type + * + * @param {string} name The annotation name + * @param {function} func A function to call to create the annotation. + * @param {object|undefined} features A map of features that are used by this + * annotation. Each key is a feature that is used. If the value is true, + * the that feature is always needed. If a list, then it is the set of + * annotation states for which that feature is required. These can be + * used to pick an pparopriate renderer when creating an annotation layer. + * @returns {object} if this annotation replaces an existing one, this was the + * value that was replaced. In this case, a warning is issued. + */ +////////////////////////////////////////////////////////////////////////////// +util.registerAnnotation = function (name, func, features) { + var old = annotations[name]; + if (old) { + console.warn('The ' + name + ' annotation is already registered'); + } + annotations[name] = {func: func, features: features || {}}; + return old; +}; + +////////////////////////////////////////////////////////////////////////////// +/** + * Get a list of registered annotation types. + * + * @return {array} a list of registered annotations. + */ +////////////////////////////////////////////////////////////////////////////// +util.listAnnotations = function () { + return Object.keys(annotations); +}; + +////////////////////////////////////////////////////////////////////////////// +/** + * Get a list of required features for a set of annotations. + * + * @param {array|object|undefined} annotationList A list of annotations that + * will be used. Instead of a list, if this is an object, the keys are the + * annotation names, and the values are each a list of modes that will be + * used with that annotation. For example, ['polygon', 'rectangle'] lists + * features required to show those annotations in any mode, whereas + * {polygon: [annotationState.done], rectangle: [annotationState.done]} only + * lists features thatre are needed to show the completed annotations. + * @return {array} a list of features needed for the specified annotations. + * There may be duplicates in the list. + */ +////////////////////////////////////////////////////////////////////////////// +util.featuresForAnnotations = function (annotationList) { + var features = []; + + var annList = Array.isArray(annotationList) ? annotationList : Object.keys(annotationList); + annList.forEach(function (ann) { + if (!annotations[ann]) { + return; + } + Object.keys(annotations[ann].features).forEach(function (feature) { + if (Array.isArray(annotationList) || annotationList[ann] === true || + !Array.isArray(annotations[ann].features[feature])) { + features.push(feature); + } else { + annotationList[ann].forEach(function (state) { + if ($.inArray(state, annotations[ann].features[feature]) >= 0) { + features.push(feature); + } + }); + } + }); + }); + return features; +}; + +////////////////////////////////////////////////////////////////////////////// +/** + * Check if there is a renderer that is supported and supports a list of + * annotations. If not, display a warning. This generates a list of required + * features, then picks the first renderer that supports all of thse features. + * + * @param {array|object|undefined} annotationList A list of annotations that + * will be used with this renderer. Instead of a list, if this is an object, + * the keys are the annotation names, and the values are each a list of modes + * that will be used with that annotation. See featuresForAnnotations for + * more details. + * @return {string|null|false} the name of the renderer that should be used or + * false if no valid renderer can be determined. + */ +////////////////////////////////////////////////////////////////////////////// +util.rendererForAnnotations = function (annotationList) { + return util.rendererForFeatures(util.featuresForAnnotations(annotationList)); +}; + module.exports = util; diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index d040a6e742..496c1395f8 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -44,7 +44,7 @@ describe('geo.annotation', function () { expect(ann.geojson()).toBe('not implemented'); map = create_map(); layer = map.createLayer('annotation', { - features: ['polygon', 'line', 'point'] + annotations: geo.listAnnotations() }); ann = geo.annotation.annotation('test2', { layer: layer, @@ -198,7 +198,7 @@ describe('geo.annotation', function () { it('mouseClick', function () { var map = create_map(); var layer = map.createLayer('annotation', { - features: ['polygon', 'line'] + annotations: ['polygon'] }); var ann = geo.annotation.polygonAnnotation({layer: layer}); var time = new Date().getTime(); @@ -303,4 +303,55 @@ describe('geo.annotation', function () { expect(ann.state()).toBe(geo.annotation.state.done); }); }); + + describe('annotation registry', function () { + it('listAnnotations', function () { + var list = geo.listAnnotations(); + expect($.inArray('rectangle', list) >= 0).toBe(true); + expect($.inArray('polygon', list) >= 0).toBe(true); + expect($.inArray('point', list) >= 0).toBe(true); + expect($.inArray('unknown', list) >= 0).toBe(false); + }); + it('registerAnnotation', function () { + var func = function () {}; + 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('featuresForAnnotations', function () { + var features = geo.featuresForAnnotations(['polygon']); + expect($.inArray('polygon', features) >= 0).toBe(true); + expect($.inArray('line.basic', features) >= 0).toBe(true); + expect($.inArray('point', features) >= 0).toBe(false); + features = geo.featuresForAnnotations({polygon: true}); + expect($.inArray('polygon', features) >= 0).toBe(true); + expect($.inArray('line.basic', features) >= 0).toBe(true); + expect($.inArray('point', features) >= 0).toBe(false); + features = geo.featuresForAnnotations({polygon: [geo.annotation.state.done]}); + expect($.inArray('polygon', features) >= 0).toBe(true); + expect($.inArray('line.basic', features) >= 0).toBe(false); + expect($.inArray('point', features) >= 0).toBe(false); + features = geo.featuresForAnnotations({polygon: [geo.annotation.state.done, geo.annotation.state.create]}); + expect($.inArray('polygon', features) >= 0).toBe(true); + expect($.inArray('line.basic', features) >= 0).toBe(true); + expect($.inArray('point', features) >= 0).toBe(false); + features = geo.featuresForAnnotations(['polygon', 'point']); + expect($.inArray('polygon', features) >= 0).toBe(true); + expect($.inArray('line.basic', features) >= 0).toBe(true); + expect($.inArray('point', features) >= 0).toBe(true); + features = geo.featuresForAnnotations(['polygon', 'unknown']); + expect($.inArray('polygon', features) >= 0).toBe(true); + expect($.inArray('line.basic', features) >= 0).toBe(true); + expect($.inArray('point', features) >= 0).toBe(false); + }); + it('rendererForAnnotations', function () { + expect(geo.rendererForAnnotations(['polygon'])).toBe('vgl'); + expect(geo.rendererForAnnotations(['point'])).toBe('vgl'); + geo.gl.vglRenderer.supported = function () { return false; }; + expect(geo.rendererForAnnotations(['polygon'])).toBe(false); + expect(geo.rendererForAnnotations(['point'])).toBe('d3'); + }); + }); });