map/esriMap.js

  1. 'use strict';
  2. const basemap = require('./basemap.js');
  3. const mapPrint = require('./print.js');
  4. function esriMap(esriBundle, geoApi) {
  5. const printModule = mapPrint(esriBundle);
  6. class Map {
  7. static get Extent () { return esriBundle.Extent; }
  8. // TODO when jshint parses instance fields properly we can change this from a property to a field
  9. get _passthroughBindings () { return [
  10. 'on', 'reorderLayer', 'addLayer', 'disableKeyboardNavigation', 'removeLayer', 'resize', 'reposition',
  11. 'centerAt', 'setZoom', 'centerAndZoom', 'toScreen', 'setExtent'
  12. ]; }
  13. get _passthroughProperties () { return ['graphicsLayerIds', 'layerIds', 'spatialReference', 'extent']; } // TODO when jshint parses instance fields properly we can change this from a property to a field
  14. constructor (domNode, opts) {
  15. this._passthroughBindings.forEach(bindingName =>
  16. this[bindingName] = (...args) => this._map[bindingName](...args));
  17. this._passthroughProperties.forEach(propName => {
  18. const descriptor = {
  19. enumerable: true,
  20. get: () => this._map[propName]
  21. };
  22. Object.defineProperty(this, propName, descriptor);
  23. });
  24. this._map = new esriBundle.Map(domNode, { extent: opts.extent, lods: opts.lods });
  25. if (opts.proxyUrl) {
  26. this.proxy = opts.proxyUrl;
  27. }
  28. if (opts.basemaps) {
  29. this.basemapGallery = basemap.initBasemaps(esriBundle, opts.basemaps, this._map);
  30. this.basemapGallery.on('selection-change', () => this.resetOverviewMap());
  31. } else {
  32. throw new Error('The basemaps option is required to and at least one basemap must be defined');
  33. }
  34. if (opts.scalebar) {
  35. this.scalebar = new esriBundle.Scalebar({
  36. map: this._map,
  37. attachTo: opts.scalebar.attachTo,
  38. scalebarUnit: opts.scalebar.scalebarUnit
  39. });
  40. this.scalebar.show();
  41. }
  42. if (opts.overviewMap) {
  43. this.initOverviewMap();
  44. }
  45. this.zoomPromise = Promise.resolve();
  46. this.zoomCounter = 0;
  47. }
  48. printLocal (options) { return printModule.printLocal(this._map, options); }
  49. printServer (options) { return printModule.printServer(this._map, options); }
  50. /**
  51. * Create an ESRI Extent object from extent setting JSON object.
  52. *
  53. * @function getExtentFromJson
  54. * @param {Object} extentJson that follows config spec
  55. * @return {Object} an ESRI Extent object
  56. */
  57. static getExtentFromJson (extentJson) {
  58. return esriBundle.Extent({ xmin: extentJson.xmin, ymin: extentJson.ymin,
  59. xmax: extentJson.xmax, ymax: extentJson.ymax,
  60. spatialReference: { wkid: extentJson.spatialReference.wkid } });
  61. }
  62. /**
  63. * Take a JSON object with extent properties and convert it to an ESRI Extent.
  64. * Reprojects to map projection if required.
  65. *
  66. * @param {Object} extent the extent to enhance
  67. * @returns {Extent} extent cast in Extent prototype, and in map spatial reference
  68. */
  69. enhanceConfigExtent (extent) {
  70. const realExtent = Map.getExtentFromJson(extent);
  71. if (geoApi.proj.isSpatialRefEqual(this._map.spatialReference, extent.spatialReference)) {
  72. return realExtent;
  73. } else {
  74. return geoApi.proj.projectEsriExtent(realExtent, this._map.spatialReference);
  75. }
  76. }
  77. /**
  78. * Finds the level of detail closest to the provided scale.
  79. *
  80. * @function findClosestLOD
  81. * @param {Array} lods list of levels of detail objects
  82. * @param {Number} scale scale value to search for in the levels of detail
  83. * @return {Object} the level of detail object closest to the scale
  84. */
  85. static findClosestLOD (lods, scale) {
  86. const diffs = lods.map(lod => Math.abs(lod.scale - scale));
  87. const lodIdx = diffs.indexOf(Math.min(...diffs));
  88. return lods[lodIdx];
  89. }
  90. /**
  91. * Calculate north arrow bearing. Angle returned is to to rotate north arrow image.
  92. * http://www.movable-type.co.uk/scripts/latlong.html
  93. * @function getNorthArrowAngle
  94. * @returns {Number} map rotation angle (in degree)
  95. */
  96. getNorthArrowAngle () {
  97. // get center point in longitude and use bottom value for latitude
  98. const pointB = geoApi.proj.localProjectPoint(this._map.extent.spatialReference, 'EPSG:4326',
  99. { x: (this._map.extent.xmin + this._map.extent.xmax) / 2, y: this._map.extent.ymin });
  100. // north value (set longitude to be half of Canada extent (141° W, 52° W))
  101. const pointA = { x: -96, y: 90 };
  102. // set info on longitude and latitude
  103. const dLon = (pointB.x - pointA.x) * Math.PI / 180;
  104. const lat1 = pointA.y * Math.PI / 180;
  105. const lat2 = pointB.y * Math.PI / 180;
  106. // calculate bearing
  107. const y = Math.sin(dLon) * Math.cos(lat2);
  108. const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
  109. const bearing = Math.atan2(y, x) * 180 / Math.PI;
  110. // return angle (180 is pointiong north)
  111. return ((bearing + 360) % 360).toFixed(1);
  112. }
  113. /**
  114. * Calculate distance between min and max extent to know the pixel ratio between
  115. * screen size and earth distance.
  116. * http://www.movable-type.co.uk/scripts/latlong.html
  117. * @function getScaleRatio
  118. * @param {Number} mapWidth optional the map width to use to calculate ratio
  119. * @returns {Object} contain information about the scale
  120. * - distance: distance between min and max extentId
  121. * - ratio: measure for 1 pixel in earth distance
  122. * - units: array of units [metric, imperial]
  123. */
  124. getScaleRatio (mapWidth = 0) {
  125. const map = this._map;
  126. // get left and right maximum value point to calculate distance from
  127. const pointA = geoApi.proj.localProjectPoint(map.spatialReference, 'EPSG:4326',
  128. { x: map.extent.xmin, y: (map.extent.ymin + map.extent.ymax) / 2 });
  129. const pointB = geoApi.proj.localProjectPoint(map.spatialReference, 'EPSG:4326',
  130. { x: map.extent.xmax, y: (map.extent.ymin + map.extent.ymax) / 2 });
  131. // Haversine formula to calculate distance
  132. const R = 6371e3; // earth radius in meters
  133. const rad = Math.PI / 180;
  134. const phy1 = pointA.y * rad; // radiant
  135. const phy2 = pointB.y * rad; // radiant
  136. const deltaPhy = (pointB.y - pointA.y) * rad; // radiant
  137. const deltaLambda = (pointB.x - pointA.x) * rad; // radiant
  138. const a = Math.sin(deltaPhy / 2) * Math.sin(deltaPhy / 2) +
  139. Math.cos(phy1) * Math.cos(phy2) *
  140. Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2);
  141. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  142. const d = (R * c);
  143. // set map / image width (if mapWidth = 0, use map.width)
  144. const width = mapWidth ? mapWidth : map.width;
  145. // get unit from distance, set distance and ratio (earth size for 1 pixel)
  146. const units = [(d > 1000) ? 'km' : 'm', (d > 1600) ? 'mi' : 'ft'];
  147. const distance = (d > 1000) ? d / 1000 : d;
  148. const ratio = distance / width;
  149. return { distance, ratio, units };
  150. }
  151. /**
  152. * Compares to sets of co-ordinates for extents (valid for both x and y). If center of input co-ordinates falls outside
  153. * map co-ordiantes, function will adjust them so the center is inside the map co-ordinates.
  154. *
  155. * @function clipExtentCoords
  156. * @private
  157. * @param {Numeric} mid middle of the the range to test
  158. * @param {Numeric} max maximum value of the range to test
  159. * @param {Numeric} min minimum value of the range to test
  160. * @param {Numeric} mapMax maximum value of the map range
  161. * @param {Numeric} mapMin minimum value of the map range
  162. * @param {Numeric} len length of the adjusted range, if adjusted
  163. * @return {Array} two element array of Numeric, containing result max and min values
  164. */
  165. static clipExtentCoords (mid, max, min, mapMax, mapMin, len) {
  166. if (mid > mapMax) {
  167. [max, min] = [mapMax, mapMax - len];
  168. } else if (mid < mapMin) {
  169. [max, min] = [mapMin + len, mapMin];
  170. }
  171. return [max, min];
  172. }
  173. /**
  174. * Checks if the center of the given extent is outside of the maximum extent. If it is,
  175. * will determine an adjusted extent with a center inside the maximum extent. Returns both
  176. * an indicator flag if an adjustment happened, and the adjusted extent.
  177. *
  178. * @function enforceBoundary
  179. * @param {Object} extent an ESRI extent to test
  180. * @param {Object} maxExtent an ESRI extent indicating the boundary of the map
  181. * @return {Object} an object with two properties. adjusted - boolean, true if extent was adjusted. newExtent - object, adjusted ESRI extent
  182. */
  183. static enforceBoundary (extent, maxExtent) {
  184. // clone extent
  185. const newExtent = esriBundle.Extent(extent.toJson());
  186. // determine dimensions of adjusted extent.
  187. // same as input, unless input is so large it consumes max.
  188. // in that case, we shrink to the max. This avoids the "washing machine"
  189. // bug where we over-correct past the valid range,
  190. // and achieve infinite oscillating pans
  191. const height = Math.min(extent.getHeight(), maxExtent.getHeight());
  192. const width = Math.min(extent.getWidth(), maxExtent.getWidth());
  193. const center = extent.getCenter();
  194. [newExtent.xmax, newExtent.xmin] =
  195. this.clipExtentCoords(center.x, newExtent.xmax, newExtent.xmin, maxExtent.xmax, maxExtent.xmin, width);
  196. [newExtent.ymax, newExtent.ymin] =
  197. this.clipExtentCoords(center.y, newExtent.ymax, newExtent.ymin, maxExtent.ymax, maxExtent.ymin, height);
  198. return {
  199. newExtent,
  200. adjusted: !extent.contains(newExtent) // true if we adjusted the extent
  201. };
  202. }
  203. initOverviewMap () {
  204. this.overviewMap = new esriBundle.OverviewMap({ map: this._map, expandFactor: 1, visible: true });
  205. this.overviewMap.startup();
  206. }
  207. resetOverviewMap () {
  208. this.overviewMap.destroy();
  209. this.initOverviewMap();
  210. }
  211. /**
  212. * Changes the zoom level by the specified value relative to the current level; can be negative.
  213. * To avoid multiple chained zoom animations when rapidly pressing the zoom in/out icons, we
  214. * update the zoom level only when the one before it resolves with the net zoom change.
  215. *
  216. * @function shiftZoom
  217. * @param {number} byValue a number of zoom levels to shift by
  218. */
  219. shiftZoom (byValue) {
  220. this.zoomCounter += byValue;
  221. this.zoomPromise.then(() => {
  222. if (this.zoomCounter !== 0) {
  223. const zoomValue = this._map.getZoom() + this.zoomCounter;
  224. const zoomPromise = Promise.resolve(this.setZoom(zoomValue));
  225. this.zoomCounter = 0;
  226. // undefined signals we've zoomed in/out as far as we can
  227. if (typeof zoomPromise !== 'undefined') {
  228. this.zoomPromise = zoomPromise;
  229. }
  230. }
  231. });
  232. }
  233. /**
  234. * Sets or gets map default config values.
  235. *
  236. * @function mapDefault
  237. * @param {String} key name of the default property
  238. * @param {Any} [value] optional value to set for the specified default property
  239. */
  240. mapDefault (key, value) {
  241. if (typeof value === 'undefined') {
  242. return esriBundle.esriConfig.defaults.map[key];
  243. } else {
  244. esriBundle.esriConfig.defaults.map[key] = value;
  245. }
  246. }
  247. /**
  248. * Set proxy service URL to avoid same origin issues.
  249. */
  250. set proxy (proxyUrl) { esriBundle.esriConfig.defaults.io.proxyUrl = proxyUrl; }
  251. get proxy () { return esriBundle.esriConfig.defaults.io.proxyUrl; }
  252. set basemapGallery (val) { this._basemapGallery = val; }
  253. get basemapGallery () { return this._basemapGallery; }
  254. set scalebar (val) { this._scalebar = val; }
  255. get scalebar () { return this._scalebar; }
  256. set overviewMap (val) { this._overviewMap = val; }
  257. get overviewMap () { return this._overviewMap; }
  258. }
  259. return Map;
  260. }
  261. /**
  262. * The `MapManager` module exports an object with the following properties:
  263. * - `Extent` esri/geometry type
  264. * - `Map` esri/map type
  265. * - `OverviewMap` esri/dijit/OverviewMap type
  266. * - `Scalebar` sri/dijit/Scalebar type
  267. * - `getExtentFromSetting function to create an ESRI Extent object from extent setting JSON object.
  268. * - `setupMap` function that interates over config settings and apply logic for any items present.
  269. * - `setProxy` function to set proxy service URL to avoid same origin issues
  270. */
  271. // mapManager module, provides function to setup a map
  272. module.exports = (esriBundle, geoApi) => esriMap(esriBundle, geoApi);