map-widget.js

610 lines | 25.752 kB Blame History Raw Download
/*
 * Copyright © 2016-2018 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 tinycolor from 'tinycolor2';

import TbGoogleMap from './google-map';
import TbOpenStreetMap from './openstreet-map';

function procesTooltipPattern(tbMap, pattern, datasources, dsIndex) {
    var match = tbMap.varsRegex.exec(pattern);
    var replaceInfo = {};
    replaceInfo.variables = [];
    while (match !== null) {
        var variableInfo = {};
        variableInfo.dataKeyIndex = -1;
        var variable = match[0];
        var label = match[1];
        var valDec = 2;
        var splitVals = label.split(':');
        if (splitVals.length > 1) {
            label = splitVals[0];
            valDec = parseFloat(splitVals[1]);
        }
        variableInfo.variable = variable;
        variableInfo.valDec = valDec;

        if (label.startsWith('#')) {
            var keyIndexStr = label.substring(1);
            var n = Math.floor(Number(keyIndexStr));
            if (String(n) === keyIndexStr && n >= 0) {
                variableInfo.dataKeyIndex = n;
            }
        }
        if (variableInfo.dataKeyIndex === -1) {
            var offset = 0;
            for (var i=0;i<datasources.length;i++) {
                var datasource = datasources[i];
                if (angular.isUndefined(dsIndex) || dsIndex == i) {
                    for (var k = 0; k < datasource.dataKeys.length; k++) {
                        var dataKey = datasource.dataKeys[k];
                        if (dataKey.label === label) {
                            variableInfo.dataKeyIndex = offset + k;
                            break;
                        }
                    }
                }
                offset += datasource.dataKeys.length;
            }
        }
        replaceInfo.variables.push(variableInfo);
        match = tbMap.varsRegex.exec(pattern);
    }
    return replaceInfo;
}


export default class TbMapWidget {
    constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) {

        var tbMap = this;
        this.ctx = ctx;
        if (!$element) {
            $element = ctx.$container;
        }
        this.utils = ctx.$scope.$injector.get('utils');
        this.drawRoutes = drawRoutes;
        this.markers = [];
        if (this.drawRoutes) {
            this.polylines = [];
        }
        this.locationsSettings = [];
        this.varsRegex = /\$\{([^\}]*)\}/g;

        var settings = ctx.settings;

        this.callbacks = {};
        this.callbacks.onLocationClick = function(){};

        if (settings.defaultZoomLevel) {
            if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {
                this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel);
            }
        }

        this.dontFitMapBounds = settings.fitMapBounds === false;

        if (!useDynamicLocations) {
            this.subscription = this.ctx.defaultSubscription;
            this.configureLocationsFromSettings();
        }

        var minZoomLevel = this.drawRoutes ? 18 : 15;

        var initCallback = function() {
              tbMap.update();
              tbMap.resize();
        };

        if (mapProvider === 'google-map') {
            this.map = new TbGoogleMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType);
        } else if (mapProvider === 'openstreet-map') {
            this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel);
        }

    }

    setCallbacks(callbacks) {
        Object.assign(this.callbacks, callbacks);
    }

    clearLocations() {
        if (this.locations) {
            var tbMap = this;
            this.locations.forEach(function(location) {
                if (location.marker) {
                    tbMap.map.removeMarker(location.marker);
                }
                if (location.polyline) {
                    tbMap.map.removePolyline(location.polyline);
                }
            });
            this.locations = null;
            this.markers = [];
            if (this.drawRoutes) {
                this.polylines = [];
            }
        }
    }

    configureLocationsFromSubscription(subscription, subscriptionLocationSettings) {
        this.subscription = subscription;
        this.clearLocations();
        this.locationsSettings = [];
        var latKeyName = subscriptionLocationSettings.latKeyName;
        var lngKeyName = subscriptionLocationSettings.lngKeyName;
        var index = 0;
        for (var i=0;i<subscription.datasources.length;i++) {
            var datasource = subscription.datasources[i];
            var dataKeys = datasource.dataKeys;
            var latKeyIndex = -1;
            var lngKeyIndex = -1;
            var localLatKeyName = latKeyName;
            var localLngKeyName = lngKeyName;
            for (var k=0;k<dataKeys.length;k++) {
                var dataKey = dataKeys[k];
                if (dataKey.name === latKeyName) {
                    latKeyIndex = index;
                    localLatKeyName = localLatKeyName + index;
                    dataKey.locationAttrName = localLatKeyName;
                } else if (dataKey.name === lngKeyName) {
                    lngKeyIndex = index;
                    localLngKeyName = localLngKeyName + index;
                    dataKey.locationAttrName = localLngKeyName;
                }
                if (latKeyIndex > -1 && lngKeyIndex > -1) {
                    var locationsSettings = {
                        latKeyName: localLatKeyName,
                        lngKeyName: localLngKeyName,
                        showLabel: subscriptionLocationSettings.showLabel !== false,
                        displayTooltip: subscriptionLocationSettings.displayTooltip !== false,
                        label: datasource.name,
                        labelText: datasource.name,
                        labelColor: subscriptionLocationSettings.labelColor || this.ctx.widgetConfig.color || '#000000',
                        color: "#FE7569",
                        useColorFunction: false,
                        colorFunction: null,
                        markerImage: null,
                        markerImageSize: 34,
                        useMarkerImage: false,
                        useMarkerImageFunction: false,
                        markerImageFunction: null,
                        markerImages: [],
                        tooltipPattern: subscriptionLocationSettings.tooltipPattern || "<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}"
                    };

                    locationsSettings.tooltipReplaceInfo = procesTooltipPattern(this, locationsSettings.tooltipPattern, this.subscription.datasources, i);

                    locationsSettings.useColorFunction = subscriptionLocationSettings.useColorFunction === true;
                    if (angular.isDefined(subscriptionLocationSettings.colorFunction) && subscriptionLocationSettings.colorFunction.length > 0) {
                        try {
                            locationsSettings.colorFunction = new Function('data, dsData, dsIndex', subscriptionLocationSettings.colorFunction);
                        } catch (e) {
                            locationsSettings.colorFunction = null;
                        }
                    }

                    this.locationsSettings.push(locationsSettings);
                    latKeyIndex = -1;
                    lngKeyIndex = -1;
                }
                index++;
            }
        }
    }

    configureLocationsFromSettings() {
        var configuredLocationsSettings = this.drawRoutes ? this.ctx.settings.routesSettings : this.ctx.settings.markersSettings;
        if (!configuredLocationsSettings) {
            configuredLocationsSettings = [];
        }

        for (var i=0;i<configuredLocationsSettings.length;i++) {
            this.locationsSettings[i] = {
                latKeyName: "lat",
                lngKeyName: "lng",
                showLabel: true,
                displayTooltip: true,
                label: "",
                labelText: "",
                labelColor: this.ctx.widgetConfig.color || '#000000',
                color: "#FE7569",
                useColorFunction: false,
                colorFunction: null,
                markerImage: null,
                markerImageSize: 34,
                useMarkerImage: false,
                useMarkerImageFunction: false,
                markerImageFunction: null,
                markerImages: [],
                tooltipPattern: "<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}"
            };

            if (this.drawRoutes) {
                this.locationsSettings[i].strokeWeight = 2;
                this.locationsSettings[i].strokeOpacity = 1.0;
            }

            if (configuredLocationsSettings[i]) {
                this.locationsSettings[i].latKeyName = configuredLocationsSettings[i].latKeyName || this.locationsSettings[i].latKeyName;
                this.locationsSettings[i].lngKeyName = configuredLocationsSettings[i].lngKeyName || this.locationsSettings[i].lngKeyName;

                this.locationsSettings[i].tooltipPattern = configuredLocationsSettings[i].tooltipPattern || "<b>Latitude:</b> ${"+this.locationsSettings[i].latKeyName+":7}<br/><b>Longitude:</b> ${"+this.locationsSettings[i].lngKeyName+":7}";

                this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this, this.locationsSettings[i].tooltipPattern, this.subscription.datasources);

                this.locationsSettings[i].showLabel = configuredLocationsSettings[i].showLabel !== false;
                this.locationsSettings[i].label = configuredLocationsSettings[i].label || this.locationsSettings[i].label;
                this.locationsSettings[i].labelText = this.locationsSettings[i].label;
                this.locationsSettings[i].color = configuredLocationsSettings[i].color ? tinycolor(configuredLocationsSettings[i].color).toHexString() : this.locationsSettings[i].color;

                this.locationsSettings[i].useColorFunction = configuredLocationsSettings[i].useColorFunction === true;
                if (angular.isDefined(configuredLocationsSettings[i].colorFunction) && configuredLocationsSettings[i].colorFunction.length > 0) {
                    try {
                        this.locationsSettings[i].colorFunction = new Function('data, dsData, dsIndex', configuredLocationsSettings[i].colorFunction);
                    } catch (e) {
                        this.locationsSettings[i].colorFunction = null;
                    }
                }

                this.locationsSettings[i].useMarkerImageFunction = configuredLocationsSettings[i].useMarkerImageFunction === true;
                if (angular.isDefined(configuredLocationsSettings[i].markerImageFunction) && configuredLocationsSettings[i].markerImageFunction.length > 0) {
                    try {
                        this.locationsSettings[i].markerImageFunction = new Function('data, images, dsData, dsIndex', configuredLocationsSettings[i].markerImageFunction);
                    } catch (e) {
                        this.locationsSettings[i].markerImageFunction = null;
                    }
                }

                this.locationsSettings[i].markerImages = configuredLocationsSettings[i].markerImages || [];

                if (!this.locationsSettings[i].useMarkerImageFunction &&
                    angular.isDefined(configuredLocationsSettings[i].markerImage) &&
                    configuredLocationsSettings[i].markerImage.length > 0) {
                    this.locationsSettings[i].useMarkerImage = true;
                    var url = this.ctx.settings.markerImage;
                    var size = this.ctx.settings.markerImageSize || 34;
                    this.locationsSettings[i].currentImage = {
                        url: url,
                        size: size
                    };
                }

                if (this.drawRoutes) {
                    this.locationsSettings[i].strokeWeight = configuredLocationsSettings[i].strokeWeight || this.locationsSettings[i].strokeWeight;
                    this.locationsSettings[i].strokeOpacity = angular.isDefined(configuredLocationsSettings[i].strokeOpacity) ? configuredLocationsSettings[i].strokeOpacity : this.locationsSettings[i].strokeOpacity;
                }
            }
        }
    }

    update() {

        var tbMap = this;

        function isNumber(n) {
            return !isNaN(parseFloat(n)) && isFinite(n);
        }

        function padValue(val, dec, int) {
            var i = 0;
            var s, strVal, n;

            val = parseFloat(val);
            n = (val < 0);
            val = Math.abs(val);

            if (dec > 0) {
                strVal = val.toFixed(dec).toString().split('.');
                s = int - strVal[0].length;

                for (; i < s; ++i) {
                    strVal[0] = '0' + strVal[0];
                }

                strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];
            }

            else {
                strVal = Math.round(val).toString();
                s = int - strVal.length;

                for (; i < s; ++i) {
                    strVal = '0' + strVal;
                }

                strVal = (n ? '-' : '') + strVal;
            }

            return strVal;
        }

        function arraysEqual(a, b) {
            if (a === b) return true;
            if (a === null || b === null) return false;
            if (a.length != b.length) return false;

            for (var i = 0; i < a.length; ++i) {
                if (!a[i].equals(b[i])) return false;
            }
            return true;
        }

        function calculateLocationColor(location, dataMap) {
            if (location.settings.useColorFunction && location.settings.colorFunction) {
                var color = '#FE7569';
                try {
                    color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
                } catch (e) {
                    color = '#FE7569';
                }
                return tinycolor(color).toHexString();
            } else {
                return location.settings.color;
            }
        }

        function updateLocationColor(location, dataMap) {
            var color = calculateLocationColor(location, dataMap);
            if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) {
                if (!location.settings.useMarkerImage && !location.settings.useMarkerImageFunction) {
                    tbMap.map.updateMarkerColor(location.marker, color);
                }
                if (location.polyline) {
                    tbMap.map.updatePolylineColor(location.polyline, location.settings, color);
                }
                location.settings.calculatedColor = color;
            }
        }

        function calculateLocationMarkerImage(location, dataMap) {
            if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) {
                var image = null;
                try {
                    image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex);
                } catch (e) {
                    image = null;
                }
                return image;
            } else {
                return null;
            }
        }

        function updateLocationMarkerIcon(location, dataMap) {
            var image = calculateLocationMarkerImage(location, dataMap);
            if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) {
                location.settings.currentImage = image;
                tbMap.map.updateMarkerIcon(location.marker, location.settings);
            }
        }

        function updateLocationStyle(location, dataMap) {
            updateLocationColor(location, dataMap);
            updateLocationMarkerIcon(location, dataMap);
        }

        function createOrUpdateLocationMarker(location, markerLocation, dataMap) {
            var changed = false;
            if (!location.marker) {
                var image = calculateLocationMarkerImage(location, dataMap);
                if (image && (!location.settings.currentImage || !angular.equals(location.settings.currentImage, image))) {
                    location.settings.currentImage = image;
                }
                location.marker = tbMap.map.createMarker(markerLocation, location.dsIndex, location.settings,
                    function() {
                        tbMap.callbacks.onLocationClick(location);
                    }
                );
                tbMap.markers.push(location.marker);
                changed = true;
            } else {
                var prevPosition = tbMap.map.getMarkerPosition(location.marker);
                if (!prevPosition.equals(markerLocation)) {
                    tbMap.map.setMarkerPosition(location.marker, markerLocation);
                    changed = true;
                }
            }
            return changed;
        }

        function updateLocation(location, data, dataMap) {
            var locationChanged = false;
            if (location.latIndex > -1 && location.lngIndex > -1) {
                var latData = data[location.latIndex].data;
                var lngData = data[location.lngIndex].data;
                var lat, lng, latLng;
                if (latData.length > 0 && lngData.length > 0) {
                    if (tbMap.drawRoutes) {
                        // Create or update route
                        var latLngs = [];
                        for (var i = 0; i < latData.length; i++) {
                            lat = latData[i][1];
                            lng = lngData[i][1];
                            latLng = tbMap.map.createLatLng(lat, lng);
                            if (i == 0 || !latLngs[latLngs.length-1].equals(latLng)) {
                                latLngs.push(latLng);
                            }
                        }
                        if (latLngs.length > 0) {
                            var markerLocation = latLngs[latLngs.length-1];
                            createOrUpdateLocationMarker(location, markerLocation, dataMap);
                        }
                        if (!location.polyline) {
                            location.polyline = tbMap.map.createPolyline(latLngs, location.settings);
                            tbMap.polylines.push(location.polyline);
                            locationChanged = true;
                        } else {
                            var prevPath = tbMap.map.getPolylineLatLngs(location.polyline);
                            if (!prevPath || !arraysEqual(prevPath, latLngs)) {
                                tbMap.map.setPolylineLatLngs(location.polyline, latLngs);
                                locationChanged = true;
                            }
                        }
                    } else {
                        // Create or update marker
                        lat = latData[latData.length-1][1];
                        lng = lngData[lngData.length-1][1];
                        latLng = tbMap.map.createLatLng(lat, lng);
                        if (createOrUpdateLocationMarker(location, latLng, dataMap)) {
                            locationChanged = true;
                        }
                    }
                    updateLocationStyle(location, dataMap);
                }
            }
            return locationChanged;
        }

        function toLabelValueMap(data, datasources) {
            var dataMap = {};
            var dsDataMap = [];
            for (var d=0;d<datasources.length;d++) {
                dsDataMap[d] = {};
            }
            for (var i = 0; i < data.length; i++) {
                var dataKey = data[i].dataKey;
                var label = dataKey.label;
                var keyData = data[i].data;
                var val = null;
                if (keyData.length > 0) {
                    val = keyData[keyData.length-1][1];
                }
                dataMap[label] = val;
                var dsIndex = datasources.indexOf(data[i].datasource);
                dsDataMap[dsIndex][label] = val;
            }
            return {
                dataMap: dataMap,
                dsDataMap: dsDataMap
            };
        }

        function loadLocations(data, datasources) {
            var bounds = tbMap.map.createBounds();
            tbMap.locations = [];
            var dataMap = toLabelValueMap(data, datasources);
            for (var l=0; l < tbMap.locationsSettings.length; l++) {
                var locationSettings = tbMap.locationsSettings[l];
                var latIndex = -1;
                var lngIndex = -1;
                for (var i = 0; i < data.length; i++) {
                    var dataKey = data[i].dataKey;
                    var nameToCheck;
                    if (dataKey.locationAttrName) {
                        nameToCheck = dataKey.locationAttrName;
                    } else {
                        nameToCheck = dataKey.label;
                    }
                    if (nameToCheck === locationSettings.latKeyName) {
                        latIndex = i;
                    } else if (nameToCheck === locationSettings.lngKeyName) {
                        lngIndex = i;
                    }
                }
                if (latIndex > -1 && lngIndex > -1) {
                    var ds = data[latIndex].datasource;
                    var dsIndex = datasources.indexOf(ds);
                    var location = {
                        latIndex: latIndex,
                        lngIndex: lngIndex,
                        dsIndex: dsIndex,
                        settings: locationSettings
                    };
                    tbMap.locations.push(location);
                    updateLocation(location, data, dataMap);
                    if (location.polyline) {
                        tbMap.map.extendBounds(bounds, location.polyline);
                    } else if (location.marker) {
                        tbMap.map.extendBoundsWithMarker(bounds, location.marker);
                    }
                }
            }
            tbMap.map.fitBounds(bounds);
        }

        function updateLocations(data, datasources) {
            var locationsChanged = false;
            var bounds = tbMap.map.createBounds();
            var dataMap = toLabelValueMap(data, datasources);
            for (var p = 0; p < tbMap.locations.length; p++) {
                var location = tbMap.locations[p];
                locationsChanged |= updateLocation(location, data, dataMap);
                if (location.polyline) {
                    tbMap.map.extendBounds(bounds, location.polyline);
                } else if (location.marker) {
                    tbMap.map.extendBoundsWithMarker(bounds, location.marker);
                }
            }
            if (locationsChanged) {
                tbMap.map.fitBounds(bounds);
            }
        }

        if (this.map && this.map.inited() && this.subscription) {
            if (this.subscription.data) {
                if (!this.locations) {
                    loadLocations(this.subscription.data, this.subscription.datasources);
                } else {
                    updateLocations(this.subscription.data, this.subscription.datasources);
                }
                var tooltips = this.map.getTooltips();
                for (var t=0; t < tooltips.length; t++) {
                    var tooltip = tooltips[t];
                    var settings = tooltip.locationSettings;
                    var text = settings.tooltipPattern;
                    var replaceInfo = settings.tooltipReplaceInfo;
                    for (var v = 0; v < replaceInfo.variables.length; v++) {
                        var variableInfo = replaceInfo.variables[v];
                        var txtVal = '';
                        if (variableInfo.dataKeyIndex > -1 && this.subscription.data[variableInfo.dataKeyIndex]) {
                            var varData = this.subscription.data[variableInfo.dataKeyIndex].data;
                            if (varData.length > 0) {
                                var val = varData[varData.length - 1][1];
                                if (isNumber(val)) {
                                    txtVal = padValue(val, variableInfo.valDec, 0);
                                } else {
                                    txtVal = val;
                                }
                            }
                        }
                        text = text.split(variableInfo.variable).join(txtVal);
                    }
                    tooltip.popup.setContent(text);
                }
            }
        }
    }

    resize() {
        if (this.map && this.map.inited()) {
            this.map.invalidateSize();
            if (this.locations && this.locations.length > 0) {
                var bounds = this.map.createBounds();
                for (var m = 0; m < this.markers.length; m++) {
                    this.map.extendBoundsWithMarker(bounds, this.markers[m]);
                }
                if (this.polylines) {
                    for (var p = 0; p < this.polylines.length; p++) {
                        this.map.extendBounds(bounds, this.polylines[p]);
                    }
                }
                this.map.fitBounds(bounds);
            }
        }
    }

}