var geo_action = require('./action');
var util = require('./util/common');
var widgets = {
dom: {}
};
var layers = {};
var layerDefaultFeatures = {};
var renderers = {};
var features = {};
var featureCapabilities = {};
var fileReaders = {};
var rendererLayerAdjustments = {};
var annotations = {};
var registry = {};
/**
* Register a new file reader type.
*
* @alias geo.registerFileReader
* @param {string} name Name of the reader to register. If the name already
* exists, the class creation function is replaced.
* @param {function} func Class creation function.
*/
registry.registerFileReader = function (name, func) {
fileReaders[name] = func;
};
/**
* Create a new file reader.
*
* @alias geo.createFileReader
* @param {string} name Name of the reader to create.
* @param {object} opts Options for the new reader.
* @returns {geo.fileReader|null} The new reader or null if no such name is
* registered.
*/
registry.createFileReader = function (name, opts) {
if (fileReaders.hasOwnProperty(name)) {
return fileReaders[name](opts);
}
return null;
};
/**
* Register a new renderer type.
*
* @alias geo.registerRenderer
* @param {string} name Name of the renderer to register. If the name already
* exists, the class creation function is replaced.
* @param {function} func Class creation function.
*/
registry.registerRenderer = function (name, func) {
renderers[name] = func;
};
/**
* Create new instance of the renderer.
*
* @alias geo.createRenderer
* @param {string} name Name of the renderer to create.
* @param {geo.layer} layer The layer associated with the renderer.
* @param {HTMLCanvasElement} [canvas] A canvas object to share between
* renderers.
* @param {object} [options] Options for the new renderer.
* @returns {geo.renderer|null} The new renderer or null if no such name is
* registered.
*/
registry.createRenderer = function (name, layer, canvas, options) {
if (renderers.hasOwnProperty(name)) {
var ren = renderers[name](
{layer: layer, canvas: canvas, options: options}
);
ren._init();
return ren;
}
return null;
};
/**
* Check if the named renderer is supported. If not, display a warning and get
* the name of a fallback renderer. Ideally, we would pass a list of desired
* features, and, if the renderer is unavailable, this would choose a fallback
* that would support those features.
*
* @alias geo.checkRenderer
* @param {string|null} name Name of the desired renderer.
* @param {boolean} [noFallback] If truthy, don't recommend a fallback.
* @returns {string|null|false} The name of the renderer that should be used
* or false if no valid renderer can be determined.
*/
registry.checkRenderer = function (name, noFallback) {
if (name === null) {
return name;
}
if (renderers.hasOwnProperty(name)) {
var ren = renderers[name];
if (!ren.supported || ren.supported()) {
return ren.apiname || name;
}
if (!ren.fallback || noFallback) {
return false;
}
var fallback = registry.checkRenderer(ren.fallback(), true);
if (fallback !== false) {
console.warn(name + ' renderer is unavailable, using ' + fallback +
' renderer instead');
}
return fallback;
}
return false;
};
/**
* Check if there is a renderer that is supported and supports a list of
* features. If not, display a warning. This picks the first renderer that
* supports all of the listed features.
*
* @alias geo.rendererForFeatures
* @param {string[]|undefined} featureList A list of features that will be used
* with this renderer. Features are the basic feature names (e.g.,
* `'quad'`), or the feature name followed by a required capability (e.g.,
* `'quad.image'`). If more than one feature or more than one capability
* of a feature is required, include each feature and capability
* combination in the list (e.g., `['quad.image', 'plane']`). If no
* capability is specified for a feature (or that feature was registered
* without a capability object), then the feature will match regardless of
* capabilities.
* @returns {string|null|false} The name of the renderer that should be used
* or false if no valid renderer can be determined.
*/
registry.rendererForFeatures = function (featureList) {
var preferredRenderers = ['webgl', 'canvas', 'svg', 'vtkjs', null];
var renderer, ridx, feature, fidx, capability, available;
for (ridx = 0; ridx < preferredRenderers.length; ridx += 1) {
renderer = preferredRenderers[ridx];
if (registry.checkRenderer(renderer, true) === false) {
continue;
}
if (!featureList) {
return renderer;
}
if (!features[renderer]) {
continue;
}
available = true;
for (fidx = 0; fidx < featureList.length; fidx += 1) {
feature = featureList[fidx];
capability = null;
if (feature.indexOf('.') >= 0) {
capability = feature;
feature = feature.substr(0, feature.indexOf('.'));
}
if (features[renderer][feature] === undefined) {
available = false;
break;
}
if (capability && featureCapabilities[renderer][feature] &&
!featureCapabilities[renderer][feature][capability]) {
available = false;
break;
}
}
if (available) {
return renderer;
}
}
console.warn('There is no renderer available for the feature list "' +
(featureList || []).join(', ') + '".');
return false;
};
/**
* Register a new feature type.
*
* @alias geo.registerFeature
* @param {string} category The feature category -- this is the renderer name.
* @param {string} name The feature name.
* @param {function} func A function to call to create the feature.
* @param {object|undefined} capabilities A map of capabilities that this
* feature supports. If the feature is implemented with different
* capabilities in multiple categories (renderers), then the feature
* should expose a simple dictionary of supported and unsupported
* features. For instance, the quad feature has color quads, image quads,
* and image quads that support full transformations. The capabilities
* 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.
*/
registry.registerFeature = function (category, name, func, capabilities) {
if (!(category in features)) {
features[category] = {};
featureCapabilities[category] = {};
}
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;
};
/**
* Create new instance of a feature.
*
* @alias geo.createFeature
* @param {string} name Name of the feature to create.
* @param {geo.layer} layer The layer associated with the feature.
* @param {geo.renderer} renderer The renderer associated with the feature.
* This is usually `layer.renderer()`.
* @param {object} arg Options for the new feature.
* @returns {geo.feature|null} The new feature or null if no such name is
* registered.
*/
registry.createFeature = function (name, layer, renderer, arg) {
var category = renderer.api(),
options = {layer: layer, renderer: renderer};
if (category in features && name in features[category]) {
if (arg !== undefined) {
util.deepMerge(options, arg);
}
var feature = features[category][name](options);
if (layer.gcs === undefined) {
layer.gcs = function () {
return layer.map().gcs();
};
}
return feature;
}
return null;
};
/**
* Register a layer adjustment. Layer adjustments are appiled to specified
* layers when they are created as a method to mixin specific changes,
* usually based on the renderer used for that layer.
*
* @alias geo.registerLayerAdjustment
* @param {string} category The category for the adjustment; this is commonly
* the renderer name.
* @param {string} name The name of the adjustment; this is commonly the layer
* type, or `'all'` for the base layer class.
* @param {function} func The function to call when the adjustment is used.
* @returns {object} if this layer adjustment replaces an existing one, this
* was the value that was replaced. In this case, a warning is issued.
*/
registry.registerLayerAdjustment = function (category, name, func) {
if (!(category in rendererLayerAdjustments)) {
rendererLayerAdjustments[category] = {};
}
var old = rendererLayerAdjustments[category][name];
if (old) {
console.warn('The ' + category + '.' + name + ' layer adjustment is already registered');
}
rendererLayerAdjustments[category][name] = func;
return old;
};
/**
* If a layer needs to be adjusted based on the renderer, call the function
* that adjusts it.
*
* @alias geo.adjustLayerForRenderer
* @param {string} name Name of the layer or adjustment.
* @param {object} layer Instantiated layer object.
*/
registry.adjustLayerForRenderer = function (name, layer) {
var rendererName = layer.rendererName();
if (rendererName) {
if (rendererLayerAdjustments &&
rendererLayerAdjustments[rendererName] &&
rendererLayerAdjustments[rendererName][name]) {
rendererLayerAdjustments[rendererName][name].apply(layer);
}
}
};
/**
* Register a new layer type.
*
* @alias geo.registerLayer
* @param {string} name Name of the layer to register. If the name already
* exists, the class creation function is replaced.
* @param {function} func Class creation function.
* @param {string[]} [defaultFeatures] An optional list of feature capabilities
* that are required to use this layer.
*/
registry.registerLayer = function (name, func, defaultFeatures) {
layers[name] = func;
layerDefaultFeatures[name] = defaultFeatures;
};
/**
* Create new instance of the layer.
*
* @alias geo.createLayer
* @param {string} name Name of the layer to create.
* @param {geo.map} map The map class instance that owns the layer.
* @param {object} arg Options for the new layer.
* @returns {geo.layer|null} The new layer or null if no such name is
* registered.
*/
registry.createLayer = function (name, map, arg) {
// Default renderer is webgl
var options = {map: map},
layer = null;
if (name in layers) {
if (!arg.renderer && !arg.features && layerDefaultFeatures) {
options.features = layerDefaultFeatures[name];
}
if (arg !== undefined) {
const argdata = arg.data;
delete arg.data;
util.deepMerge(options, arg);
if (argdata) {
options.data = argdata;
}
}
layer = layers[name](options);
layer.layerName = name;
layer._init();
return layer;
} else {
return null;
}
};
/**
* Register a new widget type.
*
* @alias geo.registerWidget
* @param {string} category A category for this widget. This is usually
* `'dom'`.
* @param {string} name The name of the widget to register.
* @param {function} func Class creation function.
* @returns {object} If this widget replaces an existing one, this was the
* value that was replaced. In this case, a warning is issued.
*/
registry.registerWidget = function (category, name, func) {
if (!(category in widgets)) {
widgets[category] = {};
}
var old = widgets[category][name];
if (old) {
console.warn('The ' + category + '.' + name + ' widget is already registered');
}
widgets[category][name] = func;
return old;
};
/**
* Create new instance of a dom widget.
*
* @alias geo.createWidget
* @param {string} name Name of the widget to create.
* @param {geo.layer} layer The layer associated with the widget.
* @param {object} arg Options for the new widget.
* @returns {geo.widget} The new widget.
*/
registry.createWidget = function (name, layer, arg) {
var options = {
layer: layer
};
if (name in widgets.dom) {
if (arg !== undefined) {
util.deepMerge(options, arg);
}
return widgets.dom[name](options);
}
throw new Error('Cannot create unknown widget ' + name);
};
/**
* Register a new annotation type.
*
* @alias geo.registerAnnotation
* @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 appropriate 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.
*/
registry.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 || {}};
geo_action['annotation_' + name] = 'geo_annotation_' + name;
return old;
};
/**
* Create an annotation based on a registered type.
*
* @alias geo.createAnnotation
* @param {string} name The annotation name
* @param {object} options The options for the annotation.
* @returns {object} the new annotation.
*/
registry.createAnnotation = function (name, options) {
if (!annotations[name]) {
console.warn('The ' + name + ' annotation is not registered');
return;
}
var annotation = annotations[name].func(options);
return annotation;
};
/**
* Get a list of registered annotation types.
*
* @alias geo.listAnnotations
* @returns {string[]} A list of registered annotations.
*/
registry.listAnnotations = function () {
return Object.keys(annotations);
};
/**
* Get a list of required features for a set of annotations.
*
* @alias geo.featuresForAnnotations
* @param {string[]|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 that are needed to show the completed annotations.
* @returns {string[]} a list of features needed for the specified annotations.
* There may be duplicates in the list.
*/
registry.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 (annotations[ann].features[feature].includes(state)) {
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 these features.
*
* @alias geo.rendererForAnnotations
* @param {string[]|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.
* @returns {string|null|false} the name of the renderer that should be used or
* false if no valid renderer can be determined.
*/
registry.rendererForAnnotations = function (annotationList) {
return registry.rendererForFeatures(registry.featuresForAnnotations(annotationList));
};
/**
* Expose the various registries so that the can be examined to see what
* things are registered.
*
* @namespace geo.registries
*/
registry.registries = {
annotations: annotations,
features: features,
featureCapabilities: featureCapabilities,
fileReaders: fileReaders,
layers: layers,
renderers: renderers,
widgets: widgets
};
module.exports = registry;