/* Minification failed. Returning unminified contents.
(774,7-8): run-time error JS1010: Expected identifier: .
(774,7-8): run-time error JS1195: Expected expression: .
(1687,7-8): run-time error JS1010: Expected identifier: .
(1687,7-8): run-time error JS1195: Expected expression: .
 */
var module = angular.module('appResources', ['appConfig']);
module.factory('appResources', ['appConfig', function (appConfig) {
    var iconData = {
        'metro': {
            url: 'icon_metro.svg',
            anchorX: 0.5,
            anchorY: 0.5
        },
        //!!!! RouteSearch
        'source': {
            path: 'M 0,119.99998 30.30001,81.999964 51.000017,61.199957 30.00001,39.99995 l 30.00001,0 26.700009,-27.90001 -3.500001,-3.6000013 q -1.500001,-1.4000004 -1.500001,-3.5000011 0,-2.1000007 1.500001,-3.5000012 1.4,-1.500000510943 3.500001,-1.500000510943 2.1,0 3.500001,1.500000510943 l 28.30001,28.2000096 q 1.5,1.500001 1.5,3.600001 0,2.100001 -1.5,3.500002 -1.4,1.5 -3.5,1.5 -2.1,0 -3.5,-1.5 l -3.6,-3.500002 -27.900013,26.700009 0,30.00001 L 58.800019,68.999959 38.000013,89.699966 Z',
            fillColor: 'black',
            fillOpacity: 1,
            scale: 0.25,
            strokeColor: 'white',
            strokeWeight: 1,
            anchorX: 0.01,
            anchorY: 0.7
        },
        'target': {
            path: 'm -0.31910926,61.599237 q 0,-16.182727 8.03097036,-30.212076 8.0309799,-14.02935 21.9026599,-22.4469201 13.87168,-8.41766845 30.06637,-8.41766845 16.32743,0 30.13274,8.22182645 13.805309,8.2219251 21.836289,22.1207131 8.03097,13.898886 8.03097,29.951052 0,16.052264 -9.69027,35.497546 -9.69026,14.8777 -22.101769,30.92987 -12.4115,16.05226 -19.97787,24.66558 l -7.56637,8.61341 q -1.32744,-1.17456 -3.58407,-3.39312 -2.25664,-2.21866 -8.82744,-9.20074 -6.57079,-6.98199 -12.74336,-14.2251 -6.17257,-7.24301 -13.80531,-17.68349 Q 13.751691,105.57963 8.0437211,95.79176 -0.31910926,77.651403 -0.31910926,61.599237 Z M 25.433101,58.075555 q 0,13.964167 10.02213,23.817322 10.02212,9.853152 24.09292,9.853152 14.20354,0 24.22566,-9.853152 10.02212,-9.853155 10.02212,-23.817322 0,-13.964069 -10.02212,-23.752042 -10.02212,-9.787875 -24.22566,-9.787875 -14.20354,0 -24.1593,9.853156 -9.95575,9.853155 -9.95575,23.686761 z',
            fillColor: 'black',
            fillOpacity: 1,
            scale: 0.25,
            strokeColor: 'white',
            strokeWeight: 1,
            anchorX: 0.2,
            anchorY: 0.9
        },
        //--
        'no_bearing': {
            path: 'M 189.9082 5.9003906 A 183.79132 183.79132 0 0 0 6.1171875 189.69141 A 183.79132 183.79132 0 0 0 189.9082 373.48242 A 183.79132 183.79132 0 0 0 373.69922 189.69141 A 183.79132 183.79132 0 0 0 189.9082 5.9003906 z M 191.88281 106.68555 A 83.555581 83.555581 0 0 1 275.43945 190.24219 A 83.555581 83.555581 0 0 1 191.88281 273.79688 A 83.555581 83.555581 0 0 1 108.32812 190.24219 A 83.555581 83.555581 0 0 1 191.88281 106.68555 z',
            width: 380.8,
            height: 376.8,
            scale: 0.08,
            strokeWeight: 2,
            strokeColor: 'white',
            fillOpacity: 1,
            anchorX: 0.5,
            anchorY: 0.5
        },
        'selection': {
            path: 'M 0,119.99998 30.30001,81.999964 51.000017,61.199957 30.00001,39.99995 l 30.00001,0 26.700009,-27.90001 -3.500001,-3.6000013 q -1.500001,-1.4000004 -1.500001,-3.5000011 0,-2.1000007 1.500001,-3.5000012 1.4,-1.500000510943 3.500001,-1.500000510943 2.1,0 3.500001,1.500000510943 l 28.30001,28.2000096 q 1.5,1.500001 1.5,3.600001 0,2.100001 -1.5,3.500002 -1.4,1.5 -3.5,1.5 -2.1,0 -3.5,-1.5 l -3.6,-3.500002 -27.900013,26.700009 0,30.00001 L 58.800019,68.999959 38.000013,89.699966 Z',
            width: 120.0,
            height: 120.0,
            scale: 0.24,
            strokeWeight: 1,
            strokeColor: 'white',
            fillOpacity: 1,
            anchorX: 0,
            anchorY: 1
        },
        'current': {
            path: 'm -0.31910926,61.599237 q 0,-16.182727 8.03097036,-30.212076 8.0309799,-14.02935 21.9026599,-22.4469201 13.87168,-8.41766845 30.06637,-8.41766845 16.32743,0 30.13274,8.22182645 13.805309,8.2219251 21.836289,22.1207131 8.03097,13.898886 8.03097,29.951052 0,16.052264 -9.69027,35.497546 -9.69026,14.8777 -22.101769,30.92987 -12.4115,16.05226 -19.97787,24.66558 l -7.56637,8.61341 q -1.32744,-1.17456 -3.58407,-3.39312 -2.25664,-2.21866 -8.82744,-9.20074 -6.57079,-6.98199 -12.74336,-14.2251 -6.17257,-7.24301 -13.80531,-17.68349 Q 13.751691,105.57963 8.0437211,95.79176 -0.31910926,77.651403 -0.31910926,61.599237 Z M 25.433101,58.075555 q 0,13.964167 10.02213,23.817322 10.02212,9.853152 24.09292,9.853152 14.20354,0 24.22566,-9.853152 10.02212,-9.853155 10.02212,-23.817322 0,-13.964069 -10.02212,-23.752042 -10.02212,-9.787875 -24.22566,-9.787875 -14.20354,0 -24.1593,9.853156 -9.95575,9.853155 -9.95575,23.686761 z',
            width: 120.0,
            height: 160.0,
            scale: 0.24,
            strokeWeight: 1,
            strokeColor: 'white',
            fillOpacity: 1,
            anchorX: 0.5,
            anchorY: 1
        },
        'default': {
            path: 'M 89.952178,121.20406 190.75215,10.761472 290.49709,121.62646 C 234.33422,89.720142 169.11554,76.109182 89.952178,121.20406 Z M 190,105.03125 A 183.79132,183.79132 0 0 0 6.2089844,288.82227 183.79132,183.79132 0 0 0 190,472.61328 183.79132,183.79132 0 0 0 373.79102,288.82227 183.79132,183.79132 0 0 0 190,105.03125 Z m 0.0781,100.1543 a 83.555581,83.555581 0 0 1 83.55665,83.55664 83.555581,83.555581 0 0 1 -83.55665,83.55469 83.555581,83.555581 0 0 1 -83.55468,-83.55469 83.555581,83.555581 0 0 1 83.55468,-83.55664 z',
            width: 380.8,
            height: 576.6,
            scale: 0.08,
            strokeWeight: 2,
            strokeColor: 'white',
            fillOpacity: 1,
            anchorX: 0.5,
            anchorY: 0.5
        }
    };

    var getStopIconData = function (tripType) {
        var data = iconData[tripType];
        return data || stop['default'];
    };

    /**
     * Merge defaults with user options
     * @private
     * @param {Object} defaults Default settings
     * @param {Object} options User options
     * @returns {Object} Merged values of defaults and options
     */
    var extend = function (defaults, options) {
        var extended = {};
        var prop;
        for (prop in defaults) {
            if (Object.prototype.hasOwnProperty.call(defaults, prop)) {
                extended[prop] = defaults[prop];
            }
        }
        for (prop in options) {
            if (Object.prototype.hasOwnProperty.call(options, prop)) {
                extended[prop] = options[prop];
            }
        }
        return extended;
    };

    return {
        straightColor: '#4d67ac',
        reverseColor: '#d24a43',
        customColor: '#483d8b',
        lookMarkerZoom: 16,
        mapCol: 2,

        getStopType: function (stop) {
            var type;
            if (stop.bearing < 0 && stop.type === undefined) {
                type = 'no_bearing';
            } else {
                type = 'default';
            }
            return type;
        },
        getStopIconData: function (stop) {
            var type = this.getStopType(stop);
            var data = getStopIconData(type);
            var ext = {
                fillColor: this.getStopTripColor(stop.tripType),
                zIndex: this.getStopTripZIndex(stop.tripType)
            };
            return extend(data, ext);
        },
        getIconData: function (name) {
            return iconData[name];
        },
        getStopTripColor: function (tripType) {
            switch (tripType) {
                case 0: return this.straightColor;
                case 1: return this.reverseColor;
                case 2: return this.customColor;
                case 3: return this.customColor;
                default: return this.customColor;
            }
        },
        getStopTripZIndex: function (tripType) {
            switch (tripType) {
                case 0: return 6;
                case 1: return 5;
                case 2: return 4;
                case 3: return 4;
                default: return 4;
            }
        }
    };
}]);
;
var mapTransportTypeToTag = {
    // latin
    'A': 'bus',
    'T': 'trolleybus',
    'Tr': 'tram',
    '#': 'tram',
    // russian
    'А': 'bus',
    'Т': 'trolleybus',
    'Тр': 'tram',
    'М': 'routetaxi'
};
var _daysOfWeek = {
    'ru': {
        1: 'пн.',
        2: 'вт.',
        4: 'ср.',
        8: 'чт.',
        16: 'пт.',
        32: 'сб.',
        64: 'вс.'
    },
    'be': {
        1: 'пан.',
        2: 'аут.',
        4: 'сер.',
        8: 'чац.',
        16: 'пят.',
        32: 'суб.',
        64: 'няд.'
    },
    'en': {
        1: 'mon',
        2: 'tue',
        4: 'wed',
        8: 'thu',
        16: 'fri',
        32: 'sat',
        64: 'sun'
    },
    daily: {
        'en': 'daily',
        'be': 'штодня',
        'ru': 'ежедн.',
    },
    weekdays: {
        'en': 'weekdays',
        'be': 'будні',
        'ru': 'будни',
    },
    weekend: {
        'en': 'weekend',
        'be': 'вых.',
        'ru': 'вых.',
    }
};
var appHelper = {
    encodedMapTransportTypeToTag: {
        // latin
        'A': 0,
        'T': 1,
        'Tr': 2,
        '#': 2,
        'М': 3,
        // russian
        'А': 0,
        'Т': 1,
        'Тр': 2,
        'М': 3
    },
    decodedMapTransportTypeToTag: {
        // latin
        0: 'A',
        1: 'T',
        2: '#',
        3: 'М'
    },
    decodeDaysOfWeek: function (days, lang) {
        var output = '';

        switch (days) {
            case 127:
                output += _daysOfWeek.daily[lang];
                break;
            case 31:
                output += _daysOfWeek.weekdays[lang];
                break;
            case 96:
                output += _daysOfWeek.weekend[lang];
                break;
            default:
                for (var [key, value] of Object.entries(_daysOfWeek[lang])) {
                    if (isDayOfWeek(days, Number(key))) {
                        addCommaIfNeed();
                        output += value;
                    }
                }
                break;
        }

        function addCommaIfNeed() {
            if (output.Length > 0) {
                output += ', ';
            }
        }

        function isDayOfWeek(weekDays, checkDay) {
            if ((weekDays & checkDay) / checkDay == 1) {
                return true;
            }
            return false;
        }

        return output;
    },
    getStopSearchTag: function (searchName) {
        if (searchName == null) return null;
        /*
        var tag = $.trim(searchName.replace(/[\s]/g, "")).substr(0, 3).toLowerCase();
        if (tag == null || tag.length < 4) {
            tag = $.trim(searchName.replace(/[\s]/g, "")).substr(0, 1).toLowerCase();
        }
        */
        tag = $.trim(searchName.replace(/[\s]/g, "")).toLowerCase();
        return tag;       
    },
    parseDate: function (date) {
        if (date == undefined) return 0;
        return parseInt(date.replace(/\/Date\((\d+)\)\//, function (str, p1, offset, s) {
            return p1;
        }));
    },
    grepFirst: function (arr, func) {
        for (var i = 0; i < arr.length; i++) {
            if (func(arr[i], i)) {
                return arr[i];
            }
        }
        return null;
    },
    lastIndex: function (arr, func) {
        var last = -1;
        for (var i = 0; i < arr.length; i++) {
            if (func(arr[i], i)) {
                last = i;
            }
        }
        return last;
    },
    indexOf: function (arr, value) {
        for (var i = 0; i < arr.length; i++) {
            if (arr[i] == value) {
                return i;
            }
        }
        return -1;
    },
    inArray: function (arr, value) {
        for (var i = 0; i < arr.length; i++) {
            if (arr[i] === value) {
                return true;
            }
        }
        return false;
    },
    addIfNew: function (arr, value, func) {
        var index = -1;
        for (var i = 0; i < arr.length; i++) {
            if (func(arr[i], i, arr)) {
                index = i;
                break;
            }
        }
        if (index === -1) {
            arr.push(value);
        }
    },
    mergeStops: function (stops, names) {
        if (stops == null || names == null)
            return;

        if (stops.length !== names.length)
            return;

        for (var i = 0; i < stops.length; i++) {
            stops[i].Name = names[i];
        }
    },
    where: function (arr, prop, value) {
        var newArray = [];
        for (var i = 0; i < arr.length; i++) {
            if (arr[i][prop] == value) {
                newArray.push(arr[i]);
            }
        }
        return newArray;
    },
    select: function (arr, prop) {
        var newArray = [];
        for (var i = 0; i < arr.length; i++) {
            newArray.push(arr[i][prop]);
        }
        return newArray;
    },
    doSelect: function (arr, func) {
        var newArray = [];
        for (var i = 0; i < arr.length; i++) {
            newArray.push(func(arr[i], i, arr));
        }
        return newArray;
    },
    distinct: function (arr) {
        if (arr.length === 0) {
            return [];
        }

        var newArray = [];
        arr.sort();
        newArray.push(arr[0]);
        var last = arr[0];
        for (var i = 1; i < arr.length; i++) {
            if (arr[i] != last) {
                last = arr[i];
                newArray.push(last);
            }
        }
        return newArray;
    },
    getUserLang: function () {
        //return navigator.language || navigator.userLanguage;
        return 'ru-RU';
    },
    formatDepart: function (info) {
        switch (info) {
            case 'D':
                return 'Задерж.';
            case 'A':
                return 'Приб.';
            default:
                return info;
        }
    },
    getActionColor: function (actionType, ridingType) {
        switch (actionType) {
            case 'riding':
                switch (ridingType) {
                    case 'bus':
                        return 'red';
                    case 'trolleybus':
                        return 'blue';
                    case 'tram':
                        return 'black';
                    case 'routetaxi':
                        return 'orange';
                    case 'metro':
                        return 'green';
                    default:
                        return 'black';
                }
            default:
                return 'gray';
        }
    },
    resolveStop: function (search, postfix) {
        var type = search['type' + postfix];
        if (type == 'stop') {
            var id = search['stopId' + postfix];
            var name = search['stopName' + postfix];
            return {
                id: id == null ? 0 : id,
                name: name == null || name == 'none' ? null : name
            };
        } else {
            return {
                id: 0,
                name: 'none'
            };
        }
    },
    resolveLocation: function (search, postfix) {
        var type = search['type' + postfix];
        if (type == 'location') {
            var lat = parseFloat(search['lat' + postfix]);
            var lon = parseFloat(search['lon' + postfix]);
            return {
                lat: lat,
                lon: lon
            };
        } else if (type == 'current') {
            return {
                lat: NaN,
                lon: NaN
            };
        } else {
            return {
                lat: NaN,
                lon: NaN
            };
        }
    },
    isInvalid: function (value) {
        return value == null || isNaN(value);
    },
    isEmptyOrSpaces: function (str) {
        return str == null || str.match(/^ *$/) !== null;
    },
    transportTypeToTag: function (type) {
        var tag = mapTransportTypeToTag[type];
        if (tag == undefined) {
            return 'unknown';
        }
        return tag;
    },
    encodeTransportTypeToTag: function (type) {
        var tag = this.encodedMapTransportTypeToTag[type];
        if (tag == undefined) {
            throw new Error('encodedMapTransportTypeToTag not found');
        }
        return tag;
    },
    decodeTransportTypeToTag: function (type) {
        var tag = appHelper.decodedMapTransportTypeToTag[type];
        if (tag == undefined) {
            throw new Error('encodedMapTransportTypeToTag not found');
        }
        return tag;
    },
    isIPhoneDevice: function iOS() {
        return [
            'iPhone Simulator',
            'iPhone'
        ].includes(navigator.platform);
    },
    isOnScreen: function (element)
    {
        var curPos = element.offset();
        var curTop = curPos.top;
        var screenHeight = $(window).height();
        return (curTop > screenHeight) ? false : true;
    },
    LOG_LEVEL_ERROR: 1,
    LOG_LEVEL_WARNING: 2,
    LOG_LEVEL_INFO: 3,
    LOG_LEVEL_DEBUG: 4,
    LOG_LEVEL_VERBOSE: 5
};

function CacheItem(data, time, duration) {
    // Fields
    this.data = data;
    if (time instanceof Date) {
        this.time = time.getTime();
    } else if (time instanceof Number || typeof (time) == 'number') {
        this.time = time;
    } else {
        throw new Error('Type of time is not supported.');
    }
    if (duration instanceof Number || typeof (duration) == 'number') {
        this.duration = duration;
    } else {
        throw new Error('Type of duration is not supported.');
    }

    // Methods
    this.isExpired = function () {
        return Date.now() > (time + duration);
    };
}
function Cancellation($q, key) {
    var deferred = $q.defer();
    if (key == undefined) {
        key = null;
    }
    return {
        /**
         * $http cancellation as $http({timeout:promise}).
         * @returns {} 
         */
        promise: function () { return deferred.promise; },
        isCancelled: false,
        key: key,
        cancel: function () {
            this.isCancelled = true;
            if (deferred != null) {
                deferred.resolve();
                deferred = null;
            }
        }
    };
}

var app = angular.module('appExt', []);
app.filter('uriEncode', function () {
    return window.encodeURIComponent;
});
app.filter('select', function () {
    return function (array, expression) {
        if (arguments.length > 0 && !angular.isUndefined(array) && !angular.isUndefined(expression)) {
            var newArray = [];
            for (var i = 0; i < array.length; i++) {
                newArray.push(array[i][expression]);
            }
            return newArray;
        } else {
            return Array;
        }
    }
});
app.filter('commas', function () {
    return function (array) {
        return array.join(', ');
    }
});
app.filter('none', function () {
    return function (value) {
        if (value == null || value == '') {
            return 'none';
        }
        return value;
    }
});
app.factory('focus', ['$timeout', '$window', function ($timeout, $window) {
    return function (id) {
        $timeout(function () {
            var elem = $window.document.getElementById(id);
            if (elem) {
                elem.focus();
            }
        });
    };
}]);
app.directive('ngFocusType', function (focus) {
    return function (scope, elem, attrs) {
        elem.on(attrs['ngFocusType'], function () {
            focus(attrs['ngFocus']);
        });

        // Removes bound events in the element itself
        // when the scope is destroyed
        scope.$on('$destroy', function () {
            elem.off(attrs['ngFocusType']);
        });
    };
});
app.directive('ngClassWatch', function () {
    return {
        link: function (scope, elem, attrs) {
            var handlerName = attrs.ngClassWatch;
            var handler = scope[handlerName];
            if ($.isFunction(handler)) {
                scope.$watch(function () {
                    return elem.attr('class');
                }, handler);
            }
        }
    };
});
app.directive('ngKeepScroll', function ($timeout) {
    return function (scope, element, attrs) {
        $timeout(function () {
            var scrollY = parseInt(scope.$eval(attrs.ngKeepScroll));
            $(element).scrollTop(scrollY ? scrollY : 0);
        }, 0);

        scope.$on('$routeChangeStart', function () {
            scope.$eval(attrs.ngKeepScroll + ' = ' + $(element).scrollTop());
        });
    }
});
app.directive("ngKeepScrollId", ['$route', '$timeout', '$document', function($route, $timeout, $document) {
    var scrollPosCache = {};
    // templateUrl: $route.current.loadedTemplateUrl
    var getId = function(attrs) {
        var id = attrs.ngKeepScrollId;
        if (id === undefined) {
            id = attrs.id;
        }
        return id;
    };
    var getType = function (attrs) {
        var type = {
            save: false,
            restory: false,
            reset: false
        };
        if (attrs.ngKeepScrollIdType === undefined) {
            type.save = true;
            type.restory = true;
        } else {
            var flags = attrs.ngKeepScrollIdType.split('|');
            for (var i = 0; i < flags.length; i++) {
                type[flags[i]] = true;
            }
        }
        return type;
    }
    return {
        restrict: 'A', // Only for attribute
        scope: false,
        link: function (scope, element, attrs) {
            scope.$on('$routeChangeStart', function () {
                var id = getId(attrs);
                if (id === undefined) {
                    return;
                }
                var type = getType(attrs);
                if (type.save) {
                    scrollPosCache[id] = element[0].scrollTop;
                }
                if (type.reset) {
                    delete scrollPosCache[id];
                }
            });
            var restoreScroll = function () {
                var id = getId(attrs);
                if (id === undefined) {
                    return;
                }
                var scrollTopValue = scrollPosCache[id];
                if (scrollTopValue === undefined) {
                    return;
                }
                var type = getType(attrs);
                if (type.restory) {
                    element[0].scrollTop = scrollTopValue;
                }
            };
            var ngKeepScrollAtRaw = attrs.ngKeepScrollAt;
            if (ngKeepScrollAtRaw != null) {
                angular.forEach(ngKeepScrollAtRaw.split(','), function (ngKeepScrollAt) {
                    var foundElement = null;
                    var whenElement = null;
                    var unbind = scope.$watch(function () {
                        if (foundElement == null || whenElement.length === 0) {
                            foundElement = document.getElementById(ngKeepScrollAt);
                            whenElement = angular.element(foundElement);
                            return 0;
                        }
                        return whenElement[0].childNodes.length;
                    }, function (newValue, oldValue) {
                        restoreScroll();
                        //console.debug(newValue);
                        if (newValue !== 0) {
                            if (whenElement != null && whenElement.length !== 0) {
                                unbind();
                            }
                        }
                    });
                });
            } else {
                scope.$on('$routeChangeSuccess', function () {
                    var id = getId(attrs);
                    if (id === undefined) {
                        return;
                    }
                    $timeout(function () {
                        restoreScroll();
                    }, 0);
                });
            }
        }
    };
}]);

// Progress Circular
app.provider('progressCircularDefaults', function () {
    var defaults = {
        size: 200,
        strokeWidth: 5,
        stroke: 'lightgray',
        background: null
    };
    this.setDefault = function(name, value) {
        defaults[name] = value;
        return this;
    };
    this.$get = function() {
        return function(attr) {
            angular.forEach(defaults, function(value, key) {
                if (!attr[key]) {
                    attr[key] = value;
                }
            });
        };
    };
});
app.directive('ngProgressCircular', ['progressCircularDefaults', function (defaults) {
    return {
        restrict: 'A',
        scope: {
            'size': '@',
            'counterClockwise': '@',
            'progress': '&'
            //'text': '&'
        },
        compile: function(element, attr) {
            defaults(attr);
            return function(scope, element, attr) {
                scope.offset = /firefox/i.test(navigator.userAgent) ? -89.9 : -90;
                var updateRadius = function () {
                    var circles = element.find('circle');
                    var strokeWidthPx = circles.css('stroke-width');
                    var strokeWidth = strokeWidthPx != null ? parseInt(strokeWidthPx) : 0;
                    scope.radius = Math.max(0, (scope.size - strokeWidth) / 2 - 1);
                    scope.circumference = 2 * Math.PI * scope.radius;
                };
                scope.$watchCollection('[size, strokeWidth]', updateRadius);
                updateRadius();
            };
        },
        template:
            '<svg ng-attr-width="{{size}}" ng-attr-height="{{size}}">' +
                '<circle class="progress-circular-background" fill="none" ng-attr-cx="{{size/2}}" ng-attr-cy="{{size/2}}" ng-attr-r="{{radius}}"/>' +
                '<circle class="progress-circular" fill="none" ng-attr-cx="{{size/2}}" ng-attr-cy="{{size/2}}" ng-attr-r="{{radius}}" ng-attr-stroke-dasharray="{{circumference}}" ng-attr-stroke-dashoffset="{{(1 - progress())*circumference}}" ng-attr-transform="rotate({{offset}}, {{size/2}}, {{size/2}}){{ (counterClockwise && counterClockwise != \'false\') ? \' translate(0, \' + size + \') scale(1, -1)\' : \'\' }}"/>' +
                //'<text class="progress-circular-text" ng-attr-x="{{size/2}}" ng-attr-y="{{size/2}}" fill="{{stroke}}" text-anchor="middle" alignment-baseline="central">{{text()}}</text>' +
                '<text class="progress-circular-text" ng-attr-x="{{size/2}}" ng-attr-y="{{size/2}}" fill="#3b78e7" text-anchor="middle" alignment-baseline="central">&#xe031</text>' +
            '</svg>'
    };
}]);;
var module = angular.module('map', ['appConfig', 'appResources']);
module.factory('mapService', ['appConfig', 'appResources', function (appConfig, appResources) {
    var map = null;
    var mapCol = 2;
    var popupComponent = new AppMapPopup();
    var typedMarkers = {};
    var tracks = [];
    // apparel info title on the map
    var _apparelInfoElem = null;

    var listeners = {
        map: {
            click: []
        },
        stop: {
            click: []
        },
        vehicles: {
            click: []
        },
        marker: {
            click: []
        },
        track: {
            click: []
        },
        col: {
            changed: []
        }
    };

    var internalUpdate = function () {
        if (map == null) { return; }
        map.updateSize();
    };

    var lazyUpdate = function () {
        setTimeout(function () {
            if (map == null) { return; }
            map.updateSize();
        }, 0);
    };

    var buildSvg = function (iconData) {
        var strokeColor = 'white';
        var strokeWeight = iconData.strokeWeight;
        var strokeWidth = strokeWeight / iconData.scale;
        var svg = '<svg width="' + iconData.width + '" height="' + iconData.height + '" version="1.1" xmlns="http://www.w3.org/2000/svg">' +
            '<path fill="' + iconData.fillColor + '" stroke="' + strokeColor + '" stroke-width="' + strokeWidth + '" ' + 'd="' + iconData.path + '"></path>' +
            '</svg>';
        return svg;
    };

    var createDynamicLayer = function () {
        var source = new ol.source.Vector();
        var layer = new ol.layer.Vector({
            source: source
        });
        return layer;
    };

    var createImageLayer = function (imageUrl) {
        var source = new ol.source.Vector();
        var style = new ol.style.Style({
            image: new ol.style.Icon(/** @type {olx.style.IconOptions} */ {
                anchor: [0.5, 1.0],
                anchorXUnits: 'fraction',
                anchorYUnits: 'fraction',
                src: appConfig.urlImages + '/' + imageUrl
            })
        });
        var layer = new ol.layer.Vector({
            source: source,
            style: style
        });
        return layer;
    };

    function AppMapPopup() {
        //
        // Private fields
        //
        var that = this;
        var containerElement;
        var contentElement;
        var closerElement;
        var overlay;
        var isOpen = false;
        var onCloseListener;
        //
        // Public methods
        //
        this.init = function (map) {
            /**
             * Elements that make up the popup.
             */
            containerElement = document.getElementById('popup');
            contentElement = document.getElementById('popup-content');
            closerElement = document.getElementById('popup-closer');
            if (!containerElement || !contentElement || !closerElement) {
                containerElement = contentElement = closerElement = null;
                console.warn('Popup hasn\'t initialized');
                return;
            }

            /**
             * Create an overlay to anchor the popup to the map.
             */
            overlay = new ol.Overlay({
                id: 'popup',
                element: containerElement
            });

            /**
             * Add a click handler to hide the popup.
             * @return {boolean} Don't follow the href.
             */
            closerElement.onclick = function () {
                if (onCloseListener) {
                    onCloseListener();
                }
                that.close();
                return false;
            };

            map.addOverlay(overlay);
        };
        this.close = function () {
            isOpen = false;
            overlay.setPosition(undefined);
            closerElement.blur();
        };
        this.open = function (coord, content) {
            if (!containerElement) {
                return;
            }
            if (content) {
                contentElement.innerHTML = content;
            }
            overlay.setPosition(coord);
            isOpen = true;
        };
        this.isOpen = function () {
            return isOpen;
        };
        this.setOnCloseListener = function (listener) {
            onCloseListener = listener;
        };
    }


    var createTrackLayer = function (color, isDashLine) {
        var source = new ol.source.Vector();
        var style = [
            new ol.style.Style({
                stroke: new ol.style.Stroke({
                    color: [255, 255, 255, 1], // white
                    width: 7
                })
            }),
            new ol.style.Style({
                stroke: new ol.style.Stroke({
                    color: color, //[0, 153, 255, 1], // blue
                    width: 5
                })
            })
        ];
        // lineDash: [.1, 5]
        var layer = new ol.layer.Vector({
            source: source,
            style: style,
            opacity: 0.7
        });
        return layer;
    };

    var radians = function (degrees) {
        return degrees * Math.PI / 180;
    };

    var trackLayers = {};
    var stopMarkerLayers = {};
    var vehicleMarkerLayers = {};
    var typedMarkerLayers = {};
    var tooltipOverlays = [];

    var getPoint = function (obj) {
        var point = new ol.geom.Point(ol.proj.transform([obj.lon, obj.lat], 'EPSG:4326', 'EPSG:3857'));
        return point;
    };
    var getProj = function (location) {
        return ol.proj.fromLonLat([location.lon, location.lat]);
    };

    var exclude = function (targetArray, excludeArray) {
        for (var i = 0; i < excludeArray.length; i++) {
            var target = excludeArray[i];
            for (var j = 0; j < targetArray.length; j++) {
                if (target === targetArray[j]) {
                    targetArray.splice(j, 1);
                    break;
                }
            }
        }
    };
    var contains = function (array, value) {
        for (var i = 0; i < array.length; i++) {
            if (array[i] === value) {
                return true;
            }
        }
        return false;
    };
    var forEachLayer = function (layers, action) {
        for (var layerName in layers) {
            var layer = layers[layerName];
            var result = action(layer);
            if (result != null) {
                return result;
            }
        }
    };
    var forEachLayersFeature = function (layers, action) {
        forEachLayer(layers, function (layer) {
            var source = layer.getSource();
            var result = source.forEachFeature(action);
            if (result != null) {
                return result;
            }
        });
    };
    var buildContent = function (stopName) {
        var content =
            '<div id="stopInfo" style="white-space: nowrap;">' + stopName + '</div>';
        return content;
    }
    var mapFindById = function (layers, id, remove) {
        if (id === undefined) {
            return null;
        }
        return forEachLayer(layers, function (layer) {
            var source = layer.getSource();
            var feature = source.getFeatureById(id);
            if (feature == null) {
                return null;
            }
            if (remove) {
                layer.removeFeature(feature);
            }
            return feature;
        });
    };
    var clearLayers = function (layers) {
        forEachLayer(layers, function (layer) {
            var source = layer.getSource();
            source.clear();
        });
    };
    var deprecateLayerFeatures = function (layers) {
        for (var layer in layers) {
            var source = layers[layer].getSource();
            source.forEachFeature(function (feature) {
                feature.set('deprecated', true);
            });
        }
    };
    var clearDeprecatedLayerFeatures = function (layers) {
        for (var layerName in layers) {
            var source = layers[layerName].getSource();
            var toRemove = [];
            source.forEachFeature(function (feature) {
                var isDeprecated = feature.get('deprecated');
                if (isDeprecated) {
                    toRemove.push(feature);
                }
            });
            for (var i = 0; i < toRemove.length; i++) {
                source.removeFeature(toRemove[i]);
            }
            source.changed();
        }
        internalUpdate();
    };
    var extentFeature = function (feature, extent) {
        var geometry = feature.getGeometry();
        if (!geometry) { return; }

        var featureExtent = feature.getGeometry().getExtent();
        if (!featureExtent) { return; }

        ol.extent.extend(extent, featureExtent);
    };
    var extendLayersBounds = function (layers, extent) {
        if (!extent) {
            extent = ol.extent.createEmpty();
        }
        forEachLayersFeature(layers, function (feature) {
            extentFeature(feature, extent);
        });
        return extent;
    };
    var extendLayersBoundsById = function (layers, id, extent) {
        if (!extent) {
            extent = ol.extent.createEmpty();
        }
        var feature = mapFindById(layers, id, false);
        if (!feature) { return extent; }

        return extentFeature(feature, extent);
    };
    var lookAtExtent = function (extent) {
        if (!extent || ol.extent.isEmpty(extent)) { return; }

        map.getView().fit(extent, map.getSize());
    };
    var getPolylineBounds = function (polyline) {
        // TODO:
    };
    return {
        STOP_TYPE_STRAIGHT: 0,
        STOP_TYPE_REVERSE: 1,
        STOP_TYPE_CUSTOM: 2,
        update: function () {
            internalUpdate();
        },
        init: function () {
            var i;
            var j;
            var mapContainer = document.getElementById('mapContainer');
            if (!mapContainer) {
                console.info('no container');
                return;
            }

            // Quick Start https://openlayers.org/en/latest/doc/quickstart.html
            map = new ol.Map({
                target: 'mapContainer',
                layers: [
                    new ol.layer.Tile({
                        source: new ol.source.OSM()
                    })
                ],
                view: new ol.View({
                    center: getProj(appConfig),
                    zoom: appConfig.zoom,
                    maxZoom: 17
                })
            });

            popupComponent.init(map);

            var tooltip = document.createElement('div');
            tooltip.classList.add('sws-tooltip');
            var overlayTooltip = new ol.Overlay({
                element: tooltip,
                offset: [10, 0],
                positioning: 'bottom-left'
            });
            map.addOverlay(overlayTooltip);

            var hitOptions = {
                hitTolerance: 5
            };
            map.on('pointermove', function (evt) {
                if (evt.dragging) return;
                var pixel = map.getEventPixel(evt.originalEvent);

                var hit = false;
                var ftr = map.forEachFeatureAtPixel(pixel, function (feature) {
                    if (feature.get('clickable')) {
                        hit = true;
                        if (feature.get('type') === 'vehicle') {
                            return feature;
                        }
                    }
                });

                tooltip.style.display = ftr ? '' : 'none';
                map.getTargetElement().style.cursor = hit ? 'pointer' : 'initial';

                if (ftr) {
                    overlayTooltip.setPosition(evt.coordinate);
                    var tag = ftr.get('tag');
                    if (tag.isApparel) {
                        tooltip.innerHTML = tag.tooltip + ' <span class="glyphicon glyphicon-triangle-top sws-text-warning rotate45"></span>';
                    } else {
                        tooltip.innerHTML = tag.tooltip;
                    }
                }
            });
            map.on('click', function (e) {
                if (e.dragging) return;
                var pixel = map.getEventPixel(e.originalEvent);
                var clickFeature = null;
                map.forEachFeatureAtPixel(pixel, function (feature, layer) {
                    if (!clickFeature && feature.get('clickable')) {
                        clickFeature = feature;
                    }
                }, hitOptions);

                if (clickFeature && clickFeature.get('type') === 'stop') {
                    listeners.stop.click.forEach(function (item) {
                        item(clickFeature.get('tag'));
                    });
                }
                //!!!! RouteSearch
                else {
                    listeners.map.click.forEach(function (item) {
                        var coords = ol.proj.toLonLat(e.coordinate);
                        item(coords[1], coords[0]);
                    });
                }
                //--
            });

            trackLayers = {
                'a': createTrackLayer(appResources.straightColor),
                'b': createTrackLayer(appResources.reverseColor)
            };

            stopMarkerLayers = {
                'metro': createDynamicLayer(),
                'no_bearing': createDynamicLayer(),
                'selection': createDynamicLayer(),
                'default': createDynamicLayer()
            };

            vehicleMarkerLayers = {
                'bus-1': createImageLayer('marker-bus-1.png'),
                'bus-2': createImageLayer('marker-bus-2.png'),
                'trolleybus-1': createImageLayer('marker-trolleybus-1.png'),
                'trolleybus-2': createImageLayer('marker-trolleybus-2.png'),
                'tram-1': createImageLayer('marker-tram-1.png'),
                'tram-2': createImageLayer('marker-tram-2.png'),
                'routetaxi-1': createImageLayer('marker-routetaxi-1.png'),
                'routetaxi-2': createImageLayer('marker-routetaxi-2.png'),
                'bus-1-apparel': createImageLayer('/apparel/marker-bus-1.png'),
                'bus-2-apparel': createImageLayer('/apparel/marker-bus-2.png'),
                'trolleybus-1-apparel': createImageLayer('/apparel/marker-trolleybus-1.png'),
                'trolleybus-2-apparel': createImageLayer('/apparel/marker-trolleybus-2.png'),
                'tram-1-apparel': createImageLayer('/apparel/marker-tram-1.png'),
                'tram-2-apparel': createImageLayer('/apparel/marker-tram-2.png'),
            };

            typedMarkerLayers = {
                'any': createDynamicLayer()
            };

            forEachLayer(trackLayers, function (layer) {
                map.addLayer(layer);
            });
            forEachLayer(stopMarkerLayers, function (layer) {
                map.addLayer(layer);
            });
            forEachLayer(vehicleMarkerLayers, function (layer) {
                map.addLayer(layer);
            });
            forEachLayer(typedMarkerLayers, function (layer) {
                map.addLayer(layer);
            });

            _apparelInfoElem = document.querySelector('.sws-apparel-map-info');
        },
        unbind: function (type, event, func) {
            var clearEvents = function (obj) {
                for (var e in obj) {
                    if (obj.hasOwnProperty(e)) {
                        obj[e] = [];
                    }
                }
            };
            if (type === undefined) {
                for (var t in listeners) {
                    if (listeners.hasOwnProperty(t)) {
                        clearEvents(listeners[t]);
                    }
                }
            } else if (event === undefined) {
                clearEvents(listeners[type]);
            } else {
                if (arguments.length >= 3) {
                    if ($.isFunction(func)) {
                        var ls = listeners[type][event];
                        for (var i = 0; i < ls.length; i++) {
                            if (ls[i] == func) {
                                ls.splice(i, 1);
                                break;
                            }
                        }
                    }
                } else {
                    listeners[type][event] = [];
                }
            }
        },
        clear: function (types, clearType) {
            if (arguments.length === 0) {
                clearLayers(stopMarkerLayers);
                clearLayers(trackLayers);
                clearLayers(vehicleMarkerLayers);
                this.removeMarker();
            } else {
                var self = this;
                var complexClear = function (complexType) {
                    var parts = complexType.split('.');
                    if (parts.length > 1) {
                        switch (parts[0]) {
                            case 'marker':
                                self.removeMarker(parts[1]);
                                break;
                        }
                    }
                };
                var removeArray;
                if (clearType === 'except') {
                    removeArray = ['stop', 'track', 'vehicle', 'marker'];
                    if (!contains(types, 'marker')) {
                        for (var propertyName in typedMarkers) {
                            if (typedMarkers.hasOwnProperty(propertyName)) {
                                removeArray.push('marker.' + propertyName);
                            }
                        }
                    }
                    exclude(removeArray, types);
                } else {
                    removeArray = types;
                }
                for (var i = 0; i < removeArray.length; i++) {
                    var t = removeArray[i];
                    switch (t) {
                        case 'stop':
                            clearLayers(stopMarkerLayers);
                            break;

                        case 'track':
                            clearLayers(trackLayers);
                            break;

                        case 'vehicle':
                            clearLayers(vehicleMarkerLayers);
                            break;

                        case 'marker':
                            this.removeMarker();
                            break;

                        default:
                            complexClear(t);
                            break;
                    }
                }
            }
            this.removeOverlays(tooltipOverlays);
            tooltipOverlays = [];
        },
        removeOverlays: function (arr) {
            for (var i = 0; i < arr.length; i++) {
                map.removeOverlay(arr[i]);
            }
        },
        on: function (type, event, func) {
            var ls = listeners[type][event];
            if (!appHelper.inArray(ls, func)) {
                ls.push(func);
            }
        },
        setCol: function (col) {
            if (mapCol !== col) {
                mapCol = col;
                listeners.col.changed.forEach(function (item) {
                    item();
                });
            }
            lazyUpdate();
        },
        getCol: function () {
            return mapCol;
        },

        setMarker: function (type, lat, lon) {
            var layer = typedMarkerLayers['any'];
            if (!layer) {
                console.warn('No layer for ' + type + '!');
                return;
            }
            var feature = mapFindById(typedMarkerLayers, type, false);
            if (!feature) {
                layer.getSource().addFeature(feature = new ol.Feature());
            }
            feature.setId(type);
            var iconData = appResources.getIconData(type);
            if (!iconData) {
                console.warn('No icon data for ' + type + '!');
                return;
            }
            var svg = buildSvg(iconData);
            var style = new ol.style.Style({
                image: new ol.style.Icon({
                    anchor: [iconData.anchorX, iconData.anchorY],
                    anchorXUnits: 'fraction',
                    anchorYUnits: 'fraction',
                    src: 'data:image/svg+xml;base64,' + btoa(svg),
                    scale: iconData.scale
                })
            });
            var location = { lat: lat, lon: lon };
            feature.setGeometry(getPoint(location));
            feature.setStyle(style);
            feature.set('tag', location);
            feature.set('type', 'marker');
        },
        removeMarker: function (type) {
            if (type == null) {
                clearLayers(typedMarkerLayers);
            } else {
                mapFindById(typedMarkerLayers, type, true);
            }
        },
        lookMarker: function (type) {
            var feature = mapFindById(typedMarkerLayers, type, false);
            if (!feature) { return; }

            var location = feature.get('tag');
            idleUpdate = true;
            map.getView().setCenter(getProj(location));
            map.getView().setZoom(appResources.lookMarkerZoom);
        },

        showPopup: function (stop, isLowercase) {
            if (isLowercase) {
                var content = buildContent(stop.name);
                var coordinate = ol.proj.fromLonLat([stop.lon, stop.lat])
                popupComponent.open(coordinate, content);
            } else {
                var content = buildContent(stop.Name);
                var coordinate = ol.proj.fromLonLat([stop.Longitude, stop.Latitude])
                popupComponent.open(coordinate, content);
            }
        },

        beginStops: function () {
            deprecateLayerFeatures(stopMarkerLayers);
        },
        /**
         * 
         * @param {{id, lat, lon, bearing, type}} stop - Stop
         * @param {number} tripType - Trip type
         * @param {google.maps.Marker} marker - Reuse marker
         * @returns {void} - Nothing
         */
        addStop: function (stop) {
            var type = appResources.getStopType(stop);
            var layer = stopMarkerLayers[type];
            if (!layer) {
                layer = stopMarkerLayers[type = 'default'];
            }
            if (!layer) {
                console.warn('No stop layer ' + layerName);
                return;
            }

            var feature = mapFindById(stopMarkerLayers, stop.id, false);
            if (!feature) {
                layer.getSource().addFeature(feature = new ol.Feature());
            }
            feature.setId(stop.id);
            var iconData = appResources.getStopIconData(stop);
            var svg = buildSvg(iconData);
            var style = new ol.style.Style({
                image: new ol.style.Icon({
                    anchor: [0.5, 0.5],
                    anchorXUnits: 'fraction',
                    anchorYUnits: 'fraction',
                    src: 'data:image/svg+xml;base64,' + btoa(svg),
                    scale: iconData.scale,
                    rotation: radians(stop.bearing || 0)
                })
            });
            feature.setGeometry(getPoint(stop));
            feature.setStyle(style);
            feature.set('tag', stop);
            feature.set('type', 'stop');
            feature.set('clickable', true);
            feature.set('deprecated', false);

            if (stop.name) {
                var tooltip = document.createElement('div');
                tooltip.classList.add('sws-tooltip-min');
                var overlayTooltip = new ol.Overlay({
                    element: tooltip,
                    offset: [10, 0],
                    positioning: 'bottom-left'
                });
                overlayTooltip.set('type', 'tooltip');
                map.addOverlay(overlayTooltip);
                tooltipOverlays.push(overlayTooltip);
                overlayTooltip.setPosition(feature.getGeometry().getCoordinates());
                tooltip.innerHTML = stop.name
            }
        },
        /**
         * 
         * @param {Array<{id, lat, lon, bearing, type}>} stops - Stops
         * @param {number} tripType - Trip type
         * @returns {void} - Nothing
         */
        addStops: function (stops) {
            for (var i = 0; i < stops.length; i++) {
                this.addStop(stops[i]);
            }
        },
        endStops: function () {
            clearDeprecatedLayerFeatures(stopMarkerLayers);
        },
        lookStop: function (id) {
            var feature = mapFindById(stopMarkerLayers, id, false);
            if (!feature) { return; }

            var stop = feature.get('tag');
            idleUpdate = true;
            map.getView().setCenter(getProj(stop));
            map.getView().setZoom(appResources.lookMarkerZoom);
        },
        lookStops: function () {
            lookAtExtent(extendLayersBounds(stopMarkerLayers));
        },

        beginVehicles: function () {
            deprecateLayerFeatures(vehicleMarkerLayers);
        },
        getVehicleHtmlLabel: function (type) {
            switch (type) {
                case 'bus':
                    return '<span class="sws-veh-label-bus">A</span>';
                case 'trolleybus':
                    return '<span class="sws-veh-label-trolleybus">T</span>';
                case 'tram':
                    return '<span class="sws-veh-label-tram">T</span>';
                default:
                    return '';
            }
        },
        /**
         * Add vehicle
         * @param {object} vehicle - Vehicle
         * @param {object} marker - Reuse marker
         * @returns {void} - Nothing
         */
        addVehicle: function (vehicle) {

            var layerName = vehicle.type + '-' + vehicle.tripType;
            if (vehicle.isApparel) {
                layerName += '-apparel';
            }

            var layer = vehicleMarkerLayers[layerName];
            if (!layer) {
                console.warn('No vehicle layer ' + layerName);
                return;
            }

            var feature = mapFindById(vehicleMarkerLayers, vehicle.id, false);
            if (!feature) {
                feature = new ol.Feature();
                layer.getSource().addFeature(feature);
            }
            feature.setGeometry(getPoint(vehicle));
            feature.setId(vehicle.id);
            feature.set('tag', vehicle);
            feature.set('type', 'vehicle');
            feature.set('clickable', true);
            feature.set('deprecated', false);
        },
        /**
         * Add vehicles
         * @param {Array<object>} vehicles - Vehicles
         * @returns {void} - Nothing
         */
        addVehicles: function (vehicles) {
            for (var i = 0; i < vehicles.length; i++) {
                this.addVehicle(vehicles[i]);
            }
        },
        endVehicles: function () {
            clearDeprecatedLayerFeatures(vehicleMarkerLayers);
        },
        /**
         * Move view to vehicle
         * @param {number} id - Vehicle id
         * @returns {void} - Nothing
         */
        lookVehicle: function (id) {
            var feature = mapFindById(vehicleMarkerLayers, id, false);
            if (!feature) { return; }

            var vehicle = feature.get('tag');
            idleUpdate = true;
            map.getView().setCenter(getProj(vehicle));
            map.getView().setZoom(appResources.lookMarkerZoom);
        },

        beginTrack: function () {
            deprecateLayerFeatures(trackLayers);
        },
        addTrack: function (locations, id, color, isDashLine) {
            //!!!! RouteSearch
            if (locations == null) { return; }
            //--
            var layer = trackLayers[id];
            if (!layer) {
                trackLayers[id] = layer = createTrackLayer(color, isDashLine);
                //!!!! RouteSearch
                map.addLayer(trackLayers[id]);
                //--
            }

            var feature = mapFindById(trackLayers, id, false);
            if (!feature) {
                layer.getSource().addFeature(feature = new ol.Feature());
            }
            var projs = locations.map(function (val) {
                return getProj(val);
            });
            var line = new ol.geom.LineString(projs);
            feature.setGeometry(line);
            feature.setId(id);
            feature.set('tag', locations);
            feature.set('type', 'track');
            feature.set('deprecated', false);
        },
        endTrack: function () {
            clearDeprecatedLayerFeatures(trackLayers);
        },
        lookTrack: function (index) {
            // TODO:
        },
        /**
         * Look map view to tracks bound.
         * @param {Array<string>|string} id - Array of strings or single string
         * @returns {void} - Nothing
         */
        lookTracks: function (id) {
            // TODO:
        },
        look: function (types) {
            var bounds = null;
            if (types == null) {
                bounds = extendLayersBounds(typedMarkerLayers, bounds);
                bounds = extendLayersBounds(trackLayers, bounds);
                bounds = extendLayersBounds(vehicleMarkerLayers, bounds);
                bounds = extendLayersBounds(stopMarkerLayers, bounds);
            } else {
                var typesArray;
                if (types instanceof Array) {
                    typesArray = id;
                } else {
                    typesArray = [id];
                }

                for (var i = 0; i < typesArray.length; i++) {
                    var type = typesArray[i];
                    var parts = type.split('.');
                    switch (parts[0]) {
                        case 'marker':
                            if (parts.length > 1) {
                                bounds = extendLayersBoundsById(typedMarkerLayers, parts[1], bounds);
                            } else {
                                bounds = extendLayersBounds(typedMarkerLayers, bounds);
                            }
                            break;

                        case 'track':
                            bounds = extendLayersBounds(trackLayers, bounds);
                            break;

                        case 'vehicle':
                            if (parts.length > 1) {
                                bounds = extendLayersBoundsById(vehicleMarkerLayers, parseInt(parts[1]), bounds);
                            } else {
                                bounds = extendLayersBounds(vehicleMarkerLayers, bounds);
                            }
                            break;

                        case 'stop':
                            if (parts.length > 1) {
                                bounds = extendLayersBoundsById(stopMarkerLayers, parseInt(parts[1]), bounds);
                            } else {
                                bounds = extendLayersBounds(stopMarkerLayers, bounds);
                            }
                            break;
                    }
                }
            }

            if (bounds && !ol.extent.isEmpty(bounds)) {
                idleUpdate = true;
                if (ol.extent.getTopLeft(bounds) === ol.extent.getBottomRight(bounds)) {
                    map.getView().setCenter(bounds.getCenter());
                    map.getView().setZoom(appResources.lookMarkerZoom);
                } else {
                    lookAtExtent(bounds);
                }
            }
        },
        toggleApparelTitle: function (isShow) {
            if (_apparelInfoElem) {
                _apparelInfoElem.style.visibility = isShow ? 'visible' : 'hidden';
            } else {
                _apparelInfoElem = document.querySelector('.sws-apparel-map-info');
            }
        },
    };
}]);;
var module = angular.module('net', ['appConfig']);
module.factory('appService', [
    '$q', '$http', '$cacheFactory', 'appSession', 'appConfig', function ($q, $http, $cacheFactory, appSession, appConfig) {
        var cache = $cacheFactory('appService.cache');
        var postContentType = 'application/x-www-form-urlencoded';
        var cacheScheduleDuration = 5 * 60 * 1000;
        var advertisingDuration = 1 * 60 * 1000;
        var cacheDynamicDuration = 9 * 1000;
        var ATTRS_IS_VISIBLE = 1 << 0;
        var ATTRS_IS_FINAL = 1 << 1;
        var ATTRS_IS_SCHEDULE = 1 << 2;
        var Pending = function (deferred) {
            return {
                deferred: deferred
            };
        };
        var routeListCancellationName = 'routeListCancellation';
        var routeInfoCancellationName = 'routeInfoCancellation';
        var trackInfoCancellationName = 'trackInfoCancellation';
        var scheduleCancellationName = 'scheduleCancellation';
        var vehiclesCancellationName = 'vehiclesCancellation';
        var scoreboardCancellationName = 'scoreboardCancellation';
        var schedulesByStopCancellationName = 'schedulesByStopCancellation';
        var stopNamesCancellationName = 'stopNamesCancellation';
        var stopsByNameCancellationName = 'stopsByNameCancellation';
        var stopsNearestCancellationName = 'stopsNearestCancellation';
        var routeSearchCancellationName = 'routeSearchCancellation';
        var advertisingCancellationName = 'advertisingCancellation';
        var cancellations = {
            /** @type {Cancellation} */
            'routeListCancellation': null,
            /** @type {Cancellation} */
            'routeInfoCancellation': null,
            /** @type {Cancellation} */
            'trackInfoCancellation': null,
            /** @type {Cancellation} */
            'scheduleCancellation': null,
            /** @type {Cancellation} */
            'vehiclesCancellation': null,
            /** @type {Cancellation} */
            'scoreboardCancellation': null,
            /** @type {Cancellation} */
            'schedulesByStopCancellation': null,
            /** @type {Cancellation} */
            'stopNamesCancellation': null,
            /** @type {Cancellation} */
            'stopsByNameCancellation': null,
            /** @type {Cancellation} */
            'stopsNearestCancellation': null,
            /** @type {Cancellation} */
            'routeSearchCancellation': null,
            /** @type {Cancellation} */
            'advertisingCancellation': null
        };
        var pendings = {};
        var cancel = function (name) {
            if (cancellations[name] != null) {
                cancellations[name].cancel();
                cancellations[name] = null;
            }
        };
        var internalCancelAll = function () {
            for (var propertyName in cancellations) {
                if (cancellations.hasOwnProperty(propertyName)) {
                    cancel(propertyName);
                }
            }
        };
        var clearCancellation = function (name, current) {
            if (cancellations[name] === current) {
                cancellations[name] = null;
            }
        };
        var clearPending = function (key, pending) {
            var p = pendings[key];
            if (p != null && p === pending) {
                delete pendings[key];
            }
        };
        var processError = function (deferred, result) {
            var msg = {
                'status': result.status,
                'isExpired': function () {
                    return this['status'] === 444;
                },
                'isUnauthorized': function () {
                    return this['status'] === 401;
                }
            };
            switch (result.status) {
                case -1:
                    msg['message'] = appLocale['networkError'];
                    break;

                case 401:
                    msg['message'] = appLocale['unauthorized'];
                    msg['detail'] = appLocale['pageRedirectLoginProcess'];
                    msg['detailIcon'] = 'glyphicon glyphicon-refresh';
                    break;

                case 444:
                    internalCancelAll();
                    msg['message'] = appLocale['sessionExpire'];
                    msg['detail'] = appLocale['pageReloadProcess'];
                    msg['detailIcon'] = 'glyphicon glyphicon-refresh';
                    break;
            }
            deferred.reject(msg);
        };
        var getUrl = function (action, region) {
            if (region === undefined) {
                return appConfig['urlService'] + '/' + action;
            } else {
                return appConfig['urlService'] + '/' + action + '/' + region;
            }
        };
        var isAttr = function (value, attr) {
            return (value & attr) === attr;
        };
        return {
            region: appConfig['region'],
            lang: appConfig['lang'],

            isPendingRouteList: function () {
                return cancellations['routeListCancellation'] != null;
            },
            cancelRouteList: function () {
                cancel(routeListCancellationName);
            },
            getRouteList: function (transportTypeTag) {
                var deferred = $q.defer();
                var url = getUrl('RouteList');
                var params = {
                    p: this.region,
                    tt: transportTypeTag
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelRouteInfo();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[routeListCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var routes = result.data.Routes;
                        var regex = /([0-9]+)/s;
                        var regexLetters = /[^0-9]+/;
                        for (var i = 0; i < routes.length; i++) {
                            var r = routes[i];
                            var parts = regex.exec(r.Number);
                            var symbols = regexLetters.exec(r.Number);
                            r.ShortNumber = Number(parts[0]);
                            r.Symbol = symbols ? symbols[0] : null;
                        }
                        cache.put(key, new CacheItem(routes, time, cacheScheduleDuration));
                        clearCancellation(routeListCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(routes);
                    }, function (result) {
                        clearCancellation(routeListCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            isPendingRouteInfo: function () {
                return routeInfoCancellation != null;
            },
            cancelRouteInfo: function () {
                cancel(routeInfoCancellationName);
            },
            getRouteInfo: function (transportTypeTag, routeNumber) {
                var deferred = $q.defer();
                var url = getUrl('Route');
                var params = {
                    p: this.region,
                    tt: transportTypeTag,
                    r: routeNumber
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelRouteInfo();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[routeInfoCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var trips = result.data.Trips;
                        try {
                            if (trips != null) {
                                appHelper.mergeStops(trips.StopsA, trips.StopNamesA);
                                appHelper.mergeStops(trips.StopsB, trips.StopNamesB);
                                //  another directions:
                                if (trips.OtherDirections.length > 0) {
                                    for (var i = 0; i < trips.OtherDirections.length; i++) {
                                        var otherTrip = trips.OtherDirections[i];
                                        appHelper.mergeStops(otherTrip.Stops, otherTrip.StopNames);
                                    }
                                }
                            }
                            cache.put(key, new CacheItem(result.data, time, cacheScheduleDuration));
                        } catch (error) {
                            try {
                                console.warn(error.stack);
                            } catch (e) {
                                // Empty
                            }
                        }
                        clearCancellation(routeInfoCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(result.data);
                    }, function (result) {
                        clearCancellation(routeInfoCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            isPendingTrackInfo: function () {
                return trackInfoCancellation != null;
            },
            cancelTrackInfo: function () {
                cancel(trackInfoCancellationName);
            },
            getTrackInfo: function (transportTypeTag, routeNumber) {
                var deferred = $q.defer();
                var url = getUrl('Track');
                var params = {
                    p: this.region,
                    tt: transportTypeTag,
                    r: routeNumber
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelTrackInfo();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[trackInfoCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var track = result.data;
                        cache.put(key, new CacheItem(track, time, cacheScheduleDuration));
                        clearCancellation(trackInfoCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(track);
                    }, function (result) {
                        clearCancellation(trackInfoCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            isPendingSchedule: function () {
                return scheduleCancellation != null;
            },
            cancelSchedule: function () {
                cancel(scheduleCancellationName);
            },
            getSchedule: function (transportTypeTag, routeNumber, stopId, routeDir) {
                var deferred = $q.defer();
                var url = getUrl('Schedule');
                var params = {
                    p: this.region,
                    tt: transportTypeTag,
                    r: routeNumber,
                    s: stopId,
                    d: routeDir
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelSchedule();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[scheduleCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var schedule = result.data;
                        cache.put(key, new CacheItem(schedule, time, cacheScheduleDuration));
                        clearCancellation(scheduleCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(schedule);
                    }, function (result) {
                        clearCancellation(scheduleCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },
            getSchedulesByStop: function (dataParam) {
                var deferred = $q.defer();
                var url = getUrl('SchedulesByStop');
                var params = dataParam;
                params.place = this.region;
                params.lang = this.lang;
                var key = url + '?' + $.param(dataParam);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelSchedulesByStop();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[schedulesByStopCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var schedule = result.data;
                        cache.put(key, new CacheItem(schedule, time, cacheScheduleDuration));
                        clearCancellation(schedulesByStopCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(schedule);
                    }, function (result) {
                        clearCancellation(schedulesByStopCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },
            cancelSchedulesByStop: function () {
                cancel(schedulesByStopCancellationName);
            },
            getTripDepartures: function (transportTypeTag, routeNumber, stopId, routeDir, selectedStopDepartureTime, dayOfWeek) {
                var ssdt = selectedStopDepartureTime;
                var deferred = $q.defer();
                var url = getUrl('TripDepartures');
                var params = {
                    p: this.region,
                    tt: transportTypeTag,
                    r: routeNumber,
                    s: stopId,
                    d: routeDir,
                    dt: ssdt,
                    wd: dayOfWeek
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelSchedule();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[scheduleCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var schedule = result.data;
                        cache.put(key, new CacheItem(schedule, time, cacheScheduleDuration));
                        clearCancellation(scheduleCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(schedule);
                    }, function (result) {
                        clearCancellation(scheduleCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            isPendingVehicles: function () {
                return vehiclesCancellation != null;
            },
            cancelVehicles: function () {
                cancel(vehiclesCancellationName);
            },
            getVehicles: function (transportTypeTag, routeNumber) {
                var deferred = $q.defer();
                if (appConfig.disableVehicles) {
                    deferred.resolve({
                        Vehicles: []
                    });
                    return deferred.promise;
                }
                var url = getUrl('Vehicles');
                var params = {
                    p: this.region,
                    tt: transportTypeTag,
                    r: routeNumber
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelVehicles();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    params.v = appSession.v(parseInt(routeNumber));
                    var cancellation = new Cancellation($q, key);
                    cancellations[vehiclesCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var vehicles = result.data.Vehicles;
                        cache.put(key, new CacheItem(vehicles, time, cacheDynamicDuration));
                        clearCancellation(vehiclesCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(vehicles);
                    }, function (result) {
                        clearCancellation(vehiclesCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            isPendingScoreboard: function () {
                return scoreboardCancellation != null;
            },
            cancelScoreboard: function () {
                cancel(scoreboardCancellationName);
            },
            getScoreboard: function (stopId) {
                var deferred = $q.defer();
                var url = getUrl('Scoreboard');
                var params = {
                    p: this.region,
                    s: stopId
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelScoreboard();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    params.v = appSession.v(parseInt(stopId));
                    var cancellation = new Cancellation($q, key);
                    cancellations[scoreboardCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var scoreboard = result.data;
                        if (scoreboard != null) {
                            scoreboard.Timestamp = appHelper.parseDate(scoreboard.Timestamp);
                            scoreboard.Time = DateFormat.format.date(scoreboard.Timestamp, "HH:mm");

                            var routes = scoreboard.Routes;
                            for (var i = 0; i < routes.length; i++) {
                                var info = routes[i].Info;
                                var attrs = routes[i].Attrs;
                                var finfo = [
                                    appHelper.formatDepart(info[0]),
                                    appHelper.formatDepart(info[1])
                                ];
                                routes[i].fInfo = finfo;
                                routes[i].fAttrs = [
                                    {
                                        isSchedule: isAttr(attrs[0], ATTRS_IS_SCHEDULE)
                                    },
                                    {
                                        isSchedule: isAttr(attrs[1], ATTRS_IS_SCHEDULE)
                                    }
                                ];
                                routes[i].isArrived = info[0] === 'A' || info[1] === 'A';
                            }
                        }

                        cache.put(key, new CacheItem(scoreboard, time, cacheDynamicDuration));
                        clearCancellation(scoreboardCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(scoreboard);
                    }, function (result) {
                        clearCancellation(scoreboardCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            cancelStopNames: function () {
                cancel(stopNamesCancellationName);
            },

            getStopNames: function (tag) {
                var deferred = $q.defer();
                var url = getUrl('StopNames');
                var params = {
                    p: this.region,
                    t: tag
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelStopNames();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[stopNamesCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var data = result.data;
                        data.tag = tag;
                        cache.put(key, new CacheItem(data, time, cacheScheduleDuration));
                        clearCancellation(stopNamesCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(data);
                    }, function (result) {
                        clearCancellation(stopNamesCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            getStopName: function (stopId) {
                var deferred = $q.defer();
                var url = getUrl('StopName');
                var params = {
                    stopid: stopId
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelStopNames();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[stopNamesCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var data = result.data;
                        data.tag = stopId;
                        cache.put(key, new CacheItem(data, time, cacheScheduleDuration));
                        clearCancellation(stopNamesCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(data);
                    }, function (result) {
                        clearCancellation(stopNamesCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            cancelStopsByName: function () {
                cancel(stopsByNameCancellationName);
            },

            getStopsByName: function (name) {
                var deferred = $q.defer();
                var url = getUrl('StopsByName');
                var params = {
                    p: this.region,
                    n: name
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelStopsByName();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[stopsByNameCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var data = result.data;
                        var stops = data.Stops;
                        if (stops != null) {
                            for (var i = 0; i < data.Stops.length; i++) {
                                stops[i].Name = name;
                            }
                        }
                        data.name = name;
                        data.stops = appHelper.doSelect(data.Stops, function (item) {
                            return {
                                id: item.Id,
                                lat: item.Latitude,
                                lon: item.Longitude,
                                bearing: item.Bearing,
                                name: item.Name
                            };
                        });
                        cache.put(key, new CacheItem(data, time, cacheScheduleDuration));
                        clearCancellation(stopsByNameCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(data);
                    }, function (result) {
                        clearCancellation(stopsByNameCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            cancelStopsNearest: function () {
                cancel(stopsNearestCancellationName);
            },
            getStopsNearest: function (lat, lon) {
                var deferred = $q.defer();
                var url = getUrl('NearestStops');
                var params = {
                    p: this.region,
                    lat: lat,
                    lon: lon
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelStopsNearest();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[stopsNearestCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var data = result.data;
                        data.name = name;
                        try {
                            var myLoc = new window.google.maps.LatLng(lat, lon);
                            for (var i = 0; i < data.name.Stops.length; i++) {
                                var stop = data.name.Stops[i];
                                var loc = new window.google.maps.LatLng(stop.Latitude, stop.Longitude);
                                stop.distance = window.google.maps.geometry.spherical.computeDistanceBeetween(myLoc, loc);
                            }
                        } catch (error) {
                            // Empty
                        }
                        cache.put(key, new CacheItem(data, time, cacheDynamicDuration));
                        clearCancellation(stopsNearestCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(data);
                    }, function (result) {
                        clearCancellation(stopsNearestCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            cancelRouteSearch: function () {
                cancel(routeSearchCancellationName);
            },
            getRouteSearch: function (params) {
                var deferred = $q.defer();
                var url = getUrl('RouteSearch', this.region);
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelRouteSearch();

                    var time = Date.now();
                    params.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                    var cancellation = new Cancellation($q, key);
                    cancellations[routeSearchCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': 'application/x-www-form-urlencoded'
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        var data = result.data;
                        var pathes = data.pathes;
                        data.time = appHelper.parseDate(data.time);
                        if (pathes != null) {
                            for (var i = 0; i < pathes.length; i++) {
                                var path = pathes[i];
                                var actions = path.actions;
                                var dataTime = data.time;
                                var totalDuration = 0;
                                var toRemove = [];
                                path.routes = [];
                                path.stops = [];
                                for (var j = 0; j < actions.length; j++) {
                                    var action = actions[j];
                                    dataTime += action.dur * 60 * 1000;
                                    totalDuration += action.dur;
                                    action.timeStr = DateFormat.format.date(dataTime, "HH:mm");
                                    if (action.si >= 0) {
                                        var stop = data.stops[action.si];
                                        action.stop = stop;
                                        appHelper.addIfNew(path.stops, stop, function (value) {
                                            return stop.id != 0 && value.id == stop.id;
                                        });
                                    }
                                    if (action.ri >= 0) {
                                        var route = data.routes[action.ri];
                                        action.route = route;
                                        appHelper.addIfNew(path.routes, route, function (value) {
                                            return value.type == route.type && value.num == route.num;
                                        });
                                    }

                                    switch (action.type) {
                                        case 'waiting':
                                            if (action.dur === 0) {
                                                toRemove.push(j);
                                            }
                                            break;

                                        case 'none':
                                            if (j === actions.length - 1) {
                                                toRemove.push(j);
                                            }
                                            break;
                                    }
                                }

                                toRemove = toRemove.reverse();
                                for (var k = 0; k < toRemove.length; k++) {
                                    actions.splice(toRemove[k], 1);
                                }

                                var toMerge = [];
                                var lastTypeChange = null;
                                var lastTypeChangeIndex = -1;
                                var sameDuration = 0;
                                for (var h = 0; h < actions.length; h++) {
                                    if (lastTypeChange == null || lastTypeChange.type != actions[h].type) {
                                        var sameCount = h - lastTypeChangeIndex;
                                        if (sameCount > 1 && lastTypeChange.type == 'walking') {
                                            toMerge.push({ i: lastTypeChangeIndex, count: sameCount, duration: sameDuration });
                                        }
                                        lastTypeChange = actions[h];
                                        lastTypeChangeIndex = h;
                                        sameDuration = 0;
                                    }

                                    sameDuration += actions[h].dur;

                                    if (h > 0) {
                                        actions[h].prev = actions[h - 1];
                                    } else {
                                        actions[h].prev = null;
                                    }
                                    if (h < actions.length - 1) {
                                        actions[h].next = actions[h + 1];
                                    }
                                }

                                toMerge = toMerge.reverse();
                                for (var m = 0; m < toMerge.length; m++) {
                                    actions[toMerge[m].i + toMerge[m].count - 1].dur = toMerge[m].duration;
                                    actions.splice(toMerge[m].i, toMerge[m].count - 1);
                                }

                                path.duration = totalDuration;

                                path.routeTypes = [];
                                var types = appHelper.distinct(appHelper.select(path.routes, 'type'));
                                for (var o = 0; o < types.length; o++) {
                                    path.routeTypes.push({
                                        type: types[o],
                                        routes: appHelper.where(path.routes, 'type', types[o])
                                    });
                                }
                            }
                        }

                        cache.put(key, new CacheItem(data, time, cacheDynamicDuration));
                        clearCancellation(routeSearchCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(data);
                    }, function (result) {
                        clearCancellation(routeSearchCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            cancelAdvertising: function () {
                cancel(advertisingCancellationName);
            },
            getAdvertisingCached: function (category) {
                var url = getUrl('Ads');
                var params = {
                    cat: category
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item != null) {
                    return item.data;
                }
                return null;
            },
            getAdvertising: function (category) {
                var deferred = $q.defer();
                var url = getUrl('Ads');
                var params = {
                    p: this.region,
                    c: category
                };
                var key = url + '?' + $.param(params);
                var item = cache.get(key);
                if (item && !item.isExpired()) {
                    deferred.resolve(item.data);
                } else {
                    var pending = pendings[key];
                    if (pending != null) {
                        return pending.deferred.promise;
                    } else {
                        pending = new Pending(deferred);
                        pendings[key] = pending;
                    }
                    this.cancelAdvertising();

                    var time = Date.now();
                    var cancellation = new Cancellation($q, key);
                    cancellations[advertisingCancellationName] = cancellation;
                    $http({
                        url: url,
                        method: 'POST',
                        headers: {
                            'Accept-Language': this.lang,
                            'Content-Type': postContentType
                        },
                        data: $.param(params),
                        timeout: cancellation.promise
                    }).then(function (result) {
                        if (pendings[key] !== undefined) {
                            delete pendings[key];
                        }
                        var data = result.data;
                        cache.put(key, new CacheItem(data, time, advertisingDuration));
                        clearCancellation(advertisingCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        deferred.resolve(data);
                    }, function (result) {
                        if (pendings[key] !== undefined) {
                            delete pendings[key];
                        }
                        clearCancellation(advertisingCancellationName, cancellation);
                        clearPending(key, pending);
                        if (cancellation.isCancelled) return;

                        processError(deferred, result);
                    });
                }
                return deferred.promise;
            },

            cancelAll: function () {
                internalCancelAll();
            },

            // Helpers
            getLocationByNavigator: function (onGotPosition, onError) {
                var callOnError = function (errorName, code) {
                    var error = {
                        message: appLocale[errorName],
                        code: code
                    };
                    onError(error);
                };
                if (navigator.geolocation) {
                    navigator.geolocation.getCurrentPosition(function (position) {
                        var lat = position.coords.latitude;
                        var lon = position.coords.longitude;
                        onGotPosition(lat, lon);
                    }, function (error) {
                        switch (error.code) {
                            case error.PERMISSION_DENIED:
                                callOnError('geolocationDenied', error.code);
                                break;
                            case error.POSITION_UNAVAILABLE:
                                callOnError('geolocationUnavailable', error.code);
                                break;
                            case error.TIMEOUT:
                                callOnError('geolocationTimeout', error.code);
                                break;
                            default:
                                callOnError('geolocationErrorUnknown', error.code);
                                break;
                        }
                    });
                } else {
                    callOnError('geolocationNotSupported', 0);
                }
            },
            getStopsByNavigator: function (onGotPosition, onChange) {
                var service = this;
                onChange('locationRequest', null);
                service.getLocationByNavigator(function (lat, lon) {
                    onGotPosition(lat, lon);
                    onChange('nearestStopsLoading', null);
                    service.getStopsNearest(this.region, lat, lon).then(function (data) {
                        onChange('result', data['Stops']);
                    });
                }, function (state) {
                    var error = {
                        message: appLocale[state]
                    };
                    if (error.message == null) {
                        error.message = appLocale['error'];
                    }
                    onChange('error', error);
                });
            }
        };
    }
]);
;
var app = angular.module('appMain', ['ngRoute', 'ngAnimate', 'appExt', 'appConfig', 'map', 'net', 'ngCookies']);
app.config(['$locationProvider', function ($locationProvider) {
    $locationProvider.hashPrefix('');
}]);
app.config(['$routeProvider', '$locationProvider', '$provide', 'appConfig',
    function ($routeProvider, $locationProvider, $provide, appConfig) {
        $routeProvider
            .when('/', {
                redirectTo: '/routes/' + appConfig['defaultTransportType']
            })
            .when('/routes/:transportType', {
                controller: 'RouteListCtrl',
                templateUrl: appConfig['urlTemplate'] + '/RouteList'
            })
            .when('/routes/:transportType/:routeNumber', {
                controller: 'RouteInfoCtrl',
                templateUrl: appConfig['urlTemplate'] + '/RouteInfo',
                isMap: false
            })
            .when('/routes/:transportType/:routeNumber/map', {
                controller: 'RouteInfoCtrl',
                templateUrl: appConfig['urlTemplate'] + '/RouteInfo',
                isMap: true
            })
            .when('/routes/:transportType/:routeNumber/stops/:stopId/:routeDir?', {
                controller: 'RouteStopInfoCtrl',
                templateUrl: appConfig['urlTemplate'] + '/RouteStopInfo',
                isMap: false
            })
            .when('/routes/:transportType/:routeNumber/stops/:stopId/:routeDir?/map', {
                controller: 'RouteStopInfoCtrl',
                templateUrl: appConfig['urlTemplate'] + '/RouteStopInfo',
                isMap: true
            })
            .when('/stops/search', {
                controller: 'StopSearchByNameCtrl',
                templateUrl: appConfig['urlTemplate'] + '/StopSearch',
                reloadOnSearch: false
            })
            .when('/stops/names', {
                redirectTo: '/stops/search'
            })
            .when('/stops/names/:stopName*', {
                controller: 'StopSearchInfoCtrl',
                templateUrl: appConfig['urlTemplate'] + '/StopSearchInfo'
            })
            .when('/stops/nearest', {
                controller: 'StopNearestCtrl',
                templateUrl: appConfig['urlTemplate'] + '/StopNearest',
                isMap: false
            })
            .when('/stops/nearest/map', {
                controller: 'StopNearestCtrl',
                templateUrl: appConfig['urlTemplate'] + '/StopNearest',
                isMap: true
            })
            .when('/stops/nearest/:stopId/:lat/:lon', {
                controller: 'StopNearestInfoCtrl',
                templateUrl: appConfig['urlTemplate'] + '/StopNearestInfo',
                isMap: false
            })
            .when('/stops/nearest/:stopId/:lat/:lon/map', {
                controller: 'StopNearestInfoCtrl',
                templateUrl: appConfig['urlTemplate'] + '/StopNearestInfo',
                isMap: true
            })
            .when('/stops/:stopId', {
                controller: 'StopInfoCtrl',
                templateUrl: appConfig['urlTemplate'] + '/StopInfo',
                isMap: false
            })
            .when('/stops/:stopId/map', {
                controller: 'StopInfoCtrl',
                templateUrl: appConfig['urlTemplate'] + '/StopInfo',
                isMap: true
            })
            .otherwise({
                redirectTo: '/'
            });

        var errorDuration = 60;
        var errorTick = 1000;
        var baseMapTransition = {
            'vehicle': ['transportType', 'routeNumber'],
            'track': ['transportType', 'routeNumber'],
            'stop': ['transportType', 'routeNumber']
        };
        var anyMapTransition = { 'stop': [] };
        var stopMapTransition = { 'stop': ['lat', 'lon'], 'marker.source': ['lat', 'lon'] };
        $provide.value('moduleConf',
        {
            errorDuration: errorDuration,
            errorTick: errorTick,
            errorStep: errorTick / 1000.0 / errorDuration,
            checkScheduleTime: 30 * 60 * 1000,
            straightColor: '#4d67ac',
            reverseColor: '#d24a43',
            customColor: '#483d8b',
            mapTransitions: {
                'RouteInfoCtrl->RouteStopInfoCtrl': baseMapTransition,
                'RouteInfoCtrl->RouteInfoCtrl': baseMapTransition,
                'RouteStopInfoCtrl->RouteInfoCtrl': baseMapTransition,
                'RouteStopInfoCtrl->RouteStopInfoCtrl': baseMapTransition,
                'StopNearestCtrl->StopNearestInfoCtrl': anyMapTransition,
                'StopNearestInfoCtrl->StopNearestInfoCtrl': stopMapTransition
            }
        });
    }]);
app.factory('page', function () {
    return {
        'name': '',
        'link': '#/',
        /**
         * @type {Array<{name,link,class}>}
         */
        'uplinks': [],

        templates: {
            getDefaultRoutesUplink: function ($scope, $interpolate) {
                return {
                    'name': appLocale['routeList'],
                    'link': $interpolate('#/routes/{{::transportType.Tag}}')($scope)
                };
            },
            getRoutesUplinks: function ($scope, $interpolate) {
                return {
                    'name': appLocale['routeList'],
                    'link': $interpolate('#/routes/{{::transportType.Tag}}')($scope)
                };
            },
            getIsMapLinks: function ($scope, $interpolate, baseLink, columns) {
                if ($scope['isMap']) {
                    return {
                        'name': appLocale["hideMap"],
                        'link': $interpolate(baseLink)($scope),
                        'gClass': columns === 1 || columns === 2 ? 'hidden-xs-2 hidden-sm hidden-md hidden-lg' : 'hidden-md hidden-lg',
                        'iconClass': 'glyphicon glyphicon-globe'
                    };
                } else {
                    return {
                        'name': appLocale['showOnMap'],
                        'link': $interpolate(baseLink + '/map')($scope),
                        'gClass': columns === 1 || columns === 2 ? ' hidden-xs-2 hidden-sm hidden-md hidden-lg' : 'hidden-md hidden-lg',
                        'iconClass': 'glyphicon glyphicon-globe'
                    };
                }
            }
        }
    };
});
app.factory('sharedData', function () {
    return {};
});
app.run(['$rootScope', function ($rootScope) {
    $rootScope.collapseMenu = function () {
        $('.navmenu').offcanvas('hide');
    };
}]);
// Ads
app.factory('adService', ['$rootScope', function ($rootScope) {
    return {
        'places': {},
        'isHidden': false,
        hide: function () {
            this['isHidden'] = true;
        },
        show: function () {
            this['isHidden'] = false;
        },
        getPlace: function (name) {
            var places = this['places'];
            var place = places[name];
            if (place == null) {
                place = {
                    // Empty
                };
                places[name] = place;
            }
            return place;
        },
        has: function (name) {
            var place = this.getPlace(name);
            if (place == null) {
                return false;
            }

            var isVisible = place['isVisible'];
            if (isVisible === undefined) {
                return false;
            }

            return isVisible;
        }
    };
}]);
app.controller('AppAdsCtrl', [
    '$scope', '$interval', '$timeout', 'appConfig', 'appService', 'adService',
    function ($scope, $interval, $timeout, appConfig, appService, adService) {
        $scope['ctx'] = adService;

        var AdsContainer = function () {
            var getRandomInt = function (min, max) {
                return Math.floor(Math.random() * (max - min + 1)) + min;
            };
            return {
                'list': [],
                'curIndex': 0,
                'curChangeTime': 0,
                'lastChangeTime': 0,

                'updatePlace': function () {
                    var placeNameU = $scope['place'];
                    if (placeNameU == null) {
                        return;
                    }
                    adService.getPlace(placeNameU)['isVisible'] = this['list'] != null && this['list'].length > 0 && !adService["isHidden"];
                },
                'has': function () {
                    var list = this['list'];
                    return list.length > 0;
                },
                'remainTime': function () {
                    return this['lastChangeTime'] + this['curChangeTime'] - new Date().getTime();
                },
                'isValidRemainTime': function (time) {
                    return $.isNumeric(time) && time > 0 && time <= this['curChangeTime'];
                },
                'update': function (data) {
                    if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                        console.debug(new Date().toLocaleTimeString() + ': update');
                    }
                    if (data == null || !$.isArray(data['a'])) {
                        this['list'] = [];
                        this['curIndex'] = 0;
                        return;
                    }
                    var list = data['a'];
                    if (this['list'] !== list) {
                        this['list'] = list;
                        var changeTime;
                        if (list != null && list.length > 0) {
                            var nextIndex = getRandomInt(0, list.length - 1);
                            this['curIndex'] = nextIndex;
                            var ad = list[nextIndex];
                            changeTime = ad['t'];
                            if (changeTime == null || changeTime < 1000) {
                                changeTime = 1000;
                            }
                        } else {
                            this['curIndex'] = 0;
                            changeTime = 5000;
                        }
                        this['curChangeTime'] = changeTime;
                        this['lastChangeTime'] = new Date().getTime();
                        this['updatePlace']();
                    }
                },
                'change': function () {
                    if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                        console.debug(new Date().toLocaleTimeString() + ': change');
                    }
                    var possibleNextIndex = Math.abs(this['curIndex']) + 1;
                    var list = this['list'];
                    var nextIndex = possibleNextIndex % list.length;
                    this['curIndex'] = nextIndex;
                    var ad = list[nextIndex];
                    var changeTime = ad['t'];
                    if (changeTime == null || changeTime < 1000) {
                        changeTime = 1000;
                    }
                    this['curChangeTime'] = changeTime;
                    this['lastChangeTime'] = new Date().getTime();
                },
                'save': function () {
                    if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                        console.debug(new Date().toLocaleTimeString() + ': save');
                    }
                    var place = $scope['place'];
                    if (place != null) {
                        var statePlace = adService.getPlace(place);
                        statePlace['lastChangeTime'] = this['lastChangeTime'];
                        statePlace['remainTime'] = this['remainTime']();
                        statePlace['curIndex'] = this['curIndex'];
                    }
                },
                'restore': function (data) {
                    if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                        console.debug(new Date().toLocaleTimeString() + ': restore');
                    }
                    var place = $scope['place'];
                    if (place == null) {
                        return;
                    }
                    if (data !== undefined) {
                        this['update'](data);
                    } else {
                        this['update'](appService.getAdvertisingCached());
                    }
                    var restoreStateObj = adService.getPlace(place);
                    if (restoreStateObj['curIndex'] != null) {
                        var index = restoreStateObj['curIndex'];
                        this['curIndex'] = index;
                        var list = this['list'];
                        var changeTime = 0;
                        if (index < list.length) {
                            changeTime = list[index]['t'];
                        }
                        this['curChangeTime'] = changeTime;
                    }
                    if (restoreStateObj['lastChangeTime'] != null) {
                        this['lastChangeTime'] = restoreStateObj['lastChangeTime'];
                    }
                }
            };
        };
        $scope['ads'] = new AdsContainer();

        var checkUpdatePromise = undefined;
        var destroyCheckUpdatePromise = function () {
            if (checkUpdatePromise != null) {
                $interval.cancel(checkUpdatePromise);
                checkUpdatePromise = undefined;
            }
        };

        var transitionPromise = undefined;
        var destroyTransitionPromise = function () {
            if (transitionPromise != null) {
                $timeout.cancel(transitionPromise);
                transitionPromise = undefined;
            }
        };
        var startTransitionPromise = function () {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                console.debug(new Date().toLocaleTimeString() + ': startTransitionPromise');
            }
            var ads = $scope['ads'];
            if (!ads['has']()) {
                return;
            }

            var remainTime = ads['remainTime']();
            if (!ads['isValidRemainTime'](remainTime)) {
                ads['change']();
                remainTime = ads['remainTime']();
            }
            transitionPromise = $timeout(function () {
                $scope['ads']['change']();
                startTransitionPromise();
            }, remainTime);
        };

        var restoreState = function (data) {
            $scope['ads']['restore'](data);
            destroyTransitionPromise();
            startTransitionPromise();
        };

        // Called from directive
        $scope['restoreState'] = restoreState;

        var checkUpdate = function (isUpdate) {
            appService.getAdvertising().then(function (data) {
                //updateAdvertising(data);
                if (isUpdate !== undefined && isUpdate) {
                    $scope['ads']['save']();
                }
                restoreState(data);
            }, function (error) {
                // Empty
            });
        };

        checkUpdate();
        checkUpdatePromise = $interval(function () {
            checkUpdate(true);
        }, 5 * 60 * 1000);

        $scope.$on('$destroy', function () {
            appService.cancelAdvertising();
            destroyTransitionPromise();
            destroyCheckUpdatePromise();
        });
        $scope.$on('$routeChangeStart', function () {
            $scope['ads']['save']();
        });
    }]);
app.directive('ngAds', ['appService', function (appService) {
    return {
        controller: 'AppAdsCtrl',
        link: function ($scope, element, attrs, ctrls) {
            var place = attrs.ngAdsPlacement;
            $scope['place'] = place;
            $scope['restoreState']();
        },
        template:
            '<div>' +
                '<div class="advertising-1 animate-if" ng-if="!ctx.isHidden && ads.list.length>0">' +
                    '<figure ng-repeat="ad in ads.list track by $index" ng-class="{\'show\': ads.curIndex===$index, \'hide\': ads.curIndex!==$index}">' +
                        '<a ng-if="ad.u !== null && ad.u.length > 0" " href="{{ad.u}}">' +
                            '<img class="center-block" ng-src="{{ad.i}}"/>' +
                        '</a>' +
                        '<img ng-if="ad.u == null || ad.u.length === 0" class="center-block" ng-src="{{ad.i}}"/>' +
                    '</figure>' +
                '</div>' +
            '</div>'
    };
}]);
// App Controllers
app.controller('NavMenuCtl', ['$scope', '$window', 'appConfig', 'page',
    function ($scope, $window, appConfig, page) {
        $scope['region'] = appConfig['region'];
        $scope['regions'] = appConfig['regions'];
        $scope['page'] = page;
        $scope['setCulture'] = function (culture) {
            $window.location.href = appConfig['urlSetCulture'] +
                '?culture=' + encodeURIComponent(culture) +
                '&returnUrl=' + encodeURIComponent($window.location.href);
        };
    }
]);
app.controller('RouteListCtrl', [
    '$scope', '$routeParams', 'appConfig', 'mapService', 'page', 'adService',
    function ($scope, $routeParams, appConfig, map, page, adService) {
        var adPlaceName = 'bottom-1';
        $scope['adBottom1'] = adService.getPlace(adPlaceName);
        $scope['locale'] = appLocale;
        $scope['transportTypes'] = appConfig['transportTypes'];
        $scope['transportType'] = appHelper.grepFirst($scope['transportTypes'], function (elem) {
            return elem.Tag === $routeParams['transportType'];
        });
        map.setCol(2);

        page['name'] = appLocale['routeList'];
        page['type'] = 'Route';
        page['uplinks'] = [];
        page['links'] = [];
    }
]);
app.controller('RouteInfoCtrl', [
    '$scope', '$routeParams', '$route', '$location', '$interpolate', '$q', 'appConfig', 'mapService', 'page', 'adService',
    function ($scope, $routeParams, $route, $location, $interpolate, $q, appConfig, map, page, adService) {
        var adPlaceName = 'bottom-1';
        $scope['adBottom1'] = adService.getPlace(adPlaceName);
        $scope['locale'] = appLocale;
        $scope['routeNumber'] = $routeParams['routeNumber'];
        $scope['transportTypes'] = appConfig['transportTypes'];
        $scope['transportType'] = appHelper.grepFirst($scope['transportTypes'], function (elem) {
            return elem.Tag === $routeParams['transportType'];
        });
        $scope['trackDeferred'] = $q.defer();
        $scope['trackDeferred'].promise.then(function () {
            map.look();
        });
        $scope['isMap'] = $route.current.$$route.isMap;

        var columns = 2;
        map.on('stop', 'click', function (stop) {
            $scope.$apply(function () {
                $location.path('/routes/' + $scope['transportType'].Tag + '/' + $scope['routeNumber'] + '/stops/' + stop.id + '/' + stop.tripType);
            });
        });
        map.setCol(columns);

        page['name'] = appLocale['routeInfo'];
        page['type'] = 'Route';
        page['uplinks'] = [
            page.templates.getRoutesUplinks($scope, $interpolate)
        ];
        page['links'] = [
            page.templates.getIsMapLinks($scope,
                $interpolate,
                '#/routes/{{::transportType.Tag}}/{{::routeNumber}}',
                columns)
        ];
    }
]);
app.controller('RouteStopInfoCtrl', [
    '$scope', '$routeParams', '$route', '$interpolate', '$location', '$q', 'mapService', 'appConfig', 'page', 'adService', 'appService',
    function ($scope, $routeParams, $route, $interpolate, $location, $q, map, appConfig, page, adService, appService) {
        $scope['stopId'] = Number($routeParams['stopId']);

        appService.getStopName($scope['stopId']).then(function (data) {
            $scope['StopName'] = data.Name;
        });

        var adPlaceName = 'bottom-1';
        $scope['adBottom1'] = adService.getPlace(adPlaceName);
        $scope['locale'] = appLocale;
        $scope['routeNumber'] = $routeParams['routeNumber'];
        $scope['transportTypes'] = appConfig['transportTypes'];
        $scope['transportType'] = appHelper.grepFirst($scope['transportTypes'], function (elem) {
            return elem.Tag === $routeParams['transportType'];
        });

        $scope['isMap'] = $route.current.$$route.isMap;
        $scope['shared'] = {
            'routeDir': Number($routeParams['routeDir']),
            'routeDirDeferred': $q.defer()
        };

        var columns = 3;
        map.on('stop', 'click', function (stop) {
            $scope.$apply(function () {
                $location.path('/routes/' + $scope['transportType'].Tag + '/' + $scope['routeNumber'] + '/stops/' + stop.id + '/' + stop.tripType);
            });
        });
        map.setCol(columns);

        page['name'] = appLocale['routeInfo'];
        page['type'] = 'Route';
        page['uplinks'] = [
            page.templates.getRoutesUplinks($scope, $interpolate),
            {
                name: appLocale['routeInfo'],
                link: $interpolate('#/routes/{{::transportType.Tag}}/{{::routeNumber}}')($scope)
            }
        ];
        page['links'] = [
            page.templates.getIsMapLinks($scope,
                $interpolate,
                '#/routes/{{::transportType.Tag}}/{{::routeNumber}}/stops/{{::stopId}}/{{::shared.routeDir}}',
                columns)
        ];
    }
]);
app.controller('StopSearchByNameCtrl', [
    '$scope', '$location', 'appService', 'mapService', 'page', 'adService',
    function ($scope, $location, appService, map, page, adService) {
        var adPlaceName = 'bottom-1';
        $scope['adBottom1'] = adService.getPlace(adPlaceName);
        $scope['locale'] = appLocale;
        $scope['name'] = appLocale['stopSearchByName'];
        $scope['namesState'] = 'listen';
        $scope['searchStopName'] = function (newValue) {
            if (arguments.length === 0) {
                // as getter
                return $scope['stopName'];
            }

            $location.search({ name: newValue });
            var tag = appHelper.getStopSearchTag(newValue);
            if ($scope['tag'] !== tag) {
                $scope['stopNames'] = [];
                $scope['tag'] = tag;
            }
            if (tag != null) {
                $scope['namesState'] = 'namesLoading';
                appService.getStopNames(tag).then(function (data) {
                    if (data['tag'] === tag) {
                        if (tag !== null && tag.length === 0) {
                            $scope['namesState'] = 'namesListen';
                        } else if (data['Names'] == null || data['Names'].length === 0) {
                            if (tag.length < 3)
                                $scope['namesState'] = 'StopSearchCharsLimit';
                            else
                                $scope['namesState'] = 'matchNotFound';
                        } else {
                            var stopNames = data['Names'];
                            stopNames.sort(function (a, b) {
                                if (a == null) {
                                    if (b == null) {
                                        return 0;
                                    } else {
                                        return -1;
                                    }
                                }
                                if (b == null) {
                                    return 1;
                                }
                                var alc = a.toLowerCase();
                                var blc = b.toLowerCase();
                                var c;
                                if (alc.startsWith(tag)) {
                                    if (blc.startsWith(tag)) {
                                        c = alc.localeCompare(blc);
                                        if (c === 0) {
                                            return a.localeCompare(b);
                                        }
                                        return c;
                                    } else {
                                        return -1;
                                    }
                                }
                                if (blc.startsWith(tag)) {
                                    return 1;
                                }
                                c = alc.localeCompare(blc);
                                if (c === 0) {
                                    return a.localeCompare(b);
                                }
                                return c;
                            });
                            $scope['stopNames'] = stopNames;
                            $scope['namesState'] = 'result';
                        }
                    }
                });
            } else {
                $scope['namesState'] = 'namesListen';
            }
            return $scope['stopName'] = newValue;
        };

        var searchName = $location.search().name;
        if (searchName == undefined) {
            searchName = '';
        }
        $scope['searchStopName'](searchName);

        var columns = 2;
        map.setCol(columns);

        page['name'] = appLocale['searchByName'];
        page['type'] = 'StopNames';
        page['uplinks'] = [];
        page['links'] = [];

        $scope.$on('$routeUpdate', function () {
            var searchName2 = $location.search().name;
            if (searchName2 == undefined) {
                searchName2 = '';
            }
            if ($scope['searchStopName']() !== searchName2) {
                $scope["searchStopName"](searchName2);
            }
        });
    }
]);
app.controller('StopSearchInfoCtrl', [
    '$scope', '$routeParams', '$location', 'appService', 'appConfig', 'mapService', 'page', 'adService',
    function ($scope, $routeParams, $location, appService, appConfig, map, page, adService) {
        var adPlaceName = 'bottom-1';
        $scope['adBottom1'] = adService.getPlace(adPlaceName);
        $scope['locale'] = appLocale;
        $scope['stopsState'] = 'loading';
        appService.getStopsByName($routeParams['stopName']).then(function (data) {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                console.debug("OK getStopsByName");
            }
            var stops = data['Stops'];
            $scope['stops'] = stops;
            $scope['stopsState'] = 'result';
            if (stops.length === 1) {
                var stop = stops[0];
                $location.path('/stops/' + stop.Id).replace();
            } else {
                var mapStops = [];
                map.beginStops();
                for (var i = 0; i < stops.length; i++) {
                    var s = stops[i];
                    mapStops.push({
                        id: s.Id,
                        lat: s.Latitude,
                        lon: s.Longitude,
                        bearing: s.Bearing,
                        tripType: map.STOP_TYPE_CUSTOM,
                        name: s.Name
                    });
                    map.addStops(mapStops);
                }
                map.endStops();
                map.lookStops();
            }
        }, function (error) {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                console.debug("ERR getStopsByName");
            }
            $scope['stopsError'] = error;
            $scope['stopsState'] = 'error';
        });

        var columns = 1;
        map.on('stop', 'click', function (stop) {
            $scope.$apply(function () {
                $location.path('/stops/' + stop.id);
            });
        });
        map.setCol(columns);

        page['name'] = appLocale['sameStops'];
        page['type'] = 'StopNames';
        page['uplinks'] = [];
        page['links'] = [];
    }
]);
app.controller('StopInfoCtrl', [
    '$scope', '$q', '$routeParams', '$route', '$interpolate', 'appConfig', 'mapService', 'page', 'adService', 'appService', '$location', '$cookies', '$timeout',
    function ($scope, $q, $routeParams, $route, $interpolate, appConfig, map, page, adService, appService, $location, $cookies) {
        // scope data
        var adPlaceName = 'bottom-1';
        $scope['langFromCookie'] = $cookies.get('sws_lang');
        $scope['adBottom1'] = adService.getPlace(adPlaceName);
        $scope['locale'] = appLocale;
        $scope['routeNumber'] = $routeParams['routeNumber'];
        $scope['transportTypes'] = appConfig['transportTypes'];
        $scope['stopId'] = Number($routeParams['stopId']);
        $scope['isMap'] = $route.current.$$route.isMap;
        // share methods
        $scope['decodeTransportTypeToTag'] = appHelper.decodeTransportTypeToTag;
        $scope['decodeDaysOfWeek'] = appHelper.decodeDaysOfWeek;
        $scope['transportType'] = appHelper.grepFirst($scope['transportTypes'], function (elem) {
            return elem['Tag'] === $routeParams['transportType'];
        });
        $scope['getRouteType'] = function (row) {
            $location.path('/routes/' + appHelper.transportTypeToTag(row.Type) + '/' + row.Number + '/stops/' + $scope['stopId']);
        };
        // for query scoreboard data
        $scope['shared'] = {
            'scoreboardDeferred': $q.defer()
        };

        /**
         * Get shedules data by stop
         * @param {any} scoreboard
         */
        var getSchedulesBystop = function (param) {
            appService.getSchedulesByStop(param)
                .then(function (schedulesData) {
                    // set expanded table
                    $scope['shTblIsExpand'] = false;
                    // set data
                    $scope['shedules'] = getResolvedSchedules(schedulesData.Schedules, schedulesData.Days);
                    $scope['shDays'] = schedulesData.Days;
                    // set day now
                    var index = getCuurentDayIndex();
                    $scope['todayDayIndex'] = index;
                    $scope['todayDayIndexState'] = index;
                    // bootstrap tab fix
                    $scope['changeTab'] = function ($event) {
                        var elem = $event.currentTarget;
                        if (elem) {
                            $scope['todayDayIndex'] = Number(elem.getAttribute('data-index'));
                        }
                    };

                    $scope['changeShTbl'] = function () {
                        if ($scope['shTblIsExpand'] === true) {
                            $scope['shTblIsExpand'] = false;
                        } else {
                            $scope['shTblIsExpand'] = true;
                        }
                    };

                    $(function () {
                        $('[data-toggle="tooltip"]').tooltip({ html: false, delay: { 'show': 300, 'hide': 100 }, offset: '10%' });
                    });
                },
                function (error) {
                    console.warn(error);
                }).finally(function () {
                    ;
                });
        }

        var getResolvedSchedules = function (schedules, days) {
            if (schedules) {
                for (var i = 0; i < schedules.length; i++) {
                    var allDaysOfWeekTemp = [null, null, null, null, null, null, null];

                    for (var j = 0; j < allDaysOfWeekTemp.length; j++) {
                        allDaysOfWeekTemp[j] = appHelper.grepFirst(schedules[i].AllDaysOfWeek, function (s) {
                            return s.Day === days[j];
                        });
                    }

                    schedules[i].AllDaysOfWeek = allDaysOfWeekTemp;
                }

                return schedules;
            }

            return null;
        }

        Date.prototype.addHours = function (h) {
            this.setTime(this.getTime() + (h * 60 * 60 * 1000));
            return this;
        }

        var getCuurentDayIndex = function () {
            // for skiping today`s day
            var now = new Date().addHours(-3);
            var todayDayIndex = now.getDay() - 1;
            if (todayDayIndex === -1) {
                todayDayIndex = 7 - 1;
            }
            return todayDayIndex;
        }

        var setupMarkerForList = function (stops) {
            var stopId = $scope['stopId'];
            for (var i = 0; i < stops.length; i++) {
                var stop = stops[i];
                if (stop.id === stopId) {
                    map.setMarker('selection', stop.lat, stop.lon);
                    map.lookMarker('selection');
                    return true;
                }
            }
            return false;
        }

        // get scoreboard data
        $scope['shared']["scoreboardDeferred"].promise.then(function (scoreboard) {
            var stops = [
            {
                id: $scope['stopId'],
                lat: scoreboard.Latitude,
                lon: scoreboard.Longitude,
                bearing: scoreboard.Bearing,
                tripType: map.STOP_TYPE_CUSTOM
            }];
            map.beginStops();
            map.addStops(stops);
            map.endStops();
            setupMarkerForList(stops);
        });

        // add shedules by stop
        getSchedulesBystop({ stopId: $scope['stopId'] });

        var columns = 2;
        map.setCol(columns);

        page['name'] = appLocale['stops'];
        page['type'] = 'Stop';
        page['uplinks'] = [];
        page['links'] = [
            page.templates.getIsMapLinks($scope, $interpolate, '#/stops/{{::stopId}}', columns)
        ];
    }
]);
app.controller('StopNearestCtrl', [
    '$scope', '$routeParams', '$route', '$location', '$interpolate', 'appService', 'appConfig', 'mapService', 'page', 'adService',
    function ($scope, $routeParams, $route, $location, $interpolate, appService, appConfig, map, page, adService) {
        var adPlaceName = 'bottom-1';
        $scope['adBottom1'] = adService.getPlace(adPlaceName);
        $scope['locale'] = appLocale;
        $scope['name'] = appLocale['nearestStops'];
        $scope['routeNumber'] = $routeParams['routeNumber'];
        $scope['transportTypes'] = appConfig['transportTypes'];
        $scope['transportType'] = appHelper.grepFirst($scope['transportTypes'], function (elem) {
            return elem['Tag'] === $routeParams['transportType'];
        });
        $scope['stopId'] = Number($routeParams['stopId']);
        $scope['isMap'] = $route.current.$$route.isMap;
        
        $scope['stopsState'] = 'locationRequest';
        appService.getLocationByNavigator(function (lat, lon) {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                console.debug("OK getLocationByNavigator");
            }
            $scope['lat'] = lat;
            $scope['lon'] = lon;
            map.setMarker('source', lat, lon);
            $scope['stopsState'] = 'nearestStopsLoading';
            appService.getStopsNearest($scope['lat'], $scope['lon']).then(function (data) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("OK getStopsNearest");
                }
                var stops = data['Stops'];
                $scope['stops'] = stops;
                $scope['stopsState'] = 'result';
                var mapStops = [];
                map.beginStops();
                for (var i = 0; i < stops.length; i++) {
                    var s = stops[i];
                    mapStops.push({
                        id: s.Id,
                        lat: s.Latitude,
                        lon: s.Longitude,
                        bearing: s.Bearing,
                        tripType: map.STOP_TYPE_CUSTOM
                    });
                }
                map.addStops(mapStops);
                map.endStops();
                map.lookStops();
            }, function (error) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("ERR getStopsNearest");
                }
                $scope['stopsError'] = error;
                $scope['stopsState'] = 'error';
            });
        }, function (error) {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                console.debug("ERR getLocationByNavigator");
            }
            $scope['stopsError'] = error;
            $scope['stopsState'] = 'error';
            $scope['stops'] = null;
        });

        var columns = 2;
        map.on('stop', 'click', function (stop) {
            $scope.$apply(function () {
                $location.path('/stops/nearest/' + stop.id + '/' + $scope['lat'] + '/' + $scope['lon']);
            });
        });
        map.setCol(columns);

        page['name'] = appLocale['nearestStops'];
        page['type'] = 'StopNearest';
        page['uplinks'] = [];
        page['links'] = [
            page.templates.getIsMapLinks($scope, $interpolate, '#/stops/nearest', columns)
        ];
    }
]);
app.controller('StopNearestInfoCtrl', [
    '$scope', '$routeParams', '$location', '$route', '$interpolate', 'appService', 'appConfig', 'mapService', 'page', 'adService',
    function ($scope, $routeParams, $location, $route, $interpolate, appService, appConfig, map, page, adService) {
        var adPlaceName = 'bottom-1';
        $scope['adBottom1'] = adService.getPlace(adPlaceName);
        $scope['locale'] = appLocale;
        $scope['name'] = appLocale['nearestStops'];
        $scope['routeNumber'] = $routeParams['routeNumber'];
        $scope['transportTypes'] = appConfig['transportTypes'];
        $scope['transportType'] = appHelper.grepFirst($scope['transportTypes'], function (elem) {
            return elem.Tag === $routeParams['transportType'];
        });
        $scope['stopId'] = Number($routeParams['stopId']);
        $scope['lat'] = $routeParams['lat'];
        $scope['lon'] = $routeParams['lon'];
        $scope['isMap'] = $route.current.$$route.isMap;

        map.setMarker('source', $scope['lat'], $scope['lon']);
        var setupMarkerForList = function (stops) {
            var stopId = $scope['stopId'];
            for (var i = 0; i < stops.length; i++) {
                var stop = stops[i];
                if (stop.Id === stopId) {
                    map.setMarker('selection', stop.Latitude, stop.Longitude);
                    map.lookMarker('selection');
                    return true;
                }
            }
            return false;
        };

        $scope['stopsState'] = 'nearestStopsLoading';
        appService.getStopsNearest($scope['lat'], $scope['lon']).then(function (data) {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                console.debug("OK getStopsNearest");
            }
            var stops = data['Stops'];
            $scope['stops'] = stops;
            $scope['stopsState'] = 'result';
            var mapStops = [];
            map.beginStops();
            for (var i = 0; i < stops.length; i++) {
                var s = stops[i];
                mapStops.push({
                    id: s.Id,
                    lat: s.Latitude,
                    lon: s.Longitude,
                    bearing: s.Bearing,
                    tripType: map.STOP_TYPE_CUSTOM
                });
            }
            map.addStops(mapStops);
            map.endStops();
            setupMarkerForList(stops);
        }, function (error) {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                console.debug("ERR getStopsNearest");
            }
            $scope['stopsError'] = error;
            $scope['stopsState'] = 'error';
        });

        var columns = 3;
        map.on('stop', 'click', function (stop) {
            $scope.$apply(function () {
                $location.path('/stops/nearest/' + stop.id + '/' + $scope['lat'] + '/' + $scope['lon']);
            });
        });
        map.setCol(columns);

        page['name'] = appLocale['nearestStops'];
        page['type'] = 'StopNearest';
        page['uplinks'] = [];
        page['links'] = [
            page.templates.getIsMapLinks($scope, $interpolate, '#/stops/nearest/{{::stopId}}/{{::lat}}/{{::lon}}', columns)
        ];
    }
]);
app.controller('MapCtrl', [
    '$scope', 'mapService', 'adService',
    function ($scope, map, adService) {
        $scope['initMap'] = map.init;
        $scope['isMapMode'] = function () {
            return $scope['mapMode'] != null && $scope['mapMode'] !== 'none';
        };
        $scope['mapMode'] = 'none';
        $scope['mapCol'] = function () { return map.getCol(); };
        $scope['hasBottomAd'] = function () { return adService.has('bottom-1'); };
        $scope['vehInfoApparel'] = appLocale['vehInfoApparel'];
    }
]);
app.controller('MapTransitionCtrl', [
    '$scope', 'mapService', 'moduleConf',
    function ($scope, map, moduleConf) {
        var transitionEquals = function (keys, nextParams, currentParams) {
            if (keys == null) {
                return false;
            }
            for (var i = 0; i < keys.length; i++) {
                var key = keys[i];
                if (nextParams[key] !== currentParams[key]) {
                    return false;
                }
            }
            return true;
        };
        var clearExcepts = [];
        $scope.$on('$routeChangeStart', function (event, next, current) {
            if (next.$$route == undefined) { // Переход на внешнюю страницу.
                return;
            }
            var transitions = moduleConf['mapTransitions'];
            var transDep = transitions[current.$$route.controller + '->' + next.$$route.controller];
            if (transDep != null) {
                for (var propertyName in transDep) {
                    if (transDep.hasOwnProperty(propertyName)) {
                        if (transitionEquals(transDep[propertyName], next.pathParams, current.pathParams)) {
                            clearExcepts.push(propertyName);
                        }
                    }
                }
            }
        });
        $scope.$on('$destroy', function () {
            map.clear(clearExcepts, 'except');
            map.unbind();
            map.toggleApparelTitle(false);
        });
    }
]);
app.controller('RoutesCtrl', [
    '$scope', '$window', '$interval', 'appConfig', 'appService', 'moduleConf',
    function ($scope, $window, $interval, appConfig, appService, moduleConf) {
        var errorPromise;
        var destroyErrorPromise = function () {
            if (errorPromise != null) {
                $interval.cancel(errorPromise);
                errorPromise = undefined;
            }
        };

        var getRoutes = function () {
            appService.getRouteList($scope['transportType']['Tag']).then(function (routes) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("OK getRouteList");
                }
                $scope['routes'] = routes;
                $scope['routesState'] = 'result';
            }, function (error) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("ERR getRouteList");
                }
                error['progress'] = 0.0;
                $scope['routesError'] = error;
                $scope['routesState'] = 'error';
                destroyErrorPromise();
                if (error['isUnauthorized']()) {
                    appService.cancelAll();
                    $window.location = appConfig['urlLogin'] + '?returnUrl=' + encodeURIComponent($window.location.href);
                    return;
                }
                if (error['isExpired']()) {
                    appService.cancelAll();
                    $window.location.reload();
                    return;
                }
                errorPromise = $interval(function () {
                    var progressCtx = $scope['routesError'];
                    progressCtx['progress'] = Math.min(1.0, progressCtx['progress'] + moduleConf.errorStep);
                    if (progressCtx['progress'] === 1.0) {
                        destroyErrorPromise();
                        $scope['routesState'] = 'loading';
                        getRoutes();
                    }
                }, moduleConf.errorTick);
            });
        };
        $scope['getRoutes'] = function () {
            destroyErrorPromise();
            getRoutes();
        };
        if ($scope['transportType'] != null) {
            $scope['routesState'] = 'loading';
            getRoutes();
        } else {
            $scope['routes'] = [];
            $scope['routesState'] = 'result';
        }

        $scope.$on('$destroy', function () {
            destroyErrorPromise();
        });
    }
]);
app.controller('TripsCtrl', [
    '$scope', '$window', '$interval', 'appConfig', 'appService', 'mapService', 'moduleConf',
    function ($scope, $window, $interval, appConfig, appService, map, moduleConf) {
        var errorPromise;

        var resolveLocation = function () {
            if ($window.location.href == undefined || $window.location.href == null) {
                return;
            }
            var href = decodeURI($window.location.href);
            var hrefSplitted = href.split('stops');
           
            if (hrefSplitted.length > 1) {
                var stopAndDir = hrefSplitted[1].split("/");
                if (stopAndDir.length > 0 && stopAndDir.length !== 2) {
                    var routeDir = stopAndDir[stopAndDir.length - 1];
                    $scope['shared']['routeDir'] = routeDir;
                }
            }
            else {
                return;
            }
        };
        resolveLocation();

        $scope['toggleTripVisibility'] = function ($event) {
            if ($event) {
                var elem = $event.currentTarget;
                if (elem) {
                    // set/unset active class
                    if (elem.classList.contains('activeCollapsible')) {
                        elem.classList.remove('activeCollapsible');
                    } else {
                        elem.classList.add('activeCollapsible');
                    }
                    // show/hide - plus/minus sign
                    if (elem.children[0].classList.contains('sws-hidden')) {
                        elem.children[0].classList.remove('sws-hidden');
                        elem.children[1].classList.add('sws-hidden');
                    } else {
                        elem.children[0].classList.add('sws-hidden');
                        elem.children[1].classList.remove('sws-hidden');
                    }
                    // show/hide inner data
                    var cpllapseElem = elem.nextElementSibling;
                    if (cpllapseElem) {
                        if (cpllapseElem.classList.contains('sws-hidden')) {
                            cpllapseElem.classList.remove('sws-hidden');
                        } else {
                            cpllapseElem.classList.add('sws-hidden');
                        }
                    }
                }
            }
        };

        var destroyErrorPromise = function () {
            if (errorPromise != null) {
                $interval.cancel(errorPromise);
                errorPromise = undefined;
            }
        };

        var resolveRouteDirection = function (trips) {
            var stopId = $scope['stopId'];
            var resolveList = function (stops, routeDir) {
                for (var i = 0; i < stops.length; i++) {
                    var stop = stops[i];
                    if (stop.Id === stopId && i !== stops.length - 1) {
                        $scope['shared']['routeDir'] = routeDir;
                        return true;
                    }
                }
                return false;
            };
            if (resolveList(trips.StopsA, 0)) return;
            if (resolveList(trips.StopsB, 1)) return;
        };

        var setupMarker = function (trips) {
            var stopId = $scope['stopId'];
            if (isNaN(stopId)) {
                return;
            }
            var routeDir = $scope['shared']['routeDir'];
            if (isNaN(routeDir)) {
                return;
            }
            var setupMarkerForList = function (stops) {
                for (var i = 0; i < stops.length; i++) {
                    var stop = stops[i];
                    if (stop.Id === stopId) {
                        map.setMarker('selection', stop.Latitude, stop.Longitude);
                        map.lookMarker('selection');

                        //popup
                        map.showPopup(stop);

                        return true;
                    }
                }
                return false;
            };
            switch (Number(routeDir)) {
                case 0:
                    setupMarkerForList(trips.StopsA);
                    break;
                case 1:
                    setupMarkerForList(trips.StopsB);
            }
        };

        var getInfo = function () {
            $scope['InfoState'] = $scope['InfoState'] != 'result' ? 'result' : '';
        };

        $scope['getInfo'] = function () {
            getInfo();
        };

        var getTrips = function () {
            var transportType = $scope['transportType'] == null ? null : $scope['transportType']['Tag'];
            appService.getRouteInfo(transportType, $scope['routeNumber']).then(function (tripsData) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("OK getRouteInfo");
                }
                var trips = tripsData.Trips;
                var operInfo = tripsData.OperInfo;
                $scope['trips'] = trips;
                $scope['operInfo'] = operInfo;

                var state = 'result';
                if (trips == null) {
                    state = 'result_empty';
                } else {
                    var checkStopsA = trips.StopsA;
                    var checkStopsB = trips.StopsB;
                    if ((checkStopsA == null || checkStopsA.length === 0) && (checkStopsB == null || checkStopsB.length === 0)) {
                        state = 'result_empty';
                    }
                }
                $scope['tripsState'] = state;

                if (trips != null) {
                    try {
                        if (trips.Info) {
                            var infos = trips.Info.split(',');
                            $scope['Info'] = infos.length > 0 ? infos : [];
                        } else {
                            $scope['Info'] = [];
                        }
                        //  Allow to show Other directions block in UI
                        var tripsAnotherDirectionsAny = false;
                        if (trips.OtherDirections.length > 0) {
                            tripsAnotherDirectionsAny = true;
                        }
                        $scope['tripsAnotherDirectionsAny'] = tripsAnotherDirectionsAny;
                        //
                        map.beginStops();
                        var stopsA = trips.StopsA;
                        if (stopsA != null) {
                            var mapStopsA = [];
                            for (var i = 0; i < stopsA.length; i++) {
                                var stopA = stopsA[i];
                                mapStopsA.push({
                                    id: stopA.Id,
                                    lat: stopA.Latitude,
                                    lon: stopA.Longitude,
                                    bearing: stopA.Bearing,
                                    tripType: 0
                                });
                            }
                            map.addStops(mapStopsA);
                        }
                        var stopsB = trips.StopsB;
                        if (stopsB != null) {
                            var mapStopsB = [];
                            for (var j = 0; j < stopsB.length; j++) {
                                var stopB = stopsB[j];
                                mapStopsB.push({
                                    id: stopB.Id,
                                    lat: stopB.Latitude,
                                    lon: stopB.Longitude,
                                    bearing: stopB.Bearing,
                                    tripType: 1
                                });
                            }
                            map.addStops(mapStopsB);
                        }
                    } catch (error) {
                        try {
                            if (appConfig.logLevel >= appHelper.LOG_LEVEL_WARNING) {
                                console.warn(error.stack);
                            }
                        } catch (e) {
                            // Empty
                        }
                    } finally {
                        map.endStops();
                    }

                    try {
                        if ($scope['shared'] !== undefined) {
                            if (isNaN($scope['shared']['routeDir'])) {
                                resolveRouteDirection(trips);
                            }
                            $scope['shared']['routeDirDeferred'].resolve();
                        }
                        setupMarker(trips);
                    } catch (error2) {
                        try {
                            if (appConfig.logLevel >= appHelper.LOG_LEVEL_WARNING) {
                                console.warn(error2.stack);
                            }
                        } catch (e) {
                            // Empty
                        }
                    }
                } else {
                    $scope['Info'] = [];
                }
            }, function (error) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("ERR getRouteInfo");
                }
                error['progress'] = 0.0;
                $scope['tripsError'] = error;
                $scope['tripsState'] = 'error';
                destroyErrorPromise();
                if (error['isUnauthorized']()) {
                    appService.cancelAll();
                    $window.location = appConfig['urlLogin'] + '?returnUrl=' + encodeURIComponent($window.location.href);
                    return;
                }
                if (error['isExpired']()) {
                    appService.cancelAll();
                    $window.location.reload();
                    return;
                }
                errorPromise = $interval(function () {
                    var progressCtx = $scope['tripsError'];
                    progressCtx['progress'] = Math.min(1.0, progressCtx['progress'] + moduleConf.errorStep);
                    if (progressCtx['progress'] === 1.0) {
                        destroyErrorPromise();
                        $scope['tripsState'] = 'loading';
                        getTrips();
                    }
                }, moduleConf.errorTick);
            });
        };

        $scope['getTrips'] = function () {
            destroyErrorPromise();
            getTrips();
        };

        $scope['tripsState'] = 'loading';
        getTrips();

        $scope.$on('$destroy', function () {
            appService.cancelRouteInfo();
            destroyErrorPromise();
        });
    }
]);
app.controller('TrackCtrl', [
    '$scope', '$window', '$interval', 'appConfig', 'appService', 'mapService', 'moduleConf',
    function ($scope, $window, $interval, appConfig, appService, map, moduleConf) {
        var errorPromise;
        var destroyErrorPromise = function () {
            if (errorPromise != null) {
                $interval.cancel(errorPromise);
                errorPromise = undefined;
            }
        };

        $scope['trackState'] = 'loading';
        var transportType = $scope['transportType'] == null ? null : $scope['transportType']['Tag'];
        appService.getTrackInfo(transportType, $scope['routeNumber']).then(function (track) {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                console.debug("OK getTrackInfo");
            }
            try {
                map.beginTrack();
                var pointsA = track.PointsA;
                if ($.isArray(pointsA)) {
                    var trackA = [];
                    for (var i = 0; i < pointsA.length; i++) {
                        var p1 = pointsA[i];
                        trackA.push({
                            lat: p1.Latitude,
                            lon: p1.Longitude
                        });
                    }
                    map.addTrack(trackA, 'a', moduleConf.straightColor);
                }
                var pointsB = track.PointsB;
                if ($.isArray(pointsB)) {
                    var trackB = [];
                    for (var j = 0; j < pointsB.length; j++) {
                        var p2 = pointsB[j];
                        trackB.push({
                            lat: p2.Latitude,
                            lon: p2.Longitude
                        });
                    }
                    map.addTrack(trackB, 'b', moduleConf.reverseColor);
                }
                map.endTrack();
                if ($scope['trackDeferred'] !== undefined) {
                    $scope['trackDeferred'].resolve();
                }
            } catch (error) {
                map.endTrack();
                if ($scope['trackDeferred'] !== undefined) {
                    $scope['trackDeferred'].reject();
                }
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_WARNING) {
                    console.warn(error.stack);
                }
            }
            $scope['trackState'] = 'result';
        }, function (error) {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                console.debug("ERR getTrackInfo");
            }
            error['progress'] = 0.0;
            $scope['trackError'] = error;
            $scope['trackState'] = 'error';
            destroyErrorPromise();
            if (error['isUnauthorized']()) {
                appService.cancelAll();
                $window.location = appConfig['urlLogin'] + '?returnUrl=' + encodeURIComponent($window.location.href);
                return;
            }
            if (error['isExpired']()) {
                appService.cancelAll();
                $window.location.reload();
                return;
            }
            errorPromise = $interval(function () {
                var progressCtx = $scope['trackError'];
                progressCtx['progress'] = Math.min(1.0, progressCtx['progress'] + moduleConf.errorStep);
                if (progressCtx['progress'] === 1.0) {
                    destroyErrorPromise();
                    $scope['trackState'] = 'loading';

                    var getTrips = $scope['getTrips'];
                    if (getTrips != null) {
                        getTrips();
                    }
                }
            }, moduleConf.errorTick);
        });

        $scope.$on('$destroy', function () {
            appService.cancelTrackInfo();
            destroyErrorPromise();
        });
    }
]);
app.controller('VehicleCtrl', [
     '$scope', '$window', '$interval', 'appConfig', 'appService', 'mapService',
    function ($scope, $window, $interval, appConfig, appService, map) {
        var intervalPromise;
        var destroyIntervalPromise = function () {
            if (intervalPromise != null) {
                $interval.cancel(intervalPromise);
                intervalPromise = undefined;
            }
        };

        var getVehicles = function () {
            var transportType = $scope['transportType'] == null ? null : $scope['transportType']['Tag'];
            appService.getVehicles(transportType, $scope['routeNumber']).then(function (vehicles) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("OK getVehicles");
                }
                try {
                    map.beginVehicles();

                    var tooltip = map.getVehicleHtmlLabel(transportType) + ' № ' + $scope['routeNumber'];
                    var isApparelExists = 0;

                    if ($.isArray(vehicles)) {
                        var tarVehicles = [];
                        var vehType = transportType;
                        for (var i = 0; i < vehicles.length; i++) {
                            var veh = vehicles[i];

                            if (isApparelExists === 0) {
                                isApparelExists = veh.IsApparel;
                            }

                            tarVehicles.push({
                                id: veh.Id,
                                type: vehType,
                                lat: veh.Latitude,
                                lon: veh.Longitude,
                                tripType: veh.TripType === 10 ? 1 : 2,
                                isApparel: veh.IsApparel,
                                tooltip: tooltip
                            });
                        }

                        map.addVehicles(tarVehicles);
                    }
                    map.endVehicles();
                    map.toggleApparelTitle(isApparelExists);
                } catch (error) {
                    map.endVehicles();
                    if (appConfig.logLevel >= appHelper.LOG_LEVEL_WARNING) {
                        console.warn(error.stack);
                    }
                }

                $scope['vehiclesState'] = 'result';
            }, function (error) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("ERR getVehicles");
                }
                $scope['vehiclesError'] = error;
                $scope['vehiclesState'] = 'error';
                if (error['isUnauthorized']()) {
                    destroyIntervalPromise();
                    appService.cancelAll();
                    $window.location = appConfig['urlLogin'] + '?returnUrl=' + encodeURIComponent($window.location.href);
                    return;
                }
                if (error['isExpired']()) {
                    destroyIntervalPromise();
                    appService.cancelAll();
                    $window.location.reload();
                    return;
                }
            });
        };

        $scope['vehiclesState'] = 'loading';
        getVehicles();
        intervalPromise = $interval(function () {
            var prevState = $scope['vehiclesState'];
            var newState;
            switch (prevState) {
                case 'result':
                    newState = 'resultLoading';
                    break;
                default:
                    newState = 'loading';
                    break;
            }
            $scope['vehiclesState'] = newState;
            getVehicles();
        }, 10000);

        $scope.$on('$destroy', function () {
            appService.cancelVehicles();
            destroyIntervalPromise();
        });
    }
]);
app.controller('ScoreboardCtrl', [
    '$scope', '$window', '$location', '$interval', 'appConfig', 'appService',
    function ($scope, $window, $location, $interval, appConfig, appService) {
        var intervalPromise;
        var destroyIntervalPromise = function () {
            if (intervalPromise != null) {
                $interval.cancel(intervalPromise);
                intervalPromise = undefined;
            }
        };

        $scope['selectRoute'] = function (row) {
            $location.path('/routes/' + appHelper.transportTypeToTag(row.Type) + '/' + row.Number + '/stops/' + $scope['stopId']);
        };

        var getScoreboard = function () {
            appService.getScoreboard($scope['stopId']).then(function (scoreboard) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("OK getScoreboard");
                }
                $scope['scoreboard'] = scoreboard;
                $scope['scoreboardState'] = 'result';
                var shared = $scope['shared'];
                if (shared != undefined) {
                    var scoreboardDeffered = shared['scoreboardDeferred'];
                    if (scoreboardDeffered != undefined) {
                        scoreboardDeffered.resolve(scoreboard);
                    }
                }
            }, function (error) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("ERR getScoreboard");
                }
                $scope['scoreboardError'] = error;
                $scope['scoreboardState'] = 'error';
                if (error['isUnauthorized']()) {
                    destroyIntervalPromise();
                    appService.cancelAll();
                    $window.location = appConfig['urlLogin'] + '?returnUrl=' + encodeURIComponent($window.location.href);
                    return;
                }
                if (error['isExpired']()) {
                    destroyIntervalPromise();
                    appService.cancelAll();
                    $window.location.reload();
                    return;
                }
            });
        };

        $scope['scoreboardState'] = 'loading';
        getScoreboard();
        intervalPromise = $interval(function () {
            var prevState = $scope['scoreboardState'];
            var newState;
            switch (prevState) {
                case 'result':
                    newState = 'resultLoading';
                    break;
                default:
                    newState = 'loading';
                    break;
            }
            $scope['scoreboardState'] = newState;
            getScoreboard();
        }, 10000);

        $scope.$on('$destroy', function () {
            appService.cancelScoreboard();
            destroyIntervalPromise();
        });
    }
]);
//filter hours:minutes
app.filter('hmFilter', ['$filter',
    function ($filter) {
        return function (input) {
            var inp = new Date(0, 0, 0, 0, input, 0);
            var hours = inp.getHours();
            var minutes = inp.getMinutes();
            return hours + ':' + minutes.toLocaleString('en-US', { minimumIntegerDigits: 2 });
        };
    }
]);
app.controller('ScheduleCtrl', [
    '$scope', '$window', '$interval', 'appConfig', 'appService', 'moduleConf',
    function ($scope, $window, $interval, appConfig, appService, moduleConf) {
        var days = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'];
        var map = new Map();
        var resetSchedule = function () {
            for (var i = 0; i < days.length; i++) {
                $scope[days[i]] = null;
            }
            $scope['todayDow'] = -1;
        };
        var resolveLocation = function () {
            if ($window.location.href == undefined || $window.location.href == null) {
                return;
            }
            var href = decodeURI($window.location.href);
            var hrefSplitted = href.split('stops');

            if (hrefSplitted.length > 1) {
                var stopAndDir = hrefSplitted[1].split("/");
                if (stopAndDir.length > 0 && stopAndDir.length !== 2) {
                    var routeDir = stopAndDir[stopAndDir.length - 1];
                    $scope['shared']['routeDir'] = routeDir;
                }
            }
            else {
                return;
            }
        };
        $scope['showTripDeparturesSpinner'] = false;
        $scope['loaderLocation'] = appConfig.urlImages;

        //  old version
        var resolveDayOfWeek = function (daysOfWeek, dayIndex) {
            var dayName = days[dayIndex];
            var dayFlag = 1 << dayIndex;
            for (var i = 0; i < daysOfWeek.length; i++) {
                var dayOfWeek = daysOfWeek[i];
                if ((dayOfWeek['DaysOfWeek'] & dayFlag) === dayFlag) {
                    $scope[dayName] = dayOfWeek;
                }
            }
        };

        //  new version
        var resolveDayOfWeekV2 = function (daysOfWeek, dayIndex) { 
            // for skiping today`s day
            var now = new Date().addHours(-3);
            var todayDayIndex = now.getDay() - 1;
            if (todayDayIndex === -1) {
                todayDayIndex = 7 - 1;
            }
            var todayDayName = days[todayDayIndex];
            //
            var dayName = days[dayIndex];
            var dayFlag = 1 << dayIndex;
            //  here we need to regroup hourLines if we have a few daysOfWeek for a single dayName
            var newHourLinesObj = [];

            for (var i = 0; i < daysOfWeek.length; i++) {
                var dayOfWeek = daysOfWeek[i];
                if ((dayOfWeek['DaysOfWeek'] & dayFlag) === dayFlag) {
                    if (todayDayName == dayName) {
                        // skip today, because we have already processed it
                        var today = $scope[days[todayDayIndex]];
                        if (today != null) {
                            $scope[dayName] = today;
                            break;
                        }
                    }

                    if ($scope[dayName] == null) {
                        $scope[dayName] = dayOfWeek;
                    }
                    //
                    var hourLines = dayOfWeek['HourLines'];
                    if (/*i == 0*/newHourLinesObj.length == 0) {
                        for (var e = 0; e < hourLines.length; e++) {
                            newHourLinesObj.push(hourLines[e]);
                        }
                        //newHourLinesObj = hourLines;
                        continue;
                    }

                    newHourLinesObj = resolveNewHourLinesObject(hourLines, newHourLinesObj);
                }
            }
            // sort newHourLinesObj by hours
            if (newHourLinesObj.length > 0) {
                newHourLinesObj.sort(function (a, b) {
                    if (a.Hour > b.Hour) {
                        return 1;
                    }
                    if (a.Hour < b.Hour) {
                        return -1;
                    }
                    // a должно быть равным b
                    return 0;
                });
            }

            newHourLinesObj = resolveScheduleHoursSorting(newHourLinesObj);

            var resultDayOfWeek = $scope[dayName];
            if (resultDayOfWeek != null) {
                if (todayDayName != dayName) {
                    var copy = Object.assign({}, resultDayOfWeek);
                    copy.HourLines = newHourLinesObj;
                    $scope[dayName] = copy;
                }
            }
        };

        var resolveScheduleHoursSorting = function (newHourLinesObj) {
            if (newHourLinesObj) {
                var tempHourLines = [];
                var newHourLines = [];

                for (var i = 0; i < newHourLinesObj.length; i++) {
                    var hour = Number(newHourLinesObj[i].Hour);
                    if (hour === 0 || hour === 1 || hour === 2) {
                        tempHourLines.push(newHourLinesObj[i]);
                    }
                    else {
                        newHourLines.push(newHourLinesObj[i]);
                    }
                }

                for (var i = 0; i < tempHourLines.length; i++) {
                    newHourLines.push(tempHourLines[i]);
                }
            }

            return newHourLines;
        }

        var resolveTodaySchedule = function (daysOfWeek) {
            var now = new Date().addHours(-3);
            var dayIndex = now.getDay() - 1;
            if (dayIndex === -1) {
                dayIndex = 7 - 1;
            }
            //var dayName = days[dayIndex];
            var dayFlag = 1 << dayIndex;
            var today = $scope[days[dayIndex]];

            if (today != null) {
                //  here we need to regroup hourLines if we have a few daysOfWeek for a single dayName
                var newHourLinesObj = [];
                //  fill result hourLines from todays schedule
                var todayHourLines = today['HourLines'];
                for (var j = 0; j < todayHourLines.length; j++) {
                    newHourLinesObj.push(todayHourLines[j]);
                }

                for (var i = 0; i < daysOfWeek.length; i++) {
                    var dayOfWeek = daysOfWeek[i];
                    if ((dayOfWeek['DaysOfWeek'] & dayFlag) === dayFlag) {
                        //  process today schedule
                        var hourLines = dayOfWeek['HourLines'];
                        newHourLinesObj = resolveNewHourLinesObject(hourLines, newHourLinesObj);
                    }
                }
                // sort newHourLinesObj by hours
                if (newHourLinesObj.length > 0) {
                    newHourLinesObj.sort(function (a, b) {
                        if (a.Hour > b.Hour) {
                            return 1;
                        }
                        if (a.Hour < b.Hour) {
                            return -1;
                        }
                        // a должно быть равным b
                        return 0;
                    });
                }
                // rewrite today schedule HourLines
                today['HourLines'] = resolveScheduleHoursSorting(newHourLinesObj);
            }
        };

        var resolveNewHourLinesObject = function (hourLines, newHourLinesObj) {
            //  Here we check if we already have Hours
            //  if not, we add current HourLine
            //  otherwise we check if we already have suck departure minutes
            //  and if we dont have such minutes, then add them
            for (var j = 0; j < hourLines.length; j++) {
                var hourLine = hourLines[j];
                var hour = hourLine['Hour'];
                //hour = '05';    //  test
                var minutes = hourLine['Minutes'];
                var minutesSplited = minutes.split(' ');
                //minutesSplited.push('15');  //  test
                var findHour = newHourLinesObj.find(function (element, index, array) {
                    if (element['Hour'] != hour) {
                        return false;
                    }
                    else {
                        return true;
                    }
                });
                //  add hourLine and continue because we have no such hour departure;
                if (findHour == undefined) {
                    newHourLinesObj.push(hourLine);
                    continue;
                }
                //  check if we already have such departure minutes
                for (var k = 0; k < newHourLinesObj.length; k++) {
                    var newHourLine = newHourLinesObj[k];
                    if (newHourLine['Hour'] == hour) {
                        var newHourLineMinutesSplited = newHourLine['Minutes'].split(' ');
                        //  result, we will replace hourLines with this
                        var newHourLinesMinutes = [];
                        //  add every newHourLineMinutesSplited
                        newHourLineMinutesSplited.forEach(x => newHourLinesMinutes.push(x));
                        //  check for new minutes
                        for (var m = 0; m < minutesSplited.length; m++) {
                            var minute = minutesSplited[m];
                            if (newHourLinesMinutes.indexOf(minute) == -1) {
                                //  add new minutes
                                newHourLinesMinutes.push(minute);
                            }
                        }
                        newHourLinesMinutes.sort();
                        newHourLine.Minutes = newHourLinesMinutes.join(' ');
                    }
                }
            }
            return newHourLinesObj;
        };

        var resolveDaysOfWeekSchedule = function (daysOfWeek) {
            // sort daysOfWeek ascending, very important!
            if (daysOfWeek.length > 0) {
                daysOfWeek.sort(function (a, b) {
                    if (a.DaysOfWeek > b.DaysOfWeek) {
                        return 1;
                    }
                    if (a.DaysOfWeek < b.DaysOfWeek) {
                        return -1;
                    }
                    // a должно быть равным b
                    return 0;
                });
            }
            //  resolve todays schedule
            resolveTodaySchedule(daysOfWeek);
            //  resolve other days schedule
            for (var i = 0; i < 7; i++) {
                //resolveDayOfWeek(daysOfWeek, i);
                resolveDayOfWeekV2(daysOfWeek, i);
            }
        };

        var hasAnyDay = function() {
            for (var i = 0; i < 7; i++) {
                var day = $scope[days[i]];
                if (day != null) {
                    return true;
                }
            }
            return false;
        };

        var transportType = $scope['transportType'];
        var transportTypeTag;
        if (transportType != null) {
            $scope['transportTypeName'] = transportType['Name'];
            transportTypeTag = transportType['Tag'];
        } else {
            transportTypeTag = 'unknown';
        }

        var errorPromise;
        var destroyErrorPromise = function () {
            if (errorPromise != null) {
                $interval.cancel(errorPromise);
                errorPromise = undefined;
            }
        };

        var checkPromise;
        var destroyCheckPromise = function () {
            if (checkPromise != null) {
                $interval.cancel(checkPromise);
                checkPromise = undefined;
            }
        };

        var selectDay = function (day, force) {
            var schedule = null;
            $scope['isApparels'] =false;
            var has = false;
            if (day >= 0) {
                has = $scope[days[day]] != null;

                if ($scope[days[day]] != null) {
                    schedule = $scope[days[day]];
                    //to do every schedule departure clickable
                    var spaceSeparator = ' ';
                    var hourLines = schedule['HourLines'];
                    for (var i = 0; i < hourLines.length; i++) {
                        var separatedMinutes = hourLines[i].Minutes.split(spaceSeparator);
                        separatedMinutes.forEach(minutes => minutes.trim());

                        if (hourLines[i].ApparelByMinutes) {
                            var separatedApparels = hourLines[i].ApparelByMinutes.split(spaceSeparator);
                            joinApparels = separatedApparels.map(function (v, i) {
                                return { isApparel: Number(v.trim()), minute: separatedMinutes[i] }
                            });

                            hourLines[i].separatedApparels = joinApparels;
                            $scope['isApparels'] = true;                            
                        } else {
                            hourLines[i].separatedApparels = null;
                        }

                        hourLines[i].separatedMinutes = separatedMinutes;
                    }
                    //fill map object
                    map.clear();
                    for (var j = 0; j < schedule.HourLines.length; j++) {
                        var mapKey = schedule.HourLines[j].Hour;
                        var mapValue = {
                            "showRow": false,
                            "mapSchedule": null
                        };
                        map.set(mapKey, mapValue);
                    }
                }

                $(function () {
                    $('[data-toggle="tooltip"]').tooltip({ html: true, delay: { 'show': 700, 'hide': 100 } });
                });
            }

            if (has) {
                $scope['selectedDow'] = day;
            } else if (!force) {
                return false;
            }
            $scope['schedule'] = schedule;

            var state = 'result';
            if (!hasAnyDay()) {
                state = 'result_empty';
            } else if (schedule == null) {
                state = 'result_no_select';
            } else {
                var hLines = schedule['HourLines'];
                if (hLines == null || hLines.length === 0) {
                    state = 'result_empty';
                }
            }
            $scope['scheduleState'] = state;
            return has;
        };

        $scope['selectedDow'] = -1;
        $scope['selectDay'] = function (day) {
            return selectDay(day, false);
        };
        $scope['showTripDeparturesTable'] = function (hours) {
            var mapVal = map.get(hours);
            return mapVal.showRow;
        };
        $scope['tripSchedule'] = function (hours) {
            var mapVal = map.get(hours);
            return mapVal.mapSchedule;
        };

        var showTripDepartures = function (hours, minutes) {
            var mapVal = map.get(hours);
            mapVal.showRow = true;
            mapVal.mapSchedule = null;
            getTripDepartures(departure(hours, minutes, false), hours, minutes);
        };

        var departure = function (hours, minutes, needToAdd24 = false) {
            /*
            var sc = $scope['schedule']['HourLines'];
            // check if hours < first value sc.Hour then add 24h to hours
            var needToAdd24 = false;
            var hoursInt = parseInt(hours);
            if (sc != undefined) {
                if (sc != null) {
                    if (sc.length > 0) {
                        var firstArrayValueInt = parseInt(sc[0].Hour);
                        var isMidnight = (hoursInt == firstArrayValueInt && hoursInt == 0); //  for 00:00 hours
                        if (hoursInt < firstArrayValueInt || isMidnight) {
                            needToAdd24 = true;
                        }
                    }
                }
            }
            */
            var hoursValue = needToAdd24 ? (parseInt(hours) + 24) * 60 : parseInt(hours) * 60;
            return (hoursValue + parseInt(minutes));
        };

        var isReloadTripDepartures = false;

        var getTripDepartures = function (selectedStopDeparture, mapKey, minutes) {
            try {
                var hours = mapKey;
                //  show loading spinner gif image while fetching data
                $scope['showTripDeparturesSpinner'] = true;
                //send request for data
                appService.getTripDepartures(transportTypeTag,
                    $scope['routeNumber'],
                    $scope['stopId'],
                    $scope['shared']['routeDir'],
                    selectedStopDeparture,
                    $scope['schedule']['DaysOfWeek']
                    )
                    .then(function (tripDepartures) {
                        if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                            console.debug("OK getTripDepartures");
                        }
                        var mVal = map.get(mapKey);
                        if (tripDepartures.model === undefined) {
                            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                                console.debug("TripDepartures is null");
                            }

                            if (isReloadTripDepartures) {
                                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                                    console.debug("TripDepartures is null after reload with 24 hours added. Check data in cache.");
                                }
                                closeTripDeparturesTable(mapKey);
                            }

                            isReloadTripDepartures = !isReloadTripDepartures;
                            if (isReloadTripDepartures) {
                                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                                    console.debug("Reloading TripDepartures...");
                                }
                                //  try add 24 hours and check departure time again
                                getTripDepartures(departure(hours, minutes, true), hours, minutes);
                            }
                        }
                        else {
                            isReloadTripDepartures = false; // not reload trip departures table
                            mVal.mapSchedule = tripDepartures.model;
                            $scope['showTripDeparturesSpinner'] = false;
                        }
                        },
                        function (error) {
                            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                                console.debug("ERR getTripDepartures -> error: " + error);
                            }
                            closeTripDeparturesTable(mapKey);
                        });
            } catch (error) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_WARNING) {
                    console.warn(error);
                }
                closeTripDeparturesTable(mapKey);
            }
        };

        var closeTripDeparturesTable = function (idClosingRow) {
            var mapValue = map.get(idClosingRow);
            mapValue.showRow = false;
            mapValue.mapSchedule = null;
            $scope['showTripDeparturesSpinner'] = false;
        };

        Date.prototype.addHours = function (h) {
            this.setTime(this.getTime() + (h * 60 * 60 * 1000));
            return this;
        }

        var getSchedule = function () {
            try {
                resolveLocation();
                appService.getSchedule(transportTypeTag,
                        $scope['routeNumber'],
                        $scope['stopId'],
                        $scope['shared']['routeDir'])
                    .then(function (schedule) {
                            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                                console.debug("OK getSchedule");
                            }
                            var selectedDow = $scope['selectedDow'];
                            $scope['exclWorkingDay'] = -1;

                            // set oper info
                            $scope['operInfo'] = schedule.OperInfo;

                            // set is last stop
                            $scope['isLastStop'] = schedule.IsLastStop;

                            resetSchedule();
                            if (schedule == null) {
                                selectDay(-1, true);
                                return;
                            }

                            var now = new Date().addHours(-3);
                            var dayIndex = now.getDay() - 1;
                            if (dayIndex === -1) {
                                dayIndex = 7 - 1;
                            }
                            var today = schedule['Schedule'];

                            //if ((today.DaysOfWeek & Math.pow(2, dayIndex)) === 0) {
                            //    today = null;
                            //}

                            if (today != null) {
                                $scope[days[dayIndex]] = today;
                            }
                            else {
                                $scope[days[dayIndex]] = null;
                            }

                            try {
                                var daysOfWeek = schedule['DaysOfWeek'];
                                if (daysOfWeek != null) {
                                    resolveDaysOfWeekSchedule(daysOfWeek);
                                }
                            } catch (error) {
                                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                                    console.debug(error);
                                }
                            }
                            if ($scope[days[selectedDow]] == null) {
                                selectedDow = -1;
                            }
                           
                            $scope['todayDow'] = dayIndex;

                            if (selectedDow === -1) {
                                selectedDow = dayIndex;
                            }

                            var exclInfo = schedule['InfoExclusion'];
                            if (exclInfo) {
                                $scope['InfoExclusion'] = exclInfo;
                            } else {
                                $scope['InfoExclusion'] = null;
                            }

                            var workingDay = schedule['WorkingDay'];
                            if (workingDay) {
                                selectedDow = workingDay - 1; // 0-6 Mon-Sun
                                // set style
                                $scope['exclWorkingDay'] = selectedDow;
                            }
                            
                            selectDay(selectedDow, true);     
                        },
                        function (error) {
                            if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                                console.debug("ERR getSchedule");
                            }
                            error['progress'] = 0.0;
                            $scope['scheduleError'] = error;
                            $scope['scheduleState'] = 'error';
                            destroyErrorPromise();
                            if (error['isUnauthorized']()) {
                                appService.cancelAll();
                                $window.location = appConfig['urlLogin'] +
                                    '?returnUrl=' +
                                    encodeURIComponent($window.location.href);
                                return;
                            }
                            if (error['isExpired']()) {
                                appService.cancelAll();
                                $window.location.reload();
                                return;
                            }
                            errorPromise = $interval(function () {
                                    var progressCtx = $scope['scheduleError'];
                                    progressCtx['progress'] = Math.min(1.0, progressCtx['progress'] + moduleConf.errorStep);
                                    if (progressCtx['progress'] === 1.0) {
                                        destroyErrorPromise();
                                        $scope['scheduleState'] = 'loading';
                                        getSchedule();
                                    }
                                },
                                moduleConf.errorTick);
                        });
            } catch (error2) {
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_WARNING) {
                    console.warn(error2);
                }
            }
        };

        checkPromise = $interval(function () {
                if (errorPromise != null) {
                    return;
                }
                if (appConfig.logLevel >= appHelper.LOG_LEVEL_DEBUG) {
                    console.debug("CHK checkSchedule");
                }
                getSchedule();
            },
            moduleConf.checkScheduleTime);

        $scope['getSchedule'] = function () {
            destroyErrorPromise();
            getSchedule();
        };
        $scope['showTripDepartures'] = function (hours, minutes, index) {
            showTripDepartures(hours, minutes);
        };
        $scope['closeTripDeparturesTable'] = function (idClosingRow) {
            closeTripDeparturesTable(idClosingRow);
        };
        $scope['shared']['routeDirDeferred'].promise.then(function () {
            $scope['scheduleState'] = 'loading';
            getSchedule();
        }, function (error) {
            if (appConfig.logLevel >= appHelper.LOG_LEVEL_WARNING) {
                console.warn(error.stack);
            }
        });

        $scope.$on('$destroy', function () {
            appService.cancelSchedule();
            destroyErrorPromise();
            destroyCheckPromise();
        });
    }
]);
app.controller('AdsCtrl', [
    '$scope', 'ads',
    function ($scope, ads) {
        if (ads['items'] == null || ads['items'] === 0) {
            ads['show'] = false;
            ads['index'] = 0;
        } else {
            if (ads['index'] >= ads['items'].length) {
                ads['index'] = 0;
            }
            ads['show'] = true;
        }
        $scope['ads'] = ads;
    }
]);;
