undefined

API Docs for: 5.4.0
Show:

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

/* global define, console, window, $, i18n, RAMP, t, TimelineLite */

/**
* @module RAMP
* @submodule FilterManager
* @main FilterManager
*/

/**
* Creates a choice tree for adding datasets.
* 
* ####Imports RAMP Modules:
* {{#crossLink "PopupManager"}}{{/crossLink}}  
* {{#crossLink "DataLoader"}}{{/crossLink}}  
* {{#crossLink "Theme"}}{{/crossLink}}  
* {{#crossLink "Map"}}{{/crossLink}}  
* {{#crossLink "LayerLoader"}}{{/crossLink}}  
* {{#crossLink "GlobalStorage"}}{{/crossLink}}  
* {{#crossLink "StepItem"}}{{/crossLink}}  
* {{#crossLink "Util"}}{{/crossLink}}  
* {{#crossLink "TmplHelper"}}{{/crossLink}}  
* {{#crossLink "TmplUtil"}}{{/crossLink}}  
* {{#crossLink "Array"}}{{/crossLink}}  
* {{#crossLink "Dictionary"}}{{/crossLink}}  
* {{#crossLink "Bricks"}}{{/crossLink}}    
* 
* ####Uses RAMP Templates:
* {{#crossLink "templates/filter_manager_template.json"}}{{/crossLink}}
* 
* @class DataLoaderGui
* @static
* @uses dojo/lang
*/

define([
    /* Dojo */
    'dojo/_base/lang',

    /* Text */
    'dojo/text!./templates/filter_manager_template.json',

    /* Ramp */

    'utils/popupManager', 'ramp/dataLoader', 'ramp/theme', 'ramp/map', 'ramp/layerLoader', 'ramp/globalStorage', 'ramp/stepItem',

    /* Util */
    'utils/util', 'utils/tmplHelper', 'utils/tmplUtil', 'utils/array', 'utils/dictionary', 'utils/bricks'
],
    function (
        lang,
        filter_manager_template,
        PopupManager, DataLoader, Theme, RampMap, LayerLoader, GlobalStorage, StepItem,
        UtilMisc, TmplHelper, TmplUtil, UtilArray, UtilDict, Bricks
    ) {
        'use strict';

        var rootNode,

            addDatasetToggle,
            addDatasetContainer,

            layerList,
            layerToggles,
            filterToggles,

            symbologyPreset = {},

            choiceTree,
            choiceTreeCallbacks,
            choiceTreeErrors,
            stepLookup = {},

            addDatasetPopup,

            transitionDuration = 0.5,

            templates = JSON.parse(TmplHelper.stringifyTemplate(filter_manager_template));

        /**
         * A collection of callbacks used by the choice tree.
         * 
         * @property choiceTreeCallbacks
         * @private
         * @type {Object}
         */
        choiceTreeCallbacks = {
            /**
             * A callback that advances the choice tree to the specified child step of the supplied step item.
             * 
             * @method choiceTreeCallbacks.simpleAdvance
             * @private
             * @param  {StepItem} step            step item to advance from
             * @param  {Object} data            data from the choice brick in step item
             * @param  {Object} targetChildData data to be passed to the step being advanced to
             */
            simpleAdvance: function (step, data, targetChildData) {
                step.advance(data.selectedChoice, targetChildData);
            },

            /**
             * A callback that retreats part of the choice tree to the step item specified.
             * 
             * @method choiceTreeCallbacks.simpleCancel
             * @private
             * @param  {StepItem} step step item to retreat to
             * @param  {Object} data data from the callback function
             */
            simpleCancel: function (step, data) {
                console.log('Step cancel click:', this, step, data);

                if (step.isCompleted()) {
                    step.retreat();
                } else {
                    step.clearStep();
                }
            },

            /**
             * A callback to guess the service type from the service url provided in step data.
             * 
             * @method choiceTreeCallbacks.serviceTypeStepGuess
             * @private
             * @param  {StepItem} step step item to guess service type on
             * @param  {[type]} data data from the callback function
             */
            serviceTypeStepGuess: function (step, data) {
                var value = data.inputValue,
                    serviceTypeBrick = step.contentBricks.serviceType,
                    guess = '';

                // make a guess if it's a feature or wms server only if the user hasn't already selected the type
                if (!serviceTypeBrick.isUserSelected()) {
                    if (value.match(/ArcGIS\/rest\/services/ig)) {
                        guess = 'featureServiceAttrStep';
                    } else if (value.match(/wms/ig)) {
                        guess = 'wmsServiceAttrStep';
                    }

                    serviceTypeBrick.setChoice(guess);
                }
            },

            /**
             * A callback to guess the file type from the file url or file object provided in step data.
             * 
             * @method choiceTreeCallbacks.fileTypeStepGuess
             * @private
             * @param  {StepItem} step step item to guess file type on
             * @param  {[type]} data data from the callback function
             */
            fileTypeStepGuess: function (step, data) {
                var fileName = data.inputValue,
                    serviceFileBrick = step.contentBricks.fileType,
                    guess = '';

                if (!serviceFileBrick.isUserSelected() && !serviceFileBrick.isUserEntered) {
                    if (fileName.endsWith('.csv')) {
                        guess = 'csvFileAttrStep';
                    } else if (fileName.endsWith('.json')) {
                        guess = 'geojsonFileAttrStep';
                    } else if (fileName.endsWith('.zip')) {
                        guess = 'shapefileFileAttrStep';
                    }

                    serviceFileBrick.setChoice(guess);
                }
            }
        };

        /**
         * Create choice tree structure. This function is executed as part of the module initialization so that i18n strings can be properly loaded
         * 
         * @method prepareChoiceTreeStructure
         * @private
         */
        function prepareChoiceTreeStructure() {
            /**
             * A collection of precanned error messages that are used by the choice tree.
             * 
             * @property choiceTreeErrors
             * @private
             * @type {Object}
             */
            choiceTreeErrors = {
                base: {
                    type: 'error',
                    header: 'Cannot load',
                    message: 'You have IE9?'
                }
            };

            choiceTreeErrors.featureError = lang.mixin({}, choiceTreeErrors.base, {
                header: i18n.t('addDataset.error.headerFeature')
            });

            choiceTreeErrors.wmsError = lang.mixin({}, choiceTreeErrors.base, {
                header: i18n.t('addDataset.error.headerWMS')
            });

            choiceTreeErrors.fileError = lang.mixin({}, choiceTreeErrors.base, {
                header: i18n.t('addDataset.error.headerFile')
            });

            choiceTreeErrors.geojsonError = lang.mixin({}, choiceTreeErrors.base, {
                header: i18n.t('addDataset.error.headerGeojson')
            });

            choiceTreeErrors.csvError = lang.mixin({}, choiceTreeErrors.base, {
                header: i18n.t('addDataset.error.headerCSV')
            });

            choiceTreeErrors.shapefileError = lang.mixin({}, choiceTreeErrors.base, {
                header: i18n.t('addDataset.error.headerShapefile')
            });

            /**
             * A choice tree config object
             *
             * Config has a simple tree structure, with content being an array of Brick object to be placed inside a StepItem.
             * 
             * @property choiceTree
             * @private
             * @type {Object}
             */
            choiceTree = {
                // step for choosing between adding a service or a file
                id: 'sourceTypeStep',
                content: [
                    {
                        id: 'sourceType',
                        type: Bricks.ChoiceBrick,
                        config: {
                            header: i18n.t('addDataset.dataSource'),
                            instructions: i18n.t('addDataset.help.dataSource'),
                            choices: [
                                {
                                    key: 'serviceTypeStep',
                                    value: i18n.t('addDataset.dataSourceService')
                                },
                                {
                                    key: 'fileTypeStep',
                                    value: i18n.t('addDataset.dataSourceFile')
                                }
                            ]
                        },
                        on: [
                            {
                                eventName: Bricks.ChoiceBrick.event.CHANGE,
                                //expose: { as: 'advance' },
                                callback: choiceTreeCallbacks.simpleAdvance
                            }
                        ]
                    }
                ],
                children: [
                    {
                        // step for choosing between feature and wms service and providing a service url
                        id: 'serviceTypeStep',
                        content: [
                            {
                                id: 'serviceURL',
                                type: Bricks.SimpleInputBrick,
                                config: {
                                    header: i18n.t('addDataset.serviceLayerURL'),
                                    instructions: i18n.t('addDataset.help.serviceURL'),
                                    placeholder: i18n.t('addDataset.serviceLayerURLPlaceholder'),
                                    freezeStates: [Bricks.Brick.state.SUCCESS]
                                },
                                on: [
                                    {
                                        eventName: Bricks.SimpleInputBrick.event.CHANGE,
                                        callback: choiceTreeCallbacks.serviceTypeStepGuess
                                    }
                                ]
                            },
                            {
                                id: 'serviceType',
                                type: Bricks.ChoiceBrick,
                                config: {
                                    //template: 'template_name', //optional, has a default
                                    header: i18n.t('addDataset.serviceType'),
                                    instructions: i18n.t('addDataset.help.serviceType'),
                                    choices: [
                                        {
                                            key: 'featureServiceAttrStep',
                                            value: i18n.t('addDataset.serviceTypeFeature')
                                        },
                                        {
                                            key: 'wmsServiceAttrStep',
                                            value: i18n.t('addDataset.serviceTypeWMS')
                                        }
                                    ],
                                    freezeStates: [Bricks.Brick.state.SUCCESS]
                                }
                            },
                            {
                                id: 'serviceTypeOkCancel',
                                type: Bricks.OkCancelButtonBrick,
                                config: {
                                    okLabel: i18n.t('addDataset.connect'),
                                    okFreezeStates: [
                                        Bricks.Brick.state.SUCCESS,
                                        Bricks.Brick.state.ERROR
                                    ],
                                    cancelLabel: i18n.t('addDataset.cancel'),
                                    //cancelFreezeStates: false,
                                    reverseOrder: true,

                                    required: [
                                        {
                                            id: Bricks.OkCancelButtonBrick.okButtonId,
                                            type: 'all',
                                            check: ['serviceType', 'serviceURL']
                                        },
                                        {
                                            id: Bricks.OkCancelButtonBrick.cancelButtonId,
                                            type: 'any',
                                            check: ['serviceType', 'serviceURL']
                                        }
                                    ]
                                },
                                on: [
                                    /*{
                                        eventName: Bricks.OkCancelButtonBrick.event.CLICK,
                                        callback: function (step, data) {
                                            console.log('Just Click:', this, step, data);
                                        }
                                    },*/
                                    {
                                        eventName: Bricks.OkCancelButtonBrick.event.OK_CLICK,
                                        // connect to feature service
                                        callback: function (step/*, data*/) {
                                            var promise,
                                                handle = delayLoadingState(step, 100),
                                                bricksData = step.getData().bricksData,
                                                serviceTypeValue = bricksData.serviceType.selectedChoice,
                                                serviceUrlValue = bricksData.serviceURL.inputValue.trim(); // trimming spaces from service url string

                                            switch (serviceTypeValue) {
                                                case 'featureServiceAttrStep':
                                                    // get data from feature layer endpoint
                                                    promise = DataLoader.getFeatureLayer(serviceUrlValue);

                                                    promise.then(function (data) {
                                                        // get data from feature layer's legend endpoint

                                                        var layerInRampLODRange = RampMap.layerInLODRange(data.maxScale, data.minScale);

                                                        if (!layerInRampLODRange) {
                                                            handleFailure(step, handle, {
                                                                serviceType:
                                                                    lang.mixin(choiceTreeErrors.featureError, {
                                                                        message: i18n.t('addDataset.error.messageFeatureOutsideZoomRange')
                                                                    })
                                                            });
                                                        } else {
                                                            var legendPromise = DataLoader.getFeatureLayerLegend(serviceUrlValue);
                                                            legendPromise.then(function (legendLookup) {
                                                                var fieldOptions;
                                                                window.clearTimeout(handle);

                                                                data.legendLookup = legendLookup;
                                                                // TODO: when field name aliases are available, change how the dropdown values are generated
                                                                fieldOptions = data.fields.map(function (field) { return { value: field, text: field }; });

                                                                // no fields available; likely this is not a Feature service
                                                                if (!fieldOptions || fieldOptions.length === 0) {
                                                                    handleFailure(step, handle, {
                                                                        serviceType:
                                                                            lang.mixin(choiceTreeErrors.featureError, {
                                                                                message: i18n.t('addDataset.error.messageFeatureInvalid')
                                                                            })
                                                                    });
                                                                } else {
                                                                    choiceTreeCallbacks.simpleAdvance(step, bricksData.serviceType, {
                                                                        stepData: data,
                                                                        bricksData: {
                                                                            primaryAttribute: {
                                                                                options: fieldOptions
                                                                            }
                                                                        }
                                                                    });
                                                                }
                                                            }, function (event) {
                                                                console.error(event);
                                                                handleFailure(step, handle, {
                                                                    serviceType:
                                                                        lang.mixin(choiceTreeErrors.featureError, {
                                                                            message: i18n.t('addDataset.error.messageFeatureLegend')
                                                                        })
                                                                });
                                                            });
                                                        }

                                                    }, function (event) {
                                                        // error connection to service
                                                        console.error(event);
                                                        handleFailure(step, handle, {
                                                            serviceURL:
                                                                lang.mixin(choiceTreeErrors.featureError, {
                                                                    message: i18n.t('addDataset.error.messageFeatureConnect')
                                                                })
                                                        });
                                                    });

                                                    break;

                                                case 'wmsServiceAttrStep':
                                                    // get data from wms endpoint
                                                    promise = DataLoader.getWmsLayerList(serviceUrlValue);

                                                    promise.then(function (data) {
                                                        var layerOptions;
                                                        window.clearTimeout(handle);

                                                        // TODO: when field name aliases are available, change how the dropdown values are generated
                                                        layerOptions = data.layers.map(function (layer) { return { value: layer.name, text: layer.desc }; });

                                                        // no layer names available; likely this is not a WMS service
                                                        if (!layerOptions || layerOptions.length === 0) {
                                                            handleFailure(step, handle, {
                                                                serviceType:
                                                                    lang.mixin(choiceTreeErrors.wmsError, {
                                                                        message: i18n.t('addDataset.error.messageWMSInvalid')
                                                                    })
                                                            });
                                                        } else {
                                                            choiceTreeCallbacks.simpleAdvance(step, bricksData.serviceType, {
                                                                stepData: {
                                                                    wmsData: data,
                                                                    wmsUrl: serviceUrlValue
                                                                },
                                                                bricksData: {
                                                                    layerName: {
                                                                        options: layerOptions
                                                                    }
                                                                }
                                                            });
                                                        }
                                                    }, function (event) {
                                                        console.error(event);
                                                        handleFailure(step, handle, {
                                                            serviceURL:
                                                                lang.mixin(choiceTreeErrors.wmsError, {
                                                                    message: i18n.t('addDataset.error.messageWMSConnect')
                                                                })
                                                        });
                                                    });

                                                    break;
                                            }
                                        }
                                        //expose: { as: 'advance' }
                                    },
                                    {
                                        eventName: Bricks.OkCancelButtonBrick.event.CANCEL_CLICK,
                                        //expose: { as: 'retreat' },
                                        callback: choiceTreeCallbacks.simpleCancel
                                    }

                                ]
                            }
                        ],
                        children: [
                            {
                                id: 'featureServiceAttrStep',
                                content: [
                                    {
                                        id: 'primaryAttribute',
                                        type: Bricks.DropDownBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.featurePrimaryAttribute'),
                                            header: i18n.t('addDataset.primaryAttribute')
                                        }
                                    },
                                    {
                                        id: 'addDataset',
                                        type: Bricks.ButtonBrick,
                                        config: {
                                            label: i18n.t('addDataset.addDatasetButton'),
                                            containerClass: 'button-brick-container-main',
                                            buttonClass: 'btn-primary'
                                        },
                                        on: [
                                            {
                                                eventName: Bricks.ButtonBrick.event.CLICK,
                                                // add feature service layer to the map
                                                callback: function (step /*,data*/) {
                                                    var data = step.getData(),
                                                        bricksData = data.bricksData,
                                                        layerData = data.stepData,

                                                        newConfig = { //make feature layer config.
                                                            id: LayerLoader.nextId(),
                                                            displayName: layerData.layerName,
                                                            nameField: bricksData.primaryAttribute.dropDownValue,
                                                            datagrid: DataLoader.createDatagridConfig(layerData.fields, layerData.aliasMap),
                                                            symbology: DataLoader.createSymbologyConfig(layerData.renderer, layerData.legendLookup),
                                                            url: layerData.layerUrl,
                                                            aliasMap: layerData.aliasMap
                                                        },
                                                        featureLayer;

                                                    //TODO: set symbology and colour on feature layer (obj.data)
                                                    newConfig = GlobalStorage.applyFeatureDefaults(newConfig);

                                                    //make layer
                                                    featureLayer = RampMap.makeFeatureLayer(newConfig, true);
                                                    RAMP.config.layers.feature.push(newConfig);

                                                    LayerLoader.loadLayer(featureLayer);
                                                    addDatasetPopup.close();

                                                    //mainPopup.close();
                                                }
                                                //expose: { as: 'ADD_DATASET' }
                                            }
                                        ]
                                    }
                                ]
                            },
                            {
                                id: 'wmsServiceAttrStep',
                                content: [
                                    {
                                        id: 'layerName',
                                        type: Bricks.DropDownBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.wmsLayerName'),
                                            header: i18n.t('addDataset.layerName')
                                        }
                                    },
                                    {
                                        id: 'addDataset',
                                        type: Bricks.ButtonBrick,
                                        config: {
                                            label: i18n.t('addDataset.addDatasetButton'),
                                            containerClass: 'button-brick-container-main',
                                            buttonClass: 'btn-primary'
                                        },
                                        on: [
                                            {
                                                eventName: Bricks.ButtonBrick.event.CLICK,
                                                // add wms service layer to the map
                                                callback: function (step /*,data*/) {
                                                    var data = step.getData(),
                                                        bricksData = data.bricksData,
                                                        stepData = data.stepData,

                                                        wmsLayerName = bricksData.layerName.dropDownValue,

                                                        wmsConfig,
                                                        layer,
                                                        wmsLayer;

                                                    layer = UtilArray.find(stepData.wmsData.layers,
                                                        function (l) {
                                                            return l.name === wmsLayerName;
                                                        }
                                                    );

                                                    wmsConfig = {
                                                        id: LayerLoader.nextId(),
                                                        displayName: layer.desc,
                                                        format: 'png',
                                                        layerName: wmsLayerName,
                                                        imageUrl: 'assets/images/wms.png',
                                                        url: stepData.wmsUrl,
                                                        legendMimeType: 'image/jpeg'
                                                    };

                                                    if (layer.queryable) {
                                                        wmsConfig.featureInfo = {
                                                            parser: 'stringParse',
                                                            mimeType: 'text/plain'
                                                        };
                                                    }

                                                    wmsConfig = GlobalStorage.applyWMSDefaults(wmsConfig);

                                                    wmsLayer = RampMap.makeWmsLayer(wmsConfig, true);
                                                    RAMP.config.layers.wms.push(wmsConfig);

                                                    LayerLoader.loadLayer(wmsLayer);
                                                    addDatasetPopup.close();
                                                }
                                                //expose: { as: 'ADD_DATASET' }
                                            }
                                        ]
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        id: 'fileTypeStep',
                        content: [
                            {
                                id: 'fileOrFileURL',
                                type: Bricks.FileInputBrick,
                                config: {
                                    //template: 'template_name', //optional, has a default
                                    header: i18n.t('addDataset.fileOrURL'),
                                    instructions: i18n.t('addDataset.help.fileOrURL'),
                                    //label: 'Service URL', // optional, equals to header by default
                                    placeholder: i18n.t('addDataset.fileOrURLPlaceholder'),
                                    freezeStates: [Bricks.Brick.state.SUCCESS]
                                },
                                on: [
                                    {
                                        eventName: Bricks.FileInputBrick.event.CHANGE,
                                        callback: choiceTreeCallbacks.fileTypeStepGuess
                                    }
                                ]
                            },
                            {
                                id: 'fileType',
                                type: Bricks.ChoiceBrick,
                                config: {
                                    //template: 'template_name', //optional, has a default
                                    instructions: i18n.t('addDataset.help.fileOrURLType'),
                                    header: i18n.t('addDataset.fileType'),
                                    choices: [
                                        {
                                            key: 'geojsonFileAttrStep',
                                            value: i18n.t('addDataset.geojson')
                                        },
                                        {
                                            key: 'csvFileAttrStep',
                                            value: i18n.t('addDataset.csv')
                                        },
                                        {
                                            key: 'shapefileFileAttrStep',
                                            value: i18n.t('addDataset.shapefile')
                                        }
                                    ],
                                    freezeStates: [Bricks.Brick.state.SUCCESS]
                                }
                            },
                            {
                                id: 'fileTypeOkCancel',
                                type: Bricks.OkCancelButtonBrick,
                                config: {
                                    okLabel: i18n.t('addDataset.load'),
                                    okFreezeStates: [
                                        Bricks.Brick.state.SUCCESS,
                                        Bricks.Brick.state.ERROR
                                    ],
                                    cancelLabel: i18n.t('addDataset.cancel'),
                                    reverseOrder: true,

                                    required: [
                                        {
                                            id: Bricks.OkCancelButtonBrick.okButtonId,
                                            type: 'all',
                                            check: ['fileType', 'fileOrFileURL']
                                        },
                                        {
                                            id: Bricks.OkCancelButtonBrick.cancelButtonId,
                                            type: 'any',
                                            check: ['fileType', 'fileOrFileURL']
                                        }
                                    ]
                                },
                                on: [
                                    /*{
                                        eventName: Bricks.OkCancelButtonBrick.event.CLICK,
                                        callback: function (step, data) {
                                            console.log('Just Click:', this, step, data);
                                        }
                                    },*/
                                    {
                                        eventName: Bricks.OkCancelButtonBrick.event.OK_CLICK,
                                        // load and process files
                                        callback: function (step/*, data*/) {
                                            var promise,
                                                handle = delayLoadingState(step, 100),
                                                bricksData = step.getData().bricksData,
                                                fileTypeValue = bricksData.fileType.selectedChoice,
                                                fileValue = bricksData.fileOrFileURL.fileValue,
                                                fileUrlValue = bricksData.fileOrFileURL.inputValue.trim(), // trimming spaces from file url string
                                                fileName = bricksData.fileOrFileURL.fileName;

                                            promise = DataLoader.loadDataSet({
                                                url: fileValue ? null : fileUrlValue,
                                                file: fileValue,
                                                type: fileTypeValue === 'shapefileFileAttrStep' ? 'binary' : 'text'
                                            });

                                            promise.then(function (data) {
                                                switch (fileTypeValue) {
                                                    case 'geojsonFileAttrStep':
                                                        var geojsonPromise = DataLoader.buildGeoJson(data);

                                                        geojsonPromise.then(function (featureLayer) {
                                                            var fieldOptions;
                                                            window.clearTimeout(handle);

                                                            // TODO: when field name aliases are available, change how the dropdown values are generated
                                                            fieldOptions = featureLayer.fields.map(function (field) { return { value: field.name, text: field.name }; });

                                                            // no layer names available; likely this is not a geojson file
                                                            if (!fieldOptions || fieldOptions.length === 0) {
                                                                handleFailure(step, handle, {
                                                                    fileType:
                                                                        lang.mixin(choiceTreeErrors.geojsonError, {
                                                                            message: i18n.t('addDataset.error.messageGeojsonInvalid')
                                                                        })
                                                                });
                                                            } else {
                                                                choiceTreeCallbacks.simpleAdvance(step, bricksData.fileType, {
                                                                    stepData: featureLayer,
                                                                    bricksData: {
                                                                        datasetName: {
                                                                            inputValue: fileName
                                                                        },
                                                                        primaryAttribute: {
                                                                            options: fieldOptions
                                                                        }
                                                                    }
                                                                });
                                                            }
                                                        }, function (event) {
                                                            //error building geojson
                                                            console.error(event);
                                                            handleFailure(step, handle, {
                                                                fileType:
                                                                    lang.mixin(choiceTreeErrors.geojsonError, {
                                                                        message: i18n.t('addDataset.error.messageGeojsonBroken')
                                                                    })
                                                            });
                                                        });

                                                        break;

                                                    case 'csvFileAttrStep':
                                                        var rows,
                                                            delimiter = UtilMisc.detectDelimiter(data),

                                                            guess,
                                                            primaryAttribute,
                                                            headers;

                                                        window.clearTimeout(handle);

                                                        rows = DataLoader.csvPeek(data, delimiter);
                                                        headers = rows[0].map(function (header) { return { value: header, text: header }; });

                                                        // no properties names available; likely this is not a csv file
                                                        if (!headers || headers.length === 0) {
                                                            handleFailure(step, handle, {
                                                                fileType:
                                                                    lang.mixin(choiceTreeErrors.csvError, {
                                                                        message: i18n.t('addDataset.error.messageCSVInvalid')
                                                                    })
                                                            });
                                                        } else if (!rows || rows.length < 2) {
                                                            // no rows, no layer
                                                            handleFailure(step, handle, {
                                                                fileType:
                                                                    lang.mixin(choiceTreeErrors.csvError, {
                                                                        message: i18n.t('addDataset.error.messageCSVShort')
                                                                    })
                                                            });
                                                        } else if (headers.length < 2) {
                                                            // only one column? are you kidding me?
                                                            handleFailure(step, handle, {
                                                                fileType:
                                                                    lang.mixin(choiceTreeErrors.csvError, {
                                                                        message: i18n.t('addDataset.error.messageCSVThin')
                                                                    })
                                                            });
                                                        } else {
                                                            guess = guessLatLong(rows);

                                                            // preselect primary attribute so it's not one of the lat or long guesses;
                                                            // if csv has only two fields (lat, long), select the first as primary
                                                            primaryAttribute = rows[0].filter(function (header) {
                                                                return header !== guess.lat && header !== guess.long;
                                                            })[0] || rows[0][0];

                                                            // TODO: if you can't detect lat or long make the user choose them, don't just select the first header from the list, maybe.
                                                            choiceTreeCallbacks.simpleAdvance(step, bricksData.fileType, {
                                                                stepData: {
                                                                    csvData: data,
                                                                    csvHeaders: rows[0],
                                                                    csvDelimeter: delimiter
                                                                },
                                                                bricksData: {
                                                                    datasetName: {
                                                                        inputValue: fileName
                                                                    },
                                                                    primaryAttribute: {
                                                                        options: headers,
                                                                        selectedOption: primaryAttribute
                                                                    },
                                                                    latitude: {
                                                                        options: headers,
                                                                        selectedOption: guess.lat
                                                                    },
                                                                    longitude: {
                                                                        options: headers,
                                                                        selectedOption: guess.long
                                                                    }
                                                                }
                                                            });
                                                        }

                                                        break;

                                                    case 'shapefileFileAttrStep':
                                                        var shapefilePromise = DataLoader.buildShapefile(data);

                                                        shapefilePromise.then(function (featureLayer) {
                                                            var fieldOptions;

                                                            window.clearTimeout(handle);

                                                            // TODO: when field name aliases are available, change how the dropdown values are generated
                                                            fieldOptions = featureLayer.fields.map(function (field) { return { value: field.name, text: field.name }; });

                                                            // no layer names available; likely this is not a geojson file
                                                            if (!fieldOptions || fieldOptions.length === 0) {
                                                                handleFailure(step, handle, {
                                                                    fileType:
                                                                        lang.mixin(choiceTreeErrors.shapefileError, {
                                                                            message: i18n.t('addDataset.error.messageShapefileInvalid')
                                                                        })
                                                                });
                                                            } else {
                                                                choiceTreeCallbacks.simpleAdvance(step, bricksData.fileType, {
                                                                    stepData: featureLayer,
                                                                    bricksData: {
                                                                        datasetName: {
                                                                            inputValue: fileName
                                                                        },
                                                                        primaryAttribute: {
                                                                            options: fieldOptions
                                                                        }
                                                                    }
                                                                });
                                                            }
                                                        }, function (event) {
                                                            // error to build shapefiles
                                                            console.error(event);
                                                            handleFailure(step, handle, {
                                                                fileType:
                                                                    lang.mixin(choiceTreeErrors.shapefileError, {
                                                                        message: i18n.t('addDataset.error.messageShapefileBroken')
                                                                    })
                                                            });
                                                        });

                                                        break;
                                                }
                                            }, function (event) {
                                                //error loading file
                                                console.error(event);
                                                handleFailure(step, handle, {
                                                    fileOrFileURL:
                                                        lang.mixin(choiceTreeErrors.fileError, {
                                                            message: i18n.t('addDataset.error.messageFileConnect')
                                                        })
                                                });
                                            });
                                        }
                                        //expose: { as: 'advance' },
                                    },
                                    {
                                        eventName: Bricks.OkCancelButtonBrick.event.CANCEL_CLICK,
                                        expose: { as: 'retreat' },
                                        callback: choiceTreeCallbacks.simpleCancel
                                    }

                                ]
                            }
                        ],
                        children: [
                            {
                                id: 'geojsonFileAttrStep',
                                content: [
                                    {
                                        id: 'datasetName',
                                        type: Bricks.SimpleInputBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.geojsonDatasetName'),
                                            header: i18n.t('addDataset.datasetName')
                                        }
                                    },
                                    {
                                        id: 'primaryAttribute',
                                        type: Bricks.DropDownBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.geojsonPrimaryAttribute'),
                                            header: i18n.t('addDataset.primaryAttribute')
                                        }
                                    },
                                    {
                                        id: 'color',
                                        type: Bricks.ColorPickerBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.geojsonColour'),
                                            header: i18n.t('addDataset.colour')
                                        }
                                    },
                                    {
                                        id: 'addDataset',
                                        type: Bricks.ButtonBrick,
                                        config: {
                                            label: i18n.t('addDataset.addDatasetButton'),
                                            containerClass: 'button-brick-container-main',
                                            buttonClass: 'btn-primary'
                                        },
                                        on: [
                                            {
                                                eventName: Bricks.ButtonBrick.event.CLICK,
                                                // add geojson layer to the map
                                                callback: function (step /*,data*/) {
                                                    var data = step.getData(),
                                                        bricksData = data.bricksData,
                                                        featureLayer = data.stepData,

                                                        iconTemplate = makeIconTemplate('a_d_icon_' + featureLayer.renderer._RAMP_rendererType, bricksData.color.hex);

                                                    DataLoader.enhanceFileFeatureLayer(featureLayer, {
                                                        //renderer: obj.style,
                                                        colour: [
                                                            bricksData.color.rgb_[0],
                                                            bricksData.color.rgb_[1],
                                                            bricksData.color.rgb_[2],
                                                            255
                                                        ],
                                                        nameField: bricksData.primaryAttribute.dropDownValue,
                                                        icon: iconTemplate,
                                                        datasetName: bricksData.datasetName.inputValue,
                                                        fields: featureLayer.fields.map(function (field) { return field.name; })
                                                    });

                                                    LayerLoader.loadLayer(featureLayer);
                                                    addDatasetPopup.close();
                                                }
                                            }
                                        ]
                                    }
                                ]
                            },
                            {
                                id: 'csvFileAttrStep',
                                content: [
                                    {
                                        id: 'datasetName',
                                        type: Bricks.SimpleInputBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.csvDatasetName'),
                                            header: i18n.t('addDataset.datasetName')
                                        }
                                    },
                                    {
                                        id: 'primaryAttribute',
                                        type: Bricks.DropDownBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.csvPrimaryAttribute'),
                                            header: i18n.t('addDataset.primaryAttribute')
                                        }
                                    },
                                    {
                                        id: 'latLongAttribute',
                                        type: Bricks.MultiBrick,
                                        config: {
                                            //header: 'Service URL', //optional, omitted if not specified
                                            content: [
                                                {
                                                    id: 'latitude',
                                                    type: Bricks.DropDownBrick,
                                                    config: {
                                                        instructions: i18n.t('addDataset.help.csvLatitude'),
                                                        header: i18n.t('addDataset.latitude')
                                                    }
                                                },
                                                {
                                                    id: 'longitude',
                                                    type: Bricks.DropDownBrick,
                                                    config: {
                                                        instructions: i18n.t('addDataset.help.csvLongitude'),
                                                        header: i18n.t('addDataset.longitude')
                                                    }
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        id: 'color',
                                        type: Bricks.ColorPickerBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.csvColour'),
                                            header: i18n.t('addDataset.colour')
                                        }
                                    },
                                    {
                                        id: 'addDataset',
                                        type: Bricks.ButtonBrick,
                                        config: {
                                            label: i18n.t('addDataset.addDatasetButton'),
                                            containerClass: 'button-brick-container-main',
                                            buttonClass: 'btn-primary'
                                        },
                                        on: [
                                            {
                                                eventName: Bricks.ButtonBrick.event.CLICK,
                                                // add wms service layer to the map
                                                callback: function (step /*,data*/) {
                                                    var data = step.getData(),
                                                        bricksData = data.bricksData,
                                                        stepData = data.stepData,

                                                        csvData = stepData.csvData,
                                                        csvHeaders = stepData.csvHeaders,
                                                        csvDelimeter = stepData.csvDelimeter,

                                                        featureLayer,
                                                        iconTemplate = makeIconTemplate('a_d_icon_circlePoint', bricksData.color.hex),

                                                        promise;

                                                    promise = DataLoader.buildCsv(csvData, {
                                                        latfield: bricksData.latitude.dropDownValue,
                                                        lonfield: bricksData.longitude.dropDownValue,
                                                        delimiter: csvDelimeter,

                                                        fields: csvHeaders
                                                    });

                                                    promise.then(function (event) {
                                                        featureLayer = event;

                                                        DataLoader.enhanceFileFeatureLayer(featureLayer, {
                                                            renderer: 'circlePoint',
                                                            colour: [
                                                                bricksData.color.rgb_[0],
                                                                bricksData.color.rgb_[1],
                                                                bricksData.color.rgb_[2],
                                                                255
                                                            ],
                                                            nameField: bricksData.primaryAttribute.dropDownValue,
                                                            icon: iconTemplate,
                                                            datasetName: bricksData.datasetName.inputValue,
                                                            fields: csvHeaders
                                                        });

                                                        //TODO: set symbology and colour on feature layer (obj.data)
                                                        LayerLoader.loadLayer(featureLayer);
                                                        addDatasetPopup.close();
                                                    }, function () {
                                                        // can't construct csv
                                                        handleFailure(step, null, {
                                                            datasetName:
                                                                lang.mixin(choiceTreeErrors.csvError, {
                                                                    message: i18n.t('addDataset.error.messageCSVBroken')
                                                                })
                                                        });
                                                    });
                                                }
                                            }
                                        ]
                                    }
                                ]
                            },
                            {
                                id: 'shapefileFileAttrStep',
                                content: [
                                    {
                                        id: 'datasetName',
                                        type: Bricks.SimpleInputBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.shapefileDatasetName'),
                                            header: i18n.t('addDataset.datasetName')
                                        }
                                    },
                                    {
                                        id: 'primaryAttribute',
                                        type: Bricks.DropDownBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.shapefilePrimaryAttribute'),
                                            header: i18n.t('addDataset.primaryAttribute')
                                        }
                                    },
                                    {
                                        id: 'color',
                                        type: Bricks.ColorPickerBrick,
                                        config: {
                                            instructions: i18n.t('addDataset.help.shapefileColour'),
                                            header: i18n.t('addDataset.colour')
                                        }
                                    },
                                    {
                                        id: 'addDataset',
                                        type: Bricks.ButtonBrick,
                                        config: {
                                            label: i18n.t('addDataset.addDatasetButton'),
                                            containerClass: 'button-brick-container-main',
                                            buttonClass: 'btn-primary'
                                        },
                                        on: [
                                            {
                                                eventName: Bricks.ButtonBrick.event.CLICK,
                                                // add wms service layer to the map
                                                callback: function (step /*,data*/) {
                                                    var data = step.getData(),
                                                        bricksData = data.bricksData,
                                                        featureLayer = data.stepData,

                                                        iconTemplate = makeIconTemplate('a_d_icon_' + featureLayer.renderer._RAMP_rendererType, bricksData.color.hex);

                                                    DataLoader.enhanceFileFeatureLayer(featureLayer, {
                                                        //renderer: obj.style,
                                                        colour: [
                                                            bricksData.color.rgb_[0],
                                                            bricksData.color.rgb_[1],
                                                            bricksData.color.rgb_[2],
                                                            255
                                                        ],
                                                        nameField: bricksData.primaryAttribute.dropDownValue,
                                                        icon: iconTemplate,
                                                        datasetName: bricksData.datasetName.inputValue,
                                                        fields: featureLayer.fields.map(function (field) { return field.name; })
                                                    });

                                                    LayerLoader.loadLayer(featureLayer);
                                                    addDatasetPopup.close();
                                                }
                                            }
                                        ]
                                    }
                                ]
                            }
                        ]
                    }
                ]
            };
        }

        /**
         * Creates a new choice tree html representation and appends it to the page.
         * 
         * @method createChoiceTree
         * @private
         */
        function createChoiceTree() {
            // clear steps
            t.dfs(choiceTree, function (node) {
                node.stepItem = null;
            });

            // create the choice tree
            t.dfs(choiceTree, function (node, par/*, ctrl*/) {
                var stepItem,
                    level = par ? par.level + 1 : 1;

                node.level = level;

                stepItem = new StepItem(node);
                stepItem.on(StepItem.event.CURRENT_STEP_CHANGE, setCurrentStep);
                stepItem.on(StepItem.event.STATE_CHANGE, setStepState);

                node.stepItem = stepItem;
                stepLookup[node.id] = stepItem;

                if (par) {
                    par.stepItem.addChild(stepItem);
                }

                console.log(node);
            });

            // append tree to the page
            rootNode
                .find('.add-dataset-content')
                .empty()
                .append(stepLookup.sourceTypeStep.node)
            ;

            // set the first step as active
            stepLookup.sourceTypeStep.currentStep(1);

            // It's IE9. Run Away!
            if (!window.FileReader) {
                var fileOrFileURL = stepLookup.fileTypeStep.contentBricks.fileOrFileURL;

                console.warn('You have IE9');

                fileOrFileURL.fileNode.remove();
                fileOrFileURL.filePseudoNode.attr('disabled', true);
                fileOrFileURL.browseFilesContainer
                    .attr({
                        title: i18n.t('addDataset.error.ie9FileAPI')
                    })
                    .addClass('_tooltip')
                ;
            }

            Theme.tooltipster(addDatasetContainer);
        }

        /**
         * From provided CSV data, guesses which columns are long and lat.
         * 
         * @method guessLatLong
         * @private
         * @param  {Array} rows csv data
         * @return {Object}      an object with lat and long string properties indicating corresponding field names
         */
        function guessLatLong(rows) {
            // try guessing lat and long columns
            var latRegex = new RegExp(/^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)$/i),
                longRegex = new RegExp(/^[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/i),

                guessesLat,
                guessesLong,

                guessedLatHeader,
                guessedLongHeader;

            // first filter out all columns that are not lat fro sure
            guessesLat = rows[0].filter(function (header, i) {
                return rows.every(function (row, rowi) {
                    return rowi === 0 || latRegex.test(row[i]);
                });
            });

            // filter out all columns that are not long for sure
            guessesLong = rows[0].filter(function (header, i) {
                return rows.every(function (row, rowi) {
                    return rowi === 0 || longRegex.test(row[i]);
                });
            });

            // console.log(guessesLat);
            // console.log(guessesLong);

            // if there more than one lat guesses
            if (guessesLat.length > 1) {
                // filter out ones that don't have 'la' or 'y' in header name
                guessesLat = guessesLat.filter(function (header) {
                    var h = header.toLowerCase();

                    return h.indexOf('la') !== -1 || h.indexOf('y') !== -1;
                });
            }

            // console.log(guessesLat);
            // pick the first lat guess or null
            guessedLatHeader = guessesLat[0] || null;

            // if there more than one long guesses
            if (guessesLong.length > 1) {
                // first, remove lat guess from long options in case they overlap
                UtilArray.remove(guessesLong, guessedLatHeader);

                // filter out ones that don't have 'lo' or 'x' in header name
                guessesLong = guessesLong.filter(function (header) {
                    var h = header.toLowerCase();

                    return h.indexOf('lo') !== -1 || h.indexOf('x') !== -1;
                });
            }

            // console.log(guessesLong);
            // pick the first long guess or null
            guessedLongHeader = guessesLong[0] || null;

            return {
                lat: guessedLatHeader,
                long: guessedLongHeader
            };
        }

        /**
         * Creates a icon base64 template to be displayed in the layer selector.
         * 
         * @method makeIconTemplate
         * @private 
         * @param {String} templateName a name of the template to use for an icon
         * @param {String} hex color value in hex
         * @return {String} a base64 encoded icon template
         */
        function makeIconTemplate(templateName, hex) {
            /*jshint validthis: true */
            return 'data:image/svg+xml;base64,' +
                UtilMisc.b64EncodeUnicode(
                    TmplHelper.template.call(this, templateName, {
                        colour: hex
                    }, templates)
                );
        }

        /**
         * Delay setting loading state to the step for a specified time in case it happens really fast and it will flicker.
         * 
         * @method delayLoadingState
         * @param {StepItem} step step to delay setting loading state on
         * @param {Number} time a delay in ms
         * @private
         * @return {Number} setTimeout handle
         */
        function delayLoadingState(step, time) {
            return window.setTimeout(function () {
                step._notifyStateChange(StepItem.state.LOADING);
            }, time);
        }

        /**
         * Handles any failure happening in the choice tree by setting the responsible step to error and displaying appropriate notices.
         * 
         * @method handleFailure
         * @private
         * @param  {StepItem} step         a step item that should handle failure
         * @param  {Number} handle       a timeout handle to be canceled
         * @param  {Object} brickNotices brick notices to be displayed 
         */
        function handleFailure(step, handle, brickNotices) {
            if (handle) {
                window.clearTimeout(handle);
            }

            step
                ._notifyStateChange(StepItem.state.ERROR)
                .displayBrickNotices(brickNotices)
            ;
        }

        /**
         * Notifies all step items in the tree which step is current at the moment.
         * 
         * @method setCurrentStep
         * @private
         * @param {String} event a StepItem.CURRENT_STEP_CHANGE event
         */
        function setCurrentStep(event) {
            t.dfs(choiceTree, function (node) {
                node.stepItem.currentStep(event.level, event.id);
            });
        }

        /**
         * Notifies all step items in the tree that a certain step has changed its state.
         * 
         * @method setStepState
         * @private
         * @param {Object} event a StepItem.STATE_CHANGE event
         * @param {StepItem} step  a step item; this doesn't seem to be used
         * @param {String} state a state; this doesn't seem to be used
         */
        function setStepState(event, step, state) {
            if (step && state) {
                event = {
                    id: step.id,
                    level: step.level,
                    state: state
                };
            }

            t.dfs(choiceTree, function (node) {
                node.stepItem.setState(event.level, event.id, event.state);
            });
        }

        /**
         * Closes the add dataset choice tree.
         * 
         * @method closeChoiceTree
         * @private
         */
        function closeChoiceTree() {
            stepLookup.sourceTypeStep.retreat();
        }

        return {
            /**
             * Initialize add-dataset functionality and creates a add-dataset choice tree.
             * 
             * @method init
             * @static
             */
            init: function () {
                var tl = new TimelineLite({ paused: true });

                rootNode = $('#searchMapSectionBody');

                addDatasetToggle = rootNode.find('#addDatasetToggle');
                addDatasetContainer = rootNode.find('#add-dataset-section-container');

                layerList = rootNode.find('#layerList');
                layerToggles = rootNode.find('.layer-checkboxes:first');
                filterToggles = rootNode.find('#filterGlobalToggles');

                prepareChoiceTreeStructure();
                createChoiceTree();

                tl
                    .set(addDatasetContainer, { display: 'block' })

                    .set(layerList, { className: '+=scroll' }, 0.01)
                    .set(filterToggles, { className: '+=scroll' }, 0.01)

                    .to(filterToggles, transitionDuration, { top: -60, ease: 'easeOutCirc' })
                    .to(layerList, transitionDuration, { top: layerList.height() / 3, ease: 'easeOutCirc' }, 0)
                    .to(layerList, transitionDuration / 2, { autoAlpha: 0, ease: 'easeOutCirc' }, transitionDuration / 2)

                    .set(layerToggles, { display: 'none' })
                ;

                addDatasetPopup = PopupManager.registerPopup(addDatasetToggle, 'click',
                    function (d) {
                        createChoiceTree();

                        tl
                            .eventCallback('onComplete', function () {
                                addDatasetContainer.find(':focusable:first').focus();
                            })
                           .play()
                        ;

                        d.resolve();
                    },
                    {
                        closeHandler: function (d) {
                            closeChoiceTree();

                            tl
                                .eventCallback('onReverseComplete', function () {
                                })
                               .reverse()
                            ;

                            d.resolve();
                        },
                        target: addDatasetContainer,
                        activeClass: 'button-pressed',
                        resetFocusOnClose: true
                    }
                );

                UtilDict.forEachEntry(GlobalStorage.DefaultRenderers,
                    function (key) {
                        symbologyPreset[key] = i18n.t('presets.defaultRenderers.' + key);
                        //symbologyPreset[key] = value.title;
                    }
                );
            }
        };
    });