var inherit = require('./inherit');
var sceneObject = require('./sceneObject');
var feature = require('./feature');
var checkRenderer = require('./registry').checkRenderer;
var rendererForFeatures = require('./registry').rendererForFeatures;
var rendererForAnnotations = require('./registry').rendererForAnnotations;
/**
* Object specification for a layer.
*
* @typedef {object} geo.layer.spec
* @property {number} [id] The id of the layer. Defaults to a increasing
* sequence.
* @property {geo.map} [map=null] Parent map of the layer.
* @property {string|geo.renderer} [renderer] Renderer to associate with the
* layer. If not specified, either `annotations` or `features` can be used
* to determine the renderer. If a {@link geo.renderer} instance, the
* renderer is not recreated; not all renderers can be shared by multiple
* layers.
* @property {boolean|string} [autoshareRenderer=true] If truthy and the
* renderer supports it, auto-share renderers between layers. Currently,
* auto-sharing can only occur for webgl renderers and adjacent layers. If
* `true`, sharing will only occur if the layers have the same opacity and it
* is 1 or 0 and any tile layers are below non-tile layers. If `"more"`,
* sharing will occur for any adjacent layers that have the same opacity.
* Shared renderers has slightly different behavior than non-shared
* renderers: changing z-index may result in rerendering and be slightly
* slower; only one DOM canvas is used for all shared renderers. Some
* features have slight z-stacking differences in shared versus non-shared
* renderers.
* @property {HTMLElement} [canvas] If specified, use this canvas rather than
* a canvas associaied with the renderer directly. Renderers may not support
* sharing a canvas.
* @property {string[]|object} [annotations] A list of annotations that will be
* used on this layer, used to select a 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` more details. This is ignored if `renderer` is
* specified.
* @property {string[]} [features] A list of features that will be used on this
* layer, used to select a renderer. Features are the basic feature names
* (e.g., `'quad'`), or the feature name followed by a required capability
* (e.g., `'quad.image'`). This is ignored if `renderer` or `annotations` is
* specified.
* @property {boolean} [active=true] Truthy if the layer has the `active` css
* class and may receive native mouse events.
* @property {string} [attribution] An attribution string to display.
* @property {number} [opacity=1] The layer opacity on a scale of [0-1].
* @property {string} [name=''] A name for the layer for user convenience. If
* specified, this is also the `id` property of the containing DOM element.
* @property {boolean} [selectionAPI=true] Truthy if the layer can generate
* selection and other interaction events.
* @property {boolean} [sticky=true] Truthy if the layer should navigate with
* the map.
* @property {boolean} [visible=true] Truthy if the layer is visible.
* @property {number} [zIndex] The z-index to assign to the layer (defaults to
* the index of the layer inside the map).
*/
/**
* Create a new layer.
*
* @class
* @alias geo.layer
* @extends geo.sceneObject
* @param {geo.layer.spec} [arg] Specification for the new layer.
* @returns {geo.layer}
*/
var layer = function (arg) {
'use strict';
if (!(this instanceof layer)) {
return new layer(arg);
}
arg = arg || {};
sceneObject.call(this, arg);
var $ = require('jquery');
var timestamp = require('./timestamp');
var renderer = require('./renderer');
var createRenderer = require('./registry').createRenderer;
var adjustLayerForRenderer = require('./registry').adjustLayerForRenderer;
var geo_event = require('./event');
/**
* @private
*/
var m_this = this,
s_exit = this._exit,
m_id = arg.id === undefined ? layer.newLayerId() : arg.id,
m_name = arg.name === undefined ? '' : arg.name,
m_map = arg.map === undefined ? null : arg.map,
m_node = null,
m_canvas = arg.canvas === undefined ? null : arg.canvas,
m_renderer = arg.renderer instanceof renderer ? arg.renderer : null,
m_initialized = false,
m_rendererName = arg.renderer !== undefined ? (
arg.renderer instanceof renderer ? arg.renderer.api() : arg.renderer) : (
arg.annotations ? rendererForAnnotations(arg.annotations) :
rendererForFeatures(arg.features)),
m_autoshareRenderer = arg.autoshareRenderer === undefined ? true : arg.autoshareRenderer,
m_dataTime = timestamp(),
m_updateTime = timestamp(),
m_sticky = arg.sticky === undefined ? true : arg.sticky,
m_active = arg.active === undefined ? true : arg.active,
m_opacity = arg.opacity === undefined ? 1 : arg.opacity,
m_attribution = arg.attribution || null,
m_visible = arg.visible === undefined ? true : arg.visible,
m_selectionAPI = arg.selectionAPI === undefined ? true : arg.selectionAPI,
m_zIndex;
m_rendererName = checkRenderer(m_rendererName);
if (!m_map) {
throw new Error('Layers must be initialized on a map.');
}
/**
* Get a list of sibling layers. If no parent has been assigned to this
* layer, assume that the map will be the parent. This gets all of the
* parent's children that are layer instances.
*
* @returns {geo.layer[]} A list of sibling layers.
*/
function _siblingLayers() {
return (m_this.parent() || m_this.map()).children().filter(function (child) {
return child instanceof layer;
});
}
/**
* Get the name of the renderer.
*
* @returns {string}
*/
this.rendererName = function () {
return m_rendererName;
};
/**
* Get the setting of autoshareRenderer.
*
* @returns {boolean|string}
*/
this.autoshareRenderer = function () {
return m_autoshareRenderer;
};
/**
* Get or set the z-index of the layer. The z-index controls the display
* order of the layers in much the same way as the CSS z-index property.
*
* @param {number} [zIndex] The new z-index, or undefined to return the
* current z-index.
* @param {boolean} [allowDuplicate] When setting the z index, if this is
* truthy, allow other layers to have the same z-index. Otherwise,
* ensure that other layers have distinct z-indices from this one.
* @returns {number|this}
*/
this.zIndex = function (zIndex, allowDuplicate) {
if (zIndex === undefined) {
return m_zIndex;
}
if (!allowDuplicate) {
// if any extant layer has the same index, then we move all of those
// layers up. We do this in reverse order since, if two layers above
// this one share a z-index, they will resolve to the layer insert order.
_siblingLayers().reverse().forEach(function (child) {
if (child !== m_this && child.zIndex() === zIndex) {
child.zIndex(zIndex + 1);
}
});
}
if (zIndex !== m_zIndex) {
m_zIndex = zIndex;
m_node.css('z-index', m_zIndex);
m_this.geoTrigger(geo_event.layerMove, {
layer: m_this
});
}
return m_this;
};
/**
* Bring the layer above the given number of layers. This will rotate the
* current z-indices for this and the next `n` layers.
*
* @param {number} [n=1] The number of positions to move.
* @returns {this}
*/
this.moveUp = function (n) {
var order, i, me = null, tmp, sign;
// set the default
if (n === undefined) {
n = 1;
}
// set the sort direction that controls if we are moving up
// or down the z-index
sign = 1;
if (n < 0) {
sign = -1;
n = -n;
}
// get a sorted list of layers
order = _siblingLayers().sort(
function (a, b) { return sign * (a.zIndex() - b.zIndex()); }
);
for (i = 0; i < order.length; i += 1) {
if (me === null) {
// loop until we get to the current layer
if (order[i] === m_this) {
me = i;
}
} else if (i - me <= n) {
// swap the next n layers
tmp = m_this.zIndex();
m_this.zIndex(order[i].zIndex(), true);
order[i].zIndex(tmp, true);
} else {
// all the swaps are done now
break;
}
}
return m_this;
};
/**
* Bring the layer below the given number of layers. This will rotate the
* current z-indices for this and the previous `n` layers.
*
* @param {number} [n=1] The number of positions to move.
* @returns {this}
*/
this.moveDown = function (n) {
if (n === undefined) {
n = 1;
}
return m_this.moveUp(-n);
};
/**
* Bring the layer to the top of the map layers.
*
* @returns {this}
*/
this.moveToTop = function () {
return m_this.moveUp(_siblingLayers().length - 1);
};
/**
* Bring the layer to the bottom of the map layers.
*
* @returns {this}
*/
this.moveToBottom = function () {
return m_this.moveDown(_siblingLayers().length - 1);
};
/**
* Get whether or not the layer is sticky (navigates with the map).
*
* @returns {boolean}
*/
this.sticky = function () {
return m_sticky;
};
/**
* Get/Set whether or not the layer is active. An active layer will receive
* native mouse when the layer is on top. Non-active layers will never
* receive native mouse events.
*
* @param {boolean} [arg] If specified, the new `active` value.
* @returns {boolean|this}
*/
this.active = function (arg) {
if (arg === undefined) {
return m_active;
}
if (m_active !== arg) {
m_active = arg;
m_node.toggleClass('active', m_active);
}
return m_this;
};
/**
* Get root node of the layer.
*
* @returns {HTMLDivElement}
*/
this.node = function () {
return m_node;
};
/**
* Get/Set id of the layer.
*
* @param {string|null} [val] If `null`, generate a new layer id. Otherwise,
* if specified, the new id of the layer.
* @returns {string|this}
*/
this.id = function (val) {
if (val === undefined) {
return m_id;
}
m_id = val === null ? layer.newLayerId() : val;
m_this.modified();
return m_this;
};
/**
* Get/Set name of the layer.
*
* @param {string} [val] If specified, the new name of the layer.
* @returns {string|this}
*/
this.name = function (val) {
if (val === undefined) {
return m_name;
}
m_name = val;
m_node.attr('id', m_name);
m_this.modified();
return m_this;
};
/**
* Get the map associated with this layer.
*
* @returns {geo.map} The map associated with the layer.
*/
this.map = function () {
return m_map;
};
/**
* Get the renderer for the layer.
*
* @returns {geo.renderer} The renderer associated with the layer or `null`
* if there is no renderer.
*/
this.renderer = function () {
return m_renderer;
};
/**
* Get/Set the renderer for the layer.
*
* @param {boolean} [val] If specified, update the renderer value.
* Otherwise, return the current instance.
* @returns {geo.renderer|this} The renderer associated with the layer,
* `null`, or the current instance.
*/
this._renderer = function (val) {
if (val === undefined) {
return m_renderer;
}
m_renderer = val;
return m_this;
};
/**
* Get canvas of the layer.
*
* @returns {HTMLCanvasElement} The canvas element associated with the layer.
*/
this.canvas = function () {
return m_canvas;
};
/**
* Get/set the canvas of the layer.
*
* @param {boolean} [val] If specified, update the canvas value. Otherwise,
* return the current instance.
* @returns {HTMLCanvasElement|this} The canvas element associated with the
* layer or the current instance.
*/
this._canvas = function (val) {
if (val === undefined) {
return m_canvas;
}
m_canvas = val;
return m_this;
};
/**
* Return last time data got changed.
*
* @returns {geo.timestamp} The data time.
*/
this.dataTime = function () {
return m_dataTime;
};
/**
* Return the modified time for the last update that did something.
*
* @returns {geo.timestamp} The update time.
*/
this.updateTime = function () {
return m_updateTime;
};
/**
* Get/Set if the layer has been initialized.
*
* @param {boolean} [val] If specified, update the initialized value.
* Otherwise, return the current instance.
* @returns {boolean|this} Either the initialized value or this.
*/
this.initialized = function (val) {
if (val !== undefined) {
m_initialized = val;
return m_this;
}
return m_initialized;
};
/**
* Transform coordinates from world coordinates into a local coordinate
* system specific to the underlying renderer. This method is exposed
* to allow direct access the rendering context, but otherwise should
* not be called directly. The default implementation is the identity
* operator.
*
* @param {geo.geoPosition} input World coordinates.
* @returns {geo.geoPosition} Renderer coordinates.
*/
this.toLocal = function (input) {
return input;
};
/**
* Transform coordinates from a local coordinate system to world coordinates.
*
* @param {geo.geoPosition} input Renderer coordinates.
* @returns {geo.geoPosition} World coordinates.
*/
this.fromLocal = function (input) {
return input;
};
/**
* Get or set the attribution html content that will displayed with the
* layer. By default, nothing will be displayed. Note, this content is
* **not** html escaped, so care should be taken when rendering user provided
* content.
*
* @param {string?} arg An html fragment
* @returns {string|this} Chainable as a setter
*/
this.attribution = function (arg) {
if (arg !== undefined) {
m_attribution = arg;
m_this.map().updateAttribution();
return m_this;
}
return m_attribution;
};
/**
* Get/Set visibility of the layer.
*
* @param {boolean} [val] If specified, change the visibility. Otherwise,
* get it.
* @returns {boolean|this} either the visibility (if getting) or the layer
* (if setting).
*/
this.visible = function (val) {
if (val === undefined) {
return m_visible;
}
if (m_visible !== val) {
m_visible = val;
m_node.toggleClass('hidden', !m_visible);
m_this.modified();
}
return m_this;
};
/**
* Get/Set selectionAPI of the layer.
*
* @param {boolean} [val] If specified, set the selectionAPI state, otherwise
* return it.
* @returns {boolean|this} Either the selectionAPI state or the layer.
*/
this.selectionAPI = function (val) {
if (val === undefined) {
return m_selectionAPI;
}
if (m_selectionAPI !== val) {
m_selectionAPI = val;
}
return m_this;
};
/**
* Init layer.
*
* @param {boolean} noEvents If a subclass of this intends to bind the
* resize, pan, and zoom events itself, set this flag to true to avoid
* binding them here.
* @returns {this}
*/
this._init = function (noEvents) {
if (m_initialized) {
return m_this;
}
m_map.node().append(m_node);
/* Pass along the arguments, but not the map reference */
var options = $.extend({}, arg);
delete options.map;
if (m_renderer) {
m_this._canvas(m_renderer.canvas());
} else if (m_rendererName === null) {
// if given a "null" renderer, then pass the layer element as the canvas
m_this._renderer(null);
m_this._canvas(m_node);
} else if (m_canvas) { // Share context if we have valid one
m_this._renderer(createRenderer(m_rendererName, m_this, m_canvas, options));
} else {
m_this._renderer(createRenderer(m_rendererName, m_this, undefined, options));
m_this._canvas(m_renderer.canvas());
}
m_node.toggleClass('active', m_this.active());
m_initialized = true;
if (!noEvents) {
// Bind events to handlers
m_this.geoOn(geo_event.resize, function (event) {
m_this._update({event: event});
});
m_this.geoOn(geo_event.pan, function (event) {
m_this._update({event: event});
});
m_this.geoOn(geo_event.rotate, function (event) {
m_this._update({event: event});
});
m_this.geoOn(geo_event.zoom, function (event) {
m_this._update({event: event});
});
}
return m_this;
};
/**
* Clean up resources.
*/
this._exit = function () {
m_this.geoOff();
if (m_renderer) {
m_renderer._exit();
}
m_node.off();
m_node.remove();
arg = {};
m_this._canvas(null);
m_this._renderer(null);
s_exit();
};
/**
* Update layer.
*
* This is a stub that should be subclassed.
*
* @param {object} [arg] An object, possibly with an ``event`` key and value.
* @returns {this}
*/
this._update = function (arg) {
return m_this;
};
/**
* Return the width of the layer in pixels.
*
* @returns {number} The width of the parent map in pixels.
*/
this.width = function () {
return m_this.map().size().width;
};
/**
* Return the height of the layer in pixels.
*
* @returns {number} The height of the parent map in pixels.
*/
this.height = function () {
return m_this.map().size().height;
};
/**
* Get or set the current layer opacity. The opacity is in the range [0-1].
* An opacity of 0 is not the same as setting `visible(false)`, as
* interactions can still occur with the layer.
*
* @param {number} [opacity] If specified, set the opacity. Otherwise,
* return the opacity.
* @returns {number|this} The current opacity or the current layer.
*/
this.opacity = function (opacity) {
if (opacity !== undefined) {
m_opacity = opacity;
m_node.css('opacity', m_opacity);
return m_this;
}
return m_opacity;
};
// Create top level div for the layer
m_node = $(document.createElement('div'));
m_node.addClass('geojs-layer');
m_node.attr('renderer', m_rendererName);
if (m_name) {
m_node.attr('id', m_name);
}
m_this.opacity(m_opacity);
m_this.visible(m_visible);
// set the z-index (this prevents duplication)
if (arg.zIndex === undefined) {
var maxZ = -1;
_siblingLayers().forEach(function (child) {
if (child.zIndex() !== undefined) {
maxZ = Math.max(maxZ, child.zIndex());
}
});
arg.zIndex = maxZ + 1;
}
m_this.zIndex(arg.zIndex);
adjustLayerForRenderer('all', m_this);
return m_this;
};
/**
* Gets a new id number for a layer.
* @protected
* @instance
* @returns {number}
*/
layer.newLayerId = (function () {
'use strict';
var currentId = 1;
return function () {
var id = currentId;
currentId += 1;
return id;
};
}());
/**
* General object specification for feature types.
* @typedef {geo.layer.spec} geo.layer.createSpec
* @extends {geo.layer.spec}
* @property {string} [type='feature'] For feature compatibility with more than
* one kind of creatable layer
* @property {object[]} [data=[]] The default data array to apply to each
* feature if none exists.
* @property {string} [renderer='webgl'] The renderer to use.
* @property {geo.feature.spec[]} [features=[]] Features to add to the layer.
*/
/**
* Create a layer from an object. Any errors in the creation
* of the layer will result in returning null.
* @param {geo.map} map The map to add the layer to
* @param {geo.layer.createSpec} spec The layer specification.
* @returns {geo.layer|null}
*/
layer.create = function (map, spec) {
'use strict';
spec = spec || {};
spec.type = spec.type || 'feature';
spec.renderer = spec.renderer === undefined ? 'webgl' : spec.renderer;
spec.renderer = checkRenderer(spec.renderer);
if (!spec.renderer) {
console.warn('Invalid renderer');
return null;
}
var layer = map.createLayer(spec.type, spec);
if (!layer) {
console.warn('Unable to create a layer');
return null;
}
if (spec.features) {
// probably move this down to featureLayer eventually
spec.features.forEach(function (f) {
f.data = f.data || spec.data;
f.feature = feature.create(layer, f);
});
}
return layer;
};
inherit(layer, sceneObject);
module.exports = layer;