WME SC Emergency Management Closures

Adds a road closure layer for Sourth Carolina Emergency Management

// ==UserScript==
// @name         WME SC Emergency Management Closures
// @namespace    https://greasyfork.org/users/45389
// @version      0.5
// @description  Adds a road closure layer for Sourth Carolina Emergency Management
// @author       MapOMatic
// @include      /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @license      GNU GPLv3
// @grant        GM_xmlhttpRequest
// @connect      arcgis.com
// ==/UserScript==

(function() {
    'use strict';

    var _settingsStoreName = 'wme_sc_emergency_management_closures';
    var _alertUpdate = false;
    var _debugLevel = 0;
    var _scriptVersion = GM_info.script.version;
    var _scriptVersionChanges = [
        GM_info.script.name + '\nv' + _scriptVersion + '\n\nWhat\'s New\n------------------------------\n',
        '\n- .',
    ].join('');
    var _tabDiv = {};  // stores the user tab div so it can be restored after switching back from Events mode to Default mode
    var _mapLayer = null;
    var _styles = {};
    var _settings = {};
    var _statesHash = {
        'Alabama': 'AL',
        'Alaska': 'AK',
        'American Samoa': 'AS',
        'Arizona': 'AZ',
        'Arkansas': 'AR',
        'California': 'CA',
        'Colorado': 'CO',
        'Connecticut': 'CT',
        'Delaware': 'DE',
        'District Of Columbia': 'DC',
        'Federated States Of Micronesia': 'FM',
        'Florida': 'FL',
        'Georgia': 'GA',
        'Guam': 'GU',
        'Hawaii': 'HI',
        'Idaho': 'ID',
        'Illinois': 'IL',
        'Indiana': 'IN',
        'Iowa': 'IA',
        'Kansas': 'KS',
        'Kentucky': 'KY',
        'Louisiana': 'LA',
        'Maine': 'ME',
        'Marshall Islands': 'MH',
        'Maryland': 'MD',
        'Massachusetts': 'MA',
        'Michigan': 'MI',
        'Minnesota': 'MN',
        'Mississippi': 'MS',
        'Missouri': 'MO',
        'Montana': 'MT',
        'Nebraska': 'NE',
        'Nevada': 'NV',
        'New Hampshire': 'NH',
        'New Jersey': 'NJ',
        'New Mexico': 'NM',
        'New York': 'NY',
        'North Carolina': 'NC',
        'North Dakota': 'ND',
        'Northern Mariana Islands': 'MP',
        'Ohio': 'OH',
        'Oklahoma': 'OK',
        'Oregon': 'OR',
        'Palau': 'PW',
        'Pennsylvania': 'PA',
        'Puerto Rico': 'PR',
        'Rhode Island': 'RI',
        'South Carolina': 'SC',
        'South Dakota': 'SD',
        'Tennessee': 'TN',
        'Texas': 'TX',
        'Utah': 'UT',
        'Vermont': 'VT',
        'Virgin Islands': 'VI',
        'Virginia': 'VA',
        'Washington': 'WA',
        'West Virginia': 'WV',
        'Wisconsin': 'WI',
        'Wyoming': 'WY'
    };
    var _stateSettings = {
        SC: {
            baseUrl: '',
            mapLayers: [
                { layerID:'https://services1.arcgis.com/VaY7cY9pvUYUP1Lf/ArcGIS/rest/services/CLOSURES_06_OCT_LINES/FeatureServer/0', outFields:[''] },
                { layerID:'https://services1.arcgis.com/VaY7cY9pvUYUP1Lf/arcgis/rest/services/Hurricane_Matthew_Closures_Lines/FeatureServer/0', outFields:[''] },
            ],
            colors: {closure:'#ff0000'}
        }
    };

    function log(message, level) {
        if (message && level <= _debugLevel) {
            console.log('WME SCEM: ' + message);
        }
    }

    function loadSettingsFromStorage() {
        var settings = $.parseJSON(localStorage.getItem(_settingsStoreName));
        if(!settings) {
            settings = {
                lastVersion:null,
                layerVisible:true
            };
        } else {
            settings.layerVisible = true; //(settings.layerVisible === true);
        }
        _settings = settings;
    }

    function saveSettingsToStorage() {
        if (localStorage) {
            var settings = {
                lastVersion: _scriptVersion,
                layerVisible: _mapLayer.visibility,
            };
            localStorage.setItem(_settingsStoreName, JSON.stringify(settings));
            log('Settings saved', 1);
        }
    }

    function getLineWidth() {
        return 8 * Math.pow(1.15, (W.map.getZoom()-1));
    }

    function draw(buckets) {
        _mapLayer.removeAllFeatures();
        for(var i=buckets.length-1; i>-1; i--) {
            _mapLayer.addFeatures(buckets[i]);
        }
    }

    function processFeatures(data,context) {
        var layer = context.layer;
        var stateSettings = context.stateSettings;
        var styles = {};
        for (var prefix in stateSettings.colors) {
            var color = stateSettings.colors[prefix];
            styles[prefix] = {
                strokeColor: color,
                strokeDashstyle: "solid",
                strokeOpacity: 0.6,
                strokeWidth: getLineWidth()
            };
        }

        data.features.forEach(function(feature) {
            var style = styles.closure;
            var lineFeatures = [];
            feature.geometry.paths.forEach(function(path){
                var pointList = [];
                var newPoint = null;
                path.forEach(function(point){
                    pointList.push(new OpenLayers.Geometry.Point(point[0],point[1]));
                });
                context.buckets[0].push( new OpenLayers.Feature.Vector(
                    new OpenLayers.Geometry.LineString(pointList),null,style
                ));
            });
        });

        context.callCount.count -= 1;
        if (context.callCount.count === 0) {
            //console.log(context.buckets);
            draw(context.buckets);
        }
        if (data.exceededTransferLimit) {
            log('Exceeded server\'s feature transfer limit.  Some features may not be drawn.',0);
        }
    }

    function fetchFeatures() {
        var visibleStates = [];
       //debugger;
        console.log(W.model.states.additionalInfo);
        W.model.states.additionalInfo.forEach(function(state) {
            visibleStates.push(_statesHash[state.name]);
        });

        var zoom = W.map.getZoom();
        var ext = W.map.getExtent();

        var geometry = {
            xmin: ext.left,
            ymin: ext.bottom,
            xmax: ext.right,
            ymax: ext.top
        };

        var geometryStr = JSON.stringify(geometry).replaceAll('{','%7B').replaceAll('}','%7D').replaceAll('"','%22').replaceAll(':','%3A').replaceAll(',','%2C');
        var callCount = 0;
        var buckets = [];
        var urls = [];
        var contexts = [];

        visibleStates.forEach(function(stateAbbr) {
            var state = _stateSettings[stateAbbr];
            if (state) {
                for(var i=0; i<7; i++) {buckets.push([]);}
                state.mapLayers.forEach(function(layer) {
                    if (!layer.zoomLevels || layer.zoomLevels.indexOf(zoom) !== -1) {
                        var context = {
                            stateSettings: state,
                            layer: layer,
                            buckets: buckets
                        };
                        var url = state.baseUrl + layer.layerID + '/query?';
                        url += 'geometry=' + geometryStr;
                        url += '&outFields=' + layer.outFields.join('%2C');
                        url += '&returnGeometry=true&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=102100&outSR=3857&f=json';
                        urls.push(url);
                        console.log(url);
                        contexts.push(context);
                        callCount += 1;

                    }
                });
            }
        });
        var countObj = {count: callCount};
        for (var j=0; j<urls.length; j++) {
            contexts[j].callCount = countObj;
            console.log(urls[j]);
            GM_xmlhttpRequest({
                context: contexts[j],
                method: "GET",
                url: urls[j],
                onload: function(res) {
                    processFeatures($.parseJSON(res.responseText), res.context);
                }
            });
        }
    }

    function onLayerVisibilityChanged(evt) {
        saveSettingsToStorage();
    }

    function restoreUserTab() {
        // $('#user-tabs > .nav-tabs').append(_tabDiv.tab);
        // $('#user-info > .flex-parent > .tab-content').append(_tabDiv.panel);
        // $('#fcl-state-select').change(function () {
        //     _settings.activeStateAbbr = this.value;
        //     saveSettingsToStorage();
        //     fetchFeatures();
        // });
    }

    function onModeChanged(model, modeId, context) {
        if(!modeId || modeId === 1) {
            restoreUserTab();
        }
    }

    function showScriptInfoAlert() {
        /* Check version and alert on update */
        if (_alertUpdate && _scriptVersion !== _settings.lastVersion) {
            alert(_scriptVersionChanges);
        }
    }

    function initLayer(){
        var mapLayerZIndex = 334;
        _mapLayer = new OpenLayers.Layer.Vector("SC Emergency Closures ", {
            uniqueName: "__SCECRoadsLayer",
            displayInLayerSwitcher: false
        });
        I18n.translations[W.location.locale].layers.name.__FCLayer = "SC Emergency Closures";
        W.map.addLayer(_mapLayer);
        _mapLayer.setZIndex(mapLayerZIndex);

        _mapLayer.displayInLayerSwitcher = true;
        _mapLayer.events.register('visibilitychanged',null,onLayerVisibilityChanged);
        _mapLayer.setVisibility(_settings.layerVisible);
        // Hack to fix layer zIndex.  Some other code is changing it sometimes but I have not been able to figure out why.
        // It may be that the FC layer is added to the map before some Waze code loads the base layers and forces other layers higher.
        var checkLayerZIndex = function(layerZIndex) {
            if (_mapLayer.getZIndex() != mapLayerZIndex)  {
                //log("ADJUSTED LAYER Z-INDEX",1);
                _mapLayer.setZIndex(mapLayerZIndex);
            }
        };

        setInterval(function(){checkLayerZIndex(mapLayerZIndex);}, 200);

        W.map.events.register("moveend",W.map,function(e){
            fetchFeatures();
            return true;
        },true);

        // for(var i=0; i<_activeState.defaultColors.length; i++) {
        //     var color = _activeState.defaultColors[i];
        //     var fc = i + 1;
        //     _styles[fc] = {
        //         strokeColor: color,
        //         strokeDashstyle: "solid",
        //         strokeOpacity: 0.5,
        //         strokeWidth: getLineWidth()
        //     };
        // }
    }

    function initUserPanel() {
        //         _tabDiv.tab = $('<li>').append(
        //             $('<a>', {'data-toggle':'tab', href:'#sidepanel-fc-layer'}).text('FC Layer')
        //         );

        //         _tabDiv.panel = $('<div>', {class:'tab-pane', id:'sidepanel-fc-layer'});/*.append(
        //             $('<div>',  {class:'side-panel-section>'}).append(
        //                 $('<div>', {class:'form-group'}).append(
        //                     $('<label>', {class:'control-label'}).text('Select a state')
        //                 ).append(
        //                     $('<div>', {class:'controls', id:'fcl-state-select-container'}).append(
        //                         $('<div>').append(
        //                             $('<select>', {id:'fcl-state-select',class:'form-control disabled',style:'disabled'})
        //                             .append($('<option>', {value:'IN'}).text('Indiana'))
        //                             //.append($('<option>', {value:'KY'}).text('Kentucky'))
        //                             .append($('<option>', {value:'MD'}).text('Maryland'))
        //                             .append($('<option>', {value:'MI'}).text('Michigan'))
        //                             .append($('<option>', {value:'NC'}).text('North Carolina'))
        //                             .append($('<option>', {value:'VA'}).text('Virginia'))
        //                             .val(_settings.activeStateAbbr)
        //                             // ).append(
        //                             //     $('<div>',{class:'controls-container'})
        //                             //     .append($('<input>', {type:'checkbox',class:'csSettingsCheckBox',name:'csDirectionButtonsCheckBox',id:'csDirectionButtonsCheckBox'}))
        //                             //     .append($('<label>', {for:'csDirectionButtonsCheckBox'}).text('Add road direction buttons'))
        //                             // ).append(
        //                             //     $('<input class="jscolor" value="ab2567">').change(function(evt) {console.log(evt);})
        //                         )
        //                     )
        //                 )
        //             )
        //         );*/

        //         restoreUserTab();
    }

    function initGui() {
        initLayer();
        initUserPanel();
        showScriptInfoAlert();
    }

    function init() {
        loadSettingsFromStorage();
        String.prototype.replaceAll = function(search, replacement) {
            var target = this;
            return target.replace(new RegExp(search, 'g'), replacement);
        };
        //_activeState = _stateSettings[_settings.activeStateAbbr];
        initGui();
        unsafeWindow.addEventListener('beforeunload', function saveOnClose() { saveSettingsToStorage(); }, false);
        Waze.app.modeController.model.bind('change:mode', onModeChanged);
        fetchFeatures();
        log('Initialized.', 0);
    }

    function bootstrap() {
        if (W && W.loginManager &&
            W.loginManager.events.register && W.model && W.model.states && W.model.states.additionalInfo &&
            W.map && W.loginManager.isLoggedIn()) {
            log('Initializing...', 0);
            init();
        } else {
            log('Bootstrap failed. Trying again...', 0);
            setTimeout(function () {
                bootstrap();
            }, 1000);
        }
    }

    log('Bootstrap...', 0);
    bootstrap();
})();