undefined

API Docs for: 5.4.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 "AttributeLoader"}}{{/crossLink}}
* {{#crossLink "EventManager"}}{{/crossLink}}
* {{#crossLink "FeatureClickHandler"}}{{/crossLink}}
* {{#crossLink "FilterManager"}}{{/crossLink}}
* {{#crossLink "GlobalStorage"}}{{/crossLink}}
* {{#crossLink "GraphicExtension"}}{{/crossLink}}
* {{#crossLink "LayerItem"}}{{/crossLink}}
* {{#crossLink "Map"}}{{/crossLink}}
* {{#crossLink "MapClickHandler"}}{{/crossLink}}
* {{#crossLink "Util"}}{{/crossLink}}
*
* @class LayerLoader
* @static
* @uses dojo/topic
* @uses esri/geometry/Extent
* @uses esri/layers/GraphicsLayer
* @uses esri/tasks/GeometryService
* @uses esri/tasks/ProjectParameters
*/

define([
/* Dojo */
"dojo/topic",

/* 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/filterManager", "ramp/layerItem", "ramp/attributeLoader", "ramp/graphicExtension",

/* Util */
"utils/util"],

    function (
    /* Dojo */
    topic,

    /* ESRI */
    GraphicsLayer, GeometryService, ProjectParameters, EsriExtent,

    /* RAMP */
    EventManager, RampMap, GlobalStorage, FeatureClickHandler, MapClickHandler,
    FilterManager, LayerItem, AttributeLoader, GraphicExtension,

     /* 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.
        *
        * @method  nextId
        * @returns {String} an auto-generated layer id
        */
        function nextId() {
            idCounter += 1;
            return 'rampAutoId_' + idCounter;
        }

        /**
        * Get a layer config for a given layer ID.
        *
        * @method  getLayerConfig
        * @param   {String} layerId a RAMP layer ID
        * @returns {Object} a RAMP config object from the layer registry
        */
        function getLayerConfig(layerId) {
            var layer = RAMP.layerRegistry[layerId];
            return layer ? layer.ramp.config : null;
        }

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

        /**
        * Determines if the layer has an active hilight for the highlight type (defined by the state).
        *
        * @method isValidHilight
        * @private
        * @param  {Object} layer map layer object
        * @param  {Object} state the state of a highlight (defines if active and layer it applies to)
        * @returns {Boolean} if layer has valid highlight
        */
        function isValidHilight(layer, state) {
            var ret = false;
            if (state.objId >= 0) {
                //there is an active highlight
                if (layer.id === state.layerId) {
                    //it belongs to this layer
                    ret = true;
                }
            }
            return ret;
        }

        /**
        * Will remove a layer from the map, along with any related items (bounding box, feature data) and adjust counts.
        *
        * @method removeFromMap
        * @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;
                }
                delete RampMap.getBoundingBoxMapping()[layer.id];
            }

            //remove data, if it exists
            if (RAMP.data[layerId]) {
                delete RAMP.data[layerId];
            }

            //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;

            //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:

                    //initiate the feature data download
                    if (layer.url) {
                        //service based. get feature data from the service
                        AttributeLoader.loadAttributeData(layer.id, layer.url, layer.ramp.type);
                    } else {
                        //file based. scrape data from the layer
                        AttributeLoader.extractAttributeData(layer);
                    }

                    //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 (layerConfig.layerExtent) {
                        var boundingBoxExtent,
                            boundingBox = new GraphicsLayer({
                                id: String.format("boundingBoxLayer_{0}", layer.id),
                                visible: layerConfig.settings.boundingBoxVisible
                            });
                   
                        boundingBoxExtent = new EsriExtent(layerConfig.layerExtent);
                        // add ramp.user property to the bounding box as well
                        boundingBox.ramp = { type: GlobalStorage.layerType.BoundingBox, user: layer.ramp.user };

                        //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.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;
                    }
            }

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

        return {
            /**
            * Initializes properties.  Set up event listeners
            *
            * @method init
            */
            init: function () {
                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) {
                RampMap.updateDatagridUpdatingState(evt.layer, true);

                //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) {
                var g;

                //check if we have any active highlites for this layer
                if (isValidHilight(evt.layer, RAMP.state.hilite.click)) {
                    //re-request the click hilight
                    g = GraphicExtension.findGraphic(RAMP.state.hilite.click.objId, evt.layer.id);
                    if (g) {
                        topic.publish(EventManager.FeatureHighlighter.HIGHLIGHT_SHOW, {
                            graphic: g
                        });
                    } //else graphic is off-view and does not exist in layer. dont' change higlight
                }

                if (isValidHilight(evt.layer, RAMP.state.hilite.zoom)) {
                    //re-request the zoom hilight
                    g = GraphicExtension.findGraphic(RAMP.state.hilite.zoom.objId, evt.layer.id);
                    if (g) {
                        topic.publish(EventManager.FeatureHighlighter.ZOOMLIGHT_SHOW, {
                            graphic: g
                        });
                    } //else graphic is off-view and does not exist in layer. dont' change higlight
                }

                //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";
                }

                RampMap.updateDatagridUpdatingState(evt.layer);

                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.  This removes the layer from the entire app.
            *
            * @method onLayerRemove
            * @param  {Object} evt
            * @param  {Object} evt.layerId the layer id to be removed
            */
            onLayerRemove: function (evt) {
                var layer,
                    configIdx,
                    layerSection,
                    configCollection;

                    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);

                delete RAMP.layerRegistry[evt.layerId];

                // publish LAYER_REMOVED event for every removed layer
                topic.publish(EventManager.LayerLoader.LAYER_REMOVED, { layer: layer, layerCounts: RAMP.layerCounts });
            },

            /**
            * Reacts to a request for a layer to be reloaded.  Usually the case when a layer errors and user wants to try again
            *
            * @method onLayerReload
            * @param  {Object} evt
            * @param  {Object} evt.layerId the layer id to be reloaded
            */
            onLayerReload: function (evt) {
                var curlayer,
                    layerConfig,
                    user,
                    newLayer,
                    layerIndex,
                    layerList,
                    idArray,
                    cleanIdArray;

                removeFromMap(evt.layerId);

                    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.toArray().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 = cleanIdArray.indexOf(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;
                }

                if (evt.mode) {
                    layerConfig.mode = evt.mode;
                }

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

            getLayerConfig: getLayerConfig,

            nextId: nextId
        };
    });