Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an annotation registry. #616

Merged
merged 2 commits into from
Sep 28, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/annotations/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 10 additions & 1 deletion src/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var $ = require('jquery');
var inherit = require('./inherit');
var geo_event = require('./event');
var transform = require('./transform');
var registerAnnotation = require('./registry').registerAnnotation;

var annotationId = 0;

Expand All @@ -16,7 +17,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.
Expand Down Expand Up @@ -309,6 +311,8 @@ var rectangleAnnotation = function (args) {
};
inherit(rectangleAnnotation, annotation);

registerAnnotation('rectangle', rectangleAnnotation, {polygon: true});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1


/////////////////////////////////////////////////////////////////////////////
/**
* Polygon annotation class
Expand Down Expand Up @@ -504,6 +508,9 @@ var polygonAnnotation = function (args) {
};
inherit(polygonAnnotation, annotation);

registerAnnotation('polygon', polygonAnnotation, {
polygon: true, 'line.basic': [annotationState.create]});

/////////////////////////////////////////////////////////////////////////////
/**
* Point annotation class
Expand Down Expand Up @@ -598,6 +605,8 @@ var pointAnnotation = function (args) {
};
inherit(pointAnnotation, annotation);

registerAnnotation('point', pointAnnotation, {point: true});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only suggestion would is that features (keys) are not visible anywhere else. Should we create a list of it somewhere at the top may be? then you can say annotation.point: true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure having another list aids much here. There is no strict reason why an annotation should need to use the same feature that any other annotation used (I don't know what a heatmap annotation would look like, but we could have one).

I think if we wanted to make this more based on definitions, then we should probably add a general key to each base feature's capabilities map which contains the plain feature's name. Then we would import the base feature (not the renderer-specific feature) so that we could specify one of its capabilities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed a change which does this.

Copy link

@kotfic kotfic Sep 28, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@manthey Just as a side note, heatmap annotations look like the old-school MS Paint spray can tool. I've seen papers where this is used to allow users to generate distributions over vernacular geographic entities (e.g. downtown). There might actually be some useful applications of an annotation type like this (especially in medical imaging?)


module.exports = {
state: annotationState,
annotation: annotation,
Expand Down
5 changes: 4 additions & 1 deletion src/layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

//////////////////////////////////////////////////////////////////////////////
/**
Expand Down Expand Up @@ -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,
Expand Down
122 changes: 118 additions & 4 deletions src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var features = {};
var featureCapabilities = {};
var fileReaders = {};
var rendererLayerAdjustments = {};
var annotations = {};
var util = {};

//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
};

//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -207,15 +214,22 @@ 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) {
if (!(category in rendererLayerAdjustments)) {
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;
};

//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -276,15 +290,22 @@ 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) {
if (!(category in widgets)) {
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;
};

//////////////////////////////////////////////////////////////////////////////
Expand All @@ -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;
55 changes: 53 additions & 2 deletions tests/cases/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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');
});
});
});