image-map.js

428 lines | 15.302 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 'leaflet/dist/leaflet.css';
import * as L from 'leaflet';

const maxZoom = 4;

export default class TbImageMap {

    constructor(ctx, $containerElement, utils, initCallback, imageUrl, posFunction, imageEntityAlias, imageUrlAttribute) {

        this.ctx = ctx;
        this.utils = utils;
        this.tooltips = [];

        this.$containerElement = $containerElement;
        this.$containerElement.css('background', '#fff');

        this.aspect = 0;
        this.width = 0;
        this.height = 0;
        this.markers = [];
        this.initCallback = initCallback;

        if (angular.isDefined(posFunction) && posFunction.length > 0) {
            try {
                this.posFunction = new Function('origXPos, origYPos', posFunction);
            } catch (e) {
                this.posFunction = null;
            }
        }
        if (!this.posFunction) {
            this.posFunction = (origXPos, origYPos) => {return {x: origXPos, y: origYPos}};
        }

        if (!this.subscribeForImageAttribute(imageEntityAlias, imageUrlAttribute)) {
            this.loadImage(imageUrl, initCallback);
        }
    }

    subscribeForImageAttribute(imageEntityAlias, imageUrlAttribute) {
        if (!imageEntityAlias || !imageEntityAlias.length ||
            !imageUrlAttribute || !imageUrlAttribute.length) {
            return false;
        }
        var entityAliasId = this.ctx.aliasController.getEntityAliasId(imageEntityAlias);
        if (!entityAliasId) {
            return false;
        }
        var types = this.ctx.$scope.$injector.get('types');
        var datasources = [
            {
                type: types.datasourceType.entity,
                name: imageEntityAlias,
                aliasName: imageEntityAlias,
                entityAliasId: entityAliasId,
                dataKeys: [
                    {
                        type: types.dataKeyType.attribute,
                        name: imageUrlAttribute,
                        label: imageUrlAttribute,
                        settings: {},
                        _hash: Math.random()
                    }
                ]
            }
        ];
        var imageMap = this;
        var imageUrlSubscriptionOptions = {
            datasources: datasources,
            useDashboardTimewindow: false,
            type: types.widgetType.latest.value,
            callbacks: {
                onDataUpdated: (subscription, apply) => {imageMap.imageUrlDataUpdated(subscription, apply)}
            }
        };
        this.ctx.subscriptionApi.createSubscription(imageUrlSubscriptionOptions, true).then(
            (subscription) => {
                imageMap.imageUrlSubscription = subscription;
            }
        );
        return true;
    }

    imageUrlDataUpdated(subscription, apply) {
        var data = subscription.data;
        if (data.length) {
            var keyData = data[0];
            if (keyData && keyData.data && keyData.data[0]) {
                var attrValue = keyData.data[0][1];
                if (attrValue && attrValue.length) {
                    this.loadImage(attrValue, this.aspect > 0 ? null : this.initCallback, true);
                }
            }
        }
        if (apply) {
            this.ctx.$scope.$digest();
        }
    }

    loadImage(imageUrl, initCallback, updateImage) {
        if (!imageUrl) {
            imageUrl = 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';
        }
        this.imageUrl = imageUrl;
        var imageMap = this;
        this.utils.loadImageAspect(imageUrl).then(
            (aspect) => {
                imageMap.aspect = aspect;
                imageMap.onresize(updateImage);
                if (initCallback) {
                    setTimeout(initCallback, 0); //eslint-disable-line
                }
            }
        );
    }

    onresize(updateImage) {
        if (this.aspect > 0) {
            var width = this.$containerElement.width();
            if (width > 0) {
                var height = width / this.aspect;
                var imageMapHeight = this.$containerElement.height();
                if (imageMapHeight > 0 && height > imageMapHeight) {
                    height = imageMapHeight;
                    width = height * this.aspect;
                }
                width *= maxZoom;
                var prevWidth = this.width;
                var prevHeight = this.height;
                if (this.width !== width) {
                    this.width = width;
                    this.height = width / this.aspect;
                    if (!this.map) {
                        this.initMap(updateImage);
                    } else {
                        var lastCenterPos = this.latLngToPoint(this.map.getCenter());
                        lastCenterPos.x /= prevWidth;
                        lastCenterPos.y /= prevHeight;
                        this.updateBounds(updateImage, lastCenterPos);
                        this.map.invalidateSize(true);
                        this.updateMarkers();
                    }
                }
            }
        }
    }

    initMap(updateImage) {
        if (!this.map && this.aspect > 0) {
            var center = this.pointToLatLng(this.width/2, this.height/2);
            this.map = L.map(this.$containerElement[0], {
                minZoom: 1,
                maxZoom: maxZoom,
                center: center,
                zoom: 1,
                crs: L.CRS.Simple,
                attributionControl: false
            });
            this.updateBounds(updateImage);
            this.updateMarkers();
        }
    }

    pointToLatLng(x, y) {
        return L.CRS.Simple.pointToLatLng({x:x, y:y}, maxZoom-1);
    }

    latLngToPoint(latLng) {
        return L.CRS.Simple.latLngToPoint(latLng, maxZoom-1);
    }

    inited() {
        return angular.isDefined(this.map);
    }

    updateBounds(updateImage, lastCenterPos) {
        var w = this.width;
        var h = this.height;
        var southWest = this.pointToLatLng(0, h);
        var northEast = this.pointToLatLng(w, 0);
        var bounds = new L.LatLngBounds(southWest, northEast);

        if (updateImage && this.imageOverlay) {
            this.imageOverlay.remove();
            this.imageOverlay = null;
        }

        if (this.imageOverlay) {
            this.imageOverlay.setBounds(bounds);
        } else {
            this.imageOverlay = L.imageOverlay(this.imageUrl, bounds).addTo(this.map);
        }
        var padding = 200 * maxZoom;
        southWest = this.pointToLatLng(-padding, h + padding);
        northEast = this.pointToLatLng(w+padding, -padding);
        var maxBounds = new L.LatLngBounds(southWest, northEast);
        this.map.setMaxBounds(maxBounds);
        if (lastCenterPos) {
            lastCenterPos.x *= w;
            lastCenterPos.y *= h;
            var center = this.pointToLatLng(lastCenterPos.x, lastCenterPos.y);
            this.ctx.$scope.$injector.get('$mdUtil').nextTick(() => {
                this.map.panTo(center, {animate: false});
            });
        }
    }

    updateMarkerLabel(marker, settings) {
        marker.unbindTooltip();
        marker.bindTooltip('<div style="color: '+ settings.labelColor +';"><b>'+settings.labelText+'</b></div>',
            { className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset });
    }

    updateMarkerColor(marker, color) {
        this.createDefaultMarkerIcon(marker, color, (iconInfo) => {
            marker.setIcon(iconInfo.icon);
        });
    }

    updateMarkerIcon(marker, settings) {
        this.createMarkerIcon(marker, settings, (iconInfo) => {
            marker.setIcon(iconInfo.icon);
            if (settings.showLabel) {
                marker.unbindTooltip();
                marker.tooltipOffset = [0, -iconInfo.size[1] * marker.offsetY + 10];
                marker.bindTooltip('<div style="color: '+ settings.labelColor +';"><b>'+settings.labelText+'</b></div>',
                    { className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset });
            }
        });
    }

    createMarkerIcon(marker, settings, onMarkerIconReady) {
        var currentImage = settings.currentImage;
        var opMap = this;
        if (currentImage && currentImage.url) {
            this.utils.loadImageAspect(currentImage.url).then(
                (aspect) => {
                    if (aspect) {
                        var width;
                        var height;
                        if (aspect > 1) {
                            width = currentImage.size;
                            height = currentImage.size / aspect;
                        } else {
                            width = currentImage.size * aspect;
                            height = currentImage.size;
                        }
                        var icon = L.icon({
                            iconUrl: currentImage.url,
                            iconSize: [width, height],
                            iconAnchor: [marker.offsetX * width, marker.offsetY * height],
                            popupAnchor: [0, -height]
                        });
                        var iconInfo = {
                            size: [width, height],
                            icon: icon
                        };
                        onMarkerIconReady(iconInfo);
                    } else {
                        opMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
                    }
                }
            );
        } else {
            this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
        }
    }

    createDefaultMarkerIcon(marker, color, onMarkerIconReady) {
        var pinColor = color.substr(1);
        var icon = L.icon({
            iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + pinColor,
            iconSize: [21, 34],
            iconAnchor: [21 * marker.offsetX, 34 * marker.offsetY],
            popupAnchor: [0, -34],
            shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
            shadowSize: [40, 37],
            shadowAnchor: [12, 35]
        });
        var iconInfo = {
            size: [21, 34],
            icon: icon
        };
        onMarkerIconReady(iconInfo);
    }

    createMarker(position, dsIndex, settings, onClickListener, markerArgs) {
        var pos = this.posFunction(position.x, position.y);
        var x = pos.x * this.width;
        var y = pos.y * this.height;
        var location = this.pointToLatLng(x, y);
        var marker = L.marker(location, {});//.addTo(this.map);
        marker.position = position;
        marker.offsetX = settings.markerOffsetX;
        marker.offsetY = settings.markerOffsetY;
        var opMap = this;
        this.createMarkerIcon(marker, settings, (iconInfo) => {
            marker.setIcon(iconInfo.icon);
            if (settings.showLabel) {
                marker.tooltipOffset = [0, -iconInfo.size[1] * marker.offsetY + 10];
                marker.bindTooltip('<div style="color: '+ settings.labelColor +';"><b>'+settings.labelText+'</b></div>',
                    { className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset });
            }
            marker.addTo(opMap.map);
        });

        if (settings.displayTooltip) {
            this.createTooltip(marker, dsIndex, settings, markerArgs);
        }

        if (onClickListener) {
            marker.on('click', onClickListener);
        }
        this.markers.push(marker);
        return marker;
    }

    updateMarkers() {
        this.markers.forEach((marker) => {
            this.updateMarkerLocation(marker);
        });
    }

    updateMarkerLocation(marker) {
        this.setMarkerPosition(marker, marker.position);
    }

    removeMarker(marker) {
        this.map.removeLayer(marker);
        var index = this.markers.indexOf(marker);
        if (index > -1) {
            marker.pinElement.remove();
            this.markers.splice(index, 1);
        }
    }

    createTooltip(marker, dsIndex, settings, markerArgs) {
        var popup = L.popup();
        popup.setContent('');
        marker.bindPopup(popup, {autoClose: settings.autocloseTooltip, closeOnClick: false});
        this.tooltips.push( {
            markerArgs: markerArgs,
            popup: popup,
            locationSettings: settings,
            dsIndex: dsIndex
        });
    }

    updatePolylineColor(/*polyline, settings, color*/) {
    }

    createPolyline(/*locations, settings*/) {
    }

    removePolyline(/*polyline*/) {
    }

    fitBounds() {
    }

    createLatLng(x, y) {
        return new Position(x, y);
    }

    extendBoundsWithMarker() {
    }

    getMarkerPosition(marker) {
        return marker.position;
    }

    setMarkerPosition(marker, position) {
        marker.position = position;
        var pos = this.posFunction(position.x, position.y);
        var x = pos.x * this.width;
        var y = pos.y * this.height;
        var location = this.pointToLatLng(x, y);
        marker.setLatLng(location);
    }

    getPolylineLatLngs(/*polyline*/) {
    }

    setPolylineLatLngs(/*polyline, latLngs*/) {
    }

    createBounds() {
        return {};
    }

    extendBounds() {
    }

    invalidateSize() {
        this.onresize();
    }

    getTooltips() {
        return this.tooltips;
    }

}

class Position {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    equals(loc) {
        return loc && loc.x == this.x && loc.y == this.y;
    }
}