<commit_before>/**
 * FeatureLayer.selection-complete
 * @external FeatureLayer#event:selection-complete
 * @see {link:https://developers.arcgis.com/javascript/jsapi/featurelayer-amd.html#event-selection-complete}
 * @member {Graphic[]} features - The features that were selected by a query.
 * @member {Number} method - The selection method.
 * @member {FeatureLayer} target - The feature layer that performed the selection.
 */

/**
 * Create map response
 * @external createMapResponse
 * @see {@link https://developers.arcgis.com/javascript/jsapi/esri.arcgis.utils-amd.html#createmap esri/arcgis/utils.createMap()
 * @member {external:esri/Map} map
 * @member {(EventHandler|undefined)} clickEventHandle
 * @member {(Function|undefined)} clickEventListener
 * @member {Object} itemInfo
 * @member {Object} itemInfo.item
 * @member {Object} itemInfo.itemData
 * @member {Object.<string, Error>} itemInfo.errors
 */

/**
 * dojo/Deferred
 * @description Deferred response from an asynchronous operation.
 * @external dojo/Deferred
 * @see {@link http://dojotoolkit.org/reference-guide/1.10/dojo/Deferred.html dojo/Deferred}
 */

require([
  "esri/config",
  "esri/map",
  "esri/geometry/geometryEngineAsync",
  "esri/tasks/query",
  "esri/layers/FeatureLayer",
  "esri/layers/ArcGISDynamicMapServiceLayer",
  "esri/dijit/FeatureTable",
  "esri/dijit/LayerList",
  "esri/dijit/HomeButton",
  "dijit/registry",
  "witpa/ProjectFilter",
  "dojo/parser",
  "dojo/dnd/Moveable",
  "esri/arcgis/utils",
  "esri/dijit/Search",
  "esri/dijit/BasemapGallery",
  "esri/dijit/Legend",
  "esri/tasks/QueryTask",
  "esri/tasks/StatisticDefinition",
  "esri/symbols/SimpleLineSymbol",
  "esri/Color",
  "witpa/infoWindowUtils",
  "witpa/wsdotMapUtils",
  "dojo/text!./webmap/item.json",
  "dojo/text!./webmap/itemdata.json",

  "dijit/layout/BorderContainer",
  "dijit/layout/ContentPane",
  "dijit/layout/AccordionContainer",
  "dijit/layout/AccordionPane"
], function (
  esriConfig,
  Map,
  geometryEngineAsync,
  Query,
  FeatureLayer,
  ArcGISDynamicMapServiceLayer,
  FeatureTable,
  LayerList,
  HomeButton,
  registry,
  ProjectFilter,
  parser,
  Moveable,
  arcgisUtils,
  Search,
  BasemapGallery,
  Legend,
  QueryTask,
  StatisticDefinition,
  SimpleLineSymbol,
  Color,
  infoWindowUtils,
  wsdotMapUtils,
  webmapItem,
  webmapItemData
) {
    "use strict";
    var projectFilter = new ProjectFilter(document.forms.filterForm);
    var filterPane = document.getElementById("filterPane");

    filterPane.appendChild(projectFilter.form);

    ["hqolymgis98d"].forEach(function (server) {
        esriConfig.defaults.io.corsEnabledServers.push(server);
    });

    // Parse the Dojo layout widgets defined in HTML markup.
    parser.parse();

    webmapItem = JSON.parse(webmapItem);
    webmapItemData = JSON.parse(webmapItemData);

    // Create the map using JSON webmap definition.
    arcgisUtils.createMap({
        item: webmapItem,
        itemData: webmapItemData
    }, "map", {
        mapOptions: wsdotMapUtils.defaultMapOptions
    }
    /**
     * Post map creation tasks.
     * @param {external:createMapResponse} response
     */
    ).then(function (response) {
        var map = response.map;

        infoWindowUtils.makeInfoWindowDraggable(response.map.infoWindow);

        var dynamicLayer = map.getLayer("PlannedProjects");

        var search = new Search({
            map: response.map
        }, "search");

        search.on("load", function () {
            var sources, source;
            sources = search.get("sources");
            source = search.defaultSource; //or sources[0];
            source.countryCode = "US";
            source.searchExtent = response.map.extent;
            search.set("sources", sources);
        });

        search.startup();

        var outFields = [
            "OBJECTID",
            "StateRouteNumber",
            "Project_Title",
            "County",
            "Legislative_District",
            "Revenue_Package",
            "Sub_Category",
            "PIN",
            "Improvement_Type",
            "Work_Description",
            "Sub_Program",
            "Region",
            "Program",
            "Begin_Preliminary_Engineering",
            "Advertisement_Date",
            "Operationally_Complete",
            "Congressional_District",
            "Begin_Mile_Post",
            "End_Mile_Post",
            "Total_Dollars",
            "Direction_Ind",
            "RouteID",
            "Begin_ARM",
            "End_ARM",
            "SRMP_Begin_AB_Ind",
            "SRMP_End_AB_Ind",
            "SRMP_Date",
            "LRSDATE",
            "RelRouteType",
            "RelRouteQual",
            "Mid_Arm",
            "Mid_Mile_Post",
            "Project_List",
            "LOC_ERROR"
        ];

        var table;

        // Create the feature layer that will be used by the FeatureTable and to highlight selected table rows on the map.
        var featureLayerUrl = [dynamicLayer.url, "0"].join("/");
        var layer = new FeatureLayer(featureLayerUrl, {
            id: "wsdotprojects",
            mode: FeatureLayer.MODE_SELECTION,
            outFields: outFields
        });

        map.addLayer(layer);

        var selectionSymbol = new SimpleLineSymbol().setColor(new Color([0, 255, 255, 1])).setWidth(2);
        layer.setSelectionSymbol(selectionSymbol);

        // When the user submits a query, set the feature layer's definition expression
        // so that only those rows are shown in the table.
        projectFilter.form.addEventListener("submit-query", function (e) {
            layer.clearSelection();
            layer.setDefinitionExpression(e.detail.where);
            dynamicLayer.setLayerDefinitions([
                e.detail.where
            ]);
            // Query extent. The layer def. exp. is already applied
            layer.queryExtent(new Query()).then(function (queryExtentResponse) {
                var extent = queryExtentResponse.extent;
                map.setExtent(extent);
            });
            table.grid.refresh();

        });

        // Reset the definition expression to show all rows.
        projectFilter.form.addEventListener("reset", function (e) {
            layer.clearSelection();
            dynamicLayer.setDefaultLayerDefinitions();
            layer.setDefinitionExpression(layer.defaultDefinitionExpression);
            table.grid.refresh();
        });

        // Add the home button that allows the user to zoom to full extent.
        var homeButton = new HomeButton({
            map: map
        }, "homeButton");
        homeButton.startup();

        // Create the feature table
        table = new FeatureTable({
            featureLayer: layer,
            outFields: outFields,
            editable: false,
            syncSelection: false,
            zoomToSelection: false,
            // The dateOptions are not actually honored: https://geonet.esri.com/message/520158
            /*
            dateOptions: {
              datePattern: "yyyyMMdd",
              timeEnabled: false,
              timePattern: null
            },
            */
            // These fields are hidden by default, but user can turn them back on.
            hiddenFields: [
                "Direction_Ind",
                "RouteID",
                "Begin_ARM",
                "End_ARM",
                "SRMP_Begin_AB_Ind",
                "SRMP_End_AB_Ind",
                "SRMP_Date",
                "LRSDATE",
                "RelRouteType",
                "RelRouteQual",
                "Mid_Arm",
                "Mid_Mile_Post",
                "Project_List",
                "LOC_ERROR"
            ],
            showGridHeader: true,
            map: map
        }, "table");
        table.startup();

        function zoomToFeatures(features) {
            var geometries;
            if (features && features.length > 0) {
                if (features.length === 1) {
                    map.setExtent(features[0].geometry.getExtent(), true);
                } else {
                    geometries = features.map(function (graphic) {
                        return graphic.geometry;
                    });
                    geometryEngineAsync.union(geometries).then(function (unionedGeometry) {
                        map.setExtent(unionedGeometry.getExtent(), true);
                    });
                }
            }
        }

        /**
         * @typedef {Object} DGridRow
         * @property {Object} data
         * @property {number} data.OBJECTID
         * @property {HTMLElement} element
         * @property {string} id - A string containing the corresponding feature's Object ID.
         */

        /**
         * Selects or deselects features corresponding to row selected in the feature table.
         * @param {DGridRow[]} rows - An array of rows either selected or deselected from the feature table dGrid.
         * @param {Boolean} [deselect=false] - True to subtract the features associated with the rows to the feature layer selection, false to add them.
         * @returns {external:dojo/Deferred} Returns the promise from the selectFeatures function.
         */
        function selectOrDeselectFeatures(rows, deselect) {
            var selectionMethod = deselect ? FeatureLayer.SELECTION_SUBTRACT : FeatureLayer.SELECTION_ADD;
            var query = new Query();
            // Convert the array of rows into an array of corresponding object IDs.
            var objectIds = rows.map(function (row) {
                return parseInt(row.id, 10);
            });
            query.objectIds = objectIds;
            return layer.selectFeatures(query, selectionMethod);
        }

        layer.on("selection-complete", function (features, selectionMethod) {
            features = layer.getSelectedFeatures();
            zoomToFeatures(features);
        });

        /**
         *
         * @param {DGridRow[]} rows - The rows that were selected
         */
        table.on("row-select", function (rows) {
            selectOrDeselectFeatures(rows);
        });

        /**
         *
         * @param {DGridRow[]} rows - The rows that were unselected
         */
        table.on("row-deselect", function (rows) {
            selectOrDeselectFeatures(rows, true);
        });

        // resize panel when table close is toggled.
        table.tableCloseButton.addEventListener("click", function (e) {
            var gridMenuNode = registry.byId(table._gridMenu).domNode;
            var tableNode = registry.byId("tablePane").domNode;
            var borderContainer = registry.byId("borderContainer");
            var isOpening = e.target.classList.contains("toggleClosed");
            if (isOpening) {
                tableNode.style.height = tableNode.dataset.openHeight || "50%";
            } else {
                // Store the old height.
                tableNode.dataset.openHeight = tableNode.style.height || [tableNode.clientHeight, "px"].join("");
                // Set to "closed" height.
                tableNode.style.height = [gridMenuNode.clientHeight, "px"].join("");
            }
            borderContainer.resize();
        });

        var layerListItems = arcgisUtils.getLayerList(response);

        // Custom sort the layer list items array so that the item with the
        // title containing "Planned Projects" is last in the array. Other
        // items will remain in their original order.
        layerListItems.sort(function (a, b) {
            var re = /Planned\s?Projects/i;
            var output;
            if (re.test(a.title)) {
                output = 1;
            } else if (re.test(b.title)) {
                output = -1;
            } else {
                output = 0;
            }
            return output;
        });

        // Create the layer list.
        var layerList = new LayerList({
            map: response.map,
            layers: layerListItems,
            showLegend: true,
            showOpacitySlider: true,
            showSublayers: true
        }, "layerList");
        layerList.startup();

        var basemapGallery = new BasemapGallery({
            map: response.map,
            basemapsGroup: {
                id: "085a9cb0bb664d29bf62b731ccc4aa64"
            },
            basemaps: [
                wsdotMapUtils.esriBasemaps["wsdot-multilevel"]
            ],
            basemapIds: wsdotMapUtils.esriBasemaps["wsdot-multilevel"].layers.map(function (layer) {
                return layer.id;
            })
        }, "basemapGallery");
        basemapGallery.startup();

        // Select the default basemap in the gallery.
        basemapGallery.on("load", function () {
            var basemap, basemaps = basemapGallery.basemaps, i;
            if (basemaps && basemaps.length > 0) {
                for (i = 0; i < basemaps.length; i++) {
                    basemap = basemaps[i];
                    if (/wsdot[\-\s]?multilevel/i.test(basemap.id)) {
                        basemapGallery.select(basemap.id);
                        break;
                    }
                }
            }
        });

        var legend = new Legend({
            map: response.map,
            layerInfos: arcgisUtils.getLegendLayers(response)
        }, "legend");
        legend.startup();

    });

    function getOperationalLayer(webMapData, opLayerId) {
        var opLayers = webMapData.operationalLayers;
        var opLayer;
        for (var i = 0; i < opLayers.length; i++) {
            if (opLayers[i].id === opLayerId) {
                opLayer = opLayers[i];
                break;
            }
        }
        return opLayer;
    }

    /**
     * Setup the background worker process for querying the 6-year project map service
     * for input suggestions.
     * @param {string} url - The URL for the map service layer to be queried.
     */
    (function (url) {

        if (!window.Worker) {
            console.log("This browser doesn't support web workers. Some features are not available.");
            return;
        }

        // Create the worker.
        var worker = new Worker("QueryWorker.js");

        /**
         * Handles messages from web worker.
         * @param {Event} e - worker message event
         * @param {Object} e.data - Data passed from worker.
         */
        worker.addEventListener("message", function (e) {

            /**
             * Adds values to the datalist corresponding to the given field name.
             * @param {string} fieldName - The name of a field in a map service layer.
             * @param {string[]} values - Values to be added to the datalist.
             */
            function addToDataList(fieldName, values) {
                var datalist, docFrag;
                datalist = document.querySelector("datalist[data-field-name='" + fieldName + "']");
                if (datalist) {
                    docFrag = document.createDocumentFragment();
                    values.forEach(function (value) {
                        var option = document.createElement("option");
                        option.value = value;
                        docFrag.appendChild(option);
                    });
                    datalist.appendChild(docFrag);
                }
            }

            /**
             * Adds attributes to input elements.
             * @param {Object.<string, Object.<string, string>>} values - Object containing objects containing date representation strings.
             */
            function addAttributes(values) {
                var fieldName, value, input, selector;
                for (fieldName in values) {
                    if (values.hasOwnProperty(fieldName)) {
                        value = values[fieldName];
                        selector = "input[type=date][name=" + fieldName + "]";
                        input = document.querySelector(selector);
                        if (input) {
                            for (var attrName in value) {
                                if (value.hasOwnProperty(attrName)) {
                                    input.setAttribute(attrName, value[attrName]);
                                }
                            }
                        } else {
                            console.error("Expected input element not found.", selector);
                        }
                    }
                }
            }

            // Get the data from the worker message.
            var data = e.data;
            // If the data contains a "fieldName" property, it is the result of
            // a query for unique values for a single field.
            //
            // If the data has a "ranges" property, it is the result of a query
            // for the min and max values of the date fields.
            if (data.fieldName) {
                addToDataList(data.fieldName, data.values);
            } else if (data.ranges) {
                addAttributes(data.ranges);
            }
        });

        // Set up worker event handler for error messages.
        worker.addEventListener("error", function (e) {
            console.error(e.data);
        });

        // Send the map service layer URL to the web worker, which will start it processing.
        worker.postMessage({ url: url });
    }(getOperationalLayer(webmapItemData, "PlannedProjects").url + "/0"));
});<commit_msg>RE-omitted "OBJECTID"<commit_after>/**
 * FeatureLayer.selection-complete
 * @external FeatureLayer#event:selection-complete
 * @see {link:https://developers.arcgis.com/javascript/jsapi/featurelayer-amd.html#event-selection-complete}
 * @member {Graphic[]} features - The features that were selected by a query.
 * @member {Number} method - The selection method.
 * @member {FeatureLayer} target - The feature layer that performed the selection.
 */

/**
 * Create map response
 * @external createMapResponse
 * @see {@link https://developers.arcgis.com/javascript/jsapi/esri.arcgis.utils-amd.html#createmap esri/arcgis/utils.createMap()
 * @member {external:esri/Map} map
 * @member {(EventHandler|undefined)} clickEventHandle
 * @member {(Function|undefined)} clickEventListener
 * @member {Object} itemInfo
 * @member {Object} itemInfo.item
 * @member {Object} itemInfo.itemData
 * @member {Object.<string, Error>} itemInfo.errors
 */

/**
 * dojo/Deferred
 * @description Deferred response from an asynchronous operation.
 * @external dojo/Deferred
 * @see {@link http://dojotoolkit.org/reference-guide/1.10/dojo/Deferred.html dojo/Deferred}
 */

require([
  "esri/config",
  "esri/map",
  "esri/geometry/geometryEngineAsync",
  "esri/tasks/query",
  "esri/layers/FeatureLayer",
  "esri/layers/ArcGISDynamicMapServiceLayer",
  "esri/dijit/FeatureTable",
  "esri/dijit/LayerList",
  "esri/dijit/HomeButton",
  "dijit/registry",
  "witpa/ProjectFilter",
  "dojo/parser",
  "dojo/dnd/Moveable",
  "esri/arcgis/utils",
  "esri/dijit/Search",
  "esri/dijit/BasemapGallery",
  "esri/dijit/Legend",
  "esri/tasks/QueryTask",
  "esri/tasks/StatisticDefinition",
  "esri/symbols/SimpleLineSymbol",
  "esri/Color",
  "witpa/infoWindowUtils",
  "witpa/wsdotMapUtils",
  "dojo/text!./webmap/item.json",
  "dojo/text!./webmap/itemdata.json",

  "dijit/layout/BorderContainer",
  "dijit/layout/ContentPane",
  "dijit/layout/AccordionContainer",
  "dijit/layout/AccordionPane"
], function (
  esriConfig,
  Map,
  geometryEngineAsync,
  Query,
  FeatureLayer,
  ArcGISDynamicMapServiceLayer,
  FeatureTable,
  LayerList,
  HomeButton,
  registry,
  ProjectFilter,
  parser,
  Moveable,
  arcgisUtils,
  Search,
  BasemapGallery,
  Legend,
  QueryTask,
  StatisticDefinition,
  SimpleLineSymbol,
  Color,
  infoWindowUtils,
  wsdotMapUtils,
  webmapItem,
  webmapItemData
) {
    "use strict";
    var projectFilter = new ProjectFilter(document.forms.filterForm);
    var filterPane = document.getElementById("filterPane");

    filterPane.appendChild(projectFilter.form);

    ["hqolymgis98d"].forEach(function (server) {
        esriConfig.defaults.io.corsEnabledServers.push(server);
    });

    // Parse the Dojo layout widgets defined in HTML markup.
    parser.parse();

    webmapItem = JSON.parse(webmapItem);
    webmapItemData = JSON.parse(webmapItemData);

    // Create the map using JSON webmap definition.
    arcgisUtils.createMap({
        item: webmapItem,
        itemData: webmapItemData
    }, "map", {
        mapOptions: wsdotMapUtils.defaultMapOptions
    }
    /**
     * Post map creation tasks.
     * @param {external:createMapResponse} response
     */
    ).then(function (response) {
        var map = response.map;

        infoWindowUtils.makeInfoWindowDraggable(response.map.infoWindow);

        var dynamicLayer = map.getLayer("PlannedProjects");

        var search = new Search({
            map: response.map
        }, "search");

        search.on("load", function () {
            var sources, source;
            sources = search.get("sources");
            source = search.defaultSource; //or sources[0];
            source.countryCode = "US";
            source.searchExtent = response.map.extent;
            search.set("sources", sources);
        });

        search.startup();

        var outFields = [
            //"OBJECTID",
            "StateRouteNumber",
            "Project_Title",
            "County",
            "Legislative_District",
            "Revenue_Package",
            "Sub_Category",
            "PIN",
            "Improvement_Type",
            "Work_Description",
            "Sub_Program",
            "Region",
            "Program",
            "Begin_Preliminary_Engineering",
            "Advertisement_Date",
            "Operationally_Complete",
            "Congressional_District",
            "Begin_Mile_Post",
            "End_Mile_Post",
            "Total_Dollars",
            "Direction_Ind",
            "RouteID",
            "Begin_ARM",
            "End_ARM",
            "SRMP_Begin_AB_Ind",
            "SRMP_End_AB_Ind",
            "SRMP_Date",
            "LRSDATE",
            "RelRouteType",
            "RelRouteQual",
            "Mid_Arm",
            "Mid_Mile_Post",
            "Project_List",
            "LOC_ERROR"
        ];

        var table;

        // Create the feature layer that will be used by the FeatureTable and to highlight selected table rows on the map.
        var featureLayerUrl = [dynamicLayer.url, "0"].join("/");
        var layer = new FeatureLayer(featureLayerUrl, {
            id: "wsdotprojects",
            mode: FeatureLayer.MODE_SELECTION,
            outFields: outFields
        });

        map.addLayer(layer);

        var selectionSymbol = new SimpleLineSymbol().setColor(new Color([0, 255, 255, 1])).setWidth(2);
        layer.setSelectionSymbol(selectionSymbol);

        // When the user submits a query, set the feature layer's definition expression
        // so that only those rows are shown in the table.
        projectFilter.form.addEventListener("submit-query", function (e) {
            layer.clearSelection();
            layer.setDefinitionExpression(e.detail.where);
            dynamicLayer.setLayerDefinitions([
                e.detail.where
            ]);
            // Query extent. The layer def. exp. is already applied
            layer.queryExtent(new Query()).then(function (queryExtentResponse) {
                var extent = queryExtentResponse.extent;
                map.setExtent(extent);
            });
            table.grid.refresh();

        });

        // Reset the definition expression to show all rows.
        projectFilter.form.addEventListener("reset", function (e) {
            layer.clearSelection();
            dynamicLayer.setDefaultLayerDefinitions();
            layer.setDefinitionExpression(layer.defaultDefinitionExpression);
            table.grid.refresh();
        });

        // Add the home button that allows the user to zoom to full extent.
        var homeButton = new HomeButton({
            map: map
        }, "homeButton");
        homeButton.startup();

        // Create the feature table
        table = new FeatureTable({
            featureLayer: layer,
            outFields: outFields,
            editable: false,
            syncSelection: false,
            zoomToSelection: false,
            // These fields are hidden by default, but user can turn them back on.
            hiddenFields: [
                "Direction_Ind",
                "RouteID",
                "Begin_ARM",
                "End_ARM",
                "SRMP_Begin_AB_Ind",
                "SRMP_End_AB_Ind",
                "SRMP_Date",
                "LRSDATE",
                "RelRouteType",
                "RelRouteQual",
                "Mid_Arm",
                "Mid_Mile_Post",
                "Project_List",
                "LOC_ERROR"
            ],
            showGridHeader: true,
            map: map
        }, "table");
        table.startup();

        function zoomToFeatures(features) {
            var geometries;
            if (features && features.length > 0) {
                if (features.length === 1) {
                    map.setExtent(features[0].geometry.getExtent(), true);
                } else {
                    geometries = features.map(function (graphic) {
                        return graphic.geometry;
                    });
                    geometryEngineAsync.union(geometries).then(function (unionedGeometry) {
                        map.setExtent(unionedGeometry.getExtent(), true);
                    });
                }
            }
        }

        /**
         * @typedef {Object} DGridRow
         * @property {Object} data
         * @property {number} data.OBJECTID
         * @property {HTMLElement} element
         * @property {string} id - A string containing the corresponding feature's Object ID.
         */

        /**
         * Selects or deselects features corresponding to row selected in the feature table.
         * @param {DGridRow[]} rows - An array of rows either selected or deselected from the feature table dGrid.
         * @param {Boolean} [deselect=false] - True to subtract the features associated with the rows to the feature layer selection, false to add them.
         * @returns {external:dojo/Deferred} Returns the promise from the selectFeatures function.
         */
        function selectOrDeselectFeatures(rows, deselect) {
            var selectionMethod = deselect ? FeatureLayer.SELECTION_SUBTRACT : FeatureLayer.SELECTION_ADD;
            var query = new Query();
            // Convert the array of rows into an array of corresponding object IDs.
            var objectIds = rows.map(function (row) {
                return parseInt(row.id, 10);
            });
            query.objectIds = objectIds;
            return layer.selectFeatures(query, selectionMethod);
        }

        layer.on("selection-complete", function (features, selectionMethod) {
            features = layer.getSelectedFeatures();
            zoomToFeatures(features);
        });

        /**
         *
         * @param {DGridRow[]} rows - The rows that were selected
         */
        table.on("row-select", function (rows) {
            selectOrDeselectFeatures(rows);
        });

        /**
         *
         * @param {DGridRow[]} rows - The rows that were unselected
         */
        table.on("row-deselect", function (rows) {
            selectOrDeselectFeatures(rows, true);
        });

        // resize panel when table close is toggled.
        table.tableCloseButton.addEventListener("click", function (e) {
            var gridMenuNode = registry.byId(table._gridMenu).domNode;
            var tableNode = registry.byId("tablePane").domNode;
            var borderContainer = registry.byId("borderContainer");
            var isOpening = e.target.classList.contains("toggleClosed");
            if (isOpening) {
                tableNode.style.height = tableNode.dataset.openHeight || "50%";
            } else {
                // Store the old height.
                tableNode.dataset.openHeight = tableNode.style.height || [tableNode.clientHeight, "px"].join("");
                // Set to "closed" height.
                tableNode.style.height = [gridMenuNode.clientHeight, "px"].join("");
            }
            borderContainer.resize();
        });

        var layerListItems = arcgisUtils.getLayerList(response);

        // Custom sort the layer list items array so that the item with the
        // title containing "Planned Projects" is last in the array. Other
        // items will remain in their original order.
        layerListItems.sort(function (a, b) {
            var re = /Planned\s?Projects/i;
            var output;
            if (re.test(a.title)) {
                output = 1;
            } else if (re.test(b.title)) {
                output = -1;
            } else {
                output = 0;
            }
            return output;
        });

        // Create the layer list.
        var layerList = new LayerList({
            map: response.map,
            layers: layerListItems,
            showLegend: true,
            showOpacitySlider: true,
            showSublayers: true
        }, "layerList");
        layerList.startup();

        var basemapGallery = new BasemapGallery({
            map: response.map,
            basemapsGroup: {
                id: "085a9cb0bb664d29bf62b731ccc4aa64"
            },
            basemaps: [
                wsdotMapUtils.esriBasemaps["wsdot-multilevel"]
            ],
            basemapIds: wsdotMapUtils.esriBasemaps["wsdot-multilevel"].layers.map(function (layer) {
                return layer.id;
            })
        }, "basemapGallery");
        basemapGallery.startup();

        // Select the default basemap in the gallery.
        basemapGallery.on("load", function () {
            var basemap, basemaps = basemapGallery.basemaps, i;
            if (basemaps && basemaps.length > 0) {
                for (i = 0; i < basemaps.length; i++) {
                    basemap = basemaps[i];
                    if (/wsdot[\-\s]?multilevel/i.test(basemap.id)) {
                        basemapGallery.select(basemap.id);
                        break;
                    }
                }
            }
        });

        var legend = new Legend({
            map: response.map,
            layerInfos: arcgisUtils.getLegendLayers(response)
        }, "legend");
        legend.startup();

    });

    function getOperationalLayer(webMapData, opLayerId) {
        var opLayers = webMapData.operationalLayers;
        var opLayer;
        for (var i = 0; i < opLayers.length; i++) {
            if (opLayers[i].id === opLayerId) {
                opLayer = opLayers[i];
                break;
            }
        }
        return opLayer;
    }

    /**
     * Setup the background worker process for querying the 6-year project map service
     * for input suggestions.
     * @param {string} url - The URL for the map service layer to be queried.
     */
    (function (url) {

        if (!window.Worker) {
            console.log("This browser doesn't support web workers. Some features are not available.");
            return;
        }

        // Create the worker.
        var worker = new Worker("QueryWorker.js");

        /**
         * Handles messages from web worker.
         * @param {Event} e - worker message event
         * @param {Object} e.data - Data passed from worker.
         */
        worker.addEventListener("message", function (e) {

            /**
             * Adds values to the datalist corresponding to the given field name.
             * @param {string} fieldName - The name of a field in a map service layer.
             * @param {string[]} values - Values to be added to the datalist.
             */
            function addToDataList(fieldName, values) {
                var datalist, docFrag;
                datalist = document.querySelector("datalist[data-field-name='" + fieldName + "']");
                if (datalist) {
                    docFrag = document.createDocumentFragment();
                    values.forEach(function (value) {
                        var option = document.createElement("option");
                        option.value = value;
                        docFrag.appendChild(option);
                    });
                    datalist.appendChild(docFrag);
                }
            }

            /**
             * Adds attributes to input elements.
             * @param {Object.<string, Object.<string, string>>} values - Object containing objects containing date representation strings.
             */
            function addAttributes(values) {
                var fieldName, value, input, selector;
                for (fieldName in values) {
                    if (values.hasOwnProperty(fieldName)) {
                        value = values[fieldName];
                        selector = "input[type=date][name=" + fieldName + "]";
                        input = document.querySelector(selector);
                        if (input) {
                            for (var attrName in value) {
                                if (value.hasOwnProperty(attrName)) {
                                    input.setAttribute(attrName, value[attrName]);
                                }
                            }
                        } else {
                            console.error("Expected input element not found.", selector);
                        }
                    }
                }
            }

            // Get the data from the worker message.
            var data = e.data;
            // If the data contains a "fieldName" property, it is the result of
            // a query for unique values for a single field.
            //
            // If the data has a "ranges" property, it is the result of a query
            // for the min and max values of the date fields.
            if (data.fieldName) {
                addToDataList(data.fieldName, data.values);
            } else if (data.ranges) {
                addAttributes(data.ranges);
            }
        });

        // Set up worker event handler for error messages.
        worker.addEventListener("error", function (e) {
            console.error(e.data);
        });

        // Send the map service layer URL to the web worker, which will start it processing.
        worker.postMessage({ url: url });
    }(getOperationalLayer(webmapItemData, "PlannedProjects").url + "/0"));
});<|endoftext|>


<commit_before>var config           = require('mbc-common').config.Mosto.HeartBeats,
    util             = require('util'),
    events           = require('events'),
    Mosto            = require('./models/Mosto'),
    moment           = require('moment'),
    mvcp_server      = require('./drivers/mvcp/mvcp-driver'),
    utils            = require('./utils');

function heartbeats(customConfig) {
    //THIS MODULE ASSUMES MELTED ALWAYS HAS THE SAME CLIPS AS MELTED_MEDIAS
    //AND THAT THEY ARE IN THE SAME ORDER
    defaults = {
        gc_interval: 1000 * 60 * 60,
        sync_interval: 50,
        min_scheduled: 1000 * 60 * 60 * 4,
        mvcp_server: "melted"
    };
    this.config = customConfig || config || defaults;

    this.melted_medias = Mosto.Playlists.get('melted_medias');

    this.current_media = false;

    this.stop_timers = false;

    this.server = new mvcp_server(this.config.mvcp_server);

    events.EventEmitter.call(this);
}

util.inherits(heartbeats, events.EventEmitter);
heartbeats.prototype.startMvcpServer = function(callback) {
    var self = this;
    var result = self.server.initServer();
    result.then(function() {
        console.log("[HEARTBEAT-MVCP] MVCP server started");
        if (callback !== undefined) {
            callback();
        }
    }, function(err) {
        var e = new Error("[HEARTBEAT-MVCP] Error starting MVCP server: " + err + ".\nRetrying in 2 seconds...");
        console.error(e);
        setTimeout(function() {
            self.startMvcpServer(callback);
        }, 2000);
    });
};

heartbeats.prototype.init = function() {
    this.startMvcpServer(this.initTimers);
};

heartbeats.prototype.initTimers = function() {
    var self = this;
    self.scheduleGc();
    self.scheduleSync();
    self.scheduleCheckout();
};

heartbeats.prototype.scheduleGc = function() {
    var self = this;
    if (!self.stop_timers) {
        setTimeout(function() {
            self.executeGc();
        }, self.config.gc_interval);
    }
};

heartbeats.prototype.scheduleSync = function() {
    var self = this;
    if (!self.stop_timers) {
        setTimeout(function() {
            self.melted_medias.take(self.syncMelted.bind(self));
        }, self.config.sync_interval);
    }
};

heartbeats.prototype.scheduleCheckout = function() {
    var self = this;
    if (!self.stop_timers) {
        setTimeout(function() {
            self.checkSchedules();
        }, self.config.min_scheduled / 4);
    }
};

heartbeats.prototype.stop = function() {
    this.stop_timers = true;
};

heartbeats.prototype.checkSchedules =  function() {
    var self = this;
    var last = self.melted_medias.at(self.melted_medias.length - 1);
    var scheduled =  last.get('end') - moment();
    if (scheduled < self.config.min_scheduled)
        self.emit("forceCheckout", scheduled);
};

heartbeats.prototype.executeGc = function() {
    var self = this;
    console.log("[HEARTBEAT-GC] Started Garbage Collector");
    var timeLimit = moment().subtract('hours', 1);
    var oldMedias = self.melted_medias.filter(function(media) {
        return moment(media.get('end')) < timeLimit;
    });
    if (oldMedias) {
        oldMedias.forEach(function(media) {
            self.melted_medias.remove(media);
        });
        self.melted_medias.save();
    }
    self.scheduleGc();
    console.log("[HEARTBEAT-GC] Finished Garbage Collector: " + oldMedias.length + " removed.");
};

heartbeats.prototype.sendStatus = function() {
    var self = this;
    console.log("[HEARTBEAT-FS] Started Status");
//    try {
        var expected = getExpectedMedia();
        if (!self.current_media)
            self.current_media = expected.media;
        if (expected.media.get("id").toString() !== self.current_media.get("id").toString()) {
            self.emit("clipStatus", {old_media: self.current_media, new_media: expected.media, frame: expected.frame});
            self.current_media = expected.media;
        } else {
            self.emit("frameStatus", {media: expected.media, frame: expected.frame});
        }
//    } catch(error) {
//        self.handleError(error);
//    }
    console.log("[HEARTBEAT-FS] Finished Status");
};

heartbeats.prototype.getExpectedMedia = function() {
    var self = this;
    var now = moment();
    var expected = {};
    var media = self.melted_medias.find(function(media) {
        return moment(media.end) >= now;
    });
    if (media) {
        var elapsed = now - moment(media.start);
        var frame = elapsed / media.fps;
        expected.media = media;
        expected.frame = frame;
        return expected;
    } else {
        throw new Error("[HEARTBEAT-SY] Could not find expected clip!");
    }
};

heartbeats.prototype.syncMelted = function() {
    console.log("[HEARTBEAT-SY] Start Sync");
    var self = this;
    self.server.getServerStatus().then(function(meltedStatus) {
//        var deferred = Q.defer();
        if (meltedStatus.status !== "playing") {
//            self.handleError(new Error("[HEARTBEAT-SY] Melted is not playing!"));
//            deferred.reject(new Error("[HEARTBEAT-SY] Melted is not playing!"));
            throw new Error("[HEARTBEAT-SY] Melted is not playing!");
        } else {
//            try {
                var expected = self.getExpectedMedia();
                var meltedClip = meltedStatus.clip;
                if (expected.media.get("id").toString() !== meltedClip.id.toString()) {
                    var index = self.melted_medias.indexOf(expected.media);
                    var frames = 9999;
                    var mediaAbove = self.melted_medias.at(index - 1);
                    if (mediaAbove.get("id").toString() === meltedClip.id.toString()) {
                        frames = meltedClip.length - meltedClip.currentFrame + expected.frame;
                    } else {
                        var mediaBelow = self.melted_medias.at(index + 1);
                        if (mediaBelow.get("id").toString() === meltedClip.id.toString()) {
                            frames = meltedClip.currentFrame + (expected.media.length - expected.frame);
                        }
                    }
                    if (frames > expected.media.fps) {
//                        self.fixMelted(expected);
//                        deferred.resolve(self.fixMelted(expected));
                        return self.fixMelted(expected);
                    } else {
//                        deferred.resolve(self.sendStatus());
                        self.sendStatus();
                    }
                } else if (Math.abs(meltedClip.currentFrame - expected.frame) > expected.media.fps) {
//                    self.fixMelted(expected);
//                    deferred.resolve(self.fixMelted(expected));
                    return self.fixMelted(expected);
                } else {
//                    self.sendStatus();
//                    deferred.resolve(self.sendStatus());
                    self.sendStatus();
                }
//            } catch(err) {
////                self.handleError(err);
//                deferred.reject(err);
//            }
        }
//        return deferred.promise;
    }).fail(self.handleError).fin(function() {
        self.scheduleSync();
        self.melted_medias.leave();
    });
    console.log("[HEARTBEAT-SY] Finish Sync");
};

heartbeats.prototype.fixMelted = function(expected) {
    console.error("[HEARTBEAT-SY] Melted is out of sync!");
    self.emit("OutOfSync", expected);
    return self.server.goto(expected.media.actual_order, expected.frame).then(self.sendStatus).fail(self.handleError);
};

heartbeats.prototype.handleError =  function(error) {
    console.error(error);
    self.emit("Error", error);
    //FORCING LIST TO SYNC, SHOULD CHECK MELTED PLAYLIST, FIX IT AND START PLAYING
    self.melted_medias.save();
};

exports = module.exports = function(customConfig) {
    var hb = new heartbeats(customConfig);
    return hb;
};<commit_msg>Modified forceCheckout event to send a window {from: to:}<commit_after>var config           = require('mbc-common').config.Mosto.HeartBeats,
    util             = require('util'),
    events           = require('events'),
    Mosto            = require('./models/Mosto'),
    moment           = require('moment'),
    mvcp_server      = require('./drivers/mvcp/mvcp-driver'),
    utils            = require('./utils');

function heartbeats(customConfig) {
    //THIS MODULE ASSUMES MELTED ALWAYS HAS THE SAME CLIPS AS MELTED_MEDIAS
    //AND THAT THEY ARE IN THE SAME ORDER
    defaults = {
        gc_interval: 1000 * 60 * 60,
        sync_interval: 50,
        min_scheduled: 1000 * 60 * 60 * 4,
        mvcp_server: "melted"
    };
    this.config = customConfig || config || defaults;

    this.melted_medias = Mosto.Playlists.get('melted_medias');

    this.current_media = false;

    this.stop_timers = false;

    this.server = new mvcp_server(this.config.mvcp_server);

    events.EventEmitter.call(this);
}

util.inherits(heartbeats, events.EventEmitter);
heartbeats.prototype.startMvcpServer = function(callback) {
    var self = this;
    var result = self.server.initServer();
    result.then(function() {
        console.log("[HEARTBEAT-MVCP] MVCP server started");
        if (callback !== undefined) {
            callback();
        }
    }, function(err) {
        var e = new Error("[HEARTBEAT-MVCP] Error starting MVCP server: " + err + ".\nRetrying in 2 seconds...");
        console.error(e);
        setTimeout(function() {
            self.startMvcpServer(callback);
        }, 2000);
    });
};

heartbeats.prototype.init = function() {
    this.startMvcpServer(this.initTimers);
};

heartbeats.prototype.initTimers = function() {
    var self = this;
    self.scheduleGc();
    self.scheduleSync();
    self.scheduleCheckout();
};

heartbeats.prototype.scheduleGc = function() {
    var self = this;
    if (!self.stop_timers) {
        setTimeout(function() {
            self.executeGc();
        }, self.config.gc_interval);
    }
};

heartbeats.prototype.scheduleSync = function() {
    var self = this;
    if (!self.stop_timers) {
        setTimeout(function() {
            self.melted_medias.take(self.syncMelted.bind(self));
        }, self.config.sync_interval);
    }
};

heartbeats.prototype.scheduleCheckout = function() {
    var self = this;
    if (!self.stop_timers) {
        setTimeout(function() {
            self.checkSchedules();
        }, self.config.min_scheduled / 4);
    }
};

heartbeats.prototype.stop = function() {
    this.stop_timers = true;
};

heartbeats.prototype.checkSchedules =  function() {
    var self = this;
    var last = self.melted_medias.at(self.melted_medias.length - 1);
    var scheduled =  last.get('end') - moment();
    if (scheduled < self.config.min_scheduled)
        self.emit("forceCheckout", {from: last.get('end'), to: last.get('end') + scheduled});
};

heartbeats.prototype.executeGc = function() {
    var self = this;
    console.log("[HEARTBEAT-GC] Started Garbage Collector");
    var timeLimit = moment().subtract('hours', 1);
    var oldMedias = self.melted_medias.filter(function(media) {
        return moment(media.get('end')) < timeLimit;
    });
    if (oldMedias) {
        oldMedias.forEach(function(media) {
            self.melted_medias.remove(media);
        });
        self.melted_medias.save();
    }
    self.scheduleGc();
    console.log("[HEARTBEAT-GC] Finished Garbage Collector: " + oldMedias.length + " removed.");
};

heartbeats.prototype.sendStatus = function() {
    var self = this;
    console.log("[HEARTBEAT-FS] Started Status");
//    try {
        var expected = getExpectedMedia();
        if (!self.current_media)
            self.current_media = expected.media;
        if (expected.media.get("id").toString() !== self.current_media.get("id").toString()) {
            self.emit("clipStatus", {old_media: self.current_media, new_media: expected.media, frame: expected.frame});
            self.current_media = expected.media;
        } else {
            self.emit("frameStatus", {media: expected.media, frame: expected.frame});
        }
//    } catch(error) {
//        self.handleError(error);
//    }
    console.log("[HEARTBEAT-FS] Finished Status");
};

heartbeats.prototype.getExpectedMedia = function() {
    var self = this;
    var now = moment();
    var expected = {};
    var media = self.melted_medias.find(function(media) {
        return moment(media.end) >= now;
    });
    if (media) {
        var elapsed = now - moment(media.start);
        var frame = elapsed / media.fps;
        expected.media = media;
        expected.frame = frame;
        return expected;
    } else {
        throw new Error("[HEARTBEAT-SY] Could not find expected clip!");
    }
};

heartbeats.prototype.syncMelted = function() {
    console.log("[HEARTBEAT-SY] Start Sync");
    var self = this;
    self.server.getServerStatus().then(function(meltedStatus) {
//        var deferred = Q.defer();
        if (meltedStatus.status !== "playing") {
//            self.handleError(new Error("[HEARTBEAT-SY] Melted is not playing!"));
//            deferred.reject(new Error("[HEARTBEAT-SY] Melted is not playing!"));
            throw new Error("[HEARTBEAT-SY] Melted is not playing!");
        } else {
//            try {
                var expected = self.getExpectedMedia();
                var meltedClip = meltedStatus.clip;
                if (expected.media.get("id").toString() !== meltedClip.id.toString()) {
                    var index = self.melted_medias.indexOf(expected.media);
                    var frames = 9999;
                    var mediaAbove = self.melted_medias.at(index - 1);
                    if (mediaAbove.get("id").toString() === meltedClip.id.toString()) {
                        frames = meltedClip.length - meltedClip.currentFrame + expected.frame;
                    } else {
                        var mediaBelow = self.melted_medias.at(index + 1);
                        if (mediaBelow.get("id").toString() === meltedClip.id.toString()) {
                            frames = meltedClip.currentFrame + (expected.media.length - expected.frame);
                        }
                    }
                    if (frames > expected.media.fps) {
//                        self.fixMelted(expected);
//                        deferred.resolve(self.fixMelted(expected));
                        return self.fixMelted(expected);
                    } else {
//                        deferred.resolve(self.sendStatus());
                        self.sendStatus();
                    }
                } else if (Math.abs(meltedClip.currentFrame - expected.frame) > expected.media.fps) {
//                    self.fixMelted(expected);
//                    deferred.resolve(self.fixMelted(expected));
                    return self.fixMelted(expected);
                } else {
//                    self.sendStatus();
//                    deferred.resolve(self.sendStatus());
                    self.sendStatus();
                }
//            } catch(err) {
////                self.handleError(err);
//                deferred.reject(err);
//            }
        }
//        return deferred.promise;
    }).fail(self.handleError).fin(function() {
        self.scheduleSync();
        self.melted_medias.leave();
    });
    console.log("[HEARTBEAT-SY] Finish Sync");
};

heartbeats.prototype.fixMelted = function(expected) {
    console.error("[HEARTBEAT-SY] Melted is out of sync!");
    self.emit("OutOfSync", expected);
    return self.server.goto(expected.media.actual_order, expected.frame).then(self.sendStatus).fail(self.handleError);
};

heartbeats.prototype.handleError =  function(error) {
    console.error(error);
    self.emit("Error", error);
    //FORCING LIST TO SYNC, SHOULD CHECK MELTED PLAYLIST, FIX IT AND START PLAYING
    self.melted_medias.save();
};

exports = module.exports = function(customConfig) {
    var hb = new heartbeats(customConfig);
    return hb;
};<|endoftext|>


<commit_before><commit_msg>Detected released workspace plugins<commit_after><|endoftext|>


<commit_before>//http://www.html5rocks.com/en/tutorials/doodles/gamepad/gamepad-tester/tester.html
//useful for testing the buttons and finding the numbers
//
//Requires the https://github.com/kallaspriit/HTML5-JavaScript-Gamepad-Controller-Library
//library.
var GamePad = function() {
  var gamepad = new Gamepad();
  var gp = {};

    var padStatus = {
        position: {
            throttle: 0,
            yaw: 0,
            lift: 0
        },
        tilt: 0,
        light: 0
    }

    var isSupported = function () {
    };

    var servoTiltHandler = function(value){};
    var brightnessHandler = function(value){};
    var detectionHandler = function(value){};
    var ignoreInputUntil = 0;

    //These must be bound to by the code that instantiates the gamepad.
    gp.bindServoTilt = function(callback){
        servoTiltHandler=callback;
    };
    gp.bindBrightness = function(callback){
        brightnessHandler=callback;
    };
    gp.bindDetectionEvent = function(callback){
        detectionHandler=callback;
    };

    gp.getPositions = function() {
        window.requestAnimationFrame(updateStatus);
        return padStatus.position;
    }

    var powerLevel = .4;
    var advancePowerLevels = function(){
        powerLevel+= .2;
        if (powerLevel > 1) powerLevel = .2;

    };

    var ttrim = 0;
    var ltrim = 0;
    gamepad.bind(Gamepad.Event.BUTTON_DOWN, function(e) {
      switch (e.control) {
        case 'DPAD_UP':
          brightnessHandler(1);
          break;
        case 'DPAD_DOWN':
          brightnessHandler(-1);
          break;
        case 'Y':
          servoTiltHandler(1);
          break;
        case 'B':
          servoTiltHandler(0);
          break;
        case 'A':
          servoTiltHandler(-1);
          break;
        case 'RB':
          if (ttrim != 0 || ltrim != 0) {
            ttrim = 0;
            ltrim = 0;
          } else {
            ttrim = padStatus.position.throttle;
            ltrim = padStatus.position.lift;
          }
          break;
        case 'START':
          advancePowerLevels();
      };
    });

  gamepad.bind(Gamepad.Event.AXIS_CHANGED, function(e) {
    if ((new Date().getTime()) < ignoreInputUntil) return; //avoids inacurrate readings when the gamepad has just been connected from affecting the ROV

    switch (e.axis) {
      case 'LEFT_STICK_X':
        padStatus.position.yaw = e.value*powerLevel;
        break;
      case 'LEFT_STICK_Y':
        if (e.value == 0) {
          padStatus.position.throttle = ttrim
        } else {
          padStatus.position.throttle = -1*e.value*powerLevel;
        }
        break;
      case 'RIGHT_STICK_Y':
        if (e.value == 0) {
          padStatus.position.lift = ltrim
        } else {
          padStatus.position.lift = -1*e.value*powerLevel;
        }
        break;
    }

  });

  var updateStatus = function() {
     window.requestAnimationFrame(updateStatus);
  }

  gamepad.bind(Gamepad.Event.CONNECTED, function(device) {
    ignoreInputUntil = new Date().getTime() + 1000;
    console.log('Controller connected', device);
    detectionHandler();
  });

  gamepad.bind(Gamepad.Event.DISCONNECTED, function(device) {
    console.log('Controller disconnected', device);
    detectionHandler();
  });

  gamepad.bind(Gamepad.Event.UNSUPPORTED, function(device) {
    console.log('Unsupported controller connected', device);
  });

  gp.isAvailable = function() {
    if(gamepad.count() == 0) return false;
      return true;
  }

  if (!gamepad.init()) {
          console.log('Your browser does not support gamepads, get the latest Google Chrome or Firefox.');
  }

  return gp;
}<commit_msg>Fixed the trim button so it stops the motors as expected if the controls are neutral.<commit_after>//http://www.html5rocks.com/en/tutorials/doodles/gamepad/gamepad-tester/tester.html
//useful for testing the buttons and finding the numbers
//
//Requires the https://github.com/kallaspriit/HTML5-JavaScript-Gamepad-Controller-Library
//library.
var GamePad = function() {
  var gamepad = new Gamepad();
  var gp = {};

    var padStatus = {
        position: {
            throttle: 0,
            yaw: 0,
            lift: 0
        },
        tilt: 0,
        light: 0
    }

    var isSupported = function () {
    };

    var servoTiltHandler = function(value){};
    var brightnessHandler = function(value){};
    var detectionHandler = function(value){};
    var ignoreInputUntil = 0;

    //These must be bound to by the code that instantiates the gamepad.
    gp.bindServoTilt = function(callback){
        servoTiltHandler=callback;
    };
    gp.bindBrightness = function(callback){
        brightnessHandler=callback;
    };
    gp.bindDetectionEvent = function(callback){
        detectionHandler=callback;
    };

    gp.getPositions = function() {
        window.requestAnimationFrame(updateStatus);
        return padStatus.position;
    }

    var powerLevel = .4;
    var advancePowerLevels = function(){
        powerLevel+= .2;
        if (powerLevel > 1) powerLevel = .2;

    };

    var ttrim = 0;
    var ltrim = 0;
    gamepad.bind(Gamepad.Event.BUTTON_DOWN, function(e) {
      switch (e.control) {
        case 'DPAD_UP':
          brightnessHandler(1);
          break;
        case 'DPAD_DOWN':
          brightnessHandler(-1);
          break;
        case 'Y':
          servoTiltHandler(1);
          break;
        case 'B':
          servoTiltHandler(0);
          break;
        case 'A':
          servoTiltHandler(-1);
          break;
        case 'RB':
          if (ttrim != 0 || ltrim != 0) {
            ttrim = 0;
            ltrim = 0;
            padStatus.position.throttle = 0;
            padStatus.position.lift = 0;
          } else {
            ttrim = padStatus.position.throttle;
            ltrim = padStatus.position.lift;
          }
          break;
        case 'START':
          advancePowerLevels();
      };
    });

  gamepad.bind(Gamepad.Event.AXIS_CHANGED, function(e) {
    if ((new Date().getTime()) < ignoreInputUntil) return; //avoids inacurrate readings when the gamepad has just been connected from affecting the ROV

    switch (e.axis) {
      case 'LEFT_STICK_X':
        padStatus.position.yaw = e.value*powerLevel;
        break;
      case 'LEFT_STICK_Y':
        if (e.value == 0) {
          padStatus.position.throttle = ttrim
        } else {
          padStatus.position.throttle = -1*e.value*powerLevel;
        }
        break;
      case 'RIGHT_STICK_Y':
        if (e.value == 0) {
          padStatus.position.lift = ltrim
        } else {
          padStatus.position.lift = -1*e.value*powerLevel;
        }
        break;
    }

  });

  var updateStatus = function() {
     window.requestAnimationFrame(updateStatus);
  }

  gamepad.bind(Gamepad.Event.CONNECTED, function(device) {
    ignoreInputUntil = new Date().getTime() + 1000;
    console.log('Controller connected', device);
    detectionHandler();
  });

  gamepad.bind(Gamepad.Event.DISCONNECTED, function(device) {
    console.log('Controller disconnected', device);
    detectionHandler();
  });

  gamepad.bind(Gamepad.Event.UNSUPPORTED, function(device) {
    console.log('Unsupported controller connected', device);
  });

  gp.isAvailable = function() {
    if(gamepad.count() == 0) return false;
      return true;
  }

  if (!gamepad.init()) {
          console.log('Your browser does not support gamepads, get the latest Google Chrome or Firefox.');
  }

  return gp;
}<|endoftext|>


<commit_before>/*
Copyright 2017 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Promise from 'bluebird';

const BULK_REQUEST_DEBOUNCE_MS = 200;

// Does the server support groups? Assume yes until we receive M_UNRECOGNIZED.
// If true, flair can function and we should keep sending requests for groups and avatars.
let groupSupport = true;

const USER_GROUPS_CACHE_BUST_MS = 1800000; // 30 mins
const GROUP_PROFILES_CACHE_BUST_MS = 1800000; // 30 mins

/**
 * Stores data used by <Flair/>
 */
class FlairStore {
    constructor(matrixClient) {
        this._matrixClient = matrixClient;
        this._userGroups = {
            // $userId: ['+group1:domain', '+group2:domain', ...]
        };
        this._groupProfiles = {
            //  $groupId: {
            //      avatar_url: 'mxc://...'
            //  }
        };
        this._usersPending = {
            //  $userId: {
            //      prom: Promise
            //      resolve: () => {}
            //      reject: () => {}
            //  }
        };
        this._usersInFlight = {
            // This has the same schema as _usersPending
        };

        this._debounceTimeoutID = null;
    }

    groupSupport() {
        return groupSupport;
    }

    invalidatePublicisedGroups(userId) {
        delete this._userGroups[userId];
    }

    getPublicisedGroupsCached(matrixClient, userId) {
        if (this._userGroups[userId]) {
            return Promise.resolve(this._userGroups[userId]);
        }

        // Bulk lookup ongoing, return promise to resolve/reject
        if (this._usersPending[userId]) {
            return this._usersPending[userId].prom;
        }
        // User has been moved from pending to in-flight
        if (this._usersInFlight[userId]) {
            return this._usersInFlight[userId].prom;
        }

        this._usersPending[userId] = {};
        this._usersPending[userId].prom = new Promise((resolve, reject) => {
            this._usersPending[userId].resolve = resolve;
            this._usersPending[userId].reject = reject;
        }).then((groups) => {
            this._userGroups[userId] = groups;
            setTimeout(() => {
                delete this._userGroups[userId];
            }, USER_GROUPS_CACHE_BUST_MS);
            return this._userGroups[userId];
        }).catch((err) => {
            // Indicate whether the homeserver supports groups
            if (err.errcode === 'M_UNRECOGNIZED') {
                console.warn('Cannot display flair, server does not support groups');
                groupSupport = false;
                // Return silently to avoid spamming for non-supporting servers
                return;
            }
            console.error('Could not get groups for user', userId, err);
            throw err;
        }).finally(() => {
            delete this._usersInFlight[userId];
        });

        // This debounce will allow consecutive requests for the public groups of users that
        // are sent in intervals of < BULK_REQUEST_DEBOUNCE_MS to be batched and only requested
        // when no more requests are received within the next BULK_REQUEST_DEBOUNCE_MS. The naive
        // implementation would do a request that only requested the groups for `userId`, leading
        // to a worst and best case of 1 user per request. This implementation's worst is still
        // 1 user per request but only if the requests are > BULK_REQUEST_DEBOUNCE_MS apart and the
        // best case is N users per request.
        //
        // This is to reduce the number of requests made whilst trading off latency when viewing
        // a Flair component.
        if (this._debounceTimeoutID) clearTimeout(this._debounceTimeoutID);
        this._debounceTimeoutID = setTimeout(() => {
            this._batchedGetPublicGroups(matrixClient);
        }, BULK_REQUEST_DEBOUNCE_MS);

        return this._usersPending[userId].prom;
    }

    async _batchedGetPublicGroups(matrixClient) {
        // Move users pending to users in flight
        this._usersInFlight = this._usersPending;
        this._usersPending = {};

        let resp = {
            users: [],
        };
        try {
            resp = await matrixClient.getPublicisedGroups(Object.keys(this._usersInFlight));
        } catch (err) {
            // Propagate the same error to all usersInFlight
            Object.keys(this._usersInFlight).forEach((userId) => {
                // The promise should always exist for userId, but do a null-check anyway
                if (!this._usersInFlight[userId]) return;
                this._usersInFlight[userId].reject(err);
            });
            return;
        }
        const updatedUserGroups = resp.users;
        Object.keys(this._usersInFlight).forEach((userId) => {
            // The promise should always exist for userId, but do a null-check anyway
            if (!this._usersInFlight[userId]) return;
            this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []);
        });
    }

    async getGroupProfileCached(matrixClient, groupId) {
        if (this._groupProfiles[groupId]) {
            return this._groupProfiles[groupId];
        }

        const profile = await matrixClient.getGroupProfile(groupId);
        this._groupProfiles[groupId] = {
            groupId,
            avatarUrl: profile.avatar_url,
            name: profile.name,
            shortDescription: profile.short_description,
        };
        setTimeout(() => {
            delete this._groupProfiles[groupId];
        }, GROUP_PROFILES_CACHE_BUST_MS);

        return this._groupProfiles[groupId];
    }
}

if (global.singletonFlairStore === undefined) {
    global.singletonFlairStore = new FlairStore();
}
export default global.singletonFlairStore;
<commit_msg>Dedupe requests to fetch group profile data<commit_after>/*
Copyright 2017 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Promise from 'bluebird';

const BULK_REQUEST_DEBOUNCE_MS = 200;

// Does the server support groups? Assume yes until we receive M_UNRECOGNIZED.
// If true, flair can function and we should keep sending requests for groups and avatars.
let groupSupport = true;

const USER_GROUPS_CACHE_BUST_MS = 1800000; // 30 mins
const GROUP_PROFILES_CACHE_BUST_MS = 1800000; // 30 mins

/**
 * Stores data used by <Flair/>
 */
class FlairStore {
    constructor(matrixClient) {
        this._matrixClient = matrixClient;
        this._userGroups = {
            // $userId: ['+group1:domain', '+group2:domain', ...]
        };
        this._groupProfiles = {
            //  $groupId: {
            //      avatar_url: 'mxc://...'
            //  }
        };
        this._groupProfilesPromise = {
            //  $groupId: Promise
        };
        this._usersPending = {
            //  $userId: {
            //      prom: Promise
            //      resolve: () => {}
            //      reject: () => {}
            //  }
        };
        this._usersInFlight = {
            // This has the same schema as _usersPending
        };

        this._debounceTimeoutID = null;
    }

    groupSupport() {
        return groupSupport;
    }

    invalidatePublicisedGroups(userId) {
        delete this._userGroups[userId];
    }

    getPublicisedGroupsCached(matrixClient, userId) {
        if (this._userGroups[userId]) {
            return Promise.resolve(this._userGroups[userId]);
        }

        // Bulk lookup ongoing, return promise to resolve/reject
        if (this._usersPending[userId]) {
            return this._usersPending[userId].prom;
        }
        // User has been moved from pending to in-flight
        if (this._usersInFlight[userId]) {
            return this._usersInFlight[userId].prom;
        }

        this._usersPending[userId] = {};
        this._usersPending[userId].prom = new Promise((resolve, reject) => {
            this._usersPending[userId].resolve = resolve;
            this._usersPending[userId].reject = reject;
        }).then((groups) => {
            this._userGroups[userId] = groups;
            setTimeout(() => {
                delete this._userGroups[userId];
            }, USER_GROUPS_CACHE_BUST_MS);
            return this._userGroups[userId];
        }).catch((err) => {
            // Indicate whether the homeserver supports groups
            if (err.errcode === 'M_UNRECOGNIZED') {
                console.warn('Cannot display flair, server does not support groups');
                groupSupport = false;
                // Return silently to avoid spamming for non-supporting servers
                return;
            }
            console.error('Could not get groups for user', userId, err);
            throw err;
        }).finally(() => {
            delete this._usersInFlight[userId];
        });

        // This debounce will allow consecutive requests for the public groups of users that
        // are sent in intervals of < BULK_REQUEST_DEBOUNCE_MS to be batched and only requested
        // when no more requests are received within the next BULK_REQUEST_DEBOUNCE_MS. The naive
        // implementation would do a request that only requested the groups for `userId`, leading
        // to a worst and best case of 1 user per request. This implementation's worst is still
        // 1 user per request but only if the requests are > BULK_REQUEST_DEBOUNCE_MS apart and the
        // best case is N users per request.
        //
        // This is to reduce the number of requests made whilst trading off latency when viewing
        // a Flair component.
        if (this._debounceTimeoutID) clearTimeout(this._debounceTimeoutID);
        this._debounceTimeoutID = setTimeout(() => {
            this._batchedGetPublicGroups(matrixClient);
        }, BULK_REQUEST_DEBOUNCE_MS);

        return this._usersPending[userId].prom;
    }

    async _batchedGetPublicGroups(matrixClient) {
        // Move users pending to users in flight
        this._usersInFlight = this._usersPending;
        this._usersPending = {};

        let resp = {
            users: [],
        };
        try {
            resp = await matrixClient.getPublicisedGroups(Object.keys(this._usersInFlight));
        } catch (err) {
            // Propagate the same error to all usersInFlight
            Object.keys(this._usersInFlight).forEach((userId) => {
                // The promise should always exist for userId, but do a null-check anyway
                if (!this._usersInFlight[userId]) return;
                this._usersInFlight[userId].reject(err);
            });
            return;
        }
        const updatedUserGroups = resp.users;
        Object.keys(this._usersInFlight).forEach((userId) => {
            // The promise should always exist for userId, but do a null-check anyway
            if (!this._usersInFlight[userId]) return;
            this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []);
        });
    }

    async getGroupProfileCached(matrixClient, groupId) {
        if (this._groupProfiles[groupId]) {
            return this._groupProfiles[groupId];
        }

        // No request yet, start one
        if (!this._groupProfilesPromise[groupId]) {
            this._groupProfilesPromise[groupId] = matrixClient.getGroupProfile(groupId);
        }

        const profile = await this._groupProfilesPromise[groupId];
        this._groupProfiles[groupId] = {
            groupId,
            avatarUrl: profile.avatar_url,
            name: profile.name,
            shortDescription: profile.short_description,
        };

        setTimeout(() => {
            delete this._groupProfiles[groupId];
            delete this._groupProfilesPromise[groupId];
        }, GROUP_PROFILES_CACHE_BUST_MS);

        return this._groupProfiles[groupId];
    }
}

if (global.singletonFlairStore === undefined) {
    global.singletonFlairStore = new FlairStore();
}
export default global.singletonFlairStore;
<|endoftext|>


<commit_before>import { observable, action, computed } from 'mobx';
import { validateName, validateTicker } from '../utils/utils'
import { VALIDATION_TYPES } from '../utils/constants'
import autosave from './autosave'
const { EMPTY, VALID, INVALID } = VALIDATION_TYPES;

class TokenStore {

  @observable name;
  @observable ticker;
  @observable supply;
  @observable decimals;
  @observable validToken;
  @observable reservedTokensInput;
  @observable reservedTokens;

  constructor() {
    this.reset()
    autosave(this, 'TokenStore')
  }

  @action reset = () => {
    this.name = undefined
    this.ticker = undefined
    this.supply = 0
    this.decimals = undefined
    this.validToken = {
      'name': EMPTY,
      'ticker': EMPTY,
      'decimals': EMPTY
    }
    this.reservedTokensInput = {}
    this.reservedTokens = []
  }

  @action setProperty = (property, value) => {
    this[property] = value;
  }

  @action validateTokens = (property) => {
    if (property === 'name') {
      this.validToken[property] = validateName(this.name) ? VALID : INVALID;
    } else if (property === 'ticker') {
      this.validToken[property] = validateTicker(this.ticker) ? VALID : INVALID;
    }
  }

  @action updateValidity = (property, validity) => {
    this.validToken[property] = validity
  }

  @action invalidateToken = () => {
    if (!this.validToken) {
      return;
    }

    Object.keys(this.validToken).forEach(key => {
      if (this.validToken[key] === EMPTY) {
        this.validToken[key] = INVALID;
      }
    });
  }

  // Getters
  @computed get isTokenValid() {
    if (!this.validToken) {
      return;
    }

    const validKeys = Object.keys(this.validToken).filter((key) => {
      if (this.validToken[key] === VALID) {
        return true;
      } else {
        return false;
      }
    });
    return validKeys.length === Object.keys(this.validToken).length
  }
}

export default TokenStore;
<commit_msg>Remove unused action from TokenStore<commit_after>import { observable, action, computed } from 'mobx';
import { VALIDATION_TYPES } from '../utils/constants'
import autosave from './autosave'
const { EMPTY, VALID, INVALID } = VALIDATION_TYPES;

class TokenStore {

  @observable name;
  @observable ticker;
  @observable supply;
  @observable decimals;
  @observable validToken;
  @observable reservedTokensInput;
  @observable reservedTokens;

  constructor() {
    this.reset()
    autosave(this, 'TokenStore')
  }

  @action reset = () => {
    this.name = undefined
    this.ticker = undefined
    this.supply = 0
    this.decimals = undefined
    this.validToken = {
      'name': EMPTY,
      'ticker': EMPTY,
      'decimals': EMPTY
    }
    this.reservedTokensInput = {}
    this.reservedTokens = []
  }

  @action setProperty = (property, value) => {
    this[property] = value;
  }

  @action updateValidity = (property, validity) => {
    this.validToken[property] = validity
  }

  @action invalidateToken = () => {
    if (!this.validToken) {
      return;
    }

    Object.keys(this.validToken).forEach(key => {
      if (this.validToken[key] === EMPTY) {
        this.validToken[key] = INVALID;
      }
    });
  }

  // Getters
  @computed get isTokenValid() {
    if (!this.validToken) {
      return;
    }

    const validKeys = Object.keys(this.validToken).filter((key) => {
      if (this.validToken[key] === VALID) {
        return true;
      } else {
        return false;
      }
    });
    return validKeys.length === Object.keys(this.validToken).length
  }
}

export default TokenStore;
<|endoftext|>


<commit_before>/**
 * @file CSV Parser
 * @since 0.2.3
 */
/*#ifndef(UMD)*/
"use strict";
/*global _GPF_START*/ // 0
/*global _GpfStreamBufferedRead*/ // gpf.stream.BufferedRead
/*global _gpfArrayForEach*/ // Almost like [].forEach (undefined are also enumerated)
/*global _gpfArrayForEachFalsy*/ // _gpfArrayForEach that returns first truthy value computed by the callback
/*global _gpfAssert*/ // Assertion method
/*global _gpfDefine*/ // Shortcut for gpf.define
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*global _gpfIgnore*/ // Helper to remove unused parameter warning
/*global _gpfStreamSecureWrite*/ // Generates a wrapper to secure multiple calls to stream#write
/*global _gpfStringEscapeFor*/ // Make the string content compatible with lang
/*global _gpfStringReplaceEx*/ // String replacement using dictionary map
/*exported _GpfStreamCsvParser*/ // gpf.stream.csv.Parser
/*#endif*/

_gpfErrorDeclare("csv", {
    invalidCSV:
        "Invalid CSV syntax (bad quote sequence or missing end of file)"
});

/**
 * @typedef gpf.typedef.csvParserOptions
 * @property {String} [header] Header line: if not specified, the first write of the input stream becomes the header
 * @property {String} [separator] Column separator, detected from the header line if not specified (allowed characters
 * are ";" "," and "\t")
 * @property {String} [quote="\""] Quote sign: introduces an escaped value in which quotes, separator and carriage
 * returns are allowed. Consequently, the value may stand on several lines
 * @property {String} [newLine="\n"] New line: each input stream write is considered as a separate line.
 * If a quoted value stands on several lines, this character is used to represents every new line
 * @since 0.2.3
 */

/**
 * @namespace gpf.stream.csv
 * @description Root namespace for CSV related streams
 * @since 0.2.3
 */
gpf.stream.csv = {};

var
    // Usual CSV separators
    _gpfCsvSeparators = ";,\t ".split("");

var
    _GpfStreamCsvParser = _gpfDefine({
        $class: "gpf.stream.csv.Parser",
        $extend: _GpfStreamBufferedRead,

        /**
         * CSV Parser
         *
         * Parses the incoming stream by considering each write as a separate line.
         * It is recommended to use the {@link gpf.stream.LineAdapter} class in between the incoming stream and the CSV
         * parser.
         *
         * Generates objects where properties are matching header columns and values are string extracted from record
         * lines.
         *
         * @param {gpf.typedef.csvParserOptions} [parserOptions] Parser options
         * @constructor gpf.stream.csv.Parser
         * @implements {gpf.interfaces.IReadableStream}
         * @implements {gpf.interfaces.IWritableStream}
         * @implements {gpf.interfaces.IFlushableStream}
         * @extends gpf.stream.BufferedRead
         * @since 0.2.3
         */
        constructor: function (parserOptions) {
            this._readParserOptions(parserOptions);
            if (this._header) {
                this._parseHeader();
            } else {
                this._write = this._writeHeader;
            }
        },

        //region Parser options

        /**
         * Read parser options
         *
         * @param {gpf.typedef.csvParserOptions} [parserOptions] Parser options
         * @since 0.2.3
         */
        _readParserOptions: function (parserOptions) {
            var me = this;
            if (parserOptions) {
                _gpfArrayForEach([
                    "header",
                    "separator",
                    "quote",
                    "newLine"
                ], function (optionName) {
                    if (parserOptions[optionName]) {
                        me["_" + optionName] = parserOptions[optionName];
                    }
                });
            }
        },

        /**
         * Header line
         *
         * @type {String}
         * @since 0.2.3
         */
        _header: "",

        /**
         * Column separator
         *
         * @type {String}
         * @since 0.2.3
         */
        _separator: "",

        /**
         * Deduce separator from header line
         * @since 0.2.3
         */
        _deduceSeparator: function () {
            var header = this._header;
            this._separator = _gpfArrayForEachFalsy(_gpfCsvSeparators, function (separator) {
                if (header.includes(separator)) {
                    return separator;
                }
            }) || _gpfCsvSeparators[_GPF_START];
        },

        /**
         * Quote sign
         *
         * @type {String}
         * @since 0.2.3
         */
        _quote: "\"",

        /**
         * New line
         *
         * @type {String}
         * @since 0.2.3
         */
        _newLine: "\n",

        //endregion

        //region Header processing

        /**
         * @property {String[]} Columns' name
         * @since 0.2.3
         */
        _columns: [],

        _buildParsingHelpers: function () {
            this._unescapeDictionary = {};
            this._unescapeDictionary[this._quote + this._quote] = this._quote;
            this._parser = new RegExp(_gpfStringReplaceEx("^(?:([^QS][^S]*)|Q((?:[^Q]|QQ)+)Q)(?=$|S)", {
                Q: _gpfStringEscapeFor(this._quote, "regexp"),
                S: _gpfStringEscapeFor(this._separator, "regexp")
            }));
        },

        /**
         * Once header line is known, process it to prepare the parser
         * @since 0.2.3
         */
        _parseHeader: function () {
            if (!this._separator) {
                this._deduceSeparator();
            }
            this._columns = this._header.split(this._separator);
            this._buildParsingHelpers();
            this._write = this._writeContent;
        },

        /**
         * Write header line
         *
         * @param {String} line CSV line
         * @since 0.2.3
         */
        _writeHeader: function (line) {
            this._header = line;
            this._parseHeader();
        },

        //endregion

        //region Content processing

        /**
         * Values being built
         * @since 0.2.3
         */
        _values: [],

        /**
         * Content to parse
         * @since 0.2.3
         */
        _content: "",

        /**
         * Unescape quoted value
         *
         * @param {String} value Quoted value
         * @return {String} unescaped value
         * @since 0.2.3
         */
        _unescapeQuoted: function (value) {
            return _gpfStringReplaceEx(value, this._unescapeDictionary);
        },

        /**
         * Add the matching value to the array of values
         *
         * @param {Object} match Regular expression match
         * @since 0.2.3
         */
        _addValue: function (match) {
            var UNQUOTED = 1,
                QUOTED = 2;
            if (match[UNQUOTED]) {
                this._values.push(match[UNQUOTED]);
            } else /* if (match[QUOTED]) */ {
                this._values.push(this._unescapeQuoted(match[QUOTED]));
            }
        },

        /**
         * Move the content to the next value
         *
         * @param {Number} index Position where the next value starts
         * @return {Boolean} True if some remaining content must be parsed
         * @since 0.2.3
         */
        _nextValue: function (index) {
            this._content = this._content.substr(index);
            return Boolean(this._content.length);
        },

        /**
         * Check what appears after the extracted value
         *
         * @param {Object} match Regular expression match
         * @return {Boolean} True if some remaining content must be parsed
         * @since 0.2.3
         */
        _checkAfterValue: function (match) {
            var lengthOfMatchedString = match[_GPF_START].length,
                charAfterValue = this._content.charAt(lengthOfMatchedString);
            if (charAfterValue) {
                _gpfAssert(charAfterValue === this._separator, "Positive lookahead works");
                return this._nextValue(++lengthOfMatchedString);
            }
            delete this._content;
            return false; // No value means end of content
        },

        /**
         * Extract value
         *
         * @return {Boolean} True if some remaining content must be parsed
         * @since 0.2.3
         */
        _extractValue: function () {
            var match = this._parser.exec(this._content);
            if (!match) {
                return false; // Stop parsing
            }
            this._addValue(match);
            return this._checkAfterValue(match);
        },

        /**
         * Check if the content starts with a separator or assume it's a value
         *
         * @return {Boolean} True if some remaining content must be parsed
         * @since 0.2.3
         */
        _checkForValue: function () {
            if (this._content.startsWith(this._separator)) {
                this._values.push(""); // Separator here means empty value
                return this._nextValue(this._separator.length);
            }
            return this._extractValue();
        },

        /**
         * Extract all values in the content
         *
         * @since 0.2.3
         */
        _parseValues: function () {
            while (this._checkForValue()) {
                _gpfIgnore(); // Not my proudest but avoid empty block warning
            }
        },

        /**
         * Parse content contained in the line (and any previously unterminated content)
         *
         * @return {String[]|undefined} Resulting values or undefined if record is not finalized yet
         * @since 0.2.3
         */
        _parseContent: function () {
            this._parseValues();
            if (this._content) {
                return;
            }
            return this._values;
        },

        /**
         * If some content remains from previous parsing, concatenate it and parse
         *
         * @param {String} line CSV line
         * @return {String[]|undefined} Resulting values or undefined if not yet finalized
         * @since 0.2.3
         */
        _processContent: function (line) {
            if (this._content) {
                this._content = this._content + this._newLine + line;
            } else {
                this._values = [];
                this._content = line;
            }
            return this._parseContent();
        },

        /**
         * Generate a record from values
         *
         * @param {String[]} values Array of values
         * @return {Object} Record based on header names
         * @since 0.2.3
         */
        _getRecord: function (values) {
            var record = {};
            _gpfArrayForEach(this._columns, function (name, idx) {
                var value = values[idx];
                if (value !== undefined) {
                    record[name] = values[idx];
                }
            });
            return record;
        },

        /**
         * Write content line
         *
         * @param {String} line CSV line
         * @since 0.2.3
         */
        _writeContent: function (line) {
            var values = this._processContent(line);
            if (values) {
                this._appendToReadBuffer(this._getRecord(values));
            }
        },

        //endregion

        //region gpf.interfaces.IReadableStream

        /**
         * @gpf:sameas gpf.interfaces.IWritableStream#write
         * @since 0.2.3
         */
        write: _gpfStreamSecureWrite(function (line) {
            var me = this; //eslint-disable-line no-invalid-this
            me._write(line);
            return Promise.resolve();
        }),

        //endregion

        //region gpf.interfaces.IFlushableStream

        /**
         * @gpf:sameas gpf.interfaces.IFlushableStream#flush
         * @since 0.2.3
         */
        flush: function () {
            if (this._content) {
                var error = new gpf.Error.InvalidCSV();
                this._setReadError(error);
                return Promise.reject(error);
            }
            this._completeReadBuffer();
            return Promise.resolve();
        }

        //endregion

    });
<commit_msg>no-substr (#293)<commit_after>/**
 * @file CSV Parser
 * @since 0.2.3
 */
/*#ifndef(UMD)*/
"use strict";
/*global _GPF_START*/ // 0
/*global _GpfStreamBufferedRead*/ // gpf.stream.BufferedRead
/*global _gpfArrayForEach*/ // Almost like [].forEach (undefined are also enumerated)
/*global _gpfArrayForEachFalsy*/ // _gpfArrayForEach that returns first truthy value computed by the callback
/*global _gpfAssert*/ // Assertion method
/*global _gpfDefine*/ // Shortcut for gpf.define
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*global _gpfIgnore*/ // Helper to remove unused parameter warning
/*global _gpfStreamSecureWrite*/ // Generates a wrapper to secure multiple calls to stream#write
/*global _gpfStringEscapeFor*/ // Make the string content compatible with lang
/*global _gpfStringReplaceEx*/ // String replacement using dictionary map
/*exported _GpfStreamCsvParser*/ // gpf.stream.csv.Parser
/*#endif*/

_gpfErrorDeclare("csv", {
    invalidCSV:
        "Invalid CSV syntax (bad quote sequence or missing end of file)"
});

/**
 * @typedef gpf.typedef.csvParserOptions
 * @property {String} [header] Header line: if not specified, the first write of the input stream becomes the header
 * @property {String} [separator] Column separator, detected from the header line if not specified (allowed characters
 * are ";" "," and "\t")
 * @property {String} [quote="\""] Quote sign: introduces an escaped value in which quotes, separator and carriage
 * returns are allowed. Consequently, the value may stand on several lines
 * @property {String} [newLine="\n"] New line: each input stream write is considered as a separate line.
 * If a quoted value stands on several lines, this character is used to represents every new line
 * @since 0.2.3
 */

/**
 * @namespace gpf.stream.csv
 * @description Root namespace for CSV related streams
 * @since 0.2.3
 */
gpf.stream.csv = {};

var
    // Usual CSV separators
    _gpfCsvSeparators = ";,\t ".split("");

var
    _GpfStreamCsvParser = _gpfDefine({
        $class: "gpf.stream.csv.Parser",
        $extend: _GpfStreamBufferedRead,

        /**
         * CSV Parser
         *
         * Parses the incoming stream by considering each write as a separate line.
         * It is recommended to use the {@link gpf.stream.LineAdapter} class in between the incoming stream and the CSV
         * parser.
         *
         * Generates objects where properties are matching header columns and values are string extracted from record
         * lines.
         *
         * @param {gpf.typedef.csvParserOptions} [parserOptions] Parser options
         * @constructor gpf.stream.csv.Parser
         * @implements {gpf.interfaces.IReadableStream}
         * @implements {gpf.interfaces.IWritableStream}
         * @implements {gpf.interfaces.IFlushableStream}
         * @extends gpf.stream.BufferedRead
         * @since 0.2.3
         */
        constructor: function (parserOptions) {
            this._readParserOptions(parserOptions);
            if (this._header) {
                this._parseHeader();
            } else {
                this._write = this._writeHeader;
            }
        },

        //region Parser options

        /**
         * Read parser options
         *
         * @param {gpf.typedef.csvParserOptions} [parserOptions] Parser options
         * @since 0.2.3
         */
        _readParserOptions: function (parserOptions) {
            var me = this;
            if (parserOptions) {
                _gpfArrayForEach([
                    "header",
                    "separator",
                    "quote",
                    "newLine"
                ], function (optionName) {
                    if (parserOptions[optionName]) {
                        me["_" + optionName] = parserOptions[optionName];
                    }
                });
            }
        },

        /**
         * Header line
         *
         * @type {String}
         * @since 0.2.3
         */
        _header: "",

        /**
         * Column separator
         *
         * @type {String}
         * @since 0.2.3
         */
        _separator: "",

        /**
         * Deduce separator from header line
         * @since 0.2.3
         */
        _deduceSeparator: function () {
            var header = this._header;
            this._separator = _gpfArrayForEachFalsy(_gpfCsvSeparators, function (separator) {
                if (header.includes(separator)) {
                    return separator;
                }
            }) || _gpfCsvSeparators[_GPF_START];
        },

        /**
         * Quote sign
         *
         * @type {String}
         * @since 0.2.3
         */
        _quote: "\"",

        /**
         * New line
         *
         * @type {String}
         * @since 0.2.3
         */
        _newLine: "\n",

        //endregion

        //region Header processing

        /**
         * @property {String[]} Columns' name
         * @since 0.2.3
         */
        _columns: [],

        _buildParsingHelpers: function () {
            this._unescapeDictionary = {};
            this._unescapeDictionary[this._quote + this._quote] = this._quote;
            this._parser = new RegExp(_gpfStringReplaceEx("^(?:([^QS][^S]*)|Q((?:[^Q]|QQ)+)Q)(?=$|S)", {
                Q: _gpfStringEscapeFor(this._quote, "regexp"),
                S: _gpfStringEscapeFor(this._separator, "regexp")
            }));
        },

        /**
         * Once header line is known, process it to prepare the parser
         * @since 0.2.3
         */
        _parseHeader: function () {
            if (!this._separator) {
                this._deduceSeparator();
            }
            this._columns = this._header.split(this._separator);
            this._buildParsingHelpers();
            this._write = this._writeContent;
        },

        /**
         * Write header line
         *
         * @param {String} line CSV line
         * @since 0.2.3
         */
        _writeHeader: function (line) {
            this._header = line;
            this._parseHeader();
        },

        //endregion

        //region Content processing

        /**
         * Values being built
         * @since 0.2.3
         */
        _values: [],

        /**
         * Content to parse
         * @since 0.2.3
         */
        _content: "",

        /**
         * Unescape quoted value
         *
         * @param {String} value Quoted value
         * @return {String} unescaped value
         * @since 0.2.3
         */
        _unescapeQuoted: function (value) {
            return _gpfStringReplaceEx(value, this._unescapeDictionary);
        },

        /**
         * Add the matching value to the array of values
         *
         * @param {Object} match Regular expression match
         * @since 0.2.3
         */
        _addValue: function (match) {
            var UNQUOTED = 1,
                QUOTED = 2;
            if (match[UNQUOTED]) {
                this._values.push(match[UNQUOTED]);
            } else /* if (match[QUOTED]) */ {
                this._values.push(this._unescapeQuoted(match[QUOTED]));
            }
        },

        /**
         * Move the content to the next value
         *
         * @param {Number} index Position where the next value starts
         * @return {Boolean} True if some remaining content must be parsed
         * @since 0.2.3
         */
        _nextValue: function (index) {
            this._content = this._content.substring(index);
            return Boolean(this._content.length);
        },

        /**
         * Check what appears after the extracted value
         *
         * @param {Object} match Regular expression match
         * @return {Boolean} True if some remaining content must be parsed
         * @since 0.2.3
         */
        _checkAfterValue: function (match) {
            var lengthOfMatchedString = match[_GPF_START].length,
                charAfterValue = this._content.charAt(lengthOfMatchedString);
            if (charAfterValue) {
                _gpfAssert(charAfterValue === this._separator, "Positive lookahead works");
                return this._nextValue(++lengthOfMatchedString);
            }
            delete this._content;
            return false; // No value means end of content
        },

        /**
         * Extract value
         *
         * @return {Boolean} True if some remaining content must be parsed
         * @since 0.2.3
         */
        _extractValue: function () {
            var match = this._parser.exec(this._content);
            if (!match) {
                return false; // Stop parsing
            }
            this._addValue(match);
            return this._checkAfterValue(match);
        },

        /**
         * Check if the content starts with a separator or assume it's a value
         *
         * @return {Boolean} True if some remaining content must be parsed
         * @since 0.2.3
         */
        _checkForValue: function () {
            if (this._content.startsWith(this._separator)) {
                this._values.push(""); // Separator here means empty value
                return this._nextValue(this._separator.length);
            }
            return this._extractValue();
        },

        /**
         * Extract all values in the content
         *
         * @since 0.2.3
         */
        _parseValues: function () {
            while (this._checkForValue()) {
                _gpfIgnore(); // Not my proudest but avoid empty block warning
            }
        },

        /**
         * Parse content contained in the line (and any previously unterminated content)
         *
         * @return {String[]|undefined} Resulting values or undefined if record is not finalized yet
         * @since 0.2.3
         */
        _parseContent: function () {
            this._parseValues();
            if (this._content) {
                return;
            }
            return this._values;
        },

        /**
         * If some content remains from previous parsing, concatenate it and parse
         *
         * @param {String} line CSV line
         * @return {String[]|undefined} Resulting values or undefined if not yet finalized
         * @since 0.2.3
         */
        _processContent: function (line) {
            if (this._content) {
                this._content = this._content + this._newLine + line;
            } else {
                this._values = [];
                this._content = line;
            }
            return this._parseContent();
        },

        /**
         * Generate a record from values
         *
         * @param {String[]} values Array of values
         * @return {Object} Record based on header names
         * @since 0.2.3
         */
        _getRecord: function (values) {
            var record = {};
            _gpfArrayForEach(this._columns, function (name, idx) {
                var value = values[idx];
                if (value !== undefined) {
                    record[name] = values[idx];
                }
            });
            return record;
        },

        /**
         * Write content line
         *
         * @param {String} line CSV line
         * @since 0.2.3
         */
        _writeContent: function (line) {
            var values = this._processContent(line);
            if (values) {
                this._appendToReadBuffer(this._getRecord(values));
            }
        },

        //endregion

        //region gpf.interfaces.IReadableStream

        /**
         * @gpf:sameas gpf.interfaces.IWritableStream#write
         * @since 0.2.3
         */
        write: _gpfStreamSecureWrite(function (line) {
            var me = this; //eslint-disable-line no-invalid-this
            me._write(line);
            return Promise.resolve();
        }),

        //endregion

        //region gpf.interfaces.IFlushableStream

        /**
         * @gpf:sameas gpf.interfaces.IFlushableStream#flush
         * @since 0.2.3
         */
        flush: function () {
            if (this._content) {
                var error = new gpf.Error.InvalidCSV();
                this._setReadError(error);
                return Promise.reject(error);
            }
            this._completeReadBuffer();
            return Promise.resolve();
        }

        //endregion

    });
<|endoftext|>