/*
 * Copyright © 2016-2020 The Thingsboard Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import './trip-animation-widget.scss';
import template from "./trip-animation-widget.tpl.html";
import TbOpenStreetMap from '../openstreet-map';
import L from 'leaflet';
import 'leaflet-polylinedecorator'
import tinycolor from "tinycolor2";
import {fillPatternWithActions, isNumber, padValue, processPattern} from "../widget-utils";

(function () {
    // save these original methods before they are overwritten
    var proto_initIcon = L.Marker.prototype._initIcon;
    var proto_setPos = L.Marker.prototype._setPos;
    var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');

    L.Marker.addInitHook(function () {
        var iconOptions = this.options.icon && this.options.icon.options;
        var iconAnchor = iconOptions && this.options.icon.options.iconAnchor;
        if (iconAnchor) {
            iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px');
        }
        this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom';
        this.options.rotationAngle = this.options.rotationAngle || 0;

        // Ensure marker keeps rotated during dragging
        this.on('drag', function (e) {
            e.target._applyRotation();
        });
    });

    L.Marker.include({
        _initIcon: function () {
            proto_initIcon.call(this);
        },

        _setPos: function (pos) {
            proto_setPos.call(this, pos);
            this._applyRotation();
        },

        _applyRotation: function () {
            if (this.options.rotationAngle) {
                this._icon.style[L.DomUtil.TRANSFORM + 'Origin'] = this.options.rotationOrigin;

                if (oldIE) {
                    // for IE 9, use the 2D rotation
                    this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)';
                } else {
                    // for modern browsers, prefer the 3D accelerated version
                    let rotation = ' rotateZ(' + this.options.rotationAngle + 'deg)';
                    if (!this._icon.style[L.DomUtil.TRANSFORM].includes(rotation)) {
                        this._icon.style[L.DomUtil.TRANSFORM] += rotation;
                    }
                }
            }
        },

        setRotationAngle: function (angle) {
            this.options.rotationAngle = angle;
            this.update();
            return this;
        },

        setRotationOrigin: function (origin) {
            this.options.rotationOrigin = origin;
            this.update();
            return this;
        }
    });
})();


export default angular.module('thingsboard.widgets.tripAnimation', [])
    .directive('tripAnimation', tripAnimationWidget)
    .filter('tripAnimation', function ($filter) {
        return function (label) {
            label = label.toString();

            let translateSelector = "widgets.tripAnimation." + label;
            let translation = $filter('translate')(translateSelector);

            if (translation !== translateSelector) {
                return translation;
            }

            return label;
        }
    })
    .name;


/*@ngInject*/
function tripAnimationWidget() {
    return {
        restrict: "E",
        scope: true,
        bindToController: {
            ctx: '=',
            self: '='
        },
        controller: tripAnimationController,
        controllerAs: 'vm',
        templateUrl: template
    };
}

/*@ngInject*/
function tripAnimationController($document, $scope, $log, $http, $timeout, $filter, $sce) {
    let vm = this;

    vm.initBounds = true;

    vm.markers = [];
    vm.index = 0;
    vm.dsIndex = 0;
    vm.minTime = 0;
    vm.minTimeIndex = 0;
    vm.maxTimeIndex = 0;
    vm.isPlaying = false;
    vm.trackingLine = {
        "type": "FeatureCollection",
        features: []
    };
    vm.speeds = [1, 5, 10, 25];
    vm.speed = 1;
    vm.trips = [];
    vm.activeTripIndex = 0;

    vm.showHideTooltip = showHideTooltip;
    vm.recalculateTrips = recalculateTrips;

    $scope.$watch('vm.ctx', function () {
        if (vm.ctx) {
            vm.utils = vm.ctx.$scope.$injector.get('utils');
            vm.settings = vm.ctx.settings;
            vm.widgetConfig = vm.ctx.widgetConfig;
            vm.data = vm.ctx.data;
            vm.datasources = vm.ctx.datasources;
            configureStaticSettings();
            initialize();
            initializeCallbacks();
        }
    });


    function initializeCallbacks() {
        vm.self.onDataUpdated = function () {
            createUpdatePath(true);
        };

        vm.self.onResize = function () {
            resize();
        };

        vm.self.typeParameters = function () {
            return {
                maxDatasources: 1, // Maximum allowed datasources for this widget, -1 - unlimited
                maxDataKeys: -1 //Maximum allowed data keys for this widget, -1 - unlimited
            }
        };
        return true;
    }


    function resize() {
        if (vm.map) {
            vm.map.invalidateSize();
        }
    }

    function initCallback() {
        //createUpdatePath();
        //resize();
    }

    vm.playMove = function (play) {
        if (play && vm.isPlaying) return;
        if (play || vm.isPlaying) vm.isPlaying = true;
        if (vm.isPlaying) {
            moveInc(1);
            vm.timeout = $timeout(function () {
                vm.playMove();
            }, 1000 / vm.speed)
        }
    };

    vm.moveNext = function () {
        vm.stopPlay();
        if (vm.staticSettings.usePointAsAnchor) {
            let newIndex = vm.maxTimeIndex;
            for (let index = vm.index + 1; index < vm.maxTimeIndex; index++) {
                if (vm.trips.some(function (trip) {
                    return calculateCurrentDate(trip.timeRange, index).hasAnchor;
                })) {
                    newIndex = index;
                    break;
                }
            }
            moveToIndex(newIndex);
        } else moveInc(1);
    };

    vm.movePrev = function () {
        vm.stopPlay();
        if (vm.staticSettings.usePointAsAnchor) {
            let newIndex = vm.minTimeIndex;
            for (let index = vm.index - 1; index > vm.minTimeIndex; index--) {
                if (vm.trips.some(function (trip) {
                    return calculateCurrentDate(trip.timeRange, index).hasAnchor;
                })) {
                    newIndex = index;
                    break;
                }
            }
            moveToIndex(newIndex);
        } else moveInc(-1);
    };

    vm.moveStart = function () {
        vm.stopPlay();
        moveToIndex(vm.minTimeIndex);
    };

    vm.moveEnd = function () {
        vm.stopPlay();
        moveToIndex(vm.maxTimeIndex);
    };

    vm.stopPlay = function () {
        if (vm.isPlaying) {
            vm.isPlaying = false;
            $timeout.cancel(vm.timeout);
        }
    };

    function moveInc(inc) {
        let newIndex = vm.index + inc;
        moveToIndex(newIndex);
    }

    function moveToIndex(newIndex) {
        if (newIndex > vm.maxTimeIndex || newIndex < vm.minTimeIndex) return;
        vm.index = newIndex;
        vm.animationTime = vm.minTime + vm.index * vm.staticSettings.normalizationStep;
        recalculateTrips();
    }

    function recalculateTrips() {
        vm.trips.forEach(function (value) {
            moveMarker(value);
        })
    }

    function initialize() {
        $scope.currentDate = $filter('date')(0, "yyyy.MM.dd HH:mm:ss");

        vm.self.actionSources = [vm.searchAction];
        vm.endpoint = vm.ctx.settings.endpointUrl;
        $scope.title = vm.ctx.widgetConfig.title;
        vm.utils = vm.self.ctx.$scope.$injector.get('utils');

        vm.showTimestamp = vm.settings.showTimestamp !== false;
        vm.ctx.$element = angular.element("#trip-animation-map", vm.ctx.$container);
        vm.defaultZoomLevel = 2;
        if (vm.ctx.settings.defaultZoomLevel) {
            if (vm.ctx.settings.defaultZoomLevel > 0 && vm.ctx.settings.defaultZoomLevel < 21) {
                vm.defaultZoomLevel = Math.floor(vm.ctx.settings.defaultZoomLevel);
            }
        }
        vm.dontFitMapBounds = vm.ctx.settings.fitMapBounds === false;
        vm.map = new TbOpenStreetMap(vm.ctx.$element, vm.utils, initCallback, vm.defaultZoomLevel, vm.dontFitMapBounds, vm.staticSettings.disableScrollZooming, null, vm.staticSettings.mapProvider);
        vm.map.bounds = vm.map.createBounds();
        vm.map.invalidateSize(true);
        vm.map.bounds = vm.map.createBounds();

        vm.tooltipActionsMap = {};
        var descriptors = vm.ctx.actionsApi.getActionDescriptors('tooltipAction');
        descriptors.forEach(function (descriptor) {
            if (descriptor) vm.tooltipActionsMap[descriptor.name] = descriptor;
        });
    }

    function configureStaticSettings() {
        let staticSettings = {};
        vm.staticSettings = staticSettings;
        //Calculate General Settings
        staticSettings.normalizationStep = vm.ctx.settings.normalizationStep || 1000;
        staticSettings.buttonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.54).toRgbString();
        staticSettings.disabledButtonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.3).toRgbString();
        staticSettings.polygonColor = tinycolor(vm.ctx.settings.polygonColor).toHexString();
        staticSettings.polygonStrokeColor = tinycolor(vm.ctx.settings.polygonStrokeColor).toHexString();
        staticSettings.mapProvider = vm.ctx.settings.mapProvider ? {name: vm.ctx.settings.mapProvider} : {name: "OpenStreetMap.Mapnik"};
        staticSettings.disableScrollZooming = vm.ctx.settings.disableScrollZooming || false;
        staticSettings.latKeyName = vm.ctx.settings.latKeyName || "latitude";
        staticSettings.lngKeyName = vm.ctx.settings.lngKeyName || "longitude";
        staticSettings.polKeyName = vm.ctx.settings.polKeyName || "coordinates";
        staticSettings.rotationAngle = vm.ctx.settings.rotationAngle || 0;
        staticSettings.polygonOpacity = vm.ctx.settings.polygonOpacity || 0.5;
        staticSettings.polygonStrokeOpacity = vm.ctx.settings.polygonStrokeOpacity || 1;
        staticSettings.polygonStrokeWeight = vm.ctx.settings.polygonStrokeWeight || 1;
        staticSettings.showPolygon = vm.ctx.settings.showPolygon || false;
        staticSettings.usePolygonColorFunction = vm.ctx.settings.usePolygonColorFunction || false;
        staticSettings.usePolygonTooltipFunction = vm.ctx.settings.usePolygonTooltipFunction || false;
        staticSettings.displayTooltip = vm.ctx.settings.showTooltip || false;
        staticSettings.defaultZoomLevel = vm.ctx.settings.defaultZoomLevel || true;
        staticSettings.showTooltip = false;
        staticSettings.label = vm.ctx.settings.label || "${entityName}";
        staticSettings.useLabelFunction = vm.ctx.settings.useLabelFunction || false;
        staticSettings.autocloseTooltip = vm.ctx.settings.autocloseTooltip || false;
        staticSettings.pointTooltipOnRightPanel = vm.ctx.settings.pointTooltipOnRightPanel || false;
        staticSettings.usePointAsAnchor = vm.ctx.settings.usePointAsAnchor || false;
        staticSettings.showLabel = vm.ctx.settings.showLabel || false;
        staticSettings.useTooltipFunction = vm.ctx.settings.useTooltipFunction || false;
        staticSettings.usePolylineDecorator = vm.ctx.settings.usePolylineDecorator || false;
        staticSettings.useDecoratorCustomColor = vm.ctx.settings.useDecoratorCustomColor || false;
        staticSettings.decoratorCustomColor = tinycolor(vm.ctx.settings.decoratorCustomColor).toHexString();
        staticSettings.decoratorSymbol = vm.ctx.settings.decoratorSymbol || "arrowHead";
        staticSettings.decoratorSymbolSize = vm.ctx.settings.decoratorSymbolSize || 10;
        staticSettings.decoratorOffset = vm.ctx.settings.decoratorOffset || "20px";
        staticSettings.endDecoratorOffset = vm.ctx.settings.endDecoratorOffset || "20px";
        staticSettings.decoratorRepeat = vm.ctx.settings.decoratorRepeat || "20px";
        staticSettings.tooltipPattern = vm.ctx.settings.tooltipPattern || "<span style=\"font-size: 26px; color: #666; font-weight: bold;\">${entityName}</span>\n" +
            "<br/>\n" +
            "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Time:</span><span style=\"font-size: 12px;\"> ${formattedTs}</span>\n" +
            "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Latitude:</span> ${latitude:7}\n" +
            "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Longitude:</span> ${longitude:7}";
        staticSettings.polygonTooltipPattern = vm.ctx.settings.polygonTooltipPattern || "<span style=\"font-size: 26px; color: #666; font-weight: bold;\">${entityName}</span>\n" +
            "<br/>\n" +
            "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Time:</span><span style=\"font-size: 12px;\"> ${formattedTs}</span>\n";
        staticSettings.tooltipOpacity = angular.isNumber(vm.ctx.settings.tooltipOpacity) ? vm.ctx.settings.tooltipOpacity : 1;
        staticSettings.tooltipColor = vm.ctx.settings.tooltipColor ? tinycolor(vm.ctx.settings.tooltipColor).toRgbString() : "#ffffff";
        staticSettings.tooltipFontColor = vm.ctx.settings.tooltipFontColor ? tinycolor(vm.ctx.settings.tooltipFontColor).toRgbString() : "#000000";
        staticSettings.pathColor = vm.ctx.settings.color ? tinycolor(vm.ctx.settings.color).toHexString() : "#ff6300";
        staticSettings.pathWeight = vm.ctx.settings.strokeWeight || 1;
        staticSettings.pathOpacity = vm.ctx.settings.strokeOpacity || 1;
        staticSettings.usePathColorFunction = vm.ctx.settings.useColorFunction || false;
        staticSettings.showPoints = vm.ctx.settings.showPoints || false;
        staticSettings.pointSize = vm.ctx.settings.pointSize || 1;
        staticSettings.markerImageSize = vm.ctx.settings.markerImageSize || 20;
        staticSettings.useMarkerImageFunction = vm.ctx.settings.useMarkerImageFunction || false;
        staticSettings.pointColor = vm.ctx.settings.pointColor ? tinycolor(vm.ctx.settings.pointColor).toHexString() : "#ff6300";
        staticSettings.markerImages = vm.ctx.settings.markerImages || [];
        staticSettings.icon = L.icon({
            iconUrl: "data:image/png;base64,[encoded data, 16448 characters]",
            iconSize: [30, 30],
            iconAnchor: [15, 15]
        });

        if (vm.ctx.settings.useCustomProvider && vm.ctx.settings.customProviderTileUrl) {
            staticSettings.mapProvider.name = vm.ctx.settings.customProviderTileUrl;
            staticSettings.mapProvider.isCustom = true;
        }

        if (angular.isDefined(vm.ctx.settings.markerImage)) {
            staticSettings.icon = L.icon({
                iconUrl: vm.ctx.settings.markerImage,
                iconSize: [staticSettings.markerImageSize, staticSettings.markerImageSize],
                iconAnchor: [(staticSettings.markerImageSize / 2), (staticSettings.markerImageSize / 2)]
            })
        }

        if (staticSettings.usePathColorFunction && angular.isDefined(vm.ctx.settings.colorFunction)) {
            staticSettings.colorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.colorFunction);
        }

        if (staticSettings.usePointAsAnchor && angular.isDefined(vm.ctx.settings.pointAsAnchorFunction)) {
            staticSettings.pointAsAnchorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.pointAsAnchorFunction);
        }

        if (staticSettings.usePolygonTooltipFunction && angular.isDefined(vm.ctx.settings.polygonTooltipFunction)) {
            staticSettings.polygonTooltipFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.polygonTooltipFunction);
        }

        if (staticSettings.useLabelFunction && angular.isDefined(vm.ctx.settings.labelFunction)) {
            staticSettings.labelFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.labelFunction);
        }

        if (staticSettings.useTooltipFunction && angular.isDefined(vm.ctx.settings.tooltipFunction)) {
            staticSettings.tooltipFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.tooltipFunction);
        }

        if (staticSettings.usePolygonColorFunction && angular.isDefined(vm.ctx.settings.polygonColorFunction)) {
            staticSettings.polygonColorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.polygonColorFunction);
        }

        if (staticSettings.useMarkerImageFunction && angular.isDefined(vm.ctx.settings.markerImageFunction)) {
            staticSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', vm.ctx.settings.markerImageFunction);
        }

        if (!staticSettings.useMarkerImageFunction &&
            angular.isDefined(vm.ctx.settings.markerImage) &&
            vm.ctx.settings.markerImage.length > 0) {
            staticSettings.useMarkerImage = true;
            let url = vm.ctx.settings.markerImage;
            let size = staticSettings.markerImageSize || 20;
            staticSettings.currentImage = {
                url: url,
                size: size
            };
            vm.utils.loadImageAspect(staticSettings.currentImage.url).then(
                (aspect) => {
                    if (aspect) {
                        let width;
                        let height;
                        if (aspect > 1) {
                            width = staticSettings.currentImage.size;
                            height = staticSettings.currentImage.size / aspect;
                        } else {
                            width = staticSettings.currentImage.size * aspect;
                            height = staticSettings.currentImage.size;
                        }
                        staticSettings.icon = L.icon({
                            iconUrl: staticSettings.currentImage.url,
                            iconSize: [width, height],
                            iconAnchor: [width / 2, height / 2]
                        });
                    }
                    if (vm.trips) {
                        vm.trips.forEach(function (trip) {
                            if (trip.marker) {
                                trip.marker.setIcon(staticSettings.icon);
                            }
                        });
                    }
                }
            )
        }
    }

    function configureTripSettings(trip, apply) {
        trip.settings = {};
        trip.settings.color = calculateColor(trip);
        trip.settings.polygonColor = calculatePolygonColor(trip);
        trip.settings.strokeWeight = vm.staticSettings.pathWeight;
        trip.settings.strokeOpacity = vm.staticSettings.pathOpacity;
        trip.settings.pointColor = vm.staticSettings.pointColor;
        trip.settings.polygonStrokeColor = vm.staticSettings.polygonStrokeColor;
        trip.settings.polygonStrokeOpacity = vm.staticSettings.polygonStrokeOpacity;
        trip.settings.polygonOpacity = vm.staticSettings.polygonOpacity;
        trip.settings.polygonStrokeWeight = vm.staticSettings.polygonStrokeWeight;
        trip.settings.pointSize = vm.staticSettings.pointSize;
        trip.settings.icon = calculateIcon(trip);
        if (apply) {
            $timeout(() => {
                trip.settings.labelText = calculateLabel(trip);
                trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip));
                trip.settings.polygonTooltipText = $sce.trustAsHtml(calculatePolygonTooltip(trip));
            }, 0, true);
        } else {
            trip.settings.labelText = calculateLabel(trip);
            trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip));
            trip.settings.polygonTooltipText = $sce.trustAsHtml(calculatePolygonTooltip(trip));
        }
    }

    function calculateLabel(trip) {
        let label = '';
        if (vm.staticSettings.showLabel) {
            let labelReplaceInfo;
            let labelText = vm.staticSettings.label;
            if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) {
                try {
                    labelText = vm.staticSettings.labelFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dsIndex);
                } catch (e) {
                    labelText = null;
                }
            }
            labelText = vm.utils.createLabelFromDatasource(trip.dataSource, labelText);
            labelReplaceInfo = processPattern(labelText, vm.ctx.datasources, trip.dSIndex);
            label = fillPattern(labelText, labelReplaceInfo, calculateCurrentDate(trip.timeRange, vm.index));
            if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) {
                try {
                    labelText = vm.staticSettings.labelFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex);
                } catch (e) {
                    labelText = null;
                }
            }
        }
        return label;
    }

    function calculateTooltip(trip) {
        let tooltip = '';
        if (vm.staticSettings.displayTooltip) {
            let tooltipReplaceInfo;
            let tooltipText = vm.staticSettings.tooltipPattern;
            if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) {
                try {
                    tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex);
                } catch (e) {
                    tooltipText = null;
                }
            }
            tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText);
            tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex);
            tooltip = fillPattern(tooltipText, tooltipReplaceInfo, calculateCurrentDate(trip.timeRange, vm.index));
            tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null);

        }
        return tooltip;
    }

    function calculatePolygonTooltip(trip) {
        let tooltip = '';
        if (vm.staticSettings.displayTooltip) {
            let tooltipReplaceInfo;
            let tooltipText = vm.staticSettings.polygonTooltipPattern;
            if (vm.staticSettings.usePolygonTooltipFunction && angular.isDefined(vm.staticSettings.polygonTooltipFunction)) {
                try {
                    tooltipText = vm.staticSettings.polygonTooltipFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex);
                } catch (e) {
                    tooltipText = null;
                }
            }
            tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText);
            tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex);
            tooltip = fillPattern(tooltipText, tooltipReplaceInfo, calculateCurrentDate(trip.timeRange, vm.index));
            tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null);

        }
        return tooltip;
    }

    function calculatePointTooltip(trip, index) {
        let tooltip = '';
        if (vm.staticSettings.displayTooltip) {
            let tooltipReplaceInfo;
            let tooltipText = vm.staticSettings.tooltipPattern;
            if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) {
                try {
                    tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, index), trip.dSIndex);
                } catch (e) {
                    tooltipText = null;
                }
            }
            tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText);
            tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex);
            tooltip = fillPattern(tooltipText, tooltipReplaceInfo, calculateCurrentDate(trip.timeRange, index));
            tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null);

        }
        return tooltip;
    }

    function calculateColor(trip) {
        let color = vm.staticSettings.pathColor;
        let colorFn;
        if (vm.staticSettings.usePathColorFunction && angular.isDefined(vm.staticSettings.colorFunction)) {
            try {
                colorFn = vm.staticSettings.colorFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex);
            } catch (e) {
                colorFn = null;
            }
        }
        if (colorFn && colorFn !== color && trip.polyline) {
            trip.polyline.setStyle({color: colorFn});
        }
        return colorFn || color;
    }

    function calculatePolygonColor(trip) {
        let color = vm.staticSettings.polygonColor;
        let colorFn;
        if (vm.staticSettings.usePolygonColorFunction && angular.isDefined(vm.staticSettings.polygonColorFunction)) {
            try {
                colorFn = vm.staticSettings.polygonColorFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex);
            } catch (e) {
                colorFn = null;
            }
        }
        if (colorFn && colorFn !== color && trip.polygon) {
            trip.polygon.setStyle({fillColor: colorFn});
        }
        return colorFn || color;
    }

    function calculateIcon(trip) {
        let icon = vm.staticSettings.icon;
        if (vm.staticSettings.useMarkerImageFunction && angular.isDefined(vm.staticSettings.markerImageFunction)) {
            let rawIcon;
            try {
                rawIcon = vm.staticSettings.markerImageFunction(vm.ctx.data, vm.staticSettings.markerImages, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex);
            } catch (e) {
                rawIcon = null;
            }
            if (rawIcon) {
                vm.utils.loadImageAspect(rawIcon).then(
                    (aspect) => {
                        if (aspect) {
                            let width;
                            let height;
                            if (aspect > 1) {
                                width = rawIcon.size;
                                height = rawIcon.size / aspect;
                            } else {
                                width = rawIcon.size * aspect;
                                height = rawIcon.size;
                            }
                            icon = L.icon({
                                iconUrl: rawIcon,
                                iconSize: [width, height],
                                iconAnchor: [width / 2, height / 2]
                            });
                        }
                        if (trip.marker) {
                            trip.marker.setIcon(icon);
                        }
                    }
                )
            }
        }
        return icon;
    }

    function createUpdatePath(apply) {
        if (vm.trips && vm.map) {
            vm.trips.forEach(function (trip) {
                if (trip.marker) {
                    trip.marker.remove();
                    delete trip.marker;
                }
                if (trip.polyline) {
                    trip.polyline.remove();
                    delete trip.polyline;
                }
                if (trip.polylineDecorator) {
                    trip.polylineDecorator.remove();
                    delete trip.polylineDecorator;
                }
                if (trip.polygon) {
                    trip.polygon.remove();
                    delete trip.polygon;
                }
                if (trip.points && trip.points.length) {
                    trip.points.forEach(function (point) {
                        point.remove();
                    });
                    delete trip.points;
                }
            });
            vm.initBounds = true;
        }
        createNormalizedTime(vm.data, vm.staticSettings.normalizationStep);
        createNormalizedTrips(vm.datasources, vm.data, vm.staticSettings.normalizationStep);
        createTripsOnMap(apply);
        if (vm.initBounds && !vm.initTrips) {
            vm.trips.forEach(function (trip) {
                vm.map.extendBounds(vm.map.bounds, trip.polyline);
                vm.initBounds = !vm.datasources.every(
                    function (ds) {
                        return ds.dataReceived === true;
                    });
                vm.initTrips = vm.trips.every(function (trip) {
                    return angular.isDefined(trip.marker) && angular.isDefined(trip.polyline);
                });
            });

            vm.map.fitBounds(vm.map.bounds);
        }
    }

    function fillPattern(pattern, replaceInfo, currentNormalizedValue) {
        let text = angular.copy(pattern);
        let reg = /\$\{([^}]*)\}/g;
        if (replaceInfo) {
            for (let v = 0; v < replaceInfo.variables.length; v++) {
                let variableInfo = replaceInfo.variables[v];
                let label = reg.exec(pattern)[1].split(":")[0];
                let txtVal = '';
                if (label.length > -1 && angular.isDefined(currentNormalizedValue[label])) {
                    let varData = currentNormalizedValue[label];
                    if (isNumber(varData)) {
                        txtVal = padValue(varData, variableInfo.valDec, 0);
                    } else {
                        txtVal = varData;
                    }
                }
                text = text.split(variableInfo.variable).join(txtVal);
            }
        }
        return text;
    }

    function createNormalizedTime(data, step) {
        if (!step) step = 1000;
        let max_time = -Infinity;
        let min_time = Infinity;
        if (data) {
            for (let i = 0; i < data.length; i++) {
                for (let j = 0; j < data[i].data.length; j++) {
                    if (max_time < data[i].data[j][0]) {
                        max_time = data[i].data[j][0]
                    }
                    if (min_time > data[i].data[j][0]) {
                        min_time = data[i].data[j][0];
                    }
                }
            }
        }
        vm.minTime = vm.animationTime = min_time;
        if(min_time === Infinity){
            vm.animationTime = null;
        } else {
            vm.animationTime = min_time
        }
        vm.maxTimeIndex = Math.ceil((max_time - min_time) / step);
        if (vm.index < vm.minTimeIndex) {
            vm.index = vm.minTimeIndex;
        } else if (vm.index > vm.maxTimeIndex) {
            vm.index = vm.maxTimeIndex;
        }
    }

    function createNormalizedTrips(dataSources, data, step) {
        vm.trips = [];
        step = step || 1000;
        if (dataSources && data) {
            for (let i = 0; i < dataSources.length; i++) {
                vm.trips.push({
                    dataSource: dataSources[i],
                    dSIndex: i,
                    timeRange: {}
                })
            }

            for (let i = 0; i < data.length; i++) {
                let ds = data[i].datasource;
                let tripIndex = vm.trips.findIndex(function (el) {
                    return el.dataSource.entityId === ds.entityId;
                });
                if (tripIndex > -1) {
                    createNormalizedValue(data[i].data, data[i].dataKey.label, vm.trips[tripIndex].timeRange, step);
                }
            }
        }

        createNormalizedLatLngs();
    }

    function createNormalizedValue(dataArray, dataKey, timeRange, step) {
        for (let i = 0; i < dataArray.length; i++) {
            let normalizeTime = vm.minTime + Math.ceil((dataArray[i][0] - vm.minTime) / step) * step;
            timeRange[normalizeTime] = timeRange[normalizeTime] || {};
            timeRange[normalizeTime][dataKey] = dataArray[i][1];
        }
    }

    function createNormalizedLatLngs() {
        vm.trips.forEach(function (item) {
            item.latLngs = [];
            for (let timestamp in item.timeRange) {
                if(Object.prototype.hasOwnProperty.call(item.timeRange, timestamp)) {
                    let lat = item.timeRange[timestamp][vm.staticSettings.latKeyName];
                    let lng = item.timeRange[timestamp][vm.staticSettings.lngKeyName];
                    if (lat && lng && vm.map) {
                        item.timeRange[timestamp].latLng = vm.map.createLatLng(lat, lng);
                    }
                    item.latLngs.push(item.timeRange[timestamp].latLng);
                }
            }
        });
    }

    function createPointPopup(point, index, trip) {
        let popup = L.popup();
        popup.setContent(calculatePointTooltip(trip, index));
        point.bindPopup(popup, {autoClose: vm.staticSettings.autocloseTooltip, closeOnClick: false});
        return popup;
    }

    function createTripsOnMap(apply) {
        if (vm.trips.length > 0) {
            vm.trips.forEach(function (trip) {
                configureTripSettings(trip, apply);
                if (Object.keys(trip.timeRange).length > 0 && trip.latLngs.every(el => angular.isDefined(el))) {
                    if (vm.staticSettings.showPoints) {
                        trip.points = [];
                        Object.keys(trip.timeRange).forEach(function (tRange, index) {
                            if (trip.timeRange[tRange] && trip.timeRange[tRange].latLng
                                && (!vm.staticSettings.usePointAsAnchor || vm.staticSettings.pointAsAnchorFunction(vm.ctx.data, trip.timeRange[tRange], trip.dSIndex))) {
                                let point = L.circleMarker(trip.timeRange[tRange].latLng, {
                                    color: trip.settings.pointColor,
                                    radius: trip.settings.pointSize
                                }).addTo(vm.map.map);
                                if (vm.staticSettings.pointTooltipOnRightPanel) {
                                    point.popup = createPointPopup(point, index, trip);
                                } else {
                                    point.on('click', function () {
                                        showHidePointTooltip(calculatePointTooltip(trip, index), index);
                                    });
                                }
                                if (vm.staticSettings.usePointAsAnchor) trip.timeRange[tRange].hasAnchor = true;
                                trip.points.push(point);
                            }
                        });
                    }

                    if (angular.isUndefined(trip.marker)) {
                        trip.polyline = vm.map.createPolyline(trip.latLngs, trip.settings);
                        if (vm.staticSettings.usePolylineDecorator) {
                            trip.polylineDecorator = L.polylineDecorator(trip.polyline, {
                                patterns: [
                                    {
                                        offset: vm.staticSettings.decoratorOffset,
                                        endOffset: vm.staticSettings.endDecoratorOffset,
                                        repeat: vm.staticSettings.decoratorRepeat,
                                        symbol: L.Symbol[vm.staticSettings.decoratorSymbol]({
                                            pixelSize: vm.staticSettings.decoratorSymbolSize,
                                            polygon: false,
                                            pathOptions: {
                                                color: vm.staticSettings.useDecoratorCustomColor ? vm.staticSettings.decoratorCustomColor : trip.settings.color,
                                                stroke: true
                                            }
                                        })
                                    }
                                ],
                                interactive: false,
                            }).addTo(vm.map.map);
                        }
                    }


                    if (trip.timeRange && Object.keys(trip.timeRange).length && angular.isUndefined(trip.marker)) {
                        trip.marker = L.marker(calculateCurrentDate(trip.timeRange, vm.index).latLng);
                        trip.marker.setZIndexOffset(1000);
                        trip.marker.setIcon(vm.staticSettings.icon);
                        trip.marker.setRotationOrigin('center center');
                        trip.marker.on('click', function () {
                            showHideTooltip(trip);
                        });
                        trip.marker.addTo(vm.map.map);
                        moveMarker(trip);
                    }
                }

                if (vm.staticSettings.showPolygon && angular.isDefined(calculateCurrentDate(trip.timeRange, vm.index)[vm.staticSettings.polKeyName])) {
                    let polygonSettings = {
                        fill: true,
                        fillColor: trip.settings.polygonColor,
                        color: trip.settings.polygonStrokeColor,
                        weight: trip.settings.polygonStrokeWeight,
                        fillOpacity: trip.settings.polygonOpacity,
                        opacity: trip.settings.polygonStrokeOpacity
                    };
                    let polygonLatLngsRaw = mapPolygonArray(angular.fromJson(calculateCurrentDate(trip.timeRange, vm.index)[vm.staticSettings.polKeyName]));
                    trip.polygon = L.polygon(polygonLatLngsRaw, polygonSettings).addTo(vm.map.map);
                    trip.polygon.on('click',function(){showHidePolygonTooltip(trip)});
                }
            });
        }
    }

    function calculateCurrentDate(tripTimeRange, index) {
        let time = vm.minTime + index * vm.staticSettings.normalizationStep;
        if (Object.hasOwnProperty.call(tripTimeRange, time)) {
            return tripTimeRange[time];
        } else {
            let timeInterval = Object.keys(tripTimeRange);
            for (let i = 1; i < timeInterval.length; i++) {
                if (timeInterval[i - 1] < time && timeInterval[i] > time) {
                    let calcPosition = angular.copy(tripTimeRange[timeInterval[i - 1]]);
                    let startLatLng = tripTimeRange[timeInterval[i - 1]].latLng;
                    let finishLatLng = tripTimeRange[timeInterval[i]].latLng;
                    let percentRouteComplete = (time - timeInterval[i - 1]) / (timeInterval[i] - timeInterval[i - 1]);
                    calcPosition[vm.staticSettings.latKeyName] = startLatLng.lat + (finishLatLng.lat - startLatLng.lat) * percentRouteComplete;
                    calcPosition[vm.staticSettings.lngKeyName] = startLatLng.lng + (finishLatLng.lng - startLatLng.lng) * percentRouteComplete;
                    calcPosition.latLng = vm.map.createLatLng(calcPosition[vm.staticSettings.latKeyName], calcPosition[vm.staticSettings.lngKeyName]);
                    calcPosition.angle = vm.staticSettings.rotationAngle + findAngle(startLatLng, finishLatLng);
                    return calcPosition;
                }
            }
        }
        return {};
    }

    function findAngle(startPoint, endPoint) {
        let angle = -Math.atan2(endPoint.lat - startPoint.lat, endPoint.lng - startPoint.lng);
        angle = angle * 180 / Math.PI;
        return parseInt(angle.toFixed(2));
    }

    function mapPolygonArray(rawArray) {
        return rawArray.map(function (el) {
            if (el.length === 2) {
                if (!angular.isNumber(el[0]) && !angular.isNumber(el[1])) {
                    return el.map(function (subEl) {
                        return mapPolygonArray(subEl);
                    })
                } else {
                    return vm.map.createLatLng(el[0], el[1]);
                }
            } else if (el.length > 2) {
                return mapPolygonArray(el);
            } else {
                return vm.map.createLatLng(false);
            }
        });
    }

    function moveMarker(trip) {
        let postionMarker = calculateCurrentDate(trip.timeRange, vm.index);
        if (angular.isDefined(postionMarker)) {
            if (angular.isDefined(trip.marker)) {
                trip.markerAngleIsSet = true;
                trip.marker.setLatLng(postionMarker.latLng);
                trip.marker.setRotationAngle(postionMarker.angle);
                trip.marker.update();
            } else {
                if (trip.timeRange && trip.timeRange.length) {
                    trip.marker = L.marker(postionMarker.latLng);
                    trip.marker.setZIndexOffset(1000);
                    trip.marker.setIcon(vm.staticSettings.icon);
                    trip.marker.setRotationOrigin('center center');
                    trip.marker.on('click', function () {
                        showHideTooltip(trip);
                    });
                    trip.marker.addTo(vm.map.map);
                    trip.marker.update();
                }
            }
        }
        configureTripSettings(trip);
    }


    function showHideTooltip(trip) {
        if (vm.staticSettings.displayTooltip) {
            if (vm.staticSettings.showTooltip && trip && (vm.activeTripIndex !== trip.dSIndex || vm.staticSettings.tooltipMarker !== 'marker')) {
                vm.staticSettings.showTooltip = true;
                vm.staticSettings.tooltipMarker = 'marker';
            } else {
                vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
            }
        }
        if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex;
        if (vm.trips.length) {
            vm.mainTooltip = vm.trips[vm.activeTripIndex].settings.tooltipText;
        }
    }

    function showHidePointTooltip(text, index) {
        if (vm.staticSettings.displayTooltip) {
            if (vm.staticSettings.tooltipMarker && vm.staticSettings.tooltipMarker.includes('point')) {
               if (vm.staticSettings.tooltipMarker === 'point' + index) {
                   vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
               } else {
                   vm.staticSettings.showTooltip = true;
                   vm.mainTooltip = $sce.trustAsHtml(text);
                   vm.staticSettings.tooltipMarker = 'point' + index;
               }
            } else {
                vm.staticSettings.showTooltip = true;
                vm.mainTooltip = $sce.trustAsHtml(text);
                vm.staticSettings.tooltipMarker = 'point' + index;
            }
        }
    }

    function showHidePolygonTooltip(trip) {
        if (vm.staticSettings.displayTooltip) {
            if (vm.staticSettings.showTooltip && trip && (vm.activeTripIndex !== trip.dSIndex || vm.staticSettings.tooltipMarker !== 'polygon')) {
                vm.staticSettings.showTooltip = true;
                vm.staticSettings.tooltipMarker = 'polygon';
            } else {
                vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
            }
        }
        if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex;
        vm.mainTooltip = vm.trips[vm.activeTripIndex].settings.polygonTooltipText;
    }
}
