Source: Models/ArcGisMapServerCatalogGroup.js

'use strict';

/*global require*/
var URI = require('urijs');

var clone = require('terriajs-cesium/Source/Core/clone');
var defined = require('terriajs-cesium/Source/Core/defined');
var defineProperties = require('terriajs-cesium/Source/Core/defineProperties');
var freezeObject = require('terriajs-cesium/Source/Core/freezeObject');
var knockout = require('terriajs-cesium/Source/ThirdParty/knockout');
var loadJson = require('../Core/loadJson');
var when = require('terriajs-cesium/Source/ThirdParty/when');

var TerriaError = require('../Core/TerriaError');
var CatalogGroup = require('./CatalogGroup');
var inherit = require('../Core/inherit');
var proxyCatalogItemUrl = require('./proxyCatalogItemUrl');
var replaceUnderscores = require('../Core/replaceUnderscores');
var ArcGisMapServerCatalogItem = require('./ArcGisMapServerCatalogItem');

/**
 * A {@link CatalogGroup} representing a collection of layers from an ArcGIS Map Service.
 * Eg. http://www.ga.gov.au/gis/rest/services/earth_observation/Landcover_WM/MapServer
 *
 * @alias ArcGisMapServerCatalogGroup
 * @constructor
 * @extends CatalogGroup
 *
 * @param {Terria} terria The Terria instance.
 */
var ArcGisMapServerCatalogGroup = function(terria) {
    CatalogGroup.call(this, terria, 'esri-mapServer-group');

    /**
     * Gets or sets the URL of the Map Server.  This property is observable.
     * @type {String}
     */
    this.url = '';

    /**
     * Gets or sets a description of the custodian of the data sources in this group.
     * This property is an HTML string that must be sanitized before display to the user.
     * This property is observable.
     * @type {String}
     */
    this.dataCustodian = undefined;

    /**
     * Gets or sets a hash of names of blacklisted data layers.  A layer that appears in this hash
     * will not be shown to the user.  In this hash, the keys should be the Title of the layers to blacklist,
     * and the values should be "true".  This property is observable.
     * @type {Object}
     */
    this.blacklist = undefined;

    /**
     * Gets or sets a hash of properties that will be set on each child item.
     * For example, { 'treat404AsError': false }
     */
    this.itemProperties = undefined;

    knockout.track(this, ['url', 'dataCustodian', 'blacklist', 'itemProperties']);
};

inherit(CatalogGroup, ArcGisMapServerCatalogGroup);

defineProperties(ArcGisMapServerCatalogGroup.prototype, {
    /**
     * Gets the type of data member represented by this instance.
     * @memberOf ArcGisMapServerCatalogGroup.prototype
     * @type {String}
     */
    type : {
        get : function() {
            return 'esri-mapServer-group';
        }
    },

    /**
     * Gets a human-readable name for this type of data source, such as 'Web Map Service (WMS)'.
     * @memberOf ArcGisMapServerCatalogGroup.prototype
     * @type {String}
     */
    typeName : {
        get : function() {
            return 'ArcGIS Map Server Group';
        }
    },

    /**
     * Gets the set of functions used to serialize individual properties in {@link CatalogMember#serializeToJson}.
     * When a property name on the model matches the name of a property in the serializers object lieral,
     * the value will be called as a function and passed a reference to the model, a reference to the destination
     * JSON object literal, and the name of the property.
     * @memberOf ArcGisMapServerCatalogGroup.prototype
     * @type {Object}
     */
    serializers : {
        get : function() {
            return ArcGisMapServerCatalogGroup.defaultSerializers;
        }
    }
});

/**
 * Gets or sets the set of default serializer functions to use in {@link CatalogMember#serializeToJson}.  Types derived from this type
 * should expose this instance - cloned and modified if necesary - through their {@link CatalogMember#serializers} property.
 * @type {Object}
 */
ArcGisMapServerCatalogGroup.defaultSerializers = clone(CatalogGroup.defaultSerializers);

ArcGisMapServerCatalogGroup.defaultSerializers.items = CatalogGroup.enabledShareableItemsSerializer;

ArcGisMapServerCatalogGroup.defaultSerializers.isLoading = function(wmsGroup, json, propertyName, options) {};

freezeObject(ArcGisMapServerCatalogGroup.defaultSerializers);

ArcGisMapServerCatalogGroup.prototype._getValuesThatInfluenceLoad = function() {
    return [this.url, this.blacklist];
};

ArcGisMapServerCatalogGroup.prototype._load = function() {
    return loadMapServer(this);
};

// loadMapServer is exposed so that ArcGisCatalogGroup can call it,
// to load a MapServer as if it were an ArcGisMapServerCatalogGroup.
ArcGisMapServerCatalogGroup.loadMapServer = function(catalogGroup) {
    return loadMapServer(catalogGroup);
};

function loadMapServer(catalogGroup) {
    function getJson(segment) {
        var uri = new URI(catalogGroup.url)
            .segment(segment)
            .addQuery('f', 'json');
        return loadJson(proxyCatalogItemUrl(catalogGroup, uri.toString(), '1d'));
    }
    var terria = catalogGroup.terria;
    return when.all([getJson(''), getJson('layers'), getJson('legend')]).then(function(result) {
        var serviceJson = result[0];
        var layersJson = result[1];
        var legendJson = result[2];

        // Is this really a MapServer REST response?
        if (!serviceJson || !serviceJson.layers || !layersJson || !layersJson.layers) {
            throw new TerriaError({
                title: 'Invalid ArcGIS Map Service',
                message: '\
An error occurred while invoking the ArcGIS Map Service.  The server\'s response does not appear to be a valid Map Service document.  \
<p>If you entered the link manually, please verify that the link is correct.</p>\
<p>If you did not enter this link manually, this error may indicate that the group you opened is temporarily unavailable or there is a \
problem with your internet connection.  Try opening the group again, and if the problem persists, please report it by \
sending an email to <a href="mailto:'+terria.supportEmail+'">'+terria.supportEmail+'</a>.</p>'
            });
        }

        var dataCustodian = catalogGroup.dataCustodian;
        if (!defined(dataCustodian) && defined(serviceJson.documentInfo) && defined(serviceJson.documentInfo.Author)) {
            dataCustodian = serviceJson.documentInfo.Author;
        }
        if (catalogGroup.name === 'Unnamed Item' && defined(serviceJson.mapName) && serviceJson.mapName.length > 0) {
            catalogGroup.name = serviceJson.mapName;
        }

        addLayersRecursively(catalogGroup, serviceJson, layersJson, legendJson, -1, layersJson.layers, catalogGroup, dataCustodian);
    }).otherwise(function(e) {
        throw new TerriaError({
            sender: catalogGroup,
            title: 'Group is not available',
            message: '\
An error occurred while invoking the ArcGIS Map Service. \
<p>If you entered the link manually, please verify that the link is correct.</p>\
<p>This error may also indicate that the server does not support <a href="http://enable-cors.org/" target="_blank">CORS</a>.  If this is your \
server, verify that CORS is enabled and enable it if it is not.  If you do not control the server, \
please contact the administrator of the server and ask them to enable CORS.  Or, contact the '+terria.appName+' \
team by emailing <a href="mailto:'+terria.supportEmail+'">'+terria.supportEmail+'</a> \
and ask us to add this server to the list of non-CORS-supporting servers that may be proxied by '+terria.appName+' \
itself.</p>\
<p>If you did not enter this link manually, this error may indicate that the group you opened is temporarily unavailable or there is a \
problem with your internet connection.  Try opening the group again, and if the problem persists, please report it by \
sending an email to <a href="mailto:'+terria.supportEmail+'">'+terria.supportEmail+'</a>.</p>'
        });
    });
}

function addLayersRecursively(mapServiceGroup, topLevelJson, topLevelLayersJson, topLevelLegendJson, parentID, layers, thisGroup, dataCustodian) {
    if (!(layers instanceof Array)) {
        layers = [layers];
    }

    for (var i = 0; i < layers.length; ++i) {
        var layer = layers[i];

        if (parentID === -1 && layer.parentLayer !== null && defined(layer.parentLayer)) {
            continue;
        } else if (parentID !== -1 && (!layer.parentLayer || layer.parentLayer.id !== parentID)) {
            continue;
        }

        if (mapServiceGroup.blacklist && mapServiceGroup.blacklist[layer.name]) {
            console.log('Provider Feedback: Filtering out ' + layer.name + ' (' + layer.id + ') because it is blacklisted.');
            continue;
        }

        if (layer.type === 'Group Layer') {
            var subGroup = new CatalogGroup(mapServiceGroup.terria);
            subGroup.name = replaceUnderscores(layer.name);
            thisGroup.add(subGroup);
            addLayersRecursively(mapServiceGroup, topLevelJson, topLevelLayersJson, topLevelLegendJson, layer.id, layers, subGroup, dataCustodian);
        } else if (layer.type === 'Feature Layer' || layer.type === 'Raster Layer' || layer.type === 'Mosaic Layer') {
            thisGroup.add(createDataSource(mapServiceGroup, topLevelJson, topLevelLayersJson, topLevelLegendJson, layer, dataCustodian));
        }
    }
}

function createDataSource(mapServiceGroup, topLevelJson, topLevelLayersJson, topLevelLegendJson, layer, dataCustodian) {
    var result = new ArcGisMapServerCatalogItem(mapServiceGroup.terria);

    result.name = replaceUnderscores(layer.name);
    result.dataCustodian = dataCustodian;
    result.url = mapServiceGroup.url;
    result.layers = layer.id.toString();
    result.maximumScale = layer.maxScale;

    result.updateFromMetadata(topLevelJson, topLevelLayersJson, topLevelLegendJson, true, layer);

    if (typeof(mapServiceGroup.itemProperties) === 'object') {
        result.updateFromJson(mapServiceGroup.itemProperties);
    }

    return result;
}

module.exports = ArcGisMapServerCatalogGroup;