layer.js

  1. var inherit = require('./inherit');
  2. var sceneObject = require('./sceneObject');
  3. var feature = require('./feature');
  4. var checkRenderer = require('./registry').checkRenderer;
  5. var rendererForFeatures = require('./registry').rendererForFeatures;
  6. var rendererForAnnotations = require('./registry').rendererForAnnotations;
  7. /**
  8. * Object specification for a layer.
  9. *
  10. * @typedef {object} geo.layer.spec
  11. * @property {number} [id] The id of the layer. Defaults to a increasing
  12. * sequence.
  13. * @property {geo.map} [map=null] Parent map of the layer.
  14. * @property {string|geo.renderer} [renderer] Renderer to associate with the
  15. * layer. If not specified, either `annotations` or `features` can be used
  16. * to determine the renderer. If a {@link geo.renderer} instance, the
  17. * renderer is not recreated; not all renderers can be shared by multiple
  18. * layers.
  19. * @property {boolean|string} [autoshareRenderer=true] If truthy and the
  20. * renderer supports it, auto-share renderers between layers. Currently,
  21. * auto-sharing can only occur for webgl renderers and adjacent layers. If
  22. * `true`, sharing will only occur if the layers have the same opacity and it
  23. * is 1 or 0 and any tile layers are below non-tile layers. If `"more"`,
  24. * sharing will occur for any adjacent layers that have the same opacity.
  25. * Shared renderers has slightly different behavior than non-shared
  26. * renderers: changing z-index may result in rerendering and be slightly
  27. * slower; only one DOM canvas is used for all shared renderers. Some
  28. * features have slight z-stacking differences in shared versus non-shared
  29. * renderers.
  30. * @property {HTMLElement} [canvas] If specified, use this canvas rather than
  31. * a canvas associaied with the renderer directly. Renderers may not support
  32. * sharing a canvas.
  33. * @property {string[]|object} [annotations] A list of annotations that will be
  34. * used on this layer, used to select a renderer. Instead of a list, if
  35. * this is an object, the keys are the annotation names, and the values are
  36. * each a list of modes that will be used with that annotation. See
  37. * `featuresForAnnotations` more details. This is ignored if `renderer` is
  38. * specified.
  39. * @property {string[]} [features] A list of features that will be used on this
  40. * layer, used to select a renderer. Features are the basic feature names
  41. * (e.g., `'quad'`), or the feature name followed by a required capability
  42. * (e.g., `'quad.image'`). This is ignored if `renderer` or `annotations` is
  43. * specified.
  44. * @property {boolean} [active=true] Truthy if the layer has the `active` css
  45. * class and may receive native mouse events.
  46. * @property {string} [attribution] An attribution string to display.
  47. * @property {number} [opacity=1] The layer opacity on a scale of [0-1].
  48. * @property {string} [name=''] A name for the layer for user convenience. If
  49. * specified, this is also the `id` property of the containing DOM element.
  50. * @property {boolean} [selectionAPI=true] Truthy if the layer can generate
  51. * selection and other interaction events.
  52. * @property {boolean} [sticky=true] Truthy if the layer should navigate with
  53. * the map.
  54. * @property {boolean} [visible=true] Truthy if the layer is visible.
  55. * @property {number} [zIndex] The z-index to assign to the layer (defaults to
  56. * the index of the layer inside the map).
  57. */
  58. /**
  59. * Create a new layer.
  60. *
  61. * @class
  62. * @alias geo.layer
  63. * @extends geo.sceneObject
  64. * @param {geo.layer.spec} [arg] Specification for the new layer.
  65. * @returns {geo.layer}
  66. */
  67. var layer = function (arg) {
  68. 'use strict';
  69. if (!(this instanceof layer)) {
  70. return new layer(arg);
  71. }
  72. arg = arg || {};
  73. sceneObject.call(this, arg);
  74. var $ = require('jquery');
  75. var timestamp = require('./timestamp');
  76. var renderer = require('./renderer');
  77. var createRenderer = require('./registry').createRenderer;
  78. var adjustLayerForRenderer = require('./registry').adjustLayerForRenderer;
  79. var geo_event = require('./event');
  80. /**
  81. * @private
  82. */
  83. var m_this = this,
  84. s_exit = this._exit,
  85. m_id = arg.id === undefined ? layer.newLayerId() : arg.id,
  86. m_name = arg.name === undefined ? '' : arg.name,
  87. m_map = arg.map === undefined ? null : arg.map,
  88. m_node = null,
  89. m_canvas = arg.canvas === undefined ? null : arg.canvas,
  90. m_renderer = arg.renderer instanceof renderer ? arg.renderer : null,
  91. m_initialized = false,
  92. m_rendererName = arg.renderer !== undefined ? (
  93. arg.renderer instanceof renderer ? arg.renderer.api() : arg.renderer) : (
  94. arg.annotations ? rendererForAnnotations(arg.annotations) :
  95. rendererForFeatures(arg.features)),
  96. m_autoshareRenderer = arg.autoshareRenderer === undefined ? true : arg.autoshareRenderer,
  97. m_dataTime = timestamp(),
  98. m_updateTime = timestamp(),
  99. m_sticky = arg.sticky === undefined ? true : arg.sticky,
  100. m_active = arg.active === undefined ? true : arg.active,
  101. m_opacity = arg.opacity === undefined ? 1 : arg.opacity,
  102. m_attribution = arg.attribution || null,
  103. m_visible = arg.visible === undefined ? true : arg.visible,
  104. m_selectionAPI = arg.selectionAPI === undefined ? true : arg.selectionAPI,
  105. m_zIndex;
  106. m_rendererName = checkRenderer(m_rendererName);
  107. if (!m_map) {
  108. throw new Error('Layers must be initialized on a map.');
  109. }
  110. /**
  111. * Get a list of sibling layers. If no parent has been assigned to this
  112. * layer, assume that the map will be the parent. This gets all of the
  113. * parent's children that are layer instances.
  114. *
  115. * @returns {geo.layer[]} A list of sibling layers.
  116. */
  117. function _siblingLayers() {
  118. return (m_this.parent() || m_this.map()).children().filter(function (child) {
  119. return child instanceof layer;
  120. });
  121. }
  122. /**
  123. * Get the name of the renderer.
  124. *
  125. * @returns {string}
  126. */
  127. this.rendererName = function () {
  128. return m_rendererName;
  129. };
  130. /**
  131. * Get the setting of autoshareRenderer.
  132. *
  133. * @returns {boolean|string}
  134. */
  135. this.autoshareRenderer = function () {
  136. return m_autoshareRenderer;
  137. };
  138. /**
  139. * Get or set the z-index of the layer. The z-index controls the display
  140. * order of the layers in much the same way as the CSS z-index property.
  141. *
  142. * @param {number} [zIndex] The new z-index, or undefined to return the
  143. * current z-index.
  144. * @param {boolean} [allowDuplicate] When setting the z index, if this is
  145. * truthy, allow other layers to have the same z-index. Otherwise,
  146. * ensure that other layers have distinct z-indices from this one.
  147. * @returns {number|this}
  148. */
  149. this.zIndex = function (zIndex, allowDuplicate) {
  150. if (zIndex === undefined) {
  151. return m_zIndex;
  152. }
  153. if (!allowDuplicate) {
  154. // if any extant layer has the same index, then we move all of those
  155. // layers up. We do this in reverse order since, if two layers above
  156. // this one share a z-index, they will resolve to the layer insert order.
  157. _siblingLayers().reverse().forEach(function (child) {
  158. if (child !== m_this && child.zIndex() === zIndex) {
  159. child.zIndex(zIndex + 1);
  160. }
  161. });
  162. }
  163. if (zIndex !== m_zIndex) {
  164. m_zIndex = zIndex;
  165. m_node.css('z-index', m_zIndex);
  166. m_this.geoTrigger(geo_event.layerMove, {
  167. layer: m_this
  168. });
  169. }
  170. return m_this;
  171. };
  172. /**
  173. * Bring the layer above the given number of layers. This will rotate the
  174. * current z-indices for this and the next `n` layers.
  175. *
  176. * @param {number} [n] The number of positions to move.
  177. * @returns {this}
  178. */
  179. this.moveUp = function (n) {
  180. var order, i, me = null, tmp, sign;
  181. // set the default
  182. if (n === undefined) {
  183. n = 1;
  184. }
  185. // set the sort direction that controls if we are moving up
  186. // or down the z-index
  187. sign = 1;
  188. if (n < 0) {
  189. sign = -1;
  190. n = -n;
  191. }
  192. // get a sorted list of layers
  193. order = _siblingLayers().sort(
  194. function (a, b) { return sign * (a.zIndex() - b.zIndex()); }
  195. );
  196. for (i = 0; i < order.length; i += 1) {
  197. if (me === null) {
  198. // loop until we get to the current layer
  199. if (order[i] === m_this) {
  200. me = i;
  201. }
  202. } else if (i - me <= n) {
  203. // swap the next n layers
  204. tmp = m_this.zIndex();
  205. m_this.zIndex(order[i].zIndex(), true);
  206. order[i].zIndex(tmp, true);
  207. } else {
  208. // all the swaps are done now
  209. break;
  210. }
  211. }
  212. return m_this;
  213. };
  214. /**
  215. * Bring the layer below the given number of layers. This will rotate the
  216. * current z-indices for this and the previous `n` layers.
  217. *
  218. * @param {number} [n] The number of positions to move.
  219. * @returns {this}
  220. */
  221. this.moveDown = function (n) {
  222. if (n === undefined) {
  223. n = 1;
  224. }
  225. return m_this.moveUp(-n);
  226. };
  227. /**
  228. * Bring the layer to the top of the map layers.
  229. *
  230. * @returns {this}
  231. */
  232. this.moveToTop = function () {
  233. return m_this.moveUp(_siblingLayers().length - 1);
  234. };
  235. /**
  236. * Bring the layer to the bottom of the map layers.
  237. *
  238. * @returns {this}
  239. */
  240. this.moveToBottom = function () {
  241. return m_this.moveDown(_siblingLayers().length - 1);
  242. };
  243. /**
  244. * Get whether or not the layer is sticky (navigates with the map).
  245. *
  246. * @returns {boolean}
  247. */
  248. this.sticky = function () {
  249. return m_sticky;
  250. };
  251. /**
  252. * Get/Set whether or not the layer is active. An active layer will receive
  253. * native mouse when the layer is on top. Non-active layers will never
  254. * receive native mouse events.
  255. *
  256. * @param {boolean} [arg] If specified, the new `active` value.
  257. * @returns {boolean|this}
  258. */
  259. this.active = function (arg) {
  260. if (arg === undefined) {
  261. return m_active;
  262. }
  263. if (m_active !== arg) {
  264. m_active = arg;
  265. m_node.toggleClass('active', m_active);
  266. }
  267. return m_this;
  268. };
  269. /**
  270. * Get root node of the layer.
  271. *
  272. * @returns {HTMLDivElement}
  273. */
  274. this.node = function () {
  275. return m_node;
  276. };
  277. /**
  278. * Get/Set id of the layer.
  279. *
  280. * @param {string|null} [val] If `null`, generate a new layer id. Otherwise,
  281. * if specified, the new id of the layer.
  282. * @returns {string|this}
  283. */
  284. this.id = function (val) {
  285. if (val === undefined) {
  286. return m_id;
  287. }
  288. m_id = val === null ? layer.newLayerId() : val;
  289. m_this.modified();
  290. return m_this;
  291. };
  292. /**
  293. * Get/Set name of the layer.
  294. *
  295. * @param {string} [val] If specified, the new name of the layer.
  296. * @returns {string|this}
  297. */
  298. this.name = function (val) {
  299. if (val === undefined) {
  300. return m_name;
  301. }
  302. m_name = val;
  303. m_node.attr('id', m_name);
  304. m_this.modified();
  305. return m_this;
  306. };
  307. /**
  308. * Get the map associated with this layer.
  309. *
  310. * @returns {geo.map} The map associated with the layer.
  311. */
  312. this.map = function () {
  313. return m_map;
  314. };
  315. /**
  316. * Get the renderer for the layer.
  317. *
  318. * @returns {geo.renderer} The renderer associated with the layer or `null`
  319. * if there is no renderer.
  320. */
  321. this.renderer = function () {
  322. return m_renderer;
  323. };
  324. /**
  325. * Get/Set the renderer for the layer.
  326. *
  327. * @param {boolean} [val] If specified, update the renderer value.
  328. * Otherwise, return the current instance.
  329. * @returns {geo.renderer|this} The renderer associated with the layer,
  330. * `null`, or the current instance.
  331. */
  332. this._renderer = function (val) {
  333. if (val === undefined) {
  334. return m_renderer;
  335. }
  336. m_renderer = val;
  337. return m_this;
  338. };
  339. /**
  340. * Get canvas of the layer.
  341. *
  342. * @returns {HTMLCanvasElement} The canvas element associated with the layer.
  343. */
  344. this.canvas = function () {
  345. return m_canvas;
  346. };
  347. /**
  348. * Get/set the canvas of the layer.
  349. *
  350. * @param {boolean} [val] If specified, update the canvas value. Otherwise,
  351. * return the current instance.
  352. * @returns {HTMLCanvasElement|this} The canvas element associated with the
  353. * layer or the current instance.
  354. */
  355. this._canvas = function (val) {
  356. if (val === undefined) {
  357. return m_canvas;
  358. }
  359. m_canvas = val;
  360. return m_this;
  361. };
  362. /**
  363. * Return last time data got changed.
  364. *
  365. * @returns {geo.timestamp} The data time.
  366. */
  367. this.dataTime = function () {
  368. return m_dataTime;
  369. };
  370. /**
  371. * Return the modified time for the last update that did something.
  372. *
  373. * @returns {geo.timestamp} The update time.
  374. */
  375. this.updateTime = function () {
  376. return m_updateTime;
  377. };
  378. /**
  379. * Get/Set if the layer has been initialized.
  380. *
  381. * @param {boolean} [val] If specified, update the initialized value.
  382. * Otherwise, return the current instance.
  383. * @returns {boolean|this} Either the initialized value or this.
  384. */
  385. this.initialized = function (val) {
  386. if (val !== undefined) {
  387. m_initialized = val;
  388. return m_this;
  389. }
  390. return m_initialized;
  391. };
  392. /**
  393. * Transform coordinates from world coordinates into a local coordinate
  394. * system specific to the underlying renderer. This method is exposed
  395. * to allow direct access the rendering context, but otherwise should
  396. * not be called directly. The default implementation is the identity
  397. * operator.
  398. *
  399. * @param {geo.geoPosition} input World coordinates.
  400. * @returns {geo.geoPosition} Renderer coordinates.
  401. */
  402. this.toLocal = function (input) {
  403. return input;
  404. };
  405. /**
  406. * Transform coordinates from a local coordinate system to world coordinates.
  407. *
  408. * @param {geo.geoPosition} input Renderer coordinates.
  409. * @returns {geo.geoPosition} World coordinates.
  410. */
  411. this.fromLocal = function (input) {
  412. return input;
  413. };
  414. /**
  415. * Get or set the attribution html content that will displayed with the
  416. * layer. By default, nothing will be displayed. Note, this content is
  417. * **not** html escaped, so care should be taken when rendering user provided
  418. * content.
  419. *
  420. * @param {string?} arg An html fragment
  421. * @returns {string|this} Chainable as a setter
  422. */
  423. this.attribution = function (arg) {
  424. if (arg !== undefined) {
  425. m_attribution = arg;
  426. m_this.map().updateAttribution();
  427. return m_this;
  428. }
  429. return m_attribution;
  430. };
  431. /**
  432. * Get/Set visibility of the layer.
  433. *
  434. * @param {boolean} [val] If specified, change the visibility. Otherwise,
  435. * get it.
  436. * @returns {boolean|this} either the visibility (if getting) or the layer
  437. * (if setting).
  438. */
  439. this.visible = function (val) {
  440. if (val === undefined) {
  441. return m_visible;
  442. }
  443. if (m_visible !== val) {
  444. m_visible = val;
  445. m_node.toggleClass('hidden', !m_visible);
  446. m_this.modified();
  447. }
  448. return m_this;
  449. };
  450. /**
  451. * Get/Set selectionAPI of the layer.
  452. *
  453. * @param {boolean} [val] If specified, set the selectionAPI state, otherwise
  454. * return it.
  455. * @returns {boolean|this} Either the selectionAPI state or the layer.
  456. */
  457. this.selectionAPI = function (val) {
  458. if (val === undefined) {
  459. return m_selectionAPI;
  460. }
  461. if (m_selectionAPI !== val) {
  462. m_selectionAPI = val;
  463. }
  464. return m_this;
  465. };
  466. /**
  467. * Init layer.
  468. *
  469. * @param {boolean} noEvents If a subclass of this intends to bind the
  470. * resize, pan, and zoom events itself, set this flag to true to avoid
  471. * binding them here.
  472. * @returns {this}
  473. */
  474. this._init = function (noEvents) {
  475. if (m_initialized) {
  476. return m_this;
  477. }
  478. m_map.node().append(m_node);
  479. /* Pass along the arguments, but not the map reference */
  480. var options = Object.assign({}, arg);
  481. delete options.map;
  482. if (m_renderer) {
  483. m_this._canvas(m_renderer.canvas());
  484. } else if (m_rendererName === null) {
  485. // if given a "null" renderer, then pass the layer element as the canvas
  486. m_this._renderer(null);
  487. m_this._canvas(m_node);
  488. } else if (m_canvas) { // Share context if we have valid one
  489. m_this._renderer(createRenderer(m_rendererName, m_this, m_canvas, options));
  490. } else {
  491. m_this._renderer(createRenderer(m_rendererName, m_this, undefined, options));
  492. m_this._canvas(m_renderer.canvas());
  493. }
  494. m_node.toggleClass('active', m_this.active());
  495. m_initialized = true;
  496. if (!noEvents) {
  497. // Bind events to handlers
  498. m_this.geoOn(geo_event.resize, function (event) {
  499. m_this._update({event: event});
  500. });
  501. m_this.geoOn(geo_event.pan, function (event) {
  502. m_this._update({event: event});
  503. });
  504. m_this.geoOn(geo_event.rotate, function (event) {
  505. m_this._update({event: event});
  506. });
  507. m_this.geoOn(geo_event.zoom, function (event) {
  508. m_this._update({event: event});
  509. });
  510. }
  511. return m_this;
  512. };
  513. /**
  514. * Clean up resources.
  515. */
  516. this._exit = function () {
  517. m_this.geoOff();
  518. if (m_renderer) {
  519. m_renderer._exit();
  520. }
  521. m_node.off();
  522. m_node.remove();
  523. arg = {};
  524. m_this._canvas(null);
  525. m_this._renderer(null);
  526. s_exit();
  527. };
  528. /**
  529. * Update layer.
  530. *
  531. * This is a stub that should be subclassed.
  532. *
  533. * @param {object} [arg] An object, possibly with an ``event`` key and value.
  534. * @returns {this}
  535. */
  536. this._update = function (arg) {
  537. return m_this;
  538. };
  539. /**
  540. * Return the width of the layer in pixels.
  541. *
  542. * @returns {number} The width of the parent map in pixels.
  543. */
  544. this.width = function () {
  545. return m_this.map().size().width;
  546. };
  547. /**
  548. * Return the height of the layer in pixels.
  549. *
  550. * @returns {number} The height of the parent map in pixels.
  551. */
  552. this.height = function () {
  553. return m_this.map().size().height;
  554. };
  555. /**
  556. * Get or set the current layer opacity. The opacity is in the range [0-1].
  557. * An opacity of 0 is not the same as setting `visible(false)`, as
  558. * interactions can still occur with the layer.
  559. *
  560. * @param {number} [opacity] If specified, set the opacity. Otherwise,
  561. * return the opacity.
  562. * @returns {number|this} The current opacity or the current layer.
  563. */
  564. this.opacity = function (opacity) {
  565. if (opacity !== undefined) {
  566. m_opacity = opacity;
  567. m_node.css('opacity', m_opacity);
  568. return m_this;
  569. }
  570. return m_opacity;
  571. };
  572. // Create top level div for the layer
  573. m_node = $(document.createElement('div'));
  574. m_node.addClass('geojs-layer');
  575. m_node.attr('renderer', m_rendererName);
  576. if (m_name) {
  577. m_node.attr('id', m_name);
  578. }
  579. m_this.opacity(m_opacity);
  580. m_this.visible(m_visible);
  581. // set the z-index (this prevents duplication)
  582. if (arg.zIndex === undefined) {
  583. var maxZ = -1;
  584. _siblingLayers().forEach(function (child) {
  585. if (child.zIndex() !== undefined) {
  586. maxZ = Math.max(maxZ, child.zIndex());
  587. }
  588. });
  589. arg.zIndex = maxZ + 1;
  590. }
  591. m_this.zIndex(arg.zIndex);
  592. adjustLayerForRenderer('all', m_this);
  593. return m_this;
  594. };
  595. /**
  596. * Gets a new id number for a layer.
  597. * @protected
  598. * @instance
  599. * @returns {number}
  600. */
  601. layer.newLayerId = (function () {
  602. 'use strict';
  603. var currentId = 1;
  604. return function () {
  605. var id = currentId;
  606. currentId += 1;
  607. return id;
  608. };
  609. }());
  610. /**
  611. * General object specification for feature types.
  612. * @typedef {geo.layer.spec} geo.layer.createSpec
  613. * @extends {geo.layer.spec}
  614. * @property {string} [type='feature'] For feature compatibility with more than
  615. * one kind of creatable layer
  616. * @property {object[]} [data=[]] The default data array to apply to each
  617. * feature if none exists.
  618. * @property {string} [renderer='webgl'] The renderer to use.
  619. * @property {geo.feature.spec[]} [features=[]] Features to add to the layer.
  620. */
  621. /**
  622. * Create a layer from an object. Any errors in the creation
  623. * of the layer will result in returning null.
  624. * @param {geo.map} map The map to add the layer to
  625. * @param {geo.layer.createSpec} spec The layer specification.
  626. * @returns {geo.layer|null}
  627. */
  628. layer.create = function (map, spec) {
  629. 'use strict';
  630. spec = spec || {};
  631. spec.type = spec.type || 'feature';
  632. spec.renderer = spec.renderer === undefined ? 'webgl' : spec.renderer;
  633. spec.renderer = checkRenderer(spec.renderer);
  634. if (!spec.renderer) {
  635. console.warn('Invalid renderer');
  636. return null;
  637. }
  638. var layer = map.createLayer(spec.type, spec);
  639. if (!layer) {
  640. console.warn('Unable to create a layer');
  641. return null;
  642. }
  643. if (spec.features) {
  644. // probably move this down to featureLayer eventually
  645. spec.features.forEach(function (f) {
  646. f.data = f.data || spec.data;
  647. f.feature = feature.create(layer, f);
  648. });
  649. }
  650. return layer;
  651. };
  652. inherit(layer, sceneObject);
  653. module.exports = layer;