Reusable Accessible Mapping Platform

API Docs for: 5.0.0
Show:

File: src\js\RAMP\Modules\layerLoader.js

  1. /* global define, console, RAMP, $ */
  2.  
  3. /**
  4. *
  5. *
  6. * @module RAMP
  7. * @submodule Map
  8. */
  9.  
  10. /**
  11. * Layer Loader class.
  12. *
  13. * Handles the asynchronous loading of map layers (excluding basemaps)
  14. * This includes dealing with errors, and raising appropriate events when the layer loads
  15. *
  16. * @class LayerLoader
  17. * @static
  18. * @uses dojo/topic
  19. * @uses dojo/_base/array
  20. * @uses esri/geometry/Extent
  21. * @uses esri/layers/GraphicsLayer
  22. * @uses esri/tasks/GeometryService
  23. * @uses esri/tasks/ProjectParameters
  24. * @uses EventManager
  25. * @uses FeatureClickHandler
  26. * @uses FilterManager
  27. * @uses GlobalStorage
  28. * @uses LayerItem
  29. * @uses Map
  30. * @uses MapClickHandler
  31. * @uses Ramp
  32. * @uses Util
  33. */
  34.  
  35. define([
  36. /* Dojo */
  37. "dojo/topic", "dojo/_base/array",
  38.  
  39. /* ESRI */
  40. "esri/layers/GraphicsLayer", "esri/tasks/GeometryService", "esri/tasks/ProjectParameters", "esri/geometry/Extent",
  41.  
  42. /* RAMP */
  43. "ramp/eventManager", "ramp/map", "ramp/globalStorage", "ramp/featureClickHandler", "ramp/mapClickHandler", "ramp/ramp",
  44. "ramp/filterManager", "ramp/layerItem",
  45.  
  46. /* Util */
  47. "utils/util"],
  48.  
  49. function (
  50. /* Dojo */
  51. topic, dojoArray,
  52.  
  53. /* ESRI */
  54. GraphicsLayer, GeometryService, ProjectParameters, EsriExtent,
  55.  
  56. /* RAMP */
  57. EventManager, RampMap, GlobalStorage, FeatureClickHandler, MapClickHandler, Ramp,
  58. FilterManager, LayerItem,
  59.  
  60. /* Util */
  61. UtilMisc) {
  62. "use strict";
  63.  
  64. var idCounter = 0;
  65.  
  66. /**
  67. * Get an auto-generated layer id. This works because javascript is single threaded: if this gets called
  68. * from a web-worker at some point it will need to be synchronized.
  69. *
  70. * @returns {String} an auto-generated layer id
  71. */
  72. function nextId() {
  73. idCounter += 1;
  74. return 'rampAutoId_' + idCounter;
  75. }
  76.  
  77. /**
  78. * Will set a layerId's layer selector state to a new state.
  79. *
  80. * @method onLayerError
  81. * @private
  82. * @param {String} layerId config id of the layer
  83. * @param {String} newState the state to set the layer to in the layer selector
  84. * @param {Boolean} abortIfError if true, don't update state if current state is an error state
  85. */
  86. function updateLayerSelectorState(layerId, newState, abortIfError) {
  87. if (abortIfError) {
  88. var layerState;
  89. layerState = FilterManager.getLayerState(layerId);
  90.  
  91. //check if this layer is in an error state. if so, exit the function
  92. if (layerState === LayerItem.state.ERROR) {
  93. return;
  94. }
  95. }
  96.  
  97. //set layer selector to new state
  98. FilterManager.setLayerState(layerId, newState);
  99. }
  100.  
  101. /**
  102. * Will remove a layer from the map, and adjust counts.
  103. *
  104. * @method onLayerError
  105. * @private
  106. * @param {String} layerId config id of the layer
  107. */
  108. function removeFromMap(layerId) {
  109. var map = RampMap.getMap(),
  110. bbLayer = RampMap.getBoundingBoxMapping()[layerId],
  111. layer = map._layers[layerId];
  112.  
  113. if (layer) {
  114. map.removeLayer(layer);
  115. } else {
  116. //layer was kicked out of the map. grab it from the registry
  117. layer = RAMP.layerRegistry[layerId];
  118. }
  119.  
  120. //if bounding box exists, and is in the map, remove it too
  121. if (bbLayer) {
  122. if (map._layers[bbLayer.id]) {
  123. map.removeLayer(bbLayer);
  124. RAMP.layerCounts.bb -= 1;
  125. }
  126. }
  127.  
  128. //just incase its really weird and layer is not in the registry
  129. if (layer) {
  130. //adjust layer counts
  131. switch (layer.ramp.type) {
  132. case GlobalStorage.layerType.wms:
  133. if (layer.ramp.load.inCount) {
  134. RAMP.layerCounts.wms -= 1;
  135. layer.ramp.load.inCount = false;
  136. }
  137. break;
  138.  
  139. case GlobalStorage.layerType.feature:
  140. case GlobalStorage.layerType.Static:
  141.  
  142. if (layer.ramp.load.inCount) {
  143. RAMP.layerCounts.feature -= 1;
  144. layer.ramp.load.inCount = false;
  145. }
  146. break;
  147. }
  148. }
  149. }
  150.  
  151. /**
  152. * This function initiates the loading of an ESRI layer object to the map.
  153. * Will add it to the map in the appropriate spot, wire up event handlers, and generate any bounding box layers
  154. * Note: a point of confusion. The layer objects "load" event may have already finished by the time this function is called.
  155. * This means the object's constructor has initialized itself with the layers data source.
  156. * This functions is not event triggered to guarantee the order in which things are added.
  157. *
  158. * @method _loadLayer
  159. * @private
  160. * @param {Object} layer an instantiated, unloaded ESRI layer object
  161. * @param {Integer} reloadIndex Optional. If reloading a layer, supply the index it should reside at. Do not set for new layers
  162. */
  163. function _loadLayer(layer, reloadIndex) {
  164. var insertIdx,
  165. layerSection,
  166. map = RampMap.getMap(),
  167. layerConfig = layer.ramp.config,
  168. lsState;
  169.  
  170. if (!layer.ramp) {
  171. console.log('you failed to supply a ramp.type to the layer!');
  172. }
  173.  
  174. //derive section
  175. switch (layer.ramp.type) {
  176. case GlobalStorage.layerType.wms:
  177. layerSection = GlobalStorage.layerType.wms;
  178. if (UtilMisc.isUndefined(reloadIndex)) {
  179. insertIdx = RAMP.layerCounts.base + RAMP.layerCounts.wms;
  180.  
  181. // generate wms legend image url and store in the layer config
  182. if (layerConfig.legendMimeType) {
  183. layer.ramp.config.legend = {
  184. type: "wms",
  185. imageUrl: String.format("{0}?SERVICE=WMS&REQUEST=GetLegendGraphic&TRANSPARENT=true&VERSION=1.1.1&FORMAT={2}&LAYER={3}",
  186. layerConfig.url,
  187. layer.version,
  188. layerConfig.legendMimeType,
  189. layerConfig.layerName
  190. )
  191. };
  192. }
  193. } else {
  194. insertIdx = reloadIndex;
  195. }
  196.  
  197. RAMP.layerCounts.wms += 1;
  198. layer.ramp.load.inCount = true;
  199.  
  200. break;
  201.  
  202. case GlobalStorage.layerType.feature:
  203. case GlobalStorage.layerType.Static:
  204. layerSection = GlobalStorage.layerType.feature;
  205. if (UtilMisc.isUndefined(reloadIndex)) {
  206. //NOTE: these static layers behave like features, in that they can be in any position and be re-ordered.
  207. insertIdx = RAMP.layerCounts.feature;
  208. } else {
  209. insertIdx = reloadIndex;
  210. }
  211. RAMP.layerCounts.feature += 1;
  212. layer.ramp.load.inCount = true;
  213.  
  214. break;
  215. }
  216.  
  217. //derive initial state
  218. switch (layer.ramp.load.state) {
  219. case "loaded":
  220. lsState = LayerItem.state.LOADED;
  221. break;
  222. case "loading":
  223. lsState = LayerItem.state.LOADING;
  224. break;
  225. case "error":
  226. lsState = LayerItem.state.ERROR;
  227. break;
  228. }
  229.  
  230. //add entry to layer selector
  231. if (UtilMisc.isUndefined(reloadIndex)) {
  232. FilterManager.addLayer(layerSection, layer.ramp.config, lsState);
  233. } else {
  234. updateLayerSelectorState(layerConfig.id, lsState);
  235. }
  236. layer.ramp.load.inLS = true;
  237.  
  238. //sometimes the ESRI api will kick a layer out of the map if it errors after the add process.
  239. //store a pointer here so we can find it (and it's information)
  240. RAMP.layerRegistry[layer.id] = layer;
  241.  
  242. //add layer to map, triggering the loading process. should add at correct position
  243. map.addLayer(layer, insertIdx);
  244.  
  245. //this will force a recreation of the highlighting graphic group.
  246. //if not done, can cause mouse interactions to get messy if adding more than
  247. //one layer at one time
  248. topic.publish(EventManager.Map.REORDER_END);
  249.  
  250. //wire up event handlers to the layer
  251. switch (layer.ramp.type) {
  252. case GlobalStorage.layerType.wms:
  253.  
  254. // WMS binding for getFeatureInfo calls
  255. if (!UtilMisc.isUndefined(layerConfig.featureInfo)) {
  256. MapClickHandler.registerWMSClick({ wmsLayer: layer, layerConfig: layerConfig });
  257. }
  258.  
  259. break;
  260.  
  261. case GlobalStorage.layerType.feature:
  262.  
  263. //TODO consider the case where a layer was loaded by the user, and we want to disable things like maptips?
  264.  
  265. //wire up click handler
  266. layer.on("click", function (evt) {
  267. evt.stopImmediatePropagation();
  268. FeatureClickHandler.onFeatureSelect(evt);
  269. });
  270.  
  271. //wire up mouse over / mouse out handler
  272. layer.on("mouse-over", function (evt) {
  273. FeatureClickHandler.onFeatureMouseOver(evt);
  274. });
  275.  
  276. layer.on("mouse-out", function (evt) {
  277. FeatureClickHandler.onFeatureMouseOut(evt);
  278. });
  279.  
  280. //generate bounding box
  281. //if a reload, the bounding box still exists from the first load
  282. if (UtilMisc.isUndefined(reloadIndex)) {
  283. var boundingBoxExtent,
  284. boundingBox = new GraphicsLayer({
  285. id: String.format("boundingBoxLayer_{0}", layer.id),
  286. visible: layerConfig.settings.boundingBoxVisible
  287. });
  288.  
  289. boundingBox.ramp = { type: GlobalStorage.layerType.BoundingBox };
  290.  
  291. //TODO test putting this IF before the layer creation, see what breaks. ideally if there is no box, we should not make a layer
  292. if (!UtilMisc.isUndefined(layerConfig.layerExtent)) {
  293. boundingBoxExtent = new EsriExtent(layerConfig.layerExtent);
  294.  
  295. if (UtilMisc.isSpatialRefEqual(boundingBoxExtent.spatialReference, map.spatialReference)) {
  296. //layer is in same projection as basemap. can directly use the extent
  297. boundingBox.add(UtilMisc.createGraphic(boundingBoxExtent));
  298. } else {
  299. //layer is in different projection. reproject to basemap
  300.  
  301. var box = RampMap.localProjectExtent(boundingBoxExtent, map.spatialReference);
  302. boundingBox.add(UtilMisc.createGraphic(box));
  303.  
  304. //Geometry Service Version. Makes a more accurate bounding box, but requires an arcserver
  305. /*
  306. var params = new ProjectParameters(),
  307. gsvc = new GeometryService(RAMP.config.geometryServiceUrl);
  308. params.geometries = [boundingBoxExtent];
  309. params.outSR = map.spatialReference;
  310.  
  311. gsvc.project(params, function (projectedExtents) {
  312. console.log('esri says: ' + JSON.stringify(projectedExtents[0]));
  313. console.log('proj4 says: ' + JSON.stringify(box));
  314. });
  315. */
  316. }
  317. }
  318.  
  319. //add mapping to bounding box
  320. RampMap.getBoundingBoxMapping()[layer.id] = boundingBox;
  321.  
  322. //bounding boxes are on top of feature layers
  323. insertIdx = RAMP.layerCounts.feature + RAMP.layerCounts.bb;
  324. RAMP.layerCounts.bb += 1;
  325.  
  326. map.addLayer(boundingBox, insertIdx);
  327. }
  328. break;
  329. }
  330. }
  331.  
  332. return {
  333. /**
  334. * Initializes properties. Set up event listeners
  335. *
  336. * @method init
  337. */
  338. init: function () {
  339. //counters for layers loaded, so we know where to insert things
  340. //default basemap count to 1, as we always load 1 to begin with
  341.  
  342. RAMP.layerCounts = {
  343. feature: 0,
  344. bb: 0,
  345. wms: 0,
  346. base: 1
  347. };
  348.  
  349. RAMP.layerRegistry = {};
  350.  
  351. topic.subscribe(EventManager.LayerLoader.LAYER_LOADED, this.onLayerLoaded);
  352. topic.subscribe(EventManager.LayerLoader.LAYER_UPDATED, this.onLayerUpdateEnd);
  353. topic.subscribe(EventManager.LayerLoader.LAYER_UPDATING, this.onLayerUpdateStart);
  354. topic.subscribe(EventManager.LayerLoader.LAYER_ERROR, this.onLayerError);
  355. topic.subscribe(EventManager.LayerLoader.REMOVE_LAYER, this.onLayerRemove);
  356. topic.subscribe(EventManager.LayerLoader.RELOAD_LAYER, this.onLayerReload);
  357. },
  358.  
  359. /**
  360. * Deals with a layer that had an error when it tried to load.
  361. *
  362. * @method onLayerError
  363. * @param {Object} evt
  364. * @param {Object} evt.layer the layer object that failed
  365. * @param {Object} evt.error the error object
  366. */
  367. onLayerError: function (evt) {
  368. console.log("failed to load layer " + evt.layer.url);
  369. console.log(evt.error.message);
  370.  
  371. evt.layer.ramp.load.state = "error";
  372.  
  373. var layerId = evt.layer.id;
  374.  
  375. //get that failed layer outta here
  376. removeFromMap(layerId);
  377.  
  378. //if layer is in layer selector, update the status
  379. if (evt.layer.ramp.load.inLS) {
  380. updateLayerSelectorState(evt.layer.ramp.config.id, LayerItem.state.ERROR, false);
  381. }
  382. },
  383.  
  384. /**
  385. * Reacts when a layer begins to update. This happens when a feature layer starts to download its data.
  386. * Data download doesn't start until points are made visible. It also happens when a WMS requests a new picture.
  387. *
  388. * @method onLayerUpdateStart
  389. * @param {Object} evt
  390. * @param {Object} evt.layer the layer object that loaded
  391. */
  392. onLayerUpdateStart: function (evt) {
  393. //console.log("LAYER UPDATE START: " + evt.layer.url);
  394. updateLayerSelectorState(evt.layer.ramp.config.id, LayerItem.state.UPDATING, true);
  395. },
  396.  
  397. /**
  398. * Reacts when a layer has updated successfully. This means the layer has pulled its data and displayed it.
  399. *
  400. * @method onLayerUpdateEnd
  401. * @param {Object} evt
  402. * @param {Object} evt.layer the layer object that loaded
  403. */
  404. onLayerUpdateEnd: function (evt) {
  405. updateLayerSelectorState(evt.layer.ramp.config.id, LayerItem.state.LOADED, true);
  406. },
  407.  
  408. /**
  409. * Reacts when a layer has loaded successfully. This means the site has shaken hands with the layer and it seems ok.
  410. * This does not mean data has been downloaded
  411. *
  412. * @method onLayerLoaded
  413. * @param {Object} evt
  414. * @param {Object} evt.layer the layer object that loaded
  415. */
  416. onLayerLoaded: function (evt) {
  417. //set state to loaded
  418. //if we have a row in layer selector, update it to loaded (unless already in error)
  419.  
  420. //set flags that we have loaded ok
  421. evt.layer.ramp.load.state = "loaded";
  422.  
  423. //if a row already exists in selector, set it to LOADED state. (unless already in error state)
  424. if (evt.layer.ramp.load.inLS) {
  425. updateLayerSelectorState(evt.layer.ramp.config.id, LayerItem.state.LOADED, true);
  426. }
  427.  
  428. console.log("layer loaded: " + evt.layer.url);
  429. },
  430.  
  431. /**
  432. * Reacts to a request for a layer to be removed. Usually the case when a layer errors and the user clicks remove.
  433. *
  434. * @method onLayerRemove
  435. * @param {Object} evt
  436. * @param {Object} evt.layerId the layer id to be removed
  437. */
  438. onLayerRemove: function (evt) {
  439. var map = RampMap.getMap(),
  440. layer,
  441. configIdx,
  442. layerSection,
  443. configCollection;
  444.  
  445. layer = map._layers[evt.layerId]; //map.getLayer is not reliable, so we use this
  446.  
  447. if (UtilMisc.isUndefined(layer)) {
  448. //layer was kicked out of the map. grab it from the registry
  449. layer = RAMP.layerRegistry[evt.layerId];
  450. }
  451.  
  452. //derive section layer is in and the config collection it is in
  453. switch (layer.ramp.type) {
  454. case GlobalStorage.layerType.wms:
  455. layerSection = GlobalStorage.layerType.wms;
  456. configCollection = RAMP.config.layers.wms;
  457. break;
  458.  
  459. case GlobalStorage.layerType.feature:
  460. case GlobalStorage.layerType.Static:
  461. layerSection = GlobalStorage.layerType.feature;
  462. configCollection = RAMP.config.layers.feature;
  463. break;
  464. }
  465.  
  466. //remove item from layer selector
  467. FilterManager.removeLayer(layerSection, evt.layerId);
  468.  
  469. removeFromMap(evt.layerId);
  470.  
  471. //remove node from config
  472. configIdx = configCollection.indexOf(layer.ramp.config);
  473. configCollection.splice(configIdx, 1);
  474.  
  475. RAMP.layerRegistry[evt.layerId] = undefined;
  476. },
  477.  
  478. /**
  479. * Reacts to a request for a layer to be reloaded. Usually the case when a layer errors and user wants to try again
  480. *
  481. * @method onLayerRemove
  482. * @param {Object} evt
  483. * @param {Object} evt.layerId the layer id to be reloaded
  484. */
  485. onLayerReload: function (evt) {
  486. var map = RampMap.getMap(),
  487. layer,
  488. layerConfig,
  489. user,
  490. newLayer,
  491. layerIndex,
  492. inMap,
  493. layerList,
  494. idArray,
  495. cleanIdArray;
  496.  
  497. layer = map._layers[evt.layerId]; //map.getLayer is not reliable, so we use this
  498.  
  499. if (layer) {
  500. inMap = true;
  501. } else {
  502. //layer was kicked out of the map. grab it from the registry
  503. inMap = false;
  504. layer = RAMP.layerRegistry[evt.layerId];
  505. }
  506.  
  507. layerConfig = layer.ramp.config;
  508. user = layer.ramp.user;
  509.  
  510. //figure out index of layer
  511. //since the layer may not be in the map, we have to use some trickery to derive where it is sitting
  512.  
  513. //get our list of layers
  514. layerList = $("#" + RAMP.config.divNames.filter).find("#layerList").find("> li > ul");
  515.  
  516. //make an array of the ids in order of the list on the page
  517. idArray = layerList
  518. .map(function (i, elm) { return $(elm).find("> li").toArray().reverse(); }) // for each layer list, find its items and reverse their order
  519. .map(function (i, elm) { return elm.id; });
  520.  
  521. cleanIdArray = idArray.filter(function (i, elm) {
  522. //check if layer is in error state. error layers should not be part of the count. exception being the layer we are reloading
  523. return ((FilterManager.getLayerState(elm) !== LayerItem.state.ERROR) || (elm === evt.layerId));
  524. });
  525.  
  526. //find where our index is
  527. layerIndex = dojoArray.indexOf(cleanIdArray, evt.layerId);
  528.  
  529. if (layer.ramp.type === GlobalStorage.layerType.wms) {
  530. //adjust for wms, as it's in a different layer list on the map
  531. layerIndex = layerIndex + RAMP.layerCounts.base - RAMP.layerCounts.feature;
  532. }
  533.  
  534. //remove layer from map
  535. if (inMap) {
  536. map.removeLayer(layer);
  537. }
  538.  
  539. //generate new layer
  540. switch (layer.ramp.type) {
  541. case GlobalStorage.layerType.wms:
  542. newLayer = RampMap.makeWmsLayer(layerConfig, user);
  543. break;
  544.  
  545. case GlobalStorage.layerType.feature:
  546. newLayer = RampMap.makeFeatureLayer(layerConfig, user);
  547. break;
  548.  
  549. case GlobalStorage.layerType.Static:
  550. newLayer = RampMap.makeStaticLayer(layerConfig, user);
  551. break;
  552. }
  553.  
  554. //load the layer at the previous index
  555. console.log("Reloading Layer at index " + layerIndex.toString());
  556. _loadLayer(newLayer, layerIndex);
  557. },
  558.  
  559. /**
  560. * Public endpoint to initiate the loading of an ESRI layer object to the map.
  561. *
  562. * @method loadLayer
  563. * @param {Object} layer an instantiated, unloaded ESRI layer object
  564. * @param {Integer} reloadIndex Optional. If reloading a layer, supply the index it should reside at. Do not set for new layers
  565. */
  566. loadLayer: function (layer, reloadIndex) {
  567. _loadLayer(layer, reloadIndex);
  568. },
  569.  
  570. nextId: nextId
  571. };
  572. });