/* 
    This is an api for showing a blocker and loading spinner. each call to showSpinner and hideSpinner 
    takes a process ID so that one process cannot hide the spinner of another.
*/
window.loadBlockerApi = window.loadBlockerApi || new function () {
    var currentIds = [];

    //the spinner and blocker.
    var spinner = null;

    //show a spinner for a specific id.
    this.showSpinner = function (id) {
        //fix nulls.
        id = id || 'noId';

        //id is not present in the array.
        if ($.inArray(id, currentIds) == -1) {
            currentIds.push(id);
        }

        //spinner is not currently visible.
        if (!spinner) {

            //you are not supposed to reuse bootbox modals, animate parameter causes right click on ERS map to behave like browser right click on  Google map v3.32 and 3.33
            spinner = bootbox.dialog({
                message: '<i class=\'far fa-spinner fa-spin fa-3x fa-fw\'></i><span class=\'sr-only\'>' + resources.Loading + '</span>',
                closeButton: false,
                //animate: false,
                className: 'loadingModal',
                show: true
            });
        }
    };

    //hide the spinner for a specific id.
    this.hideSpinner = function (id) {
        //fix nulls.
        id = id || 'noId';

        //remove that id from the array.
        currentIds = jQuery.grep(currentIds, function (value) {
            return value != id;
        });

        //no more items in currentIds means blocker no longer required.
        if (currentIds.length == 0 && spinner) {
            spinner.modal('hide');
            spinner = null;
        }
    };
};
var AjaxCache = function () {
    var publicItem = {};

    var ajaxCallIdHashTable = new Hashtable();

    //remove item from cache.
    var checkCache = function (cacheKey) {
        var cachedItem = ajaxCallIdHashTable.get(cacheKey);

        //if item found.
        if (cachedItem) {
            //if not yet expired, return data.
            if (new Date().getTime() < cachedItem.cacheTTL) {
                return cachedItem.data;
            } else {
                //item expired, remove it.
                ajaxCallIdHashTable.remove(cacheKey);
                return null;
            }
        }

        //nothing to return.
        return null;
    };

    //add data to cache.
    var addToCache = function (data, cacheKey, minutesToCache) {
        ajaxCallIdHashTable.put(cacheKey, new CachedAjaxCall(data, minutesToCache));
    };

    publicItem.Remove = function (cacheKey) {
        ajaxCallIdHashTable.remove(cacheKey);
    }

    //check cache, if nothing then perform ajax call. returns deferred promise.
    publicItem.Get = function (cacheKey, cacheMins, ajaxOptions, skipCache) {
        var dfd = $.Deferred();

        //check cache first.
        var cachedData = checkCache(cacheKey);
        if (!skipCache && cachedData) {
            //fire callback for cached data.
            dfd = cachedData;
        } else {
            //cache for cacheMins, cache the promise so any requests made before the ajax call completes use the cache
            addToCache(dfd, cacheKey, cacheMins);
            //perform ajax call.
            $.ajax(ajaxOptions).done(function (data) {
                dfd.resolve(data);
                $(document).trigger('layerIconsUpdated', [cacheKey, data]);
            }).fail(dfd.reject);
        }

        return dfd.promise();
    };

    //private class.
    var CachedAjaxCall = function (data, minutesToCache) {
        var item = {};

        item.data = data;
        item.cacheTTL = new Date().getTime() + 60000 * minutesToCache; //60000 milliseconds in a minute.

        return item;
    };

    return publicItem;
};

(function ($, window) {
    var map,
        publicApi,
        appHelper,
        myLocationMarker,     
        mapTypePos,
        mapTypeStyle,
        zoomPos,
        locateMeBtnPosition,
        toggleFullScreenBtnPosition,
        toggleMapModeBtnPosition,
        truckerModeBtnPosition,
        mapLocationMarker,
        locationBtn = $(".locationBtn"),
        displayPois = 'off',
        isDarkMode,
        darkLabelsLayerType;

    function getCookie(name, valuesFromCookie) {
        var nameEq = '"' + name + '":';
        var ca = valuesFromCookie.split(',');
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ') c = c.substring(1, c.length);
            var subString = c.substring(nameEq.length, c.length);
            if (c.indexOf(nameEq) == 0) {
                var removeSpecialChars = subString.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '');
                return removeSpecialChars === "true" ? true : false;
            }
        }
        return null;
    }

    var isGoogleZoomVisible = true;

    var toggleMapMode = function () {

        if (map.getMapTypeId() === 'hybrid' || map.getMapTypeId() === 'satellite') {
            document.getElementsByClassName('gm-style-mtc')[0].getElementsByTagName('button')[0].click();
        }
        isDarkMode = !isDarkMode;
        let newOptions = { styles: isDarkMode ? mapModeStyles.dark(displayPois) : mapModeStyles.light(displayPois) };
        map.setOptions(newOptions);
        Cookies.set('mapClrMode', isDarkMode ? 'dark' : 'light');

        setDarkLabelsState();
    }

    function setDesktopCtrlPositions() {
        mapTypePos = google.maps.ControlPosition[resources.MapTypePos];
        mapTypeStyle = google.maps.MapTypeControlStyle[resources.MapTypeStyle];
        zoomPos = google.maps.ControlPosition[resources.MapControlPos];
        locateMeBtnPosition = google.maps.ControlPosition[resources.MapControlPos];
        truckerModeBtnPosition = google.maps.ControlPosition[resources.TruckerModeMapControlPos];
        toggleFullScreenBtnPosition = google.maps.ControlPosition[resources.MapControlPos];
        toggleMapModeBtnPosition = google.maps.ControlPosition[resources.MapControlPos];
        isGoogleZoomVisible = resources.UseGoogleMapZoom === "true";
    }

    function setMobileCtrlPositions() {
        mapTypePos = google.maps.ControlPosition[resources.MobileFirstMapTypePos];
        zoomPos = google.maps.ControlPosition[resources.MobileFirstMapControlPos];
        locateMeBtnPosition = google.maps.ControlPosition[resources.MobileFirstMapControlPos];
        truckerModeBtnPosition = google.maps.ControlPosition[resources.TruckerModeMobileFirstMapControlPos];
    }

    // position custom map controls - streetview, map/satellite, zoom, location
    var positionCustomControls = function (isMobileFirst, isERS, isWta) {      
        // For embedded map 810px width and up, use desktop values.
        var isEmbedded = $("#embedmap").length > 0;
        if (Modernizr.mq('(min-width: 993px)') || isEmbedded )
        {
            setDesktopCtrlPositions();
        } else {
            if (isMobileFirst || isERS || isWta) {
                setMobileCtrlPositions();
            } else {
                mapTypePos = google.maps.ControlPosition[resources.MobileMapTypePos];
                zoomPos = google.maps.ControlPosition[resources.MobileMapTypePos];
                locateMeBtnPosition = google.maps.ControlPosition[resources.MobileMapControlPos];
                truckerModeBtnPosition = google.maps.ControlPosition[resources.TruckerModeMobileMapControlPos];
            }

            mapTypeStyle = google.maps.MapTypeControlStyle[resources.MapTypeStyle];
            toggleFullScreenBtnPosition = google.maps.ControlPosition[resources.MobileMapControlPos];        
            isGoogleZoomVisible = true;
        }
    };
    
    let appResize;
    var init = function (options, callback) {
        appResize = new AppResize();

        var mapContainer = $('#map-canvas');
        var isERS = $(".ersLogo").length > 0;
        var isWta = $("#wtaMap").length > 0;
        var isTruckerModeQuery = URI().hasQuery('mode', "trucker");
        $(document).trigger("adjustHeightTriggered");
        var isMobileFirst = resources.MobileFirst == "True";
        positionCustomControls(isMobileFirst, isERS, isWta);
      
        if (options.DisplayPOIs) {
            displayPois = 'on';
        }

        //google map settings. center the map and set zoom level.
        var mapOptions = {
            mapTypeControlOptions: {
                style: mapTypeStyle,
                position: mapTypePos
            },
            center: new google.maps.LatLng(options.MapCenter.Latitude, options.MapCenter.Longitude),
            zoom: options.DefaultZoom,
            mapTypeId: options.MapTypeId,
            scaleControl: true,
            zoomControl: isGoogleZoomVisible,
            zoomControlOptions: {
                position: zoomPos
            },
            fullscreenControl: false,
            streetViewControlOptions: {
                position: zoomPos
            },
            gestureHandling: 'greedy',
            disableDefaultUI: !options.DisplayControls,
            styles: Cookies.get('mapClrMode') ? mapModeStyles[Cookies.get('mapClrMode')](displayPois) : mapModeStyles.light(displayPois)
        };

        //Hide map legend and Disable google map UI if specified
        if (URI().hasQuery('maponly', "true")) {
            mapOptions.disableDefaultUI = true;
            $(".legend-container").hide();
        }
       
        //init the map.
        map = new google.maps.Map(mapContainer[0], mapOptions);

        var labelsLayer = mapModeStyles.lightLabelsLayer(displayPois);
        var darkLabelsLayer = mapModeStyles.darkLabelsLayer(displayPois);

        var satelliteLabels = mapModeStyles.satelliteLabels(displayPois);

        var satelliteLabelsLayerType = new google.maps.StyledMapType(satelliteLabels);
        var labelsLayerType = new google.maps.StyledMapType(labelsLayer, { name: 'labels' });
        darkLabelsLayerType = new google.maps.StyledMapType(darkLabelsLayer, { name: 'darkLabels' });

        map.overlayMapTypes.push(labelsLayerType);
        isDarkMode = Cookies.get('mapClrMode') && Cookies.get('mapClrMode') === 'dark' ? true : false;
        setDarkLabelsState();

        google.maps.event.addListener(map, 'maptypeid_changed', function (d) {                     
          
            if (map.getMapTypeId() === 'hybrid' || map.getMapTypeId() === 'satellite') {
                //hide dark mode, and remove dark labels
                map.setOptions({
                    styles: mapModeStyles.light(displayPois, true) });

                let darkLblLayerIndex = map.overlayMapTypes.getArray().indexOf(darkLabelsLayerType);
                if (darkLblLayerIndex > -1) {
                    map.overlayMapTypes.removeAt(darkLblLayerIndex);
                }
                isDarkMode = false;
                Cookies.set('mapClrMode', isDarkMode ? 'dark' : 'light');
            } else {
                map.setOptions({ styles: isDarkMode ? mapModeStyles.dark(displayPois) : mapModeStyles.light(displayPois) });
                setDarkLabelsState();
            }
        
            var asArray = map.overlayMapTypes.getArray();
            var existingLabelIndex = asArray.indexOf(labelsLayerType) != -1 ? asArray.indexOf(labelsLayerType) : asArray.indexOf(satelliteLabelsLayerType);
            var labelType = map.getMapTypeId() == "hybrid" ? satelliteLabelsLayerType : labelsLayerType;
            if (existingLabelIndex != -1) {
                if (map.getMapTypeId() === 'satellite') {
                    map.overlayMapTypes.removeAt(existingLabelIndex);
                } else {
                    map.overlayMapTypes.setAt(existingLabelIndex, labelType);
                }
            }
            else {
                map.overlayMapTypes.push(labelType);
            }

        });

        // create custom zoom when Google zoom is false (Desktop mode only)
        if (!isGoogleZoomVisible && options.DisplayControls) {
            var zoomControlDiv = document.createElement('div');
            var zoomControl = new ZoomControl(zoomControlDiv, map);
            zoomControlDiv.index = 1;
            map.controls[zoomPos].push(zoomControlDiv);
        }

        if (typeof toggleMapModeBtnPosition === 'number') {
            var mapModeControlDiv = document.createElement('div');
            toggleDarkMapBtnBuilder(mapModeControlDiv, map);
            map.controls[toggleMapModeBtnPosition].push(mapModeControlDiv);
        } else {
           var mobileDarkModeControlDiv = document.createElement('div');
           mobileToggleMapModeControl(mobileDarkModeControlDiv, map);           
           map.controls[zoomPos].push(mobileDarkModeControlDiv);
           google.maps.event.addDomListener(mobileDarkModeControlDiv, 'click', toggleMapMode );
        }

        if (resources.EnableSaveMapView === "true") {
            var saveMapViewControlDiv = document.createElement('div');
            var saveMapViewControl = new SaveMapViewControl(saveMapViewControlDiv, map);
            saveMapViewControlDiv.index = 1;
            map.controls[zoomPos].push(saveMapViewControlDiv);
        }

        if (options.DisplayControls) {
            //Making a custom Locate Me button
            var locateMeDiv = document.createElement('div');
            locateMeBtnBuilder(locateMeDiv, map);
            //Have the locate me button on the left side in mobile so that the legend button doensn't hide it
            map.controls[locateMeBtnPosition].push(locateMeDiv);
        }

        if (resources.EnableToggleFullScreenMapBtn === 'true') {
            var toggleFullScreenDiv = document.createElement('div');
            toggleFullScreenMapBtnBuilder(toggleFullScreenDiv, map);
            map.controls[toggleFullScreenBtnPosition].push(toggleFullScreenDiv);
        }


        if (resources.EnableTruckerMode === 'True' && !isERS) {
            var truckerModeDiv = document.createElement('div');
            if (isTruckerModeQuery) {
                Cookies.set("_truckerMode", true);
            }
            truckerModeBtnBuilder(truckerModeDiv, map);
            map.controls[truckerModeBtnPosition].push(truckerModeDiv);
        } else {
            Cookies.remove("_truckerMode");
        }

        if (resources.EnableNearby511Kml == 'True' && !isERS & !isWta) {
            var kmlOptions = {
                clickable: true,
                suppressInfoWindows: true,  //always suppress kml infowindows
                preserveViewport: true,
                map: map
            };

            var nearby511Layer = new google.maps.KmlLayer(URI(resources.Nearby511Kml).addSearch("t", roundDate(moment(), 5).unix()).addSearch('lang', Cookies.get("_culture")).toString(), kmlOptions);

            nearby511Layer.addListener('click', function (kmlEvent) {
                appHelper.showInfoWindow(kmlEvent.featureData.description, null, true, kmlEvent.latLng, "Nearby511", null, false, new google.maps.Size(0,-32));
            });
        }

        //on map init. fires only once.
        google.maps.event.addListenerOnce(map, 'idle', function () {
            //init the app helper.
            appHelper = new AppHelper(map, options, getLayerInfo());

            //public api that we pass as a parameter to callback.
            publicApi = {
                center: centerTo,
                map: map,
                layerSelectorClosed: appHelper.layerSelectorClosed,
                tileManager: appHelper.tileManager,
                appHelper: appHelper
            };

            window.SetUserRegion = function (locationInfo) {
                if (locationInfo) {
                    var pos = [locationInfo[0], locationInfo[1]];
                    publicApi.center(pos, parseInt(locationInfo[2]));
                }
            };

            // set map legend to open by default on larger screen if thet is what is indicated in the config
            if (Modernizr.mq('(min-width: 993px)') && resources.MapLegendDisplayByDefault === 'true') {
                $(".legend-toggle").trigger("click");
            }

            UserCameras = new CameraLocater(publicApi);

            //trigger a 'finished' event.
            $(document).trigger('appInitComplete', [publicApi, options]);

            //if callback defined.
            if (callback) {
                callback(publicApi);
            }

            // if layer is checked, uncollapse that section (even if it is specified by default)
            var legendGroups = $(".layerSelection .toggleButton");
            for (var j = 0; j < legendGroups.length + 2; j++) {
                var className = ".panel-collapse." + j;
                if ($(className).length > 0 && !$(className).hasClass("in")) {
                    var classNameWithCheckbox = className + " input[type=checkbox]";
                    var isCheck = $(classNameWithCheckbox).is(":checked");
                    if (isCheck) {
                        $(className).addClass("in");
                        $(className).parent().find(".fa-plus-circle").removeClass("fa-plus-circle").addClass("fa-minus-circle");
                    }
                }
            }            
        });

        var showLegendToggleBtn = function (isWta, isERS) {
                        if (isWta || isERS) {
                $('.mobileSetting').show();
                $('.legend-toggle').show();
                if (!$(".bootbox").is(":visible")) {
                    $('#layerSelection').hide();
                }
            }
        };


        function getMapOptions() {
            return {
                mapTypeControlOptions: {
                    style: mapTypeStyle,
                    position: mapTypePos
                },
                zoomControlOptions: {
                    position: zoomPos
                },
                streetViewControlOptions: {
                    position: zoomPos
                }
            }
        }

        function windowResize() {
            var os = getMobileOs();
            if (os !== "Android") {
                isERS = $(".ersLogo").length > 0;
                isWta = $("#wtaMap").length > 0;
                if ($(window).outerWidth() < 993) {
                    // close draggable tooltip if open
                    if ($(".draggableWindowContainer").length > 0) {
                        $(document).trigger("closeDraggableWindow");
                    }

                    if (isMobileFirst && !isERS && !isWta) {

                        $("body").addClass("mobileFirst");
                        $('.mobileSetting').hide();
                        $('#layerSelection').hide();
                        if ($(".closeSideBar").is(":visible")) {
                            $(".closeSideBar").trigger("click");
                        }
                        if ($(".showSideBar").is(":visible")) {
                            $(".mobileLocationBar").hide();
                        } else {
                            if (!$(".mobileLocationBar").is(":visible")) {
                                $(".mobileLocationBar").show();
                            }
                        }
                        $(".myCamerasContainer").appendTo(".myCamerasGroup");
                    } else {
                        showLegendToggleBtn(isWta, isERS);
                    }
                    $("footer").hide();
                    autocompleteForLocationBar();
                } else {
                    if ($("body.mobileFirst").length > 0) {
                        $("body").removeClass("mobileFirst");
                        if (resources.EnableLocationSearchBar != "True") {
                            if (!$(".showSideBar").is(":visible")) {
                                $(".showSideBar").trigger("click");
                            }
                            if ($(".mobileLocationBar").is(":visible")) {
                                $(".mobileLocationBar").hide();
                            }
                        } else {
                            if (resources.OpenDesktopRoutePlannerDefault.toLowerCase() === "true" || window.location.hash.substr(0) == "#:MyRoutes" || window.location.hash.indexOf('#route') !== -1) {
                                $(".myRouteBtn").trigger("click"); //show route planner by default on desktop instead of locate bar 
                            } else {
                                locationBtn.hide();
                                $("#sideBar").hide();
                            }
                        }
                    }
                    $('.mobileSetting').show();
                    var myCamerasGroup = $(".myCamerasGroup");
                    if (myCamerasGroup.is(":visible")) {
                        myCamerasGroup.hide();
                    }
                    if (myCamerasGroup.html() != undefined && myCamerasGroup.html().trim().length > 0) {
                        $(".myCamerasGroup .myCamerasContainer").appendTo(".myCamerasRoutePlanner");
                    }

                    if (!$("footer").is(":visible") && !isERS) {
                        $("footer").show();
                    }
                }

                var oldPos = mapTypePos;
                var locateMeBtnPositionOld = locateMeBtnPosition;
                var truckerModeBtnPositionOld = truckerModeBtnPosition;
                positionCustomControls(isMobileFirst, isERS, isWta);
                if (oldPos != mapTypePos) {
                    if ($(".zoomControlContainer").length < 1) {
                        if (!isGoogleZoomVisible && options.DisplayControls) {
                            var zoomControlDiv = document.createElement('div');
                            var zoomControl = new ZoomControl(zoomControlDiv, map);
                            zoomControlDiv.index = 1;
                            map.controls[zoomPos].push(zoomControlDiv);
                        }
                    }

                    var controlArray = map.controls[locateMeBtnPositionOld].getArray();
                    for (var div in controlArray) {
                        if ($(controlArray[div]).attr('class') == 'locateMeContainer') {
                            var locateMeDiv = map.controls[locateMeBtnPositionOld].getAt(div);
                            map.controls[locateMeBtnPositionOld].removeAt(div);
                            map.controls[locateMeBtnPosition].push(locateMeDiv);
                            break;
                        }
                    }
                    var truckerControlArray = map.controls[truckerModeBtnPositionOld].getArray();
                    for (var arr in truckerControlArray) {
                        if ($(truckerControlArray[arr]).attr('class') == 'truckerModeContainer') {
                            var truckerModeDiv = map.controls[truckerModeBtnPositionOld].getAt(arr);
                            map.controls[truckerModeBtnPositionOld].removeAt(arr);
                            map.controls[truckerModeBtnPosition].push(truckerModeDiv);
                            break;
                        }
                    }
                    var mapOptions = getMapOptions();
                    mapOptions.zoomControl = isGoogleZoomVisible;
                    map.setOptions(mapOptions);
                }
            }
            appResize.checkRightBtmCtrls();
            appResize.checkTopCenterCtrls(map, setDesktopCtrlPositions, setMobileCtrlPositions, getMapOptions);

            clearTimeout(window.resizedFinished);
            window.resizedFinished = setTimeout(function () {
                if ($("#routingResults").is(":visible")) {
                    $(document).trigger("checkEventOnRoutePlanner", ['windowResize']);
                }
            }, 250);

        }

        //run this function once on page load to make sure css "mobileFirst" class is added or removed from the body.
        windowResize();

        // when map viewport changes, adjust the position of the custom map controls - overkill!
        google.maps.event.addDomListener(window, 'resize', windowResize );  // windows resize


        var streetView = map.getStreetView();
        var backToMapViewBtn = $('#backToMapView');
        google.maps.event.addListener(streetView, 'visible_changed', function () {

            if (!streetView.getVisible()) {
                backToMapViewBtn.hide();
                $(".showSideBar").removeClass("streetViewTop");
            } else {
                // Hide tiny default close button located on top right
                streetView.setOptions({ enableCloseButton: false, fullscreenControl: false });
                backToMapViewBtn.show();

                $(backToMapViewBtn).on('click', function () {
                    streetView.setVisible(false);
                });

                $(".showSideBar").addClass("streetViewTop");
                $(".mobileFirst.sideBarGroup.showSideBar").addClass("streetViewTop");
            }
        });

        var avoidTolls = false;
        var avoidFerries = false;
        var valuesFromCookie;

        var avoidTollsCb = $("#avoidTollsCheckBox");
        if (avoidTollsCb) {
            valuesFromCookie = Cookies.get("map");

            if (valuesFromCookie != null) {
                avoidTolls = getCookie("AvoidTolls", valuesFromCookie);
            }
            avoidTollsCb.attr("checked", avoidTolls);
        }

        var avoidFerriesCb = $("#avoidFerriesCheckBox");
        if (avoidFerriesCb) {
            valuesFromCookie = Cookies.get("map");

            if (valuesFromCookie != null) {
                avoidFerries = getCookie("AvoidFerries", valuesFromCookie);
            }
            avoidFerriesCb.attr("checked", avoidFerries);
        }

        if (resources.StateOutline != '') {
            var statePoly = new google.maps.Polyline({
                path: google.maps.geometry.encoding.decodePath(resources.StateOutline),
                strokeColor: '#00000',
                strokeOpacity: 1,
                strokeWeight: 1,
                map: map
            });
        }

        //show route planner in mobile mode when you click on create route btn on manage route/alert page
        if (Modernizr.mq('(max-width: 992px)')) {
            if (isMobileFirst && window.location.hash.substr(0) == "#:MyRoutes") {
                $(".UIControls .directions").click();
            }
        } else {
            if (resources.OpenDesktopRoutePlannerDefault.toLowerCase() === "true" || window.location.hash.substr(0) == "#:MyRoutes" || window.location.hash.indexOf('#route') !== -1) {
                $(".myRouteBtn").trigger("click"); //show route planner by default on desktop instead of locate bar 

            } else {
                locationBtn.hide();
                $("#sideBar").hide();
            }
        }

        var iosDetect = getMobileOs();     
        if (iosDetect != "unknown") {
            $(".printRoute").hide();
            if (iosDetect == "iOS") {
                $('.twitter.newsContent').addClass('isIos');
            }
            if (iosDetect == "Android") {
                //hide search box for ersRegion dropdown in Android because the soft keyboard doesn't stay
                $("#ersRegion-Combobox .bs-searchbox, #mobileErsRegion-Combobox .bs-searchbox").hide(); 
            }
        }
  
        autocompleteForLocationBar();
        showLegendToggleBtn(isWta, isERS);  
 
        var isTruckerMode = resources.EnableTruckerMode;
        if (isTruckerMode == "True") {            
            var isActive = Cookies.get("_truckerMode");
            if (isActive) {
                showTruckerModeIndicator(true);                             
            } else {
                showTruckerModeIndicator(false);
            }            
        }      
    };//init   

    var autocompleteForLocationBar = function () {
        var isMobileFirst = $("body").hasClass("mobileFirst") && !$(".ersLogo").length > 0;
        isWta = $("#wtaMap").length > 0;
        if (resources.EnableLocationSearchBar == "True"){
            locationSearchBar();
        }
        if (Modernizr.mq('(max-width: 992px)') && isMobileFirst && !isWta) {
            locationSearchBar();
            $(".myCamerasContainer").appendTo(".myCamerasGroup");
            $("footer").hide();
        }
    };

    var locationSearchBar = function () {
        var locationInput = document.getElementById("mapLocation");
        var locationOptions = {
            componentRestrictions: { country: resources.AutocompleteCountryCode.split(',') },
            fields: ['address_component,adr_address,alt_id,formatted_address,geometry,icon,id,name,place_id,plus_code,scope,type,url,utc_offset,vicinity']
        };

        if (locationInput) { 
            var autocomplete = new google.maps.places.Autocomplete(locationInput, locationOptions);

            autocomplete.bindTo('bounds', map);
            mapLocationMarker = new google.maps.Marker({
                map: map
            });
         
            // GoogleMaps API custom eventlistener method
            google.maps.event.addDomListener(locationInput, "keydown", function (e) {
                // Maps API e.stopPropagation();
                e.cancelBubble = true;              

                // If enter key, or tab key
                if (e.keyCode === 13 || e.keyCode === 9) {
                    var hasSelectedLocation = $(".pac-item-selected").length > 0;
                    // If user isn't navigating using arrows and this hasn't ran yet
                    if (!hasSelectedLocation && !e.hasRanOnce) {
                        var eKeydown = new Event("keydown");
                        eKeydown.keyCode = 40;
                        eKeydown.hasRanOnce = true;
                        google.maps.event.trigger(e.target, "keydown", eKeydown);
                    }
                }
            });

            google.maps.event.addListener(autocomplete, 'place_changed', function (e) {
                mapLocationMarker.setVisible(false);
                locationInput.className = '';
                var place = autocomplete.getPlace();               
                if (!place.geometry) {                  
                    // User entered the name of a Place that was not suggested and
                    // pressed the Enter key, or the Place Details request failed.
                    bootbox.dialog({
                        title: resources.InvalidLocation,
                        message: "<div class='alert alert-info'><i class='far fa-exclamation-circle'></i>" + resources.SelectValidLocationList + "</div>",
                        className: "mapLocationMsg"
                    });
                    return;
                }

                // If the place has a geometry, then present it on a map.
                if (place.geometry.viewport) {
                    map.fitBounds(place.geometry.viewport);
                } else {
                    map.setCenter(place.geometry.location);
                    map.setZoom(17); // Why 17? Because it looks good.
                }
                mapLocationMarker.setPosition(place.geometry.location);
                mapLocationMarker.setVisible(true);
                $(".mobileLocationBar .clearLocateBtn").show();           
            });
        }
    };
 
    //Builds MyLocation button
    var locateMeBtnBuilder = function (controlDiv, map) {

        controlDiv.className = 'locateMeContainer customMapCtrl';

        var locationButton = document.createElement('button');
        locationButton.className = 'locateMeBtn btn btn-default';
        locationButton.setAttribute('id', 'locateMeBtn');
        locationButton.setAttribute('type', 'button');
        locationButton.setAttribute('title', resources.CurrentLocation);
        // Add for AODA 
        locationButton.setAttribute('aria-label', resources.CurrentLocation);

        var btnImage = document.createElement('i');
        btnImage.className = 'fas fa-crosshairs';
        // Add for AODA
        btnImage.setAttribute('aria-hidden', 'true');
        btnImage.setAttribute('title', resources.CurrentLocation);
        locationButton.appendChild(btnImage);
        controlDiv.appendChild(locationButton);

        locationButton.addEventListener('click', function () {
            // Avail GetUserGeolocation in ERS mode
            if (window.GetUserGeolocation == undefined) {
                var geolocation = new UserGeolocation(null, null, null, null, null);
            }
           
            window.GetUserGeolocation(function (location) {
                map.panTo(location);
                //Remove the old marker, otherwise when the button is clicked more than once, the old marker can not be removed.
                $(document).trigger('removeMyLocationMarker');
                myLocationMarker = new google.maps.Marker({
                    position: location,
                    map: map,
                    title: resources.CurrentLocation,
                    icon: {
                        url: "Content/images/locationDot.png",
                        size: new google.maps.Size(24, 24)
                    }
                });
                var currentZoom = map.getZoom();
                map.setZoom(12 < currentZoom ? currentZoom : 12);
            });

        });
        $(document).on('removeMyLocationMarker', function () {
            if (myLocationMarker) myLocationMarker.setMap(null);
        });
    };


    //Builds toggle map color mode btn
    var toggleDarkMapBtnBuilder = function (controlDiv, map) {
        controlDiv.className = 'toggleDarkLMapContainer customMapCtrl';
        controlDiv.setAttribute('id', 'toggleDarkLMapContainer');
        var toggleMapModeButton = document.createElement('button');
        toggleMapModeButton.className = 'toggleMapModeButton btn btn-default';
        toggleMapModeButton.setAttribute('id', 'toggleMapModeButton');
        toggleMapModeButton.setAttribute('type', 'button');
        toggleMapModeButton.setAttribute('title', resources.ToggleDarkLightMap);
        // Add for AODA
        toggleMapModeButton.setAttribute('aria-label', resources.ToggleDarkLightMap);
        var btnImage = document.createElement('i');
        btnImage.className = 'fas fa-adjust';
        // Add for AODA
        btnImage.setAttribute('aria-hidden', 'true');
        btnImage.setAttribute('title', resources.ToggleDarkLightMap);
        toggleMapModeButton.appendChild(btnImage);
        controlDiv.appendChild(toggleMapModeButton);
       
        google.maps.event.addDomListener(toggleMapModeButton, 'click', toggleMapMode);
    };

    var setDarkLabelsState = function(){
        let darkLblLayerIndex = map.overlayMapTypes.getArray().indexOf(darkLabelsLayerType);
        if (isDarkMode) {
            if (darkLblLayerIndex > -1) {
                map.overlayMapTypes.setAt(0, darkLabelsLayerType);
            }
            else {
                map.overlayMapTypes.push(darkLabelsLayerType);
            }
        } else {
            if (darkLblLayerIndex > -1) {
                map.overlayMapTypes.removeAt(darkLblLayerIndex);
            }
        }
    }

    //Builds toggle fullscreen map btn
    var toggleFullScreenMapBtnBuilder = function (controlDiv, map) {

        controlDiv.className = 'toggleFullScreenMapContainer customMapCtrl';

        var toggleFullScreenButton = document.createElement('button');
        toggleFullScreenButton.className = 'toggleFullScreenMapBtn btn btn-default';
        toggleFullScreenButton.setAttribute('id', 'toggleFullScreenMapBtn');
        toggleFullScreenButton.setAttribute('type', 'button');
        toggleFullScreenButton.setAttribute('title', resources.ToggleFullscreen);
        // Add for AODA
        toggleFullScreenButton.setAttribute('aria-label', resources.ToggleFullscreen);

        var btnImage = document.createElement('i');
        btnImage.className = 'far fa-arrows-alt';
        // Add for AODA
        btnImage.setAttribute('aria-hidden', 'true');
        btnImage.setAttribute('title', resources.ToggleFullscreen);
        toggleFullScreenButton.appendChild(btnImage);
        controlDiv.appendChild(toggleFullScreenButton);

        toggleFullScreenButton.addEventListener('click', function () {
            if (Modernizr.mq('(min-width: 993px)')) {
                $(".navbar, .scrollRow, footer").toggle();
                $(document).trigger("fullScreenMapMode");
            }
        });
    };

    // Custom zoom control
    var ZoomControl = function (controlDiv, map) {

        // Creating divs & styles for custom zoom control
        controlDiv.className = 'zoomControlContainer customMapCtrl';

        // Set CSS for the control wrapper
        var controlWrapper = document.createElement('div');
        controlWrapper.className = "zoomControl";
        controlDiv.appendChild(controlWrapper);

        // Set CSS for the zoomIn
        var zoomInButton = document.createElement('i');
        zoomInButton.className = "far fa-plus";
        //Add for AODA to ZoomIn button
        zoomInButton.setAttribute('title', resources.ZoomInMap);
        zoomInButton.setAttribute("aria-label", resources.ZoomInMap);
        zoomInButton.setAttribute("role", "button");   
        zoomInButton.setAttribute("tabindex", "0"); 
        controlWrapper.appendChild(zoomInButton);
      

        // Set CSS for the zoomOut
        var zoomOutButton = document.createElement('i');
        zoomOutButton.className = "far fa-minus";
        //Add for AODA to ZoomIn button
        zoomOutButton.setAttribute('title', resources.ZoomOutMap);
        zoomOutButton.setAttribute("aria-label", resources.ZoomOutMap);
        zoomOutButton.setAttribute("role", "button");  
        zoomOutButton.setAttribute("tabindex", "0");  
        controlWrapper.appendChild(zoomOutButton);     

        // Setup the click event listener - zoomIn
        google.maps.event.addDomListener(zoomInButton, 'click',  function () {
            map.setZoom(map.getZoom() + 1);
        });

        google.maps.event.addDomListener(zoomInButton, 'keydown', function (e) {
            if (e.code === 'Enter' || e.code === "Space") {
                map.setZoom(map.getZoom() + 1);
            }
        });       

        // Setup the click event listener - zoomOut
        google.maps.event.addDomListener(zoomOutButton, 'click', function () {
            map.setZoom(map.getZoom() - 1);
        });

        google.maps.event.addDomListener(zoomOutButton, 'keydown', function (e) {
            if (e.code === 'Enter' || e.code === "Space") {
                map.setZoom(map.getZoom() - 1);
            }
        });
    };

    //Builds Trucker Mode button
    var truckerModeBtnBuilder = function (controlDiv, map) {

        controlDiv.className = 'truckerModeContainer customMapCtrl';
        var truckerModeButton = document.createElement('button');        
       
        truckerModeButton.className = Cookies.get("_truckerMode") ? 'truckerModeBtn btn btn-default active' : 'truckerModeBtn btn btn-default';
        truckerModeButton.setAttribute('id', 'truckerModeBtn');
        truckerModeButton.setAttribute('type', 'button');
        truckerModeButton.setAttribute('title', resources.TruckerMode);
        // Add for AODA 
        truckerModeButton.setAttribute('aria-label', resources.TruckerMode);
        truckerModeButton.setAttribute('tabindex', 0);
        var btnImage = document.createElement('i');
        btnImage.className = 'far fa-truck-moving';
        btnImage.setAttribute('aria-label', 'true');
     
        var btnText = document.createElement('span');
        btnText.innerHTML = resources.TruckerMode;
             
        // Add for AODA
        btnImage.setAttribute('aria-hidden', 'true');
        btnImage.setAttribute('title', resources.TruckerMode);
        truckerModeButton.appendChild(btnImage);
        truckerModeButton.appendChild(btnText);
        controlDiv.appendChild(truckerModeButton);       

        truckerModeButton.addEventListener('click', function () {            
            var isActive = $(".truckerModeBtn").hasClass("active");
            if (isActive) {
                Cookies.remove("_truckerMode");
                showTruckerModeIndicator(false);  
            } else {
                Cookies.set("_truckerMode", true);
                showTruckerModeIndicator(true);               
            }
            $(".truckerModeBtn").toggleClass("active");            
        });          
    };

    var showTruckerModeIndicator = function (show) {
        var socialIcons = $(".topBanner .socialIcons");
        var childSet = $(".TruckerInfoLegendSection ul[id$='-children']");
        if (show) {
            if (typeof (ga) !== 'undefined') {
                ga('send', 'event', 'TruckerMode', 'Show');
            }   
            $(".truckerModeIndicator").show();
            if (socialIcons.length > 0) {
                socialIcons.addClass("down");
            }  
        
            if (!$(".mapLegend > .TruckerInfoLegendSection").is(":first")) {
                moveTruckerLegendTop(true);
            }
            
            if (childSet.length > 0) {
                childSet.show();
                childSet.find("input[type='checkbox']").show();
            }
        } else {    
            $(".truckerModeIndicator").hide();
            if (socialIcons.length > 0) {
                socialIcons.removeClass("down");
            }    
            moveTruckerLegendTop(false);
            show = false;    

            if (childSet.length > 0) {
                childSet.hide();
                childSet.find("input[type='checkbox']").hide();
            }
        }
        
        $(".TruckerInfoLegendSection label input[type='checkbox']").prop("checked", show);     

        if (appHelper) {
            $.each($(".TruckerInfoLegendSection label input[type='checkbox']"), function (idx, val) {
                appHelper.layerToggled($(val).attr('data-layerid'), false);
            });
        }        
    };

    var moveTruckerLegendTop = function (top) {    
        var isExpanded = $(".TruckerInfoLegendSection .collapse.in").length > 0;
        if (top) {        
            $(".TruckerInfoLegendSection").insertAfter(".clearAllLayers");
            if (!isExpanded) {
                $(".TruckerInfoLegendSection .toggleButton").trigger("click");
            }
        } else {
            $(".TruckerInfoLegendSection").insertBefore(".markerClusterLegend");
            if (isExpanded) {
                $(".TruckerInfoLegendSection .toggleButton").trigger("click");
            }
        }   
    };

    //centers the map to a lat, lng and sets the zoom level.
    var centerTo = function (latLng, zoom) {
        var location = new google.maps.LatLng(latLng[0], latLng[1]);
        map.panTo(location);

        if (zoom) {
            map.setZoom(zoom);
        }
    };

    //--- section related to icon layer details.
    var parseIconDetails = function (item) {
        var iconUrl = item.attr('data-icon');

        if (!iconUrl) {
            return null;
        }

        var iconSize = item.attr('data-iconsize');
        var anchor = item.attr('data-iconanchor');
        var origin = item.attr('data-iconorigin');        

        var icon = {
            url: iconUrl
        };

        if (iconSize) {
            icon.size = splitStringToGoogleSize(iconSize);
        }

        if (anchor) {
            icon.anchor = splitStringToGooglePoint(anchor);
        }

        if (origin) {
            icon.origin = splitStringToGooglePoint(origin);
        }

        return icon;
    };

    var splitStringToGoogleSize = function (stringValue) {
        var numbers = splitStringToNumberArray(stringValue);
        return new google.maps.Size(numbers[0], numbers[1]);
    };

    var splitStringToGooglePoint = function (stringValue) {
        var numbers = splitStringToNumberArray(stringValue);
        return new google.maps.Point(numbers[0], numbers[1]);
    };

    var splitStringToNumberArray = function (stringValue) {
        var numbers = [];
        var vals = stringValue.split(',');
        for (var i = 0; i < vals.length; i++) {
            numbers[i] = +vals[i];
        }

        return numbers;
    };

    //gets the layer details such as icon image details, api url, and tooltip url.
    var getLayerInfo = function () {
        var info = {
            iconDetails: {}, apiUrls: {}, tooltipBaseUrls: {}, tooltipDraggable: {}, tooltipSize: {}, feed: {}, tile: {}, icon: {}, filterData: {} };

        var layerSelector = $('#layerSelection');

        //get checkboxes who have data-icon attribute. these layers are displayed on map as icons.
        var checkbox = $('input[type=\'checkbox\'][data-icon], [data-polyline]', layerSelector);

        //get checkboxes who have data-kmlurl attribute. these are kml map layers.
        var kmlCheckbox = $('input[type=\'checkbox\'][data-feedurl]', layerSelector);

        //get checkboxes who have data-kmlurl attribute. these are kml map layers.
        var tileCheckbox = $('input[type=\'checkbox\'][data-tileurlformat]', layerSelector);

        checkbox.each(function (i, thisCheckbox) {
            var item = $(thisCheckbox);
            var layerId = item.attr('data-layerId');
            var hasFilter = item.attr('data-hasFilter');

            //does this layer have a filter?
            info.filterData[layerId] = { "layerId": layerId, "hasFilter": hasFilter };

            //get the api and tooltip base url for this checkbox - mick.
            info.apiUrls[layerId] = item.attr('data-jsonurl');
            info.tooltipBaseUrls[layerId] = item.attr('data-tooltipbaseurl');
            info.tooltipDraggable[layerId] = item.attr('data-tooltipdraggable');
            info.tooltipSize[layerId] = item.attr('data-tooltipsize') != null ? item.attr('data-tooltipsize') : null;
            info.icon[layerId] = {
                minZoom: item.attr('data-minzoom'),
                maxZoom: item.attr('data-maxzoom'),
            }
            //grab the icon details.
            var icon = parseIconDetails(item);
            if (icon) {
                info.iconDetails[layerId] = icon;
            }
        });

        kmlCheckbox.each(function (i, thisCheckbox) {
            var item = $(thisCheckbox);
            var layerId = item.attr('data-layerId');

            //get the kml url for this checkbox and other details.
            info.feed[layerId] = {
                url: item.attr('data-feedurl'),
                clickable: item.attr('data-clickable').toLowerCase() === 'true',
                styleOptions: item.attr('data-styleOptions'),
                infoWindowTemplate: item.attr('data-infoWindowTemplate'),
                suppressInfoWindow: item.attr('data-suppressinfowindow').toLowerCase() === 'true',
                type: item.attr('data-type'),
                cacheTime: item.attr('data-cachetime')
            };
        });

        tileCheckbox.each(function (i, thisCheckbox) {
            var item = $(thisCheckbox);
            var layerId = item.attr('data-layerId');

            //get the tile url, min zoom, and max zoom for this checkbox.
            info.tile[layerId] = {
                urlFormat: item.attr('data-tileurlformat'),
                minZoom: item.attr('data-minzoom'),
                maxZoom: item.attr('data-maxzoom'),
                tooltipUrlFormat: item.attr('data-tooltipurlformat'),
                highwayFill: item.attr('data-highwayfill'),
                highwayStroke: item.attr('data-highwaystroke'),
                animationFrames: item.attr('data-animationframes'),
                startingFrame: item.attr('data-startingframe'),
                startPlaying: item.attr('data-startPlaying') == "True",
                opacity: item.attr('data-opacity'),
                zlevel: item.attr('data-zlevel'),
                cache: item.attr('data-cache') == "True"
            };
        });

        return info;
    };

    // PR set to region
    $('.setToRegion').on('click', function () {
        var value = $(this).val();
        var latLngZoom = value.split(',');
        map.panTo(new google.maps.LatLng(latLngZoom[0], latLngZoom[1]));
        map.setZoom(parseInt(latLngZoom[2]));
    });

    var getMap = function () {
        return map;
    };

    // Legend Header Section Collapsible
    $("#layerSelection button").on("click", function () {
        var header = $(this).find("i");
        if (header.hasClass('fa-minus-circle') === true) {
            header.removeClass('fa-minus-circle');
            header.addClass('fa-plus-circle');
        } else {
            header.addClass('fa-minus-circle');
            header.removeClass('fa-plus-circle');
        }
    });

    //--- end icon layer details.

    $(document).on("MapResize", function () {
        if (typeof google === 'object' && typeof google.maps === 'object') {
            google.maps.event.trigger(map, 'resize');
        }
    });
    // Desktop Route Planner 
    $(".myRouteBtn").on("click", function (evt, data) {     
        if (Modernizr.mq('(min-width: 993px)') && !locationBtn.is(":visible")){
           locationBtn.show();
        }

        $(".sideBarGroup, #sideBar").show();

        if (data == undefined) {
            $("#RoutesTab > a").trigger("click");
        }

        $('.mapPage .sideBarColContainer').show();
        $(".mobileLocationBar").hide();

        if (data && data.skipPtBSetup ? false : true) { 
            //Have to set point B peroperly when myRouteBtn is clicked, and it has a location.
            //in case the old value is the same as new, have to clear out the input of endLocationText.
            $("#endLocationText").val('');
            //this focusout is done to allow "focusout" event to run within routePlannerAntucomplete.js, and set it's locaiton to blank.
            $("#endLocationText").focusout();

            var mapLocationValue = $("#mapLocation").val();
            if (mapLocationValue.length > 0) {
                $("#endLocationText").val(mapLocationValue);
            }
            //this focusout is done to allow "focusout" event to run within routePlannerAntucomplete.js, and set it's location to the new location.
            $("#endLocationText").focusout();
            $("#startLocationText").focus();     
        }    
           

    });

    // mobile First
    $(".UIControls .directions").on("click", function (evt, data) {     
        $(".mobileFirst .mobileSetting").hide();
        $(".showSideBar").hide();
        var mapLocationValue = $("#mapLocation").val();
        if (mapLocationValue.length > 0) {
            $("#endLocationText").val(mapLocationValue);
        }
        $(".sideBarGroup").show();
        $("#sideBar").show();
        if (data == undefined) {
            $("#RoutesTab > a").trigger("click");
        }

        $("#startLocationText").focus();
        $('.mapPage .sideBarColContainer').show();
        $(".mobileLocationBar").hide();

        if ($("#routingResults").is(":visible")) {
            $(document).trigger("checkEventOnRoutePlanner");
        }
    });
    $(".clearLocateBtn").on("click", function () {   
        $(this).hide();   
        $("#mapLocation").val("");
        if (mapLocationMarker) {
            mapLocationMarker.setVisible(false);
        }
    });
    $(".closeSettings").on("click", function () {
        $(".mobileFirst .mobileSetting").hide();
        $('#layerSelection').hide();
        console.log("close???");
    });
    $(".myCamerasContainer .fa-times, .myCam").on("click", function () {
        if (resources.MobileCamBtnLink === '') {
            var camGroup = $(".myCamerasGroup");
            if (camGroup.is(":visible")) {
                camGroup.hide();
            } else {
                window.DisplayMyCameras();
                camGroup.show();
            }
        } else {
            window.location.href = window.location.origin + resources.MobileCamBtnLink;
        }       
    });

    // behaves like autocomplete - click on geo locate icon, textbox populates and zoom to location with red dot marker
    $('#setCurrentLocation').click(function (e) {
        window.GetUserGeolocation(function (latlng) {
            if (mapLocationMarker) {
                mapLocationMarker.setVisible(false);
            }
            $(document).trigger('removeMyLocationMarker');

            var geocoder = new google.maps.Geocoder;
            geocoder.geocode({ 'location': latlng }, function (results, status) {
                if (status === 'OK') {
                    if (results[0]) {
                        map.setZoom(17);
                        $("#mapLocation").val(results[0].formatted_address);
                        map.setCenter(results[0].geometry.location);
                        mapLocationMarker.setPosition(results[0].geometry.location);
                        mapLocationMarker.setVisible(true);

                        $(".mobileLocationBar .clearLocateBtn").show();
                    }
                } else {
                    bootbox.alert(resources.CouldNotFindCurrentLocation);
                }
            });
        });
        e.preventDefault();
        return false;
    });  

    $(".navbar-toggle").on("click", function () {
        var isMyCamGrpVisible = $(".myCamerasGroup").is(":visible");
        if (isMyCamGrpVisible) {
            $(".UIControls .myCam").trigger("click");
        } 
    });

    $(document).on("ChangeCursorToWait", function () {
        map.setOptions({ draggableCursor: 'wait' });
    });

    $(document).on("ChangeCursorToDefault", function () {
        map.setOptions({ draggableCursor: '' });
    });

    $(document).on("hideLocationMarker", function () {
        if (mapLocationMarker) {
            mapLocationMarker.setVisible(false);
        }
    });    

    window.$mapping = {
        init: init,
        map: getMap
    };    
}($, window));

$(window).on('load', function () {
    var isEmergencyPopupModal = $(".emergencyAlertModal.in").length > 0;
    if (!isEmergencyPopupModal && resources.FeatureSlideModal == "True") {
        $(document).trigger("displayFeatureModal");
    }

    var isMobileFirst = $("body").hasClass("mobileFirst") && !$(".ersLogo").length > 0;
    if (isMobileFirst) {
        $(".featureModal").on("shown.bs.modal", function () {
            var isBootboxOpen = $(".bootbox").is(":visible");           
            if (isBootboxOpen) {
                $(".featureModal").modal("hide");
            }
        });
    }    
});

//A fix to avoid z-index issues when multiple modals are open
$(document).on('show.bs.modal', '.modal', function () {
    const zIndex = 1040 + 10 * $('.modal:visible').length;
    $(this).css('z-index', zIndex);
    setTimeout(() => $('.modal-backdrop').not('.modal-stack').css('z-index', zIndex - 1).addClass('modal-stack'));
});
var AppCookie = function (options, possibleLayerIds) {
    var publicItem = {};

    var currentCookie = {
        selectedLayers: null,
        prevZoom: null,
        prevLatLng: null,
        mapView: null
    };

    var init = function () {
        //get cookie.
        currentCookie = this.getCookie();

        //Set selectedLayers from the MapModel.
        currentCookie.selectedLayers = options.SelectedLayers;
    };

    var saveCookie = function () {
        //convert to string and save.
        var cookieAsText = JSON.stringify(currentCookie);      
        Cookies.set('map', cookieAsText, { expires: 365, path: '/' });
    };

    getCookie = function () {
        //attempt to get cookie.
        var cookieAsText = Cookies.get('map');
        if (cookieAsText) {
            currentCookie = JSON.parse(cookieAsText);
        }

        return currentCookie;
    };

    publicItem.getCookie = getCookie;

    publicItem.SetLayer = function (layerId, checked) {
        //if checked and not already in collection.
        if (checked && currentCookie.selectedLayers.indexOf(layerId) == -1) {
            currentCookie.selectedLayers.push(layerId);
        } else if (!checked) {
            currentCookie.selectedLayers = $.grep(currentCookie.selectedLayers, function (selectedLayer) {
                return selectedLayer != layerId;
            });
        }

        //save cookie.
        saveCookie();
    };

    publicItem.SetMap = function (latitude, longitude, zoom) {
        currentCookie.prevLatLng = [latitude, longitude];
        currentCookie.prevZoom = zoom;
        currentCookie.mapView = moment.utc();
        //save cookie.
        saveCookie();
    };

    publicItem.RestoreCheckboxes = function (checkLayerIdFctn, callback) {
        //check each previously selected checkbox.
        $(currentCookie.selectedLayers).each(function (index, value) { checkLayerIdFctn(value); });

        //if callback defined.
        if (callback) {
            callback();
        }
    };

    // save change to route options - avoid tolls checkbox
    $("#avoidTollsCheckBox").on("change", function () {
        var isChecked = $("#avoidTollsCheckBox").is(":checked");
        currentCookie.AvoidTolls = isChecked;
        saveCookie();
    });

    $("#avoidFerriesCheckBox").on("change", function () {
        var isChecked = $("#avoidFerriesCheckBox").is(":checked");
        currentCookie.AvoidFerries = isChecked;
        saveCookie();
    });

    init();

    return publicItem;
};
//for layer selector input events. 
var AppEventBinding = function (appHelper) {
    var screenWidthForERSLegend = 768;
    var init = function () {

        //set-up route planner visibility toggle.
        setUpRoutePlanner();

        //set-up open and close for layer selector.
        setUpLayerSelector();

        //set-up map state cookie.
        setUpCookieCheckboxes();
        setUpCookieMap();

        //child layer checkboxes.
        setupChildLayers();

        //Center and zoom set from MapModel on app.init().
        //restore selected layers.
        var isEmbedded = $("#embedmap");
        var isAccessService = $('#accessServicesMap');

        if ((!isEmbedded.length > 0) && (!isAccessService.length > 0)) {
            appHelper.appCookie.RestoreCheckboxes(function (layerId) {
                $('input[type=\'checkbox\'][data-layerid=\'' + layerId + '\'][disabled!=disabled]', $('#layerSelection')).prop('checked', true).change();
                //for child checkboxes.

                //don't display child lists with no visible children
                if ($('ul#' + layerId + "-children").children(':visible').length > 0) {
                    $('ul#' + layerId + "-children").toggle(true);
                }
            }, function () { appHelper.layerSelectorClosed(true); });
        } else {
            appHelper.layerSelectorClosed(true);
        }
    };

    $(document).on('appInitComplete', () => {

        //Check  URL params for legend and route planner state
        let url = new URL(window.location);
        let urlParams = new URLSearchParams(url.search);

        //setup initial route planner visibility based on URL params
        if (urlParams.has("hiderouteplanner") && urlParams.get("hiderouteplanner") === 'true') {
            $('.hideSideBar').click();
        } else if (urlParams.has("hiderouteplanner") && urlParams.get("hiderouteplanner") !== 'true') {
            $('.showSideBar').click();
        }

        //setup initial legend visibility based on URL params
        //this overrides resources.MapLegendDisplayByDefault which is set by a web config
        if (urlParams.has("hidelegend")
            && urlParams.get("hidelegend") === 'true') {
            $("#layerSelection").hide();
        } else if (urlParams.has("hidelegend")
            && urlParams.get("hidelegend") !== 'true') {
            $("#layerSelection").show();
        }
    });

    var setUpCookieCheckboxes = function () {
        var layerSelector = $('#layerSelection');

        $('input[type=\'checkbox\'][data-layerid]', layerSelector).bind('click.layerSelectorCheckboxes_cookie', function () {
            //update cookie.
            appHelper.appCookie.SetLayer($(this).attr('data-layerid'), $(this).is(':checked'));
        });
    };

    var setUpCookieMap = function () {
        var map = appHelper.map;

        var updateCookie = function () {
            //get map details.
            var center = map.getCenter();
            var zoom = map.getZoom();

            //set cookie values related to map.
            appHelper.appCookie.SetMap(center.lat(), center.lng(), zoom);
        };

        //call updateCookie on zoom or pan.
        google.maps.event.addListener(map, 'zoom_changed', updateCookie);
        google.maps.event.addListener(map, 'dragend', updateCookie);
    };

    var setUpDesktopCheckboxes = function () {
        var layerSelector = $('#layerSelection');
        $('input[type=\'checkbox\']', layerSelector).bind('click.layerSelectorCheckboxes_desktopMode', function () {
            appHelper.layerToggled($(this).attr('data-layerid'), false);
        });
    };


    var setUpMobileCheckBoxesHandlers = function () {
        var layerSelector = $('#layerSelection');
        $('input[type=\'checkbox\']', layerSelector).on('change.layerSelectorCheckboxes_mobileMode', function () {
            appHelper.layerToggled($(this).attr('data-layerid'), false);
        });
    };


    var setUpLayerSelector = function () {
        //for the layer selector open link.
        var toggleSelector = $('.legend-toggle');
        var mobileToggleSelector = $('.settingToggle');
        var isMobileFirst, isErs;
        if ($(window).outerWidth() > screenWidthForERSLegend) {
            setUpDesktopCheckboxes();
        } else {
            setUpMobileCheckBoxesHandlers();
        }
        //toggleSelector = isMobileFirst && (Modernizr.mq('(max-width: 992px)')) ? $('.mobileFirst .settingToggle') : toggleSelector;
        $(toggleSelector).on('click', function (e) {
            layerSelector(e, toggleSelector);
        });
        $(mobileToggleSelector).on('click', function (e) {
            layerSelector(e, null);
        });
    };

    var layerSelector = function (e, toggleSelector) {
        var isErs = $(".ersLogo").length > 0;
        var isMobileFirst = ($(".mobileFirst").length > 0) && !isErs;
        var isWta = $(".wtaPage").length > 0;
        var screenWidth = $(window).outerWidth();
        //get context.
        var layerSelector = $('#layerSelection');

        //display the layer selector.
        if (layerSelector.css('display') == 'none') {
            //get screen width in pixels.
            if (resources.ScreenWidthERSLegend !== "768") {
                screenWidthForERSLegend = parseInt(resources.ScreenWidthERSLegend);
            }
            //if desktop size.
            if (screenWidth > screenWidthForERSLegend) {
                //set css class and place layer selector in correct spot for desktop.
                layerSelector.attr('class', 'layerSelection');
                $('#legend-container').append(layerSelector.detach());

                //handle checkbox clicks differently.
                 //if (isMobileFirst && screenWidth < 993) {
                    $(".mobileSetting").show();
                //}
            } else {
                if (isMobileFirst) {
                    $(".mobileFirst .mobileSetting").show();
                    //set css class and place layer selector in correct spot for desktop.
                    layerSelector.attr('class', 'layerSelection');
                    $('#legend-container').append(layerSelector.detach());

                    if (!isWta) {
                        //calc legend height               
                        var legendContainerHeight = $(".legend-container").outerHeight() + 20;
                        adjustLegendHeight(legendContainerHeight);
                    } else {
                        $(".mobileSetting").addClass("show");
                    }

                    //handle checkbox clicks differently.
                } else {
                    //set css class for mobile.
                    layerSelector.attr('class', 'layerSelectionModal');
                    //detach the layer selector.
                    var detached = layerSelector.detach();

                    //create modal.
                    bootbox.mapPageDialog({
                        message: ' ',
                        animate: false,
                        closeButton: false,
                        buttons: {
                            main: {
                                label: resources.OK,
                                className: 'btn-info',
                                container: '.map-container',
                                callback: function () {
                                    //"click" close button.
                                    toggleSelector.trigger('click');

                                    //on close, return the layer selector to its original location so we don't lost it.
                                    $('#legend-container').append(detached);

                                    //trigger changes.
                                    appHelper.layerSelectorClosed(true);
                                }
                            }
                        }
                    });

                    //set modal contents to layer selector. if we do not call show() then when bootbox is closed layerSelector.css('display') == 'none' is true for some reason.
                    bootbox.setDialogToJqueryObj(detached.show());
                }              
            }
            // ERS Legend Filter
            if ($(".mapFiltersContainer").css("display") == "block") {
                $(".layerSelection").addClass("expand");
            }           

            //show the layer selector.
            layerSelector.show();
            e.preventDefault();
        } else {
            //close the layer selector.
            e.preventDefault();
            layerSelector.hide();

            if (isMobileFirst && $("#embedmap").length === 0 && window.innerWidth < 992) {
                $(".mobileFirst .mobileSetting").hide();
            }
            if (isWta) {
                $(".mobileSetting").removeClass("show");
            }           
        }
    };
    var locationBtn = $(".locationBtn");
    var setUpRoutePlanner = function () {
        //hide the route planner.
        $('.hideSideBar').click(function () {
            $('.mapPage .sideBarColContainer').hide();
            $('.guideIcon').hide();
            $('.mapColContainer').toggleClass('full');            
            if (locationBtn.length > 0) {
                locationBtn.addClass("shiftLeft");
            }        
            $('.showSideBar').show(100);
            appHelper.resizeMap();
        });

        //show the route planner.
        $('.showSideBar').click(function () {
            $('.showSideBar').hide(100);
            if (locationBtn.length > 0) {
                locationBtn.removeClass("shiftLeft");
            }
            $('.mapPage .sideBarColContainer').show();
            $('.guideIcon').show();
            $('.mapColContainer').toggleClass('full');
            appHelper.resizeMap();
        });

        // mobile first - close side bar
        $('.closeSideBar, .locationBtn').on("click", function () {
            // resetRoutePlanner
            $(document).trigger('clearUserRouteTrigger');
            
            if (locationBtn.length > 0) {
                locationBtn.hide();
                locationBtn.removeClass("shiftLeft");
            }
            $(".showSideBar").hide();
            $('.mapPage .sideBarColContainer').hide();
            $('.mapColContainer').toggleClass('full');
            $('.mobileLocationBar').show();
            $("#mapLocation").val("").focus();
            appHelper.resizeMap();
        });
    };

    var setupChildLayers = function () {
        //find all ul elements in the layerSelection
        //attach click events to display the ul on parent selection
        var layerSelector = $('#layerSelection');
        var checkbox = $('input[type=\'checkbox\'][data-icon],input[type=\'checkbox\'][data-feedurl],input[type=\'checkbox\'][data-tileurlformat]', layerSelector);
        checkbox.each(function (i, thisCheckbox) {
            var item = $(thisCheckbox);
            var layerId = item.attr('data-layerId');

            //does this checkbox have a corresponding ul?
            var ul = $('ul#' + layerId + "-children");
            if (ul.length) {
                $(thisCheckbox).change(function () {
                    //check or uncheck each child.
                    var checked = this.checked;
                    var dontSelectChildLayers = $(this).attr('data-dontselectchildlayers');
                    if (dontSelectChildLayers != undefined && dontSelectChildLayers.toLowerCase() === 'true' && checked) {
                        var input = $(ul[0].children[0]).find('input');
                        //This is a hack, we need to fix this so that don't select child layers makes sense.
                        if (ul[0].children.length > 1) {
                            if ((checked && !input.is(':checked')) || (!checked && input.is(':checked'))) {
                                input.click();
                            }
                        }
                    } else {
                        ul.children().each(function () {
                            var input = $(this).find('input');

                            if ((checked && !input.is(':checked')) || (!checked && input.is(':checked'))) {
                                input.click();
                            }
                        });
                    }

                    var children = ul.find('input');
                    ul.hide();
                    children.each(function () {
                        var item = $(this);
                        if (checked) {
                            if (item.attr('data-visible') == "True") {
                                item.show();
                                ul.show();
                            }
                        } else {
                            item.hide();
                        }
                    });
                });
            }
        });
    };

    //initialize.
    init();
};
var AppHelper = function (map, options, layerInfo) {
    //private objects. describe details of layers.
    var iconDetails = layerInfo.iconDetails;
    var apiUrls = layerInfo.apiUrls;
    var tooltipBaseUrls = layerInfo.tooltipBaseUrls;
    var tooltipDraggable = layerInfo.tooltipDraggable;
    var tooltipSize = layerInfo.tooltipSize;
    var feed = layerInfo.feed;
    var tile = layerInfo.tile;
    var icon = layerInfo.icon;
    //describe current active layers.
    var layerIsActive = new Hashtable();

    var googleInfoWindowMaxWidth = 365;

    //marker clusterer, marker clusterer options, ajax cache.
    var markerClusterer, markerClustererOptions, ajaxCache;

    //true if the map is currently being dragged.
    var mapBeingDragged = false;

    //layer managers.
    var iconManager, tileManager, polylineManager, kmlManager;

    //google info window.  Assigning maxWidth
    var googleInfoWindow = new google.maps.InfoWindow({ maxWidth: googleInfoWindowMaxWidth });

    var infoWindowLoading = '<div id="infoWindowLoading">' + resources.Loading + '</div>';

    var draggableOverlay;

    //methods made available to AppEventBinding.
    var publicItem = {};

    //binds certain events related to layer selector.
    var appEventBinding;

    //handles saving and loading of persistent cookie.
    var appCookie;

    //a collection of filters
    var layerFilters = {};

    var layerName;

    var oms;

    var isDraggable = false;


    var init = function () { 
       
        oms = new OverlappingMarkerSpiderfier(map, {
            markersWontMove: false,
            markersWontHide: false,
            keepSpiderfied: true,
            nearbyDistance: 10
        });

        //set marker clusterer options.
        markerClustererOptions = {
            maxZoom: options.ClustererModel.MaximumZoom,
            minimumClusterSize: options.ClustererModel.MinimumClusterSize,
            gridSize: options.ClustererModel.GridSize,
            title: resources.AClusterOfIcons,
            imagePath: "/Content/images/markerClustererPlus/m"
        };

        //init the marker clusterer.
        markerClusterer = new MarkerClusterer(map, [], markerClustererOptions, oms);

        //calculates cluster style and cluster icon text.
        markerClusterer.setCalculator(function (markers, numStyles) {
            var index = 0;
            var title = '';
            var count = markers.length.toString();

            //divide count by 10 till it rounds to 0.
            var dv = count;
            while (dv !== 0) {
                dv = parseInt(dv / 10, 10);

                //increment index. the final value determines the style we will use.
                index++;
            }

            //Use same style for 100 icons as we use for 10 icons. make sure index is in range.
            if (index == 3) index = 2;
            if (resources.MarkererIndexOveride) index = resources.MarkererIndexOveride;
            index = Math.min(index, numStyles);

            //the text of the cluster will be the icon count within the cluster with style determined by index.
            return {
                text: count,
                index: index,
                title: title
            };
        });

        //ajax call cache.
        ajaxCache = new AjaxCache();

        //icon manager.
        iconManager = new IconManager(map, markerClusterer);
        publicItem.iconManager = iconManager;

        //tile manager.
        tileManager = new TileManager(map, publicItem);
        publicItem.tileManager = tileManager;

        //polyline manager.
        polylineManager = new PolylineManager(map);
        $(document).trigger('polylineManagerReady-appHelper', [polylineManager, showInfoWindowForItem]);

        //kml manager.
        kmlManager = new KmlManager(map, publicItem);
        publicItem.kmlManager = kmlManager;

        //set-up cookie methods.
        (function setUpAppCookie() {
            //init app cookie methods.
            appCookie = new AppCookie(options, getAllLayerIds());
        }());

        //add appCookie to publicItem and set-up layer selector.
        appEventBinding = new AppEventBinding(function () {
            publicItem.appCookie = appCookie;
            publicItem.layerSelectorClosed = layerSelectorClosed;
            return publicItem;
        }());

        //set-up auto refresh.
        setUpAutoRefresh();

        //let people know we're done
        $(document).trigger('appHelperInitComplete');
    };//init

    var getFilter = function (filterData) {
        for (var item in filterData) {
            var data = filterData[item];
            if (data.hasFilter == "True" && typeof (FilterDataTableParams) != "undefined") {

                /*
                * TODO: Currently, this won't work for any layers other than Ers because all of the filters will look like the one
                * defined in MapFilters.cshtml (ie. filter on owner, status, type, subtype, and isInternal). To add filters for 
                * additional map layers, we need to create specific filters for their corresponding layers.
                */

                var newFilter = new FilterDataTableParams(1, 0, 25, true, null);
                //search
                newFilter.search = new FilterDataTableSearchParam();
                newFilter.search.objValue = null;
                newFilter.search.regex = false;
                newFilter.search.searchOnColumn = false;
                newFilter.search.value = null;
                //order
                newFilter.order[0] = new FilterDataTableOrderParam();
                newFilter.order[0].column = 0;
                newFilter.order[0].dir = "asc";

                var layerFilter = new ColumnFilters('map-filter-div', data.layerId, 'List/UniqueColumnValuesForErsEvents/{typeId}', newFilter);
                layerFilter.initListColumnFilters();

                //return layerFilter;
                layerFilters[data.layerId] = layerFilter;
            }
        }
    };

    //called when the layer selector is closed or layer selections updated.
    var layerSelectorClosed = function (noLoadSpinner) {
        var checkboxes = $('input[type=\'checkbox\'][data-layerid]', '#layerSelection');

        //for each layer checkbox.
        checkboxes.each(function (idx, checkbox) {
            layerToggled($(checkbox).attr('data-layerid'), true);
        });

        //refresh data and display.
        refreshMapData(noLoadSpinner, true);
    };

    var layerToggled = function (layerId, noRefresh) {
        var layerSelector = $('#layerSelection');

        //generic forEach checkbox function.
        var checkboxEach = function (checkbx, activateFctn, deactivateFctn) {
            var item = $(checkbx);

            //make sure this layer has a key value in layerIsActive
            if (!layerIsActive.containsKey(layerId)) {
                layerIsActive.put(layerId, false);
            }

            //decide if layer must be added or removed.
            if (item.is(':checked')) {
                if (layerIsActive.get(layerId) == false) {
                    layerIsActive.put(layerId, true);
                    activateFctn();
                    if (item.attr('data-conflicts')) {
                        var conflicts = item.attr('data-conflicts').split(',');
                        for (var i = 0; i < conflicts.length; i++) {
                            $('input[type=\'checkbox\'][data-layerid=\'' + conflicts[i] + '\']:checked', $('#layerSelection')).click();
                        }
                    }
                    //Analytics tracking on selection of layer (not on reload)
                    if (!noRefresh && typeof (ga) !== 'undefined') {
                        ga('send', 'event', 'MapLayer', layerId);
                    }
                    $(document).trigger("mapLayerToggle", [layerId, true]);
                }
            } else {
                if (layerIsActive.get(layerId) == true) {
                    layerIsActive.put(layerId, false);
                    deactivateFctn();
                    if (tooltipDraggable[layerId] == "true") {
                        $(document).trigger("closeDraggableWindow");
                    }
                    $(document).trigger("mapLayerToggle", [layerId, false]);                   
                }
            }
        };

        //get the checkbox of the layer id.
        var checkbox = $('input[type="checkbox"][data-layerid="' + layerId + '"]', layerSelector);
        if (checkbox.is('[data-polyline="true"]')) {
            checkboxEach(checkbox[0], function () {
                //add icon layer. refresh if noRefresh is true.
                polylineManager.AddPolylineLayer(layerId, [], $(checkbox[0]).data('polylineopacity'), $(checkbox[0]).data('polylineweight'), 0, 20, function (callback, layerFilter, skipCache) {
                    var icons = updateIcon(layerId, skipCache, layerFilter).done(function (data) {
                        var polylines = [];
                        var items = data.item2;
                        for (var d in items) {
                            if (items[d].polyline) {
                                var iconTmp = null;
                                if (items[d].polyline.symbolJSON) {
                                    iconTmp = JSON.parse(items[d].polyline.symbolJSON);
                                }
                                polylines.push({ id: items[d].itemId, lineColor: items[d].polyline.color, decodedPoints: google.maps.geometry.encoding.decodePath(items[d].polyline.path), icons: iconTmp });
                            }
                        }
                        callback(polylines);
                    });
                }, layerFilters[layerId], checkbox.is('[data-icon]') ? true : false);

                $(document).on('layerRefreshed-polylineManager.' + layerId, function (e, layer) {
                    if (layer == layerId) {
                        $(document).off('layerRefreshed-polylineManager.' + layerId);
                        if (checkbox.is('[data-icon]')) {
                            iconManager.AddIconLayer(layerId, [], updateIcon, function (layerId, mapIconItem) {
                                var marker = iconMarkerBuilder(layerId, mapIconItem);
                                if (mapIconItem.polyline) {
                                    marker.addListener('marker-clustered', function (clustered) {
                                        var polylines = polylineManager.GetPolylines(layerId);
                                        for (var p in polylines) {
                                            var a = polylines[p];
                                            if (a.sourceData.id == mapIconItem.itemId) {
                                                if (marker.clustered) {
                                                    a.gMapLine.setMap(null);
                                                    a.gMapLine.canDraw = false;
                                                } else {
                                                    a.gMapLine.setMap(map);
                                                    a.gMapLine.canDraw = true;
                                                }
                                                break;
                                            }
                                        }
                                    });
                                }
                                return marker;
                            }, layerFilters[layerId], icon[layerId]);
                            iconManager.RefreshLayer(layerId).done(iconManager.Redraw);
                        }
                    }
                });
                polylineManager.RefreshLayer(layerId);

                $('#legend-icon-' + layerId, layerSelector).toggle(true);

            }, function () {
                //remove icon layer.
                polylineManager.RemovePolylineLayer(layerId);
                iconManager.RemoveIconLayer(layerId);
                noRefresh || iconManager.Redraw();
                $(document).off('layerRefreshed-polylineManager.' + layerId);
                $('#legend-icon-' + layerId, layerSelector).toggle(false);
                if (layerId == layerName) closeInfoWindow();
            });
        }
        else if (checkbox.is('[data-icon]')) {
            //determine if layer must be added or removed.
            checkboxEach(checkbox[0], function () {
                //add icon layer. refresh if noRefresh is true.
                iconManager.AddIconLayer(layerId, [], updateIcon, iconMarkerBuilder, layerFilters[layerId], icon[layerId]);
                noRefresh || iconManager.RefreshLayer(layerId).done(iconManager.Redraw);
                $('#legend-icon-' + layerId, layerSelector).toggle(true);
            }, function () {
                //remove icon layer.
                if (currentInfoWindowId.indexOf(layerId) == 0) {
                    currentInfoWindowId = "";
                }

                iconManager.RemoveIconLayer(layerId);
                $('#legend-icon-' + layerId, layerSelector).toggle(false);
                noRefresh || iconManager.Redraw();
                if (layerId == layerName) closeInfoWindow();
            });
        }

        else if (checkbox.is('[data-feedurl]')) {
            //determine if layer must be added or removed. 
           
            checkboxEach(checkbox[0], function () {
                //add kml layer. layer is visible.
                kmlManager.AddLayer(layerId, feed[layerId]);
                $('#legend-feed-' + layerId, layerSelector).toggle(true);

            }, function () {
                if (layerId == layerName && isGoogleInfoWindowOpen()) {
                    closeInfoWindow();
                }
                //remove kml layer. layer is no longer visible.
                kmlManager.RemoveLayer(layerId);           
                $('#legend-feed-' + layerId, layerSelector).toggle(false);

            });
        } else if (checkbox.is('[data-tileurlformat]')) {
            //for each tile layer determine if it needs to be added or removed.
            checkboxEach(checkbox[0], function () {
                //add tile layer. layer is visible.
                tileManager.AddTileLayer(layerId, tile[layerId]);
                //turn on tile legend if it exists.
                $('#legend-tile-' + layerId, layerSelector).toggle(true);
            }, function () {
                //remove tile layer. layer is no longer visible.
                tileManager.RemoveTileLayer(layerId);

                if (layerId == layerName && isGoogleInfoWindowOpen()) {
                    closeInfoWindow();
                }

                //turn off tile legend if it exists.
                $('#legend-tile-' + layerId, layerSelector).toggle(false);
            });
        }

    };

    //returns a deferred promise.
    var updateIcon = function (layerId, skipCache, layerFilter) {
        //ajax url and $.ajax options.
        var url = apiUrls[layerId];
        var ajaxOptions = {
            type: 'GET', url: url, dataType: 'json', timeout: 30000, tryCount: 0,
            retryLimit: 3, error: function (x, t, m) {
                //if we get a timeout, should try again. 
                if (t == "timeout") {
                    this.tryCount++;
                    if (this.tryCount <= this.retryLimit) {
                        //try again
                        $.ajax(this);
                        return;
                    } else {
                        var settings = this;
                        if (typeof Bugsnag !== "undefined") {
                            Bugsnag.notify("AjaxError", function (event) {
                                event.context = t + " " + settings.url.split("?")[0];
                                event.setMetadata('ajaxData', { url: settings.url, type: settings.type, data: settings.data ? JSON.parse('{"' + decodeURIComponent(settings.data).replace(/\+/g, ' ').replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') + '"}') : {} });
                            });
                        }
                    }
                }
            }
        }; //30000 milliseconds is 30 seconds.

        //update the ajaxOptions if we are filtering.
        if (layerFilter != null) {
            ajaxOptions.type = 'POST';
            ajaxOptions.data = JSON.stringify(layerFilter.getFilter());
            ajaxOptions.contentType = 'application/json';
            ajaxOptions.url += '?time=' + $.now();
        }

        //checks cache, if not present then performs ajax call. 
        return ajaxCache.Get(layerId, .5, ajaxOptions, skipCache);
    };

    var getSpiderfyUrl = function (url) {
        if (url == undefined) return null;
        var s = url.split("/");
        var image = s.splice(s.length - 1, 1)[0];
        return s.join("/") + "/plus/" + image;
    };

    //passed to icon manager.
    var iconMarkerBuilder = function (layerId, mapIconItem) {

        //get position of marker.
        var position = new google.maps.LatLng(mapIconItem.location[0], mapIconItem.location[1]);

        //create and add associate with map
        var marker = new google.maps.Marker({
            clickable: mapIconItem.icon.isClickable,
            position: position,
            title: mapIconItem.title,
            zIndex: mapIconItem.zindex,
            preventClustering: mapIconItem.icon.preventClustering
        });

        //Work out if it's got an icon
        var icon = getIcon(layerId, mapIconItem);
        if (icon) {
            marker.setIcon(icon);
        }

        //Change marker for Spiderfiable
        google.maps.event.addListener(marker, 'spider_format', function (status) {

            //Work out if it's got an icon
            var icon = getIcon(layerId, mapIconItem);
            var iconUrl = status == OverlappingMarkerSpiderfier.markerStatus.SPIDERFIED ? icon.url :
                status == OverlappingMarkerSpiderfier.markerStatus.SPIDERFIABLE ? getSpiderfyUrl(icon.url) :
                    status == OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIABLE ? icon.url :
                        null;
            if (icon) {
                icon.url = iconUrl;
                marker.setIcon(icon);
            }
        });

        //Do not show tooltip if the base url is empty
        if (tooltipBaseUrls[layerId] != "") {
            //tooltip url is now handled differently - mick.
            attachMarkerClickHandler(marker, mapIconItem, layerId);

            if (resources.TooltipDisplayOnHover === 'true') {
                attachMarkerHoverHandler(marker, mapIconItem, layerId);
            }
        }

        return marker;
    };

    //used by iconMarkerBuilder.
    var getIcon = function (layerId, mapIconItem) {
        //if this item has a custom icon.
        if (mapIconItem.icon.json) {
            return JSON.parse(mapIconItem.icon.json);
        }
        else if (mapIconItem.icon) {
            var customIcon = mapIconItem.icon;

            //build required icon object.
            return {
                url: customIcon.url,
                size: new google.maps.Size(customIcon.size[0], customIcon.size[1]),
                anchor: new google.maps.Point(customIcon.anchor[0], customIcon.anchor[1]),
                origin: new google.maps.Point(customIcon.origin[0], customIcon.origin[1]),
                scaledSize: new google.maps.Size(customIcon.size[0], customIcon.size[1])
            };
        }

        //return layer icon object.
        return iconDetails[layerId];
    };

    var attachMarkerClickHandler = function (marker, mapIconItem, layerId) {
        google.maps.event.addListener(marker, 'spider_click', function (e) {
            //display tooltip.
            showInfoWindowForItem(layerId, mapIconItem.itemId, marker);
            //Used in SnowPlowHistory.js
            $(document).trigger('marker-click', [publicItem, layerId, mapIconItem.itemId, marker]);
        });
    };

    var showInfoWindowTimerHandle;
    var attachMarkerHoverHandler = function (marker, mapIconItem, layerId) {
        google.maps.event.addListener(marker, 'mouseover', function (event) {
            //display tooltip.
            showInfoWindowTimerHandle = setTimeout(function () { google.maps.event.trigger(marker, 'click'); }, 500);
        });

        google.maps.event.addListener(marker, 'mouseout', function () {
            //If we mouseout before showing the tooltip, cancel the tooltip display.
            if (showInfoWindowTimerHandle) {
                clearTimeout(showInfoWindowTimerHandle);
            }
        });
    };

    var msgTimer = null;
    var currentInfoWindowId = '';

    //marker can actually be any google maps MVCObject.
    var showInfoWindowForItem = function (layerId, itemId, marker, latLng) {
       
        //Don't re-open the infowindow if it's already open.
        var newInfoWindowId = layerId + itemId;
        if (!isGoogleInfoWindowOpen() || newInfoWindowId !== currentInfoWindowId) {
            $(document).trigger("closeDraggableWindow");
            if (isGoogleInfoWindowOpen()) closeInfoWindow();
            currentInfoWindowId = newInfoWindowId;

            /*
                we do this to handle the case where a GOOGLE info window is open and the user clicks another icon. in this scenario 
                the info window never actually closes and the closeclick event never fires so any timers that may exist do not get
                cleared. also, it really can't hurt to call this.
            */
            
            if (resources.CctvEnableVideo == "True") {
                $(document).trigger("CallRemoveVideo");
            } 
            
            clearAllInfoWindowTimers();            

            var langSelected = Cookies.get("_culture") ? Cookies.get("_culture") : "";
            //url is constructed differently - mick.
            var url = URI.expand(tooltipBaseUrls[layerId], { layerId: layerId, id: itemId, lang: langSelected }).toString();
            var isMobile = $(window).width() < 768;
            // Problem with Draggable tooltip in mobile, doesn't zoom to the tooltip since it is huge
            isDraggable = tooltipDraggable[layerId] == "true" && !isMobile;
           
            //set loading... message for google info window.
            showInfoWindow(infoWindowLoading, marker, true, latLng, layerId, false);           
         
            $(document).trigger('ShowInfoWindow', [publicItem, layerId, itemId, marker, latLng]);
            //load content.
            $.ajax({
                url: url,
                success: function (content) {
                    if (currentInfoWindowId == newInfoWindowId) {
                        showInfoWindow(content, marker, false, latLng, layerId, isDraggable);
                    }
                }
            });
        }
    };

    const centerTooltip = new CenterTooltip(map);  
    centerTooltip.startTriggers();

    $(document).on('info-content-trigger', (event, layerId, content, infoWindowObj) =>  {
        //some special cases we have to consider.
        if (layerId === resources.MessageSignsLayerId) {
            rotateMsgSigns();
        }
        if (typeof content != "undefined") {
            if (content.indexOf("cctvImage") != -1 || content.indexOf("cctvCameraCarousel") != -1) {
                $(document).trigger("cameraImagesInitialized", [currentInfoWindowId]);
            }
            if (content.indexOf("data-convert-from-utc") != -1) {
                $(document).trigger("update-time-to-locale");
            }
        }
        let detourDiv = $('#detourCollapse');
        if (detourDiv[0]) {
            //add arrows to detour steps
            arrowDirection.setTooltip(detourDiv[0]);
        }

        //check if full screen camera modal needs to be setup.
        let imgSrc1 = document.querySelector('.map-tooltip .slick-track'),
            imgSrc2 = document.querySelector('.map-tooltip .singleCamTd');
        if (imgSrc1) {
            $(document).trigger('setup-fullscreen-img-modal', [imgSrc1,'multiCam']);
        }
        if (imgSrc2) {
                 $(document).trigger('setup-fullscreen-img-modal', [imgSrc2,'singleCam']);
                }
        // call from StringHelper.ts
        applyDetourArrowIcon();
        centerTooltip.run(infoWindowObj);
    });

    $(document).on('update-time-to-locale', function () {
        $("[data-type='time']").each(function (i, val) {
            var $td = $(val);
            var date = $td.html();
            var toLocale = moment.utc(date).local().format('M/D/YYYY h:mm A');
            $td.html(toLocale);
        });
    });

    var rotateMsgSigns = function () {
        //always clear timer, we don't want to lose track of an existing one
        clearInterval(msgTimer);
        //if there is more than one div then there is more than one message.
        if ($(".msgContent div").length > 1) {
            //every 2.5 seconds rotate the message. assumes max 2 messages.
            msgTimer = setInterval(function () {
                var divs = $(".msgContent").children();
                var count = divs.length;
                var next = false;
                for (var i = 0; i < count; i++) {
                    if (!$(divs[i]).hasClass("hide")) {
                        $(divs[i]).toggleClass("hide");
                        next = true;
                    }
                    else if (next == true) {
                        $(divs[i]).toggleClass("hide");
                        next = false;
                        break;
                    }
                }
                if (next == true) {
                    $(divs[0]).toggleClass("hide");
                }
            }, 2500);
        }
    };

    //undoes what rotateMsgSigns does.
    var clearRotateMsgSigns = function () {
        clearInterval(msgTimer);
        msgTimer = null;
    };

    var clearAllInfoWindowTimers = function () {        
        //clear any message sign rotating timers.
        clearRotateMsgSigns();      
        //turn off camera image refresh.
        $(document).trigger("cameraImagesClearIntervals");
    };

    //clear info window timers on close of GOOGLE info window and some other cleanup.
    google.maps.event.addListener(googleInfoWindow, 'closeclick', function () {       
        closeInfoWindow();
    });  

    //trigger closeclick event of google info window on close of MODAL info window.
    $(document).on("hidden.bs.modal", ".bootbox.modal", function (e) {
        google.maps.event.trigger(googleInfoWindow, 'closeclick');
    });

    $(document).on("layer-changed", function (e, layer) {
        if (layer == 'MyCameras') {
            //clear cache so when a new camera is added it shows on my cameras layer.
            ajaxCache.Remove(layer);
        }
        iconManager.RefreshLayer(layer, true).done(iconManager.Redraw);
        polylineManager.RefreshLayer(layer, true);
    });

    var refreshMapData = function (noLoadSpinner, noTileRefresh) {
        //show loading spinner.
        noLoadSpinner || loadBlockerApi.showSpinner('refreshMapData');

        var eventCount = 0;
        $(document).on('markerClustererRepainted.appHelper layersRefreshed-polylineManager.appHelper', function () {
            if (++eventCount == 2) {
                //hide spinner.
                loadBlockerApi.hideSpinner('refreshMapData');
                $(document).unbind('markerClustererRepainted.appHelper layersRefreshed-polylineManager.appHelper');
            }
        });

        $(document).on('layersRefreshed-iconManager.appHelper', function () {
            iconManager.Redraw();
            $(document).unbind('layersRefreshed-iconManager.appHelper');
        });

        //above code will turn off spinner when appropriate.
        //We'll probably want to find a better way of refreshing layers that respect the map filters, but this will do for now
        iconManager.RefreshLayers();
        polylineManager.RefreshLayers();

        //no need to listen for when this finishes
        noTileRefresh || tileManager.RefreshLayers();
            
        kmlManager.Refresh(feed);
    };

    var repaintMap = function (noLoadSpinner) {
        //show loading spinner.
        noLoadSpinner || loadBlockerApi.showSpinner('repaintMap');

        var eventCount = 0;
        $(document).on('mapChangedFinished-polylineManager.appHelper markerClustererRepainted.appHelper', function () {
            if (++eventCount == 2) {
                //hide spinner.
                loadBlockerApi.hideSpinner('repaintMap');
                $(document).unbind('mapChangedFinished-polylineManager.appHelper markerClustererRepainted.appHelper');
            }
        });

        //MapChanged is synchronous, so redraw can directly follow.       
        iconManager.MapChanged();
        iconManager.Redraw();       

        polylineManager.MapChanged();
    };

    //set-up automatic map refresh.
    var setUpAutoRefresh = function () {
        var refresh = { timer: null, refreshData: false };

        /*
         * this function delays refreshing for a second so that if 
         * multiple refresh request occurr in a short time, only one 
         * will fire. 
         */
        var delayRefresh = function (refreshData) {
            //if true once, takes precedence over subsequent false.
            if (refreshData) {
                refresh.refreshData = refreshData;
            }

            //reset timer if exists.
            clearTimeout(refresh.timer);

            //delay .3 seconds in case another refresh request comes in.
            refresh.timer = setTimeout(function () {
                refresh.refreshData ? refreshMapData(true) : repaintMap(true);

                //reset timer and refreshIconData parameter.
                refresh.timer = null;
                refresh.refreshData = false;
            }, 300);
        };

        //refresh map every 61 seconds. 61 so that we don't just hit the default cache (60s) again
        setInterval(function () { isItAGoodTimeToRefresh() && delayRefresh(true); }, 61000);       
        //listen to google map events.
        google.maps.event.addListener(map, 'zoom_changed', function () { 
            closeInfoWindow();
            delayRefresh(false);
            if (resources.MinimumZoom > 0) {
                if (map.getZoom() < resources.MinimumZoom) map.setZoom(Number(resources.MinimumZoom));
            }                      
        });
        google.maps.event.addListener(map, 'bounds_changed', function () {
            delayRefresh(false);
        });
        google.maps.event.addListener(map, 'dragstart', function () {
            mapBeingDragged = true;
        });
        google.maps.event.addListener(map, 'dragend', function () {
            mapBeingDragged = false;
        });

        //prevent panning of the edge of the map in latitude direction.
        noPanOffEarth(map);
    };

    //returns true if icon layerId is active.
    var isLayerIdChecked = function (layerId) {
        return $('input[type="checkbox"][data-layerId="' + layerId + '"]').is(':checked');
    };
    var isItAGoodTimeToRefresh = function () {
        //is modal open?
        var modalOpen = bootbox.isOpen();

        //if both are false and map is not being dragged.
        return !modalOpen && !mapBeingDragged;
    };

    var isGoogleInfoWindowOpen = function () {
        return googleInfoWindow.getMap() ? true : false;
    };              

    //only one of googleMarker and latLng need to be specified. however it is ok if both are. - uh this is not the case anymore.
    var showInfoWindow = function (content, googleMarker, openWindow, latLng, layerId, isDraggable, exactPosition, pixelOffset) {
        layerName = layerId;
        //dont show if there is nothing to show
        if (!layerId || content === "") return;

        var typeMaxWidth = tooltipSize[layerId] && tooltipSize[layerId].length > 0 ? parseInt(tooltipSize[layerId]) : googleInfoWindowMaxWidth;
        var isDesktop = $(window).width() > 992;
        var infoWindowObj = googleInfoWindow; //default is Google Info window

        //set info content. Not using Draggable in tablet down because the content are sometime too large to fit in a map tooltip
        if (isDraggable && isDesktop) { 
            //make sure we close the Loading... info window
            googleInfoWindow.close();
            var DraggableInfoWindow = CreateDraggableInfoWindow(draggableOverlay);
            var markerWithPosiiton = googleMarker != null ? googleMarker : new google.maps.Marker({ position: latLng });
            draggableOverlay = new DraggableInfoWindow(map, markerWithPosiiton, layerId, content, typeMaxWidth, function () {
                if (content && content != "") {
                    $(document).trigger('info-content-trigger', [layerId, content]);
                }
            });

            draggableOverlay.position = markerWithPosiiton.position;
            draggableOverlay.content = content;

            infoWindowObj = draggableOverlay; //change window to draggable window
        } else {
            google.maps.event.addListenerOnce(googleInfoWindow, 'domready', function () {
                if (content) {
                    if (openWindow) { //if loading screen
                        centerTooltip.run(infoWindowObj);
                    } else {
                        $(document).trigger('info-content-trigger', [layerId, content, infoWindowObj]);
                    }
                }
            });
            googleInfoWindow.setContent(content);
        }       
        
        bootbox.setDialogContent(content);
        if (window.location.pathname.includes("EmbeddedMap")){
            $(document).trigger('info-content-trigger', [layerId, content]);
        }

        infoWindowObj.maxWidth = typeMaxWidth; 
        //if we are to open the window.
        if (openWindow) {
            //get screen width in pixels.            

            //if not mobile size.
            if (isDesktop) {              

                if (googleMarker || exactPosition) {                    
                    if (isDraggable) {
                        googleInfoWindow.close();
                        infoWindowObj.position = latLng != undefined ? latLng : googleMarker.position;
                    } else {
                        googleInfoWindow.setOptions({ position: latLng, pixelOffset: { height: 0 } });
                    }
                }
                else {
                    if (!pixelOffset) {
                        pixelOffset = { height: -32 }
                    }
                    googleInfoWindow.setOptions({ position: latLng, pixelOffset: pixelOffset });
                }

                //Set position for "segment" polylines
                if (googleMarker && googleMarker.latLngs) {
                    googleInfoWindow.setOptions({
                        position:
                        new google.maps.LatLng(
                            /*googleMarker.getPath().getArray()[0].lat()*/googleMarker.defaultPosition.lat(),
                            /*googleMarker.getPath().getArray()[0].lng()*/googleMarker.defaultPosition.lng()
                        )
                    });
                    googleInfoWindow.open(map);
                }
                else {                 
                    if (!isDraggable) {  
                        $(document).trigger("closeDraggableWindow");
                        googleInfoWindow.open(map, googleMarker);                      
                    }
                }

                if (layerId === resources.MessageSignsLayerId) {
                    $(document).trigger("MsgSignMapTooltipTriggered"); // Trigger Msg Sign Map Tooltip
                }

            } else {

                //if modal is not already open.
                if (!bootbox.isOpen()) {
                    bootbox.mapPageDialog({
                        message: content,
                        animate: false,
                        buttons: {
                            main: {
                                label: resources.OK,
                                className: 'btn-info'
                            }
                        }
                    });
                }
            }

        } else if (isGoogleInfoWindowOpen()) {

            //get all images in the tooltip.
            var tooltipImages = $('.map-tooltip table img:not(.agencyLogo)');           
            var openfor = currentInfoWindowId;
            //if there are images in the tooltip.
            if (tooltipImages.length) {               
                //every time an image loads.
                tooltipImages.on('load', function () {          
                    //open again to trigger auto-pan of map so the infoWindow is fully visible
                    if (openfor == currentInfoWindowId) {
                        googleInfoWindow.open(map, googleMarker);
                        tooltipImages.off("load");
                    }                                                     
                });                
            } else {
                //open again to trigger auto-pan of map so the infoWindow is fully visible.
                googleInfoWindow.open(map, googleMarker);
            }         

        } else {
            if (!isDraggable) {                
                if (content && content != "") $(document).trigger('info-content-trigger', [layerId, content]);
            }            
        }      
    };

    var closeInfoWindow = function () {
      
        if (resources.CctvEnableVideo == "True") {
            // works on Draggable but not on googleinfowindow as they close right away and the dom are destroyed
            $(document).trigger("CallRemoveVideo"); 
        }        
       
        $(document).trigger("closeDraggableWindow");
       
       googleInfoWindow.close();
        //change current window ID otherwise you cant reopen it until you click on a different icon
        currentInfoWindowId = '';
        $(document).trigger('info-window-close');
    };

    var clearInfoWindowId = function () {
        currentInfoWindowId = '';
    }

    $(document).on("callCloseInfoWindow", closeInfoWindow);

    var getAllLayerIds = function () {
        var checkboxes = $('input[type=\'checkbox\'][data-layerid]', $('#layerSelection'));
        return $.makeArray(checkboxes.map(function () { return $(this).attr('data-layerid'); }));
    };
    var resizeMap = function () {
        google.maps.event.trigger(map, 'resize');
    };


    //set public items.
    publicItem.map = map;
    publicItem.resizeMap = resizeMap;
    publicItem.isLayerIdChecked = isLayerIdChecked;
    publicItem.layerSelectorClosed = layerSelectorClosed;
    publicItem.layerToggled = layerToggled;
    publicItem.showInfoWindow = showInfoWindow;
    publicItem.showInfoWindowForItem = showInfoWindowForItem;
    publicItem.closeInfoWindow = closeInfoWindow;
    publicItem.clearInfoWindowId = clearInfoWindowId;
    publicItem.centerTooltip = centerTooltip;

    //need to set filter data before we set up the app cookie
    getFilter(layerInfo.filterData);

    init();

    //return public api.
    return publicItem;
};
class AppResize {

    isEmbedded
    initalLoadOnMobile = false;
    state = {
        mobile: false,
        desktop: false
    }
    initialStackDone = false;

    constructor() {
        this.isEmbedded = $("#embedmap").length > 0;
        if (resources.MapControlPos === 'RIGHT_BOTTOM') {
            $("#legend-container").addClass("legendMovedLeft");
           
        }

        if (window.innerWidth <= 992) {
            this.initalLoadOnMobile = true;
            if (resources.MapControlPos === 'RIGHT_BOTTOM') {
                this.stackMapControlsOnMobile();
            }
        } else if (resources.MapControlPos === 'RIGHT_BOTTOM') {
            this.stackMapControlsOnDesktop();
            this.checkRightBtmCtrls();
        } 

    }

    tryAgain(attemptNum, fn) {
        setTimeout(() => {
            attemptNum++;
            if (attemptNum < 10) {
                fn.call(this, attemptNum);
            } else {
                console.warn('Could not find element to resize');
            }
        }, 200);
    }

    checkRightBtmCtrls() {
        if (resources.MapControlPos === 'RIGHT_BOTTOM') {
            if (window.innerWidth > 992 && !this.initalLoadOnMobile && !this.state.desktop) {
                this.setBtmRtCtrlsOnDesktop();
            }
            if (window.innerWidth <= 992 && this.initalLoadOnMobile && !this.state.mobile) {
                this.setBtmRtCtrlsOnMobile();
            }

            if (window.innerWidth > 992 && this.initalLoadOnMobile && !this.state.desktop) {
                this.reuseBtmRtMobileControlsOnDesktop();
            }

            if (this.isEmbedded) {
                this.fixEmbeddedCtrlStack();
                this.addEmbeddedCtrlClass();
            }
        }
    }

    checkTopCenterCtrls(map, setDesktopCtrlPositions, setMobileCtrlPositions, getMapOptions) {
        if (resources.MapControlPos === 'TOP_CENTER') {

            if (window.innerWidth > 992 && this.initalLoadOnMobile && !this.state.desktop) {
                setDesktopCtrlPositions();
                map.setOptions(getMapOptions());
                this.reuseMobileControlsOnDesktop();
            }

            if (window.innerWidth <= 992 && this.initalLoadOnMobile && !this.state.mobile) {
                if (!this.isEmbedded) {
                    setMobileCtrlPositions();
                    map.setOptions(getMapOptions());
                    this.reuseMobileControlsOnMobile();
                } else {
                    this.addEmbeddedCtrlClass();
                } 
            }

            if (window.innerWidth <= 992 && !this.initalLoadOnMobile && !this.state.mobile) {
                setMobileCtrlPositions();
                map.setOptions(getMapOptions());
                this.reuseDesktopControlsOnMobile();
            }

            if (window.innerWidth > 992 && !this.initalLoadOnMobile && !this.state.desktop) {
                setDesktopCtrlPositions();
                map.setOptions(getMapOptions());
                this.state.desktop = true;
                this.state.mobile = false;
            }
        }
    }

    stackMapControlsOnMobile(attemptNum = 0) {
        setTimeout(() => {
            if (!this.initialStackDone) {

                let svpc = $(".gm-svpc");
                if (svpc.length > 0) {

                    if (!this.isEmbedded) {
                        $(".saveMapViewControlContainer").addClass("stackSaveCtrls");
                        let darkModeControlContainer = $(".darkModeControlContainer");
                        darkModeControlContainer.addClass("stackDarkModeCtrl");
                        svpc.addClass("stackPegman2");
                    } else {
                        this.stackEmbeddedControls();  
                        
                    }                  

                    this.stackFullScreenMapBtn();

                    this.initialStackDone = true;
                } else {
                    this.tryAgain(attemptNum, this.stackMapControlsOnMobile);                    
                }
            }
        }, 200);
    }

    stackMapControlsOnDesktop(attemptNum = 0) {
        //controls take a while to show up in DOM.  Keep trying until it's done'
        setTimeout(() => {
            if (!this.initialStackDone) {
          
                let zoomCtrl = $(".zoomControlContainer");
                if (zoomCtrl.length > 0) {

                    if (!this.isEmbedded) {
                        zoomCtrl.addClass("stackZoomCtrl");
                        $(".saveMapViewControlContainer").addClass("stackSaveCtrls");
                    } else {
                        this.stackEmbeddedControls();
                    }

                    $(".toggleDarkLMapContainer").addClass("stackDarkToggle");
                    this.stackFullScreenMapBtn();                   
                    this.initialStackDone = true;
                } else {
                    this.tryAgain(attemptNum, this.stackMapControlsOnDesktop);
                }
            }
        }, 200);
    }

    stackFullScreenMapBtn() {
        if (resources.EnableToggleFullScreenMapBtn === 'true') {
            let fullScreenBtn = $(".toggleFullScreenMapContainer");

            if (window.innerWidth <= 992 && this.initalLoadOnMobile) {
                fullScreenBtn.addClass("stackFullScreenToggleMobile");
                stackFullScreenToggle();
            } else if (!this.initalLoadOnMobile) {
                if (this.isEmbedded) {
                    fullScreenBtn.hide();
                } else {
                    fullScreenBtn.addClass("stackFullScreenToggleDesktop");
                }
            }
        }
    }

    stackEmbeddedControls() {
        let embeddedWidth = document.querySelector("#embedmap").getBoundingClientRect().width;
        let zoomCtrl = $(".zoomControlContainer");
        if (embeddedWidth < 800) {
            zoomCtrl.hide();
        } else {
            zoomCtrl.addClass("embeddedZoomCtrl");
        }

        $(".saveMapViewControlContainer").addClass("embeddedStackSaveCtrls");
    }

    reuseMobileControlsOnDesktop(attemptNum = 0) {
       setTimeout(() => {
           if (!this.state.desktop) {
                let gmControls = document.getElementsByClassName("gm-bundled-control")[0];

                if (gmControls) {
                    gmControls.classList.add("reuseMobileControls");

                    let darkMode = document.querySelector(".darkModeControlContainer");
                    gmControls.appendChild(darkMode);

                    let saveMap = document.querySelector(".saveMapViewControlContainer");
                    if (saveMap) {
                        gmControls.appendChild(saveMap);
                    } else {
                        darkMode.classList.add("darkModeNoSave");
                    }                     

                    if (resources.EnableToggleFullScreenMapBtn === 'true') {
                        gmControls.appendChild(document.querySelector(".toggleFullScreenMapContainer"));
                    }
                    this.state.mobile = false;
                    this.state.desktop = true;
                } else {
                    this.tryAgain(attemptNum, this.reuseMobileControlsOnDesktop);
                }
            }          
        }, 200);
    }


    fixEmbeddedCtrlStack(attemptNum = 0) {
        setTimeout(() => {
            let svpc = document.querySelector(".gm-svpc");

            if (svpc) {
                let darkModeCtrl = $("#toggleDarkLMapContainer");
                darkModeCtrl.removeClass("stackDarkToggle");
                darkModeCtrl.hide();
                $(".gm-svpc").hide();
                $(".zoomControlContainer").hide();
                let pegmen = $(".gm-svpc");
                pegmen.show();
                pegmen.addClass("embeddedPeg");

              document.querySelector(".saveMapViewControlContainer").classList.add("embeddedStackSaveCtrls");;

            } else {
                this.tryAgain(attemptNum, this.fixEmbeddedCtrlStack);
            }
        }, 200);
    }

    addEmbeddedCtrlClass(attemptNum = 0) {

        setTimeout(() => {
            let svpc = document.querySelector(".gm-svpc");
            if (svpc) {
                svpc.classList.add("embeddedCtrl");
                let saveMapCtrl = document.querySelector(".saveMapViewControl");
                if (saveMapCtrl) {
                    saveMapCtrl.classList.add("embeddedCtrl");
                }
               document.querySelector(".gmnoprint").classList.add("embeddedCtrl");            
            } else {
                this.tryAgain(attemptNum, this.addEmbeddedCtrlClass);
            }
        }, 200);        
    }

    stackFullScreenToggle(attemptNum = 0) {
        setTimeout(() => {
            let darkModeControlContainer = document.querySelector(".darkModeControlContainer");
            if (darkModeControlContainer) {
                darkModeControlContainer.parentElement.appendChild(document.querySelector(".stackFullScreenToggleMobile"));
            } else {
                this.tryAgain(attemptNum, this.stackFullScreenToggle);
            }
        }, 200);
    }

    setBtmRtCtrlsOnMobile(attemptNum = 0) {
        setTimeout(() => {
            if (!this.state.mobile) {
                let darkModeControlContainer = document.querySelector(".darkModeControlContainer");
                if (darkModeControlContainer) {
                    darkModeControlContainer.classList.add("stackDarkModeMobile");
                    this.state.mobile = true;
                    this.state.desktop = false;
                } else {
                    this.tryAgain(attemptNum, this.setBtmRtCtrlsOnMobile);
                }
            }
        }, 200);
    }

    reuseBtmRtMobileControlsOnDesktop(attemptNum = 0) {
        setTimeout(() => {
            if (!this.state.desktop) {
                let saveMapViewControlContainer = document.querySelector(".saveMapViewControlContainer");
                if (saveMapViewControlContainer) {
                    saveMapViewControlContainer.classList.add("reuseSaveMapViewControlOnDesk");
                    if (resources.EnableToggleFullScreenMapBtn === 'true') {
                        let fullScreenToggle = document.querySelector(".toggleFullScreenMapContainer");
                        fullScreenToggle.classList.add("reuseFullScreenControlOnDesk");
                        saveMapViewControlContainer.parentElement.appendChild(fullScreenToggle);
                    }
                    this.state.desktop = true;
                    this.state.mobile = false;
                } else {
                    this.tryAgain(attemptNum, this.reuseBtmRtMobileControlsOnDesktop);
                }
            }          
        }, 200);
    }

    setBtmRtCtrlsOnDesktop(attemptNum = 0) {
        setTimeout(() => {
            if (!this.state.desktop) {
                let svpc = document.querySelector(".gm-svpc");
                if (!this.isEmbedded && svpc) {
                    svpc.classList.add("stackPegman1");
                    this.state.desktop = true;
                    this.state.mobile = false;
                } else {
                    this.tryAgain(attemptNum, this.setBtmRtCtrlsOnDesktop);
                }
            }           
        }, 200);
    }

    reuseDesktopControlsOnMobile(attemptNum = 0) {
        setTimeout(() => {
            if (!this.state.mobile) { 
                let svpc = document.querySelector(".gm-svpc");

                if (svpc) {
                    svpc.classList.add("pegmanForMobile");
                    let pElem = document.querySelector('.gm-bundled-control-on-bottom').parentElement;
                    pElem.classList.add("reuseDektopCtrls");
                    pElem.appendChild(document.querySelector(".zoomControlContainer"));
                    pElem.appendChild(document.querySelector(".toggleDarkLMapContainer"));
                    let saveMap = document.querySelector(".saveMapViewControlContainer");
                    if (saveMap) {
                        pElem.appendChild(document.querySelector(".saveMapViewControlContainer"));
                    }

                   
                    if (document.querySelector(".ersMode")) {
                        //regular ERS legend could be overlapping with controls, hide it to use the modal legend.
                        let layerSelection = document.querySelector("#layerSelection")
                        if (layerSelection.style.display === 'block') {
                            $(".legend-toggle").trigger("click");
                        }
                    }
                    this.state.desktop = false;
                    this.state.mobile = true;
                } else {
                    this.tryAgain(attemptNum, this.reuseDesktopControlsOnMobile);
                }
            }
        }, 200);
    }

    reuseMobileControlsOnMobile(attemptNum = 0) {
        setTimeout(() => {
            if (!this.state.mobile) {
                let gmControls = document.querySelector('.gm-bundled-control-on-bottom');

                if (gmControls) {
                    let pElem = gmControls.parentElement;
                    let saveMap = document.querySelector(".saveMapViewControlContainer");
                    if (saveMap) {
                        pElem.appendChild(saveMap);
                    }                    
                    pElem.appendChild(document.querySelector(".darkModeControlContainer"));

                    this.state.desktop = false;
                    this.state.mobile = true;

                } else {
                    this.tryAgain(attemptNum, this.reuseMobileControlsOnMobile);
                }
            }         
        }, 200);
    }
}
var setupSlickCarousel = function (camera) {
    var carouselDiv = $(camera).closest('.cctvCameraCarousel');
    if ($(carouselDiv).hasClass('slick-initialized')) {
        if (Modernizr.mq('(max-width: 992px)')) {
            var modalWidth = $(".bootbox-body .map-tooltip").outerWidth() - 10;
            if (modalWidth > 0) {
                $(".bootbox-body .slick-initialized, .bootbox-body .slick-slide").css("width", modalWidth + "px");
                $(carouselDiv).slick("setPosition");
            }
        }
    }
    else {
        $(carouselDiv).slick({
            dots: true,
            arrows: true,
            autoplay: true,
            lazyLoad: 'ondemand',
            autoplaySpeed: 10000,
            accessibility: true,
            infinite: false
        });
        $(carouselDiv).on('beforeChange', function (event, slick, currentSlide, nextSlide) {
            var slide = $(slick.$slides.get(nextSlide));
            var imgObj = $('img', slide);
            if (imgObj.attr('data-needsrefresh') == 'true' && imgObj[0].hasAttribute('src')) {
                imgObj.attr('src', URI(imgObj.attr('src')).hash(new Date().getTime()));
                imgObj.attr('data-needsrefresh', 'false');
            }
        });
    }
    $(camera).removeClass('carouselCctvImage');
};
var setUpImageSlide = function (slick, slideId) {
    var slickIntervalId;
    if (slick.$slides.length > 0) {
        var img = $('img', $(slick.$slides.get(slideId)));
        var title = img.data('title');
        if (title) {
            $("#myCameraTitle").text(title);
            var id = img.data('id');
            $("#myCameraLocation").show();
            $("#myCameraLocation").attr('href', '#camera-' + id);
        }
        else {
            $("#myCameraLocation").hide();
        }
        img.attr('data-lazy', URI(img.data('url')).hash(new Date().getTime()));
        var refreshRateMs = img.data('refresh-rate');
        if (refreshRateMs > 0) {
            slickIntervalId = setInterval(function (carouselId) {
                var imgObj = $('#' + carouselId);
                imgObj.attr('src', URI(imgObj.data('url')).hash(new Date().getTime()));
            }, refreshRateMs, img.attr('id'));
        }
        else {
            if (typeof Bugsnag !== "undefined") {
                Bugsnag.notify("Undefined refreshRateMs", function (event) {
                    event.setMetadata('html', $('<div/>').append(img.clone()).html());
                });
            }
        }
    }
};

"use strict";
var UserCameras = null;
var LatLng = (function () {
    function LatLng() {
    }
    return LatLng;
}());
var CameraLocater = (function () {
    function CameraLocater(appPublicApi) {
        var _this = this;
        this.appPublicApi = appPublicApi;
        this.map = this.appPublicApi.map;
        this.centerTooltip = appPublicApi.appHelper.centerTooltip;
        $(document).on('hashChanged-urlHash', function (event) {
            var hash = urlHash.hash();
            if (hash && hash.toLowerCase().lastIndexOf('camera-', 0) == 0) {
                var cameraId = decodeURIComponent(_this.getUrlHashAsId());
                if (cameraId) {
                    _this.zoomToCamera(cameraId, 'Camera', 'Cameras');
                }
            }
            else if (hash && hash.indexOf('-') > -1) {
                _this.zoomToObject(_this.getUrlHashAsId(), hash.split('-')[0]);
            }
        });
    }
    CameraLocater.prototype.zoomToObject = function (title, layer) {
        var _this = this;
        var checkBox = $('input[type=\'checkbox\'][data-layerid="' + layer + '"]', $('#layerSelection'));
        if (!checkBox.is(':checked')) {
            var isChild = checkBox.attr('data-parent');
            if (isChild) {
                var parent = $('input[type=\'checkbox\'][data-layerid="' + isChild + '"]', $('#layerSelection'));
                parent.click();
                var dontSelectChild = parent.attr('data-dontselectchildlayers');
                if (dontSelectChild === 'True') {
                    $('input[type=\'checkbox\'][data-layerid="' + layer + '"]', $('#layerSelection')).click();
                }
            }
            else {
                $('input[type=\'checkbox\'][data-layerid="' + layer + '"]', $('#layerSelection')).click();
            }
        }
        this.appPublicApi.appHelper.iconManager.RefreshLayer(layer, true);
        $(document).on('layerIconsUpdated', function (event, layerId, data) {
            if (layerId == layer) {
                title = decodeURIComponent(title);
                var item = data.item2.filter(function (icon) { return icon.itemId == title; });
                if (item.length == 1) {
                    var position = new google.maps.LatLng(item[0].location[0], item[0].location[1]);
                    _this.centerTooltip.externalPanCheck();
                    _this.map.panTo(position);
                    var currentZoom = _this.map.getZoom();
                    if ($(window).width() > 992) {
                        if (currentZoom < 13) {
                            _this.map.setZoom(13);
                        }
                    }
                    else {
                        if (currentZoom < 17) {
                            _this.map.setZoom(17);
                        }
                    }
                    _this.appPublicApi.appHelper.showInfoWindowForItem(layerId, title, null, position, layerId);
                    $(document).off('layerIconsUpdated');
                }
            }
        });
    };
    CameraLocater.prototype.zoomToCamera = function (cameraId, controller, layer) {
        var _this = this;
        $.ajax('/' + controller + '/GetLatLng?id=' + cameraId, { type: 'POST' }).done(function (data) {
            if (data && data.latitude && data.longitude) {
                var pos = new google.maps.LatLng(data.latitude, data.longitude);
                _this.map.panTo(pos);
                var currentZoom = _this.map.getZoom();
                if ($(window).width() > 992) {
                    if (currentZoom < 13) {
                        _this.map.setZoom(13);
                    }
                    _this.appPublicApi.appHelper.showInfoWindowForItem(layer, data.id, null, pos);
                    urlHash.hash(controller);
                }
                else {
                    if (currentZoom < 17) {
                        _this.map.setZoom(17);
                    }
                    _this.appPublicApi.appHelper.showInfoWindowForItem(layer, data.id, null, pos);
                    urlHash.hash('map-col-container');
                }
                if (!$('input[type=\'checkbox\'][data-layerid="' + layer + '"]', $('#layerSelection')).is(':checked')) {
                    $('input[type=\'checkbox\'][data-layerid="' + layer + '"]', $('#layerSelection')).click();
                    _this.appPublicApi.appHelper.layerToggled(layer, false);
                }
            }
        });
    };
    CameraLocater.prototype.getUrlHashAsId = function () {
        var oId = urlHash.hash();
        oId = oId.substring(oId.indexOf('-') + 1);
        return oId;
    };
    CameraLocater.prototype.setUrlHashAsObjectId = function (oId, controller) {
        var format = '{0}-{1}';
        var hash = format.replace('{0}', controller || '');
        hash = hash.replace('{1}', oId || '');
        urlHash.hash(hash);
    };
    return CameraLocater;
}());
;

$(function () {
    var timeouts = [];

    var VideoTimeout = function (cameraId) {
        StartSlider(".cctvCameraCarousel");
        if (document.getElementById(cameraId + '-video')) {
            VideoToPicture(cameraId, cameraId + "img", "showVideo-" + cameraId, "hideVideo-" + cameraId, cameraId + '-videoContainer', $(document).find("[id='hideVideo-" + cameraId + "']"));
        }
    };
    var MyVideoTimeout = function (cameraId) {
        StartSlider(".cameraCarousel");
        if (document.getElementById(cameraId + '-video')) {
            VideoToPicture(cameraId, cameraId + "img", "showVideo-" + cameraId, "hideVideo-" + cameraId, cameraId + '-videoContainer', $(document).find("[id='hideVideo-" + cameraId + "']"));
        }
    };
    $(document).on("click", ".showVideo", function (e) {
        if (e.detail && e.detail > 1) { // Stop multi-click events
            return;
        }
        //we cant run this without videojs, and it is possible for it to not be included
        //in the configuration, so just return
        if (typeof videojs == 'undefined') return;

        StopSlider(".cctvCameraCarousel");

        var _this = $(this).parent().parent().find(".cctvImage");
        var imgWidth = _this.width();
        var imgHeight = _this.height();

        var cameraId = $(this).data("camera-id");

        if (imgWidth == undefined) {
            imgWidth = parseInt(resources.CamTooltipMaxWidth);
        }

        PictureToVideo(cameraId, cameraId + "img", "showVideo-" + cameraId, "hideVideo-" + cameraId, cameraId + '-videoContainer', this, imgWidth, imgHeight);

        if (resources.EnableVideoTimeout == "True") {
            clearTimeout(timeouts[cameraId]);
            timeouts[cameraId] = setTimeout(function () { VideoTimeout(cameraId) }, parseInt(resources.VideoTimeoutInMilliseconds));
        }
    });

    $(document).on("click", ".hideVideo", function () {
        if (typeof videojs == 'undefined') return;

        StartSlider(".cctvCameraCarousel");

        var cameraId = $(this).data("camera-id");
        if (resources.EnableVideoTimeout == "True") {
            clearTimeout(timeouts[cameraId]);
            timeouts[cameraId] = null;
        }
        VideoToPicture(cameraId, cameraId + "img", "showVideo-" + cameraId, "hideVideo-" + cameraId, cameraId + '-videoContainer', this);
    });

    $(document).on("click", ".showMyVideo", function (e) {
        if (e.detail && e.detail > 1) { // Stop multi-click events
            return;
        }
        if (typeof videojs == 'undefined') return;

        StopSlider(".cameraCarousel");

        var _this = $(this).parent().parent().find(".myCamImg");
        var imgWidth = _this.width();
        var imgHeight = _this.height();

        var cameraId = $(this).data("camera-id");
        PictureToVideo(cameraId + 'my', "carouselId-" + cameraId, "showMyVideo-" + cameraId, "hideMyVideo-" + cameraId, cameraId + '-myVideoContainer', this, imgWidth, imgHeight);

        if (resources.EnableVideoTimeout == "True") {
            clearTimeout(timeouts[cameraId]);
            timeouts[cameraId] = setTimeout(function () { MyVideoTimeout(cameraId) }, parseInt(resources.VideoTimeoutInMilliseconds));
        }

    });

    $(document).on("click", ".hideMyVideo", function () {
        if (typeof videojs == 'undefined') return;

        StartSlider(".cameraCarousel");

        var cameraId = $(this).data("camera-id");
        if (resources.EnableVideoTimeout == "True") {
            clearTimeout(timeouts[cameraId]);
            timeouts[cameraId] = null;
        }
        VideoToPicture(cameraId + 'my', "carouselId-" + cameraId, "showMyVideo-" + cameraId, "hideMyVideo-" + cameraId, cameraId + '-myVideoContainer', this);
    });

    function PictureToVideo(videoElementId, pictureElementId, showElementId, hideElementId, videoContainerId, element, imgWidth, imgHeight) {

        var Id = videoElementId + '-video';
        var containerElement = document.getElementById(videoContainerId);
        var url = containerElement.getAttribute("data-videourl");
        var authRequired = containerElement.getAttribute("data-videoauth");
        var type = containerElement.getAttribute("data-streamtype");
        var cameraId = containerElement.getAttribute("data-cameraid");
        if (authRequired === "true" || !url || url == "") {
            $.ajax('/Camera/GetVideoUrl?cameraId=' + cameraId, {
                type: 'GET',
                cache: false
            }).done(function (data) {
                //data can either be a url or a post object to send to a new endpoint if we require the clients IP to do the auth
                if (typeof data === 'object') {
                    $.ajax(resources.CameraVideoUrl, {
                        type: 'POST',
                        cache: false,
                        data: JSON.stringify(data),
                        contentType: "application/json"
                    }).done(function (queryString) {
                        AddVideo(containerElement, pictureElementId, showElementId, hideElementId, Id, url + queryString, type, element, imgWidth, imgHeight);
                    });
                } else {
                    AddVideo(containerElement, pictureElementId, showElementId, hideElementId, Id, data, type, element, imgWidth, imgHeight);
                }
            });
        } else {
            AddVideo(containerElement, pictureElementId, showElementId, hideElementId, Id, url, type, element, imgWidth, imgHeight);
        }

    }

    function AddVideo(containerElement, pictureElementId, showElementId, hideElementId, Id, url, type, element, imgWidth, imgHeight) {
        var html = VideoHTML(Id, url, type);

        //add a custom error messages to give the user a better idea what to do
        videojs.addLanguage('en', { "No compatible source was found for this media.": window.resources.IE8ErrorMessage });

        containerElement.appendChild(jQuery.parseHTML(html)[0]);

        var pictureElement;

        var player = videojs(document.getElementById(Id), { "width": imgWidth, "height": imgHeight });

        $(containerElement).children("div.video-js").css({ "width": imgWidth + "px", "height": imgHeight + "px" });
        function disposeVideoPlayer() {
            if (player != null && document.getElementById(Id) == null) {
                try {
                    player.dispose();
                }
                catch (err) {
                    //do nothing, videojs exception
                    //https://github.com/videojs/video.js/issues/3755
                }
                player = null;
            }
        }

        function pollForOrphanedVideoPlayer() {
            disposeVideoPlayer();

            if (player != null) {
                setTimeout(function () {
                    pollForOrphanedVideoPlayer();
                }, 5000);
            }
        }

        pollForOrphanedVideoPlayer();

        this.errorFunction = function () {
            videoElement = document.getElementById(Id);
            var vid = videojs(videoElement);

            //https://dev.w3.org/html5/spec-author-view/video.html#mediaerror
            //error code 4 = old browser, we already configured a custom error text in this case, so do nothing here
            if (!vid.error().code == 4) {
                vid.dispose();
                var errorHtml = "<img src='" + resources.CctvVideoCustomErrorImage + "' style='display:block;width:100%'>";
                containerElement.appendChild(jQuery.parseHTML(errorHtml)[0]);
            }
        };

        if (resources.CctvVideoCustomErrorImage) {
            player.on('error', errorFunction);
        }

        pictureElement = document.getElementById(pictureElementId);

        var showVideoLinkElement = $(element)[0];
        var hideVideoLinkElement = $(element).next()[0];

        // datatable mobile    
        if (isImgColCollapsed()) {
            var videoContainer = $(".child div[id='" + containerElement.id + "']");
            if (videoContainer.length < 2) {
                $(videoContainer).html($(containerElement).children());
            }

            pictureElement = $(".child img[id='" + pictureElementId + "']")[0];
            showVideoLinkElement = $(".child button[id='" + showElementId + "']")[0];
            hideVideoLinkElement = $(".child button[id='" + hideElementId + "']")[0];
        }

        //hide all of the picture stuff we don't want to show when there is a video rolling
        pictureElement.style.display = 'none';
        showVideoLinkElement.style.display = 'none';
        hideVideoLinkElement.style.display = 'inline';

        // set same width/height of img to video     
        $(element).parent().prevUntil("video-js").children().css({ "width": imgWidth + "px" });
    }

    function VideoToPicture(cameraId, pictureElementId, showElementId, hideElementId, videoContainerId, element) {
        var videoElementId = cameraId + '-video';
        var videoElement;
        var videoElement = document.getElementById(videoElementId);
        var pictureElement = document.getElementById(pictureElementId);    
     
        var showVideoLinkElement = $(element).prev()[0];
        var hideVideoLinkElement = $(element)[0];

        // datatable mobile        
        if (isImgColCollapsed()) { 
            pictureElement = $(".child img[id='" + pictureElementId + "']")[0];
            showVideoLinkElement = $(".child button[id='" + showElementId + "']")[0];
            hideVideoLinkElement = $(".child button[id='" + hideElementId + "']")[0];
        }  

        if (videoElement != null) {
            videojs(videoElement).dispose();
        } 

        //make sure the video container is empty, will also remove error images
        var vidContainer = document.getElementById(videoContainerId);
        while (vidContainer.firstChild) {
            vidContainer.removeChild(vidContainer.firstChild);
        }
        
        pictureElement.style.display = 'block';
        showVideoLinkElement.style.display = 'inline';
        hideVideoLinkElement.style.display = 'none';
    }

    function VideoHTML(id, src, type) {
        return '<video id="' + id + '" class="video-js vjs-default-skin" ' +
            'preload="auto" ' + 
            'autoplay ' +
            'data-setup=\'{ "controls": true }\'>' +
            '\t<source src="' + src + '" type="' + type + '" /> \n' +
            '\t\t<p class="vjs-no-js"> To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video</p>\n' +
            '</video>';
    }  

    $(document).on("CallRemoveVideo", RemoveVideo);

    function RemoveVideo() {
        var video = $(".map-tooltip video");
        if (video.length > 0) {
            $(video).each(function () {
                var videoElementId = $(this).parent().attr("id");              
                var player = videojs(document.getElementById(videoElementId));
                player.dispose();
            });  
        }            
    }

    function StopSlider(cssClass) {
        var carousel = $(cssClass + '.slick-initialized');

        if (typeof carousel != 'undefined' && carousel.length > 0) {
            carousel.slick('slickSetOption', 'autoplay', false).slick('slickPause');
        }
    }

    function StartSlider(cssClass) {
        var carousel = $(cssClass + '.slick-initialized');
        if (typeof carousel != 'undefined' && carousel.length > 0) {
            carousel.slick('slickSetOption', 'autoplay', true).slick('slickPlay');
        }
    }

    function checkListPage() {
        if ($(".cctvList").length > 0) {
            return true;
        }
        if ($(".myCctvList").length > 0) {
            return true;
        }
        return false;
    }

    function isImgColCollapsed() {
        return $(".child .cctvImage").length > 0;
    }
});
class CenterTooltip {

    isErs;
    map;
    mapBounds;
    mapBottom;
    mapTop;
    tooltip;
    infoWindowObj;
    firstImage;
    imagesAlreadyLoaded = [];
    draggedMap = false;

    constructor(map) {
        this.map = map;
        this.isErs = $(".ersMode").length > 0;
    }

    startTriggers() {
        $(document).on("tooltip-info-toggled", () => {
            this.scrollTooltipMoreInfoIntoView();
            this.panBy();
            if (this.widthHasIncreased) {
                //trim excess height
                let draggableBorder = document.getElementById("draggableBorder");

                if (draggableBorder) {
                    let newContentHeight = draggableBorder.querySelector(".row .event");
                    
                    //avoid making tooltip so tall that it doesn't fit inside the view
                    let maxHeight = this.mapBottom - this.mapTop; 
                    if (newContentHeight && newContentHeight.scrollHeight !== draggableBorder.scrollHeight + 20
                        && newContentHeight.scrollHeight < maxHeight) {
                        draggableBorder.style.height = (newContentHeight.scrollHeight + 20) + 'px';
                    }
                }
            }
        });


        this.map.addListener('dragend', () => {
            //if idle event didn't fire (since map didn't need to be panned when tooltip was opened) and 
            //the user drags the map, do not center tooltips right after that.
            this.draggedMap = true;
        });
    }

    //map.panTo() done outside of this file needs to be tracked and need to know when it's done.
    externalPanListener
    externalPanDone = true;
    externalPanCheck() {
        this.externalPanDone = false;
        this.externalPanListener = this.map.addListener('idle', this.externalPanEvent.bind(this));
    }

    externalPanEvent() {
        if (this.externalPanListener && this.externalPanListener.instance) {
            google.maps.event.removeListener(this.externalPanListener);
            this.externalPanListener = undefined;
            this.externalPanDone = true;
        }
    }

    checkingForMove = false;
    run(infoWindowObj) {
        if (!this.checkingForMove) {
            let initialLatLng = this.resetRun(infoWindowObj);
            setTimeout(() => {
                let finalCenter = this.map.getBounds().getCenter();
                let finalLatLng = [finalCenter.lat(), finalCenter.lng()];
                this.checkingForMove = false;
                if (finalLatLng[0] === initialLatLng[0] && finalLatLng[1] === initialLatLng[1] && this.externalPanDone) {
                    this.doFirstMove();
                } else {
                    this.run();
                }
            }, 100);
        }
    }

    resetRun(infoWindowObj) {
        this.checkingForMove = true;
        this.draggedMap = false;
        this.panByNotBusy = true;
        this.firstMove = true;
        this.widthHasIncreased = false;
        this.removedWhiteSpace = false;
        this.firstImage = undefined;
        if (this.firstImage) {
            this.firstImage.removeEventListener("load", this.imageLoadedEvent.bind(this));
        }
        let initCenter = this.map.getBounds().getCenter();
        this.tooltip = undefined;
        this.infoWindowObj = infoWindowObj;
        return [initCenter.lat(), initCenter.lng()];
    }


    firstMove
    doFirstMove() {
        this.setupImageLoadEvent();
        this.setupTabEvent();
        this.setTooltipScrolling();
        this.panBy();
    }

    setTooltipScrolling() {
        //prevent tooltip from closing when user wants to scroll within it.
        //otherwise map zooms out on wheel scroll and user gets annoyed
        //non-draggable tooltips (default google tooltips) don't need this block, they work fine.  
        if (this.tooltip.dom && this.tooltip.isDraggable) {
            this.tooltip.dom.addEventListener('mouseover', () => {
                this.tooltip.dom.focus(); //prevent whole page from scrolling.
                this.map.set('scrollwheel', false);
            });
            this.tooltip.dom.addEventListener('mouseout', () => {
                this.map.set('scrollwheel', true);
            })
        }
    }

    imageLoadedEvent() {
        if (this.firstImage) {
            if (this.firstImage.id && this.imagesAlreadyLoaded.indexOf(this.firstImage.id) === -1) {
                this.imagesAlreadyLoaded.push(this.firstImage.id);
            }
            this.firstImage.removeEventListener("load", this.imageLoadedEvent.bind(this));
        }
        this.tryToPanBy();
    }


    panByNotBusy
    panByListener
    panByEvent() {
        if (this.panByListener && this.panByListener.instance) {
            google.maps.event.removeListener(this.panByListener);
            this.panByListener = undefined;
            this.panByNotBusy = true;
            if (this.firstMove) {
                this.firstMove = false;
                //avoid new cutoffs after tooltip size adjusted and image might have loaded by now
                this.panBy();
            }
        }
    }

    tryToPanBy() {
        if (this.panByNotBusy) {
            this.panBy();
        } else {
            setTimeout(() => {
                this.tryToPanBy();
            }, 50);
        }
    }

    panBy() {
        this.setTooltip();
        if (this.tooltip.dom && this.tooltip.measures && !this.tooltip.insideBootbox && this.panByNotBusy) {
            this.setMapBounds();
            this.adjustTooltipSize();
            this.setTooltip(); //need latest specs about tooltip
            let yMove = this.yTooltipMove();
            let xMove = this.xTooltipMove();
            if (xMove !== 0 || yMove !== 0) {
                //map is moving for sure.  idle event will fire when it's done'
                this.panByNotBusy = false;
                this.panByListener = this.map.addListener('idle', this.panByEvent.bind(this));
                this.map.panBy(xMove, yMove);
            } else if (this.firstMove) {
                this.firstMove = false;
                //avoid new cutoffs after tooltip size adjusted and image (especially in cache) might have loaded by now
                this.panBy();
            }
        }
    }

    //if tooltips have a scrollbar and they are a bit narrow, we can increase their width to
    //a max width of i.e. 450px to make better use of screen space, and reduce scrolling.
    //their default width is by set in appHelper.js via var googleInfoWindowMaxWidth = 365;
    maxTooltipWidth = 450;

    adjustTooltipSize() {
        let draggableBorder = document.getElementById("draggableBorder");
        if (draggableBorder) {
            if (draggableBorder.scrollHeight < this.mapBottom - this.mapTop) {
                //increasing height of tooltip and removing scrollbar completely
                draggableBorder.style.height = (draggableBorder.scrollHeight) + 'px';
                draggableBorder.style.maxHeight = 'unset';
                draggableBorder.style.overflowY = 'hidden';
                document.querySelector(".closeDraggableWindow").style.right = "3px";
            } else {
                //maximizing height of tooltip, but it will still have a scroll since it needs it
                draggableBorder.style.height = (this.mapBottom - this.mapTop) + 'px';
                draggableBorder.style.maxHeight = 'unset';
                draggableBorder.style.overflowY = "auto";
                //prevent "x" close btn overlapping with scrollbar
                document.querySelector(".closeDraggableWindow").style.right = "11px";
                if (!this.tooltip.hasSingleImg && !this.tooltip.hasSlickSlider) {
                    this.adjustTooltipWidth();
                }
            }

            if (this.widthHasIncreased && !this.removedWhiteSpace) {
                //reduce height of tooltip if it's excessive now.
                //Do not do this if user clicks "more info" or anythig else that expands tooltip length after it has been opened
                let newContentHeight = draggableBorder.querySelector(".row .event");

                if (!newContentHeight) {
                    //some tooltips don't have .row .event, so select using this class instead.
                    newContentHeight = draggableBorder.querySelector(".draggable-tooltip");
                }

                if (newContentHeight && newContentHeight.scrollHeight !== draggableBorder.scrollHeight + 10) {
                    draggableBorder.style.height = (newContentHeight.scrollHeight + 10) + 'px';
                    this.removedWhiteSpace = true;
                    setTimeout(() => {
                        this.tryToPanBy();
                    });
                }
            }

            this.trimExcessTabHeight(draggableBorder);

        } else if (!this.tooltip.hasSingleImg && !this.tooltip.hasSlickSlider) {
            //adjust width of default google tooltip if it has as scroll.
            //these tooltips do not need any height adjustment
            let gmScrollBox = document.querySelector(".gm-style-iw-d");
            if (gmScrollBox && gmScrollBox.scrollHeight > gmScrollBox.clientHeight
                && this.infoWindowObj && this.infoWindowObj.maxWidth !== this.maxTooltipWidth) {
                this.widthAdjusted = this.map.addListener('idle', this.googlewidthAdjustedEvent.bind(this));
                this.infoWindowObj.maxWidth = this.maxTooltipWidth;
            }
        }
    }

    trimExcessTabHeight(draggableBorder) {
        let tooltipTabs = this.tooltip.dom.querySelectorAll(".nav-tabs a");
            if (tooltipTabs.length > 0) {
                let newHeight = this.tooltip.dom.querySelector(".map-tooltip").getBoundingClientRect().height;
                if (newHeight < this.mapBottom - this.mapTop) {                    
                    draggableBorder.style.height = (newHeight + 15) + 'px';
                }
            }
    }

    widthAdjusted
    googlewidthAdjustedEvent() {
        //after google maps tooltips width is adjusted, google maps starts panning the map. 
        //centering needs to be done after idle event fires.
        if (this.widthAdjusted && this.widthAdjusted.instance) {
            google.maps.event.removeListener(this.widthAdjusted);
            this.widthAdjusted = undefined;
            if (!this.draggedMap) return;
            this.panBy();
        }
    }

    widthHasIncreased
    adjustTooltipWidth() {
        let draggableContainer = document.querySelector(".draggableWindowContainer");
        let dContainerwidth = draggableContainer.getBoundingClientRect().width;
        if (dContainerwidth < this.maxTooltipWidth) {
            draggableContainer.style.width = this.maxTooltipWidth + "px";
            this.widthHasIncreased = true;
            let tooltipScrollBox = document.getElementById("draggableBorder");
            if (tooltipScrollBox.scrollHeight === tooltipScrollBox.clientHeight) {
                document.querySelector(".closeDraggableWindow").style.right = "3px";
            }
        }
    }

    setupImageLoadEvent() {
        this.setTooltip(); //need latest specs about tooltip
        if (this.tooltip.dom && this.tooltip.measures) {
            if (this.tooltip.hasSlickSlider || this.tooltip.hasSingleImg) {

                let slider = this.tooltip.dom.querySelector(".slick-slide");
                if (slider) {
                    this.firstImage = this.tooltip.dom.querySelector(".slick-slide").getElementsByTagName("img")[0];
                }

                //video images within cctvCameraCarousel seem to always have a load event.
                let isCctvCameraCarousel = this.tooltip.dom.querySelector(".cctvCameraCarousel ");

                if (!this.firstImage) {
                    this.firstImage = this.tooltip.dom.querySelector(".cctvImage");
                }
                if (this.firstImage && this.imagesAlreadyLoaded.indexOf(this.firstImage.id) === -1 || isCctvCameraCarousel) {
                    this.firstImage.addEventListener("load", this.imageLoadedEvent.bind(this));
                }
            }
        }
    }

    setMapBounds() {
        this.mapBounds = document.getElementById("map-canvas").getBoundingClientRect();
        //map bottom: allow space for 1) bottom map controls to show and 2) marker to be visible when it's at max bottom
        this.mapBottom = this.mapBounds.bottom - 100;
        this.mapTop = this.mapBounds.top + (resources.MapControlPos === 'TOP_CENTER' ? 60 : 10) + (this.isErs ? 25 : 0);
    }

    setTooltip() {
        let tooltip = document.querySelector('.draggableWindow');
        let isDraggable = true;
        let hasSlickSlider = false;
        let hasScroll = false;
        let measures = {};
        let hasSingleImg = false;

        //bootbox is for mobile. Tooltips in mobile don't need centering'
        let bootbox = document.querySelector(".bootbox");
        let bootBoxHasTooltip = false;

        if (bootbox) {
            let hasTooltip = bootbox.querySelector(".map-tooltip");
            if (hasTooltip) {
                bootBoxHasTooltip = true;
            }
        }

        if (!tooltip) {
            //want the map-tooltip class of the actual tooltip, and not the route planner
            tooltip = document.querySelector(".map-tooltip");
            if (tooltip) {
                tooltip = tooltip.parentElement.parentElement;
            }
            isDraggable = false;
        }

        if (!tooltip) {
            tooltip = document.querySelector(".gm-style-iw"); //KML tooltip most likely.
            isDraggable = false;
        }

        if (tooltip) {
            let checkSlider = tooltip.querySelector(".slick-slider");
            if (checkSlider) {
                hasSlickSlider = true;
            }

            if (isDraggable) {
                let border = document.querySelector("#draggableBorder");
                if (border.scrollHeight > border.clientHeight) {
                    hasScroll = true;
                }

            } else if (tooltip.scrollHeight > tooltip.clientHeight) {
                hasScroll = true;
            }

            let checkSingleImg = tooltip.querySelector(".cctvImage");
            if (checkSingleImg) {
                hasSingleImg = true;
            }

            measures = tooltip.getBoundingClientRect();
        }

        this.tooltip = {
            dom: tooltip,
            isDraggable: isDraggable,
            hasSlickSlider: hasSlickSlider,
            hasScroll: hasScroll,
            measures: measures,
            insideBootbox: bootBoxHasTooltip,
            hasSingleImg: hasSingleImg
        };
    }


    setupTabEvent() {
        if (this.tooltip && this.tooltip.dom) {
            let tooltipTabs = this.tooltip.dom.querySelectorAll(".nav-tabs a");
            if (tooltipTabs) {
                $(tooltipTabs).on("shown.bs.tab", () => {
                    this.panBy();
                })
            }
        }
    }

    yTooltipMove() {
        let result = 0;
        //on the first move, google maps is likely moving the tooltips still on its own.  
        //Let it finish, allow "idle" event to fire before adjusting vertically.
        if (!this.firstMove) {
            if (this.mapTop > this.tooltip.measures.top) {
                result = this.tooltip.measures.top - this.mapTop;
                var maxSafeDistanceDown = this.mapBottom - this.mapTop;
                if (result * -1 > maxSafeDistanceDown) {
                    result = maxSafeDistanceDown * -1;
                }
            } else if (this.mapBottom < this.tooltip.measures.bottom) {
                //tooltip is down too much, move it up.
                result = this.tooltip.measures.bottom - this.mapBottom + (this.tooltip.isDraggable ? 0 : 10);

                let newTooltipTop = this.tooltip.measures.top - result;
                if (this.mapTop > newTooltipTop) {
                    result = 0;

                    if (!this.tooltip.hasScroll && !this.tooltip.isDraggable) {
                        //'More info' clicked.  tooltip expanded just a bit. 
                        let checkOuterDiv = this.tooltip.dom.parentElement.parentElement;
                        let outDivMeasures = checkOuterDiv.getBoundingClientRect();
                        result = outDivMeasures.top - this.mapTop;
                    }
                }
            }
        }
        return result;
    }

    xTooltipMove() {
        let result = 0;
        let isDraggable = this.tooltip.isDraggable;
        var isRoutePlannerFloating = $(".col-md-12.mapColContainer").length > 0;
        if (isRoutePlannerFloating) {

            var routePanel = $('#sideBarColContainer');
            if (routePanel && routePanel.is(':visible')) {
                let panelMeasures = routePanel[0].getBoundingClientRect();

                if (this.tooltip.measures.left < panelMeasures.right) {
                    result = (panelMeasures.right - this.tooltip.measures.left + (isDraggable ? 10 : 20)) * -1;
                }
            } else if (this.tooltip.measures.left < 0) {
                result = this.tooltip.measures.left - this.mapBounds.left + (isDraggable ? -10 : -25);
            }
        } else if (this.tooltip.measures.left < this.mapBounds.left) {
            //embedded map has no route planner.
            result = this.tooltip.measures.left - this.mapBounds.left + (isDraggable ? -10 : -25);
        }

        var layerSelector = $('#layerSelection');

        if (layerSelector && layerSelector.is(':visible')) {
            let legendMeasures = layerSelector[0].getBoundingClientRect();
            if (this.tooltip.measures.right > legendMeasures.left) {
                result = (legendMeasures.left - this.tooltip.measures.right - (isDraggable ? 10 : 25)) * -1;
            }
        } else if (this.tooltip.measures.right > this.mapBounds.right) {
            result = this.tooltip.measures.right - this.mapBounds.right - (isDraggable ? -10 : -25) + (resources.MapControlPos === 'TOP_CENTER' ? 0 : 50);
        }
        return result;
    }

    scrollTooltipMoreInfoIntoView() {
        this.removedWhiteSpace = true;
        let infoToggle = document.getElementById("toggleDetourText");
        let tooltipScrollBox = document.getElementById("draggableBorder");
        let draggableTooltip = true;
        if (!tooltipScrollBox) {
            draggableTooltip = false;
        }

        if (draggableTooltip) {
            //auto scrolling not working well for regular tooltips, not doing it for now.
            this.setMapBounds();
            let detourCollapse = document.getElementById("detourCollapse");
            if (detourCollapse && detourCollapse.classList.contains("in")) {
                //scroll down only if the content is expanded
                if (tooltipScrollBox.scrollHeight < this.mapBottom - this.mapTop) {
                    tooltipScrollBox.style.height = tooltipScrollBox.scrollHeight + 'px';
                    tooltipScrollBox.style.maxHeight = 'unset';
                } else {
                    tooltipScrollBox.style.height = (this.mapBottom - this.mapTop) + 'px';
                    tooltipScrollBox.style.maxHeight = 'unset';
                    let scrollBy = infoToggle.getBoundingClientRect().top - tooltipScrollBox.getBoundingClientRect().top;
                    tooltipScrollBox.scroll({
                        top: scrollBy,
                        left: 0,
                        behavior: "smooth"
                    });
                }
            } else {
                tooltipScrollBox.removeAttribute("style");
            }
        }
    }
}
var ContextMenu = (function () {
    function ContextMenu(map, helper) {
        var _this = this;
        this.onTouchStart = function (e) {
            if (!_this.touchTimer) {
                _this.touchTimer = setTimeout(function () { return _this.onRightClick(e); }, 600);
            }
        };
        this.onTouchEnd = function (e) {
            if (_this.touchTimer) {
                clearTimeout(_this.touchTimer);
                _this.touchTimer = null;
                e.domEvent.preventDefault();
            }
        };
        this.onDragStart = function () {
            if (_this.touchTimer) {
                clearTimeout(_this.touchTimer);
                _this.touchTimer = null;
            }
        };
        this.onRightClick = function (e) {
            _this.touchTimer = null;
            _this.contextMenu.hide();
            if (e.domEvent.target.parentElement.getAttribute('role') === "region") {
                var clickedX = e.pixel.x;
                var clickedY = e.pixel.y;
                var clickedLatLng = e.latLng;
                _this.helper.populateMenu(_this.contextMenu, clickedX, clickedY, clickedLatLng, _this.displayMenu);
                $(document).trigger("closeDraggableWindow");
            }
        };
        this.displayMenu = function (contextString, clickedX, clickedY, clickedLatLng) {
            _this.contextMenu.empty();
            _this.contextMenu.append(contextString);
            _this.contextMenu.append('<li class="divider"><a href="#resetMarkers" class="list-group-item"><i class="far fa-redo"></i>' + resources.StartOver + '</a></li>' +
                '<li><a href="#zoomIn" class="list-group-item"><i class="far fa-plus"></i>' + resources.ZoomIn + '</a></li>' +
                '<li><a href="#zoomOut" class="list-group-item"><i class="far fa-minus"></i>' + resources.ZoomOut + '</a></li>' +
                '<li><a href="#centerMap" class="list-group-item"><i class="far fa-compress-alt"></i>' + resources.CenterHere + '</a></li>');
            _this.setUpMenuEventHandlers(clickedLatLng);
            var mapDiv = $(_this.map.getDiv());
            if (clickedX > mapDiv.width() - _this.contextMenu.width())
                clickedX -= _this.contextMenu.width();
            if (clickedY > mapDiv.height() - _this.contextMenu.height())
                clickedY -= _this.contextMenu.height();
            _this.contextMenu.css({ top: clickedY, left: clickedX }).fadeIn(300);
        };
        this.AddWaypointToContextMenu = function (value) {
            _this.helper.AddWaypointToContextMenu(value);
        };
        this.setUpMenuEventHandlers = function (clickedLatLng) {
            _this.contextMenu.find('a').click(function (e) {
                e.preventDefault();
                var action;
                if (e.target.tagName === 'FONT') {
                    action = e.target.parentElement.parentElement.getAttribute('href').substr(1);
                }
                else {
                    action = $(e.target).attr('href').substr(1);
                }
                var actionInt = _this.possiblyParseInt(action);
                if (actionInt != null) {
                    _this.helper.doAction(_this.contextMenu, e, actionInt);
                }
                else {
                    _this.contextMenu.fadeOut(75);
                    switch (action) {
                        case 'resetMarkers':
                            _this.helper.resetMarkers(_this.contextMenu);
                            break;
                        case 'zoomIn':
                            _this.map.setZoom(_this.map.getZoom() + 1);
                            _this.map.panTo(clickedLatLng);
                            break;
                        case 'zoomOut':
                            _this.map.setZoom(_this.map.getZoom() - 1);
                            _this.map.panTo(clickedLatLng);
                            break;
                        case 'centerMap':
                            _this.map.panTo(clickedLatLng);
                            break;
                    }
                }
            });
            _this.helper.setupEventHandlers(_this.contextMenu);
        };
        this.possiblyParseInt = function (val) {
            try {
                var intVal = parseInt(val);
                return isNaN(intVal) ? null : intVal;
            }
            catch (e) {
                return null;
            }
        };
        this.helper = helper;
        this.contextMenu = $(document.createElement('ul')).attr('id', 'contextMenu');
        this.contextMenu.bind('contextmenu', function () { return false; });
        this.map = map;
        $(map.getDiv()).append(this.contextMenu);
        $.each('mousedown dragstart zoom_changed maptypeid_changed'.split(' '), function (i, name) {
            google.maps.event.addListener(map, name, function () { _this.contextMenu.hide(); });
        });
        google.maps.event.addListener(map, 'contextmenu', this.onRightClick);
        google.maps.event.addListener(map, 'dragstart', this.onDragStart);
        google.maps.event.addDomListener(map, 'mousedown', this.onTouchStart);
        google.maps.event.addDomListener(map, 'mouseup', this.onTouchEnd);
    }
    return ContextMenu;
}());

var CreateDraggableInfoWindow = function (draggableOverlay) {
    var marker, draggableWindowPath, onReady, left = 0, pos, gmap;

    function adjustTooltipTop() {
        setTimeout(() => {  
            //adjust position of close tooltip btn if there is a scrollbar.
            let dragBox = document.getElementById("draggableBorder");
            if (dragBox) {
                if (dragBox.scrollHeight > dragBox.clientHeight) {
                    //put a white strip at the top in ERS mode.  When there is a scrollbar, text could show in that sliver that we don't want
                    if (document.getElementsByClassName("ersMode").length > 0) {
                        const tooltipWhiteStrip = document.createElement("div");
                        tooltipWhiteStrip.className = "tooltipWhiteStrip";
                        tooltipWhiteStrip.id = "draggableHeader";
                        dragBox.prepend(tooltipWhiteStrip);
                    }
                } 
            } else {
                //tooltip DOM is not ready yet, try again.
                adjustTooltipTop();
            }
        },10);
    }

    $(document).on("tooltip-info-toggled", () => {
        adjustTooltipTop();
    });


    function DraggableInfoWindow(map, googleMarker, layerId, content, typeMaxWidth, ready) {
        marker = googleMarker;
        gmap = map;
        onReady = ready;
        // adding height so on certain smaller rez, the tooltip display properly with scroll
        var tooltipHeight = $("#map-canvas").outerHeight() - 180;
        tooltipHeight = tooltipHeight > 400 ? 400 : tooltipHeight;
        var isLoadingContent = content.indexOf("infoWindowLoading") !== -1;
        if (!isLoadingContent) {
            content = content.replace("map-tooltip\"", "map-tooltip draggable-tooltip\" style=\"max-height:" + tooltipHeight + "px\"");
        }    
        
        //wrap content in a div to define draggable area
        let tooltipPadding = "<div id='draggableBorder' class='tooltipPadding'>";
        content = tooltipPadding.concat(content);
        content = content.concat("</div>");
        let draggableContent = '<div class="draggableWindowContainer" style="width: ' + typeMaxWidth + 'px;"><div class="draggableWindow">' + content + '<button class="fal fa-times closeDraggableWindow" aria-label="Close"></button></div></div>';

        draggableOverlay = new DraggableOverlay(map,
            marker.getPosition(),
            draggableContent);   

        draggableWindowPath = new google.maps.Polyline({
            strokeColor: '#999',
            strokeOpacity: 1.0,
            strokeWeight: 2,
            map: map
        });
        //close menu if it's open to avoid map clutter
        $("#contextMenu").hide();

        adjustTooltipTop();
    }   

    $(document).on("closeDraggableWindow", function () {

        // draggable tooltips disable map scroll when cursor is inside them.  
        // make sure to enable map scroll again after they are closed.
        gmap.set('scrollwheel', true); 
        gmap.gestureHandling = "greedy"; //re-enable map pan and zoom since it gets dissabled on tooltip hover.

        if (resources.CctvEnableVideo == "True") {
            $(document).trigger("CallRemoveVideo");
        }  
        if (draggableOverlay && draggableOverlay.container != null) draggableOverlay.onRemove();
        // close event polygen
        $(document).trigger('info-window-close'); 
    });   

    var addLeave = function (that, target, container, event, cancelevent) {
        google.maps.event.addDomListener(target, event,
            function (e) {
                var currentTouch = e.changedTouches ? e.changedTouches.item(that.touchTracker) : null;
                if (currentTouch == null && e.type == 'touchcancel') return;
                google.maps.event.trigger(container, cancelevent, e);
            });
    };

    var addMove = function (that, target, container, event) {        
        return google.maps.event.addDomListener(target, event, function (e) { 
                var currentTouch = e.changedTouches ? e.changedTouches.item(that.touchTracker) : null;
                if (currentTouch == null && e.type == 'touchmove') return;
                if (currentTouch != null && $(document.elementFromPoint(currentTouch.pageX, currentTouch.pageY)).parents('.draggableWindow').length == 0) google.maps.event.trigger(container, 'touchend', e);
                e.preventDefault();
                var origin = that.get('origin'),
                    originTouch = origin.changedTouches ? origin.changedTouches.item(that.touchTracker) : null,
                    left = e.type == 'touchmove' ? originTouch.clientX - currentTouch.clientX : origin.clientX - e.clientX,
                    top = e.type == 'touchmove' ? originTouch.clientY - currentTouch.clientY : origin.clientY - e.clientY,
                    pos = that.getProjection()
                        .fromLatLngToDivPixel(that.get('position')),
                    latLng = that.getProjection()
                        .fromDivPixelToLatLng(new google.maps.Point(pos.x - left,
                            pos.y - top));
                that.set('origin', e);
                that.set('position', latLng);
                that.draw();
                pathUpdate(marker, that);
            });
    };

    var addDown = function (that, target, event) {
        google.maps.event.addDomListener(target, event, function (e) {
            if (e.target.className.indexOf("closeDraggableWindow") != -1) { // Click close X                       
                $(document).trigger("callCloseInfoWindow");
                document.getElementsByTagName("body")[0].style.userSelect = "unset"; //allow text to be seletable after tooltip drag ends
            } else {
                e.stopPropagation(); // don't propagate tooltip events to map - held left mousedown triggers onTouchStart
                if (e.target.getAttribute("id") === "draggableBorder" || e.target.getAttribute("id") == "draggableHeader") {
                    document.getElementsByTagName("body")[0].style.userSelect = "none"; //prevent text from getting selected or highlighted while dragging tooltip
                    that.set('origin', e);

                    if (e.changedTouches) {
                        that.touchTracker = e.changedTouches[0].identifier;
                    }

                    that.moveHandler = addMove(that, that.get('map').getDiv(), target, 'mousemove');
                    that.moveTouchHandler = addMove(that, that.get('map').getDiv(), target, 'touchmove');
                }

            }
        });
    };

    var addUp = function (that, target, event) {
        google.maps.event.addDomListener(target, event, function (e) {
            document.getElementsByTagName("body")[0].style.userSelect = "unset"; //allow text to be seletable after tooltip drag ends
            var currentTouch = e.changedTouches ? e.changedTouches.item(that.touchTracker) : null;
            if (currentTouch == null && e.type == 'touchend') return;
            that.touchTracker = null;
            google.maps.event.removeListener(that.moveHandler);
            google.maps.event.removeListener(that.moveTouchHandler);
        });
    };

    //gm-svpc and gmnoprint are google map's css classes, and customMapCtrl is ours
    //we need to apply the customMapCtrl to all our controls
    let conflictingElements = ["customMapCtrl", "gm-svpc", "gmnoprint"];
    function setConflictingDomHovers(that) {
        //elements on map that conflict with tooltip drag
        conflictingElements.forEach((e) => {
            //this selects all elements on the map that don't play nice wtih draggable tooltips.
            let elements = Array.from(document.getElementsByClassName(e));
            elements.forEach((c) => {
                cancelTooltipDrag(c, that);
            });
        });
    }

    function cancelTooltipDrag(dom, that) {
        dom.addEventListener('mouseover', () => {
            google.maps.event.removeListener(that.moveHandler);
            google.maps.event.removeListener(that.moveTouchHandler);
            document.getElementsByTagName("body")[0].style.userSelect = "unset"; //allow text to be seletable after tooltip drag ends
        })
    }

    var cancelDoubleClick = function (that, target, event) {
        google.maps.event.addDomListener(target, event, function (e) {
            //documentation:
            //https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.gestureHandling 
            gmap.gestureHandling = "none";
        });
    };

    var enableDblClick = function(that, target, event) {
          google.maps.event.addDomListener(target, event, function (e) {              
            gmap.gestureHandling = "greedy";
        });
    }

    DraggableOverlay.prototype = new google.maps.OverlayView();
    DraggableOverlay.prototype.onAdd = function () {
        var container = document.createElement('div');
        that = this;
        $(container).ready(onReady);
        var content = this.get('content');
        if (typeof content.nodeName !== 'undefined') {
            container.appendChild(content);
        }
        else {
            if (typeof content === 'string') {
                container.innerHTML = content;
            }
            else {
                return;
            }
        }
        container.style.position = 'absolute';
        var isinfoWindowLoading = content.indexOf("infoWindowLoading");
        if (isinfoWindowLoading == -1) {

            addLeave(that, this.get('map').getDiv(), container, 'touchcancel', 'touchend');
            addLeave(that, this.get('map').getDiv(), container,'mouseleave', 'mouseup');
            
            addDown(that, container, 'touchstart');
            addDown(that, container, 'mousedown');
            addDown(that, container, 'contextmenu'); //brings up default context menu on draggable tooltip with "copy" option.
            setConflictingDomHovers(that);
            
            cancelDoubleClick(that, container, 'mouseenter');
            enableDblClick(that, container, 'mouseleave');
        }       

        addUp(that, container, 'touchend');
        addUp(that, container, 'mouseup');


        this.set('container', container);
        this.getPanes().floatPane.appendChild(container);
        pos = that.getProjection()
            .fromLatLngToDivPixel(that.get('position'));
        latLng = that.getProjection()
            .fromDivPixelToLatLng(new google.maps.Point(pos.x,
                pos.y));   
    };

    function DraggableOverlay(map, position, content) {
        if (typeof draw === 'function') {
            this.draw = draw;
        }
        this.setValues({
            position: position,
            container: null,
            content: content,  
            map: map
        });
    }  

    DraggableOverlay.prototype.draw = function () {
        pos = this.getProjection().fromLatLngToDivPixel(this.get('position'));
        var container = this.get('container');
        if (container) {
            container.style.left = pos.x + 'px';
            container.style.top = pos.y + 'px';
        }
    }; 

    DraggableOverlay.prototype.onRemove = function () {
        var container = this.get('container');
        if (container && container.parentNode) {
            container.parentNode.removeChild(container);
            this.set('container', null);
        }
        $(document).trigger("closeGoogleWindow");
        removePath();      
    }; 

    function removePath() {
        if (draggableWindowPath) draggableWindowPath.setMap(null);
    }

    function pathUpdate(googleMarker, container) {
        if (googleMarker && draggableOverlay) {
            var draggableWindowPoint = container.getProjection()
                .fromDivPixelToLatLng(new google.maps.Point(pos.x - left, pos.y - 40));
            var coord = [googleMarker.getPosition(), draggableWindowPoint];
            draggableWindowPath.setPath(coord);
        }
    }

    return DraggableInfoWindow;
};
/**
 * @license jahashtable, a JavaScript implementation of a hash table. It creates a single constructor function called
 * Hashtable in the global scope.
 *
 * http://www.timdown.co.uk/jshashtable/
 * Copyright 2013 Tim Down.
 * Version: 3.0
 * Build date: 17 July 2013
 *
 * 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.
 */
var Hashtable = (function (UNDEFINED) {
    var FUNCTION = 'function', STRING = 'string', UNDEF = 'undefined';

    // Require Array.prototype.splice, Object.prototype.hasOwnProperty and encodeURIComponent. In environments not
    // having these (e.g. IE <= 5), we bail out now and leave Hashtable null.
    if (typeof encodeURIComponent == UNDEF ||
        Array.prototype.splice === UNDEFINED ||
        Object.prototype.hasOwnProperty === UNDEFINED) {
        return null;
    }

    function toStr(obj) {
        return (typeof obj == STRING) ? obj : '' + obj;
    }

    function hashObject(obj) {
        var hashCode;
        if (typeof obj == STRING) {
            return obj;
        } else if (typeof obj.hashCode == FUNCTION) {
            // Check the hashCode method really has returned a string
            hashCode = obj.hashCode();
            return (typeof hashCode == STRING) ? hashCode : hashObject(hashCode);
        } else {
            return toStr(obj);
        }
    }

    function merge(o1, o2) {
        for (var i in o2) {
            if (o2.hasOwnProperty(i)) {
                o1[i] = o2[i];
            }
        }
    }

    function equals_fixedValueHasEquals(fixedValue, variableValue) {
        return fixedValue.equals(variableValue);
    }

    function equals_fixedValueNoEquals(fixedValue, variableValue) {
        return (typeof variableValue.equals == FUNCTION) ?
            variableValue.equals(fixedValue) : (fixedValue === variableValue);
    }

    function createKeyValCheck(kvStr) {
        return function (kv) {
            if (kv === null) {
                throw new Error('null is not a valid ' + kvStr);
            } else if (kv === UNDEFINED) {
                throw new Error(kvStr + ' must not be undefined');
            }
        };
    }

    var checkKey = createKeyValCheck('key'), checkValue = createKeyValCheck('value');

    /*----------------------------------------------------------------------------------------------------------------*/

    function Bucket(hash, firstKey, firstValue, equalityFunction) {
        this[0] = hash;
        this.entries = [];
        this.addEntry(firstKey, firstValue);

        if (equalityFunction !== null) {
            this.getEqualityFunction = function () {
                return equalityFunction;
            };
        }
    }

    var EXISTENCE = 0, ENTRY = 1, ENTRY_INDEX_AND_VALUE = 2;

    function createBucketSearcher(mode) {
        return function (key) {
            var i = this.entries.length, entry, equals = this.getEqualityFunction(key);
            while (i--) {
                entry = this.entries[i];
                if (equals(key, entry[0])) {
                    switch (mode) {
                    case EXISTENCE:
                        return true;
                    case ENTRY:
                        return entry;
                    case ENTRY_INDEX_AND_VALUE:
                        return [i, entry[1]];
                    }
                }
            }
            return false;
        };
    }

    function createBucketLister(entryProperty) {
        return function (aggregatedArr) {
            var startIndex = aggregatedArr.length;
            for (var i = 0, entries = this.entries, len = entries.length; i < len; ++i) {
                aggregatedArr[startIndex + i] = entries[i][entryProperty];
            }
        };
    }

    Bucket.prototype = {
        getEqualityFunction: function (searchValue) {
            return (typeof searchValue.equals == FUNCTION) ? equals_fixedValueHasEquals : equals_fixedValueNoEquals;
        },

        getEntryForKey: createBucketSearcher(ENTRY),

        getEntryAndIndexForKey: createBucketSearcher(ENTRY_INDEX_AND_VALUE),

        removeEntryForKey: function (key) {
            var result = this.getEntryAndIndexForKey(key);
            if (result) {
                this.entries.splice(result[0], 1);
                return result[1];
            }
            return null;
        },

        addEntry: function (key, value) {
            this.entries.push([key, value]);
        },

        keys: createBucketLister(0),

        values: createBucketLister(1),

        getEntries: function (destEntries) {
            var startIndex = destEntries.length;
            for (var i = 0, entries = this.entries, len = entries.length; i < len; ++i) {
                // Clone the entry stored in the bucket before adding to array
                destEntries[startIndex + i] = entries[i].slice(0);
            }
        },

        containsKey: createBucketSearcher(EXISTENCE),

        containsValue: function (value) {
            var entries = this.entries, i = entries.length;
            while (i--) {
                if (value === entries[i][1]) {
                    return true;
                }
            }
            return false;
        }
    };

    /*----------------------------------------------------------------------------------------------------------------*/

    // Supporting functions for searching hashtable buckets

    function searchBuckets(buckets, hash) {
        var i = buckets.length, bucket;
        while (i--) {
            bucket = buckets[i];
            if (hash === bucket[0]) {
                return i;
            }
        }
        return null;
    }

    function getBucketForHash(bucketsByHash, hash) {
        var bucket = bucketsByHash[hash];

        // Check that this is a genuine bucket and not something inherited from the bucketsByHash's prototype
        return (bucket && (bucket instanceof Bucket)) ? bucket : null;
    }

    /*----------------------------------------------------------------------------------------------------------------*/

    function Hashtable() {
        var buckets = [];
        var bucketsByHash = {};
        var properties = {
            replaceDuplicateKey: true,
            hashCode: hashObject,
            equals: null
        };

        var arg0 = arguments[0], arg1 = arguments[1];
        if (arg1 !== UNDEFINED) {
            properties.hashCode = arg0;
            properties.equals = arg1;
        } else if (arg0 !== UNDEFINED) {
            merge(properties, arg0);
        }

        var hashCode = properties.hashCode, equals = properties.equals;

        this.properties = properties;

        this.put = function (key, value) {
            checkKey(key);
            checkValue(value);
            var hash = hashCode(key), bucket, bucketEntry, oldValue = null;

            // Check if a bucket exists for the bucket key
            bucket = getBucketForHash(bucketsByHash, hash);
            if (bucket) {
                // Check this bucket to see if it already contains this key
                bucketEntry = bucket.getEntryForKey(key);
                if (bucketEntry) {
                    // This bucket entry is the current mapping of key to value, so replace the old value.
                    // Also, we optionally replace the key so that the latest key is stored.
                    if (properties.replaceDuplicateKey) {
                        bucketEntry[0] = key;
                    }
                    oldValue = bucketEntry[1];
                    bucketEntry[1] = value;
                } else {
                    // The bucket does not contain an entry for this key, so add one
                    bucket.addEntry(key, value);
                }
            } else {
                // No bucket exists for the key, so create one and put our key/value mapping in
                bucket = new Bucket(hash, key, value, equals);
                buckets.push(bucket);
                bucketsByHash[hash] = bucket;
            }
            return oldValue;
        };

        this.get = function (key) {
            checkKey(key);

            var hash = hashCode(key);

            // Check if a bucket exists for the bucket key
            var bucket = getBucketForHash(bucketsByHash, hash);
            if (bucket) {
                // Check this bucket to see if it contains this key
                var bucketEntry = bucket.getEntryForKey(key);
                if (bucketEntry) {
                    // This bucket entry is the current mapping of key to value, so return the value.
                    return bucketEntry[1];
                }
            }
            return null;
        };

        this.containsKey = function (key) {
            checkKey(key);
            var bucketKey = hashCode(key);

            // Check if a bucket exists for the bucket key
            var bucket = getBucketForHash(bucketsByHash, bucketKey);

            return bucket ? bucket.containsKey(key) : false;
        };

        this.containsValue = function (value) {
            checkValue(value);
            var i = buckets.length;
            while (i--) {
                if (buckets[i].containsValue(value)) {
                    return true;
                }
            }
            return false;
        };

        this.clear = function () {
            buckets.length = 0;
            bucketsByHash = {};
        };

        this.isEmpty = function () {
            return !buckets.length;
        };

        var createBucketAggregator = function (bucketFuncName) {
            return function () {
                var aggregated = [], i = buckets.length;
                while (i--) {
                    buckets[i][bucketFuncName](aggregated);
                }
                return aggregated;
            };
        };

        this.keys = createBucketAggregator('keys');
        this.values = createBucketAggregator('values');
        this.entries = createBucketAggregator('getEntries');

        this.remove = function (key) {
            checkKey(key);

            var hash = hashCode(key), bucketIndex, oldValue = null;

            // Check if a bucket exists for the bucket key
            var bucket = getBucketForHash(bucketsByHash, hash);

            if (bucket) {
                // Remove entry from this bucket for this key
                oldValue = bucket.removeEntryForKey(key);
                if (oldValue !== null) {
                    // Entry was removed, so check if bucket is empty
                    if (bucket.entries.length == 0) {
                        // Bucket is empty, so remove it from the bucket collections
                        bucketIndex = searchBuckets(buckets, hash);
                        buckets.splice(bucketIndex, 1);
                        delete bucketsByHash[hash];
                    }
                }
            }
            return oldValue;
        };

        this.size = function () {
            var total = 0, i = buckets.length;
            while (i--) {
                total += buckets[i].entries.length;
            }
            return total;
        };
    }

    Hashtable.prototype = {
        each: function (callback) {
            var entries = this.entries(), i = entries.length, entry;
            while (i--) {
                entry = entries[i];
                callback(entry[0], entry[1]);
            }
        },

        equals: function (hashtable) {
            var keys, key, val, count = this.size();
            if (count == hashtable.size()) {
                keys = this.keys();
                while (count--) {
                    key = keys[count];
                    val = hashtable.get(key);
                    if (val === null || val !== this.get(key)) {
                        return false;
                    }
                }
                return true;
            }
            return false;
        },

        putAll: function (hashtable, conflictCallback) {
            var entries = hashtable.entries();
            var entry, key, value, thisValue, i = entries.length;
            var hasConflictCallback = (typeof conflictCallback == FUNCTION);
            while (i--) {
                entry = entries[i];
                key = entry[0];
                value = entry[1];

                // Check for a conflict. The default behaviour is to overwrite the value for an existing key
                if (hasConflictCallback && (thisValue = this.get(key))) {
                    value = conflictCallback(key, thisValue, value);
                }
                this.put(key, value);
            }
        },

        clone: function () {
            var clone = new Hashtable(this.properties);
            clone.putAll(this);
            return clone;
        }
    };

    Hashtable.prototype.toQueryString = function () {
        var entries = this.entries(), i = entries.length, entry;
        var parts = [];
        while (i--) {
            entry = entries[i];
            parts[i] = encodeURIComponent(toStr(entry[0])) + '=' + encodeURIComponent(toStr(entry[1]));
        }
        return parts.join('&');
    };

    return Hashtable;
})();
var IconManager = function (map, markerClusterer) {
    var publicItem = {};

    var iconLayers = [];
    var mapFctns = new MapFctns();

    //see IconLayer comments for more details.
    publicItem.AddIconLayer = function (layerId, icons, updateFunction, markerBuilder, layerFilter, layerOptions) {
        //make sure layer doesnt exist already.
        if (getIndexOfLayerId(layerId) != -1) {
            throw 'layerid is already active.';
        }

        //init new IconLayer.
        var iconLayer = new IconLayer(layerId, icons, updateFunction, markerBuilder, layerFilter, layerOptions);
        iconLayers.push(iconLayer);

        //update marker clusterer with new layer.
        iconLayer.MapChanged(map, mapFctns, markerClusterer);

        //trigger layer added event.
        $(document).trigger('layerAdded-iconManager', [layerId, icons]);
    };

    publicItem.RemoveIconLayer = function (layerId) {
        var index = getIndexOfLayerId(layerId);

        //index found.
        if (index != -1) {
            //hide all icons and remove layer from collection.
            iconLayers[index].Delete(markerClusterer);
            iconLayers.splice(index, 1);
        }
    };

    publicItem.GetIconLayer = function(layerId) {
        var index = getIndexOfLayerId(layerId);
        if (index != -1) {
            return iconLayers[index];
        }
    };
    //causes all icon layers to refresh.
    publicItem.RefreshLayers = function () {
        var count = iconLayers.length;
        var eventCount = 0;

        //if no layers to refresh.
        if (!count) {
            $(document).trigger('layersRefreshed-iconManager');
            return;
        }

        //refresh each layer.
        for (var i in iconLayers) {
            iconLayers[i].RefreshLayer(map, mapFctns, markerClusterer).done(function () {
                //once all layers refreshed.
                if (++eventCount == count) {
                    $(document).trigger('layersRefreshed-iconManager');
                }
            });
        }
    };

    //refresh just one layers data. returns a promise.
    publicItem.RefreshLayer = function (layerId, skipCache) {
        var index = getIndexOfLayerId(layerId);

        //index found.
        if (index != -1) {
            //return deferred promise.
            return iconLayers[index].RefreshLayer(map, mapFctns, markerClusterer, skipCache);
        }

        //just return a resolved deferred promise.
        var dfd = jQuery.Deferred();
        dfd.resolve();
        return dfd.promise();
    };

    //call when map zoom or bounds have changed.
    publicItem.MapChanged = function () {
        for (var i in iconLayers) {
            iconLayers[i].MapChanged(map, mapFctns, markerClusterer);
        }

        $(document).trigger('mapChangedFinished-iconManager');
    };

    //nothing is redrawn until this is called.
    publicItem.Redraw = function () {
        markerClusterer.repaint();
        $(document).trigger('markerClustererRepainted');
    };

    var getIndexOfLayerId = function (layerId) {
        //for each layer in polylineLayers.
        for (var i in iconLayers) {
            if (iconLayers[i].layerId === layerId) {
                return i;
            }
        }

        return -1;
    };
    return publicItem;
};

/*
 * icons is a collection of objects which have an itemId and a location array where location[0] is icon latitude, and 
 * location[1] is icon longitude.
 *
 * updateFunction is a function which takes the layerId as it's first parameter. it fetches a collection of icons which
 * have the properties mentioned above. it returns a deferred promise.
 *
 * markerBuilder is a function which takes a layerId as its first parameter and and icon object which has the properties
 * mentioned above as its second parameter. it returns google.maps.Marker object.
 *
 * layerFilter is the ColumnFilters object for this layer. It contains the getFilter method defined in ColumnFilters.ts,
 * which allows the layer to get it's latest filter on refresh. It also contains filterDataTable, which is the
 * DataTableParams object that is passed into the NoSessionController.
 */
var IconLayer = function (layerId, iconData, updateFunction, markerBuilder, layerFilter, layerOptions) {
    var publicItem = {};

    publicItem.layerId = layerId;

    var iconItemHash = new Hashtable();
    var deleted = false;
    var icons = iconData.item2;
    var defaultIcon = iconData.item1;
    var minZoom = 0;
    var maxZoom = 22;

    if (layerOptions) {
        if (layerOptions.minZoom) {
            minZoom = layerOptions.minZoom;
        }
        if (layerOptions.maxZoom) {
            maxZoom = layerOptions.maxZoom;
        }
    }

    //gets a fresh set of icons and updates marker clusterer.
    publicItem.RefreshLayer = function (map, mapFctns, markerClusterer, skipCache) {
        var dfd = jQuery.Deferred();

        updateFunction(layerId, skipCache, layerFilter).done(function (freshIcons) {
            //if layer was not removed since updateFunction called.
            if (!deleted) {
                icons = freshIcons.item2;
                defaultIcon = freshIcons.item1;
                updateIconItemHashAndMarkerClusterer(map, mapFctns, markerClusterer);
            }
        }).always(function (freshIcons) {
            //trigger event either way. resolve the deferred object.
            $(document).trigger('layerRefreshed-iconManager.' + layerId, [layerId, freshIcons]);
            dfd.resolve();
        });

        //return deferred promise.
        return dfd.promise();
    };

    //call when zoom or bounds changed. also used as init.
    publicItem.MapChanged = function (map, mapFctns, markerClusterer) {
            updateIconItemHashAndMarkerClusterer(map, mapFctns, markerClusterer);
    };

    //function to call when layer should be removed.
    publicItem.Delete = function (markerClusterer) {
        deleted = true;
        hideAll(markerClusterer);

        //trigger delete event.
        $(document).trigger('layerDeleted-iconManager', [layerId]);
    };

    publicItem.GetIcons = function () {
        var keys = iconItemHash.keys();
        var toReturn = [];
        for (var i = 0; i < keys.length; i++) {
            var hashItem = iconItemHash.get(keys[i]);
            toReturn.push(hashItem.gmapsMarker);
        }
        return toReturn;
    };

    var hideAll = function (markerClusterer) {
        var keys = iconItemHash.keys();
        var gmapMarkersToRemove = new Array();

        //for each layer icon.
        for (var i = 0; i < keys.length; i++) {
            var hashItem = iconItemHash.get(keys[i]);

            //if icon is active, add it to remove collection.
            if (hashItem.status == 1) {
                gmapMarkersToRemove.push(hashItem.gmapsMarker);
                hashItem.status = 0; //set to inactive status.
            }
        }

        //remove from marker clusterer.
        markerClusterer.removeMarkers(gmapMarkersToRemove, true);
    };

    var mergeIcon = function (baseIcon, defaultIcon) {
        if (!baseIcon.merged) {
            if (defaultIcon.url && !baseIcon.icon.url) baseIcon.icon.url = defaultIcon.url;
            if (defaultIcon.json && (!baseIcon.icon.json || baseIcon.icon.json[0] != '{')) baseIcon.icon.json = defaultIcon.json.replace("{jsondata}", baseIcon.icon.json);
            baseIcon.zindex = defaultIcon.zindex;
            if (typeof (baseIcon.icon.anchor) === 'undefined') baseIcon.icon.anchor = defaultIcon.anchor;
            if (typeof (baseIcon.icon.origin) === 'undefined') baseIcon.icon.origin = defaultIcon.origin;
            if (typeof (baseIcon.icon.size) === 'undefined') baseIcon.icon.size = defaultIcon.size;
            baseIcon.merged = true;
        }
        return baseIcon;
    };

    var updateIconItemHashAndMarkerClusterer = function (map, mapFctns, markerClusterer) {
        //collections.
        var gmapMarkersToAdd = new Array();
        var gmapMarkersToRemove = new Array();

        //all the item id we encountered.
        var keysEncountered = {};

        //all the item id that should be visible.
        var visibleKeys = {};

        //bounds of the map.
        var mapBoundsObject = mapFctns.GetMapBoundsObject(map);
        var mapZoom = map.getZoom();
        for (var i in icons) {
            icons[i] = mergeIcon(icons[i], defaultIcon);
            //By adding the icon url and location to the itemId that gets added to the icon hash, we can get the map icons to update on auto refresh.
            //This may not be the best method of getting map icons to update, but it will do for now.
            var itemId = layerId + "-" + icons[i].type + "-" + icons[i].itemId + "-" + icons[i].icon.url + "-" + icons[i].location[0] + "-" + icons[i].location[1];

            //make note of encountered key.
            keysEncountered[itemId] = 1;

            //if not in bounds, skip.
            if (mapZoom < minZoom || mapZoom > maxZoom || !isIconInBounds(icons[i], mapBoundsObject, mapFctns)) {
                continue;
            }

            //make note that this key should be visible.
            visibleKeys[itemId] = 1;

            //see if we have a hashed IconItem for this 
            var hashedIconItem = iconItemHash.get(itemId);

            //if hash table does not contain item. add it.
            if (!hashedIconItem) {
                hashedIconItem = new IconItem(icons[i], markerBuilder(layerId, icons[i]));
                iconItemHash.put(itemId, hashedIconItem);
            }
        }

        //for each item in hash table.
        var keys = iconItemHash.keys();
        for (var i in keys) {
            var key = keys[i];
            var iconItem = iconItemHash.get(key);

            //if item is not currently visible.
            if (iconItem.status == 0) {
                //if key no longer exists, remove it from hash table.
                if (!keysEncountered[key]) {
                    iconItemHash.remove(key);
                }
                //if key represents an icon to be made visible, add it to gmapMarkersToAdd.
                else if (visibleKeys[key]) {
                    iconItem.status = 1; //set to visible status.
                    gmapMarkersToAdd.push(iconItem.gmapsMarker);
                }
            } else {
                //if key no longer exists, add icon it represents to gmapMarkersToRemove and remove it from hash table.
                if (!keysEncountered[key]) {
                    gmapMarkersToRemove.push(iconItem.gmapsMarker);
                    iconItemHash.remove(key);
                }
                //if item is to no longer be visible, add it to gmapMarkersToRemove.
                else if (!visibleKeys[key]) {
                    iconItem.status = 0; //set to inactive status.

                    gmapMarkersToRemove.push(iconItem.gmapsMarker);
                }
            }
        }        

        //add and remove appropriate markers, but do not redraw.
        markerClusterer.removeMarkers(gmapMarkersToRemove, true);
        markerClusterer.addMarkers(gmapMarkersToAdd, true);
        //only trigger added if we actually did something
        if (iconItemHash.values().length > 0) {
            $(document).trigger('layerRefreshed-iconsAdded.' + layerId, [layerId, gmapMarkersToAdd]);
        }
    };

    var isIconInBounds = function (icon, mapBoundsObject, mapFctns) {
        //add 0.25 degrees to the range for icons slightly off the map
        return mapFctns.CoordinateIsContained(icon.location[0], icon.location[1], mapBoundsObject);
    };

    return publicItem;
}; //used in IconLayer.
var IconItem = function (icon, gmapsMarker) {
    var item = {};

    item.icon = icon;
    item.gmapsMarker = gmapsMarker;
    item.status = 0;

    return item;
};
var KmlManager = function (map, appHelper) {
    var publicItem = {};

    //holds active layers.
    var layers = new Hashtable();

    //kmlConfig object with properties url, clickable, and suppressInfoWindow.
    publicItem.AddLayer = function (layerId, config) {
        //layer already present.
        if (layers.get(layerId)) return;

        if (config.type == "Kml") {
            //google kml options.
            var kmlOptions = {
                clickable: config.clickable,
                suppressInfoWindows: true,  //always suppress kml infowindows
                preserveViewport: true,
                map: map
            };

            if (config.url) {
                var url = URI(config.url)
                //default cache time is 5 minutes
                var cacheTime = 5;
                if (config.cacheTime > 0) {
                    //config value is in seconds
                    cacheTime = config.cacheTime / 60;
                }
                url.addSearch("t", roundDateToDuration(moment(), moment.duration(cacheTime, 'minutes'), 'floor').unix());
                url.addSearch('lang', Cookies.get("_culture")).toString();

                //create and save layer in layers set.
                var kmlLayer = new google.maps.KmlLayer(url.toString(), kmlOptions);

                //hook native kml infowindow and handle it with our own function if infowindows are enabled for the layer
                if (!config.suppressInfoWindow) {
                    kmlLayer.addListener('click', function (kmlEvent) {
                        appHelper.showInfoWindow(kmlEvent.featureData.description, null, true, kmlEvent.latLng, layerId, null, false, kmlEvent.pixelOffset);
                    });
                }
                layers.put(layerId, kmlLayer);
            }

        } else if (config.type == "GeoJson") {
            var layer = new google.maps.Data();
            layer.loadGeoJson(config.url);
            if (config.styleOptions) layer.setStyle(JSON.parse(config.styleOptions));
            layer.setMap(map);
            layers.put(layerId, layer);

            if (!config.suppressInfoWindow) {
                layer.addListener('click', function (event) {
                    appHelper.showInfoWindow(eval('`' + config.infoWindowTemplate + '`'), null, true, event.latLng, null, null, true);
                });
            }
        }
    };

    publicItem.RemoveLayer = function (layerId) {
        //layer not found.
        if (!layers.get(layerId)) return;

        //get kml layer.
        var layer = layers.get(layerId);
        layer.setMap(null);

        //remove from collection.
        layers.remove(layerId);
    };

    publicItem.GetLayer = function (layerId) {
            return layers.get(layerId)
    };

    publicItem.Refresh = function (feed) {
        var layerIds = [...layers.keys()];
        for (var i = 0; i < layerIds.length; i++) {
            publicItem.RemoveLayer(layerIds[i]);
        }

        for (var i = 0; i < layerIds.length; i++) {
            publicItem.AddLayer(layerIds[i], feed[layerIds[i]]);
        }
    }

    return publicItem;
};
var MapFctns = function () {
    //this a singleton class. return singleton if it exists.
    if (MapFctns.instance) {
        return MapFctns.instance;
    }

    //public closure item.
    var publicItem = {};

    //returns true if a set of coordinates are within specified boundaries
    publicItem.CoordinateIsContained = function (lat, lng, bounds) {
        //check mins, maxes and return true if contained
        return bounds.contains(new google.maps.LatLng(lat, lng));
    };

    publicItem.GetMapBoundsObject = function (map) {
        //return item.        

        return map.getBounds();
    };

    publicItem.GetMapSwLatLng = function (map) {
        return map.getBounds().getSouthWest();
    };
    
    publicItem.GetMapSpanLatLng = function (map) {
        return map.getBounds().toSpan();
    };

    //function to add points from individual rings
    function AddPoints(data) {
        //first spilt the string into individual points
        var pointsData = data.split(",");
        var ptsArray = [];

        //iterate over each points data and create a latlong
        //& add it to the cords array
        var len = pointsData.length;
        for (var i = 0; i < len; i++) {
            var xy = pointsData[i].trim().split(" ");

            var pt = new google.maps.LatLng(xy[1], xy[0]);

            ptsArray.push(pt);
        }
        return ptsArray;
    }

    publicItem.GetPolygonFromWKT = function (wkt) {
        if (wkt == '') return;

        var gmapPolygons = [];

        var splitPolygons = false;
        if (wkt.indexOf("MULTIPOLYGON") !== -1) {
            splitPolygons = true;
            wkt = wkt.substring(wkt.indexOf("(") + 1, wkt.length - 1);
        }

        var polygonRegex = /\)\)(\s?,\s?)\(\(/g;
        var ringRegex = /\(([^()]+)\)/g;

        var polygons = [];

        var results;

        if (splitPolygons) {
            var lastEnd = 0;
            while (results = polygonRegex.exec(wkt)) {
                var rings = [];
                var polygonWKT = wkt.substring(lastEnd, results.index + 2);
                lastEnd = results.index + 2;

                while (results = ringRegex.exec(polygonWKT)) {
                    rings.push(AddPoints(results[1]));
                }
                polygons.push(rings);
            }

            //the regex doesn't match on the last polygon
            rings = [];
            polygonWKT = wkt.substring(lastEnd);

            while (results = ringRegex.exec(polygonWKT)) {
                rings.push(AddPoints(results[1]));
            }
            polygons.push(rings);

        }
        else {
            rings = [];
            while (results = ringRegex.exec(wkt)) {
                rings.push(AddPoints(results[1]));
            }
            polygons.push(rings);
        }

        for (var i = 0; i < polygons.length; i++) {
            var poly = new google.maps.Polygon({
                paths: polygons[i],
                strokeColor: resources["EventPolygonStrokeColor"],
                strokeOpacity: resources["EventPolygonStrokeOpacity"],
                strokeWeight: resources["EventPolygonStrokeWeight"],
                fillColor: resources["EventPolygonFillColor"],
                fillOpacity: resources["EventPolygonFillOpacity"]
            });

            gmapPolygons.push(poly);
        }
        return gmapPolygons;
    }

    publicItem.getPolygonBounds = function (polygons) {
        var bounds = new google.maps.LatLngBounds();
        for (var i = 0; i < polygons.length; i++) {
            var paths = polygons[i].getPaths();
            var path;
            for (var i = 0; i < paths.getLength(); i++) {
                path = paths.getAt(i);
                for (var ii = 0; ii < path.getLength(); ii++) {
                    bounds.extend(path.getAt(ii));
                }
            }
        }
        return bounds;
    }

    //startPoint and endPoint are objects with two properties: latitude and longitude.
    publicItem.fitMapToRoute = function (map, waypoints) {
        //create the google bounds object.
        var bounds = new google.maps.LatLngBounds();

        /*
        if the legend is open after we fit the map to the bounds then we need to resize to make sure 
        the route doesn't get hidden underneath it. this listener will fire only once and then it
        is unbound.
        */
        var boundsListener = google.maps.event.addListenerOnce(map, 'bounds_changed', function (event) {
            var ne = bounds.getNorthEast();
            var sw = bounds.getSouthWest();
            var minLong = sw.lng();
            var maxLong = ne.lng();

            var minLongitudeStart = minLong;
            var maxLongitudeEnd = maxLong;

            //get the zoom level after zooming
            var z = map.getZoom();
            var degreesPerTile = 360 / (Math.pow(2, z));
            var degreesPerPx = degreesPerTile / 256;

            //get route planner jquery object.
            var routePanel = $('#sideBarColContainer');
            //is route planner visible?
            if (routePanel && routePanel.is(':visible') && Modernizr.mq('(min-width: 993px)')) {
                /*
                subtract from the min longitude so that bounds include the extra width of 
                space on the left side where the route planner appears. in this manner the 
                route is completely visible and is not hidden beneath the route planner.
                */
                minLongitudeStart = minLongitudeStart - (degreesPerPx * routePanel.outerWidth(true));
            }

            //get legend jquery object.
            var layerSelector = $('#layerSelection');
            //is legend visible? and not mobile screen
            if (layerSelector && layerSelector.is(':visible') && Modernizr.mq('(min-width: 993px)')) {
                /*
                add to the max longitude so that bounds include the extra width of
                space on the right side where the legend appears. in this manner the 
                route is completely visible and is not hidden beneath the legend.
                */
                maxLongitudeEnd = maxLongitudeEnd + (degreesPerPx * layerSelector.outerWidth(true));
            }

            //zoom (pan across a bit) to take account of the added bits
            zoomToPointBounds(map, ne.lat(), minLongitudeStart, sw.lat(), maxLongitudeEnd);
        });

        //do an initial attempt to fit the map to the route boundaries.
        bounds = zoomToBounds(map, waypoints, bounds);

        //just in case no matter what remove the listener after 3 seconds.
        setTimeout(function () { google.maps.event.removeListener(boundsListener); }, 3000);
    };

    var zoomToBounds = function (map, waypoints, bounds) {
        for (var i = 0; i < waypoints.length; i++)
        {
            bounds.extend(new google.maps.LatLng(waypoints[i].point.latitude, waypoints[i].point.longitude));
        }

        //fit the map to the bounds.
        map.fitBounds(bounds);
        map.panBy(0, 0);
        return bounds;
    };

    //helper for fitMapToRoute.
    var zoomToPointBounds = function (map, minLat, minLon, maxLat, maxLon) {
        //create the google bounds object.
        var bounds = new google.maps.LatLngBounds();

        //extend the bounds to fit these two points.
        bounds.extend(new google.maps.LatLng(minLat, minLon));
        bounds.extend(new google.maps.LatLng(maxLat, maxLon));

        //fit the map to the bounds.
        map.fitBounds(bounds);
        map.panBy(0, 0);
    };

    //set the instance.
    MapFctns.instance = publicItem;

    //return public api.
    return publicItem;
}
//https://mapstyle.withgoogle.com/ online tool to style 
let mapModeStyles = {
    dark: function (displayPois) {
        return this.darklLblsBase(displayPois).concat([
            {
                "elementType": "geometry",
                "stylers": [
                    {
                        "color": "#242f3e"
                    }
                ]
            },
            {
                "featureType": "poi.park",
                "elementType": "geometry",
                "stylers": [
                    {
                        "color": "#263c3f"
                    }
                ]
            },
            {
                "featureType": "road",
                "elementType": "geometry",
                "stylers": [
                    {
                        "color": "#38414e"
                    }
                ]
            },
            {
                "featureType": "road",
                "elementType": "geometry.stroke",
                "stylers": [
                    {
                        "color": "#212a37"
                    }
                ]
            },

            {
                "featureType": "road.highway",
                "elementType": "geometry",
                "stylers": [
                    {
                        "color": "#746855"
                    }
                ]
            },
            {
                "featureType": "road.highway",
                "elementType": "geometry.stroke",
                "stylers": [
                    {
                        "color": "#1f2835"
                    }
                ]
            },

            {
                "featureType": "transit",
                "elementType": "geometry",
                "stylers": [
                    {
                        "color": "#2f3948"
                    }
                ]
            },

            {
                "featureType": "water",
                "elementType": "geometry",
                "stylers": [
                    {
                        "color": "#17263c"
                    }
                ]
            }
        ]);
    },
    light: function(displayPois, includeOptionalStyles) {

        let result = [
            {
                featureType: 'all',
                elementType: 'labels',
                stylers: [
                    { visibility: 'on' } //This is on to avoid labels control issue in Satellite mode.
                ]
            },
            {
                featureType: 'poi',
                elementType: 'labels',
                stylers: [
                    { visibility: displayPois }
                ]
            },
            {
                featureType: "administrative.province",
                elementType: "geometry.stroke",
                stylers: [
                    {
                        visibility: "on"
                    },
                    {
                        gamma: "10.00"
                    },
                    {
                        lightness: "-100"
                    },
                    {
                        weight: "1.13"
                    },
                    {
                        color: "#000080" //navy blue
                    }
                ]
            }
        ];

        if (includeOptionalStyles) {
            result = result.concat(this.satelliteStyles());
        }

        return result;
    },
    lightLabelsLayer: function (displayPois) {
        return this.allLblsBase(displayPois)
    },
    satelliteLabels: function(displayPois) {
        return this.allLblsBase(displayPois).concat(this.satelliteStyles()); 
    },
    satelliteStyles: function() {
        return [{ elementType: "labels.text.fill", stylers: [{ color: "#ffffff" }] },
        { elementType: "labels.text.stroke", stylers: [{ color: "#222222" }] }]
    },
    darklLblsBase: function(displayPois) {
        return [{
            "elementType": "labels.text.fill",
            "stylers": [
                {
                    "color": "#DEC69F"
                }
            ]
        }, {
            "elementType": "labels.text.stroke",
            "stylers": [
                {
                    "color": "#242f3e"
                },
                {
                    "saturation": -100
                },
                {
                    "lightness": -100
                },
                {
                    "weight": 0
                }
            ]
        }, {
            "featureType": "administrative.locality",
            "elementType": "labels.text.fill",
            "stylers": [
                {
                    "color": "#e5bfa1"
                }
            ]
            },
            {
                featureType: "administrative.province",
                elementType: "geometry.stroke",
                stylers: [
                    {
                        visibility: "on"
                    },
                    {
                        gamma: "10.00"
                    },
                    {
                        lightness: "-100"
                    },
                    {
                        weight: "1.13"
                    },
                    {
                        color: "#d49665"
                    }
                ]
            },
            {
                "featureType": "administrative.country",
                "elementType": "geometry.stroke",
                "stylers": [{
                    visibility: "on"
                    },
                    {
                        gamma: "10.00"
                    },
                    {
                        lightness: "-100"
                    },
                    {
                        weight: "1.13"
                    },
                    {
                        "color": "#d49665"
                    }
                ]
            },
        {
            "featureType": "poi",
            "elementType": "labels.text.fill",
            "stylers": [
                {
                    "color": "#d59563"
                }
            ]
        }, {
            "featureType": "poi.park",
            "elementType": "labels.text.fill",
            "stylers": [
                {
                    "color": "#98C862"
                }
            ]
        }, {
            "featureType": "road",
            "elementType": "labels.text.fill",
            "stylers": [
                {
                    "color": "#E0E0E0"
                }
            ]
        }, {
            "featureType": "road.highway",
            "elementType": "labels.text.fill",
            "stylers": [
                {
                    "color": "#f3d19c"
                }
            ]
        }, {
            "featureType": "transit.station",
            "elementType": "labels.text.fill",
            "stylers": [
                {
                    "color": "#e5bfa1"
                }
            ]
        }, {
            "featureType": "water",
            "elementType": "labels.text.fill",
            "stylers": [
                {
                    "color": "#C9C9C9"
                }
            ]
        }, {
            "featureType": "water",
            "elementType": "labels.text.stroke",
            "stylers": [
                {
                    "color": "#9E9E9E"
                }
            ]
        }, {
            featureType: 'poi',
            elementType: 'labels',
            stylers: [
                { visibility: displayPois }
            ]
        }
        ]

    },
    allLblsBase: function(displayPois) {
        return [
            {
                featureType: 'all',
                stylers: [
                    { visibility: 'off' }
                ]
            },
            {
                featureType: 'administrative',
                elementType: 'labels',
                stylers: [
                    { visibility: 'on' }
                ]
            },
            {
                featureType: 'landscape',
                elementType: 'labels',
                stylers: [
                    { visibility: 'on' }
                ]
            },
            {
                featureType: 'poi',
                elementType: 'labels',
                stylers: [
                    { visibility: displayPois }
                ]
            },
            {
                featureType: 'road',
                elementType: 'labels',
                stylers: [
                    { visibility: 'on' }
                ]
            },
            {
                featureType: 'transit',
                elementType: 'labels',
                stylers: [
                    { visibility: 'on' }
                ]
            },
            {
                featureType: 'water',
                elementType: 'labels',
                stylers: [
                    { visibility: 'on' }
                ]
            }
        ]
    },
    darkLabelsLayer: function(displayPois) {

        return this.allLblsBase(displayPois).concat(this.darklLblsBase(displayPois));
    }
}
// Custom save map view control
var SaveMapViewControl = function (controlDiv, map) {

    controlDiv.className = 'saveMapViewControlContainer customMapCtrl';

    // Set CSS for the control wrapper
    var controlWrapper = document.createElement('div');
    controlWrapper.className = "saveMapViewControl";
    controlDiv.appendChild(controlWrapper);

    // Set CSS for save map view btn
    var saveMapViewBtn = document.createElement('i');
    saveMapViewBtn.className = "far fa-save";
    saveMapViewBtn.setAttribute('title', resources.SaveMapView);
    saveMapViewBtn.setAttribute("aria-label", resources.SaveMapView);
    saveMapViewBtn.setAttribute("role", 'button');
    saveMapViewBtn.setAttribute("tabindex", "0");
    controlWrapper.appendChild(saveMapViewBtn);

    // Set CSS for go to map view btn
    var goToMapViewBtn = document.createElement('i');
    goToMapViewBtn.className = "far fa-map-marked";
    goToMapViewBtn.setAttribute('title', resources.GoToMapView);
    goToMapViewBtn.setAttribute("aria-label", resources.GoToMapView);
    goToMapViewBtn.setAttribute("role", "button");
    goToMapViewBtn.setAttribute("tabindex", "0");
    controlWrapper.appendChild(goToMapViewBtn);

    // Setup the click event listener - saveMapViewBtn
    google.maps.event.addDomListener(saveMapViewBtn, 'click', function () {
        saveMapViewInfo(map);
    });

    google.maps.event.addDomListener(saveMapViewBtn, 'keydown', function (e) {
        if (e.code === 'Enter'  || e.code === "Space") {
            saveMapViewInfo(map);
        }
    });

    // Setup the click event listener - goToMapViewBtn
    google.maps.event.addDomListener(goToMapViewBtn, 'click', function () {
        setMapViewInfo(map);
    });

    google.maps.event.addDomListener(goToMapViewBtn, 'keydown', function (e) {
        if (e.code === 'Enter' || e.code === "Space") {
            setMapViewInfo(map);
        }
    });  
};

var saveMapViewInfo = function (map) {
    var location = { lat: map.getCenter().lat(), lng: map.getCenter().lng(), zoom: map.getZoom() };
    Cookies.set("_saveMapView", JSON.stringify(location), { expires: 365, path: '/' });
    $(".map-feedback-msg span").html(resources.SaveMapViewSuccess);
    $(".map-feedback-msg").show().delay(3000).fadeOut();
}

var setMapViewInfo = function (map) {
    if (Cookies.get("_saveMapView") != null) {
        var locationFromCookie = JSON.parse(Cookies.get("_saveMapView"));
        var location = new google.maps.LatLng(locationFromCookie.lat, locationFromCookie.lng);
        map.panTo(location);
        if (locationFromCookie.zoom) {
            map.setZoom(locationFromCookie.zoom);
        }
    } else {
        $(".map-feedback-msg span").html(resources.GoToMapViewError);
        $(".map-feedback-msg").show().delay(3000).fadeOut();     
    }
}

// Custom save map view control
var mobileToggleMapModeControl = function (controlDiv, map) {

    controlDiv.className = 'darkModeControlContainer customMapCtrl';

    // Set CSS for the control wrapper
    var controlWrapper = document.createElement('div');
    controlWrapper.className = "darkModeControl";
    controlDiv.appendChild(controlWrapper);

    // Set CSS for save map view btn
    var toggleMapModeBtn = document.createElement('i');
    toggleMapModeBtn.className = "fas fa-adjust";
    toggleMapModeBtn.setAttribute('title', resources.ToggleDarkLightMap);
    toggleMapModeBtn.setAttribute("aria-label", resources.ToggleDarkLightMap);
    toggleMapModeBtn.setAttribute("role", 'button');
    toggleMapModeBtn.setAttribute("tabindex", "0");
    controlWrapper.appendChild(toggleMapModeBtn);
}
/**
 * @name MarkerClusterer for Google Maps v3
 * @version version 1.0
 * @author Luke Mahe
 * @fileoverview
 * The library creates and manages per-zoom-level clusters for large amounts of
 * markers.
 * <br/>
 * This is a v3 implementation of the
 * <a href="http://gmaps-utility-library-dev.googlecode.com/svn/tags/markerclusterer/"
 * >v2 MarkerClusterer</a>.
 */

/**
 * 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.
 */


/******* See line 171 and 990 for our code modification *******/


/**
 * A Marker Clusterer that clusters markers.
 *
 * @param {google.maps.Map} map The Google map to attach to.
 * @param {Array.<google.maps.Marker>=} opt_markers Optional markers to add to
 *   the cluster.
 * @param {Object=} opt_options support the following options:
 *     'gridSize': (number) The grid size of a cluster in pixels.
 *     'maxZoom': (number) The maximum zoom level that a marker can be part of a
 *                cluster.
 *     'zoomOnClick': (boolean) Whether the default behaviour of clicking on a
 *                    cluster is to zoom into it.
 *     'averageCenter': (boolean) Wether the center of each cluster should be
 *                      the average of all markers in the cluster.
 *     'minimumClusterSize': (number) The minimum number of markers to be in a
 *                           cluster before the markers are hidden and a count
 *                           is shown.
 *     'styles': (object) An object that has style properties:
 *       'url': (string) The image url.
 *       'height': (number) The image height.
 *       'width': (number) The image width.
 *       'anchor': (Array) The anchor position of the label text.
 *       'textColor': (string) The text color.
 *       'textSize': (number) The text size.
 *       'backgroundPosition': (string) The position of the backgound x, y.
 *       'iconAnchor': (Array) The anchor position of the icon x, y.
 * @constructor
 * @extends google.maps.OverlayView
 */
function MarkerClusterer(map, opt_markers, opt_options, oms) {
    // MarkerClusterer implements google.maps.OverlayView interface. We use the
    // extend function to extend MarkerClusterer with google.maps.OverlayView
    // because it might not always be available when the code is defined so we
    // look for it at the last possible moment. If it doesn't exist now then
    // there is no point going ahead :)
    this.extend(MarkerClusterer, google.maps.OverlayView);
    this.map_ = map;
    this.oms_ = oms;

    /**
     * @type {Array.<google.maps.Marker>}
     * @private
     */
    this.markers_ = [];

    /**
     *  @type {Array.<Cluster>}
     */
    this.clusters_ = [];

    this.sizes = [53, 56, 66, 78, 90];

    /**
     * @private
     */
    this.styles_ = [];

    /**
     * @type {boolean}
     * @private
     */
    this.ready_ = false;

    var options = opt_options || {};

    /**
     * @type {number}
     * @private
     */
    this.gridSize_ = options['gridSize'] || 60;

    /**
     * @private
     */
    this.minClusterSize_ = options['minimumClusterSize'] || 2;


    /**
     * @type {?number}
     * @private
     */
    this.maxZoom_ = options['maxZoom'] || null;

    this.styles_ = options['styles'] || [];

    /**
     * @type {string}
     * @private
     */
    this.imagePath_ = options['imagePath'] ||
        this.MARKER_CLUSTER_IMAGE_PATH_;

    /**
     * @type {string}
     * @private
     */
    this.imageExtension_ = options['imageExtension'] ||
        this.MARKER_CLUSTER_IMAGE_EXTENSION_;

    /**
     * @type {boolean}
     * @private
     */
    this.zoomOnClick_ = true;

    if (options['zoomOnClick'] != undefined) {
        this.zoomOnClick_ = options['zoomOnClick'];
    }

    /**
     * @type {boolean}
     * @private
     */
    this.averageCenter_ = false;

    if (options['averageCenter'] != undefined) {
        this.averageCenter_ = options['averageCenter'];
    }

    this.setupStyles_();

    this.setMap(map);

    /**
     * @type {number}
     * @private
     */
    this.prevZoom_ = this.map_.getZoom();

    // Add the map event listeners
    var that = this;
    google.maps.event.addListener(this.map_, 'zoom_changed', function () {
        var zoom = that.map_.getZoom();

        if (that.prevZoom_ != zoom) {
            that.prevZoom_ = zoom;
            that.resetViewport();
        }
    });

    //Remove idle listener since we call redraw ourselves
    //google.maps.event.addListener(this.map_, 'idle', function () {
    //    that.redraw();
    //});

    // Finally, add the markers
    if (opt_markers && opt_markers.length) {
        this.addMarkers(opt_markers, false);
    }
}


/**
 * The marker cluster image path.
 *
 * @type {string}
 * @private
 */
MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ =
    'https://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/' +
    'images/m';


/**
 * The marker cluster image path.
 *
 * @type {string}
 * @private
 */
MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png';


/**
 * Extends a objects prototype by anothers.
 *
 * @param {Object} obj1 The object to be extended.
 * @param {Object} obj2 The object to extend with.
 * @return {Object} The new extended object.
 * @ignore
 */
MarkerClusterer.prototype.extend = function (obj1, obj2) {
    return (function (object) {
        for (var property in object.prototype) {
            this.prototype[property] = object.prototype[property];
        }
        return this;
    }).apply(obj1, [obj2]);
};


/**
 * Implementaion of the interface method.
 * @ignore
 */
MarkerClusterer.prototype.onAdd = function () {
    this.setReady_(true);
};

/**
 * Implementaion of the interface method.
 * @ignore
 */
MarkerClusterer.prototype.draw = function () { };

/**
 * Sets up the styles object.
 *
 * @private
 */
MarkerClusterer.prototype.setupStyles_ = function () {
    if (this.styles_.length) {
        return;
    }

    for (var i = 0, size; size = this.sizes[i]; i++) {
        this.styles_.push({
            url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_,
            height: size,
            width: size
        });
    }
};

/**
 *  Fit the map to the bounds of the markers in the clusterer.
 */
MarkerClusterer.prototype.fitMapToMarkers = function () {
    var markers = this.getMarkers();
    var bounds = new google.maps.LatLngBounds();
    for (var i = 0, marker; marker = markers[i]; i++) {
        bounds.extend(marker.getPosition());
    }

    this.map_.fitBounds(bounds);
    this.map_.panBy(0, 0);
};


/**
 *  Sets the styles.
 *
 *  @param {Object} styles The style to set.
 */
MarkerClusterer.prototype.setStyles = function (styles) {
    this.styles_ = styles;
};


/**
 *  Gets the styles.
 *
 *  @return {Object} The styles object.
 */
MarkerClusterer.prototype.getStyles = function () {
    return this.styles_;
};


/**
 * Whether zoom on click is set.
 *
 * @return {boolean} True if zoomOnClick_ is set.
 */
MarkerClusterer.prototype.isZoomOnClick = function () {
    return this.zoomOnClick_;
};

/**
 * Whether average center is set.
 *
 * @return {boolean} True if averageCenter_ is set.
 */
MarkerClusterer.prototype.isAverageCenter = function () {
    return this.averageCenter_;
};


/**
 *  Returns the array of markers in the clusterer.
 *
 *  @return {Array.<google.maps.Marker>} The markers.
 */
MarkerClusterer.prototype.getMarkers = function () {
    return this.markers_;
};


/**
 *  Returns the number of markers in the clusterer
 *
 *  @return {Number} The number of markers.
 */
MarkerClusterer.prototype.getTotalMarkers = function () {
    return this.markers_.length;
};


/**
 *  Sets the max zoom for the clusterer.
 *
 *  @param {number} maxZoom The max zoom level.
 */
MarkerClusterer.prototype.setMaxZoom = function (maxZoom) {
    this.maxZoom_ = maxZoom;
};


/**
 *  Gets the max zoom for the clusterer.
 *
 *  @return {number} The max zoom level.
 */
MarkerClusterer.prototype.getMaxZoom = function () {
    return this.maxZoom_;
};


/**
 *  The function for calculating the cluster icon image.
 *
 *  @param {Array.<google.maps.Marker>} markers The markers in the clusterer.
 *  @param {number} numStyles The number of styles available.
 *  @return {Object} A object properties: 'text' (string) and 'index' (number).
 *  @private
 */
MarkerClusterer.prototype.calculator_ = function (markers, numStyles) {
    var index = 0;
    var count = markers.length;
    var dv = count;
    while (dv !== 0) {
        dv = parseInt(dv / 10, 10);
        index++;
    }

    index = Math.min(index, numStyles);
    return {
        text: count,
        index: index
    };
};


/**
 * Set the calculator function.
 *
 * @param {function(Array, number)} calculator The function to set as the
 *     calculator. The function should return a object properties:
 *     'text' (string) and 'index' (number).
 *
 */
MarkerClusterer.prototype.setCalculator = function (calculator) {
    this.calculator_ = calculator;
};


/**
 * Get the calculator function.
 *
 * @return {function(Array, number)} the calculator function.
 */
MarkerClusterer.prototype.getCalculator = function () {
    return this.calculator_;
};


/**
 * Add an array of markers to the clusterer.
 *
 * @param {Array.<google.maps.Marker>} markers The markers to add.
 * @param {boolean=} opt_nodraw Whether to redraw the clusters.
 */
MarkerClusterer.prototype.addMarkers = function (markers, opt_nodraw) {
    for (var i = 0, marker; marker = markers[i]; i++) {
        if (!marker.preventClustering) {
            this.oms_.addMarker(marker);
        }
        this.pushMarkerTo_(marker);
    }
    if (!opt_nodraw) {
        this.redraw();
    }
};


/**
 * Pushes a marker to the clusterer.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @private
 */
MarkerClusterer.prototype.pushMarkerTo_ = function (marker) {
    marker.isAdded = false;
    if (marker['draggable']) {
        // If the marker is draggable add a listener so we update the clusters on
        // the drag end.
        var that = this;
        google.maps.event.addListener(marker, 'dragend', function () {
            marker.isAdded = false;
            that.repaint();
        });
    }
    this.markers_.push(marker);
};


/**
 * Adds a marker to the clusterer and redraws if needed.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @param {boolean=} opt_nodraw Whether to redraw the clusters.
 */
MarkerClusterer.prototype.addMarker = function (marker, opt_nodraw) {
    if (!marker.preventClustering) {
        this.oms_.addMarker(marker);
    }
    this.pushMarkerTo_(marker);
    if (!opt_nodraw) {
        this.redraw();
    }
};


/**
 * Removes a marker and returns true if removed, false if not
 *
 * @param {google.maps.Marker} marker The marker to remove
 * @return {boolean} Whether the marker was removed or not
 * @private
 */
MarkerClusterer.prototype.removeMarker_ = function (marker) {
    var index = -1;
    if (this.markers_.indexOf) {
        index = this.markers_.indexOf(marker);
    } else {
        for (var i = 0, m; m = this.markers_[i]; i++) {
            if (m == marker) {
                index = i;
                break;
            }
        }
    }

    if (index == -1) {
        // Marker is not in our list of markers.
        return false;
    }

    marker.setMap(null);
    marker.clustered = true;
    google.maps.event.trigger(marker, 'marker-clustered', true);
    this.markers_.splice(index, 1);

    return true;
};


/**
 * Remove a marker from the cluster.
 *
 * @param {google.maps.Marker} marker The marker to remove.
 * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
 * @return {boolean} True if the marker was removed.
 */
MarkerClusterer.prototype.removeMarker = function (marker, opt_nodraw) {
    this.oms_.removeMarker(marker);
    var removed = this.removeMarker_(marker);

    if (!opt_nodraw && removed) {
        this.resetViewport();
        this.redraw();
        return true;
    } else {
        return false;
    }
};


/**
 * Removes an array of markers from the cluster.
 *
 * @param {Array.<google.maps.Marker>} markers The markers to remove.
 * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
 */
MarkerClusterer.prototype.removeMarkers = function (markers, opt_nodraw) {
    var removed = false;

    for (var i = 0, marker; marker = markers[i]; i++) {
        this.oms_.removeMarker(marker);
        var r = this.removeMarker_(marker);
        removed = removed || r;
    }

    if (!opt_nodraw && removed) {
        this.resetViewport();
        this.redraw();
        return true;
    }
};


/**
 * Sets the clusterer's ready state.
 *
 * @param {boolean} ready The state.
 * @private
 */
MarkerClusterer.prototype.setReady_ = function (ready) {
    if (!this.ready_) {
        this.ready_ = ready;
        this.createClusters_();
    }
};


/**
 * Returns the number of clusters in the clusterer.
 *
 * @return {number} The number of clusters.
 */
MarkerClusterer.prototype.getTotalClusters = function () {
    return this.clusters_.length;
};


/**
 * Returns the google map that the clusterer is associated with.
 *
 * @return {google.maps.Map} The map.
 */
MarkerClusterer.prototype.getMap = function () {
    return this.map_;
};


/**
 * Sets the google map that the clusterer is associated with.
 *
 * @param {google.maps.Map} map The map.
 */
MarkerClusterer.prototype.setMap = function (map) {
    this.map_ = map;
};


/**
 * Returns the size of the grid.
 *
 * @return {number} The grid size.
 */
MarkerClusterer.prototype.getGridSize = function () {
    return this.gridSize_;
};


/**
 * Sets the size of the grid.
 *
 * @param {number} size The grid size.
 */
MarkerClusterer.prototype.setGridSize = function (size) {
    this.gridSize_ = size;
};


/**
 * Returns the min cluster size.
 *
 * @return {number} The grid size.
 */
MarkerClusterer.prototype.getMinClusterSize = function () {
    return this.minClusterSize_;
};

/**
 * Sets the min cluster size.
 *
 * @param {number} size The grid size.
 */
MarkerClusterer.prototype.setMinClusterSize = function (size) {
    this.minClusterSize_ = size;
};


/**
 * Extends a bounds object by the grid size.
 *
 * @param {google.maps.LatLngBounds} bounds The bounds to extend.
 * @return {google.maps.LatLngBounds} The extended bounds.
 */
MarkerClusterer.prototype.getExtendedBounds = function (bounds) {
    var projection = this.getProjection();

    // Turn the bounds into latlng.
    var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
        bounds.getNorthEast().lng());
    var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
        bounds.getSouthWest().lng());

    // Convert the points to pixels and the extend out by the grid size.
    var trPix = projection.fromLatLngToDivPixel(tr);
    trPix.x += this.gridSize_;
    trPix.y -= this.gridSize_;

    var blPix = projection.fromLatLngToDivPixel(bl);
    blPix.x -= this.gridSize_;
    blPix.y += this.gridSize_;

    // Convert the pixel points back to LatLng
    var ne = projection.fromDivPixelToLatLng(trPix);
    var sw = projection.fromDivPixelToLatLng(blPix);

    // Extend the bounds to contain the new bounds.
    bounds.extend(ne);
    bounds.extend(sw);

    return bounds;
};


/**
 * Determins if a marker is contained in a bounds.
 *
 * @param {google.maps.Marker} marker The marker to check.
 * @param {google.maps.LatLngBounds} bounds The bounds to check against.
 * @return {boolean} True if the marker is in the bounds.
 * @private
 */
MarkerClusterer.prototype.isMarkerInBounds_ = function (marker, bounds) {
    if (marker.getPosition) return bounds.contains(marker.getPosition());
    else return false;
};


/**
 * Clears all clusters and markers from the clusterer.
 */
MarkerClusterer.prototype.clearMarkers = function () {
    this.resetViewport(true);

    // Set the markers a empty array.
    this.markers_ = [];
};


/**
 * Clears all existing clusters and recreates them.
 * @param {boolean} opt_hide To also hide the marker.
 */
MarkerClusterer.prototype.resetViewport = function (opt_hide) {
    // Remove all the clusters
    for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
        cluster.remove();
    }

    // Reset the markers to not be added and to be invisible.
    for (var i = 0, marker; marker = this.markers_[i]; i++) {
        marker.isAdded = false;
        if (opt_hide) {
            marker.setMap(null);
        }
    }

    this.clusters_ = [];
};

/**
 *
 */
MarkerClusterer.prototype.repaint = function () {
    var oldClusters = this.clusters_.slice();
    this.clusters_.length = 0;
    this.resetViewport();
    this.redraw();

    // Remove the old clusters.
    // Do it in a timeout so the other clusters have been drawn first.
    window.setTimeout(function () {
        for (var i = 0, cluster; cluster = oldClusters[i]; i++) {
            cluster.remove();
        }
    }, 0);
};


/**
 * Redraws the clusters.
 */
MarkerClusterer.prototype.redraw = function () {
    this.createClusters_();
};


/**
 * Calculates the distance between two latlng locations in km.
 * @see http://www.movable-type.co.uk/scripts/latlong.html
 *
 * @param {google.maps.LatLng} p1 The first lat lng point.
 * @param {google.maps.LatLng} p2 The second lat lng point.
 * @return {number} The distance between the two points in km.
 * @private
*/
MarkerClusterer.prototype.distanceBetweenPoints_ = function (p1, p2) {
    if (!p1 || !p2) {
        return 0;
    }

    var R = 6371; // Radius of the Earth in km
    var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
    var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
    var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
        Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
        Math.sin(dLon / 2) * Math.sin(dLon / 2);
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    var d = R * c;
    return d;
};


/**
 * Add a marker to a cluster, or creates a new cluster.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @private
 */
MarkerClusterer.prototype.addToClosestCluster_ = function (marker) {
    var distance = 40000; // Some large number
    var clusterToAddTo = null;
    var pos = marker.getPosition();
    for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
        var center = cluster.getCenter();
        if (center) {
            var d = this.distanceBetweenPoints_(center, marker.getPosition());
            if (d < distance) {
                distance = d;
                clusterToAddTo = cluster;
            }
        }
    }

    if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
        clusterToAddTo.addMarker(marker);
    } else {
        var cluster = new Cluster(this);
        cluster.addMarker(marker);
        this.clusters_.push(cluster);
    }
};


/**
 * Creates the clusters.
 *
 * @private
 */
MarkerClusterer.prototype.createClusters_ = function () {
    if (!this.ready_) {
        return;
    }

    // Get our current map view bounds.
    // Create a new bounds object so we don't affect the map.
    var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(),
        this.map_.getBounds().getNorthEast());
    var bounds = this.getExtendedBounds(mapBounds);

    for (var i = 0, marker; marker = this.markers_[i]; i++) {
        if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
            this.addToClosestCluster_(marker);
        }
    }
};


/**
 * A cluster that contains markers.
 *
 * @param {MarkerClusterer} markerClusterer The markerclusterer that this
 *     cluster is associated with.
 * @constructor
 * @ignore
 */
function Cluster(markerClusterer) {
    this.markerClusterer_ = markerClusterer;
    this.map_ = markerClusterer.getMap();
    this.gridSize_ = markerClusterer.getGridSize();
    this.minClusterSize_ = markerClusterer.getMinClusterSize();
    this.averageCenter_ = markerClusterer.isAverageCenter();
    this.center_ = null;
    this.markers_ = [];
    this.bounds_ = null;
    this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(),
        markerClusterer.getGridSize());
}

/**
 * Determins if a marker is already added to the cluster.
 *
 * @param {google.maps.Marker} marker The marker to check.
 * @return {boolean} True if the marker is already added.
 */
Cluster.prototype.isMarkerAlreadyAdded = function (marker) {
    if (this.markers_.indexOf) {
        return this.markers_.indexOf(marker) != -1;
    } else {
        for (var i = 0, m; m = this.markers_[i]; i++) {
            if (m == marker) {
                return true;
            }
        }
    }
    return false;
};


/**
 * Add a marker the cluster.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @return {boolean} True if the marker was added.
 */
Cluster.prototype.addMarker = function (marker) {
    if (this.isMarkerAlreadyAdded(marker)) {
        return false;
    }

    if (!this.center_) {
        this.center_ = marker.getPosition();
        this.calculateBounds_();
    } else {
        if (this.averageCenter_) {
            var l = this.markers_.length + 1;
            var lat = (this.center_.lat() * (l - 1) + marker.getPosition().lat()) / l;
            var lng = (this.center_.lng() * (l - 1) + marker.getPosition().lng()) / l;
            this.center_ = new google.maps.LatLng(lat, lng);
            this.calculateBounds_();
        }
    }

    marker.isAdded = true;
    if (!marker.preventClustering) {
        this.markers_.push(marker);
    }
    
    var len = this.markers_.length;
    if (len < this.minClusterSize_ || marker.preventClustering) {
        // Min cluster size not reached so show the marker.
        if (marker.getMap() != this.map_) {
            marker.setMap(this.map_);
        }
        marker.clustered = false;
        google.maps.event.trigger(marker, 'marker-clustered', false);
    }

    if (len == this.minClusterSize_) {
        // Hide the markers that were showing.
        for (var i = 0; i < len; i++) {
            this.markers_[i].setMap(null);
            this.markers_[i].clustered = true;
            google.maps.event.trigger(this.markers_[i], 'marker-clustered', true);
        }
    }

    if (len >= this.minClusterSize_ && !marker.preventClustering) {
        marker.setMap(null);
        marker.clustered = true;
        google.maps.event.trigger(marker, 'marker-clustered', true);
    }

    this.updateIcon();
    return true;
};


/**
 * Returns the marker clusterer that the cluster is associated with.
 *
 * @return {MarkerClusterer} The associated marker clusterer.
 */
Cluster.prototype.getMarkerClusterer = function () {
    return this.markerClusterer_;
};


/**
 * Returns the bounds of the cluster.
 *
 * @return {google.maps.LatLngBounds} the cluster bounds.
 */
Cluster.prototype.getBounds = function () {
    var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
    var markers = this.getMarkers();
    for (var i = 0, marker; marker = markers[i]; i++) {
        bounds.extend(marker.getPosition());
    }
    return bounds;
};


/**
 * Removes the cluster
 */
Cluster.prototype.remove = function () {
    this.clusterIcon_.remove();
    this.markers_.length = 0;
    delete this.markers_;
};


/**
 * Returns the center of the cluster.
 *
 * @return {number} The cluster center.
 */
Cluster.prototype.getSize = function () {
    return this.markers_.length;
};


/**
 * Returns the center of the cluster.
 *
 * @return {Array.<google.maps.Marker>} The cluster center.
 */
Cluster.prototype.getMarkers = function () {
    return this.markers_;
};


/**
 * Returns the center of the cluster.
 *
 * @return {google.maps.LatLng} The cluster center.
 */
Cluster.prototype.getCenter = function () {
    return this.center_;
};


/**
 * Calculated the extended bounds of the cluster with the grid.
 *
 * @private
 */
Cluster.prototype.calculateBounds_ = function () {
    var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
    this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
};


/**
 * Determines if a marker lies in the clusters bounds.
 *
 * @param {google.maps.Marker} marker The marker to check.
 * @return {boolean} True if the marker lies in the bounds.
 */
Cluster.prototype.isMarkerInClusterBounds = function (marker) {
    return this.bounds_.contains(marker.getPosition());
};


/**
 * Returns the map that the cluster is associated with.
 *
 * @return {google.maps.Map} The map.
 */
Cluster.prototype.getMap = function () {
    return this.map_;
};


/**
 * Updates the cluster icon
 */
Cluster.prototype.updateIcon = function () {
    var zoom = this.map_.getZoom();
    var mz = this.markerClusterer_.getMaxZoom();

    if (mz && zoom > mz) {
        // The zoom is greater than our max zoom so show all the markers in cluster.
        for (var i = 0, marker; marker = this.markers_[i]; i++) {
            //check to see if the map is already set so we don't flicker the icon
            if (marker.map !== this.map_) {
                marker.setMap(this.map_);
                marker.clustered = false;
                google.maps.event.trigger(marker, 'marker-clustered', false);
            }
        }
        return;
    }

    if (this.markers_.length < this.minClusterSize_) {
        // Min cluster size not yet reached.
        this.clusterIcon_.hide();
        return;
    }

    var numStyles = this.markerClusterer_.getStyles().length;
    var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
    this.clusterIcon_.setCenter(this.center_);
    this.clusterIcon_.setSums(sums);
    this.clusterIcon_.show();
};


/**
 * A cluster icon
 *
 * @param {Cluster} cluster The cluster to be associated with.
 * @param {Object} styles An object that has style properties:
 *     'url': (string) The image url.
 *     'height': (number) The image height.
 *     'width': (number) The image width.
 *     'anchor': (Array) The anchor position of the label text.
 *     'textColor': (string) The text color.
 *     'textSize': (number) The text size.
 *     'backgroundPosition: (string) The background postition x, y.
 * @param {number=} opt_padding Optional padding to apply to the cluster icon.
 * @constructor
 * @extends google.maps.OverlayView
 * @ignore
 */
function ClusterIcon(cluster, styles, opt_padding) {
    cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);

    this.styles_ = styles;
    this.padding_ = opt_padding || 0;
    this.cluster_ = cluster;
    this.center_ = null;
    this.map_ = cluster.getMap();
    this.div_ = null;
    this.sums_ = null;
    this.visible_ = false;

    this.setMap(this.map_);
}


/**
 * Triggers the clusterclick event and zoom's if the option is set.
 *
 * @param {google.maps.MouseEvent} event The event to propagate
 */
ClusterIcon.prototype.triggerClusterClick = function (event) {
    var markerClusterer = this.cluster_.getMarkerClusterer();

    // Trigger the clusterclick event.
    google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_, event);

    if (markerClusterer.isZoomOnClick()) {
        // Zoom into the cluster.
        this.map_.fitBounds(this.cluster_.getBounds());
        this.map_.panBy(0, 0);
    }
};


/**
 * Adding the cluster icon to the dom.
 * @ignore
 */
ClusterIcon.prototype.onAdd = function () {
    this.div_ = document.createElement('DIV');
    if (this.visible_) {
        var pos = this.getPosFromLatLng_(this.center_);
        this.div_.style.cssText = this.createCss(pos);
        this.div_.innerHTML = this.sums_.text;
    }

    var panes = this.getPanes();
    panes.overlayMouseTarget.appendChild(this.div_);

    var that = this;
    google.maps.event.addDomListener(this.div_, 'click', function (event) {
        that.triggerClusterClick(event);
    });
};


/**
 * Returns the position to place the div dending on the latlng.
 *
 * @param {google.maps.LatLng} latlng The position in latlng.
 * @return {google.maps.Point} The position in pixels.
 * @private
 */
ClusterIcon.prototype.getPosFromLatLng_ = function (latlng) {
    var pos = this.getProjection().fromLatLngToDivPixel(latlng);

    if (typeof this.iconAnchor_ === 'object' && this.iconAnchor_.length === 2) {
        pos.x -= this.iconAnchor_[0];
        pos.y -= this.iconAnchor_[1];
    } else {
        pos.x -= parseInt(this.width_ / 2, 10);
        pos.y -= parseInt(this.height_ / 2, 10);
    }
    return pos;
};


/**
 * Draw the icon.
 * @ignore
 */
ClusterIcon.prototype.draw = function () {
    if (this.visible_) {
        var pos = this.getPosFromLatLng_(this.center_);
        this.div_.style.top = pos.y + 'px';
        this.div_.style.left = pos.x + 'px';
    }
};


/**
 * Hide the icon.
 */
ClusterIcon.prototype.hide = function () {
    if (this.div_) {
        this.div_.style.display = 'none';
    }
    this.visible_ = false;
};


/**
 * Position and show the icon.
 */
ClusterIcon.prototype.show = function () {
    if (this.div_) {
        var pos = this.getPosFromLatLng_(this.center_);
        this.div_.style.cssText = this.createCss(pos);
        this.div_.style.display = '';
    }
    this.visible_ = true;
};


/**
 * Remove the icon from the map
 */
ClusterIcon.prototype.remove = function () {
    this.setMap(null);
};


/**
 * Implementation of the onRemove interface.
 * @ignore
 */
ClusterIcon.prototype.onRemove = function () {
    if (this.div_ && this.div_.parentNode) {
        this.hide();
        this.div_.parentNode.removeChild(this.div_);
        this.div_ = null;
    }
};


/**
 * Set the sums of the icon.
 *
 * @param {Object} sums The sums containing:
 *   'text': (string) The text to display in the icon.
 *   'index': (number) The style index of the icon.
 */
ClusterIcon.prototype.setSums = function (sums) {
    this.sums_ = sums;
    this.text_ = sums.text;
    this.index_ = sums.index;
    if (this.div_) {
        this.div_.innerHTML = sums.text;
    }

    this.useStyle();
};


/**
 * Sets the icon to the the styles.
 */
ClusterIcon.prototype.useStyle = function () {
    var index = Math.max(0, this.sums_.index - 1);
    index = Math.min(this.styles_.length - 1, index);
    var style = this.styles_[index];
    this.url_ = style['url'];
    this.height_ = style['height'];
    this.width_ = style['width'];
    this.textColor_ = style['textColor'];
    this.anchor_ = style['anchor'];
    this.textSize_ = style['textSize'];
    this.backgroundPosition_ = style['backgroundPosition'];
    this.iconAnchor_ = style['iconAnchor'];
};


/**
 * Sets the center of the icon.
 *
 * @param {google.maps.LatLng} center The latlng to set as the center.
 */
ClusterIcon.prototype.setCenter = function (center) {
    this.center_ = center;
};


/**
 * Create the css text based on the position of the icon.
 *
 * @param {google.maps.Point} pos The position.
 * @return {string} The css style text.
 */
ClusterIcon.prototype.createCss = function (pos) {
    var style = [];
    style.push('background-image:url(' + this.url_ + ');');
    var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0';
    style.push('background-position:' + backgroundPosition + ';');

    if (typeof this.anchor_ === 'object') {
        if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
            this.anchor_[0] < this.height_) {
            style.push('height:' + (this.height_ - this.anchor_[0]) +
                'px; padding-top:' + this.anchor_[0] + 'px;');
        } else if (typeof this.anchor_[0] === 'number' && this.anchor_[0] < 0 &&
            -this.anchor_[0] < this.height_) {
            style.push('height:' + this.height_ + 'px; line-height:' + (this.height_ + this.anchor_[0]) +
                'px;');
        } else {
            style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
                'px;');
        }
        if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
            this.anchor_[1] < this.width_) {
            style.push('width:' + (this.width_ - this.anchor_[1]) +
                'px; padding-left:' + this.anchor_[1] + 'px;');
        } else {
            style.push('width:' + this.width_ + 'px; text-align:center;');
        }
    } else {
        style.push('height:' + this.height_ + 'px; line-height:' +
            this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
    }

    var txtColor = this.textColor_ ? this.textColor_ : 'black';
    var txtSize = this.textSize_ ? this.textSize_ : 11;

    style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
        pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' +
        txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold');
    return style.join('');
};
// Determine the mobile operating system.
// This function returns one of 'iOS', 'Android', 'Windows Phone', or 'unknown'./
function getMobileOs() {
    var userAgent = navigator.userAgent || navigator.vendor || window.opera;

    // Windows Phone must come first because its UA also contains "Android"
    if (/windows phone/i.test(userAgent)) {
        return "Windows Phone";
    }

    if (/android/i.test(userAgent)) {
        return "Android";
    }

    // iOS detection from: http://stackoverflow.com/a/9039885/177710
    if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
        return "iOS";
    }

    return "unknown";
}
var noPanOffEarth = function (map) {
    //thanks to this https://jsfiddle.net/44e6y/9/

    //got these from above and they seem to work well with minZoom = 3 below.
    var allowedBounds = new google.maps.LatLngBounds(
        new google.maps.LatLng(-75.05113, -180),
        new google.maps.LatLng(75.05113, 180));

    function checkMapCenter() {
        //get the x and y value.
        var mapCenter = map.getCenter();
        mapCenter = new google.maps.LatLng(mapCenter.lat(), mapCenter.lng(), false);
        //we are good!
        if (allowedBounds.contains(mapCenter)) {
            return;
        }

        
        var x = mapCenter.lng();
        var y = mapCenter.lat();

        //note: you cannot have an incorrect longitude.

        //get max and min latitude as defined by allowsBounds.
        var aMaxY = allowedBounds.getNorthEast().lat();
        var aMinY = allowedBounds.getSouthWest().lat();

        //fix the y value.
        if (y < aMinY) {
            y = aMinY;
        }
        if (y > aMaxY) {
            y = aMaxY;
        }

        //correct the map centre.
        map.panTo(new google.maps.LatLng(y, x));
    }

    //check map center whenever it is changed.
    google.maps.event.addListener(map, 'center_changed', function () {
        checkMapCenter();
    });

    //added this because it helps. works well with above code.
    map.setOptions({ minZoom: 3 });
};
/*
 OverlappingMarkerSpiderfier
https://github.com/jawj/OverlappingMarkerSpiderfier
Copyright (c) 2011 - 2017 George MacKerron
Released under the MIT licence: http://opensource.org/licenses/mit-license
Note: The Google Maps API v3 must be included *before* this code
*/
(function () {
    var m, t, w, y, u, z = {}.hasOwnProperty, A = [].slice; this.OverlappingMarkerSpiderfier = function () {
        function r(a, d) {
            var b, f, e; this.map = a; null == d && (d = {}); null == this.constructor.N && (this.constructor.N = !0, h = google.maps, l = h.event, p = h.MapTypeId, c.keepSpiderfied = !1, c.ignoreMapClick = !1, c.markersWontHide = !1, c.markersWontMove = !1, c.basicFormatEvents = !1, c.nearbyDistance = 20, c.circleSpiralSwitchover = 9, c.circleFootSeparation = 23, c.circleStartAngle = x / 12, c.spiralFootSeparation = 26, c.spiralLengthStart = 11, c.spiralLengthFactor =
                4, c.spiderfiedZIndex = h.Marker.MAX_ZINDEX + 2E4, c.highlightedLegZIndex = h.Marker.MAX_ZINDEX + 1E4, c.usualLegZIndex = h.Marker.MAX_ZINDEX + 1, c.legWeight = 1.5, c.legColors = { usual: {}, highlighted: {} }, e = c.legColors.usual, f = c.legColors.highlighted, e[p.HYBRID] = e[p.SATELLITE] = "#fff", f[p.HYBRID] = f[p.SATELLITE] = "#f00", e[p.TERRAIN] = e[p.ROADMAP] = "#444", f[p.TERRAIN] = f[p.ROADMAP] = "#f00", this.constructor.j = function (a) { return this.setMap(a) }, this.constructor.j.prototype = new h.OverlayView, this.constructor.j.prototype.draw = function () { });
            for (b in d) z.call(d, b) && (f = d[b], this[b] = f); this.g = new this.constructor.j(this.map); this.C(); this.c = {}; this.B = this.l = null; this.addListener("click", function (a, b) { return l.trigger(a, "spider_click", b) }); this.addListener("format", function (a, b) { return l.trigger(a, "spider_format", b) }); this.ignoreMapClick || l.addListener(this.map, "click", function (a) { return function () { return a.unspiderfy() } }(this)); l.addListener(this.map, "maptypeid_changed", function (a) { return function () { return a.unspiderfy() } }(this)); l.addListener(this.map,
                "zoom_changed", function (a) { return function () { a.unspiderfy(); if (!a.basicFormatEvents) return a.h() } }(this))
        } var l, h, m, v, p, c, t, x, u; c = r.prototype; t = [r, c]; m = 0; for (v = t.length; m < v; m++)u = t[m], u.VERSION = "1.0.3"; x = 2 * Math.PI; h = l = p = null; r.markerStatus = { SPIDERFIED: "SPIDERFIED", SPIDERFIABLE: "SPIDERFIABLE", UNSPIDERFIABLE: "UNSPIDERFIABLE", UNSPIDERFIED: "UNSPIDERFIED" }; c.C = function () { this.a = []; this.s = [] }; c.addMarker = function (a, d) { a.setMap(this.map); return this.trackMarker(a, d) }; c.trackMarker = function (a, d) {
            var b; if (null !=
                a._oms) return this; a._oms = !0; b = [l.addListener(a, "click", function (b) { return function (d) { return b.V(a, d) } }(this))]; this.markersWontHide || b.push(l.addListener(a, "visible_changed", function (b) { return function () { return b.D(a, !1) } }(this))); this.markersWontMove || b.push(l.addListener(a, "position_changed", function (b) { return function () { return b.D(a, !0) } }(this))); null != d && b.push(l.addListener(a, "spider_click", d)); this.s.push(b); this.a.push(a); this.basicFormatEvents ? this.trigger("format", a, this.constructor.markerStatus.UNSPIDERFIED) :
                    (this.trigger("format", a, this.constructor.markerStatus.UNSPIDERFIABLE), this.h()); return this
        }; c.D = function (a, d) { if (!this.J && !this.K) return null == a._omsData || !d && a.getVisible() || this.unspiderfy(d ? a : null), this.h() }; c.getMarkers = function () { return this.a.slice(0) }; c.removeMarker = function (a) { this.forgetMarker(a); return a.setMap(null) }; c.forgetMarker = function (a) {
            var d, b, f, e, g; null != a._omsData && this.unspiderfy(); d = this.A(this.a, a); if (0 > d) return this; g = this.s.splice(d, 1)[0]; b = 0; for (f = g.length; b < f; b++)e = g[b],
                l.removeListener(e); delete a._oms; this.a.splice(d, 1); this.h(); return this
        }; c.removeAllMarkers = c.clearMarkers = function () { var a, d, b, f; f = this.getMarkers(); this.forgetAllMarkers(); a = 0; for (d = f.length; a < d; a++)b = f[a], b.setMap(null); return this }; c.forgetAllMarkers = function () { var a, d, b, f, e, g, c, q; this.unspiderfy(); q = this.a; a = d = 0; for (b = q.length; d < b; a = ++d) { g = q[a]; e = this.s[a]; c = 0; for (a = e.length; c < a; c++)f = e[c], l.removeListener(f); delete g._oms } this.C(); return this }; c.addListener = function (a, d) {
            var b; (null != (b = this.c)[a] ?
                b[a] : b[a] = []).push(d); return this
        }; c.removeListener = function (a, d) { var b; b = this.A(this.c[a], d); 0 > b || this.c[a].splice(b, 1); return this }; c.clearListeners = function (a) { this.c[a] = []; return this }; c.trigger = function () { var a, d, b, f, e, g; d = arguments[0]; a = 2 <= arguments.length ? A.call(arguments, 1) : []; d = null != (b = this.c[d]) ? b : []; g = []; f = 0; for (e = d.length; f < e; f++)b = d[f], g.push(b.apply(null, a)); return g }; c.L = function (a, d) {
            var b, f, e, g, c; g = this.circleFootSeparation * (2 + a) / x; f = x / a; c = []; for (b = e = 0; 0 <= a ? e < a : e > a; b = 0 <= a ? ++e : --e)b =
                this.circleStartAngle + b * f, c.push(new h.Point(d.x + g * Math.cos(b), d.y + g * Math.sin(b))); return c
        }; c.M = function (a, d) { var b, f, e, c, k; c = this.spiralLengthStart; b = 0; k = []; for (f = e = 0; 0 <= a ? e < a : e > a; f = 0 <= a ? ++e : --e)b += this.spiralFootSeparation / c + 5E-4 * f, f = new h.Point(d.x + c * Math.cos(b), d.y + c * Math.sin(b)), c += x * this.spiralLengthFactor / b, k.push(f); return k }; c.V = function (a, d) {
            var b, f, e, c, k, q, n, l, h; (q = null != a._omsData) && this.keepSpiderfied || this.unspiderfy(); if (q || this.map.getStreetView().getVisible() || "GoogleEarthAPI" ===
                this.map.getMapTypeId()) return this.trigger("click", a, d); q = []; n = []; b = this.nearbyDistance; l = b * b; k = this.f(a.position); h = this.a; b = 0; for (f = h.length; b < f; b++)e = h[b], null != e.map && e.getVisible() && (c = this.f(e.position), this.i(c, k) < l ? q.push({ R: e, G: c }) : n.push(e)); return 1 === q.length ? this.trigger("click", a, d) : this.W(q, n)
        }; c.markersNearMarker = function (a, d) {
            var b, f, e, c, k, q, n, l, h, m; null == d && (d = !1); if (null == this.g.getProjection()) throw "Must wait for 'idle' event on map before calling markersNearMarker"; b = this.nearbyDistance;
            n = b * b; k = this.f(a.position); q = []; l = this.a; b = 0; for (f = l.length; b < f && !(e = l[b], e !== a && null != e.map && e.getVisible() && (c = this.f(null != (h = null != (m = e._omsData) ? m.v : void 0) ? h : e.position), this.i(c, k) < n && (q.push(e), d))); b++); return q
        }; c.F = function () {
            var a, d, b, f, e, c, k, l, n, h, m; if (null == this.g.getProjection()) throw "Must wait for 'idle' event on map before calling markersNearAnyOtherMarker"; n = this.nearbyDistance; n *= n; var p; e = this.a; p = []; h = 0; for (d = e.length; h < d; h++)f = e[h], p.push({
                H: this.f(null != (a = null != (b = f._omsData) ?
                    b.v : void 0) ? a : f.position), b: !1
            }); h = this.a; a = b = 0; for (f = h.length; b < f; a = ++b)if (d = h[a], null != d.getMap() && d.getVisible() && (c = p[a], !c.b)) for (m = this.a, d = l = 0, e = m.length; l < e; d = ++l)if (k = m[d], d !== a && null != k.getMap() && k.getVisible() && (k = p[d], (!(d < a) || k.b) && this.i(c.H, k.H) < n)) { c.b = k.b = !0; break } return p
        }; c.markersNearAnyOtherMarker = function () { var a, d, b, c, e, g, k; e = this.F(); g = this.a; k = []; a = d = 0; for (b = g.length; d < b; a = ++d)c = g[a], e[a].b && k.push(c); return k }; c.setImmediate = function (a) { return window.setTimeout(a, 0) }; c.h =
            function () { if (!this.basicFormatEvents && null == this.l) return this.l = this.setImmediate(function (a) { return function () { a.l = null; return null != a.g.getProjection() ? a.w() : null != a.B ? void 0 : a.B = l.addListenerOnce(a.map, "idle", function () { return a.w() }) } }(this)) }; c.w = function () {
                var a, d, b, c, e, g, k; if (this.basicFormatEvents) { e = []; d = 0; for (b = markers.length; d < b; d++)c = markers[d], a = null != c._omsData ? "SPIDERFIED" : "UNSPIDERFIED", e.push(this.trigger("format", c, this.constructor.markerStatus[a])); return e } e = this.F(); g = this.a;
                k = []; a = b = 0; for (d = g.length; b < d; a = ++b)c = g[a], a = null != c._omsData ? "SPIDERFIED" : e[a].b ? "SPIDERFIABLE" : "UNSPIDERFIABLE", k.push(this.trigger("format", c, this.constructor.markerStatus[a])); return k
            }; c.P = function (a) { return { m: function (d) { return function () { return a._omsData.o.setOptions({ strokeColor: d.legColors.highlighted[d.map.mapTypeId], zIndex: d.highlightedLegZIndex }) } }(this), u: function (d) { return function () { return a._omsData.o.setOptions({ strokeColor: d.legColors.usual[d.map.mapTypeId], zIndex: d.usualLegZIndex }) } }(this) } };
        c.W = function (a, d) {
            var b, c, e, g, k, q, n, m, p, r; this.J = !0; r = a.length; b = this.T(function () { var b, d, c; c = []; b = 0; for (d = a.length; b < d; b++)m = a[b], c.push(m.G); return c }()); g = r >= this.circleSpiralSwitchover ? this.M(r, b).reverse() : this.L(r, b); b = function () {
                var b, d, f; f = []; b = 0; for (d = g.length; b < d; b++)e = g[b], c = this.U(e), p = this.S(a, function (a) { return function (b) { return a.i(b.G, e) } }(this)), n = p.R, q = new h.Polyline({
                    map: this.map, path: [n.position, c], strokeColor: this.legColors.usual[this.map.mapTypeId], strokeWeight: this.legWeight,
                    zIndex: this.usualLegZIndex
                }), n._omsData = { v: n.getPosition(), X: n.getZIndex(), o: q }, this.legColors.highlighted[this.map.mapTypeId] !== this.legColors.usual[this.map.mapTypeId] && (k = this.P(n), n._omsData.O = { m: l.addListener(n, "mouseover", k.m), u: l.addListener(n, "mouseout", k.u) }), this.trigger("format", n, this.constructor.markerStatus.SPIDERFIED), n.setPosition(c), n.setZIndex(Math.round(this.spiderfiedZIndex + e.y)), f.push(n); return f
            }.call(this); delete this.J; this.I = !0; return this.trigger("spiderfy", b, d)
        }; c.unspiderfy =
            function (a) {
                var d, b, c, e, g, k, h; null == a && (a = null); if (null == this.I) return this; this.K = !0; h = []; g = []; k = this.a; d = 0; for (b = k.length; d < b; d++)e = k[d], null != e._omsData ? (e._omsData.o.setMap(null), e !== a && e.setPosition(e._omsData.v), e.setZIndex(e._omsData.X), c = e._omsData.O, null != c && (l.removeListener(c.m), l.removeListener(c.u)), delete e._omsData, e !== a && (c = this.basicFormatEvents ? "UNSPIDERFIED" : "SPIDERFIABLE", this.trigger("format", e, this.constructor.markerStatus[c])), h.push(e)) : g.push(e); delete this.K; delete this.I;
                this.trigger("unspiderfy", h, g); return this
            }; c.i = function (a, d) { var b, c; b = a.x - d.x; c = a.y - d.y; return b * b + c * c }; c.T = function (a) { var c, b, f, e, g; c = e = g = 0; for (b = a.length; c < b; c++)f = a[c], e += f.x, g += f.y; a = a.length; return new h.Point(e / a, g / a) }; c.f = function (a) { return this.g.getProjection().fromLatLngToDivPixel(a) }; c.U = function (a) { return this.g.getProjection().fromDivPixelToLatLng(a) }; c.S = function (a, c) {
                var b, d, e, g, k, h; e = k = 0; for (h = a.length; k < h; e = ++k)if (g = a[e], g = c(g), "undefined" === typeof b || null === b || g < d) d = g, b = e; return a.splice(b,
                    1)[0]
            }; c.A = function (a, c) { var b, d, e, g; if (null != a.indexOf) return a.indexOf(c); b = d = 0; for (e = a.length; d < e; b = ++d)if (g = a[b], g === c) return b; return -1 }; return r
    }(); t = /(\?.*(&|&amp;)|\?)spiderfier_callback=(\w+)/; m = document.currentScript; null == m && (m = function () { var m, l, h, w, v; h = document.getElementsByTagName("script"); v = []; m = 0; for (l = h.length; m < l; m++)u = h[m], null != (w = u.getAttribute("src")) && w.match(t) && v.push(u); return v }()[0]); if (null != m && (m = null != (w = m.getAttribute("src")) ? null != (y = w.match(t)) ? y[3] : void 0 : void 0) &&
        "function" === typeof window[m]) window[m](); "function" === typeof window.spiderfier_callback && window.spiderfier_callback()
}).call(this);
/* Thu 11 May 2017 08:40:57 BST */
var PolylineManager = function (map) {
    var publicItem = {};

    var polylineLayers = [];
    var mapFctns = new MapFctns();

    //see PolylineLayer comments for more details.
    publicItem.AddPolylineLayer = function (layerId, polylines, strokeOpacity, strokeWeight, minZoom, maxZoom, updateFunction, layerFilter, ignoreMapChange) {
        //make sure layer doesnt exist already.
        if (getIndexOfLayerId(layerId) != -1) {
            throw 'layerid is already active.';
        }

        //init new polyline layer.
        var polylineLayer = new PolylineLayer(layerId, polylines, strokeOpacity, strokeWeight, minZoom, maxZoom, updateFunction, layerFilter, ignoreMapChange);
        polylineLayers.push(polylineLayer);

        //display polyline layer.
        polylineLayer.MapChanged(map, mapFctns);
    };

    publicItem.GetPolylines = function (layerId) {
        for (var i = 0; i < polylineLayers.length; i++) {
            if (polylineLayers[i].layerId == layerId) {
                return polylineLayers[i].layerPolyLines();
            }
        }
    };

    publicItem.RemovePolylineLayer = function (layerId) {
        var index = getIndexOfLayerId(layerId);

        //index found.
        if (index != -1) {
            //hide all polylines and remove layer from collection.
            polylineLayers[index].Delete();
            polylineLayers.splice(index, 1);
        }
    };

    //causes all polyline layers to refresh.
    publicItem.RefreshLayers = function (skipCache) {
        var count = 0;
        var eventCount = 0;

        //if no layers to refresh.
        if (!polylineLayers.length) {
            $(document).trigger('layersRefreshed-polylineManager');
            return;
        }

        //once all layers refreshed, trigger an all layers refreshed event.
        $(document).on('layerRefreshed-polylineManager.internal', function () {
            if (++eventCount == count) {
                $(document).trigger('layersRefreshed-polylineManager');
                $(document).unbind('layerRefreshed-polylineManager.internal');
            }
        });

        count = polylineLayers.length;
        for (var i in polylineLayers) {
            var layer = polylineLayers[i];

            //refresh polylines data and display.
            layer.RefreshLayer(map, mapFctns, skipCache);
        }
    };
    
    //refresh a specific layer.
    publicItem.RefreshLayer = function (layerId, skipCache) {
        for (var i in polylineLayers) {
            var layer = polylineLayers[i];
            
            if (layer.layerId == layerId) {
                //refresh polylines data and display.
                layer.RefreshLayer(map, mapFctns, skipCache);
                return;
            }
        }
    };

    //call when map zoom or bounds have changed.
    publicItem.MapChanged = function () {
        for (var i in polylineLayers) {
            polylineLayers[i].MapChanged(map, mapFctns);
        }

        $(document).trigger('mapChangedFinished-polylineManager');
    };
    var getIndexOfLayerId = function (layerId) {
        //for each layer in polylineLayers.
        for (var i in polylineLayers) {
            if (polylineLayers[i].layerId === layerId) {
                return i;
            }
        }

        return -1;
    };
    return publicItem;
};

/*
 * polylines is a collection of objects which have a decodedPoints property as well as a lineColor property. 
 * decodedPoints is a collection of google.maps.LatLng. lineColor is a hex color string like #000000.
 *
 * updateFunction is a function which takes a callback function as its only parameter and fetches a collection
 * of polylines which have the properties mentioned above. it calls the callback function with the polyline 
 * collection as a parameter.
 *
 * layerFilter is the ColumnFilters object for this layer. It contains the getFilter method defined in ColumnFilters.ts,
 * which allows the layer to get it's latest filter on refresh. It also contains filterDataTable, which is the
 * DataTableParams object that is passed into the NoSessionController.
 */
var PolylineLayer = function (layerId, polylines, strokeOpacity, strokeWeight, minZoom, maxZoom, updateFunction, layerFilter) {
    var publicItem = {};
    var deleted = false;
    var layerPolyLines = [];
    var gmapPolylines = null;

    //private function.
    var buildGmapPolylines = function (map) {
        oldPolylines = gmapPolylines;
        gmapPolylines = [];

        for (var i in polylines) {
            //get polyline options.
            var polyOptions = {
                strokeColor: polylines[i].lineColor,
                //setup to only use svg path or solid colour... not both at the same time
                strokeOpacity: ((polylines[i].icons == null) ? strokeOpacity : 0),
                strokeWeight: strokeWeight,
                map: null,
                path: polylines[i].decodedPoints,
                icons: polylines[i].icons
            };
                       
            //add gmap polyline to collection.
            var polyline = new google.maps.Polyline(polyOptions);
            polyline.canDraw = true;
            for (var l in oldPolylines) {
                if (polylines[i].id == oldPolylines[l].sourceData.id) {
                    polyline.canDraw = oldPolylines[l].gMapLine.canDraw;
                }
            }
            gmapPolylines.push({ gMapLine: polyline,sourceData: polylines[i] });
        }

        //trigger event that we generated this layers polylines.
        $(document).trigger('polylineLayer-polylinesBuilt', [layerId, gmapPolylines]);
    }; 

    publicItem.layerId = layerId;
    
    buildGmapPolylines();

    publicItem.layerPolyLines = function () { return gmapPolylines };
        
    //gets a fresh set of polylines and displays them.
    publicItem.RefreshLayer = function (map, mapFctns, skipCache) {
        updateFunction(function (freshPolylines) {
            //if polyline layer was not removed since updateFunction called.
            if (!deleted) {
                polylines = freshPolylines;
                hideAll();
                buildGmapPolylines(map);
                publicItem.MapChanged(map, mapFctns);
            }

            //trigger event either way.
            $(document).trigger('layerRefreshed-polylineManager', [layerId]);
        }, layerFilter, skipCache);
    };

    //called when zoom or bounds changed.
    publicItem.MapChanged = function (map, mapFctns) {
        var mapZoom = map.getZoom();

        //map not in required zoom range.
        if (mapZoom > maxZoom || mapZoom < minZoom) {
            for (var i in gmapPolylines) {
                //set all polyline maps to null.
                gmapPolylines[i].gMapLine.setMap(null);
            }

            //nothing else to do.
            return;
        }

        //map boundaries.
        var mapBounds = mapFctns.GetMapBoundsObject(map);

        //for each gmap polyline object.
        for (var i in gmapPolylines) {
            var gmapPolyline = gmapPolylines[i].gMapLine;

            //is this polyline in the visible portion of the map?
            var visible = false;

            //get google LatLng items for this polyline.
            var googleLatLngs = gmapPolyline.getPath().getArray();

            //for each point of the gmap polyline object.
            for (var j in googleLatLngs) {
                var googleLatLng = googleLatLngs[j];

                //if there is a point which is in the visible part of the map.
                if (mapFctns.CoordinateIsContained(googleLatLng.lat(), googleLatLng.lng(), mapBounds)) {
                    visible = true;
                    break;
                }
            }

            //set visibility of polyline object.
            gmapPolyline.setMap(visible && gmapPolyline.canDraw ? map : null);
        }
    };

    //function to call when polyline layer should be removed.
    publicItem.Delete = function () {
        deleted = true;
        hideAll();
    };

    var hideAll = function () {
        for (var i in gmapPolylines) {
            //set all polyline maps to null.
            gmapPolylines[i].gMapLine.setMap(null);
        }
    };

    return publicItem;
};
//dependent on URI.js.

var cctvIntervalIds = [];
var cctvPictures = [];
var cctvRefreshInterval = 2000;

//=============================================================
//Get all camera id's from the page and update array
//TODO: Modify function call and images array to be common for all 
//opened pages so ideally it would be one request per user for
//all cameras from all pages
//=============================================================

$(function () {

    var setUpCameraRefreshTimers = function () {        
        var uniqueRates = {};
        var distinctRates = [];

        //Get all of the camera images on the page and their distinct refresh rate.
        $('.cctvImage').each(function () {
            var refreshRateMs = $(this).attr('data-refresh-rate');
            if (refreshRateMs) {
                if (!uniqueRates[refreshRateMs] && refreshRateMs > 0) {
                    distinctRates.push(refreshRateMs);
                    uniqueRates[refreshRateMs] = true;
                }
                if (!refreshRateMs > 0) Bugsnag.notify("Undefined refreshRateMs", function (event) {
                    event.context = "Happened in setUpCameraRefreshTimers()";
                    event.setMetadata('html', $('<div/>').append($('.cctvImage').clone()).html());
                });
            }
        });

        //Set up camera image refresh intervals for each distinct refresh rate.
        for (var i = 0; i < distinctRates.length; i++) {
            /*
            $('.cctvImage[data-refresh-rate="' + distinctRates[i] + '"]').each(function () {
                $(this).attr('src', URI($(this).attr('src')).hash(new Date().getTime()));
            });
            */
            cctvIntervalIds.push(
                setInterval(function (refreshRate) {
                    //Update all the visible images for this refresh rate.
                    $('.cctvImage[data-refresh-rate="' + refreshRate + '"]').each(function () {
                        if ($(this).parents('.slick-slide').length == 0 || $(this).parents('.slick-slide').hasClass('slick-active')) {
                            if ($(this).attr('src') != undefined) {
                                $(this).attr('src', URI($(this).attr('src')).search("t=" + roundDateToDuration(moment().startOf('second'), moment.duration(parseInt(refreshRate)), 'floor').unix()));
                            }
                        }
                        if ($(this).parents('.slick-slide').length > 0 && !$(this).parents('.slick-slide').hasClass('slick-active')) {
                            $(this).attr('data-needsrefresh', 'true');
                        }
                    });
                }
                , distinctRates[i], distinctRates[i])
            );
        }
    }

    var clearCameraIntervals = function () {
        //Clear any existing intervals before setting up new ones.
        for (var i = 0; i < cctvIntervalIds.length; i++) {
            clearInterval(cctvIntervalIds[i]);
        }
        cctvIntervalIds = [];
    }


    function setupFullScreenCamModal(e) {

        let imgSrc = document.querySelector('.map-tooltip');
        if (!imgSrc) {
            imgSrc = document.querySelector('#cctvTable tbody');
        }
        if (!imgSrc) {
            imgSrc = document.querySelector('#myCctvTable tbody');
        }
        if (!imgSrc) {
            imgSrc = document.querySelector('#eventTable tbody');
        }

        if (imgSrc) {
            $(document).trigger('setup-fullscreen-img-modal', [imgSrc, e]);
        }
    }

    
    $(document).on('cameraImagesInitialized', function (d, e) {
        clearCameraIntervals();
        $('.carouselCctvImage').each(function (e) {
            setupSlickCarousel($(this));
        });
        //first time on load of the individual camera images.
        $('.cctvImage').one('load', function () {
            //get the element which directly precedes this cctv image.
            var beforeElement = $(this).prev();

            //if it is an agencyLogo image then display it since the cctv image is now loaded.
            if (beforeElement.hasClass('agencyLogo')) {
                beforeElement.show();
            }
            var showVideoBtn = $('button.showVideo');
            var isCamTooltip = $(".camTooltip").length > 0 || $(".myCamTooltip").length > 0;       
            if (showVideoBtn && showVideoBtn.length > 0 && resources.StartVideoOnDisplay == "True" && isCamTooltip) {              
                showVideoBtn[0].click();
            };
        }).each(function () {
            //if the image is already loaded then trigger the load event as it happened before the above handler was defined.
            if (this.complete) {
                $(this).trigger('load');
            }
        });
        if ($(".cctvCameraCarousel").hasClass("setVisibility")) {
            $(".cctvCameraCarousel").removeClass("setVisibility");
        }     
        setUpCameraRefreshTimers();
        setupFullScreenCamModal(e);
    });
        
    $(document).on('cameraImagesClearIntervals', function () {
        clearCameraIntervals();
    });
    
});

//dependent on URI.js.

var TileManager = function (map, appHelper) {
    var publicItem = {};

    //time stamp appended to end of tile url.
    var urlTime = new Date().getTime();

    //the last time the map bounds or zoom level changed.
    var timeMapStateChanged = null;

    //layer click handlers.
    var layerClickHandlers = {};

    //For restorying styling
    var oldFill = null;
    var oldStroke = null;
    const centerTooltip = new CenterTooltip(map);  
    /*
        tileConfig is object with properties urlFormat, minZoom, maxZoom, tooltipUrlFormat.
        -tileUrlFormat has to have a placeholder {x} for x, {y} for y, {z} for z.
        -tooltipUrlFormat is optional. placeholder {lat} is latitude, {lng} is longitude, {z} is current zoom level.
    */
    publicItem.AddTileLayer = function (layerId, tileConfig) {
        //make sure layer doesnt exist already.
        if (getIndexOfLayerId(layerId).length > 0) {
            throw 'layerid is already active.';
        }

        //only one tile layer at a time.
        //disableAllTileLayersExcept(layerId);

        var tileLayer = new TileLayer(layerId, tileConfig.urlFormat, tileConfig.tooltipUrlFormat, tileConfig.animationFrames, tileConfig.cache);

            //create google ImageMapType object.
            var maptiler = new TileOverlayMapType({
                name: layerId,
                getTileUrl: function (tl, coord, zoom, frame) {
                    //return null if not in zoom range.
                    var mapZoom = map.getZoom();
                    return mapZoom > tileConfig.maxZoom || mapZoom < tileConfig.minZoom ? null : tl.getTileUrl(coord.x, coord.y, zoom, frame);
                },
                tileSize: new google.maps.Size(256, 256),
                isPng: true,
                opacity: tileConfig.opacity,
                startingFrame: tileConfig.startingFrame,
                startPlaying: tileConfig.startPlaying,
            }, tileLayer, tileConfig.animationFrames);

            if (tileConfig.tooltipUrlFormat) {
                //add left click event handler to map.
                var clickListener = google.maps.event.addListener(map, 'click', function (e) {
                    var lat = e.latLng.lat();
                    var lng = e.latLng.lng();
                    var mapZoom = map.getZoom();

                    //make sure current zoom in acceptable range.
                    if (mapZoom <= tileConfig.maxZoom && mapZoom >= tileConfig.minZoom) {
                        $.ajax(tileLayer.getTooltipUrl(lat, lng, mapZoom)).done(function (content) {
                            appHelper.showInfoWindow(content, null, true, new google.maps.LatLng(lat, lng), layerId, null, true);
                            centerTooltip.run();
                        });
                    }
                });

                //store reference to this layer id's left click handler.
                layerClickHandlers[layerId] = clickListener;
            }

            //Set highway fill and stroke if it exists
            if (tileConfig.highwayFill != "" || tileConfig.highwayStroke != "") {
                var styles = map.get('styles') || [];

                var setFill = false;
                var setStroke = false;
                //Try not to overwrite any existing styling, probably more effort than it is worth
                if (styles.length != 0) {
                    for (style in styles) {
                        if (styles[style].featureType == "road.highway" && styles[style].elementType == "geometry.fill") {
                            for (styler in styles[style].stylers) {
                                if (typeof styles[style].stylers[styler].color !== 'undefined') {
                                    oldFill = styles[style].stylers[styler].color;
                                    styles[style].stylers[styler].color = tileConfig.highwayFill;
                                    setFill = true;
                                }
                            }
                            if (!setFill) {
                                styles[style].stylers.push({ color: tileConfig.highwayFill });
                                setFill = true;
                            }
                        } else if (styles[style].featureType == "road.highway" && styles[style].elementType == "geometry.stroke") {
                            for (styler in styles[style].stylers) {
                                if (typeof styles[style].stylers[styler].color !== 'undefined') {
                                    oldStroke = styles[style].stylers[styler].color;
                                    styles[style].stylers[styler].color = tileConfig.highwayStroke;
                                    setStroke = true;
                                }
                            }
                            if (!setStroke) {
                                tyles[style].stylers.push({ color: tileConfig.highwayStroke });
                                setStroke = true;
                            }
                        }
                    }
                }
                if (!setFill) {
                    styles.push({
                        featureType: "road.highway",
                        elementType: "geometry.fill",
                        stylers: [
                            { color: tileConfig.highwayFill }
                        ]
                    });
                }
                if (!setStroke) {
                    styles.push({
                        featureType: "road.highway",
                        elementType: "geometry.stroke",
                        stylers: [
                            {
                                color: tileConfig.highwayStroke
                            }
                        ]
                    });
                }
                map.setOptions({ styles: styles });
            }

            //TODO: use zlevel to order tile layers
            if (tileConfig.zlevel >= 0) {
                //add ImageMapType to google map.
                map.overlayMapTypes.insertAt(0, maptiler);
            } else {
                //add ImageMapType to google map.
                map.overlayMapTypes.push(maptiler);
            }

    };

    publicItem.RemoveTileLayer = function (layerId) {
        var indecies = getIndexOfLayerId(layerId);

        //index found.
        if (indecies.length >= 0) {
            for (var i = 0; i < indecies.length; i++) {
                map.overlayMapTypes.getArray()[indecies[0]].clearAnimationTimer();
                map.overlayMapTypes.removeAt(indecies[0]);
            }
        }

        //Restore styling
        var styles = map.get('styles');
        for (style in styles) {
            if (styles[style].featureType == "road.highway" && styles[style].elementType == "geometry.fill") {
                for (styler in styles[style].stylers) {
                    if (styles[style].stylers[styler].color) {
                        styles[style].stylers[styler].color = oldFill;
                    }
                }
            } else if (styles[style].featureType == "road.highway" && styles[style].elementType == "geometry.stroke") {
                for (styler in styles[style].stylers) {
                    if (styles[style].stylers[styler].color) {
                        styles[style].stylers[styler].color = oldStroke;
                    }
                }
            }
        }
        map.setOptions({ styles: styles });

        //if this layer has a left click event handler.
        if (layerClickHandlers[layerId]) {

            //disable event handler and remove reference.
            google.maps.event.removeListener(layerClickHandlers[layerId]);
            layerClickHandlers[layerId] = null;
        }
    };

    //causes all tile layers to refresh.
    publicItem.RefreshLayers = function () {
        //update url time stamp.
        urlTime = new Date().getTime();

        //if map state has not changed since this function was last called.
        if (timeMapStateChanged == null || timeMapStateChanged < urlTime) {
            forceRefresh();
        }
    };

    var disableAllTileLayersExcept = function (exceptThisLayerId) {
        //disable tile layers which are checked in the legend except the layer id passed to function.
        $('input[type=\'checkbox\'][data-tileurlformat]:checked:not([data-layerid=\'' + exceptThisLayerId + '\'])', $('#layerSelection')).click();
    };

    //forces all tile layers to refresh.
    var forceRefresh = function () {

        //get array of tile overlays.
        var array = map.overlayMapTypes.getArray();

        //for each layer.
        for (var i = 0; i < array.length; i++) {
            var item = array[i];

            //force a refresh of tile layer.
            if (item.isTileOverlayMapType && item.animationTimer === null) {
                item.refreshTiles(false);
            }
        }
    };

    var getIndexOfLayerId = function (layerId) {
        var array = map.overlayMapTypes.getArray();

        var indicies = [];

        //for each layer in overlayMapTypes.
        for (var i = 0; i < array.length; i++) {
            if (array[i].name == layerId && array[i].isTileOverlayMapType) {
                indicies.push(i);
            }
        }

        return indicies;
    };

    var init = function () {
        var updateMapStateTime = function () {
            timeMapStateChanged = new Date().getTime();
        };

        //on map drag finish or map zoom changed.
        google.maps.event.addListener(map, 'bounds_changed', updateMapStateTime);
        google.maps.event.addListener(map, 'zoom_changed', updateMapStateTime);
    };

    init();

    return publicItem;
};

/*
    tileUrlFormat has to have a placeholder {x} for x, {y} for y, {z} for z.
    tooltipUrlFormat is optional. placeholder {lat} is latitude, {lng} is longitude, {z} is current zoom level.
*/
var TileLayer = function (layerId, tileUrlFormat, tooltipUrlFormat, frames, cache) {
    var publicItem = {};
    var tileUrl = tileUrlFormat;
    var tooltipUrl = tooltipUrlFormat;
    var animationFrames = frames;

    publicItem.layerId = layerId;
   
    publicItem.getTileUrl = function (x, y, z, frame) {
        var uri = URI.expand(tileUrl, { x: x, y: y, z: z });
        if (animationFrames > 0) {
            uri.addSearch('frame', frame);
        }
        else if (!cache) {
            uri.addSearch('tmTime', moment().unix());
        }
        else {
            uri.addSearch('t', moment().startOf('minute').unix());
        }
        return uri.toString();
    };

    publicItem.getTooltipUrl = function (lat, lng, z) {
        return URI.expand(tooltipUrl, { lat: lat, lng: lng, z: z }).toString();
    };

    return publicItem;
};

/*
    this is a custom google overlay map type which refreshes without causing flickering. note that
    the getTileUrl should append a cache busting timestamp if a fresh tile is required. this class
    works by loading a tile and then setting the background-image to the url of the loaded tile. it
    assumes the fresh tile will be in cache so the image request should not have no-cache headers.
    if browser has cache disabled then this will not work and it will flicker.
*/
var TileOverlayMapType = function(configObj, tileLayer, frames) {
    var loadedTiles = {};

    //configuration. NOTE: getTileUrl should add a cache busting timestamp parameter.**
    this.isTileOverlayMapType = true;
    this.name = configObj.name;
    this.getTileUrl = configObj.getTileUrl;
    this.tileSize = configObj.tileSize;
    this.isPng = configObj.isPng;
    this.tileLayer = tileLayer;
    this.animationCounter = configObj.startingFrame - 1;
    this.animationFrames = frames;
    this.opacity = configObj.opacity;
    this.startPlaying = configObj.startPlaying;
    this.animationTimer = null;
    this.isAnimating = false;

    //function to get the tile.
    this.getTile = function(coord, zoom, ownerDocument) {
        var tileUrl = this.getTileUrl(this.tileLayer, coord, zoom, this.animationCounter);
        var tileId = 'x_' + coord.x + '_y_' + coord.y + '_zoom_' + zoom;
        //create a div to hold the image.
        var tileDiv = ownerDocument.createElement('div');
        tileDiv.style.backgroundPosition = 'center center';
        tileDiv.style.backgroundRepeat = 'no-repeat';
        tileDiv.style.height = this.tileSize.height + 'px';
        tileDiv.style.width = this.tileSize.width + 'px';
        tileDiv.style.opacity = this.opacity;
        tileDiv.tileId = tileId; //	do not use 'id' as new custom property as it's a native property of all HTML elements
        tileDiv.coord = coord;
        tileDiv.zoom = zoom;
        tileDiv.timestamp = new Date();
        //add div to collection of loaded tiles.
        loadedTiles[tileId] = tileDiv;
        if (tileUrl) {
            //create image object and set its onload function.   
            var img = new Image();
            img.onload = function () {
                //set the div to the image which we know is loaded in cache.
                tileDiv.style.backgroundImage = 'url(' + tileUrl + ')';
                img.onload = null;
                img = null;
            };
            //load the image by setting it's src.
            img.src = tileUrl;
            if (this.animationFrames > 0) tileDiv.img = img;
        }
        //return div.
        return tileDiv;
    };

    //function to refresh the tiles without flickering.
    this.refreshTiles = function (advanceAnimation) {
        if (advanceAnimation != false && this.animationFrames > 0) {
            var allLoaded = true;
            for (var i = 0; i < loadedTiles.length; i++) {
                var tile = loadedTiles[i]
                if (!tile.img.complete) allLoaded = false;
            }
            if (!this.isAnimating && allLoaded) {
                this.animationCounter = (this.animationCounter + 1) % this.animationFrames;
            }
            this.isAnimating = true;
        }
        //function to set the background image of a tile div.
        function onloadCallback(img, tile, tileUrl){
            return function(){
                tile.style.backgroundImage = 'url(' + tileUrl + ')';
                img.onload = null;
                img = null;
            };
        }
        //for every loaded tile.
        for (var tileId in loadedTiles) {
            //tile div and url.
            var tile = loadedTiles[tileId];
            var tileUrl = this.getTileUrl(this.tileLayer, tile.coord, tile.zoom, this.animationCounter);
            if (tileUrl) {
                //load an image and once loaded set the div background image.
                var img = new Image();
                img.onload = onloadCallback(img, tile, tileUrl);
                img.src = tileUrl;
                if (this.animationFrames > 0) tile.img = img;
            }
        }
        this.isAnimating = false;
    };

    this.getOpacity = function() {
        return this.opacity;
    }

    //remove a tile from view.
    this.releaseTile = function (tile) {
        if (loadedTiles[tile.tileId] && tile.timestamp == loadedTiles[tile.tileId].timestamp) {
            delete loadedTiles[tile.tileId];
        }
    };

    this.clearAnimationTimer = function () {
        if (this.animationTimer != null) {
            window.clearInterval(this.animationTimer);
            this.animationTimer = null;
            $(document).off('animation-toggle-' + this.name);
        }        
    }

    if (this.animationFrames > 0 ) {
        var me = this;
        const animationSpeed = parseInt(window.resources.WeatherRadarFramesAnimation);

        if (this.startPlaying) {
            //updates frames every 2 seconds
            me.animationTimer = window.setInterval(function () {
                me.refreshTiles();
                $(document).trigger('animation-update-' + me.name, [me.animationCounter]);
            }, animationSpeed);

        }else {
            $(document).trigger('timerbox-update-' + me.name, [me.animationCounter]);
        }
        
        $(document).on('animation-toggle-' + me.name, function () {
            if (me.animationTimer) {
                window.clearInterval(me.animationTimer);
                me.animationTimer = null;
            } else {
                me.animationTimer = window.setInterval(function () {
                    me.refreshTiles();
                    $(document).trigger('animation-update-' + me.name, [me.animationCounter]);
                }, animationSpeed);
            }
        });
    }
};
"use strict";
var UrlHash = (function () {
    function UrlHash() {
        var _this = this;
        this.hashEvent = 'hashchange.urlHash';
        this.hashChangeHandler = function () {
            $(document).trigger('hashChanged-urlHash', [_this.hash()]);
        };
        this.whileWeSetHashHandler = function () {
            $(window).off(_this.hashEvent);
            $(window).on(_this.hashEvent, _this.hashChangeHandler);
        };
        this.setWhileWeSetHashHandler = function () {
            $(window).off(_this.hashEvent);
            $(window).on(_this.hashEvent, _this.whileWeSetHashHandler);
        };
        this.clearHash = function () {
            _this.setWhileWeSetHashHandler();
            window.location.hash = 'route';
        };
        this.hash = function (set, shadow) {
            var h = window.location.hash.substr(1);
            if (set && h != set) {
                _this.setWhileWeSetHashHandler();
                if (shadow)
                    window.location.replace(set);
                else
                    window.location.hash = set;
            }
            return h;
        };
        if (!$._data(window, "events")["hashchange"]) {
            $(window).on(this.hashEvent, this.hashChangeHandler);
        }
    }
    return UrlHash;
}());
var urlHash = new UrlHash();

var UserGeolocation = function (autoCompleteApi, waypointManagerApi, contextMenuApi, mapApi, publicApi) {
    var publicItem = {};
    var userGeolocation = function (callback) {
        //if supported by browser.
        if (navigator && navigator.geolocation && navigator.geolocation.getCurrentPosition) {

            //attempt to get current position.
            navigator.geolocation.getCurrentPosition(function (position) {
                //project to google LatLng object.
                var pos = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
                callback(pos);
            });
        } else {
            //geo-location not supported.
            callback(null);
        }
    };
    publicItem.AttemptToSetLocationWithUsersGeolocation = function (start, panTo) {
        //if no user route specified via url.
        //if (window.location.hash.substr(1) == '' ||  window.location.hash.substr(0) == "#:MyRoutes") {
            publicItem.GetUserGeolocation(function (pos) {
                if (pos) {
                    var simplePlace = autoCompleteApi.GetCustomSimplePlace("", null, pos.lat(), pos.lng(), false);
                    waypointManagerApi.SetLocationByGeo(start, simplePlace);
                    mapApi.panTo(pos);
                }
            });
        //}
    };
    publicItem.AttemptToSetWaypointWithUsersGeolocation = function (index) {
        //if no user route specified via url.
        publicItem.GetUserGeolocation(function (pos) {
            if (pos) {
                var simplePlace = autoCompleteApi.GetCustomSimplePlace("", null, pos.lat(), pos.lng(), false);
                waypointManagerApi.SetLocationByGeo(null, simplePlace, true, index);                    
                mapApi.panTo(pos);
            }
        });
    };
    window.SetUserRegion = function (locationInfo) {        
        if (locationInfo) {
            var pos = [locationInfo[0], locationInfo[1]];
            publicApi.center(pos, parseInt(locationInfo[2]));
        }
    };
    //this needs to be public so that we don't create a new UserGeoLocation when trying to access this function
    //
    window.GetUserGeolocation = function (callback) {
        userGeolocation(callback);  
    };
    publicItem.GetUserGeolocation = function (callback) {
        userGeolocation(callback);
    };
    return publicItem;
}
var WaypointManager = function (map, routePlannerAutocomplete, contextMenuApi, routePlanner) {

    var publicItem = {};
    var locations = [
        {
            selector: "#startLocationText"
        },
        {
            selector: "#endLocationText"
        }
    ];

    var init = function () {
        $(document).on('locationSelected-contextMenu', function (e, toFrom, simplePlace) {
            if (toFrom == 'waypoint') {
                var index = locations.length - 1;
                routePlanner.AddWaypointToPlanner("", true);
                setLocationByGeoLookup(null, simplePlace, true, index);
            } else {
                setLocationByGeoLookup(isStart(toFrom), simplePlace);
            }
        });

        $(document).on('locationSelected-autocomplete', function (e, location) {
            var index = locations.findIndex(loc => loc.selector === location.selector);
            if (index < 0) {
                return;
            }

            if (location.selector.startsWith("#waypoint")) {
                publicItem.setWaypoint(location.point, index, false, true);
            } else if (location.marker != null && location.marker.map != null){
                publicItem.setLocation(index === 0, location.point, true);
            }
        });
    };

    var setLocationByGeoLookup = function (start, simplePlace, isWaypoint, index) {
        //set location and text box values.
        if (!simplePlace.name) {
            var geocoder = new google.maps.Geocoder;
            geocoder.geocode({ 'location': new google.maps.LatLng(simplePlace.point.latitude, simplePlace.point.longitude) }, function (results, status) {
                if (status === 'OK') {
                    if (results[0]) {
                        var closestPoint = 0;
                        var shortestDistance = 10000;
                        for (var i = 0; i < results.length; i++) {
                            if (results[i].types.indexOf("route") != -1 || results[i].types.indexOf("street_address") != -1) {

                                //thanks mike xd
                                var resultLat = results[i].geometry.location.lat();
                                var resultLng = results[i].geometry.location.lng();
                                var distanceToPoint = Math.sqrt(Math.pow((simplePlace.point.latitude + 90 - (resultLat + 90)), 2) + Math.pow((simplePlace.point.longitude + 180 - (resultLng + 180)), 2));
                                if (distanceToPoint < shortestDistance) {
                                    shortestDistance = distanceToPoint;
                                    closestPoint = i;
                                }
                            }
                        }
                        var addressDetails = routePlannerAutocomplete.GetAddressDetails(results[closestPoint].address_components);
                        simplePlace.streetNumber = addressDetails.streetNumber;
                        simplePlace.streetName = addressDetails.streetName;
                        simplePlace.postalCode = addressDetails.postalCode;
                        simplePlace.state = addressDetails.state;
                        simplePlace.name = results[closestPoint].formatted_address;
                    }
                    else {
                        simplePlace.name = simplePlace.point.latitude + ', ' + simplePlace.point.longitude;
                    }
                } else {
                    simplePlace.name = simplePlace.point.latitude + ', ' + simplePlace.point.longitude;
                }
                if (isWaypoint) {
                    publicItem.setWaypoint(simplePlace, index, true, true);
                } else {
                    publicItem.setLocation(start, simplePlace, true);
                }
            });
        } else {
            if (isWaypoint) {
                publicItem.setWaypoint(simplePlace, index, true);
            } else {
                publicItem.setLocation(start, simplePlace, true);
            }
        }
    }

    var isStart = function (toFrom) {
        return toFrom.indexOf('from') == 0;
    };

    var setMarker = function (index, latLng) {
        var start = index == 0;
        var end = index == locations.length - 1;
        var wpm = publicItem;
        var marker = locations[index].marker;
        var locationLetter = start ? 'A' : end ? 'B' : 'W';//is start, end or waypoint?
        var hash = getHash((new Date().getTime() + ((latLng.lat() + latLng.lng()) * 1000000000)).toString());
        var iconUrl;
        if (start) {
            iconUrl = '/Content/Images/Green-A.png';
        } else if (end) {
            iconUrl = '/Content/Images/Red-B-New.png';
            // if location marker is still visible, hide at this point
            $(document).trigger("hideLocationMarker");
        } else {
            iconUrl = { path: google.maps.SymbolPath.CIRCLE, scale: 4, fillColor: '#ffffff', fillOpacity: 1 }
        }
        //way point already exists.
        if (marker) {
            marker.setPosition(latLng);
            marker.setMap(map);
            marker.setIcon(iconUrl);
        } else {
            //build marker.
            var gMarker = new google.maps.Marker({
                map: map,
                position: latLng,
                draggable: true,
                icon: iconUrl,
                letter: locationLetter,
                zIndex: -98,
                title: (start || end) ? "" : resources.ClickToRemove,
                hash: hash
            });
            //save reference to marker.
            locations[index].marker = gMarker;
            locations[index].hash = hash;

            //Attach the listener that updates the marker position when dragged
            if (start || end) {
                google.maps.event.addListener(gMarker, 'dragend', function (e) {
                    publicItem.clearLoationsWithoutPoints();
                    var si = { point: { latitude: e.latLng.lat(), longitude: e.latLng.lng() } };
                    wpm.getNameForSimplePlace(si, function (simplePlace) {
                        var locationsWereSwitched = locations[start ? 0 : locations.length - 1].marker !== gMarker;
                        publicItem.setLocation(locationsWereSwitched ? end : start, simplePlace, true);
                        if (locations[0].point && locations[locations.length - 1].point) {
                            if ($('#transitRouteResults').children().length > 0 && $('#routeTabContent').children().length > 0) {
                                routePlanner.calculateRoute('generateDriveTransitRouteBtn');
                            } else if ($('#transitRouteResults').children().length > 0) {
                                routePlanner.calculateRoute('generateTransitRouteBtn');
                            } else {
                                //Don't need to do this if we auto generate on filling in all the stuff
                                if (resources.AutoGenerateDriveRoute !== "True") {
                                    routePlanner.calculateRoute('drive');
                                }
                            }
                        }
                    });
                });
            } else {
                google.maps.event.addListener(gMarker, 'dragend', function (e) {
                    loadBlockerApi.showSpinner('getNearestLocations');
                    publicItem.clearLoationsWithoutPoints();
                    var newIndex = index;//needs a default and this is the best I could find for now
                    for (var i = 1; i < locations.length; ++i) {
                        if (locations[i].marker === gMarker) {
                            newIndex = i;
                            break;
                        }
                    }
                    var si = {
                        point: {
                            latitude: e.latLng.lat(),
                            longitude: e.latLng.lng()
                        }
                    };
                    wpm.getNameForSimplePlace(si, function (simplePlace) {
                        setLocationHelper(newIndex, simplePlace, true);
                        routePlanner.calculateRoute();
                        loadBlockerApi.hideSpinner('getNearestLocations');
                    });
                });
                //Remove waypoints on click
                gMarker.addListener('click', function () {
                    publicItem.ClearWaypoint(index, gMarker, true);
                    routePlanner.redrawWaypoints(false);
                });
            }
        }
    };

    publicItem.setMarker = setMarker;

    publicItem.getNameForSimplePlace = function (simplePlace, callback) {
        var geocoder = new google.maps.Geocoder;
        geocoder.geocode({ 'location': new google.maps.LatLng(simplePlace.point.latitude, simplePlace.point.longitude) }, function (results, status) {
            if (status === 'OK') {
                if (results[0]) {
                    var closestPoint = 0;
                    var shortestDistance = 10000;
                    for (var i = 0; i < results.length; i++) {
                        if (results[i].types.indexOf("route") != -1 || results[i].types.indexOf("street_address") != 1) {
                            var resultLat = results[i].geometry.location.lat();
                            var resultLng = results[i].geometry.location.lng();
                            var distanceToPoint = Math.sqrt(Math.pow((simplePlace.point.latitude + 90 - (resultLat + 90)), 2) + Math.pow((simplePlace.point.longitude + 180 - (resultLng + 180)), 2));
                            if (distanceToPoint < shortestDistance) {
                                shortestDistance = distanceToPoint;
                                closestPoint = i;
                            }
                        }
                    }
                    var addressDetails = routePlannerAutocomplete.GetAddressDetails(results[closestPoint].address_components);
                    simplePlace.streetNumber = addressDetails.streetNumber;
                    simplePlace.streetName = addressDetails.streetName;
                    simplePlace.postalCode = addressDetails.postalCode;
                    simplePlace.state = addressDetails.state;
                    simplePlace.name = results[closestPoint].formatted_address;
                } else {
                    simplePlace.name = simplePlace.point.latitude + ', ' + simplePlace.point.longitude;
                }
            } else {
                simplePlace.name = simplePlace.point.latitude + ', ' + simplePlace.point.longitude;
            }
            callback(simplePlace);
        });
    }

    var setLocationHelper = function (index, simplePlace, setText) {
        var start = index === 0;
        var end = index === locations.length - 1;
        var locationLetter = start ? 'A' : end ? 'B' : 'W';//is start, end or waypoint?
        //set location letter.
        simplePlace.letter = locationLetter;

        //set marker if appropriate.
        if (simplePlace.point) {
            setMarker(index, new google.maps.LatLng(simplePlace.point.latitude, simplePlace.point.longitude));
        }
        //set text field if appropriate.
        if (setText) {
            if (start || end) {
                $(start ? '#startLocationText' : '#endLocationText').val(simplePlace.nameDirection || simplePlace.name);
            } else {
                $('#waypointText-' + index).val(simplePlace.nameDirection || simplePlace.name);
            }
        }

        locations[index].point = simplePlace;

        // if both markers exist in mobile first, then open mobile route planner
        var mobileFirstClassExists = $("body.mobileFirst").length > 0;
        if (Modernizr.mq('(max-width: 992px)') && mobileFirstClassExists) {
            if (locations[0].point && locations[locations.length - 1].point) {
                $(".mobileLocationBar .directions").trigger("click");
            }
        }
    };


    //Put the waypoint in the array at a given index
    var setWaypointHelper = function (simplePlace, index, setText, existingWaypoint) {
        simplePlace.letter = 'W';
        if (existingWaypoint) {
            if (locations[index].marker) {
                locations[index].marker.setMap(null); //remove old marker
            }
            locations[index] = { point: simplePlace, text: simplePlace.name, selector: '#waypointText-' + index };
        }
        else {
            //locations.splice(index, 0, { point: simplePlace, text: simplePlace.name, selector: '#waypointText-' + index });
            publicItem.addWaypoint(index, simplePlace);
        }
        //set text field if appropriate.
        if (setText) {
            $('#waypointText-' + index).val(simplePlace.nameDirection || simplePlace.name);
        }
    }

    var clear = function (index, clearText) {
        if (locations[index] && locations[index].marker) {
            locations[index].marker.setMap(null);
        }

        //set location null.
        locations[index] = {};
        var start = index === 0;
        var end = index === locations.length - 1;
        //empty text field.
        if (clearText && (start || end)) {
            $(start ? '#startLocationText' : '#endLocationText').val('');
        }
    };

    var getHash = function (str) {
        var hash = 0;
        for (var i = 0; i < str.length; i++) {
            hash = ~~(((hash << 5) - hash) + str.charCodeAt(i));
        }
        return hash;
    }

    publicItem.ClearWaypoint = function (index, gMarker, recalculate) {

        //sync the UI with locations list first
        publicItem.getLocationsTextFromUI();
        var newIndex;
        var firstOrLastIndex = index === 0 || index === locations.length - 1;
        if (!firstOrLastIndex || (gMarker && gMarker.hash != null)) {
            if (gMarker && gMarker.hash != null) {
                for (var i = 0; i < locations.length; i++) {
                    if (locations[i].hash === gMarker.hash) {
                        newIndex = i;
                        break;
                    }
                }
            }
            else {
                gMarker = locations[index].marker;
                newIndex = index;
            }
            if (newIndex != null) {
                locations.splice(newIndex, 1);//remove this waypoint

                publicItem.updateTransitBtnState();

                routePlanner.redrawWaypoints();
                for (let i = 1; i < locations.length - 1; i++) {
                    //locations[i].text = $(locations[i].selector).val();
                    locations[i].selector = '#waypointText-' + i;
                }

                if (gMarker) {
                    gMarker.setMap(null);
                    if (recalculate) {
                        routePlanner.calculateRoute();
                    }
                }
            }
        }
    };

    publicItem.updateTransitBtnState = function () {
        if (locations.length < 3) {
            $("#generateTransitRouteBtn").prop('disabled', false);
            $("#generateDriveTransitRouteBtn").prop('disabled', false);
            $("#generateWalkOnlyRouteBtn").prop('disabled', false);
            $("#generateBicycleOnlyRouteBtn").prop('disabled', false);
        } else {
            $("#generateTransitRouteBtn").prop('disabled', true);
            $("#generateDriveTransitRouteBtn").prop('disabled', true);
            $("#generateWalkOnlyRouteBtn").prop('disabled', true);
            $("#generateBicycleOnlyRouteBtn").prop('disabled', true);
        }
    }

    publicItem.clearAll = function () {
        //clear all marker and locations
        publicItem.clearMarkers();

        locations = [
            {
                selector: "#startLocationText"
            },
            {
                selector: "#endLocationText"
            }
        ];;
        //clear the text
        $('#startLocationText').val('');
        $('#endLocationText').val('');
        $("#waypoints").html('');
        contextMenuApi.AddWaypointToContextMenu(false);
        publicItem.updateTransitBtnState();
        //Make the autocomplete value null
        //routePlannerAutocomplete.clearEntries();
    };

    publicItem.clearMarkers = function () {
        //hide all marker.
        locations.forEach(function (location) {
            if (location && location.marker) {
                location.marker.setMap(null);
            }
        });
    };

    //Don't remove the start and end point
    publicItem.removeWaypoints = function () {
        publicItem.clearMarkers();
        locations = [locations[0], locations[locations.length - 1]];

        //enabling transit button as soon as less than 3 locations are selected
        publicItem.updateTransitBtnState();
    }

    publicItem.getLocationArray = function () {
        publicItem.getLocationsTextFromUI();
        let getWayPointLocations = locations.map(waypoint => routePlannerAutocomplete.GetLocationDetails(waypoint));

        return Promise.all(getWayPointLocations)
            .then((values) => {
                return values;
            });
    }

    // Handler for enter key after route selection. Needs to be public so 
    // routePlanner.js can disable it when:
    // - route generation is complete,
    // - user selects Reset Route button,
    // - user selects Start Over from context menu.
    // Disabling this handler stops the UI from generating the message
    // "Please check that you have entered a valid start location."
    // when the user presses enter in other input elements.
    publicItem.enterkeyHandler = function (e) {
        // This event handler is only for the enter key
        if (e.which != 13) {
            return;
        }
        // If the user has started route planning (ie this handler is validly enabled) 
        // but then clicks the location button or the login modal and presses the enter key
        // then don't generate a route request
        if (document.activeElement != document.getElementById('mapLocation') &&
            document.activeElement != document.getElementById('Passwordmodal')) {
            // switch to Routes Tab if not active
            if (!$('#RoutesTab').hasClass('active')) {
                $('#RoutesTab > a').click();
            }
            $('#generateRouteBtn').click();
        }
    };

    //Sets one of the locations.
    publicItem.setLocation = function (start, simplePlace, setText) {
        setLocationHelper(start ? 0 : locations.length - 1, simplePlace, setText);       

        //we have two locations, generate route.
        if (locations[0].point && locations[locations.length - 1].point) {
            contextMenuApi.AddWaypointToContextMenu(true);

            if (resources.AutoGenerateDriveRoute === "True") {
                routePlanner.calculateRoute();
            }
        } else { contextMenuApi.AddWaypointToContextMenu(false); };
        //remove the my location marker when we show any thing from the route planner
        $(document).trigger('removeMyLocationMarker');
        // Only define the route based enter key handler if the user has started route
        // planning by setting a start or end location.
        // Do not add multiple handlers for the same event.
        $(document).off('keypress', null, publicItem.enterkeyHandler);
        $(document).on('keypress', null, publicItem.enterkeyHandler);

    };

    publicItem.setWaypoint = function (simplePlace, index, setText, existingWaypoint, preventAutoGenerate) {
        //this check, on page load runs too early, and does not disable the transit button since it finds only 2 locations, and the 3 location is added later.
        publicItem.updateTransitBtnState();
        setWaypointHelper(simplePlace, index, setText, existingWaypoint);
        if (resources.AutoGenerateDriveRoute === "True" && !preventAutoGenerate) {
            routePlanner.calculateRoute();
        }

        //set marker if appropriate.
        if (simplePlace.point) {
            setMarker(index, new google.maps.LatLng(simplePlace.point.latitude, simplePlace.point.longitude));
        }
    };


    publicItem.SetLocationByGeo = setLocationByGeoLookup;

    //basically sets text fields and markers based on the current start and end location.
    publicItem.setDetails = function () {
        for (var i = 0; i < locations.length; ++i) {
            if (locations[i].point) {
                setLocationHelper(i, locations[i].point, true);
            }
        }
    };

    //set the start and end locations and display them.
    publicItem.setStartEndPoint = function (inStartLocation, inEndLocation) {
        publicItem.clearAll();
        setLocationHelper(0, inStartLocation, true);
        setLocationHelper(locations.length - 1, inEndLocation, true);
    };
    //set all the locations and display them.
    publicItem.setAllLocations = function (newWaypoints) {
        //specific things for the start and end locations
        publicItem.setStartEndPoint(newWaypoints[0], newWaypoints[newWaypoints.length - 1]);
        //insert the waypoints that are not the start and end
        for (var i = 1; i < newWaypoints.length - 1; ++i) {
            publicItem.setWaypoint(newWaypoints[i], i, null, null, true);
        }

        // in case text is not set
        for (var i = 0; i < newWaypoints.length; ++i) {
            if (!locations[i].text) {
                locations[i].text = newWaypoints[i].nameDirection || newWaypoints[i].name;
            }
        }
    };

    //Swap start and end points
    publicItem.swapStartEnd = function () {
        locations.reverse();
        publicItem.resetSelectors();
    };

    publicItem.resetSelectors = function () {
        locations[0].selector = "#startLocationText";
        for (let i = 1; i < locations.length; i++) {
            locations[i].selector = '#waypointText-' + i;
        }

        locations[locations.length - 1].selector = "#endLocationText";
    }

    publicItem.getLocations = function () {
        return locations;
    }

    //Update marker position if we got links by address
    publicItem.adjustMarker = function (index, lat, lng) {
        var newPosition = new google.maps.LatLng(lat, lng);
        setMarker(index, newPosition);
    }

    publicItem.addWaypoint = function (index, point) {
        var waypoint = {
            selector: '#waypointText-' + index
        };

        if (point) {
            waypoint.point = point;
            waypoint.text = point.name;
        }

        locations.splice(index, 0, waypoint);

        //disabling transit button as soon as more then 2 locations are set.
        publicItem.updateTransitBtnState();

        publicItem.resetSelectors();
    }

    publicItem.getLocationsTextFromUI = function () {
        locations.forEach(location => {
            let uiValue = $(location.selector).val();
            location.text = uiValue != null ? uiValue : location.text;
        });
    }

    publicItem.clearLoationsWithoutPoints = function () {
        // this only happens if user drags a point on the map,
        // and there is empty waypoints to be ignored 
        let newLocations = [locations[0]];
        for (let i = 1; i < locations.length - 1; i++) {
            if (locations[i].point) {
                newLocations.push(locations[i]);
            }
        }
        newLocations.push(locations[locations.length - 1]);
        locations = newLocations;
        publicItem.resetSelectors();
    }

    init();

    return publicItem;
};
var CitizenReporter = (function () {
    function CitizenReporter() {
        this.addReport = function (id, title) {
            $.ajax('/wta/wtaoptions', { data: { id: id, modalType: "CitizenReport" } }).done(function (data) {
                var bbox = bootbox.dialog({
                    title: title,
                    message: data,
                    closeButton: false,
                    className: "wtaContentModel crStatus",
                    buttons: {
                        save: {
                            label: resources.Save,
                            className: "btn-primary bootboxSave",
                            callback: function () {
                                var statuses = [];
                                $('.wtaOptionsParent select', this).each(function () {
                                    $.each($.makeArray($(this).val()), function () {
                                        statuses.push({ Id: this });
                                    });
                                });
                                $.ajax('/wta/addCitizenReport', {
                                    type: 'POST', data: {
                                        data: {
                                            id: id,
                                            statuses: statuses
                                        }
                                    }
                                }).done(function (data) {
                                    bootbox.alert(resources.CitizenReporterReportSubmited, null);
                                }).fail(function () {
                                    bootbox.alert(resources.CitizenReporterReportFailed, null);
                                });
                            }
                        },
                        cancel: {
                            label: resources.Cancel,
                            className: "btn-primary"
                        }
                    }
                });
                $('.selectpicker', bbox).selectpicker({
                    noneSelectedText: resources.NoneSelected,
                    selectedTextFormat: 'count > 3'
                });
            }).fail(function () {
                bootbox.alert(resources.ErrorLoadingWtaContent, null);
            });
        };
    }
    return CitizenReporter;
}());
var citizenReporter = new CitizenReporter();

/*
 * printThis v1.5
 * @desc Printing plug-in for jQuery
 * @author Jason Day
 *
 * Resources (based on) :
 *              jPrintArea: http://plugins.jquery.com/project/jPrintArea
 *              jqPrint: https://github.com/permanenttourist/jquery.jqprint
 *              Ben Nadal: http://www.bennadel.com/blog/1591-Ask-Ben-Print-Part-Of-A-Web-Page-With-jQuery.htm
 *
 * Licensed under the MIT licence:
 *              http://www.opensource.org/licenses/mit-license.php
 *
 * (c) Jason Day 2014
 *
 * Usage:
 *
 *  $("#mySelector").printThis({
 *      debug: false,               * show the iframe for debugging
 *      importCSS: true,            * import page CSS
 *      importStyle: false,         * import style tags
 *      printContainer: true,       * grab outer container as well as the contents of the selector
 *      loadCSS: "path/to/my.css",  * path to additional css file - us an array [] for multiple
 *      pageTitle: "",              * add title to print page
 *      removeInline: false,        * remove all inline styles from print elements
 *      printDelay: 333,            * variable print delay
 *      header: null,               * prefix to html
 *      formValues: true            * preserve input/form values
 *  });
 *
 * Notes:
 *  - the loadCSS will load additional css (with or without @media print) into the iframe, adjusting layout
 */
;
(function($) {
    var opt;
    $.fn.printThis = function(options) {
        opt = $.extend({}, $.fn.printThis.defaults, options);
        var $element = this instanceof jQuery ? this : $(this);

        var strFrameName = "printThis-" + (new Date()).getTime();

        if (window.location.hostname !== document.domain && navigator.userAgent.match(/msie/i)) {
            // Ugly IE hacks due to IE not inheriting document.domain from parent
            // checks if document.domain is set by comparing the host name against document.domain
            var iframeSrc = "javascript:document.write(\"<head><script>document.domain=\\\"" + document.domain + "\\\";</script></head><body></body>\")";
            var printI = document.createElement('iframe');
            printI.name = "printIframe";
            printI.id = strFrameName;
            printI.className = "MSIE";
            document.body.appendChild(printI);
            printI.src = iframeSrc;

        } else {
            // other browsers inherit document.domain, and IE works if document.domain is not explicitly set
            var $frame = $("<iframe id='" + strFrameName + "' name='printIframe' />");
            $frame.appendTo("body");
        }


        var $iframe = $("#" + strFrameName);

        // show frame if in debug mode
        if (!opt.debug) $iframe.css({
            position: "absolute",
            width: "0px",
            height: "0px",
            left: "-600px",
            top: "-600px"
        });


        // $iframe.ready() and $iframe.load were inconsistent between browsers    
        setTimeout(function() {

            var $doc = $iframe.contents(),
                $head = $doc.find("head"),
                $body = $doc.find("body");

            // add base tag to ensure elements use the parent domain
            $head.append('<base href="' + document.location.protocol + '//' + document.location.host + '">');

            // import page stylesheets
            if (opt.importCSS) $("link[rel=stylesheet]").each(function() {
                var href = $(this).attr("href");
                if (href) {
                    var media = $(this).attr("media") || "all";
                    $head.append("<link type='text/css' rel='stylesheet' href='" + href + "' media='" + media + "'>")
                }
            });
            
            // import style tags
            if (opt.importStyle) $("style").each(function() {
                $(this).clone().appendTo($head);
                //$head.append($(this));
            });

            //add title of the page
            if (opt.pageTitle) $head.append("<title>" + opt.pageTitle + "</title>");

            // import additional stylesheet(s)
            if (opt.loadCSS) {
               if( $.isArray(opt.loadCSS)) {
                    jQuery.each(opt.loadCSS, function(index, value) {
                       $head.append("<link type='text/css' rel='stylesheet' href='" + this + "'>");
                    });
                } else {
                    $head.append("<link type='text/css' rel='stylesheet' href='" + opt.loadCSS + "'>");
                }
            }

            // print header
            if (opt.header) $body.append(opt.header);

            // grab $.selector as container
            if (opt.printContainer) $body.append($element.outer());

            // otherwise just print interior elements of container
            else $element.each(function() {
                $body.append($(this).html());
            });

            // capture form/field values
            if (opt.formValues) {
                // loop through inputs
                var $input = $element.find('input');
                if ($input.length) {
                    $input.each(function() {
                        var $this = $(this),
                            $name = $(this).attr('name'),
                            $checker = $this.is(':checkbox') || $this.is(':radio'),
                            $iframeInput = $doc.find('input[name="' + $name + '"]'),
                            $value = $this.val();

                        //order matters here
                        if (!$checker) {
                            $iframeInput.val($value);
                        } else if ($this.is(':checked')) {
                            if ($this.is(':checkbox')) {
                                $iframeInput.attr('checked', 'checked');
                            } else if ($this.is(':radio')) {
                                $doc.find('input[name="' + $name + '"][value=' + $value + ']').attr('checked', 'checked');
                            }
                        }

                    });
                }

                //loop through selects
                var $select = $element.find('select');
                if ($select.length) {
                    $select.each(function() {
                        var $this = $(this),
                            $name = $(this).attr('name'),
                            $value = $this.val();
                        $doc.find('select[name="' + $name + '"]').val($value);
                    });
                }

                //loop through textareas
                var $textarea = $element.find('textarea');
                if ($textarea.length) {
                    $textarea.each(function() {
                        var $this = $(this),
                            $name = $(this).attr('name'),
                            $value = $this.val();
                        $doc.find('textarea[name="' + $name + '"]').val($value);
                    });
                }
            } // end capture form/field values

            // remove inline styles
            if (opt.removeInline) {
                // $.removeAttr available jQuery 1.7+
                if ($.isFunction($.removeAttr)) {
                    $doc.find("body *").removeAttr("style");
                } else {
                    $doc.find("body *").attr("style", "");
                }
            }

            setTimeout(function() {
                if ($iframe.hasClass("MSIE")) {
                    // check if the iframe was created with the ugly hack
                    // and perform another ugly hack out of neccessity
                    window.frames["printIframe"].focus();
                    $head.append("<script>  window.print(); </script>");
                } else {
                    // proper method
                    $iframe[0].contentWindow.focus();
                    $iframe[0].contentWindow.print();
                }

                //remove iframe after print
                if (!opt.debug) {
                    setTimeout(function() {
                        $iframe.remove();
                    }, 1000);
                }

            }, opt.printDelay);

        }, 333);

    };

    // defaults
    $.fn.printThis.defaults = {
        debug: false,           // show the iframe for debugging
        importCSS: true,        // import parent page css
        importStyle: false,     // import style tags
        printContainer: true,   // print outer container/$.selector
        loadCSS: "",            // load an additional css file - load multiple stylesheets with an array []
        pageTitle: "",          // add title to print page
        removeInline: false,    // remove all inline styles
        printDelay: 333,        // variable print delay
        header: null,           // prefix to html
        formValues: true        // preserve input/form values
    };

    // $.selector container
    jQuery.fn.outer = function() {
        return $($("<div></div>").html(this.clone())).html()
    }
})(jQuery);

!function(i){"function"==typeof define&&define.amd?define(["jquery"],i):"undefined"!=typeof exports?module.exports=i(require("jquery")):i(jQuery)}(function(i){var t,s=window.Slick||{};(s=(t=0,function s(e,o){var n,l=this;l.defaults={accessibility:!0,adaptiveHeight:!1,appendArrows:i(e),appendDots:i(e),arrows:!0,asNavFor:null,prevArrow:'<button class="slick-prev" type="button">Previous</button>',nextArrow:'<button class="slick-next" type="button">Next</button>',autoplay:!1,autoplaySpeed:3e3,centerMode:!1,centerPadding:"50px",cssEase:"ease",customPaging:function(t,s){return i('<button type="button" />').text(s+1)},dots:!1,dotsClass:"slick-dots",draggable:!0,easing:"linear",edgeFriction:.35,fade:!1,focusOnSelect:!1,focusOnChange:!1,infinite:!0,initialSlide:0,lazyLoad:"ondemand",mobileFirst:!1,pauseOnHover:!0,pauseOnFocus:!0,pauseOnDotsHover:!1,respondTo:"window",responsive:null,rows:1,rtl:!1,slide:"",slidesPerRow:1,slidesToShow:1,slidesToScroll:1,speed:500,swipe:!0,swipeToSlide:!1,touchMove:!0,touchThreshold:5,useCSS:!0,useTransform:!0,variableWidth:!1,vertical:!1,verticalSwiping:!1,waitForAnimate:!0,zIndex:1e3},l.initials={animating:!1,dragging:!1,autoPlayTimer:null,currentDirection:0,currentLeft:null,currentSlide:0,direction:1,$dots:null,listWidth:null,listHeight:null,loadIndex:0,$nextArrow:null,$prevArrow:null,scrolling:!1,slideCount:null,slideWidth:null,$slideTrack:null,$slides:null,sliding:!1,slideOffset:0,swipeLeft:null,swiping:!1,$list:null,touchObject:{},transformsEnabled:!1,unslicked:!1},i.extend(l,l.initials),l.activeBreakpoint=null,l.animType=null,l.animProp=null,l.breakpoints=[],l.breakpointSettings=[],l.cssTransitions=!1,l.focussed=!1,l.interrupted=!1,l.hidden="hidden",l.paused=!0,l.positionProp=null,l.respondTo=null,l.rowCount=1,l.shouldClick=!0,l.$slider=i(e),l.$slidesCache=null,l.transformType=null,l.transitionType=null,l.visibilityChange="visibilitychange",l.windowWidth=0,l.windowTimer=null,n=i(e).data("slick")||{},l.options=i.extend({},l.defaults,o,n),l.currentSlide=l.options.initialSlide,l.originalSettings=l.options,void 0!==document.mozHidden?(l.hidden="mozHidden",l.visibilityChange="mozvisibilitychange"):void 0!==document.webkitHidden&&(l.hidden="webkitHidden",l.visibilityChange="webkitvisibilitychange"),l.autoPlay=i.proxy(l.autoPlay,l),l.autoPlayClear=i.proxy(l.autoPlayClear,l),l.autoPlayIterator=i.proxy(l.autoPlayIterator,l),l.changeSlide=i.proxy(l.changeSlide,l),l.clickHandler=i.proxy(l.clickHandler,l),l.selectHandler=i.proxy(l.selectHandler,l),l.setPosition=i.proxy(l.setPosition,l),l.swipeHandler=i.proxy(l.swipeHandler,l),l.dragHandler=i.proxy(l.dragHandler,l),l.keyHandler=i.proxy(l.keyHandler,l),l.instanceUid=t++,l.htmlExpr=/^(?:\s*(<[\w\W]+>)[^>]*)$/,l.registerBreakpoints(),l.init(!0)})).prototype.activateADA=function(){this.$slideTrack.find(".slick-active").attr({"aria-hidden":"false"}).find("a, input, button, select").attr({tabindex:"0"})},s.prototype.addSlide=s.prototype.slickAdd=function(t,s,e){var o=this;if("boolean"==typeof s)e=s,s=null;else if(s<0||s>=o.slideCount)return!1;o.unload(),"number"==typeof s?0===s&&0===o.$slides.length?i(t).appendTo(o.$slideTrack):e?i(t).insertBefore(o.$slides.eq(s)):i(t).insertAfter(o.$slides.eq(s)):!0===e?i(t).prependTo(o.$slideTrack):i(t).appendTo(o.$slideTrack),o.$slides=o.$slideTrack.children(this.options.slide),o.$slideTrack.children(this.options.slide).detach(),o.$slideTrack.append(o.$slides),o.$slides.each(function(t,s){i(s).attr("data-slick-index",t)}),o.$slidesCache=o.$slides,o.reinit()},s.prototype.animateHeight=function(){if(1===this.options.slidesToShow&&!0===this.options.adaptiveHeight&&!1===this.options.vertical){var i=this.$slides.eq(this.currentSlide).outerHeight(!0);this.$list.animate({height:i},this.options.speed)}},s.prototype.animateSlide=function(t,s){var e={},o=this;o.animateHeight(),!0===o.options.rtl&&!1===o.options.vertical&&(t=-t),!1===o.transformsEnabled?!1===o.options.vertical?o.$slideTrack.animate({left:t},o.options.speed,o.options.easing,s):o.$slideTrack.animate({top:t},o.options.speed,o.options.easing,s):!1===o.cssTransitions?(!0===o.options.rtl&&(o.currentLeft=-o.currentLeft),i({animStart:o.currentLeft}).animate({animStart:t},{duration:o.options.speed,easing:o.options.easing,step:function(i){i=Math.ceil(i),!1===o.options.vertical?(e[o.animType]="translate("+i+"px, 0px)",o.$slideTrack.css(e)):(e[o.animType]="translate(0px,"+i+"px)",o.$slideTrack.css(e))},complete:function(){s&&s.call()}})):(o.applyTransition(),t=Math.ceil(t),!1===o.options.vertical?e[o.animType]="translate3d("+t+"px, 0px, 0px)":e[o.animType]="translate3d(0px,"+t+"px, 0px)",o.$slideTrack.css(e),s&&setTimeout(function(){o.disableTransition(),s.call()},o.options.speed))},s.prototype.getNavTarget=function(){var t=this.options.asNavFor;return t&&null!==t&&(t=i(t).not(this.$slider)),t},s.prototype.asNavFor=function(t){var s=this.getNavTarget();null!==s&&"object"==typeof s&&s.each(function(){var s=i(this).slick("getSlick");s.unslicked||s.slideHandler(t,!0)})},s.prototype.applyTransition=function(i){var t=this,s={};!1===t.options.fade?s[t.transitionType]=t.transformType+" "+t.options.speed+"ms "+t.options.cssEase:s[t.transitionType]="opacity "+t.options.speed+"ms "+t.options.cssEase,!1===t.options.fade?t.$slideTrack.css(s):t.$slides.eq(i).css(s)},s.prototype.autoPlay=function(){var i=this;i.autoPlayClear(),i.slideCount>i.options.slidesToShow&&(i.autoPlayTimer=setInterval(i.autoPlayIterator,i.options.autoplaySpeed))},s.prototype.autoPlayClear=function(){this.autoPlayTimer&&clearInterval(this.autoPlayTimer)},s.prototype.autoPlayIterator=function(){var i=this,t=i.currentSlide+i.options.slidesToScroll;i.paused||i.interrupted||i.focussed||(!1===i.options.infinite&&(1===i.direction&&i.currentSlide+1===i.slideCount-1?i.direction=0:0===i.direction&&(t=i.currentSlide-i.options.slidesToScroll,i.currentSlide-1==0&&(i.direction=1))),i.slideHandler(t))},s.prototype.buildArrows=function(){var t=this;!0===t.options.arrows&&(t.$prevArrow=i(t.options.prevArrow).addClass("slick-arrow"),t.$nextArrow=i(t.options.nextArrow).addClass("slick-arrow"),t.slideCount>t.options.slidesToShow?(t.$prevArrow.removeClass("slick-hidden").removeAttr("aria-hidden tabindex"),t.$nextArrow.removeClass("slick-hidden").removeAttr("aria-hidden tabindex"),t.htmlExpr.test(t.options.prevArrow)&&t.$prevArrow.prependTo(t.options.appendArrows),t.htmlExpr.test(t.options.nextArrow)&&t.$nextArrow.appendTo(t.options.appendArrows),!0!==t.options.infinite&&t.$prevArrow.addClass("slick-disabled").attr("aria-disabled","true")):t.$prevArrow.add(t.$nextArrow).addClass("slick-hidden").attr({"aria-disabled":"true",tabindex:"-1"}))},s.prototype.buildDots=function(){var t,s,e=this;if(!0===e.options.dots&&e.slideCount>e.options.slidesToShow){for(e.$slider.addClass("slick-dotted"),s=i("<ul />").addClass(e.options.dotsClass),t=0;t<=e.getDotCount();t+=1)s.append(i("<li />").append(e.options.customPaging.call(this,e,t)));e.$dots=s.appendTo(e.options.appendDots),e.$dots.find("li").first().addClass("slick-active")}},s.prototype.buildOut=function(){var t=this;t.$slides=t.$slider.children(t.options.slide+":not(.slick-cloned)").addClass("slick-slide"),t.slideCount=t.$slides.length,t.$slides.each(function(t,s){i(s).attr("data-slick-index",t).data("originalStyling",i(s).attr("style")||"")}),t.$slider.addClass("slick-slider"),t.$slideTrack=0===t.slideCount?i('<div class="slick-track"/>').appendTo(t.$slider):t.$slides.wrapAll('<div class="slick-track"/>').parent(),t.$list=t.$slideTrack.wrap('<div class="slick-list"/>').parent(),t.$slideTrack.css("opacity",0),(!0===t.options.centerMode||!0===t.options.swipeToSlide)&&(t.options.slidesToScroll=1),i("img[data-lazy]",t.$slider).not("[src]").addClass("slick-loading"),t.setupInfinite(),t.buildArrows(),t.buildDots(),t.updateDots(),t.setSlideClasses("number"==typeof t.currentSlide?t.currentSlide:0),!0===t.options.draggable&&t.$list.addClass("draggable")},s.prototype.buildRows=function(){var i,t,s,e,o,n,l;if(e=document.createDocumentFragment(),n=this.$slider.children(),this.options.rows>0){for(i=0,l=this.options.slidesPerRow*this.options.rows,o=Math.ceil(n.length/l);i<o;i++){var r=document.createElement("div");for(t=0;t<this.options.rows;t++){var d=document.createElement("div");for(s=0;s<this.options.slidesPerRow;s++){var a=i*l+(t*this.options.slidesPerRow+s);n.get(a)&&d.appendChild(n.get(a))}r.appendChild(d)}e.appendChild(r)}this.$slider.empty().append(e),this.$slider.children().children().children().css({width:100/this.options.slidesPerRow+"%",display:"inline-block"})}},s.prototype.checkResponsive=function(t,s){var e,o,n,l=this,r=!1,d=l.$slider.width(),a=window.innerWidth||i(window).width();if("window"===l.respondTo?n=a:"slider"===l.respondTo?n=d:"min"===l.respondTo&&(n=Math.min(a,d)),l.options.responsive&&l.options.responsive.length&&null!==l.options.responsive){for(e in o=null,l.breakpoints)l.breakpoints.hasOwnProperty(e)&&(!1===l.originalSettings.mobileFirst?n<l.breakpoints[e]&&(o=l.breakpoints[e]):n>l.breakpoints[e]&&(o=l.breakpoints[e]));null!==o?null!==l.activeBreakpoint?(o!==l.activeBreakpoint||s)&&(l.activeBreakpoint=o,"unslick"===l.breakpointSettings[o]?l.unslick(o):(l.options=i.extend({},l.originalSettings,l.breakpointSettings[o]),!0===t&&(l.currentSlide=l.options.initialSlide),l.refresh(t)),r=o):(l.activeBreakpoint=o,"unslick"===l.breakpointSettings[o]?l.unslick(o):(l.options=i.extend({},l.originalSettings,l.breakpointSettings[o]),!0===t&&(l.currentSlide=l.options.initialSlide),l.refresh(t)),r=o):null!==l.activeBreakpoint&&(l.activeBreakpoint=null,l.options=l.originalSettings,!0===t&&(l.currentSlide=l.options.initialSlide),l.refresh(t),r=o),t||!1===r||l.$slider.trigger("breakpoint",[l,r])}},s.prototype.changeSlide=function(t,s){var e,o,n,l=i(t.currentTarget);switch(l.is("a")&&t.preventDefault(),l.is("li")||(l=l.closest("li")),e=(n=this.slideCount%this.options.slidesToScroll!=0)?0:(this.slideCount-this.currentSlide)%this.options.slidesToScroll,t.data.message){case"previous":o=0===e?this.options.slidesToScroll:this.options.slidesToShow-e,this.slideCount>this.options.slidesToShow&&this.slideHandler(this.currentSlide-o,!1,s);break;case"next":o=0===e?this.options.slidesToScroll:e,this.slideCount>this.options.slidesToShow&&this.slideHandler(this.currentSlide+o,!1,s);break;case"index":var r=0===t.data.index?0:t.data.index||l.index()*this.options.slidesToScroll;this.slideHandler(this.checkNavigable(r),!1,s),l.children().trigger("focus");break;default:return}},s.prototype.checkNavigable=function(i){var t,s;if(t=this.getNavigableIndexes(),s=0,i>t[t.length-1])i=t[t.length-1];else for(var e in t){if(i<t[e]){i=s;break}s=t[e]}return i},s.prototype.cleanUpEvents=function(){this.options.dots&&null!==this.$dots&&(i("li",this.$dots).off("click.slick",this.changeSlide).off("mouseenter.slick",i.proxy(this.interrupt,this,!0)).off("mouseleave.slick",i.proxy(this.interrupt,this,!1)),!0===this.options.accessibility&&this.$dots.off("keydown.slick",this.keyHandler)),this.$slider.off("focus.slick blur.slick"),!0===this.options.arrows&&this.slideCount>this.options.slidesToShow&&(this.$prevArrow&&this.$prevArrow.off("click.slick",this.changeSlide),this.$nextArrow&&this.$nextArrow.off("click.slick",this.changeSlide),!0===this.options.accessibility&&(this.$prevArrow&&this.$prevArrow.off("keydown.slick",this.keyHandler),this.$nextArrow&&this.$nextArrow.off("keydown.slick",this.keyHandler))),this.$list.off("touchstart.slick mousedown.slick",this.swipeHandler),this.$list.off("touchmove.slick mousemove.slick",this.swipeHandler),this.$list.off("touchend.slick mouseup.slick",this.swipeHandler),this.$list.off("touchcancel.slick mouseleave.slick",this.swipeHandler),this.$list.off("click.slick",this.clickHandler),i(document).off(this.visibilityChange,this.visibility),this.cleanUpSlideEvents(),!0===this.options.accessibility&&this.$list.off("keydown.slick",this.keyHandler),!0===this.options.focusOnSelect&&i(this.$slideTrack).children().off("click.slick",this.selectHandler),i(window).off("orientationchange.slick.slick-"+this.instanceUid,this.orientationChange),i(window).off("resize.slick.slick-"+this.instanceUid,this.resize),i("[draggable!=true]",this.$slideTrack).off("dragstart",this.preventDefault),i(window).off("load.slick.slick-"+this.instanceUid,this.setPosition)},s.prototype.cleanUpSlideEvents=function(){this.$list.off("mouseenter.slick",i.proxy(this.interrupt,this,!0)),this.$list.off("mouseleave.slick",i.proxy(this.interrupt,this,!1))},s.prototype.cleanUpRows=function(){var i;this.options.rows>0&&((i=this.$slides.children().children()).removeAttr("style"),this.$slider.empty().append(i))},s.prototype.clickHandler=function(i){!1===this.shouldClick&&(i.stopImmediatePropagation(),i.stopPropagation(),i.preventDefault())},s.prototype.destroy=function(t){var s=this;s.autoPlayClear(),s.touchObject={},s.cleanUpEvents(),i(".slick-cloned",s.$slider).detach(),s.$dots&&s.$dots.remove(),s.$prevArrow&&s.$prevArrow.length&&(s.$prevArrow.removeClass("slick-disabled slick-arrow slick-hidden").removeAttr("aria-hidden aria-disabled tabindex").css("display",""),s.htmlExpr.test(s.options.prevArrow)&&s.$prevArrow.remove()),s.$nextArrow&&s.$nextArrow.length&&(s.$nextArrow.removeClass("slick-disabled slick-arrow slick-hidden").removeAttr("aria-hidden aria-disabled tabindex").css("display",""),s.htmlExpr.test(s.options.nextArrow)&&s.$nextArrow.remove()),s.$slides&&(s.$slides.removeClass("slick-slide slick-active slick-center slick-visible slick-current").removeAttr("aria-hidden").removeAttr("data-slick-index").each(function(){i(this).attr("style",i(this).data("originalStyling"))}),s.$slideTrack.children(this.options.slide).detach(),s.$slideTrack.detach(),s.$list.detach(),s.$slider.append(s.$slides)),s.cleanUpRows(),s.$slider.removeClass("slick-slider"),s.$slider.removeClass("slick-initialized"),s.$slider.removeClass("slick-dotted"),s.unslicked=!0,t||s.$slider.trigger("destroy",[s])},s.prototype.disableTransition=function(i){var t=this,s={};s[t.transitionType]="",!1===t.options.fade?t.$slideTrack.css(s):t.$slides.eq(i).css(s)},s.prototype.fadeSlide=function(i,t){var s=this;!1===s.cssTransitions?(s.$slides.eq(i).css({zIndex:s.options.zIndex}),s.$slides.eq(i).animate({opacity:1},s.options.speed,s.options.easing,t)):(s.applyTransition(i),s.$slides.eq(i).css({opacity:1,zIndex:s.options.zIndex}),t&&setTimeout(function(){s.disableTransition(i),t.call()},s.options.speed))},s.prototype.fadeSlideOut=function(i){!1===this.cssTransitions?this.$slides.eq(i).animate({opacity:0,zIndex:this.options.zIndex-2},this.options.speed,this.options.easing):(this.applyTransition(i),this.$slides.eq(i).css({opacity:0,zIndex:this.options.zIndex-2}))},s.prototype.filterSlides=s.prototype.slickFilter=function(i){var t=this;null!==i&&(t.$slidesCache=t.$slides,t.unload(),t.$slideTrack.children(this.options.slide).detach(),t.$slidesCache.filter(i).appendTo(t.$slideTrack),t.reinit())},s.prototype.focusHandler=function(){var t=this;t.$slider.off("focus.slick blur.slick").on("focus.slick","*",function(s){var e=i(this);setTimeout(function(){t.options.pauseOnFocus&&e.is(":focus")&&(t.focussed=!0,t.autoPlay())},0)}).on("blur.slick","*",function(s){i(this),t.options.pauseOnFocus&&(t.focussed=!1,t.autoPlay())})},s.prototype.getCurrent=s.prototype.slickCurrentSlide=function(){return this.currentSlide},s.prototype.getDotCount=function(){var i=0,t=0,s=0;if(!0===this.options.infinite){if(this.slideCount<=this.options.slidesToShow)++s;else for(;i<this.slideCount;)++s,i=t+this.options.slidesToScroll,t+=this.options.slidesToScroll<=this.options.slidesToShow?this.options.slidesToScroll:this.options.slidesToShow}else if(!0===this.options.centerMode)s=this.slideCount;else if(this.options.asNavFor)for(;i<this.slideCount;)++s,i=t+this.options.slidesToScroll,t+=this.options.slidesToScroll<=this.options.slidesToShow?this.options.slidesToScroll:this.options.slidesToShow;else s=1+Math.ceil((this.slideCount-this.options.slidesToShow)/this.options.slidesToScroll);return s-1},s.prototype.getLeft=function(i){var t,s,e,o,n=this,l=0;return n.slideOffset=0,s=n.$slides.first().outerHeight(!0),!0===n.options.infinite?(n.slideCount>n.options.slidesToShow&&(n.slideOffset=-(n.slideWidth*n.options.slidesToShow*1),o=-1,!0===n.options.vertical&&!0===n.options.centerMode&&(2===n.options.slidesToShow?o=-1.5:1===n.options.slidesToShow&&(o=-2)),l=s*n.options.slidesToShow*o),n.slideCount%n.options.slidesToScroll!=0&&i+n.options.slidesToScroll>n.slideCount&&n.slideCount>n.options.slidesToShow&&(i>n.slideCount?(n.slideOffset=-((n.options.slidesToShow-(i-n.slideCount))*n.slideWidth*1),l=-((n.options.slidesToShow-(i-n.slideCount))*s*1)):(n.slideOffset=-(n.slideCount%n.options.slidesToScroll*n.slideWidth*1),l=-(n.slideCount%n.options.slidesToScroll*s*1)))):i+n.options.slidesToShow>n.slideCount&&(n.slideOffset=(i+n.options.slidesToShow-n.slideCount)*n.slideWidth,l=(i+n.options.slidesToShow-n.slideCount)*s),n.slideCount<=n.options.slidesToShow&&(n.slideOffset=0,l=0),!0===n.options.centerMode&&n.slideCount<=n.options.slidesToShow?n.slideOffset=n.slideWidth*Math.floor(n.options.slidesToShow)/2-n.slideWidth*n.slideCount/2:!0===n.options.centerMode&&!0===n.options.infinite?n.slideOffset+=n.slideWidth*Math.floor(n.options.slidesToShow/2)-n.slideWidth:!0===n.options.centerMode&&(n.slideOffset=0,n.slideOffset+=n.slideWidth*Math.floor(n.options.slidesToShow/2)),t=!1===n.options.vertical?-(i*n.slideWidth*1)+n.slideOffset:-(i*s*1)+l,!0===n.options.variableWidth&&(e=n.slideCount<=n.options.slidesToShow||!1===n.options.infinite?n.$slideTrack.children(".slick-slide").eq(i):n.$slideTrack.children(".slick-slide").eq(i+n.options.slidesToShow),t=!0===n.options.rtl?e[0]?-((n.$slideTrack.width()-e[0].offsetLeft-e.width())*1):0:e[0]?-1*e[0].offsetLeft:0,!0===n.options.centerMode&&(e=n.slideCount<=n.options.slidesToShow||!1===n.options.infinite?n.$slideTrack.children(".slick-slide").eq(i):n.$slideTrack.children(".slick-slide").eq(i+n.options.slidesToShow+1),t=!0===n.options.rtl?e[0]?-((n.$slideTrack.width()-e[0].offsetLeft-e.width())*1):0:e[0]?-1*e[0].offsetLeft:0,t+=(n.$list.width()-e.outerWidth())/2)),t},s.prototype.getOption=s.prototype.slickGetOption=function(i){return this.options[i]},s.prototype.getNavigableIndexes=function(){var i,t=0,s=0,e=[];for(!1===this.options.infinite?i=this.slideCount:(t=-1*this.options.slidesToScroll,s=-1*this.options.slidesToScroll,i=2*this.slideCount);t<i;)e.push(t),t=s+this.options.slidesToScroll,s+=this.options.slidesToScroll<=this.options.slidesToShow?this.options.slidesToScroll:this.options.slidesToShow;return e},s.prototype.getSlick=function(){return this},s.prototype.getSlideCount=function(){var t,s,e,o,n=this;return(o=!0===n.options.centerMode?Math.floor(n.$list.width()/2):0,e=-1*n.swipeLeft+o,!0===n.options.swipeToSlide)?(n.$slideTrack.find(".slick-slide").each(function(t,o){var l,r,d;if(l=i(o).outerWidth(),r=o.offsetLeft,!0!==n.options.centerMode&&(r+=l/2),e<(d=r+l))return s=o,!1}),t=Math.abs(i(s).attr("data-slick-index")-n.currentSlide)||1):n.options.slidesToScroll},s.prototype.goTo=s.prototype.slickGoTo=function(i,t){this.changeSlide({data:{message:"index",index:parseInt(i)}},t)},s.prototype.init=function(t){var s=this;i(s.$slider).hasClass("slick-initialized")||(i(s.$slider).addClass("slick-initialized"),s.buildRows(),s.buildOut(),s.setProps(),s.startLoad(),s.loadSlider(),s.initializeEvents(),s.updateArrows(),s.updateDots(),s.checkResponsive(!0),s.focusHandler()),t&&s.$slider.trigger("init",[s]),!0===s.options.accessibility&&s.initADA(),s.options.autoplay&&(s.paused=!1,s.autoPlay())},s.prototype.initADA=function(){var t=this,s=Math.ceil(t.slideCount/t.options.slidesToShow),e=t.getNavigableIndexes().filter(function(i){return i>=0&&i<t.slideCount});t.$slides.add(t.$slideTrack.find(".slick-cloned")).attr({"aria-hidden":"true",tabindex:"-1"}).find("a, input, button, select").attr({tabindex:"-1"}),null!==t.$dots&&(t.$slides.not(t.$slideTrack.find(".slick-cloned")).each(function(s){var o=e.indexOf(s);if(i(this).attr({role:"tabpanel",id:"slick-slide"+t.instanceUid+s,tabindex:-1}),-1!==o){var n="slick-slide-control"+t.instanceUid+o;i("#"+n).length&&i(this).attr({"aria-describedby":n})}}),t.$dots.attr("role","tablist").find("li").each(function(o){var n=e[o];i(this).attr({role:"presentation"}),i(this).find("button").first().attr({role:"tab",id:"slick-slide-control"+t.instanceUid+o,"aria-controls":"slick-slide"+t.instanceUid+n,"aria-label":o+1+" of "+s,"aria-selected":null,tabindex:"-1"})}).eq(t.currentSlide).find("button").attr({"aria-selected":"true",tabindex:"0"}).end());for(var o=t.currentSlide,n=o+t.options.slidesToShow;o<n;o++)t.options.focusOnChange?t.$slides.eq(o).attr({tabindex:"0"}):t.$slides.eq(o).removeAttr("tabindex");t.activateADA()},s.prototype.initArrowEvents=function(){!0===this.options.arrows&&this.slideCount>this.options.slidesToShow&&(this.$prevArrow.off("click.slick").on("click.slick",{message:"previous"},this.changeSlide),this.$nextArrow.off("click.slick").on("click.slick",{message:"next"},this.changeSlide),!0===this.options.accessibility&&(this.$prevArrow.on("keydown.slick",this.keyHandler),this.$nextArrow.on("keydown.slick",this.keyHandler)))},s.prototype.initDotEvents=function(){!0===this.options.dots&&this.slideCount>this.options.slidesToShow&&(i("li",this.$dots).on("click.slick",{message:"index"},this.changeSlide),!0===this.options.accessibility&&this.$dots.on("keydown.slick",this.keyHandler)),!0===this.options.dots&&!0===this.options.pauseOnDotsHover&&this.slideCount>this.options.slidesToShow&&i("li",this.$dots).on("mouseenter.slick",i.proxy(this.interrupt,this,!0)).on("mouseleave.slick",i.proxy(this.interrupt,this,!1))},s.prototype.initSlideEvents=function(){this.options.pauseOnHover&&(this.$list.on("mouseenter.slick",i.proxy(this.interrupt,this,!0)),this.$list.on("mouseleave.slick",i.proxy(this.interrupt,this,!1)))},s.prototype.initializeEvents=function(){this.initArrowEvents(),this.initDotEvents(),this.initSlideEvents(),this.$list.on("touchstart.slick mousedown.slick",{action:"start"},this.swipeHandler),this.$list.on("touchmove.slick mousemove.slick",{action:"move"},this.swipeHandler),this.$list.on("touchend.slick mouseup.slick",{action:"end"},this.swipeHandler),this.$list.on("touchcancel.slick mouseleave.slick",{action:"end"},this.swipeHandler),this.$list.on("click.slick",this.clickHandler),i(document).on(this.visibilityChange,i.proxy(this.visibility,this)),!0===this.options.accessibility&&this.$list.on("keydown.slick",this.keyHandler),!0===this.options.focusOnSelect&&i(this.$slideTrack).children().on("click.slick",this.selectHandler),i(window).on("orientationchange.slick.slick-"+this.instanceUid,i.proxy(this.orientationChange,this)),i(window).on("resize.slick.slick-"+this.instanceUid,i.proxy(this.resize,this)),i("[draggable!=true]",this.$slideTrack).on("dragstart",this.preventDefault),i(window).on("load.slick.slick-"+this.instanceUid,this.setPosition),i(this.setPosition)},s.prototype.initUI=function(){!0===this.options.arrows&&this.slideCount>this.options.slidesToShow&&(this.$prevArrow.show(),this.$nextArrow.show()),!0===this.options.dots&&this.slideCount>this.options.slidesToShow&&this.$dots.show()},s.prototype.keyHandler=function(i){i.target.tagName.match("TEXTAREA|INPUT|SELECT")||(37===i.keyCode&&!0===this.options.accessibility?this.changeSlide({data:{message:!0===this.options.rtl?"next":"previous"}}):39===i.keyCode&&!0===this.options.accessibility&&this.changeSlide({data:{message:!0===this.options.rtl?"previous":"next"}}))},s.prototype.lazyLoad=function(){var t,s,e,o,n=this;function l(t){i("img[data-lazy]",t).each(function(){var t=i(this),s=i(this).attr("data-lazy"),e=i(this).attr("data-srcset"),o=i(this).attr("data-srcloaded"),l=i(this).attr("data-sizes")||n.$slider.attr("data-sizes");o||(t.attr("data-srcloaded","true"),t.animate({opacity:0},100,function(){e&&(t.attr("srcset",e),l&&t.attr("sizes",l)),t.attr("src",s).animate({opacity:1},200,function(){t.removeAttr("data-lazy data-srcset data-sizes").removeClass("slick-loading")}),n.$slider.trigger("lazyLoaded",[n,t,s])}))})}if(!0===n.options.centerMode?!0===n.options.infinite?o=(e=n.currentSlide+(n.options.slidesToShow/2+1))+n.options.slidesToShow+2:(e=Math.max(0,n.currentSlide-(n.options.slidesToShow/2+1)),o=2+(n.options.slidesToShow/2+1)+n.currentSlide):(o=Math.ceil((e=n.options.infinite?n.options.slidesToShow+n.currentSlide:n.currentSlide)+n.options.slidesToShow),!0===n.options.fade&&(e>0&&e--,o<=n.slideCount&&o++)),t=n.$slider.find(".slick-slide").slice(e,o),"anticipated"===n.options.lazyLoad)for(var r=e-1,d=o,a=n.$slider.find(".slick-slide"),c=0;c<n.options.slidesToScroll;c++)r<0&&(r=n.slideCount-1),t=(t=t.add(a.eq(r))).add(a.eq(d)),r--,d++;l(t),n.slideCount<=n.options.slidesToShow?l(s=n.$slider.find(".slick-slide")):n.currentSlide>=n.slideCount-n.options.slidesToShow?l(s=n.$slider.find(".slick-cloned").slice(0,n.options.slidesToShow)):0===n.currentSlide&&l(s=n.$slider.find(".slick-cloned").slice(-1*n.options.slidesToShow))},s.prototype.loadSlider=function(){this.setPosition(),this.$slideTrack.css({opacity:1}),this.$slider.removeClass("slick-loading"),this.initUI(),"progressive"===this.options.lazyLoad&&this.progressiveLazyLoad()},s.prototype.next=s.prototype.slickNext=function(){this.changeSlide({data:{message:"next"}})},s.prototype.orientationChange=function(){this.checkResponsive(),this.setPosition()},s.prototype.pause=s.prototype.slickPause=function(){var i=this;i.autoPlayClear(),i.paused=!0},s.prototype.play=s.prototype.slickPlay=function(){var i=this;i.autoPlay(),i.options.autoplay=!0,i.paused=!1,i.focussed=!1,i.interrupted=!1},s.prototype.postSlide=function(t){var s=this;!s.unslicked&&(s.$slider.trigger("afterChange",[s,t]),s.animating=!1,s.slideCount>s.options.slidesToShow&&s.setPosition(),s.swipeLeft=null,s.options.autoplay&&s.autoPlay(),!0===s.options.accessibility&&(s.initADA(),s.options.focusOnChange))&&i(s.$slides.get(s.currentSlide)).attr("tabindex",0).focus()},s.prototype.prev=s.prototype.slickPrev=function(){this.changeSlide({data:{message:"previous"}})},s.prototype.preventDefault=function(i){i.preventDefault()},s.prototype.progressiveLazyLoad=function(t){t=t||1;var s,e,o,n,l,r=this,d=i("img[data-lazy]",r.$slider);d.length?(e=(s=d.first()).attr("data-lazy"),o=s.attr("data-srcset"),n=s.attr("data-sizes")||r.$slider.attr("data-sizes"),(l=document.createElement("img")).onload=function(){o&&(s.attr("srcset",o),n&&s.attr("sizes",n)),s.attr("src",e).removeAttr("data-lazy data-srcset data-sizes").removeClass("slick-loading"),!0===r.options.adaptiveHeight&&r.setPosition(),r.$slider.trigger("lazyLoaded",[r,s,e]),r.progressiveLazyLoad()},l.onerror=function(){t<3?setTimeout(function(){r.progressiveLazyLoad(t+1)},500):(s.removeAttr("data-lazy").removeClass("slick-loading").addClass("slick-lazyload-error"),r.$slider.trigger("lazyLoadError",[r,s,e]),r.progressiveLazyLoad())},l.src=e):r.$slider.trigger("allImagesLoaded",[r])},s.prototype.refresh=function(t){var s,e,o=this;e=o.slideCount-o.options.slidesToShow,!o.options.infinite&&o.currentSlide>e&&(o.currentSlide=e),o.slideCount<=o.options.slidesToShow&&(o.currentSlide=0),s=o.currentSlide,o.destroy(!0),i.extend(o,o.initials,{currentSlide:s}),o.init(),t||o.changeSlide({data:{message:"index",index:s}},!1)},s.prototype.registerBreakpoints=function(){var t,s,e,o=this,n=o.options.responsive||null;if("array"===i.type(n)&&n.length){for(t in o.respondTo=o.options.respondTo||"window",n)if(e=o.breakpoints.length-1,n.hasOwnProperty(t)){for(s=n[t].breakpoint;e>=0;)o.breakpoints[e]&&o.breakpoints[e]===s&&o.breakpoints.splice(e,1),e--;o.breakpoints.push(s),o.breakpointSettings[s]=n[t].settings}o.breakpoints.sort(function(i,t){return o.options.mobileFirst?i-t:t-i})}},s.prototype.reinit=function(){var t=this;t.$slides=t.$slideTrack.children(t.options.slide).addClass("slick-slide"),t.slideCount=t.$slides.length,t.currentSlide>=t.slideCount&&0!==t.currentSlide&&(t.currentSlide=t.currentSlide-t.options.slidesToScroll),t.slideCount<=t.options.slidesToShow&&(t.currentSlide=0),t.registerBreakpoints(),t.setProps(),t.setupInfinite(),t.buildArrows(),t.updateArrows(),t.initArrowEvents(),t.buildDots(),t.updateDots(),t.initDotEvents(),t.cleanUpSlideEvents(),t.initSlideEvents(),t.checkResponsive(!1,!0),!0===t.options.focusOnSelect&&i(t.$slideTrack).children().on("click.slick",t.selectHandler),t.setSlideClasses("number"==typeof t.currentSlide?t.currentSlide:0),t.setPosition(),t.focusHandler(),t.paused=!t.options.autoplay,t.autoPlay(),t.$slider.trigger("reInit",[t])},s.prototype.resize=function(){var t=this;i(window).width()!==t.windowWidth&&(clearTimeout(t.windowDelay),t.windowDelay=window.setTimeout(function(){t.windowWidth=i(window).width(),t.checkResponsive(),t.unslicked||t.setPosition()},50))},s.prototype.removeSlide=s.prototype.slickRemove=function(i,t,s){var e=this;if(i="boolean"==typeof i?!0===(t=i)?0:e.slideCount-1:!0===t?--i:i,e.slideCount<1||i<0||i>e.slideCount-1)return!1;e.unload(),!0===s?e.$slideTrack.children().remove():e.$slideTrack.children(this.options.slide).eq(i).remove(),e.$slides=e.$slideTrack.children(this.options.slide),e.$slideTrack.children(this.options.slide).detach(),e.$slideTrack.append(e.$slides),e.$slidesCache=e.$slides,e.reinit()},s.prototype.setCSS=function(i){var t,s,e=this,o={};!0===e.options.rtl&&(i=-i),t="left"==e.positionProp?Math.ceil(i)+"px":"0px",s="top"==e.positionProp?Math.ceil(i)+"px":"0px",o[e.positionProp]=i,!1===e.transformsEnabled?e.$slideTrack.css(o):(o={},!1===e.cssTransitions?(o[e.animType]="translate("+t+", "+s+")",e.$slideTrack.css(o)):(o[e.animType]="translate3d("+t+", "+s+", 0px)",e.$slideTrack.css(o)))},s.prototype.setDimensions=function(){var i=this;!1===i.options.vertical?!0===i.options.centerMode&&i.$list.css({padding:"0px "+i.options.centerPadding}):(i.$list.height(i.$slides.first().outerHeight(!0)*i.options.slidesToShow),!0===i.options.centerMode&&i.$list.css({padding:i.options.centerPadding+" 0px"})),i.listWidth=i.$list.width(),i.listHeight=i.$list.height(),!1===i.options.vertical&&!1===i.options.variableWidth?(i.slideWidth=Math.ceil(i.listWidth/i.options.slidesToShow),i.$slideTrack.width(Math.ceil(i.slideWidth*i.$slideTrack.children(".slick-slide").length))):!0===i.options.variableWidth?i.$slideTrack.width(5e3*i.slideCount):(i.slideWidth=Math.ceil(i.listWidth),i.$slideTrack.height(Math.ceil(i.$slides.first().outerHeight(!0)*i.$slideTrack.children(".slick-slide").length)));var t=i.$slides.first().outerWidth(!0)-i.$slides.first().width();!1===i.options.variableWidth&&i.$slideTrack.children(".slick-slide").width(i.slideWidth-t)},s.prototype.setFade=function(){var t,s=this;s.$slides.each(function(e,o){t=-(s.slideWidth*e*1),!0===s.options.rtl?i(o).css({position:"relative",right:t,top:0,zIndex:s.options.zIndex-2,opacity:0}):i(o).css({position:"relative",left:t,top:0,zIndex:s.options.zIndex-2,opacity:0})}),s.$slides.eq(s.currentSlide).css({zIndex:s.options.zIndex-1,opacity:1})},s.prototype.setHeight=function(){if(1===this.options.slidesToShow&&!0===this.options.adaptiveHeight&&!1===this.options.vertical){var i=this.$slides.eq(this.currentSlide).outerHeight(!0);this.$list.css("height",i)}},s.prototype.setOption=s.prototype.slickSetOption=function(){var t,s,e,o,n,l=this,r=!1;if("object"===i.type(arguments[0])?(e=arguments[0],r=arguments[1],n="multiple"):"string"===i.type(arguments[0])&&(e=arguments[0],o=arguments[1],r=arguments[2],"responsive"===arguments[0]&&"array"===i.type(arguments[1])?n="responsive":void 0!==arguments[1]&&(n="single")),"single"===n)l.options[e]=o;else if("multiple"===n)i.each(e,function(i,t){l.options[i]=t});else if("responsive"===n)for(s in o)if("array"!==i.type(l.options.responsive))l.options.responsive=[o[s]];else{for(t=l.options.responsive.length-1;t>=0;)l.options.responsive[t].breakpoint===o[s].breakpoint&&l.options.responsive.splice(t,1),t--;l.options.responsive.push(o[s])}r&&(l.unload(),l.reinit())},s.prototype.setPosition=function(){this.setDimensions(),this.setHeight(),!1===this.options.fade?this.setCSS(this.getLeft(this.currentSlide)):this.setFade(),this.$slider.trigger("setPosition",[this])},s.prototype.setProps=function(){var i=this,t=document.body.style;i.positionProp=!0===i.options.vertical?"top":"left","top"===i.positionProp?i.$slider.addClass("slick-vertical"):i.$slider.removeClass("slick-vertical"),(void 0!==t.WebkitTransition||void 0!==t.MozTransition||void 0!==t.msTransition)&&!0===i.options.useCSS&&(i.cssTransitions=!0),i.options.fade&&("number"==typeof i.options.zIndex?i.options.zIndex<3&&(i.options.zIndex=3):i.options.zIndex=i.defaults.zIndex),void 0!==t.OTransform&&(i.animType="OTransform",i.transformType="-o-transform",i.transitionType="OTransition",void 0===t.perspectiveProperty&&void 0===t.webkitPerspective&&(i.animType=!1)),void 0!==t.MozTransform&&(i.animType="MozTransform",i.transformType="-moz-transform",i.transitionType="MozTransition",void 0===t.perspectiveProperty&&void 0===t.MozPerspective&&(i.animType=!1)),void 0!==t.webkitTransform&&(i.animType="webkitTransform",i.transformType="-webkit-transform",i.transitionType="webkitTransition",void 0===t.perspectiveProperty&&void 0===t.webkitPerspective&&(i.animType=!1)),void 0!==t.msTransform&&(i.animType="msTransform",i.transformType="-ms-transform",i.transitionType="msTransition",void 0===t.msTransform&&(i.animType=!1)),void 0!==t.transform&&!1!==i.animType&&(i.animType="transform",i.transformType="transform",i.transitionType="transition"),i.transformsEnabled=i.options.useTransform&&null!==i.animType&&!1!==i.animType},s.prototype.setSlideClasses=function(i){var t,s,e,o;if(s=this.$slider.find(".slick-slide").removeClass("slick-active slick-center slick-current").attr("aria-hidden","true"),this.$slides.eq(i).addClass("slick-current"),!0===this.options.centerMode){var n=this.options.slidesToShow%2==0?1:0;t=Math.floor(this.options.slidesToShow/2),!0===this.options.infinite&&(i>=t&&i<=this.slideCount-1-t?this.$slides.slice(i-t+n,i+t+1).addClass("slick-active").attr("aria-hidden","false"):(e=this.options.slidesToShow+i,s.slice(e-t+1+n,e+t+2).addClass("slick-active").attr("aria-hidden","false")),0===i?s.eq(s.length-1-this.options.slidesToShow).addClass("slick-center"):i===this.slideCount-1&&s.eq(this.options.slidesToShow).addClass("slick-center")),this.$slides.eq(i).addClass("slick-center")}else i>=0&&i<=this.slideCount-this.options.slidesToShow?this.$slides.slice(i,i+this.options.slidesToShow).addClass("slick-active").attr("aria-hidden","false"):s.length<=this.options.slidesToShow?s.addClass("slick-active").attr("aria-hidden","false"):(o=this.slideCount%this.options.slidesToShow,e=!0===this.options.infinite?this.options.slidesToShow+i:i,this.options.slidesToShow==this.options.slidesToScroll&&this.slideCount-i<this.options.slidesToShow?s.slice(e-(this.options.slidesToShow-o),e+o).addClass("slick-active").attr("aria-hidden","false"):s.slice(e,e+this.options.slidesToShow).addClass("slick-active").attr("aria-hidden","false"));("ondemand"===this.options.lazyLoad||"anticipated"===this.options.lazyLoad)&&this.lazyLoad()},s.prototype.setupInfinite=function(){var t,s,e,o=this;if(!0===o.options.fade&&(o.options.centerMode=!1),!0===o.options.infinite&&!1===o.options.fade&&(s=null,o.slideCount>o.options.slidesToShow)){for(e=!0===o.options.centerMode?o.options.slidesToShow+1:o.options.slidesToShow,t=o.slideCount;t>o.slideCount-e;t-=1)s=t-1,i(o.$slides[s]).clone(!0).attr("id","").attr("data-slick-index",s-o.slideCount).prependTo(o.$slideTrack).addClass("slick-cloned");for(t=0;t<e+o.slideCount;t+=1)s=t,i(o.$slides[s]).clone(!0).attr("id","").attr("data-slick-index",s+o.slideCount).appendTo(o.$slideTrack).addClass("slick-cloned");o.$slideTrack.find(".slick-cloned").find("[id]").each(function(){i(this).attr("id","")})}},s.prototype.interrupt=function(i){var t=this;i||t.autoPlay(),t.interrupted=i},s.prototype.selectHandler=function(t){var s=parseInt((i(t.target).is(".slick-slide")?i(t.target):i(t.target).parents(".slick-slide")).attr("data-slick-index"));if(s||(s=0),this.slideCount<=this.options.slidesToShow){this.slideHandler(s,!1,!0);return}this.slideHandler(s)},s.prototype.slideHandler=function(i,t,s){var e,o,n,l,r,d=null,a=this;if(t=t||!1,(!0!==a.animating||!0!==a.options.waitForAnimate)&&(!0!==a.options.fade||a.currentSlide!==i)){if(!1===t&&a.asNavFor(i),e=i,d=a.getLeft(e),l=a.getLeft(a.currentSlide),a.currentLeft=null===a.swipeLeft?l:a.swipeLeft,!1===a.options.infinite&&!1===a.options.centerMode&&(i<0||i>a.getDotCount()*a.options.slidesToScroll)||!1===a.options.infinite&&!0===a.options.centerMode&&(i<0||i>a.slideCount-a.options.slidesToScroll)){!1===a.options.fade&&(e=a.currentSlide,!0!==s&&a.slideCount>a.options.slidesToShow?a.animateSlide(l,function(){a.postSlide(e)}):a.postSlide(e));return}if(a.options.autoplay&&clearInterval(a.autoPlayTimer),o=e<0?a.slideCount%a.options.slidesToScroll!=0?a.slideCount-a.slideCount%a.options.slidesToScroll:a.slideCount+e:e>=a.slideCount?a.slideCount%a.options.slidesToScroll!=0?0:e-a.slideCount:e,a.animating=!0,a.$slider.trigger("beforeChange",[a,a.currentSlide,o]),n=a.currentSlide,a.currentSlide=o,a.setSlideClasses(a.currentSlide),a.options.asNavFor&&(r=(r=a.getNavTarget()).slick("getSlick")).slideCount<=r.options.slidesToShow&&r.setSlideClasses(a.currentSlide),a.updateDots(),a.updateArrows(),!0===a.options.fade){!0!==s?(a.fadeSlideOut(n),a.fadeSlide(o,function(){a.postSlide(o)})):a.postSlide(o),a.animateHeight();return}!0!==s&&a.slideCount>a.options.slidesToShow?a.animateSlide(d,function(){a.postSlide(o)}):a.postSlide(o)}},s.prototype.startLoad=function(){!0===this.options.arrows&&this.slideCount>this.options.slidesToShow&&(this.$prevArrow.hide(),this.$nextArrow.hide()),!0===this.options.dots&&this.slideCount>this.options.slidesToShow&&this.$dots.hide(),this.$slider.addClass("slick-loading")},s.prototype.swipeDirection=function(){var i,t,s,e;return(i=this.touchObject.startX-this.touchObject.curX,(e=Math.round(180*(s=Math.atan2(t=this.touchObject.startY-this.touchObject.curY,i))/Math.PI))<0&&(e=360-Math.abs(e)),e<=45&&e>=0||e<=360&&e>=315)?!1===this.options.rtl?"left":"right":e>=135&&e<=225?!1===this.options.rtl?"right":"left":!0===this.options.verticalSwiping?e>=35&&e<=135?"down":"up":"vertical"},s.prototype.swipeEnd=function(i){var t,s,e=this;if(e.dragging=!1,e.swiping=!1,e.scrolling)return e.scrolling=!1,!1;if(e.interrupted=!1,e.shouldClick=!(e.touchObject.swipeLength>10),void 0===e.touchObject.curX)return!1;if(!0===e.touchObject.edgeHit&&e.$slider.trigger("edge",[e,e.swipeDirection()]),e.touchObject.swipeLength>=e.touchObject.minSwipe){switch(s=e.swipeDirection()){case"left":case"down":t=e.options.swipeToSlide?e.checkNavigable(e.currentSlide+e.getSlideCount()):e.currentSlide+e.getSlideCount(),e.currentDirection=0;break;case"right":case"up":t=e.options.swipeToSlide?e.checkNavigable(e.currentSlide-e.getSlideCount()):e.currentSlide-e.getSlideCount(),e.currentDirection=1}"vertical"!=s&&(e.slideHandler(t),e.touchObject={},e.$slider.trigger("swipe",[e,s]))}else e.touchObject.startX!==e.touchObject.curX&&(e.slideHandler(e.currentSlide),e.touchObject={})},s.prototype.swipeHandler=function(i){var t=this;if(!1!==t.options.swipe&&(!("ontouchend"in document)||!1!==t.options.swipe)){if(!1!==t.options.draggable||-1===i.type.indexOf("mouse"))switch(t.touchObject.fingerCount=i.originalEvent&&void 0!==i.originalEvent.touches?i.originalEvent.touches.length:1,t.touchObject.minSwipe=t.listWidth/t.options.touchThreshold,!0===t.options.verticalSwiping&&(t.touchObject.minSwipe=t.listHeight/t.options.touchThreshold),i.data.action){case"start":t.swipeStart(i);break;case"move":t.swipeMove(i);break;case"end":t.swipeEnd(i)}}},s.prototype.swipeMove=function(i){var t,s,e,o,n,l,r=this;return n=void 0!==i.originalEvent?i.originalEvent.touches:null,!!r.dragging&&!r.scrolling&&(!n||1===n.length)&&((t=r.getLeft(r.currentSlide),r.touchObject.curX=void 0!==n?n[0].pageX:i.clientX,r.touchObject.curY=void 0!==n?n[0].pageY:i.clientY,r.touchObject.swipeLength=Math.round(Math.sqrt(Math.pow(r.touchObject.curX-r.touchObject.startX,2))),l=Math.round(Math.sqrt(Math.pow(r.touchObject.curY-r.touchObject.startY,2))),r.options.verticalSwiping||r.swiping||!(l>4))?(!0===r.options.verticalSwiping&&(r.touchObject.swipeLength=l),s=r.swipeDirection(),void 0!==i.originalEvent&&r.touchObject.swipeLength>4&&(r.swiping=!0,i.preventDefault()),o=(!1===r.options.rtl?1:-1)*(r.touchObject.curX>r.touchObject.startX?1:-1),!0===r.options.verticalSwiping&&(o=r.touchObject.curY>r.touchObject.startY?1:-1),e=r.touchObject.swipeLength,r.touchObject.edgeHit=!1,!1===r.options.infinite&&(0===r.currentSlide&&"right"===s||r.currentSlide>=r.getDotCount()&&"left"===s)&&(e=r.touchObject.swipeLength*r.options.edgeFriction,r.touchObject.edgeHit=!0),!1===r.options.vertical?r.swipeLeft=t+e*o:r.swipeLeft=t+e*(r.$list.height()/r.listWidth)*o,!0===r.options.verticalSwiping&&(r.swipeLeft=t+e*o),!0!==r.options.fade&&!1!==r.options.touchMove&&(!0===r.animating?(r.swipeLeft=null,!1):void r.setCSS(r.swipeLeft))):(r.scrolling=!0,!1))},s.prototype.swipeStart=function(i){var t,s=this;if(s.interrupted=!0,1!==s.touchObject.fingerCount||s.slideCount<=s.options.slidesToShow)return s.touchObject={},!1;void 0!==i.originalEvent&&void 0!==i.originalEvent.touches&&(t=i.originalEvent.touches[0]),s.touchObject.startX=s.touchObject.curX=void 0!==t?t.pageX:i.clientX,s.touchObject.startY=s.touchObject.curY=void 0!==t?t.pageY:i.clientY,s.dragging=!0},s.prototype.unfilterSlides=s.prototype.slickUnfilter=function(){null!==this.$slidesCache&&(this.unload(),this.$slideTrack.children(this.options.slide).detach(),this.$slidesCache.appendTo(this.$slideTrack),this.reinit())},s.prototype.unload=function(){i(".slick-cloned",this.$slider).remove(),this.$dots&&this.$dots.remove(),this.$prevArrow&&this.htmlExpr.test(this.options.prevArrow)&&this.$prevArrow.remove(),this.$nextArrow&&this.htmlExpr.test(this.options.nextArrow)&&this.$nextArrow.remove(),this.$slides.removeClass("slick-slide slick-active slick-visible slick-current").attr("aria-hidden","true").css("width","")},s.prototype.unslick=function(i){this.$slider.trigger("unslick",[this,i]),this.destroy()},s.prototype.updateArrows=function(){var i;i=Math.floor(this.options.slidesToShow/2),!0===this.options.arrows&&this.slideCount>this.options.slidesToShow&&!this.options.infinite&&(this.$prevArrow.removeClass("slick-disabled").attr("aria-disabled","false"),this.$nextArrow.removeClass("slick-disabled").attr("aria-disabled","false"),0===this.currentSlide?(this.$prevArrow.addClass("slick-disabled").attr("aria-disabled","true"),this.$nextArrow.removeClass("slick-disabled").attr("aria-disabled","false")):this.currentSlide>=this.slideCount-this.options.slidesToShow&&!1===this.options.centerMode?(this.$nextArrow.addClass("slick-disabled").attr("aria-disabled","true"),this.$prevArrow.removeClass("slick-disabled").attr("aria-disabled","false")):this.currentSlide>=this.slideCount-1&&!0===this.options.centerMode&&(this.$nextArrow.addClass("slick-disabled").attr("aria-disabled","true"),this.$prevArrow.removeClass("slick-disabled").attr("aria-disabled","false")))},s.prototype.updateDots=function(){null!==this.$dots&&(this.$dots.find("li").removeClass("slick-active").end(),this.$dots.find("li").eq(Math.floor(this.currentSlide/this.options.slidesToScroll)).addClass("slick-active"))},s.prototype.visibility=function(){var i=this;i.options.autoplay&&(document[i.hidden]?i.interrupted=!0:i.interrupted=!1)},i.fn.slick=function(){var i,t,e=this,o=arguments[0],n=Array.prototype.slice.call(arguments,1),l=e.length;for(i=0;i<l;i++)if("object"==typeof o||void 0===o?e[i].slick=new s(e[i],o):t=e[i].slick[o].apply(e[i].slick,n),void 0!==t)return t;return e}});
/*! URI.js v1.19.7 http://medialize.github.io/URI.js/ */
/* build contains: URI.js, URITemplate.js, jquery.URI.js */
/*
 URI.js - Mutating URLs

 Version: 1.19.7

 Author: Rodney Rehm
 Web: http://medialize.github.io/URI.js/

 Licensed under
   MIT License http://www.opensource.org/licenses/mit-license

 URI.js - Mutating URLs
 URI Template Support - http://tools.ietf.org/html/rfc6570

 Version: 1.19.7

 Author: Rodney Rehm
 Web: http://medialize.github.io/URI.js/

 Licensed under
   MIT License http://www.opensource.org/licenses/mit-license

 URI.js - Mutating URLs
 jQuery Plugin

 Version: 1.19.7

 Author: Rodney Rehm
 Web: http://medialize.github.io/URI.js/jquery-uri-plugin.html

 Licensed under
   MIT License http://www.opensource.org/licenses/mit-license

*/
(function (n, x) { "object" === typeof module && module.exports ? module.exports = x(require("./punycode"), require("./IPv6"), require("./SecondLevelDomains")) : "function" === typeof define && define.amd ? define(["./punycode", "./IPv6", "./SecondLevelDomains"], x) : n.URI = x(n.punycode, n.IPv6, n.SecondLevelDomains, n) })(this, function (n, x, t, D) {
    function d(a, b) {
        var c = 1 <= arguments.length, e = 2 <= arguments.length; if (!(this instanceof d)) return c ? e ? new d(a, b) : new d(a) : new d; if (void 0 === a) {
            if (c) throw new TypeError("undefined is not a valid argument for URI");
            a = "undefined" !== typeof location ? location.href + "" : ""
        } if (null === a && c) throw new TypeError("null is not a valid argument for URI"); this.href(a); return void 0 !== b ? this.absoluteTo(b) : this
    } function F(a) { return a.replace(/([.*+?^=!:${}()|[\]\/\\])/g, "\\$1") } function G(a) { return void 0 === a ? "Undefined" : String(Object.prototype.toString.call(a)).slice(8, -1) } function y(a) { return "Array" === G(a) } function k(a, b) {
        var c = {}, e; if ("RegExp" === G(b)) c = null; else if (y(b)) { var f = 0; for (e = b.length; f < e; f++)c[b[f]] = !0 } else c[b] =
            !0; f = 0; for (e = a.length; f < e; f++)if (c && void 0 !== c[a[f]] || !c && b.test(a[f])) a.splice(f, 1), e--, f--; return a
    } function p(a, b) { var c; if (y(b)) { var e = 0; for (c = b.length; e < c; e++)if (!p(a, b[e])) return !1; return !0 } var f = G(b); e = 0; for (c = a.length; e < c; e++)if ("RegExp" === f) { if ("string" === typeof a[e] && a[e].match(b)) return !0 } else if (a[e] === b) return !0; return !1 } function v(a, b) { if (!y(a) || !y(b) || a.length !== b.length) return !1; a.sort(); b.sort(); for (var c = 0, e = a.length; c < e; c++)if (a[c] !== b[c]) return !1; return !0 } function h(a) {
        return a.replace(/^\/+|\/+$/g,
            "")
    } function l(a) { return escape(a) } function m(a) { return encodeURIComponent(a).replace(/[!'()*]/g, l).replace(/\*/g, "%2A") } function r(a) { return function (b, c) { if (void 0 === b) return this._parts[a] || ""; this._parts[a] = b || null; this.build(!c); return this } } function u(a, b) { return function (c, e) { if (void 0 === c) return this._parts[a] || ""; null !== c && (c += "", c.charAt(0) === b && (c = c.substring(1))); this._parts[a] = c; this.build(!e); return this } } var w = D && D.URI; d.version = "1.19.7"; var g = d.prototype, z = Object.prototype.hasOwnProperty;
    d._parts = function () { return { protocol: null, username: null, password: null, hostname: null, urn: null, port: null, path: null, query: null, fragment: null, preventInvalidHostname: d.preventInvalidHostname, duplicateQueryParameters: d.duplicateQueryParameters, escapeQuerySpace: d.escapeQuerySpace } }; d.preventInvalidHostname = !1; d.duplicateQueryParameters = !1; d.escapeQuerySpace = !0; d.protocol_expression = /^[a-z][a-z0-9.+-]*$/i; d.idn_expression = /[^a-z0-9\._-]/i; d.punycode_expression = /(xn--)/i; d.ip4_expression = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
    d.ip6_expression = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
    d.find_uri_expression = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u2018\u2019]))/ig; d.findUri = { start: /\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi, end: /[\s\r\n]|$/, trim: /[`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u201e\u2018\u2019]+$/, parens: /(\([^\)]*\)|\[[^\]]*\]|\{[^}]*\}|<[^>]*>)/g }; d.defaultPorts = {
        http: "80", https: "443", ftp: "21",
        gopher: "70", ws: "80", wss: "443"
    }; d.hostProtocols = ["http", "https"]; d.invalid_hostname_characters = /[^a-zA-Z0-9\.\-:_]/; d.domAttributes = { a: "href", blockquote: "cite", link: "href", base: "href", script: "src", form: "action", img: "src", area: "href", iframe: "src", embed: "src", source: "src", track: "src", input: "src", audio: "src", video: "src" }; d.getDomAttribute = function (a) { if (a && a.nodeName) { var b = a.nodeName.toLowerCase(); if ("input" !== b || "image" === a.type) return d.domAttributes[b] } }; d.encode = m; d.decode = decodeURIComponent; d.iso8859 =
        function () { d.encode = escape; d.decode = unescape }; d.unicode = function () { d.encode = m; d.decode = decodeURIComponent }; d.characters = {
            pathname: { encode: { expression: /%(24|26|2B|2C|3B|3D|3A|40)/ig, map: { "%24": "$", "%26": "&", "%2B": "+", "%2C": ",", "%3B": ";", "%3D": "=", "%3A": ":", "%40": "@" } }, decode: { expression: /[\/\?#]/g, map: { "/": "%2F", "?": "%3F", "#": "%23" } } }, reserved: {
                encode: {
                    expression: /%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig, map: {
                        "%3A": ":", "%2F": "/", "%3F": "?", "%23": "#", "%5B": "[", "%5D": "]", "%40": "@",
                        "%21": "!", "%24": "$", "%26": "&", "%27": "'", "%28": "(", "%29": ")", "%2A": "*", "%2B": "+", "%2C": ",", "%3B": ";", "%3D": "="
                    }
                }
            }, urnpath: { encode: { expression: /%(21|24|27|28|29|2A|2B|2C|3B|3D|40)/ig, map: { "%21": "!", "%24": "$", "%27": "'", "%28": "(", "%29": ")", "%2A": "*", "%2B": "+", "%2C": ",", "%3B": ";", "%3D": "=", "%40": "@" } }, decode: { expression: /[\/\?#:]/g, map: { "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" } } }
        }; d.encodeQuery = function (a, b) { var c = d.encode(a + ""); void 0 === b && (b = d.escapeQuerySpace); return b ? c.replace(/%20/g, "+") : c }; d.decodeQuery =
            function (a, b) { a += ""; void 0 === b && (b = d.escapeQuerySpace); try { return d.decode(b ? a.replace(/\+/g, "%20") : a) } catch (c) { return a } }; var B = { encode: "encode", decode: "decode" }, C, L = function (a, b) { return function (c) { try { return d[b](c + "").replace(d.characters[a][b].expression, function (e) { return d.characters[a][b].map[e] }) } catch (e) { return c } } }; for (C in B) d[C + "PathSegment"] = L("pathname", B[C]), d[C + "UrnPathSegment"] = L("urnpath", B[C]); B = function (a, b, c) {
                return function (e) {
                    var f = c ? function (E) { return d[b](d[c](E)) } : d[b];
                    e = (e + "").split(a); for (var q = 0, A = e.length; q < A; q++)e[q] = f(e[q]); return e.join(a)
                }
            }; d.decodePath = B("/", "decodePathSegment"); d.decodeUrnPath = B(":", "decodeUrnPathSegment"); d.recodePath = B("/", "encodePathSegment", "decode"); d.recodeUrnPath = B(":", "encodeUrnPathSegment", "decode"); d.encodeReserved = L("reserved", "encode"); d.parse = function (a, b) {
                b || (b = { preventInvalidHostname: d.preventInvalidHostname }); var c = a.indexOf("#"); -1 < c && (b.fragment = a.substring(c + 1) || null, a = a.substring(0, c)); c = a.indexOf("?"); -1 < c && (b.query =
                    a.substring(c + 1) || null, a = a.substring(0, c)); a = a.replace(/^(https?|ftp|wss?)?:[/\\]*/, "$1://"); "//" === a.substring(0, 2) ? (b.protocol = null, a = a.substring(2), a = d.parseAuthority(a, b)) : (c = a.indexOf(":"), -1 < c && (b.protocol = a.substring(0, c) || null, b.protocol && !b.protocol.match(d.protocol_expression) ? b.protocol = void 0 : "//" === a.substring(c + 1, c + 3).replace(/\\/g, "/") ? (a = a.substring(c + 3), a = d.parseAuthority(a, b)) : (a = a.substring(c + 1), b.urn = !0))); b.path = a; return b
            }; d.parseHost = function (a, b) {
                a || (a = ""); a = a.replace(/\\/g,
                    "/"); var c = a.indexOf("/"); -1 === c && (c = a.length); if ("[" === a.charAt(0)) { var e = a.indexOf("]"); b.hostname = a.substring(1, e) || null; b.port = a.substring(e + 2, c) || null; "/" === b.port && (b.port = null) } else { var f = a.indexOf(":"); e = a.indexOf("/"); f = a.indexOf(":", f + 1); -1 !== f && (-1 === e || f < e) ? (b.hostname = a.substring(0, c) || null, b.port = null) : (e = a.substring(0, c).split(":"), b.hostname = e[0] || null, b.port = e[1] || null) } b.hostname && "/" !== a.substring(c).charAt(0) && (c++, a = "/" + a); b.preventInvalidHostname && d.ensureValidHostname(b.hostname,
                        b.protocol); b.port && d.ensureValidPort(b.port); return a.substring(c) || "/"
            }; d.parseAuthority = function (a, b) { a = d.parseUserinfo(a, b); return d.parseHost(a, b) }; d.parseUserinfo = function (a, b) { var c = a; -1 !== a.indexOf("\\") && (a = a.replace(/\\/g, "/")); var e = a.indexOf("/"), f = a.lastIndexOf("@", -1 < e ? e : a.length - 1); -1 < f && (-1 === e || f < e) ? (e = a.substring(0, f).split(":"), b.username = e[0] ? d.decode(e[0]) : null, e.shift(), b.password = e[0] ? d.decode(e.join(":")) : null, a = c.substring(f + 1)) : (b.username = null, b.password = null); return a };
    d.parseQuery = function (a, b) { if (!a) return {}; a = a.replace(/&+/g, "&").replace(/^\?*&*|&+$/g, ""); if (!a) return {}; for (var c = {}, e = a.split("&"), f = e.length, q, A, E = 0; E < f; E++)if (q = e[E].split("="), A = d.decodeQuery(q.shift(), b), q = q.length ? d.decodeQuery(q.join("="), b) : null, "__proto__" !== A) if (z.call(c, A)) { if ("string" === typeof c[A] || null === c[A]) c[A] = [c[A]]; c[A].push(q) } else c[A] = q; return c }; d.build = function (a) {
        var b = "", c = !1; a.protocol && (b += a.protocol + ":"); a.urn || !b && !a.hostname || (b += "//", c = !0); b += d.buildAuthority(a) ||
            ""; "string" === typeof a.path && ("/" !== a.path.charAt(0) && c && (b += "/"), b += a.path); "string" === typeof a.query && a.query && (b += "?" + a.query); "string" === typeof a.fragment && a.fragment && (b += "#" + a.fragment); return b
    }; d.buildHost = function (a) { var b = ""; if (a.hostname) b = d.ip6_expression.test(a.hostname) ? b + ("[" + a.hostname + "]") : b + a.hostname; else return ""; a.port && (b += ":" + a.port); return b }; d.buildAuthority = function (a) { return d.buildUserinfo(a) + d.buildHost(a) }; d.buildUserinfo = function (a) {
        var b = ""; a.username && (b += d.encode(a.username));
        a.password && (b += ":" + d.encode(a.password)); b && (b += "@"); return b
    }; d.buildQuery = function (a, b, c) { var e = "", f, q; for (f in a) if ("__proto__" !== f && z.call(a, f)) if (y(a[f])) { var A = {}; var E = 0; for (q = a[f].length; E < q; E++)void 0 !== a[f][E] && void 0 === A[a[f][E] + ""] && (e += "&" + d.buildQueryParameter(f, a[f][E], c), !0 !== b && (A[a[f][E] + ""] = !0)) } else void 0 !== a[f] && (e += "&" + d.buildQueryParameter(f, a[f], c)); return e.substring(1) }; d.buildQueryParameter = function (a, b, c) { return d.encodeQuery(a, c) + (null !== b ? "=" + d.encodeQuery(b, c) : "") };
    d.addQuery = function (a, b, c) { if ("object" === typeof b) for (var e in b) z.call(b, e) && d.addQuery(a, e, b[e]); else if ("string" === typeof b) void 0 === a[b] ? a[b] = c : ("string" === typeof a[b] && (a[b] = [a[b]]), y(c) || (c = [c]), a[b] = (a[b] || []).concat(c)); else throw new TypeError("URI.addQuery() accepts an object, string as the name parameter"); }; d.setQuery = function (a, b, c) {
        if ("object" === typeof b) for (var e in b) z.call(b, e) && d.setQuery(a, e, b[e]); else if ("string" === typeof b) a[b] = void 0 === c ? null : c; else throw new TypeError("URI.setQuery() accepts an object, string as the name parameter");
    }; d.removeQuery = function (a, b, c) {
        var e; if (y(b)) for (c = 0, e = b.length; c < e; c++)a[b[c]] = void 0; else if ("RegExp" === G(b)) for (e in a) b.test(e) && (a[e] = void 0); else if ("object" === typeof b) for (e in b) z.call(b, e) && d.removeQuery(a, e, b[e]); else if ("string" === typeof b) void 0 !== c ? "RegExp" === G(c) ? !y(a[b]) && c.test(a[b]) ? a[b] = void 0 : a[b] = k(a[b], c) : a[b] !== String(c) || y(c) && 1 !== c.length ? y(a[b]) && (a[b] = k(a[b], c)) : a[b] = void 0 : a[b] = void 0; else throw new TypeError("URI.removeQuery() accepts an object, string, RegExp as the first parameter");
    }; d.hasQuery = function (a, b, c, e) {
        switch (G(b)) { case "String": break; case "RegExp": for (var f in a) if (z.call(a, f) && b.test(f) && (void 0 === c || d.hasQuery(a, f, c))) return !0; return !1; case "Object": for (var q in b) if (z.call(b, q) && !d.hasQuery(a, q, b[q])) return !1; return !0; default: throw new TypeError("URI.hasQuery() accepts a string, regular expression or object as the name parameter"); }switch (G(c)) {
            case "Undefined": return b in a; case "Boolean": return a = !(y(a[b]) ? !a[b].length : !a[b]), c === a; case "Function": return !!c(a[b],
                b, a); case "Array": return y(a[b]) ? (e ? p : v)(a[b], c) : !1; case "RegExp": return y(a[b]) ? e ? p(a[b], c) : !1 : !(!a[b] || !a[b].match(c)); case "Number": c = String(c); case "String": return y(a[b]) ? e ? p(a[b], c) : !1 : a[b] === c; default: throw new TypeError("URI.hasQuery() accepts undefined, boolean, string, number, RegExp, Function as the value parameter");
        }
    }; d.joinPaths = function () {
        for (var a = [], b = [], c = 0, e = 0; e < arguments.length; e++) {
            var f = new d(arguments[e]); a.push(f); f = f.segment(); for (var q = 0; q < f.length; q++)"string" === typeof f[q] &&
                b.push(f[q]), f[q] && c++
        } if (!b.length || !c) return new d(""); b = (new d("")).segment(b); "" !== a[0].path() && "/" !== a[0].path().slice(0, 1) || b.path("/" + b.path()); return b.normalize()
    }; d.commonPath = function (a, b) { var c = Math.min(a.length, b.length), e; for (e = 0; e < c; e++)if (a.charAt(e) !== b.charAt(e)) { e--; break } if (1 > e) return a.charAt(0) === b.charAt(0) && "/" === a.charAt(0) ? "/" : ""; if ("/" !== a.charAt(e) || "/" !== b.charAt(e)) e = a.substring(0, e).lastIndexOf("/"); return a.substring(0, e + 1) }; d.withinString = function (a, b, c) {
        c || (c = {});
        var e = c.start || d.findUri.start, f = c.end || d.findUri.end, q = c.trim || d.findUri.trim, A = c.parens || d.findUri.parens, E = /[a-z0-9-]=["']?$/i; for (e.lastIndex = 0; ;) {
            var H = e.exec(a); if (!H) break; var K = H.index; if (c.ignoreHtml) { var I = a.slice(Math.max(K - 3, 0), K); if (I && E.test(I)) continue } var J = K + a.slice(K).search(f); I = a.slice(K, J); for (J = -1; ;) { var M = A.exec(I); if (!M) break; J = Math.max(J, M.index + M[0].length) } I = -1 < J ? I.slice(0, J) + I.slice(J).replace(q, "") : I.replace(q, ""); I.length <= H[0].length || c.ignore && c.ignore.test(I) || (J =
                K + I.length, H = b(I, K, J, a), void 0 === H ? e.lastIndex = J : (H = String(H), a = a.slice(0, K) + H + a.slice(J), e.lastIndex = K + H.length))
        } e.lastIndex = 0; return a
    }; d.ensureValidHostname = function (a, b) {
        var c = !!a, e = !1; b && (e = p(d.hostProtocols, b)); if (e && !c) throw new TypeError("Hostname cannot be empty, if protocol is " + b); if (a && a.match(d.invalid_hostname_characters)) {
            if (!n) throw new TypeError('Hostname "' + a + '" contains characters other than [A-Z0-9.-:_] and Punycode.js is not available'); if (n.toASCII(a).match(d.invalid_hostname_characters)) throw new TypeError('Hostname "' +
                a + '" contains characters other than [A-Z0-9.-:_]');
        }
    }; d.ensureValidPort = function (a) { if (a) { var b = Number(a); if (!(/^[0-9]+$/.test(b) && 0 < b && 65536 > b)) throw new TypeError('Port "' + a + '" is not a valid port'); } }; d.noConflict = function (a) {
        if (a) return a = { URI: this.noConflict() }, D.URITemplate && "function" === typeof D.URITemplate.noConflict && (a.URITemplate = D.URITemplate.noConflict()), D.IPv6 && "function" === typeof D.IPv6.noConflict && (a.IPv6 = D.IPv6.noConflict()), D.SecondLevelDomains && "function" === typeof D.SecondLevelDomains.noConflict &&
            (a.SecondLevelDomains = D.SecondLevelDomains.noConflict()), a; D.URI === this && (D.URI = w); return this
    }; g.build = function (a) { if (!0 === a) this._deferred_build = !0; else if (void 0 === a || this._deferred_build) this._string = d.build(this._parts), this._deferred_build = !1; return this }; g.clone = function () { return new d(this) }; g.valueOf = g.toString = function () { return this.build(!1)._string }; g.protocol = r("protocol"); g.username = r("username"); g.password = r("password"); g.hostname = r("hostname"); g.port = r("port"); g.query = u("query", "?");
    g.fragment = u("fragment", "#"); g.search = function (a, b) { var c = this.query(a, b); return "string" === typeof c && c.length ? "?" + c : c }; g.hash = function (a, b) { var c = this.fragment(a, b); return "string" === typeof c && c.length ? "#" + c : c }; g.pathname = function (a, b) { if (void 0 === a || !0 === a) { var c = this._parts.path || (this._parts.hostname ? "/" : ""); return a ? (this._parts.urn ? d.decodeUrnPath : d.decodePath)(c) : c } this._parts.path = this._parts.urn ? a ? d.recodeUrnPath(a) : "" : a ? d.recodePath(a) : "/"; this.build(!b); return this }; g.path = g.pathname; g.href =
        function (a, b) {
            var c; if (void 0 === a) return this.toString(); this._string = ""; this._parts = d._parts(); var e = a instanceof d, f = "object" === typeof a && (a.hostname || a.path || a.pathname); a.nodeName && (f = d.getDomAttribute(a), a = a[f] || "", f = !1); !e && f && void 0 !== a.pathname && (a = a.toString()); if ("string" === typeof a || a instanceof String) this._parts = d.parse(String(a), this._parts); else if (e || f) { e = e ? a._parts : a; for (c in e) "query" !== c && z.call(this._parts, c) && (this._parts[c] = e[c]); e.query && this.query(e.query, !1) } else throw new TypeError("invalid input");
            this.build(!b); return this
        }; g.is = function (a) {
            var b = !1, c = !1, e = !1, f = !1, q = !1, A = !1, E = !1, H = !this._parts.urn; this._parts.hostname && (H = !1, c = d.ip4_expression.test(this._parts.hostname), e = d.ip6_expression.test(this._parts.hostname), b = c || e, q = (f = !b) && t && t.has(this._parts.hostname), A = f && d.idn_expression.test(this._parts.hostname), E = f && d.punycode_expression.test(this._parts.hostname)); switch (a.toLowerCase()) {
                case "relative": return H; case "absolute": return !H; case "domain": case "name": return f; case "sld": return q;
                case "ip": return b; case "ip4": case "ipv4": case "inet4": return c; case "ip6": case "ipv6": case "inet6": return e; case "idn": return A; case "url": return !this._parts.urn; case "urn": return !!this._parts.urn; case "punycode": return E
            }return null
        }; var N = g.protocol, O = g.port, P = g.hostname; g.protocol = function (a, b) {
            if (a && (a = a.replace(/:(\/\/)?$/, ""), !a.match(d.protocol_expression))) throw new TypeError('Protocol "' + a + "\" contains characters other than [A-Z0-9.+-] or doesn't start with [A-Z]"); return N.call(this, a,
                b)
        }; g.scheme = g.protocol; g.port = function (a, b) { if (this._parts.urn) return void 0 === a ? "" : this; void 0 !== a && (0 === a && (a = null), a && (a += "", ":" === a.charAt(0) && (a = a.substring(1)), d.ensureValidPort(a))); return O.call(this, a, b) }; g.hostname = function (a, b) {
            if (this._parts.urn) return void 0 === a ? "" : this; if (void 0 !== a) {
                var c = { preventInvalidHostname: this._parts.preventInvalidHostname }; if ("/" !== d.parseHost(a, c)) throw new TypeError('Hostname "' + a + '" contains characters other than [A-Z0-9.-]'); a = c.hostname; this._parts.preventInvalidHostname &&
                    d.ensureValidHostname(a, this._parts.protocol)
            } return P.call(this, a, b)
        }; g.origin = function (a, b) { if (this._parts.urn) return void 0 === a ? "" : this; if (void 0 === a) { var c = this.protocol(); return this.authority() ? (c ? c + "://" : "") + this.authority() : "" } c = d(a); this.protocol(c.protocol()).authority(c.authority()).build(!b); return this }; g.host = function (a, b) {
            if (this._parts.urn) return void 0 === a ? "" : this; if (void 0 === a) return this._parts.hostname ? d.buildHost(this._parts) : ""; if ("/" !== d.parseHost(a, this._parts)) throw new TypeError('Hostname "' +
                a + '" contains characters other than [A-Z0-9.-]'); this.build(!b); return this
        }; g.authority = function (a, b) { if (this._parts.urn) return void 0 === a ? "" : this; if (void 0 === a) return this._parts.hostname ? d.buildAuthority(this._parts) : ""; if ("/" !== d.parseAuthority(a, this._parts)) throw new TypeError('Hostname "' + a + '" contains characters other than [A-Z0-9.-]'); this.build(!b); return this }; g.userinfo = function (a, b) {
            if (this._parts.urn) return void 0 === a ? "" : this; if (void 0 === a) {
                var c = d.buildUserinfo(this._parts); return c ?
                    c.substring(0, c.length - 1) : c
            } "@" !== a[a.length - 1] && (a += "@"); d.parseUserinfo(a, this._parts); this.build(!b); return this
        }; g.resource = function (a, b) { if (void 0 === a) return this.path() + this.search() + this.hash(); var c = d.parse(a); this._parts.path = c.path; this._parts.query = c.query; this._parts.fragment = c.fragment; this.build(!b); return this }; g.subdomain = function (a, b) {
            if (this._parts.urn) return void 0 === a ? "" : this; if (void 0 === a) {
                if (!this._parts.hostname || this.is("IP")) return ""; var c = this._parts.hostname.length - this.domain().length -
                    1; return this._parts.hostname.substring(0, c) || ""
            } c = this._parts.hostname.length - this.domain().length; c = this._parts.hostname.substring(0, c); c = new RegExp("^" + F(c)); a && "." !== a.charAt(a.length - 1) && (a += "."); if (-1 !== a.indexOf(":")) throw new TypeError("Domains cannot contain colons"); a && d.ensureValidHostname(a, this._parts.protocol); this._parts.hostname = this._parts.hostname.replace(c, a); this.build(!b); return this
        }; g.domain = function (a, b) {
            if (this._parts.urn) return void 0 === a ? "" : this; "boolean" === typeof a && (b =
                a, a = void 0); if (void 0 === a) { if (!this._parts.hostname || this.is("IP")) return ""; var c = this._parts.hostname.match(/\./g); if (c && 2 > c.length) return this._parts.hostname; c = this._parts.hostname.length - this.tld(b).length - 1; c = this._parts.hostname.lastIndexOf(".", c - 1) + 1; return this._parts.hostname.substring(c) || "" } if (!a) throw new TypeError("cannot set domain empty"); if (-1 !== a.indexOf(":")) throw new TypeError("Domains cannot contain colons"); d.ensureValidHostname(a, this._parts.protocol); !this._parts.hostname ||
                    this.is("IP") ? this._parts.hostname = a : (c = new RegExp(F(this.domain()) + "$"), this._parts.hostname = this._parts.hostname.replace(c, a)); this.build(!b); return this
        }; g.tld = function (a, b) {
            if (this._parts.urn) return void 0 === a ? "" : this; "boolean" === typeof a && (b = a, a = void 0); if (void 0 === a) { if (!this._parts.hostname || this.is("IP")) return ""; var c = this._parts.hostname.lastIndexOf("."); c = this._parts.hostname.substring(c + 1); return !0 !== b && t && t.list[c.toLowerCase()] ? t.get(this._parts.hostname) || c : c } if (a) if (a.match(/[^a-zA-Z0-9-]/)) if (t &&
                t.is(a)) c = new RegExp(F(this.tld()) + "$"), this._parts.hostname = this._parts.hostname.replace(c, a); else throw new TypeError('TLD "' + a + '" contains characters other than [A-Z0-9]'); else { if (!this._parts.hostname || this.is("IP")) throw new ReferenceError("cannot set TLD on non-domain host"); c = new RegExp(F(this.tld()) + "$"); this._parts.hostname = this._parts.hostname.replace(c, a) } else throw new TypeError("cannot set TLD empty"); this.build(!b); return this
        }; g.directory = function (a, b) {
            if (this._parts.urn) return void 0 ===
                a ? "" : this; if (void 0 === a || !0 === a) { if (!this._parts.path && !this._parts.hostname) return ""; if ("/" === this._parts.path) return "/"; var c = this._parts.path.length - this.filename().length - 1; c = this._parts.path.substring(0, c) || (this._parts.hostname ? "/" : ""); return a ? d.decodePath(c) : c } c = this._parts.path.length - this.filename().length; c = this._parts.path.substring(0, c); c = new RegExp("^" + F(c)); this.is("relative") || (a || (a = "/"), "/" !== a.charAt(0) && (a = "/" + a)); a && "/" !== a.charAt(a.length - 1) && (a += "/"); a = d.recodePath(a); this._parts.path =
                    this._parts.path.replace(c, a); this.build(!b); return this
        }; g.filename = function (a, b) {
            if (this._parts.urn) return void 0 === a ? "" : this; if ("string" !== typeof a) { if (!this._parts.path || "/" === this._parts.path) return ""; var c = this._parts.path.lastIndexOf("/"); c = this._parts.path.substring(c + 1); return a ? d.decodePathSegment(c) : c } c = !1; "/" === a.charAt(0) && (a = a.substring(1)); a.match(/\.?\//) && (c = !0); var e = new RegExp(F(this.filename()) + "$"); a = d.recodePath(a); this._parts.path = this._parts.path.replace(e, a); c ? this.normalizePath(b) :
                this.build(!b); return this
        }; g.suffix = function (a, b) {
            if (this._parts.urn) return void 0 === a ? "" : this; if (void 0 === a || !0 === a) { if (!this._parts.path || "/" === this._parts.path) return ""; var c = this.filename(), e = c.lastIndexOf("."); if (-1 === e) return ""; c = c.substring(e + 1); c = /^[a-z0-9%]+$/i.test(c) ? c : ""; return a ? d.decodePathSegment(c) : c } "." === a.charAt(0) && (a = a.substring(1)); if (c = this.suffix()) e = a ? new RegExp(F(c) + "$") : new RegExp(F("." + c) + "$"); else { if (!a) return this; this._parts.path += "." + d.recodePath(a) } e && (a = d.recodePath(a),
                this._parts.path = this._parts.path.replace(e, a)); this.build(!b); return this
        }; g.segment = function (a, b, c) {
            var e = this._parts.urn ? ":" : "/", f = this.path(), q = "/" === f.substring(0, 1); f = f.split(e); void 0 !== a && "number" !== typeof a && (c = b, b = a, a = void 0); if (void 0 !== a && "number" !== typeof a) throw Error('Bad segment "' + a + '", must be 0-based integer'); q && f.shift(); 0 > a && (a = Math.max(f.length + a, 0)); if (void 0 === b) return void 0 === a ? f : f[a]; if (null === a || void 0 === f[a]) if (y(b)) {
                f = []; a = 0; for (var A = b.length; a < A; a++)if (b[a].length ||
                    f.length && f[f.length - 1].length) f.length && !f[f.length - 1].length && f.pop(), f.push(h(b[a]))
            } else { if (b || "string" === typeof b) b = h(b), "" === f[f.length - 1] ? f[f.length - 1] = b : f.push(b) } else b ? f[a] = h(b) : f.splice(a, 1); q && f.unshift(""); return this.path(f.join(e), c)
        }; g.segmentCoded = function (a, b, c) {
            var e; "number" !== typeof a && (c = b, b = a, a = void 0); if (void 0 === b) { a = this.segment(a, b, c); if (y(a)) { var f = 0; for (e = a.length; f < e; f++)a[f] = d.decode(a[f]) } else a = void 0 !== a ? d.decode(a) : void 0; return a } if (y(b)) for (f = 0, e = b.length; f < e; f++)b[f] =
                d.encode(b[f]); else b = "string" === typeof b || b instanceof String ? d.encode(b) : b; return this.segment(a, b, c)
        }; var Q = g.query; g.query = function (a, b) {
            if (!0 === a) return d.parseQuery(this._parts.query, this._parts.escapeQuerySpace); if ("function" === typeof a) { var c = d.parseQuery(this._parts.query, this._parts.escapeQuerySpace), e = a.call(this, c); this._parts.query = d.buildQuery(e || c, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace); this.build(!b); return this } return void 0 !== a && "string" !== typeof a ? (this._parts.query =
                d.buildQuery(a, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace), this.build(!b), this) : Q.call(this, a, b)
        }; g.setQuery = function (a, b, c) {
            var e = d.parseQuery(this._parts.query, this._parts.escapeQuerySpace); if ("string" === typeof a || a instanceof String) e[a] = void 0 !== b ? b : null; else if ("object" === typeof a) for (var f in a) z.call(a, f) && (e[f] = a[f]); else throw new TypeError("URI.addQuery() accepts an object, string as the name parameter"); this._parts.query = d.buildQuery(e, this._parts.duplicateQueryParameters,
                this._parts.escapeQuerySpace); "string" !== typeof a && (c = b); this.build(!c); return this
        }; g.addQuery = function (a, b, c) { var e = d.parseQuery(this._parts.query, this._parts.escapeQuerySpace); d.addQuery(e, a, void 0 === b ? null : b); this._parts.query = d.buildQuery(e, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace); "string" !== typeof a && (c = b); this.build(!c); return this }; g.removeQuery = function (a, b, c) {
            var e = d.parseQuery(this._parts.query, this._parts.escapeQuerySpace); d.removeQuery(e, a, b); this._parts.query =
                d.buildQuery(e, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace); "string" !== typeof a && (c = b); this.build(!c); return this
        }; g.hasQuery = function (a, b, c) { var e = d.parseQuery(this._parts.query, this._parts.escapeQuerySpace); return d.hasQuery(e, a, b, c) }; g.setSearch = g.setQuery; g.addSearch = g.addQuery; g.removeSearch = g.removeQuery; g.hasSearch = g.hasQuery; g.normalize = function () { return this._parts.urn ? this.normalizeProtocol(!1).normalizePath(!1).normalizeQuery(!1).normalizeFragment(!1).build() : this.normalizeProtocol(!1).normalizeHostname(!1).normalizePort(!1).normalizePath(!1).normalizeQuery(!1).normalizeFragment(!1).build() };
    g.normalizeProtocol = function (a) { "string" === typeof this._parts.protocol && (this._parts.protocol = this._parts.protocol.toLowerCase(), this.build(!a)); return this }; g.normalizeHostname = function (a) { this._parts.hostname && (this.is("IDN") && n ? this._parts.hostname = n.toASCII(this._parts.hostname) : this.is("IPv6") && x && (this._parts.hostname = x.best(this._parts.hostname)), this._parts.hostname = this._parts.hostname.toLowerCase(), this.build(!a)); return this }; g.normalizePort = function (a) {
        "string" === typeof this._parts.protocol &&
        this._parts.port === d.defaultPorts[this._parts.protocol] && (this._parts.port = null, this.build(!a)); return this
    }; g.normalizePath = function (a) {
        var b = this._parts.path; if (!b) return this; if (this._parts.urn) return this._parts.path = d.recodeUrnPath(this._parts.path), this.build(!a), this; if ("/" === this._parts.path) return this; b = d.recodePath(b); var c = ""; if ("/" !== b.charAt(0)) { var e = !0; b = "/" + b } if ("/.." === b.slice(-3) || "/." === b.slice(-2)) b += "/"; b = b.replace(/(\/(\.\/)+)|(\/\.$)/g, "/").replace(/\/{2,}/g, "/"); e && (c = b.substring(1).match(/^(\.\.\/)+/) ||
            "") && (c = c[0]); for (; ;) { var f = b.search(/\/\.\.(\/|$)/); if (-1 === f) break; else if (0 === f) { b = b.substring(3); continue } var q = b.substring(0, f).lastIndexOf("/"); -1 === q && (q = f); b = b.substring(0, q) + b.substring(f + 3) } e && this.is("relative") && (b = c + b.substring(1)); this._parts.path = b; this.build(!a); return this
    }; g.normalizePathname = g.normalizePath; g.normalizeQuery = function (a) {
        "string" === typeof this._parts.query && (this._parts.query.length ? this.query(d.parseQuery(this._parts.query, this._parts.escapeQuerySpace)) : this._parts.query =
            null, this.build(!a)); return this
    }; g.normalizeFragment = function (a) { this._parts.fragment || (this._parts.fragment = null, this.build(!a)); return this }; g.normalizeSearch = g.normalizeQuery; g.normalizeHash = g.normalizeFragment; g.iso8859 = function () { var a = d.encode, b = d.decode; d.encode = escape; d.decode = decodeURIComponent; try { this.normalize() } finally { d.encode = a, d.decode = b } return this }; g.unicode = function () { var a = d.encode, b = d.decode; d.encode = m; d.decode = unescape; try { this.normalize() } finally { d.encode = a, d.decode = b } return this };
    g.readable = function () {
        var a = this.clone(); a.username("").password("").normalize(); var b = ""; a._parts.protocol && (b += a._parts.protocol + "://"); a._parts.hostname && (a.is("punycode") && n ? (b += n.toUnicode(a._parts.hostname), a._parts.port && (b += ":" + a._parts.port)) : b += a.host()); a._parts.hostname && a._parts.path && "/" !== a._parts.path.charAt(0) && (b += "/"); b += a.path(!0); if (a._parts.query) {
            for (var c = "", e = 0, f = a._parts.query.split("&"), q = f.length; e < q; e++) {
                var A = (f[e] || "").split("="); c += "&" + d.decodeQuery(A[0], this._parts.escapeQuerySpace).replace(/&/g,
                    "%26"); void 0 !== A[1] && (c += "=" + d.decodeQuery(A[1], this._parts.escapeQuerySpace).replace(/&/g, "%26"))
            } b += "?" + c.substring(1)
        } return b += d.decodeQuery(a.hash(), !0)
    }; g.absoluteTo = function (a) {
        var b = this.clone(), c = ["protocol", "username", "password", "hostname", "port"], e, f; if (this._parts.urn) throw Error("URNs do not have any generally defined hierarchical components"); a instanceof d || (a = new d(a)); if (b._parts.protocol) return b; b._parts.protocol = a._parts.protocol; if (this._parts.hostname) return b; for (e = 0; f = c[e]; e++)b._parts[f] =
            a._parts[f]; b._parts.path ? (".." === b._parts.path.substring(-2) && (b._parts.path += "/"), "/" !== b.path().charAt(0) && (c = (c = a.directory()) ? c : 0 === a.path().indexOf("/") ? "/" : "", b._parts.path = (c ? c + "/" : "") + b._parts.path, b.normalizePath())) : (b._parts.path = a._parts.path, b._parts.query || (b._parts.query = a._parts.query)); b.build(); return b
    }; g.relativeTo = function (a) {
        var b = this.clone().normalize(); if (b._parts.urn) throw Error("URNs do not have any generally defined hierarchical components"); a = (new d(a)).normalize(); var c =
            b._parts; var e = a._parts; var f = b.path(); a = a.path(); if ("/" !== f.charAt(0)) throw Error("URI is already relative"); if ("/" !== a.charAt(0)) throw Error("Cannot calculate a URI relative to another relative URI"); c.protocol === e.protocol && (c.protocol = null); if (c.username === e.username && c.password === e.password && null === c.protocol && null === c.username && null === c.password && c.hostname === e.hostname && c.port === e.port) c.hostname = null, c.port = null; else return b.build(); if (f === a) return c.path = "", b.build(); f = d.commonPath(f, a);
        if (!f) return b.build(); e = e.path.substring(f.length).replace(/[^\/]*$/, "").replace(/.*?\//g, "../"); c.path = e + c.path.substring(f.length) || "./"; return b.build()
    }; g.equals = function (a) {
        var b = this.clone(), c = new d(a); a = {}; var e; b.normalize(); c.normalize(); if (b.toString() === c.toString()) return !0; var f = b.query(); var q = c.query(); b.query(""); c.query(""); if (b.toString() !== c.toString() || f.length !== q.length) return !1; b = d.parseQuery(f, this._parts.escapeQuerySpace); q = d.parseQuery(q, this._parts.escapeQuerySpace); for (e in b) if (z.call(b,
            e)) { if (!y(b[e])) { if (b[e] !== q[e]) return !1 } else if (!v(b[e], q[e])) return !1; a[e] = !0 } for (e in q) if (z.call(q, e) && !a[e]) return !1; return !0
    }; g.preventInvalidHostname = function (a) { this._parts.preventInvalidHostname = !!a; return this }; g.duplicateQueryParameters = function (a) { this._parts.duplicateQueryParameters = !!a; return this }; g.escapeQuerySpace = function (a) { this._parts.escapeQuerySpace = !!a; return this }; return d
});
(function (n, x) { "object" === typeof module && module.exports ? module.exports = x(require("./URI")) : "function" === typeof define && define.amd ? define(["./URI"], x) : n.URITemplate = x(n.URI, n) })(this, function (n, x) {
    function t(k) { if (t._cache[k]) return t._cache[k]; if (!(this instanceof t)) return new t(k); this.expression = k; t._cache[k] = this; return this } function D(k) { this.data = k; this.cache = {} } var d = x && x.URITemplate, F = Object.prototype.hasOwnProperty, G = t.prototype, y = {
        "": {
            prefix: "", separator: ",", named: !1, empty_name_separator: !1,
            encode: "encode"
        }, "+": { prefix: "", separator: ",", named: !1, empty_name_separator: !1, encode: "encodeReserved" }, "#": { prefix: "#", separator: ",", named: !1, empty_name_separator: !1, encode: "encodeReserved" }, ".": { prefix: ".", separator: ".", named: !1, empty_name_separator: !1, encode: "encode" }, "/": { prefix: "/", separator: "/", named: !1, empty_name_separator: !1, encode: "encode" }, ";": { prefix: ";", separator: ";", named: !0, empty_name_separator: !1, encode: "encode" }, "?": { prefix: "?", separator: "&", named: !0, empty_name_separator: !0, encode: "encode" },
        "&": { prefix: "&", separator: "&", named: !0, empty_name_separator: !0, encode: "encode" }
    }; t._cache = {}; t.EXPRESSION_PATTERN = /\{([^a-zA-Z0-9%_]?)([^\}]+)(\}|$)/g; t.VARIABLE_PATTERN = /^([^*:.](?:\.?[^*:.])*)((\*)|:(\d+))?$/; t.VARIABLE_NAME_PATTERN = /[^a-zA-Z0-9%_.]/; t.LITERAL_PATTERN = /[<>{}"`^| \\]/; t.expand = function (k, p, v) {
        var h = y[k.operator], l = h.named ? "Named" : "Unnamed"; k = k.variables; var m = [], r, u; for (u = 0; r = k[u]; u++) {
            var w = p.get(r.name); if (0 === w.type && v && v.strict) throw Error('Missing expansion value for variable "' +
                r.name + '"'); if (w.val.length) { if (1 < w.type && r.maxlength) throw Error('Invalid expression: Prefix modifier not applicable to variable "' + r.name + '"'); m.push(t["expand" + l](w, h, r.explode, r.explode && h.separator || ",", r.maxlength, r.name)) } else w.type && m.push("")
        } return m.length ? h.prefix + m.join(h.separator) : ""
    }; t.expandNamed = function (k, p, v, h, l, m) {
        var r = "", u = p.encode; p = p.empty_name_separator; var w = !k[u].length, g = 2 === k.type ? "" : n[u](m), z; var B = 0; for (z = k.val.length; B < z; B++) {
            if (l) {
                var C = n[u](k.val[B][1].substring(0,
                    l)); 2 === k.type && (g = n[u](k.val[B][0].substring(0, l)))
            } else w ? (C = n[u](k.val[B][1]), 2 === k.type ? (g = n[u](k.val[B][0]), k[u].push([g, C])) : k[u].push([void 0, C])) : (C = k[u][B][1], 2 === k.type && (g = k[u][B][0])); r && (r += h); v ? r += g + (p || C ? "=" : "") + C : (B || (r += n[u](m) + (p || C ? "=" : "")), 2 === k.type && (r += g + ","), r += C)
        } return r
    }; t.expandUnnamed = function (k, p, v, h, l) {
        var m = "", r = p.encode; p = p.empty_name_separator; var u = !k[r].length, w; var g = 0; for (w = k.val.length; g < w; g++) {
            if (l) var z = n[r](k.val[g][1].substring(0, l)); else u ? (z = n[r](k.val[g][1]),
                k[r].push([2 === k.type ? n[r](k.val[g][0]) : void 0, z])) : z = k[r][g][1]; m && (m += h); if (2 === k.type) { var B = l ? n[r](k.val[g][0].substring(0, l)) : k[r][g][0]; m += B; m = v ? m + (p || z ? "=" : "") : m + "," } m += z
        } return m
    }; t.noConflict = function () { x.URITemplate === t && (x.URITemplate = d); return t }; G.expand = function (k, p) { var v = ""; this.parts && this.parts.length || this.parse(); k instanceof D || (k = new D(k)); for (var h = 0, l = this.parts.length; h < l; h++)v += "string" === typeof this.parts[h] ? this.parts[h] : t.expand(this.parts[h], k, p); return v }; G.parse = function () {
        var k =
            this.expression, p = t.EXPRESSION_PATTERN, v = t.VARIABLE_PATTERN, h = t.VARIABLE_NAME_PATTERN, l = t.LITERAL_PATTERN, m = [], r = 0, u = function (L) { if (L.match(l)) throw Error('Invalid Literal "' + L + '"'); return L }; for (p.lastIndex = 0; ;) {
                var w = p.exec(k); if (null === w) { m.push(u(k.substring(r))); break } else m.push(u(k.substring(r, w.index))), r = w.index + w[0].length; if (!y[w[1]]) throw Error('Unknown Operator "' + w[1] + '" in "' + w[0] + '"'); if (!w[3]) throw Error('Unclosed Expression "' + w[0] + '"'); var g = w[2].split(","); for (var z = 0, B = g.length; z <
                    B; z++) { var C = g[z].match(v); if (null === C) throw Error('Invalid Variable "' + g[z] + '" in "' + w[0] + '"'); if (C[1].match(h)) throw Error('Invalid Variable Name "' + C[1] + '" in "' + w[0] + '"'); g[z] = { name: C[1], explode: !!C[3], maxlength: C[4] && parseInt(C[4], 10) } } if (!g.length) throw Error('Expression Missing Variable(s) "' + w[0] + '"'); m.push({ expression: w[0], operator: w[1], variables: g })
            } m.length || m.push(u(k)); this.parts = m; return this
    }; D.prototype.get = function (k) {
        var p = this.data, v = { type: 0, val: [], encode: [], encodeReserved: [] };
        if (void 0 !== this.cache[k]) return this.cache[k]; this.cache[k] = v; p = "[object Function]" === String(Object.prototype.toString.call(p)) ? p(k) : "[object Function]" === String(Object.prototype.toString.call(p[k])) ? p[k](k) : p[k]; if (void 0 !== p && null !== p) if ("[object Array]" === String(Object.prototype.toString.call(p))) { var h = 0; for (k = p.length; h < k; h++)void 0 !== p[h] && null !== p[h] && v.val.push([void 0, String(p[h])]); v.val.length && (v.type = 3) } else if ("[object Object]" === String(Object.prototype.toString.call(p))) {
            for (h in p) F.call(p,
                h) && void 0 !== p[h] && null !== p[h] && v.val.push([h, String(p[h])]); v.val.length && (v.type = 2)
        } else v.type = 1, v.val.push([void 0, String(p)]); return v
    }; n.expand = function (k, p) { var v = (new t(k)).expand(p); return new n(v) }; return t
});
(function (n, x) { "object" === typeof module && module.exports ? module.exports = x(require("jquery"), require("./URI")) : "function" === typeof define && define.amd ? define(["jquery", "./URI"], x) : x(n.jQuery, n.URI) })(this, function (n, x) {
    function t(h) { return h.replace(/([.*+?^=!:${}()|[\]\/\\])/g, "\\$1") } function D(h) { var l = h.nodeName.toLowerCase(); if ("input" !== l || "image" === h.type) return x.domAttributes[l] } function d(h) { return { get: function (l) { return n(l).uri()[h]() }, set: function (l, m) { n(l).uri()[h](m); return m } } } function F(h,
        l) { if (!D(h) || !l) return !1; var m = l.match(p); if (!m || !m[5] && ":" !== m[2] && !y[m[2]]) return !1; var r = n(h).uri(); if (m[5]) return r.is(m[5]); if (":" === m[2]) { var u = m[1].toLowerCase() + ":"; return y[u] ? y[u](r, m[4]) : !1 } u = m[1].toLowerCase(); return G[u] ? y[m[2]](r[u](), m[4], u) : !1 } var G = {}, y = {
            "=": function (h, l) { return h === l }, "^=": function (h, l) { return !!(h + "").match(new RegExp("^" + t(l), "i")) }, "$=": function (h, l) { return !!(h + "").match(new RegExp(t(l) + "$", "i")) }, "*=": function (h, l, m) {
                "directory" === m && (h += "/"); return !!(h + "").match(new RegExp(t(l),
                    "i"))
            }, "equals:": function (h, l) { return h.equals(l) }, "is:": function (h, l) { return h.is(l) }
        }; n.each("origin authority directory domain filename fragment hash host hostname href password path pathname port protocol query resource scheme search subdomain suffix tld username".split(" "), function (h, l) { G[l] = !0; n.attrHooks["uri:" + l] = d(l) }); var k = function (h, l) { return n(h).uri().href(l).toString() }; n.each(["src", "href", "action", "uri", "cite"], function (h, l) { n.attrHooks[l] = { set: k } }); n.attrHooks.uri.get = function (h) { return n(h).uri() };
    n.fn.uri = function (h) { var l = this.first(), m = l.get(0), r = D(m); if (!r) throw Error('Element "' + m.nodeName + '" does not have either property: href, src, action, cite'); if (void 0 !== h) { var u = l.data("uri"); if (u) return u.href(h); h instanceof x || (h = x(h || "")) } else { if (h = l.data("uri")) return h; h = x(l.attr(r) || "") } h._dom_element = m; h._dom_attribute = r; h.normalize(); l.data("uri", h); return h }; x.prototype.build = function (h) {
        if (this._dom_element) this._string = x.build(this._parts), this._deferred_build = !1, this._dom_element.setAttribute(this._dom_attribute,
            this._string), this._dom_element[this._dom_attribute] = this._string; else if (!0 === h) this._deferred_build = !0; else if (void 0 === h || this._deferred_build) this._string = x.build(this._parts), this._deferred_build = !1; return this
    }; var p = /^([a-zA-Z]+)\s*([\^\$*]?=|:)\s*(['"]?)(.+)\3|^\s*([a-zA-Z0-9]+)\s*$/; var v = n.expr.createPseudo ? n.expr.createPseudo(function (h) { return function (l) { return F(l, h) } }) : function (h, l, m) { return F(h, m[3]) }; n.expr[":"].uri = v; return n
});

//save quick route alert
$('.quickSaveForRouteAlert').on('click', function () {
    var subscriberRouteId = $("#subscriberRouteName").attr("data-subscriberRouteId");
    var subscriberRouteName = $("#quickRouteNotificationModal #subscriberRouteName").text();
    var isMap = $(".mapPage").length > 0;

    $.ajax('/My511/SaveQuickRouteAlert', { data: { 'routeId': subscriberRouteId, 'routeName': subscriberRouteName }, type: 'POST' })
        .done(function () {
            if (!isMap) window.location.href = '/my511';
            else AlertHelper.addAlertText(AlertType.Success, window.resources['AlertSavedSuccessfully'] + " " + window.resources['ViewLinkOnAlertSavedSuccessfully'], 7000);
        }).fail(function () {
            AlertHelper.addAlertText(AlertType.Error, window.resources['AlertFailedToSave'], 7000);
        }); 
});

//simplified route notification modal 
$('#hideSimplifyRouteNotification').on('change', function () {
    if ($(this).is(':checked')) {
        Cookies.set("_hideSimplifyRouteNotificationModal", true);
    }
    else {
        Cookies.remove("_hideSimplifyRouteNotificationModal");
    }
});

//route notification modal 
$('#hideRouteNotification').on('change', function () {
    if ($(this).is(':checked')) {
        Cookies.set("_hideRouteNotificationModal", true);
    }
    else {
        Cookies.remove("_hideRouteNotificationModal");
    }
});
