Reusable Accessible Mapping Platform

API Docs for: 5.3.2
Show:

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

/* global define, console, RAMP, $, i18n */

/**
*
*
* @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
*
* ####Imports RAMP Modules:
* {{#crossLink "EventManager"}}{{/crossLink}}
* {{#crossLink "FeatureClickHandler"}}{{/crossLink}}
* {{#crossLink "FilterManager"}}{{/crossLink}}
* {{#crossLink "GlobalStorage"}}{{/crossLink}}
* {{#crossLink "LayerItem"}}{{/crossLink}}
* {{#crossLink "Map"}}{{/crossLink}}
* {{#crossLink "MapClickHandler"}}{{/crossLink}}
* {{#crossLink "Ramp"}}{{/crossLink}}
* {{#crossLink "Util"}}{{/crossLink}}
*
* @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
*/

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 updateLayerSelectorState
        * @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
        * @param  {Object} [options] additional options for layer item (mostly error messages in this case)
        */
        function updateLayerSelectorState(layerId, newState, abortIfError, options) {
            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, options);
        }

        /**
        * 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,
                options = {},
                isNotReload = typeof reloadIndex === 'undefined';

            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 (isNotReload) {
                        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 (isNotReload) {
                        //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;
            }

            //add layer to map, triggering the loading process.  should add at correct position
            //do this before creating layer selector item, as the layer selector inspects the map
            //object to make state decisions
            map.addLayer(layer, insertIdx);

            //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)
            // TODO - this write to layer registry should be refactored into a call to state manager
            RAMP.layerRegistry[layer.id] = layer;

            // publish LAYER_ADDED event for every user-added layer
            topic.publish(EventManager.LayerLoader.LAYER_ADDED, { layer: layer });

            //derive initial state
            switch (layer.ramp.load.state) {
                case "loaded":
                    lsState = LayerItem.state.LOADED;
                    break;
                case "loading":
                    //IE10 hack. since IE10 will not fire the loaded event, check the loaded flag of the layer object
                    if (layer.loaded) {
                        lsState = LayerItem.state.LOADED;
                    } else {
                        lsState = LayerItem.state.LOADING;
                    }
                    break;
                case "error":
                    options.notices = {
                        error: {
                            message: i18n.t("filterManager.notices.error.connect")
                        }
                    };

                    lsState = LayerItem.state.ERROR;
                    break;
            }

            //add entry to layer selector
            if (isNotReload) {
                options.state = lsState; // pass initial state in the options object
                FilterManager.addLayer(layerSection, layer.ramp, options);
            } else {
                updateLayerSelectorState(layerConfig.id, lsState, false, options);
            }
            layer.ramp.load.inLS = true;

            //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 (layerConfig.featureInfo) {
                        MapClickHandler.registerWMSClick(layer);
                    }
                    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 (isNotReload) {
                        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 (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.error("failed to load layer " + evt.layer.url, evt.error);

                evt.layer.ramp.load.state = "error";

                var layerId = evt.layer.id,
                    // generic error notice
                    errorMessage = i18n.t("filterManager.notices.error.load"),
                    options;

                //get that failed layer outta here
                removeFromMap(layerId);

                // customize error notices based on the error type as much as possible
                if (evt.error.code === 400) {
                    errorMessage = i18n.t("filterManager.notices.error.draw");
                }

                options = {
                    notices: {
                        error: {
                            message: errorMessage
                        }
                    }
                };

                //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, options);
                }
            },

            /**
            * 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) {
                //IE10 hack.  since IE10 doesn't fire a loaded event, we need to also set the loaded flag on layer here.
                //            don't do it if it's in error state.  once an error, always an error
                if (evt.layer.ramp.load.state !== "error") {
                    evt.layer.ramp.load.state = "loaded";
                }
                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 (!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(),
                    curlayer,
                    layerConfig,
                    user,
                    newLayer,
                    layerIndex,
                    inMap,
                    layerList,
                    idArray,
                    cleanIdArray;

                removeFromMap(evt.layerId);

                curlayer = map._layers[evt.layerId];  //map.getLayer is not reliable, so we use this

                if (curlayer) {
                    inMap = true;
                } else {
                    //layer was kicked out of the map.  grab it from the registry
                    inMap = false;
                    curlayer = RAMP.layerRegistry[evt.layerId];
                }

                layerConfig = curlayer.ramp.config;
                user = curlayer.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 (curlayer.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(curlayer);
                }

                //generate new layer
                switch (curlayer.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
        };
    });