/* global define, console, RAMP, $ */
/**
*
*
* @module RAMP
* @submodule Map
*/
/**
* Layer Loader class.
*
* Handles the asynchronous loading of map layers (excluding basemaps)
* This includes dealing with errors, and raising appropriate events when the layer loads
*
* @class LayerLoader
* @static
* @uses dojo/topic
* @uses dojo/_base/array
* @uses esri/geometry/Extent
* @uses esri/layers/GraphicsLayer
* @uses esri/tasks/GeometryService
* @uses esri/tasks/ProjectParameters
* @uses EventManager
* @uses FeatureClickHandler
* @uses FilterManager
* @uses GlobalStorage
* @uses LayerItem
* @uses Map
* @uses MapClickHandler
* @uses Ramp
* @uses Util
*/
define([
/* Dojo */
"dojo/topic", "dojo/_base/array",
/* ESRI */
"esri/layers/GraphicsLayer", "esri/tasks/GeometryService", "esri/tasks/ProjectParameters", "esri/geometry/Extent",
/* RAMP */
"ramp/eventManager", "ramp/map", "ramp/globalStorage", "ramp/featureClickHandler", "ramp/mapClickHandler", "ramp/ramp",
"ramp/filterManager", "ramp/layerItem",
/* Util */
"utils/util"],
function (
/* Dojo */
topic, dojoArray,
/* ESRI */
GraphicsLayer, GeometryService, ProjectParameters, EsriExtent,
/* RAMP */
EventManager, RampMap, GlobalStorage, FeatureClickHandler, MapClickHandler, Ramp,
FilterManager, LayerItem,
/* Util */
UtilMisc) {
"use strict";
var idCounter = 0;
/**
* Get an auto-generated layer id. This works because javascript is single threaded: if this gets called
* from a web-worker at some point it will need to be synchronized.
*
* @returns {String} an auto-generated layer id
*/
function nextId() {
idCounter += 1;
return 'rampAutoId_' + idCounter;
}
/**
* Will set a layerId's layer selector state to a new state.
*
* @method onLayerError
* @private
* @param {String} layerId config id of the layer
* @param {String} newState the state to set the layer to in the layer selector
* @param {Boolean} abortIfError if true, don't update state if current state is an error state
*/
function updateLayerSelectorState(layerId, newState, abortIfError) {
if (abortIfError) {
var layerState;
layerState = FilterManager.getLayerState(layerId);
//check if this layer is in an error state. if so, exit the function
if (layerState === LayerItem.state.ERROR) {
return;
}
}
//set layer selector to new state
FilterManager.setLayerState(layerId, newState);
}
/**
* Will remove a layer from the map, and adjust counts.
*
* @method onLayerError
* @private
* @param {String} layerId config id of the layer
*/
function removeFromMap(layerId) {
var map = RampMap.getMap(),
bbLayer = RampMap.getBoundingBoxMapping()[layerId],
layer = map._layers[layerId];
if (layer) {
map.removeLayer(layer);
} else {
//layer was kicked out of the map. grab it from the registry
layer = RAMP.layerRegistry[layerId];
}
//if bounding box exists, and is in the map, remove it too
if (bbLayer) {
if (map._layers[bbLayer.id]) {
map.removeLayer(bbLayer);
RAMP.layerCounts.bb -= 1;
}
}
//just incase its really weird and layer is not in the registry
if (layer) {
//adjust layer counts
switch (layer.ramp.type) {
case GlobalStorage.layerType.wms:
if (layer.ramp.load.inCount) {
RAMP.layerCounts.wms -= 1;
layer.ramp.load.inCount = false;
}
break;
case GlobalStorage.layerType.feature:
case GlobalStorage.layerType.Static:
if (layer.ramp.load.inCount) {
RAMP.layerCounts.feature -= 1;
layer.ramp.load.inCount = false;
}
break;
}
}
}
/**
* This function initiates the loading of an ESRI layer object to the map.
* Will add it to the map in the appropriate spot, wire up event handlers, and generate any bounding box layers
* Note: a point of confusion. The layer objects "load" event may have already finished by the time this function is called.
* This means the object's constructor has initialized itself with the layers data source.
* This functions is not event triggered to guarantee the order in which things are added.
*
* @method _loadLayer
* @private
* @param {Object} layer an instantiated, unloaded ESRI layer object
* @param {Integer} reloadIndex Optional. If reloading a layer, supply the index it should reside at. Do not set for new layers
*/
function _loadLayer(layer, reloadIndex) {
var insertIdx,
layerSection,
map = RampMap.getMap(),
layerConfig = layer.ramp.config,
lsState;
if (!layer.ramp) {
console.log('you failed to supply a ramp.type to the layer!');
}
//derive section
switch (layer.ramp.type) {
case GlobalStorage.layerType.wms:
layerSection = GlobalStorage.layerType.wms;
if (UtilMisc.isUndefined(reloadIndex)) {
insertIdx = RAMP.layerCounts.base + RAMP.layerCounts.wms;
// generate wms legend image url and store in the layer config
if (layerConfig.legendMimeType) {
layer.ramp.config.legend = {
type: "wms",
imageUrl: String.format("{0}?SERVICE=WMS&REQUEST=GetLegendGraphic&TRANSPARENT=true&VERSION=1.1.1&FORMAT={2}&LAYER={3}",
layerConfig.url,
layer.version,
layerConfig.legendMimeType,
layerConfig.layerName
)
};
}
} else {
insertIdx = reloadIndex;
}
RAMP.layerCounts.wms += 1;
layer.ramp.load.inCount = true;
break;
case GlobalStorage.layerType.feature:
case GlobalStorage.layerType.Static:
layerSection = GlobalStorage.layerType.feature;
if (UtilMisc.isUndefined(reloadIndex)) {
//NOTE: these static layers behave like features, in that they can be in any position and be re-ordered.
insertIdx = RAMP.layerCounts.feature;
} else {
insertIdx = reloadIndex;
}
RAMP.layerCounts.feature += 1;
layer.ramp.load.inCount = true;
break;
}
//derive initial state
switch (layer.ramp.load.state) {
case "loaded":
lsState = LayerItem.state.LOADED;
break;
case "loading":
lsState = LayerItem.state.LOADING;
break;
case "error":
lsState = LayerItem.state.ERROR;
break;
}
//add entry to layer selector
if (UtilMisc.isUndefined(reloadIndex)) {
FilterManager.addLayer(layerSection, layer.ramp.config, lsState);
} else {
updateLayerSelectorState(layerConfig.id, lsState);
}
layer.ramp.load.inLS = true;
//sometimes the ESRI api will kick a layer out of the map if it errors after the add process.
//store a pointer here so we can find it (and it's information)
RAMP.layerRegistry[layer.id] = layer;
//add layer to map, triggering the loading process. should add at correct position
map.addLayer(layer, insertIdx);
//this will force a recreation of the highlighting graphic group.
//if not done, can cause mouse interactions to get messy if adding more than
//one layer at one time
topic.publish(EventManager.Map.REORDER_END);
//wire up event handlers to the layer
switch (layer.ramp.type) {
case GlobalStorage.layerType.wms:
// WMS binding for getFeatureInfo calls
if (!UtilMisc.isUndefined(layerConfig.featureInfo)) {
MapClickHandler.registerWMSClick({ wmsLayer: layer, layerConfig: layerConfig });
}
break;
case GlobalStorage.layerType.feature:
//TODO consider the case where a layer was loaded by the user, and we want to disable things like maptips?
//wire up click handler
layer.on("click", function (evt) {
evt.stopImmediatePropagation();
FeatureClickHandler.onFeatureSelect(evt);
});
//wire up mouse over / mouse out handler
layer.on("mouse-over", function (evt) {
FeatureClickHandler.onFeatureMouseOver(evt);
});
layer.on("mouse-out", function (evt) {
FeatureClickHandler.onFeatureMouseOut(evt);
});
//generate bounding box
//if a reload, the bounding box still exists from the first load
if (UtilMisc.isUndefined(reloadIndex)) {
var boundingBoxExtent,
boundingBox = new GraphicsLayer({
id: String.format("boundingBoxLayer_{0}", layer.id),
visible: layerConfig.settings.boundingBoxVisible
});
boundingBox.ramp = { type: GlobalStorage.layerType.BoundingBox };
//TODO test putting this IF before the layer creation, see what breaks. ideally if there is no box, we should not make a layer
if (!UtilMisc.isUndefined(layerConfig.layerExtent)) {
boundingBoxExtent = new EsriExtent(layerConfig.layerExtent);
if (UtilMisc.isSpatialRefEqual(boundingBoxExtent.spatialReference, map.spatialReference)) {
//layer is in same projection as basemap. can directly use the extent
boundingBox.add(UtilMisc.createGraphic(boundingBoxExtent));
} else {
//layer is in different projection. reproject to basemap
var box = RampMap.localProjectExtent(boundingBoxExtent, map.spatialReference);
boundingBox.add(UtilMisc.createGraphic(box));
//Geometry Service Version. Makes a more accurate bounding box, but requires an arcserver
/*
var params = new ProjectParameters(),
gsvc = new GeometryService(RAMP.config.geometryServiceUrl);
params.geometries = [boundingBoxExtent];
params.outSR = map.spatialReference;
gsvc.project(params, function (projectedExtents) {
console.log('esri says: ' + JSON.stringify(projectedExtents[0]));
console.log('proj4 says: ' + JSON.stringify(box));
});
*/
}
}
//add mapping to bounding box
RampMap.getBoundingBoxMapping()[layer.id] = boundingBox;
//bounding boxes are on top of feature layers
insertIdx = RAMP.layerCounts.feature + RAMP.layerCounts.bb;
RAMP.layerCounts.bb += 1;
map.addLayer(boundingBox, insertIdx);
}
break;
}
}
return {
/**
* Initializes properties. Set up event listeners
*
* @method init
*/
init: function () {
//counters for layers loaded, so we know where to insert things
//default basemap count to 1, as we always load 1 to begin with
RAMP.layerCounts = {
feature: 0,
bb: 0,
wms: 0,
base: 1
};
RAMP.layerRegistry = {};
topic.subscribe(EventManager.LayerLoader.LAYER_LOADED, this.onLayerLoaded);
topic.subscribe(EventManager.LayerLoader.LAYER_UPDATED, this.onLayerUpdateEnd);
topic.subscribe(EventManager.LayerLoader.LAYER_UPDATING, this.onLayerUpdateStart);
topic.subscribe(EventManager.LayerLoader.LAYER_ERROR, this.onLayerError);
topic.subscribe(EventManager.LayerLoader.REMOVE_LAYER, this.onLayerRemove);
topic.subscribe(EventManager.LayerLoader.RELOAD_LAYER, this.onLayerReload);
},
/**
* Deals with a layer that had an error when it tried to load.
*
* @method onLayerError
* @param {Object} evt
* @param {Object} evt.layer the layer object that failed
* @param {Object} evt.error the error object
*/
onLayerError: function (evt) {
console.log("failed to load layer " + evt.layer.url);
console.log(evt.error.message);
evt.layer.ramp.load.state = "error";
var layerId = evt.layer.id;
//get that failed layer outta here
removeFromMap(layerId);
//if layer is in layer selector, update the status
if (evt.layer.ramp.load.inLS) {
updateLayerSelectorState(evt.layer.ramp.config.id, LayerItem.state.ERROR, false);
}
},
/**
* Reacts when a layer begins to update. This happens when a feature layer starts to download its data.
* Data download doesn't start until points are made visible. It also happens when a WMS requests a new picture.
*
* @method onLayerUpdateStart
* @param {Object} evt
* @param {Object} evt.layer the layer object that loaded
*/
onLayerUpdateStart: function (evt) {
//console.log("LAYER UPDATE START: " + evt.layer.url);
updateLayerSelectorState(evt.layer.ramp.config.id, LayerItem.state.UPDATING, true);
},
/**
* Reacts when a layer has updated successfully. This means the layer has pulled its data and displayed it.
*
* @method onLayerUpdateEnd
* @param {Object} evt
* @param {Object} evt.layer the layer object that loaded
*/
onLayerUpdateEnd: function (evt) {
updateLayerSelectorState(evt.layer.ramp.config.id, LayerItem.state.LOADED, true);
},
/**
* Reacts when a layer has loaded successfully. This means the site has shaken hands with the layer and it seems ok.
* This does not mean data has been downloaded
*
* @method onLayerLoaded
* @param {Object} evt
* @param {Object} evt.layer the layer object that loaded
*/
onLayerLoaded: function (evt) {
//set state to loaded
//if we have a row in layer selector, update it to loaded (unless already in error)
//set flags that we have loaded ok
evt.layer.ramp.load.state = "loaded";
//if a row already exists in selector, set it to LOADED state. (unless already in error state)
if (evt.layer.ramp.load.inLS) {
updateLayerSelectorState(evt.layer.ramp.config.id, LayerItem.state.LOADED, true);
}
console.log("layer loaded: " + evt.layer.url);
},
/**
* Reacts to a request for a layer to be removed. Usually the case when a layer errors and the user clicks remove.
*
* @method onLayerRemove
* @param {Object} evt
* @param {Object} evt.layerId the layer id to be removed
*/
onLayerRemove: function (evt) {
var map = RampMap.getMap(),
layer,
configIdx,
layerSection,
configCollection;
layer = map._layers[evt.layerId]; //map.getLayer is not reliable, so we use this
if (UtilMisc.isUndefined(layer)) {
//layer was kicked out of the map. grab it from the registry
layer = RAMP.layerRegistry[evt.layerId];
}
//derive section layer is in and the config collection it is in
switch (layer.ramp.type) {
case GlobalStorage.layerType.wms:
layerSection = GlobalStorage.layerType.wms;
configCollection = RAMP.config.layers.wms;
break;
case GlobalStorage.layerType.feature:
case GlobalStorage.layerType.Static:
layerSection = GlobalStorage.layerType.feature;
configCollection = RAMP.config.layers.feature;
break;
}
//remove item from layer selector
FilterManager.removeLayer(layerSection, evt.layerId);
removeFromMap(evt.layerId);
//remove node from config
configIdx = configCollection.indexOf(layer.ramp.config);
configCollection.splice(configIdx, 1);
RAMP.layerRegistry[evt.layerId] = undefined;
},
/**
* Reacts to a request for a layer to be reloaded. Usually the case when a layer errors and user wants to try again
*
* @method onLayerRemove
* @param {Object} evt
* @param {Object} evt.layerId the layer id to be reloaded
*/
onLayerReload: function (evt) {
var map = RampMap.getMap(),
layer,
layerConfig,
user,
newLayer,
layerIndex,
inMap,
layerList,
idArray,
cleanIdArray;
layer = map._layers[evt.layerId]; //map.getLayer is not reliable, so we use this
if (layer) {
inMap = true;
} else {
//layer was kicked out of the map. grab it from the registry
inMap = false;
layer = RAMP.layerRegistry[evt.layerId];
}
layerConfig = layer.ramp.config;
user = layer.ramp.user;
//figure out index of layer
//since the layer may not be in the map, we have to use some trickery to derive where it is sitting
//get our list of layers
layerList = $("#" + RAMP.config.divNames.filter).find("#layerList").find("> li > ul");
//make an array of the ids in order of the list on the page
idArray = layerList
.map(function (i, elm) { return $(elm).find("> li").toArray().reverse(); }) // for each layer list, find its items and reverse their order
.map(function (i, elm) { return elm.id; });
cleanIdArray = idArray.filter(function (i, elm) {
//check if layer is in error state. error layers should not be part of the count. exception being the layer we are reloading
return ((FilterManager.getLayerState(elm) !== LayerItem.state.ERROR) || (elm === evt.layerId));
});
//find where our index is
layerIndex = dojoArray.indexOf(cleanIdArray, evt.layerId);
if (layer.ramp.type === GlobalStorage.layerType.wms) {
//adjust for wms, as it's in a different layer list on the map
layerIndex = layerIndex + RAMP.layerCounts.base - RAMP.layerCounts.feature;
}
//remove layer from map
if (inMap) {
map.removeLayer(layer);
}
//generate new layer
switch (layer.ramp.type) {
case GlobalStorage.layerType.wms:
newLayer = RampMap.makeWmsLayer(layerConfig, user);
break;
case GlobalStorage.layerType.feature:
newLayer = RampMap.makeFeatureLayer(layerConfig, user);
break;
case GlobalStorage.layerType.Static:
newLayer = RampMap.makeStaticLayer(layerConfig, user);
break;
}
//load the layer at the previous index
console.log("Reloading Layer at index " + layerIndex.toString());
_loadLayer(newLayer, layerIndex);
},
/**
* Public endpoint to initiate the loading of an ESRI layer object to the map.
*
* @method loadLayer
* @param {Object} layer an instantiated, unloaded ESRI layer object
* @param {Integer} reloadIndex Optional. If reloading a layer, supply the index it should reside at. Do not set for new layers
*/
loadLayer: function (layer, reloadIndex) {
_loadLayer(layer, reloadIndex);
},
nextId: nextId
};
});