/*global define, esri */
/**
*
* A RAMP Map module with ESRI and Dojo AMD Modules
* This module provides function to create an ESRI web map control. A global config object is
* required to initialize the map.
*
* @module RAMP
* @submodule Map
* @main Map
*/
/**
* Map class represents the ESRI map object. The map is generated based on the application configuration and templates.
*
* @class Map
* @uses dojo/_base/declare
* @uses dojo/_base/array
* @uses dojo/dom
* @uses dojo/dom-class
* @uses dojo/dom-construct
* @uses dojo/number
* @uses dojo/query
* @uses dojo/_base/lang
* @uses dojo/topic
* @uses dojo/on
* @uses esri/map
* @uses esri/layers/FeatureLayer
* @uses esri/layers/ArcGISTiledMapServiceLayer
* @uses esri/layers/ArcGISDynamicMapServiceLayer
* @uses esri/tasks/GeometryService
* @uses esri/tasks/ProjectParameters
* @uses esri/geometry/Polygon
* @uses esri/SpatialReference
* @uses esri/dijit/Scalebar
* @uses esri/geometry/Extent
* @uses esri/graphicsUtils
* @uses GlobalStorage
* @uses RAMP
* @uses FeatureClickHandler
* @uses Util
* @uses Array
* @uses Dictionary
* @uses GUI
* @uses Navigation
* @uses EventManager
*/
define([
/* Dojo */
"dojo/_base/declare", "dojo/_base/array", "dojo/dom", "dojo/dom-class",
"dojo/dom-construct", "dojo/number", "dojo/query", "dojo/_base/lang", "dojo/topic", "dojo/on",
/* Esri */
"esri/map", "esri/layers/FeatureLayer", "esri/layers/ArcGISTiledMapServiceLayer", "esri/layers/ArcGISDynamicMapServiceLayer",
"esri/tasks/GeometryService", "esri/tasks/ProjectParameters", "esri/geometry/Polygon", "esri/SpatialReference",
"esri/dijit/Scalebar", "esri/geometry/Extent", "esri/graphicsUtils",
/* Ramp */
"ramp/globalStorage", "ramp/ramp", "ramp/featureClickHandler", "ramp/navigation", "ramp/eventManager",
/* Util */
"utils/util", "utils/array", "utils/dictionary"],
function (
/* Dojo */
declare, dojoArray, dom, domClass, domConstruct, number, query, dojoLang, topic, dojoOn,
/* Esri */
EsriMap, FeatureLayer, ArcGISTiledMapServiceLayer, ArcGISDynamicMapServiceLayer,
GeometryService, ProjectParameters, Polygon, SpatialReference,
EsriScalebar, EsriExtent, esriGraphicUtils,
/* Ramp */
GlobalStorage, Ramp, FeatureClickHandler, Navigation, EventManager,
/* Util */
utilMisc, utilArray, utilDict) {
"use strict";
/**
* An Array of {{#crossLink "Esri/layer/FeatureLayer"}}{{/crossLink}} objects.
*
* @private
* @property featureLayers {Array}
*/
var featureLayers,
/**
* Maps graphicsLayerId to a GraphicsLayer Object that represents an extent bounding box.
* A dictionary of String, {{#crossLink "Esri/layer/GraphicsLayer"}}{{/crossLink}} pairs.
*
* @private
* @property attributeLayers {Object}
*/
attributeLayers,
/**
* The map not only contains feature layers, but also other layers such as the
* basemap layer, highlight layer, bounding box layer, etc. This variable is
* used to store the starting index of the feature layers in the map.
*
* @private
* @property featureLayerStartIndex {Integer}
*/
featureLayerStartIndex,
map,
fullExtent,
maxExtent,
initExtent;
/**
* Shows the loading image.
*
* @private
* @method _showLoadingImg
* @param event {Object}
*/
function _showLoadingImg() {
$("#map-load-indicator").removeClass("hidden");
}
/**
* Hides the loading image.
*
* @private
* @method _hideLoadingImg
* @param event {Object}
*/
function _hideLoadingImg() {
$("#map-load-indicator").addClass("hidden");
}
/**
* Update Map Scale when zoom level changes
*
* @private
* @method _updateScale
* @param event {Object}
*/
function _updateScale(event) {
if (event.levelChange) {
var currentScale = number.format(event.lod.scale);
var scaleLabelText = "1 : " + currentScale;
domConstruct.empty('scaleLabel');
$("#scaleLabel").text(scaleLabelText);
}
}
/**
* Initialize Map Scale
*
* @private
* @method _initScale
* @param event {Object}
*/
function _initScale(event) {
var map = event.map;
var scaleDiv = domConstruct.create("div", {
id: "scaleDiv",
"class": "esriScalebarLabel"
});
$(scaleDiv).html("<span>Scale</span><br><span id='scaleLabel'><span/>");
var currentScale = number.format(map.getScale());
var scaleLabelText = "1 : " + currentScale;
domConstruct.place(scaleDiv, query(".esriScalebarRuler")[0], "before");
domConstruct.empty('scaleLabel');
$("#scaleLabel").text(scaleLabelText);
// Change the css class of the scale bar so it shows up against
// the map
topic.subscribe(EventManager.BasemapSelector.BASEMAP_CHANGED, function (attr) {
$(".esriScalebar > div").removeClass().addClass(attr.cssStyle);
});
}
/**
* Republishes map events to the outside using topic.publish
*
* @method _initRepublishers
* @param {Object} map object
* @private
*/
function _initRepublishers(map) {
var prefix = "map";
/**
* Republish map events using topic.publish
*
* @method republish
* @param {String} name
* @private
*/
function republish(name) {
map.on(name, function (event) {
topic.publish(prefix + "/" + name, event);
});
}
republish("update-end");
republish("extent-change");
republish("zoom-start");
republish("zoom-end");
republish("pan-start");
republish("pan-end");
}
/**
* Subscribe to external events (published using topic.publish)
* and react accordingly
*
* @method _initListeners
* @param map {Object} map object
* @private
*/
function _initListeners(map) {
/* SUBSCRIBED EVENTS */
topic.subscribe(EventManager.Map.CENTER_AT, function (event) {
map.centerAt(event.point);
});
topic.subscribe(EventManager.Map.CENTER_AND_ZOOM, function (event) {
var point = new esri.geometry.Point(event.graphic.geometry.x, event.graphic.geometry.y, map.spatialReference),
d = map.centerAndZoom(point, event.level); // Last parameter is the level
if (event.callback) {
d.then(event.callback);
}
});
topic.subscribe(EventManager.Map.SET_EXTENT, function (event) {
event.extent.spatialReference = map.spatialReference;
var d = map.setExtent(event.extent);
if (event.callback) {
d.then(event.callback);
}
});
/* NAVIGATION EVENTS */
topic.subscribe(EventManager.Navigation.PAN, function (event) {
// event.direction panUp, panDown, panLeft, panRight
// this same as calling map.panUp(), map.panDown(), map.panLeft(), map.panRight()
map[event.direction]();
});
topic.subscribe(EventManager.Navigation.ZOOM_STEP, function (event) {
map.setLevel(map.getLevel() + event.level);
});
topic.subscribe(EventManager.Navigation.ZOOM, function (event) {
map.setLevel(event.level);
});
topic.subscribe(EventManager.Navigation.FULL_EXTENT, function () {
map.setExtent(fullExtent);
});
/* GUI EVENTS */
topic.subscribe(EventManager.GUI.LAYOUT_CHANGE, function () {
map.resize(true);
});
// Unhighlight the point when the subpanel is collapsed
topic.subscribe(EventManager.GUI.SUBPANEL_CHANGE, function (eventArg) {
// unhighlight only if the panel closing is the details panel
if (!eventArg.visible && (eventArg.origin === "rampPopup" || eventArg.origin === "datagrid")) {
topic.publish(EventManager.FeatureHighlighter.HIGHLIGHT_HIDE, {});
}
});
/* START BOUNDING BOX TOGGLE */
topic.subscribe(EventManager.FilterManager.LAYER_VISIBILITY_TOGGLED, function (evt) {
var setTo = evt.node.checked;
var layerId = evt.node.value;
// either take url (would need mapping to layer on map),
// map id in config, graphic layer id
var layer = map.getLayer(layerId);
layer.setVisibility(setTo);
//loops through any static layers that are mapped to the feature layer being toggled
try {
dojoArray.forEach(GlobalStorage.LayerMap[layerId], function (staticLayer) {
var layer = map.getLayer(staticLayer);
layer.setVisibility(setTo);
});
}
catch (err) {
}
});
topic.subscribe(EventManager.FilterManager.BOX_VISIBILITY_TOGGLED, function (evt) {
var boundingBox = attributeLayers[evt.node.value];
boundingBox.setVisibility(evt.checked);
});
topic.subscribe(EventManager.FilterManager.GLOBAL_LAYER_VISIBILITY_TOGGLED, function (evt) {
dojoArray.forEach(featureLayers, function (layer) {
layer.setVisibility(evt.checked);
//loops through the all static layers added to the map. Uses the array that maps static layers to feature layers
try {
dojoArray.forEach(GlobalStorage.LayerMap[layer.id], function (staticLayer) {
var layer = map.getLayer(staticLayer);
layer.setVisibility(evt.checked);
});
}
catch (err) {
}
});
});
topic.subscribe(EventManager.FilterManager.GLOBAL_BOX_VISIBILITY_TOGGLED, function (evt) {
utilDict.forEachEntry(attributeLayers, function (key, layer) {
layer.setVisibility(evt.checked);
});
});
topic.subscribe(EventManager.FilterManager.SELECTION_CHANGED, function (evt) {
if (!featureLayerStartIndex) {
// Find the index of the first feature layer
featureLayerStartIndex = utilArray.indexOf(map.graphicsLayerIds, function (layerId) {
var layer = map.getLayer(layerId);
return layer.type && layer.type === "Feature Layer";
});
}
map.reorderLayer(map.getLayer(evt.id), featureLayerStartIndex + evt.index);
topic.publish(EventManager.Map.REORDER_END);
});
/* END BOUNDING BOX TOGGLE */
/* Add Layer subscription*/
topic.subscribe(EventManager.Map.ADD_LAYER, function () {
var type = dom.byId("addLayer-select-type").value;
var URL = dom.byId("addLayer-URL-input").value;
var opacity = dom.byId("addLayer-Opacity").value;
console.log(type + " | " + URL + " | " + opacity);
addStaticLayer(type, URL, opacity);
});
topic.subscribe(EventManager.Map.ADD_LAYER_READY, function (temp_layer) {
map.addLayer(temp_layer);
});
}
/**
* Creates event handlers for the map control: click, mouse-over, load, extent change, and update events.
* @method _initEventHandlers
* @param {Object} map A ESRI map object
*/
function _initEventHandlers(map) {
dojoArray.forEach(featureLayers, function (fl) {
//TODO: set timer for maptips onMouseOver event
fl.on("click", function (evt) {
evt.stopImmediatePropagation();
FeatureClickHandler.onFeatureSelect(evt);
});
fl.on("mouse-over", function (evt) {
FeatureClickHandler.onFeatureMouseOver(evt);
//console.log("hover on point", evt);
});
fl.on("mouse-out", function (evt) {
FeatureClickHandler.onFeatureMouseOut(evt);
});
});
map.on("load", _initScale);
map.on("extent-change", function (event) {
_updateScale(event);
console.log("map - >> extent-change");
dojoOn.once(map, "update-end", function () {
console.log("map - >> update-end CAUGHT!!");
topic.publish(EventManager.Datagrid.APPLY_EXTENT_FILTER);
});
});
// Deselect all highlighted points if the map is clicked
map.on("click", function (evt) {
FeatureClickHandler.onFeatureDeselect(evt);
});
// Hide all the maptips if the map finishes updating
map.on("update-end", function () {
//topic.publish(EventManager.Maptips.HIDE, {});
});
// Show/Hide spinner for map loading
map.on("update-start", _showLoadingImg);
map.on("update-end", _hideLoadingImg);
}
/**
* Instantiates an extent from a JSON config object and spatial reference
*
* @private
* @method createExtent
* @param extentConfig {Object} the JSON config object
* @param sr {Esri/SpatialReference} the {{#crossLink "Esri/SpatialReference"}}{{/crossLink}}
* @return {esri/geometry/Extent} An ESRI extent object based on the config data
*/
function createExtent(extentConfig, sr) {
return new EsriExtent(
extentConfig.xmin, extentConfig.ymin, extentConfig.xmax, extentConfig.ymax, sr);
}
/**
* Add a static, non-interactive Llyer to the map
* @method AddStaticLayer
* @param {String} layer_type A value which controls how the layer is going to be added to the map
* @param {String} layer_url A URL pointing to a valid map service endpoint
* @param {Number} layer_op A value between 0.0 and 1.0 which determines the transparency of the layer
*/
function addStaticLayer(layer_type, layer_url, layer_op) {
layer_op = layer_op / 100; // change percentage to decimal
var tempLayer;
switch (layer_type) {
case "feature":
tempLayer = new FeatureLayer(layer_url, {
opacity: layer_op,
mode: FeatureLayer.MODE_SNAPSHOT
});
break;
case "tile":
tempLayer = new ArcGISTiledMapServiceLayer(layer_url, {
opacity: layer_op
});
break;
case "dynamic":
tempLayer = new ArcGISDynamicMapServiceLayer(layer_url, {
opacity: layer_op
});
break;
default:
break;
}
topic.publish(EventManager.Map.ADD_LAYER_READY, tempLayer);
topic.publish(EventManager.GUI.ADD_LAYER_PANEL_CHANGE, {
visible: false
});
}
return {
/**
* The maximum extent of the map control is allowed to go to
* @property getMaxExtent
* @type {Object}
*
*/
getMaxExtent: function () {
return maxExtent;
},
/**
* Return the map control object
* @property getMap
* @type {Object}
*
*/
getMap: function () {
if (utilMisc.isUndefined(map)) {
console.log("trying to get map before it is available!");
}
return map;
},
/**
* Returns a list of feature layers that are currently visible on the map.
* @method getVisibleFeatureLayers
* @return {Array} an array of {{#crossLink "Esri/layer/FeatureLayer"}}{{/crossLink}} objects
*
*/
getVisibleFeatureLayers: function () {
// Return only the feature layers
//TODO do we need to consider static layers here?
return dojoArray.filter(map.getLayersVisibleAtScale(), function (layer) {
return layer.type && (layer.type === "Feature Layer");
});
},
/**
* Return the feature layer corresponding to the given url.
*
* @method getFeatureLayer
* @private
* @param featureUrl {String} the url of the feature layer
* @return {Esri/layer/FeatureLayer} feature layer
*/
getFeatureLayer: function (featureUrl) {
return utilArray.find(featureLayers,
function (featureLayer) {
return featureLayer.url === featureUrl;
});
},
/**
* Given an ESRI Extent Object, returns a new ESRI Extent Object that
* contains the extent adjusted according to this map's maximum extent
*
* NOTE: this method is currently unused!
*
* @param e {esri/geometry/Extent} the extent Object
* @param maxExtent {esri/geometry/Extent} the maximum extent
* @return {esri/geometry/Extent}An adjusted extent, if the target extent is outside the boundary
* @method checkBoundary
*/
checkBoundary: function (e, maxExtent) {
var extent = e,
width = extent.width(),
height = extent.height(),
centerX = extent.centerX(),
centerY = extent.centerY(),
flag, adjustedEx;
adjustedEx = extent.clone();
var maxHeight = maxExtent.height();
if (height > maxHeight) {
height = maxHeight;
}
if (centerY > maxExtent.ymax) {
adjustedEx.ymax = maxExtent.ymax;
adjustedEx.ymin = maxExtent.ymax - height;
flag = true;
//} else if (extent.ymin < maxExtent.ymin) {
} else if (centerY < maxExtent.ymin) {
adjustedEx.ymin = maxExtent.ymin;
adjustedEx.ymax = maxExtent.ymin + height;
flag = true;
}
var maxWidth = maxExtent.width();
if (width > maxWidth) {
width = maxWidth;
}
if (centerX > maxExtent.xmax) {
adjustedEx.xmax = maxExtent.xmax;
adjustedEx.xmin = maxExtent.xmax - width;
flag = true;
} else if (centerX < maxExtent.xmin) {
adjustedEx.xmin = maxExtent.xmin;
adjustedEx.xmax = maxExtent.xmin + width;
flag = true;
}
if (flag) {
return adjustedEx;
}
},
/*
* Initialize map control with configuration objects provided in the bootstrapper.js file.
*
* Initialize extent
* Add base map from the config.basemaps array that has the showOnInit()
* Add Static layer from config.featureLayers.staticLayers
* Add feature layers from config.featureLayers
* Create bounding layers and add to map control
* Add map tip events to each feature layer (click/hover/out)
* Show scalebar
* Publish events to outside for other modules to use
* Subscribe events to update map control
*
* Note: Not sure if we want to include all the config requirements here.
* Map control is initialized with div id provided. The following config file entries are used:
* config.spatialReference
* config.extents.defaultExtent xmin, ymin, xmax, ymax
* config.levelOfDetails.minLevel
* config.levelOfDetails.maxLevel
* config.extents.maximumExtent
* config.extents.fullExtent
* config.basemaps arrays of basemap, only one or first one with showOnInit set to true
* config.featureLayers
*
* @method init
* @param {Object} mapDiv the HTML div that will store the map control
* @constructor
*
*/
init: function (mapDiv) {
//config object is loaded in bootstrapper.js
var config = GlobalStorage.config,
/**
* The spatial reference of the map
*
* @property spatialReference
* @private
* @type {esri/SpatialReference}
*/
spatialReference = new esri.SpatialReference({
wkid: config.spatialReference
}),
/**
* The URL of the basemap that is on by default
*
* @property url
* @private
* @type {String}
*/
url = utilArray.find(config.basemaps, function (basemap) {
return basemap.showOnInit;
}).url,
/**
* The basemap layer
*
* @property baseLayer
* @private
* @type {Esri/layers/ArcGISTiledMapServiceLayer}
*/
baseLayer = new ArcGISTiledMapServiceLayer(url, {
id: "basemapLayer"
});
/**
* The maximum extent of the map
*
* @property maxExtent
* @private
* @type {esri/geometry/Extent}
*/
maxExtent = createExtent(config.extents.maximumExtent, spatialReference);
/**
* The initial extent of the map
*
* @property InitExtent
* @private
* @type {esri/geometry/Extent}
*/
initExtent = createExtent(config.extents.defaultExtent, spatialReference);
/**
* Used for full extent in nav widget
*
* @property fullExtent
* @private
* @type {esri/geometry/Extent}
*/
fullExtent = createExtent(config.extents.fullExtent, spatialReference);
featureLayers = dojoArray.map(config.featureLayers, function (layer) {
var fl = new FeatureLayer(layer.url, {
id: String.format("featureLayer_{0}", Ramp.getLayerConfig(layer.url).displayName),
mode: FeatureLayer.MODE_SNAPSHOT,
outFields: [layer.layerAttributes]
});
return fl;
});
/**
* A list GraphicsLayer that represent the extent bounding box of the feature layers.
*
* @property boundingBoxLayers
* @type {array of esri/layer/GraphicsLayer}
* @param {[esr/layer/featurelayers]} featureLayers A list of feature layers found in the application config
* @return {[esri/layer/graphiclayer]} An array of graphic layers to add to the map
*/
var boundingBoxLayers = dojoArray.map(featureLayers, function (layer) {
// Map a list of featurelayers into a list of GraphicsLayer representing
// the extent bounding box of the feature layer (the bounding boxes are initialized
// separately, after the feature layers have finished loading (see attributeLayerAdder function
// below)
var attrLayer = new esri.layers.GraphicsLayer({
id: String.format("boundingBoxLayer_{0}", Ramp.getLayerConfig(layer.url).displayName),
visible: false // bounding boxes are not visible by default
});
return attrLayer;
});
//the map!
map = new EsriMap(mapDiv, {
extent: initExtent,
logo: false,
minZoom: config.levelOfDetails.minLevel,
maxZoom: config.levelOfDetails.maxLevel,
slider: false
});
// Add the basemap layers
map.addLayers([baseLayer]);
/* START - Add static layers */
var staticLayers = [];
var perLayerStaticMaps = [];
var staticLayerMap = [];
dojoArray.map(config.featureLayers, function (layer) {
perLayerStaticMaps = [];
dojoArray.forEach(layer.staticLayers, function (staticLayer, i) {
var tempLayer;
//determine layer type and process
switch (staticLayer.layerType) {
case "feature":
tempLayer = new FeatureLayer(staticLayer.url, {
opacity: staticLayer.opacity,
mode: FeatureLayer.MODE_SNAPSHOT,
id: "static_" + staticLayer.id
});
break;
case "tile":
tempLayer = new ArcGISTiledMapServiceLayer(staticLayer.url, {
opacity: staticLayer.opacity,
id: "static_" + staticLayer.id
});
console.log("tile layer added. " + "static_" + staticLayer.id);
break;
case "dynamic":
tempLayer = new ArcGISDynamicMapServiceLayer(staticLayer.url, {
opacity: staticLayer.opacity,
id: "static_" + staticLayer.id
});
console.log("dynamic layer added. " + "static_" + staticLayer.id);
break;
default:
//TODO add in other types of maps... wms? non-esri tile?
break;
}
staticLayers.push(tempLayer);
//creates an array of all static layers defined for the current, single feature layer
perLayerStaticMaps[i] = "static_" + staticLayer.id;
});
//adds the static layer id array as a value to an array indexed by feature layer names
staticLayerMap[String.format("featureLayer_{0}", Ramp.getLayerConfig(layer.url).displayName)] = perLayerStaticMaps;
});
//adds the static layers to the map and copies the static layer/feature layer mapping to the Global Config
map.addLayers(staticLayers);
GlobalStorage.LayerMap = staticLayerMap;
/* End - Add static layers */
// Combine the two layer arrays then add them all at once (for efficiency)
map.addLayers(boundingBoxLayers.concat(featureLayers));
// Maps graphicsLayerId to a GraphicsLayer Object that represents an extent bounding box
attributeLayers = {};
var attributeLayerAdder = function () {
dojoArray.forEach(featureLayers, function (layer, i) {
var boundingBoxExtent = esriGraphicUtils.graphicsExtent(layer.graphics);
// Make sure the boundingBoxExtent is within the max extent
// you want max for xmin, ymin and min for xmax, ymax because
// you want to make sure the extent is smaller than the maximum extent
boundingBoxExtent.xmin = Math.max(boundingBoxExtent.xmin, maxExtent.xmin);
boundingBoxExtent.ymin = Math.max(boundingBoxExtent.ymin, maxExtent.ymin);
boundingBoxExtent.xmax = Math.min(boundingBoxExtent.xmax, maxExtent.xmax);
boundingBoxExtent.ymax = Math.min(boundingBoxExtent.ymax, maxExtent.ymax);
var extentGraphic = new esri.Graphic({
"geometry": boundingBoxExtent,
"symbol": {
"color": [255, 0, 0, 64],
"outline": {
"color": [240, 128, 128, 255],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSFS",
"style": "esriSFSSolid"
}
});
boundingBoxLayers[i].add(extentGraphic);
attributeLayers[layer.id] = boundingBoxLayers[i];
});
};
// Add the attribute layers after the map finishes adding the feature layers
// makes sure this listener only fires once
dojoOn.once(map, "update-end", attributeLayerAdder);
/* Start - Show scalebar */
var scalebar = new EsriScalebar({
map: map,
attachTo: "bottom-left",
scalebarUnit: "metric"
});
scalebar.show();
/* End - Show scalebar */
_initRepublishers(map);
_initListeners(map);
_initEventHandlers(map, featureLayers);
}
};
});