Source: Scene/GetFeatureInfoFormat.js

/*global define*/
define([
        '../Core/Cartographic',
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/DeveloperError',
        '../Core/RuntimeError',
        './ImageryLayerFeatureInfo'
    ], function(
        Cartographic,
        defaultValue,
        defined,
        DeveloperError,
        RuntimeError,
        ImageryLayerFeatureInfo) {
    'use strict';

    /**
     * Describes the format in which to request GetFeatureInfo from a Web Map Service (WMS) server.
     *
     * @alias GetFeatureInfoFormat
     * @constructor
     *
     * @param {String} type The type of response to expect from a GetFeatureInfo request.  Valid
     *        values are 'json', 'xml', 'html', or 'text'.
     * @param {String} [format] The info format to request from the WMS server.  This is usually a
     *        MIME type such as 'application/json' or text/xml'.  If this parameter is not specified, the provider will request 'json'
     *        using 'application/json', 'xml' using 'text/xml', 'html' using 'text/html', and 'text' using 'text/plain'.
     * @param {Function} [callback] A function to invoke with the GetFeatureInfo response from the WMS server
     *        in order to produce an array of picked {@link ImageryLayerFeatureInfo} instances.  If this parameter is not specified,
     *        a default function for the type of response is used.
     */
    function GetFeatureInfoFormat(type, format, callback) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(type)) {
            throw new DeveloperError('type is required.');
        }
        //>>includeEnd('debug');

        this.type = type;

        if (!defined(format)) {
            if (type === 'json') {
                format = 'application/json';
            } else if (type === 'xml') {
                format = 'text/xml';
            } else if (type === 'html') {
                format = 'text/html';
            } else if (type === 'text') {
                format = 'text/plain';
            }
            //>>includeStart('debug', pragmas.debug);
            else {
                throw new DeveloperError('format is required when type is not "json", "xml", "html", or "text".');
            }
            //>>includeEnd('debug');
        }

        this.format = format;

        if (!defined(callback)) {
            if (type === 'json') {
                callback = geoJsonToFeatureInfo;
            } else if (type === 'xml') {
                callback = xmlToFeatureInfo;
            } else if (type === 'html') {
                callback = textToFeatureInfo;
            } else if (type === 'text') {
                callback = textToFeatureInfo;
            }
            //>>includeStart('debug', pragmas.debug);
            else {
                throw new DeveloperError('callback is required when type is not "json", "xml", "html", or "text".');
            }
            //>>includeEnd('debug');
        }

        this.callback = callback;
    }

    function geoJsonToFeatureInfo(json) {
        var result = [];

        var features = json.features;
        for (var i = 0; i < features.length; ++i) {
            var feature = features[i];

            var featureInfo = new ImageryLayerFeatureInfo();
            featureInfo.data = feature;
            featureInfo.properties = feature.properties;
            featureInfo.configureNameFromProperties(feature.properties);
            featureInfo.configureDescriptionFromProperties(feature.properties);

            // If this is a point feature, use the coordinates of the point.
            if (defined(feature.geometry) && feature.geometry.type === 'Point') {
                var longitude = feature.geometry.coordinates[0];
                var latitude = feature.geometry.coordinates[1];
                featureInfo.position = Cartographic.fromDegrees(longitude, latitude);
            }

            result.push(featureInfo);
        }

        return result;
    }

    var mapInfoMxpNamespace = 'http://www.mapinfo.com/mxp';
    var esriWmsNamespace = 'http://www.esri.com/wms';
    var wfsNamespace = 'http://www.opengis.net/wfs';
    var gmlNamespace = 'http://www.opengis.net/gml';

    function xmlToFeatureInfo(xml) {
        var documentElement = xml.documentElement;
        if (documentElement.localName === 'MultiFeatureCollection' && documentElement.namespaceURI === mapInfoMxpNamespace) {
            // This looks like a MapInfo MXP response
            return mapInfoXmlToFeatureInfo(xml);
        } else if (documentElement.localName === 'FeatureInfoResponse' && documentElement.namespaceURI === esriWmsNamespace) {
            // This looks like an Esri WMS response
            return esriXmlToFeatureInfo(xml);
        } else if (documentElement.localName === 'FeatureCollection' && documentElement.namespaceURI === wfsNamespace) {
            // This looks like a WFS/GML response.
            return gmlToFeatureInfo(xml);
        } else if (documentElement.localName === 'ServiceExceptionReport') {
            // This looks like a WMS server error, so no features picked.
            throw new RuntimeError(new XMLSerializer().serializeToString(documentElement));
        } else if (documentElement.localName === 'msGMLOutput') {
                return msGmlToFeatureInfo(xml);
        } else {
            // Unknown response type, so just dump the XML itself into the description.
            return unknownXmlToFeatureInfo(xml);
        }
    }

    function mapInfoXmlToFeatureInfo(xml) {
        var result = [];

        var multiFeatureCollection = xml.documentElement;

        var features = multiFeatureCollection.getElementsByTagNameNS(mapInfoMxpNamespace, 'Feature');
        for (var featureIndex = 0; featureIndex < features.length; ++featureIndex) {
            var feature = features[featureIndex];

            var properties = {};

            var propertyElements = feature.getElementsByTagNameNS(mapInfoMxpNamespace, 'Val');
            for (var propertyIndex = 0; propertyIndex < propertyElements.length; ++propertyIndex) {
                var propertyElement = propertyElements[propertyIndex];
                if (propertyElement.hasAttribute('ref')) {
                    var name = propertyElement.getAttribute('ref');
                    var value = propertyElement.textContent.trim();
                    properties[name] = value;
                }
            }

            var featureInfo = new ImageryLayerFeatureInfo();
            featureInfo.data = feature;
            featureInfo.properties = properties;
            featureInfo.configureNameFromProperties(properties);
            featureInfo.configureDescriptionFromProperties(properties);
            result.push(featureInfo);
        }

        return result;
    }

    function esriXmlToFeatureInfo(xml) {
        var featureInfoResponse = xml.documentElement;
        var result = [];
        var properties;

        var features = featureInfoResponse.getElementsByTagNameNS('*', 'FIELDS');
        if (features.length > 0) {
            // Standard esri format
            for (var featureIndex = 0; featureIndex < features.length; ++featureIndex) {
                var feature = features[featureIndex];

                properties = {};

                var propertyAttributes = feature.attributes;
                for (var attributeIndex = 0; attributeIndex < propertyAttributes.length; ++attributeIndex) {
                    var attribute = propertyAttributes[attributeIndex];
                    properties[attribute.name] = attribute.value;
                }

                result.push(imageryLayerFeatureInfoFromDataAndProperties(feature, properties));
            }
        } else {
            // Thredds format -- looks like esri, but instead of containing FIELDS, contains FeatureInfo element
            var featureInfoElements = featureInfoResponse.getElementsByTagNameNS('*', 'FeatureInfo');
            for (var featureInfoElementIndex = 0; featureInfoElementIndex < featureInfoElements.length; ++featureInfoElementIndex) {
                var featureInfoElement = featureInfoElements[featureInfoElementIndex];

                properties = {};

                // node.children is not supported in IE9-11, so use childNodes and check that child.nodeType is an element
                var featureInfoChildren = featureInfoElement.childNodes;
                for (var childIndex = 0; childIndex < featureInfoChildren.length; ++childIndex) {
                    var child = featureInfoChildren[childIndex];
                    if (child.nodeType === Node.ELEMENT_NODE) {
                        properties[child.localName] = child.textContent;
                    }
                }

                result.push(imageryLayerFeatureInfoFromDataAndProperties(featureInfoElement, properties));
            }
        }

        return result;
    }

    function gmlToFeatureInfo(xml) {
        var result = [];

        var featureCollection = xml.documentElement;

        var featureMembers = featureCollection.getElementsByTagNameNS(gmlNamespace, 'featureMember');
        for (var featureIndex = 0; featureIndex < featureMembers.length; ++featureIndex) {
            var featureMember = featureMembers[featureIndex];

            var properties = {};
            getGmlPropertiesRecursively(featureMember, properties);
            result.push(imageryLayerFeatureInfoFromDataAndProperties(featureMember, properties));
        }

        return result;
    }

    // msGmlToFeatureInfo is similar to gmlToFeatureInfo, but assumes different XML structure
    // eg. <msGMLOutput> <ABC_layer> <ABC_feature> <foo>bar</foo> ... </ABC_feature> </ABC_layer> </msGMLOutput>

    function msGmlToFeatureInfo(xml) {
        var result = [];

        // Find the first child. Except for IE, this would work:
        // var layer = xml.documentElement.children[0];
        var layer;
        var children = xml.documentElement.childNodes;
        for (var i = 0; i < children.length; i++) {
            if (children[i].nodeType === Node.ELEMENT_NODE) {
                layer = children[i];
                break;
            }
        }

        var featureMembers = layer.childNodes;
        for (var featureIndex = 0; featureIndex < featureMembers.length; ++featureIndex) {
            var featureMember = featureMembers[featureIndex];
            if (featureMember.nodeType === Node.ELEMENT_NODE) {
                var properties = {};
                getGmlPropertiesRecursively(featureMember, properties);
                result.push(imageryLayerFeatureInfoFromDataAndProperties(featureMember, properties));
            }
        }

        return result;
    }

    function getGmlPropertiesRecursively(gmlNode, properties) {
        var isSingleValue = true;

        for (var i = 0; i < gmlNode.childNodes.length; ++i) {
            var child = gmlNode.childNodes[i];

            if (child.nodeType === Node.ELEMENT_NODE) {
                isSingleValue = false;
            }

            if (child.localName === 'Point' || child.localName === 'LineString' || child.localName === 'Polygon' || child.localName === 'boundedBy') {
                continue;
            }

            if (child.hasChildNodes() && getGmlPropertiesRecursively(child, properties)) {
                properties[child.localName] = child.textContent;
            }
        }

        return isSingleValue;
    }

    function imageryLayerFeatureInfoFromDataAndProperties(data, properties) {
        var featureInfo = new ImageryLayerFeatureInfo();
        featureInfo.data = data;
        featureInfo.properties = properties;
        featureInfo.configureNameFromProperties(properties);
        featureInfo.configureDescriptionFromProperties(properties);
        return featureInfo;
    }

    function unknownXmlToFeatureInfo(xml) {
        var xmlText = new XMLSerializer().serializeToString(xml);

        var element = document.createElement('div');
        var pre = document.createElement('pre');
        pre.textContent = xmlText;
        element.appendChild(pre);

        var featureInfo = new ImageryLayerFeatureInfo();
        featureInfo.data = xml;
        featureInfo.description = element.innerHTML;
        return [featureInfo];
    }

    var emptyBodyRegex= /<body>\s*<\/body>/im;
    var wmsServiceExceptionReportRegex = /<ServiceExceptionReport([\s\S]*)<\/ServiceExceptionReport>/im;
    var titleRegex = /<title>([\s\S]*)<\/title>/im;

    function textToFeatureInfo(text) {
        // If the text is HTML and it has an empty body tag, assume it means no features were found.
        if (emptyBodyRegex.test(text)) {
            return undefined;
        }

        // If this is a WMS exception report, treat it as "no features found" rather than showing
        // bogus feature info.
        if (wmsServiceExceptionReportRegex.test(text)) {
            return undefined;
        }

        // If the text has a <title> element, use it as the name.
        var name;
        var title = titleRegex.exec(text);
        if (title && title.length > 1) {
            name = title[1];
        }

        var featureInfo = new ImageryLayerFeatureInfo();
        featureInfo.name = name;
        featureInfo.description = text;
        featureInfo.data = text;
        return [featureInfo];
    }

    return GetFeatureInfoFormat;
});