Reusable Accessible Mapping Platform

API Docs for: 5.0.0
Show:

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

  1. /*global define, esri, i18n, console, $, RAMP, proj4, window */
  2.  
  3. /**
  4. *
  5. * A RAMP Map module with ESRI and Dojo AMD Modules
  6. * This module provides function to create an ESRI web map control. A global config object is
  7. * required to initialize the map.
  8. *
  9. * @module RAMP
  10. * @submodule Map
  11. * @main Map
  12. */
  13.  
  14. /**
  15. * Map class represents the ESRI map object. The map is generated based on the application configuration and templates.
  16. *
  17. * @class Map
  18. * @uses dojo/_base/declare
  19. * @uses dojo/_base/array
  20. * @uses dojo/dom
  21. * @uses dojo/dom-construct
  22. * @uses dojo/number
  23. * @uses dojo/query
  24. * @uses dojo/topic
  25. * @uses dojo/on
  26. * @uses esri/map
  27. * @uses esri/layers/FeatureLayer
  28. * @uses esri/layers/ArcGISTiledMapServiceLayer
  29. * @uses esri/layers/ArcGISDynamicMapServiceLayer
  30. * @uses esri/layers/WMSLayer
  31. * @uses esri/SpatialReference
  32. * @uses esri/dijit/Scalebar
  33. * @uses esri/geometry/Extent
  34. * @uses esri/tasks/GeometryService
  35. * @uses esri/tasks/ProjectParameters
  36. * @uses GlobalStorage
  37. * @uses RAMP
  38. * @uses FeatureClickHandler
  39. * @uses MapClickHandler
  40. * @uses Navigation
  41. * @uses EventManager
  42. * @uses Util
  43. * @uses Array
  44. */
  45.  
  46. define([
  47. /* Dojo */
  48. "dojo/_base/declare", "dojo/_base/array", "dojo/dom",
  49. "dojo/dom-construct", "dojo/number", "dojo/query", "dojo/topic", "dojo/on",
  50.  
  51. /* Esri */
  52. "esri/map", "esri/layers/FeatureLayer", "esri/layers/ArcGISTiledMapServiceLayer", "esri/layers/ArcGISDynamicMapServiceLayer",
  53. "esri/SpatialReference", "esri/dijit/Scalebar", "esri/geometry/Extent", "esri/layers/WMSLayer", "esri/tasks/GeometryService", "esri/tasks/ProjectParameters",
  54.  
  55. /* Ramp */
  56. "ramp/globalStorage", "ramp/ramp", "ramp/featureClickHandler", "ramp/mapClickHandler", "ramp/navigation", "ramp/eventManager",
  57.  
  58. /* Util */
  59. "utils/util", "utils/array", "utils/dictionary"],
  60.  
  61. function (
  62. /* Dojo */
  63. declare, dojoArray, dom, domConstruct, number, query, topic, dojoOn,
  64.  
  65. /* Esri */
  66. EsriMap, FeatureLayer, ArcGISTiledMapServiceLayer, ArcGISDynamicMapServiceLayer,
  67. SpatialReference, EsriScalebar, EsriExtent, WMSLayer, GeometryService, ProjectParameters,
  68.  
  69. /* Ramp */
  70. GlobalStorage, Ramp, FeatureClickHandler, MapClickHandler, Navigation, EventManager,
  71.  
  72. /* Util */
  73. UtilMisc, UtilArray, UtilDict) {
  74. "use strict";
  75.  
  76. /**
  77. * An Array of {{#crossLink "Esri/layers/FeatureLayer"}}{{/crossLink}} objects.
  78. *
  79. * @private
  80. * @property featureLayers {Array}
  81. */
  82. var featureLayers,
  83.  
  84. /**
  85. * An Array of {{#crossLink "Esri/layer/WMSLayer"}}{{/crossLink}} objects.
  86. *
  87. * @private
  88. * @property wmsLayers {Array}
  89. */
  90. wmsLayers = [],
  91.  
  92. /**
  93. * Maps the id of a graphic layer to the GraphicsLayer Object that represents its extent bounding box.
  94. * A dictionary of String, {{#crossLink "Esri/layers/GraphicsLayer"}}{{/crossLink}} pairs.
  95. *
  96. * @private
  97. * @property boundingBoxMapping {Object}
  98. */
  99. boundingBoxMapping = {},
  100.  
  101. /**
  102. * The map not only contains feature layers, but also other layers such as the
  103. * basemap layer, highlight layer, bounding box layer, etc. This variable is
  104. * used to store the starting index of the feature layers in the map.
  105. *
  106. * @private
  107. * @property featureLayerStartIndex {Integer}
  108. */
  109. featureLayerStartIndex,
  110.  
  111. map,
  112.  
  113. fullExtent,
  114. maxExtent,
  115. initExtent;
  116.  
  117. /**
  118. * Shows the loading image.
  119. *
  120. * @private
  121. * @method _showLoadingImg
  122. */
  123. function _showLoadingImg() {
  124. $("#map-load-indicator").removeClass("hidden");
  125. }
  126.  
  127. /**
  128. * Hides the loading image.
  129. *
  130. * @private
  131. * @method _hideLoadingImg
  132. */
  133. function _hideLoadingImg() {
  134. $("#map-load-indicator").addClass("hidden");
  135. }
  136.  
  137. /**
  138. * Update Map Scale when zoom level changes
  139. *
  140. * @private
  141. * @method _updateScale
  142. * @param {Object} event
  143. */
  144. function _updateScale(event) {
  145. if (event.levelChange) {
  146. var currentScale = number.format(event.lod.scale),
  147. scaleLabelText = "1 : " + currentScale;
  148.  
  149. domConstruct.empty('scaleLabel');
  150. $("#scaleLabel").text(scaleLabelText);
  151. }
  152. }
  153.  
  154. /**
  155. * Initialize Map Scale
  156. *
  157. * @private
  158. * @method _initScale
  159. * @param {Object} event
  160. */
  161. function _initScale(event) {
  162. var map = event.map,
  163. scaleDiv = domConstruct.create("div", {
  164. id: "scaleDiv",
  165. class: "esriScalebarLabel"
  166. }),
  167. currentScale,
  168. scaleLabelText;
  169. $(scaleDiv).html("<span>" + i18n.t('map.scale') + "</span><br><span id='scaleLabel'><span/>");
  170. currentScale = number.format(map.getScale());
  171. scaleLabelText = "1 : " + currentScale;
  172.  
  173. domConstruct.place(scaleDiv, query(".esriScalebarRuler")[0], "before");
  174. domConstruct.empty('scaleLabel');
  175. $("#scaleLabel").text(scaleLabelText);
  176.  
  177. // Change the css class of the scale bar so it shows up against
  178. // the map
  179. topic.subscribe(EventManager.BasemapSelector.BASEMAP_CHANGED, function (attr) {
  180. $(".esriScalebar > div").removeClass().addClass(attr.cssStyle);
  181. });
  182. }
  183.  
  184. /**
  185. * Republishes map events to the outside using topic.publish
  186. *
  187. * @method _initRepublishers
  188. * @param {Object} map object
  189. * @private
  190. */
  191. function _initRepublishers(map) {
  192. var prefix = "map";
  193.  
  194. /**
  195. * Republish map events using topic.publish
  196. *
  197. * @method republish
  198. * @param {String} name
  199. * @private
  200. */
  201. function republish(name) {
  202. map.on(name, function (event) {
  203. topic.publish(prefix + "/" + name, event);
  204. });
  205. }
  206.  
  207. republish("update-end");
  208. republish("extent-change");
  209. republish("zoom-start");
  210. republish("zoom-end");
  211. republish("pan-start");
  212. republish("pan-end");
  213. }
  214.  
  215. /**
  216. * Subscribe to external events (published using topic.publish)
  217. * and react accordingly
  218. *
  219. * @method _initListeners
  220. * @param {Object} map map object
  221. * @private
  222. */
  223. function _initListeners(map) {
  224. /* SUBSCRIBED EVENTS */
  225. topic.subscribe(EventManager.Map.CENTER_AT, function (event) {
  226. map.centerAt(event.point);
  227. });
  228.  
  229. topic.subscribe(EventManager.Map.CENTER_AND_ZOOM, function (event) {
  230. var point = new esri.geometry.Point(event.graphic.geometry.x, event.graphic.geometry.y, map.spatialReference),
  231. d = map.centerAndZoom(point, event.level); // Last parameter is the level
  232.  
  233. if (event.callback) {
  234. d.then(event.callback);
  235. }
  236. });
  237.  
  238. topic.subscribe(EventManager.Map.SET_EXTENT, function (event) {
  239. event.extent.spatialReference = map.spatialReference;
  240. var d = map.setExtent(event.extent);
  241.  
  242. if (event.callback) {
  243. d.then(event.callback);
  244. }
  245. });
  246.  
  247. /* NAVIGATION EVENTS */
  248. topic.subscribe(EventManager.Navigation.PAN, function (event) {
  249. // event.direction panUp, panDown, panLeft, panRight
  250. // this same as calling map.panUp(), map.panDown(), map.panLeft(), map.panRight()
  251. map[event.direction]();
  252. });
  253.  
  254. topic.subscribe(EventManager.Navigation.ZOOM_STEP, function (event) {
  255. map.setLevel(map.getLevel() + event.level);
  256. });
  257.  
  258. topic.subscribe(EventManager.Navigation.ZOOM, function (event) {
  259. map.setLevel(event.level);
  260. });
  261.  
  262. topic.subscribe(EventManager.Navigation.FULL_EXTENT, function () {
  263. map.setExtent(fullExtent);
  264. });
  265.  
  266. /* GUI EVENTS */
  267. topic.subscribe(EventManager.GUI.LAYOUT_CHANGE, function () {
  268. map.resize(true);
  269. });
  270.  
  271. // Unhighlight the point when the subpanel is collapsed
  272. topic.subscribe(EventManager.GUI.SUBPANEL_CHANGE, function (eventArg) {
  273. // unhighlight only if the panel closing is the details panel
  274. if (!eventArg.visible &&
  275. eventArg.isComplete &&
  276. (eventArg.origin === ("rampPopup" || "datagrid"))) {
  277. topic.publish(EventManager.FeatureHighlighter.HIGHLIGHT_HIDE, {});
  278. }
  279. });
  280.  
  281. /* START BOUNDING BOX TOGGLE */
  282.  
  283. topic.subscribe(EventManager.FilterManager.LAYER_VISIBILITY_TOGGLED, function (evt) {
  284. var setTo = evt.state,
  285. layerId = evt.id,
  286. // either take url (would need mapping to layer on map),
  287. // map id in config, graphic layer id
  288. layer = map.getLayer(layerId);
  289.  
  290. layer.setVisibility(setTo);
  291. //loops through any static layers that are mapped to the feature layer being toggled
  292. try {
  293. dojoArray.forEach(GlobalStorage.LayerMap[layerId], function (staticLayer) {
  294. var layer = map.getLayer(staticLayer);
  295. layer.setVisibility(setTo);
  296. });
  297. }
  298. catch (err) {
  299. }
  300. });
  301.  
  302. topic.subscribe(EventManager.FilterManager.LAYER_TRANSPARENCY_CHANGED, function (evt) {
  303. var layer = map.getLayer(evt.layerId);
  304.  
  305. if (layer !== undefined) {
  306. layer.setOpacity(evt.value);
  307. //loops through any static layers that are mapped to the feature layer being toggled
  308. try {
  309. dojoArray.forEach(GlobalStorage.LayerMap[evt.layerId], function (staticLayer) {
  310. var layer = map.getLayer(staticLayer);
  311. layer.setOpacity(evt.value);
  312. });
  313. }
  314. catch (err) {
  315. }
  316. }
  317. });
  318.  
  319. topic.subscribe(EventManager.FilterManager.BOX_VISIBILITY_TOGGLED, function (evt) {
  320. setBoundingBoxVisibility(evt.id, evt.state);
  321. });
  322. /* END BOUNDING BOX TOGGLE */
  323.  
  324. topic.subscribe(EventManager.FilterManager.SELECTION_CHANGED, function (evt) {
  325. // this is handling the user trying to re-order the layers
  326. // graphical and feature layers must be handled separately from
  327. // all other layers as ESRI separates the two internally
  328.  
  329. var newIndex = evt.index,
  330. featureLayers;
  331.  
  332. if (map.layerIds.contains(evt.id)) {
  333. featureLayers = dojoArray.map(map.graphicsLayerIds, function (x) {
  334. return map.getLayer(x).type === 'Feature Layer' ? 1 : 0;
  335. }).sum();
  336. newIndex += 1 - featureLayers; // offset by 1 basemap not accounted for
  337. console.log('newIndex ' + newIndex);
  338. console.log(map.layerIds);
  339. } else {
  340. if (!featureLayerStartIndex) {
  341. // Find the index of the first feature layer
  342. featureLayerStartIndex = UtilArray.indexOf(map.graphicsLayerIds, function (layerId) {
  343. var layer = map.getLayer(layerId);
  344. return layer.type && layer.type === "Feature Layer";
  345. });
  346. }
  347. newIndex += featureLayerStartIndex;
  348. }
  349. map.reorderLayer(map.getLayer(evt.id), newIndex);
  350. topic.publish(EventManager.Map.REORDER_END);
  351. });
  352.  
  353. //TODO this will likely get removed or amended by aly
  354. /* Add Layer subscription*/
  355. topic.subscribe(EventManager.Map.ADD_LAYER, function () {
  356. var type = dom.byId("addLayer-select-type").value,
  357. URL = dom.byId("addLayer-URL-input").value,
  358. opacity = dom.byId("addLayer-Opacity").value;
  359.  
  360. console.log(type + " | " + URL + " | " + opacity);
  361. addStaticLayer(type, URL, opacity);
  362. });
  363.  
  364. topic.subscribe(EventManager.Map.ADD_LAYER_READY, function (temp_layer) {
  365. map.addLayer(temp_layer);
  366. });
  367. }
  368. /**
  369. * Creates event handlers for the map control: click, mouse-over, load, extent change, and update events.
  370. *
  371. * @private
  372. * @method _initEventHandlers
  373. * @param {Object} map A ESRI map object
  374. */
  375. function _initEventHandlers(map) {
  376. map.on("load", _initScale);
  377. map.on("extent-change", function (event) {
  378. _updateScale(event);
  379.  
  380. console.log("map - >> extent-change", event);
  381. dojoOn.once(map, "update-end", function () {
  382. console.log("map - >> update-end - >> Apply extent Filter");
  383. topic.publish(EventManager.Datagrid.APPLY_EXTENT_FILTER);
  384. });
  385. });
  386.  
  387. // Deselect all highlighted points if the map is clicked
  388. map.on("click", function (evt) {
  389. FeatureClickHandler.onFeatureDeselect(evt);
  390. topic.publish(EventManager.Map.CLICK, evt);
  391. });
  392.  
  393. // Show/Hide spinner for map loading
  394. map.on("update-start", _showLoadingImg);
  395. map.on("update-end", _hideLoadingImg);
  396.  
  397. // code that would wait until all layers were loaded. not used anymore, but could be useful to keep around to steal later
  398. /*
  399. var handle = map.on("update-end", function () {
  400. var isAllLoaded = dojoArray.every(
  401. map.graphicsLayerIds.concat(map.layerIds),
  402. function (layerId) {
  403. var layer = map.getLayer(layerId);
  404. console.log(layer.loaded, layerId, layer);
  405. return layer.loaded;
  406. }
  407. );
  408.  
  409. console.log("map -> is all layers loaded: ", isAllLoaded);
  410.  
  411. if (isAllLoaded) {
  412. handle.remove();
  413. console.log("map ->", EventManager.Map.ALL_LAYERS_LOADED);
  414. topic.publish(EventManager.Map.ALL_LAYERS_LOADED);
  415. }
  416. });
  417. */
  418. }
  419.  
  420. /**
  421. * Will project an extent to a desired spatial reference, using client side projection library.
  422. * Avoids the need for Esri Geometry Service
  423. *
  424. * @method localProjectExtent
  425. * @private
  426. * @param {Esri/Extent} extent extent to be projected
  427. * @param {Esri/SpatialReference} sr {{#crossLink "Esri/SpatialReference"}}{{/crossLink}} to project to
  428. * @return {Esri/Extent} extent in the desired projection
  429. */
  430. function localProjectExtent(extent, sr) {
  431. //TODO can we handle WKT?
  432.  
  433. // interpolates two points by splitting the line in half recursively
  434. function interpolate(p0, p1, steps) {
  435. var mid, i0, i1;
  436.  
  437. if (steps === 0) { return [p0, p1]; }
  438.  
  439. mid = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];
  440. if (steps === 1) {
  441. return [p0, mid, p1];
  442. }
  443. if (steps > 1) {
  444. i0 = interpolate(p0, mid, steps - 1);
  445. i1 = interpolate(mid, p1, steps - 1);
  446. return i0.concat(i1.slice(1));
  447. }
  448. }
  449.  
  450. var points = [[extent.xmin, extent.ymin], [extent.xmax, extent.ymin], [extent.xmax, extent.ymax], [extent.xmin, extent.ymax], [extent.xmin, extent.ymin]],
  451. projConvert, transformed, projExtent, x0, y0, x1, y1, xvals, yvals, interpolatedPoly = [];
  452.  
  453. // interpolate each edge by splitting it in half 3 times (since lines are not guaranteed to project to lines we need to consider
  454. // max / min points in the middle of line segments)
  455. [0, 1, 2, 3].map(function (i) { return interpolate(points[i], points[i + 1], 3).slice(1); }).forEach(function (seg) { interpolatedPoly = interpolatedPoly.concat(seg); });
  456.  
  457. //reproject the extent
  458. projConvert = proj4('EPSG:' + extent.spatialReference.wkid, 'EPSG:' + sr.wkid);
  459. transformed = interpolatedPoly.map(function (x) { return projConvert.forward(x); });
  460.  
  461. xvals = transformed.map(function (x) { return x[0]; });
  462. yvals = transformed.map(function (x) { return x[1]; });
  463.  
  464. x0 = Math.min.apply(null, xvals);
  465. x1 = Math.max.apply(null, xvals);
  466.  
  467. y0 = Math.min.apply(null, yvals);
  468. y1 = Math.max.apply(null, yvals);
  469.  
  470. projExtent = new EsriExtent(x0, y0, x1, y1, sr);
  471. console.log('localProjectExtent complete: ' + JSON.stringify(projExtent));
  472.  
  473. return projExtent;
  474. }
  475.  
  476. /**
  477. * project an extent to a new spatial reference, if required
  478. * when projection is finished, call another function and pass the result to it.
  479. *
  480. * @method projectExtent
  481. * @private
  482. * @param {Object} extent an extent object from the configuration
  483. * @param {Esri/SpatialReference} sr {{#crossLink "Esri/SpatialReference"}}{{/crossLink}} to project to
  484. * @param {Function} callWhenDone function to call when extent is projected. expects geometry parameter
  485. */
  486. function projectExtent(extent, sr, callWhenDone) {
  487. var realExtent;
  488.  
  489. //convert configuration extent to proper esri extent object
  490. realExtent = new EsriExtent(extent);
  491.  
  492. if (UtilMisc.isSpatialRefEqual(realExtent.spatialReference, sr)) {
  493. //the extent is already in the correct projection.
  494. //go to the next step
  495. callWhenDone([realExtent]);
  496. } else {
  497. //need to re-project the extent
  498.  
  499. var projectedExtent = localProjectExtent(realExtent, sr);
  500. callWhenDone([projectedExtent]);
  501.  
  502. //Geometry Service Version. Makes a more accurate bounding box, but requires an arcserver
  503. /*
  504. var geomSrv, geomParams;
  505. geomSrv = new GeometryService(RAMP.config.geometryServiceUrl);
  506. geomParams = new ProjectParameters();
  507. geomParams.geometries = [realExtent];
  508. geomParams.outSR = sr;
  509.  
  510. geomSrv.project(geomParams, function (projectedExtents) {
  511. //after service returns, continue to next step
  512. callWhenDone(projectedExtents);
  513. });
  514. */
  515. }
  516. }
  517.  
  518. /**
  519. * process the projected default extent, begin projection of full extent.
  520. * used as an asynchronous gate for the projection.
  521. *
  522. * @method projectFullExtent
  523. * @private
  524. * @param {Array} projectedDefaultExtent an array containing the default extent object in the map's projection
  525. */
  526. function projectFullExtent(projectedDefaultExtent) {
  527. //store projected result
  528. RAMP.config.extents.defaultExtent = projectedDefaultExtent[0];
  529.  
  530. //project the full extent. when finished, process max extent
  531. projectExtent(RAMP.config.extents.fullExtent, projectedDefaultExtent[0].spatialReference, projectMaxExtent);
  532. }
  533.  
  534. /**
  535. * process the projected full extent, begin projection of maximum extent.
  536. * used as an asynchronous gate for the projection.
  537. *
  538. * @method projectMaxExtent
  539. * @private
  540. * @param {Array} projectedFullExtent an array containing the full extent object in the map's projection
  541. */
  542. function projectMaxExtent(projectedFullExtent) {
  543. //store projected result
  544. RAMP.config.extents.fullExtent = projectedFullExtent[0];
  545.  
  546. //project the max extent. when finished, tell map to continue loading
  547. projectExtent(RAMP.config.extents.maximumExtent, projectedFullExtent[0].spatialReference, finishExtentProjection);
  548. }
  549.  
  550. /**
  551. * process the projected maximum extent, then alert app to continue loading the map.
  552. * used as an asynchronous gate for the projection.
  553. *
  554. * @method finishExtentProjection
  555. * @private
  556. * @param {Array} projectedMaxExtent an array containing the maximum extent object in the map's projection
  557. */
  558. function finishExtentProjection(projectedMaxExtent) {
  559. //store projected result
  560. RAMP.config.extents.maximumExtent = projectedMaxExtent[0];
  561.  
  562. //throw event
  563. topic.publish(EventManager.Map.EXTENTS_REPROJECTED);
  564. }
  565.  
  566. /**
  567. * Add a static, non-interactive Layer to the map
  568. *
  569. * @private
  570. * @method AddStaticLayer
  571. * @param {String} layer_type A value which controls how the layer is going to be added to the map
  572. * @param {String} layer_url A URL pointing to a valid map service endpoint
  573. * @param {Number} layer_op A value between 0.0 and 1.0 which determines the transparency of the layer
  574. */
  575. function addStaticLayer(layer_type, layer_url, layer_op) {
  576. layer_op = layer_op / 100; // change percentage to decimal
  577. var tempLayer;
  578. switch (layer_type) {
  579. case "feature":
  580. tempLayer = new FeatureLayer(layer_url, {
  581. opacity: layer_op,
  582. mode: FeatureLayer.MODE_SNAPSHOT
  583. });
  584. break;
  585.  
  586. case "tile":
  587. tempLayer = new ArcGISTiledMapServiceLayer(layer_url, {
  588. opacity: layer_op
  589. });
  590. break;
  591.  
  592. case "dynamic":
  593. tempLayer = new ArcGISDynamicMapServiceLayer(layer_url, {
  594. opacity: layer_op
  595. });
  596. break;
  597.  
  598. default:
  599.  
  600. break;
  601. }
  602.  
  603. topic.publish(EventManager.Map.ADD_LAYER_READY, tempLayer);
  604. topic.publish(EventManager.GUI.ADD_LAYER_PANEL_CHANGE, {
  605. visible: false
  606. });
  607. }
  608.  
  609. /**
  610. * Sets the visibility of the bounding box that belongs to the layer with the given layerId.
  611. * Note: the layerId needs to be the ID of the featurelayer, not the ID of the actual bounding
  612. * box layer.
  613. *
  614. * @private
  615. * @method setBoundingBoxVisibility
  616. * @param {String} layerId the id of the layer whose bounding box visibility should be set
  617. */
  618. function setBoundingBoxVisibility(layerId, visibility) {
  619. var boxLayer = boundingBoxMapping[layerId];
  620.  
  621. //if (boxLayer.graphics.isEmpty() && visibility) {
  622. // // get bounding box info from config object
  623. // var boundingBoxExtent;
  624. // var layerConfig = dojoArray.find(config.featureLayers, function (layerConfig) {
  625. // return layerConfig.id === layerId;
  626. // });
  627.  
  628. // boundingBoxExtent.xmin = layerConfig.boundingBox.extent.xmin;
  629. // boundingBoxExtent.ymin = layerConfig.boundingBox.extent.ymin;
  630. // boundingBoxExtent.xmax = layerConfig.boundingBox.extent.xmax;
  631. // boundingBoxExtent.ymax = layerConfig.boundingBox.extent.ymax;
  632.  
  633. // var extentGraphic = new esri.Graphic({
  634. // geometry: boundingBoxExtent,
  635. // symbol: {
  636. // color: [255, 0, 0, 64],
  637. // outline: {
  638. // color: [240, 128, 128, 255],
  639. // width: 1,
  640. // type: "esriSLS",
  641. // style: "esriSLSSolid"
  642. // },
  643. // type: "esriSFS",
  644. // style: "esriSFSSolid"
  645. // }
  646. // });
  647. // boxLayer.add(extentGraphic);
  648. //}
  649.  
  650. boxLayer.setVisibility(visibility);
  651. }
  652.  
  653. function resolveLayerOpacity(layerOpacity) {
  654. return layerOpacity.default || 1;
  655. }
  656.  
  657. /**
  658. * Sets up loading event handlers and initializes the .ramp object of a layer
  659. * Circular reference errors prevent us from calling LayerLoader directly from this module
  660. *
  661. * @private
  662. * @method prepLayer
  663. * @param {Object} layer layer to be prepped
  664. * @param {Object} config config object for the layer
  665. * @param {Boolean} userLayer optional. indicates if layer was added by a user. default value is false
  666. */
  667. function prepLayer(layer, config, userLayer) {
  668. layer.ramp = {
  669. config: config,
  670. user: UtilMisc.isUndefined(userLayer) ? false : userLayer,
  671. load: {
  672. state: "loading",
  673. inLS: false, //layer has entry in layer selector
  674. inCount: false //layer is included in the layer counts
  675. }
  676. };
  677.  
  678. layer.on('load', function (evt) {
  679. //console.log("PREP LOAD OK " + evt.layer.url);
  680. topic.publish(EventManager.LayerLoader.LAYER_LOADED, { layer: evt.layer });
  681. });
  682.  
  683. layer.on('error', function (evt) {
  684. //console.log("PREP LOAD FAIL " + evt.target.url);
  685. evt.target.ramp.loadOk = false;
  686. topic.publish(EventManager.LayerLoader.LAYER_ERROR, {
  687. layer: evt.target,
  688. error: evt.error
  689. });
  690. });
  691.  
  692. //since the update-start event doesn't let you know who threw it (supposed to but doesn't), we need to tack the handler
  693. //function onto the actual layer object so we can use the "this" keyword to grab the sending layer
  694. layer.ramp.load.onUpdateStart = function () {
  695. topic.publish(EventManager.LayerLoader.LAYER_UPDATING, { layer: this });
  696. };
  697.  
  698. layer.on('update-start', layer.ramp.load.onUpdateStart);
  699.  
  700. //add update end handler for layer
  701. layer.on('update-end', function (evt) {
  702. topic.publish(EventManager.LayerLoader.LAYER_UPDATED, { layer: evt.target });
  703. });
  704. }
  705.  
  706. return {
  707. /**
  708. * For a specified layer, zooms to the closest level that has some visible data.
  709. * @param {String} layerId a layer id to zoom to.
  710. * @method zoomToLayerScale
  711. */
  712. zoomToLayerScale: function (layerId) {
  713. var layer = map.getLayer(layerId),
  714. lods = map._params.lods,
  715. currentLevel = map.getLevel(),
  716. topLod,
  717. bottomLod,
  718. lod,
  719. i;
  720.  
  721. for (i = 0; i < lods.length; i += 1) {
  722. lod = lods[i];
  723. //console.log("lod", lod, lod.scale > layer.minScale);
  724. if (!topLod && lod.scale <= layer.minScale) {
  725. topLod = lod;
  726. }
  727.  
  728. if (!bottomLod && lod.scale <= layer.maxScale) {
  729. bottomLod = lods[Math.max(0, i - 1)];
  730. }
  731. }
  732.  
  733. //console.log(topLod, bottomLod, map.getLevel(), map.getZoom(), Math.abs(topLod.level - currentLevel) <= Math.abs(bottomLod.level - currentLevel));
  734.  
  735. if (Math.abs(topLod.level - currentLevel) <= Math.abs(bottomLod.level - currentLevel)) {
  736. map.setZoom(topLod.level);
  737. } else {
  738. map.setZoom(bottomLod.level);
  739. }
  740. },
  741.  
  742. /**
  743. * The maximum extent of the map control is allowed to go to
  744. * @property getMaxExtent
  745. * @type {Object}
  746. *
  747. */
  748. getMaxExtent: function () {
  749. return maxExtent;
  750. },
  751. /**
  752. * Return the map control object
  753. * @property getMap
  754. * @type {Object}
  755. *
  756. */
  757. getMap: function () {
  758. if (UtilMisc.isUndefined(map)) {
  759. console.log("trying to get map before it is available!");
  760. }
  761. return map;
  762. },
  763.  
  764. /**
  765. * Returns a list of feature layers that are currently visible on the map.
  766. * @method getVisibleFeatureLayers
  767. * @return {Array} an array of {{#crossLink "Esri/layers/FeatureLayer"}}{{/crossLink}} objects
  768. *
  769. */
  770. getVisibleFeatureLayers: function () {
  771. // Return only the feature layers
  772. //TODO do we need to consider static layers here?
  773. return dojoArray.filter(map.getLayersVisibleAtScale(), function (layer) {
  774. return layer.type && (layer.type === "Feature Layer") && layer.visible;
  775. });
  776. },
  777.  
  778. getVisibleLayers: function () {
  779. return map.getLayersVisibleAtScale();
  780. },
  781.  
  782. getInvisibleLayers: function () {
  783. var visibleLayers,
  784. allLayers,
  785. invisibleLayers;
  786.  
  787. visibleLayers = this.getVisibleLayers();
  788. allLayers = map._layers;
  789. invisibleLayers = [];
  790.  
  791. UtilDict.forEachEntry(allLayers, function (key, value) {
  792. var index = UtilArray.indexOf(visibleLayers, function (vl) {
  793. return key === vl.id;
  794. });
  795.  
  796. if (index === -1) {
  797. invisibleLayers.push(value);
  798. }
  799. });
  800.  
  801. return invisibleLayers;
  802. },
  803.  
  804. /**
  805. * Returns the mapping of feature layer ids to assocciated bounding box layers.
  806. * @method getBoundingBoxMapping
  807. * @return {Object} A dictionary of String, {{#crossLink "Esri/layers/GraphicsLayer"}}{{/crossLink}} pairs.
  808. *
  809. */
  810. getBoundingBoxMapping: function () {
  811. return boundingBoxMapping;
  812. },
  813.  
  814. /**
  815. * Return the feature layer corresponding to the given id.
  816. *
  817. * @method getFeatureLayer
  818. * @private
  819. * @param {String} featureId the id of the feature layer
  820. * @return {Esri/layers/FeatureLayer} feature layer
  821. */
  822. getFeatureLayer: function (featureId) {
  823. return map.getLayer(featureId);
  824. },
  825.  
  826. /**
  827. * Apply extent defaulting prior to URL overrides.
  828. *
  829. * @method applyExtentDefaulting
  830. * @private
  831. */
  832. applyExtentDefaulting: function () {
  833. //if full extent is missing, set to default extent.
  834. if (!(RAMP.config.extents.fullExtent)) {
  835. //need to deserialize/reserialize to avoid pointing to actual defaultExtent, which may be changed later by the Bookmark Link module
  836. RAMP.config.extents.fullExtent = JSON.parse(JSON.stringify(RAMP.config.extents.defaultExtent));
  837. }
  838.  
  839. //if maximum extent is missing, set to full extent.
  840. if (!(RAMP.config.extents.maximumExtent)) {
  841. RAMP.config.extents.maximumExtent = JSON.parse(JSON.stringify(RAMP.config.extents.fullExtent));
  842. }
  843. },
  844.  
  845. /**
  846. * initiate the projection of the config extents to basemap extents
  847. *
  848. * @method projectConfigExtents
  849. */
  850. projectConfigExtents: function () {
  851. //extract default basemap projection
  852. var mapSR = new SpatialReference(RAMP.config.basemaps[RAMP.config.initialBasemapIndex].spatialReference);
  853.  
  854. //project the default extent. when finished, process full extent
  855. projectExtent(RAMP.config.extents.defaultExtent, mapSR, projectFullExtent);
  856. },
  857.  
  858. /**
  859. * Given an ESRI Extent Object, returns a new ESRI Extent Object that
  860. * contains the extent adjusted according to this map's maximum extent
  861. *
  862. * NOTE: this method is currently unused!
  863. *
  864. * @param {esri/geometry/Extent} e the extent Object
  865. * @param {esri/geometry/Extent} maxExtent the maximum extent
  866. * @return {esri/geometry/Extent} An adjusted extent, if the target extent is outside the boundary
  867. * @method checkBoundary
  868. */
  869. checkBoundary: function (e, maxExtent) {
  870. var extent = e,
  871. width = extent.width(),
  872. height = extent.height(),
  873. centerX = extent.centerX(),
  874. centerY = extent.centerY(),
  875. flag, adjustedEx;
  876.  
  877. adjustedEx = extent.clone();
  878.  
  879. var maxHeight = maxExtent.height();
  880. if (height > maxHeight) {
  881. height = maxHeight;
  882. }
  883.  
  884. if (centerY > maxExtent.ymax) {
  885. adjustedEx.ymax = maxExtent.ymax;
  886. adjustedEx.ymin = maxExtent.ymax - height;
  887. flag = true;
  888. //} else if (extent.ymin < maxExtent.ymin) {
  889. } else if (centerY < maxExtent.ymin) {
  890. adjustedEx.ymin = maxExtent.ymin;
  891. adjustedEx.ymax = maxExtent.ymin + height;
  892. flag = true;
  893. }
  894.  
  895. var maxWidth = maxExtent.width();
  896. if (width > maxWidth) {
  897. width = maxWidth;
  898. }
  899.  
  900. if (centerX > maxExtent.xmax) {
  901. adjustedEx.xmax = maxExtent.xmax;
  902. adjustedEx.xmin = maxExtent.xmax - width;
  903. flag = true;
  904. } else if (centerX < maxExtent.xmin) {
  905. adjustedEx.xmin = maxExtent.xmin;
  906. adjustedEx.xmax = maxExtent.xmin + width;
  907. flag = true;
  908. }
  909.  
  910. if (flag) {
  911. return adjustedEx;
  912. }
  913. },
  914.  
  915. /**
  916. * Create a new FeatureLayer object based on the config input
  917. *
  918. * @method makeFeatureLayer
  919. * @param {Object} layerConfig config object for the layer to create
  920. * @param {Boolean} userLayer optional specifies if layer was added by a user
  921. * @return {Esri/layers/FeatureLayer} feature layer object (unloaded)
  922. */
  923. makeFeatureLayer: function (layerConfig, userLayer) {
  924. var fl = new FeatureLayer(layerConfig.url, {
  925. id: layerConfig.id,
  926. mode: FeatureLayer.MODE_SNAPSHOT,
  927. outFields: [layerConfig.layerAttributes],
  928. visible: layerConfig.settings.visible,
  929. opacity: resolveLayerOpacity(layerConfig.settings.opacity)
  930. });
  931.  
  932. prepLayer(fl, layerConfig, userLayer);
  933.  
  934. fl.ramp.type = GlobalStorage.layerType.feature;
  935.  
  936. return fl;
  937. },
  938.  
  939. /**
  940. * Return the feature layer corresponding to the given url.
  941. *
  942. * @method makeWmsLayer
  943. * @param {Object} layerConfig config object for the layer to create
  944. * @param {Boolean} userLayer optional specifies if layer was added by a user
  945. * @return {Esri/layers/WMSLayer} WMS layer
  946. */
  947.  
  948. makeWmsLayer: function (layerConfig, userLayer) {
  949. var wmsl = new WMSLayer(layerConfig.url, {
  950. id: layerConfig.id,
  951. format: layerConfig.format,
  952. opacity: resolveLayerOpacity(layerConfig.settings.opacity),
  953. visibleLayers: [layerConfig.layerName]
  954. //resourceInfo: {
  955. // extent: new EsriExtent(layer.extent),
  956. // layerInfos: [new WMSLayerInfo({name:layer.layerName,title:layer.displayName})]
  957. //}
  958. });
  959.  
  960. prepLayer(wmsl, layerConfig, userLayer);
  961.  
  962. wmsl.ramp.type = GlobalStorage.layerType.wms;
  963.  
  964. wmsl.setVisibility(layerConfig.settings.visible);
  965.  
  966. return wmsl;
  967. },
  968.  
  969. /**
  970. * Return the static layer corresponding to the given url.
  971. *
  972. * @method makeStaticLayer
  973. * @private
  974. * @param {Object} layerConfig config object for the layer to create
  975. * @param {Boolean} userLayer optional specifies if layer was added by a user
  976. * @return {Object} layer object of the appropriate type
  977. */
  978.  
  979. makeStaticLayer: function (layerConfig, userLayer) {
  980. var tempLayer,
  981. layerType = layerConfig.layerType || "feature";
  982. //determine layer type and process
  983. switch (layerType) {
  984. case "feature":
  985. tempLayer = new FeatureLayer(layerConfig.url, {
  986. opacity: resolveLayerOpacity(layerConfig.settings.opacity),
  987. mode: FeatureLayer.MODE_SNAPSHOT,
  988. visible: layerConfig.settings.visible,
  989. id: layerConfig.id
  990. });
  991.  
  992. prepLayer(tempLayer, layerConfig, userLayer);
  993.  
  994. tempLayer.ramp.type = GlobalStorage.layerType.Static;
  995.  
  996. break;
  997.  
  998. //We are currently not supporting other static layer types at the moment.
  999. //Future versions should re-implement these cases
  1000. /*
  1001. case "tile":
  1002. tempLayer = new ArcGISTiledMapServiceLayer(staticLayer.url, {
  1003. opacity: resolveLayerOpacity(staticLayer.settings.opacity),
  1004. id: staticLayer.id
  1005. });
  1006. console.log("tile layer added. " + staticLayer.id);
  1007. break;
  1008.  
  1009. case "dynamic":
  1010. tempLayer = new ArcGISDynamicMapServiceLayer(staticLayer.url, {
  1011. opacity: resolveLayerOpacity(staticLayer.settings.opacity),
  1012. id: staticLayer.id
  1013. });
  1014. console.log("dynamic layer added. " + staticLayer.id);
  1015. break;
  1016. */
  1017.  
  1018. default:
  1019. console.log("unknown static layer type encountered: " + layerType);
  1020. break;
  1021. }
  1022. return tempLayer;
  1023. },
  1024.  
  1025. /**
  1026. * Adds custom ramp properties to layer. Adds handlers to loading events.
  1027. *
  1028. * @method enhanceLayer
  1029. * @param {Object} layer layer to be prepped
  1030. * @param {Object} config config object for the layer
  1031. * @param {Boolean} userLayer optional. indicates if layer was added by a user. default value is false
  1032. */
  1033. enhanceLayer: function (layer, config, userLayer) {
  1034. //call the private function
  1035. prepLayer(layer, config, userLayer);
  1036. },
  1037.  
  1038. /**
  1039. * Will project an extent to a desired spatial reference, using client side projection library.
  1040. * Avoids the need for Esri Geometry Service
  1041. *
  1042. * @method localProjectExtent
  1043. * @param {Esri/Extent} extent extent to be projected
  1044. * @param {Esri/SpatialReference} sr {{#crossLink "Esri/SpatialReference"}}{{/crossLink}} to project to
  1045. * @return {Esri/Extent} extent in the desired projection
  1046. */
  1047. localProjectExtent: localProjectExtent,
  1048.  
  1049. /*
  1050. * Initialize map control with configuration objects provided in the bootstrapper.js file.
  1051. *
  1052. * Initialize extent
  1053. * Generate and load initial base map
  1054. * Generate map layer objects
  1055. * Show scalebar
  1056. * Publish events to outside for other modules to use
  1057. * Subscribe events to update map control
  1058. *
  1059. * @method init
  1060. * @param {Object} mapDiv the HTML div that will store the map control
  1061. * @constructor
  1062. *
  1063. */
  1064. init: function () {
  1065. var that = this,
  1066.  
  1067. schemaBasemap = RAMP.config.basemaps[RAMP.config.initialBasemapIndex],
  1068.  
  1069. /**
  1070. * The URL of the first layer of the basemap that is on by default.
  1071. *
  1072. * @property url
  1073. * @private
  1074. * @type {String}
  1075. */
  1076. url = schemaBasemap.layers[0].url,
  1077.  
  1078. /**
  1079. * The basemap layer
  1080. *
  1081. * @property baseLayer
  1082. * @private
  1083. * @type {Esri/layers/ArcGISTiledMapServiceLayer}
  1084. */
  1085. baseLayer = new ArcGISTiledMapServiceLayer(url, {
  1086. id: "basemapLayer"
  1087. }),
  1088.  
  1089. loadListener = baseLayer.on('update-end', function () {
  1090. //only load things once, pls!
  1091. loadListener.remove();
  1092.  
  1093. //basemap has loaded. continue on with the map loading
  1094. topic.publish(EventManager.Map.INITIAL_BASEMAP_LOADED);
  1095. });
  1096.  
  1097. baseLayer.on('error', function (evt) {
  1098. //basemap has died. long live the basemap.
  1099. //TODO some proper error handling here. error page? message to user of catastrophic failure?
  1100. console.log('initial basemap failed to load: ' + evt.error.message);
  1101. window.location.href = RAMP.config.mapInitFailUrl;
  1102. });
  1103.  
  1104. /**
  1105. * The initial extent of the map
  1106. *
  1107. * @property InitExtent
  1108. * @private
  1109. * @type {esri/geometry/Extent}
  1110. */
  1111. initExtent = new EsriExtent(RAMP.config.extents.defaultExtent);
  1112.  
  1113. /**
  1114. * Used for full extent in nav widget
  1115. *
  1116. * @property fullExtent
  1117. * @private
  1118. * @type {esri/geometry/Extent}
  1119. */
  1120. fullExtent = new EsriExtent(RAMP.config.extents.fullExtent);
  1121.  
  1122. /**
  1123. * The maximum extent of the map
  1124. *
  1125. * @property maxExtent
  1126. * @private
  1127. * @type {esri/geometry/Extent}
  1128. */
  1129. maxExtent = new EsriExtent(RAMP.config.extents.maximumExtent);
  1130.  
  1131. //generate WMS layers array
  1132. wmsLayers = dojoArray.map(RAMP.config.layers.wms, function (layer) {
  1133. return that.makeWmsLayer(layer);
  1134. });
  1135.  
  1136. //generate feature layers array
  1137. featureLayers = dojoArray.map(RAMP.config.layers.feature, function (layerConfig) {
  1138. var fl;
  1139.  
  1140. if (layerConfig.isStatic) {
  1141. fl = that.makeStaticLayer(layerConfig);
  1142. } else {
  1143. fl = that.makeFeatureLayer(layerConfig);
  1144. }
  1145.  
  1146. return fl;
  1147. });
  1148.  
  1149. //the map!
  1150. map = new EsriMap(RAMP.config.divNames.map, {
  1151. extent: initExtent,
  1152. logo: false,
  1153. minZoom: RAMP.config.zoomLevels.min,
  1154. maxZoom: RAMP.config.zoomLevels.max,
  1155. slider: false
  1156. });
  1157.  
  1158. RAMP.map = map;
  1159. MapClickHandler.init(map);
  1160.  
  1161. /* START - Add static layers */
  1162. //NOTE: this type of thing is not currenlty supported by the config schema. Need to revisit and determine if we want to keep this code or not.
  1163. // this only deals with static layers that are bound to a feature layer. stand alone static layers are handled like normal feature layers
  1164.  
  1165. //if this does get implemented, this code should be moved to the layerLoader.js onLayerLoaded function. After a feature layer successfully loads,
  1166. //we should then load any of it's static layers. Extra tricky because these static layers do not appear in the layer selector (their state is bound
  1167. //to the feature layer.
  1168. //may want to consider another layerType .BoundStatic
  1169.  
  1170. var staticLayers = [],
  1171. perLayerStaticMaps = [],
  1172. staticLayerMap = [];
  1173.  
  1174. dojoArray.forEach(RAMP.config.layers.feature, function (layer) {
  1175. perLayerStaticMaps = [];
  1176. dojoArray.forEach(layer.staticLayers, function (staticLayer, i) {
  1177. var tempLayer = that.makeStaticLayer(staticLayer);
  1178.  
  1179. staticLayers.push(tempLayer);
  1180. //creates an array of all static layers defined for the current, single feature layer
  1181. perLayerStaticMaps[i] = staticLayer.id;
  1182. });
  1183. //adds the static layer id array as a value to an array indexed by feature layer id
  1184. staticLayerMap[layer.id] = perLayerStaticMaps;
  1185. });
  1186.  
  1187. RAMP.staticLayerMap = staticLayerMap;
  1188.  
  1189. /* End - Add static layers */
  1190.  
  1191. //This was intended to be used to distinguish layers from each other when crawling; Looks like we are not using it. Commenting out for now. SZ
  1192. baseLayer.ramp = {
  1193. type: GlobalStorage.layerType.Basemap
  1194. };
  1195.  
  1196. //save layer objects to load after basemap.
  1197. //static layers is currently empty always
  1198. RAMP.startupLayers = wmsLayers.concat(staticLayers, featureLayers);
  1199.  
  1200. //add the basemap
  1201. map.addLayer(baseLayer);
  1202.  
  1203. /* Start - Show scalebar */
  1204. var scalebar = new EsriScalebar({
  1205. map: map,
  1206. attachTo: "bottom-left",
  1207. scalebarUnit: "metric"
  1208. });
  1209.  
  1210. scalebar.show();
  1211.  
  1212. /* End - Show scalebar */
  1213.  
  1214. _initRepublishers(map);
  1215. _initListeners(map);
  1216. _initEventHandlers(map);
  1217. }
  1218. };
  1219. });