map-widget2.js

1086 lines | 42.066 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';
import TbImageMap from './image-map';
import TbTencentMap from './tencent-map';

import {processPattern, arraysEqual, toLabelValueMap, fillPattern, fillPatternWithActions} from './widget-utils';

export default class TbMapWidgetV2 {
    constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) {
        var tbMap = this;
        this.ctx = ctx;
        this.mapProvider = mapProvider;
        if (!$element) {
            $element = ctx.$container;
        }
        this.utils = ctx.$scope.$injector.get('utils');
        this.drawRoutes = drawRoutes;
        this.markers = [];
        if (this.drawRoutes) {
            this.polylines = [];
        }

        this.locationSettings = {};

        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.configureLocationsSettings();

        var minZoomLevel = this.drawRoutes ? 18 : 15;

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

        this.ctx.$scope.onTooltipAction = function(event, actionName, dsIndex) {
            tbMap.onTooltipAction(event, actionName, dsIndex);
        };
        this.tooltipActionsMap = {};
        var descriptors = this.ctx.actionsApi.getActionDescriptors('tooltipAction');
        descriptors.forEach(function (descriptor) {
            tbMap.tooltipActionsMap[descriptor.name] = descriptor;
        });

        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, settings.mapProvider);
        } else if (mapProvider === 'image-map') {
            this.map = new TbImageMap(this.ctx, $element, this.utils, initCallback,
                settings.mapImageUrl,
                settings.posFunction,
                settings.imageEntityAlias,
                settings.imageUrlAttribute);
        } else if (mapProvider === 'tencent-map') {
            this.map = new TbTencentMap($element,this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType);
        }
    }

    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 = [];
            }
        }
    }

    setSubscription(subscription) {
        this.subscription = subscription;
        this.clearLocations();
    }

    configureLocationsSettings() {

        if (this.mapProvider  == 'image-map') {
            this.locationSettings.latKeyName = this.ctx.settings.xPosKeyName || 'xPos';
            this.locationSettings.lngKeyName = this.ctx.settings.yPosKeyName || 'yPos';
            this.locationSettings.markerOffsetX = angular.isDefined(this.ctx.settings.markerOffsetX) ? this.ctx.settings.markerOffsetX : 0.5;
            this.locationSettings.markerOffsetY = angular.isDefined(this.ctx.settings.markerOffsetY) ? this.ctx.settings.markerOffsetY : 1;
        } else {
            this.locationSettings.latKeyName = this.ctx.settings.latKeyName || 'latitude';
            this.locationSettings.lngKeyName = this.ctx.settings.lngKeyName || 'longitude';
        }

        this.locationSettings.tooltipPattern = this.ctx.settings.tooltipPattern || "<b>${entityName}</b><br/><br/><b>Latitude:</b> ${"+this.locationSettings.latKeyName+":7}<br/><b>Longitude:</b> ${"+this.locationSettings.lngKeyName+":7}";

        this.locationSettings.showLabel = this.ctx.settings.showLabel !== false;
        this.locationSettings.displayTooltip = this.ctx.settings.showTooltip !== false;
        this.locationSettings.autocloseTooltip = this.ctx.settings.autocloseTooltip !== false;
        this.locationSettings.labelColor = this.ctx.widgetConfig.color || '#000000',
        this.locationSettings.label = this.ctx.settings.label || "${entityName}";
        this.locationSettings.color = this.ctx.settings.color ? tinycolor(this.ctx.settings.color).toHexString() : "#FE7569";

        this.locationSettings.useLabelFunction = this.ctx.settings.useLabelFunction === true;
        if (angular.isDefined(this.ctx.settings.labelFunction) && this.ctx.settings.labelFunction.length > 0) {
            try {
                this.locationSettings.labelFunction = new Function('data, dsData, dsIndex', this.ctx.settings.labelFunction);
            } catch (e) {
                this.locationSettings.labelFunction = null;
            }
        }

        this.locationSettings.useTooltipFunction = this.ctx.settings.useTooltipFunction === true;
        if (angular.isDefined(this.ctx.settings.tooltipFunction) && this.ctx.settings.tooltipFunction.length > 0) {
            try {
                this.locationSettings.tooltipFunction = new Function('data, dsData, dsIndex', this.ctx.settings.tooltipFunction);
            } catch (e) {
                this.locationSettings.tooltipFunction = null;
            }
        }

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

        this.locationSettings.useMarkerImageFunction = this.ctx.settings.useMarkerImageFunction === true;
        if (angular.isDefined(this.ctx.settings.markerImageFunction) && this.ctx.settings.markerImageFunction.length > 0) {
            try {
                this.locationSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', this.ctx.settings.markerImageFunction);
            } catch (e) {
                this.locationSettings.markerImageFunction = null;
            }
        }

        this.locationSettings.markerImages = this.ctx.settings.markerImages || [];

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

        if (this.drawRoutes) {
            this.locationSettings.strokeWeight = this.ctx.settings.strokeWeight || 2;
            this.locationSettings.strokeOpacity = this.ctx.settings.strokeOpacity || 1.0;
        }
    }

    onTooltipAction(event, actionName, dsIndex) {
        var descriptor = this.tooltipActionsMap[actionName];
        if (descriptor) {
            var datasource = this.subscription.datasources[dsIndex];
            var entityId = {};
            entityId.id = datasource.entityId;
            entityId.entityType = datasource.entityType;
            var entityName = datasource.entityName;
            this.ctx.actionsApi.handleWidgetAction(event, descriptor, entityId, entityName);
        }
    }

    update() {

        var tbMap = this;

        function updateLocationLabel(location, dataMap) {
            if (location.settings.showLabel) {
                if (location.settings.useLabelFunction && location.settings.labelFunction) {
                    try {
                        location.settings.label = location.settings.labelFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
                    } catch (e) {
                        location.settings.label = null;
                    }
                    if (location.settings.label) {
                        var datasources = tbMap.subscription.datasources;
                        location.settings.label = tbMap.utils.createLabelFromDatasource(datasources[location.dsIndex], location.settings.label);
                        location.settings.labelReplaceInfo = processPattern(location.settings.label, datasources, location.dsIndex);
                        location.settings.labelText = location.settings.label;
                    }
                }
                if (location.settings.labelReplaceInfo.variables.length) {
                    location.settings.labelText = fillPattern(location.settings.label,
                        location.settings.labelReplaceInfo, tbMap.subscription.data);
                }
                tbMap.map.updateMarkerLabel(location.marker, location.settings);
            }
        }


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

        function updateLocationColor(location, color, image) {
            if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) {
                if (!location.settings.useMarkerImage && !image) {
                    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, image) {
            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) {
            updateLocationLabel(location, dataMap);
            var color = calculateLocationColor(location, dataMap);
            var image = calculateLocationMarkerImage(location, dataMap);
            updateLocationColor(location, color, image);
            updateLocationMarkerIcon(location, image);
        }

        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 (event) {
                        tbMap.callbacks.onLocationClick(location);
                        locationRowClick(event, location);
                    }, [location.dsIndex]);
                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 locationRowClick($event, location) {
            var descriptors = tbMap.ctx.actionsApi.getActionDescriptors('markerClick');
            if (descriptors.length) {
                var datasource = tbMap.subscription.datasources[location.dsIndex];
                var entityId = {};
                entityId.id = datasource.entityId;
                entityId.entityType = datasource.entityType;
                var entityName = datasource.entityName;
                tbMap.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName);
            }
        }

        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];
                            if (angular.isDefined(lat) && lat != null && angular.isDefined(lng) && lng != null) {
                                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];
                        if (angular.isDefined(lat) && lat != null && angular.isDefined(lng) && lng != null) {
                            latLng = tbMap.map.createLatLng(lat, lng);
                            if (createOrUpdateLocationMarker(location, latLng, dataMap)) {
                                locationChanged = true;
                            }
                        }
                    }
                    if (location.marker) {
                        updateLocationStyle(location, dataMap);
                    }
                }
            }
            return locationChanged;
        }

        function loadLocations(data, datasources) {
            var bounds = tbMap.map.createBounds();
            tbMap.locations = [];
            var dataMap = toLabelValueMap(data, datasources);
            var currentDatasource = null;
            var currentDatasourceIndex = -1;
            var latIndex = -1;
            var lngIndex = -1;

            for (var i=0;i<data.length;i++) {
                var dataKeyData = data[i];
                var dataKey = dataKeyData.dataKey;
                if (dataKeyData.datasource != currentDatasource) {
                    currentDatasource = dataKeyData.datasource;
                    currentDatasourceIndex++;
                    latIndex = -1;
                    lngIndex = -1;
                }
                var nameToCheck;
                if (dataKey.locationAttrName) {
                    nameToCheck = dataKey.locationAttrName;
                } else {
                    nameToCheck = dataKey.label;
                }
                if (nameToCheck === tbMap.locationSettings.latKeyName) {
                    latIndex = i;
                } else if (nameToCheck === tbMap.locationSettings.lngKeyName) {
                    lngIndex = i;
                }
                if (latIndex > -1 && lngIndex > -1) {
                    var location = {
                        latIndex: latIndex,
                        lngIndex: lngIndex,
                        dsIndex: currentDatasourceIndex,
                        settings: angular.copy(tbMap.locationSettings)
                    };
                    if (location.settings.showLabel) {
                        location.settings.label = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.label);
                        location.settings.labelReplaceInfo = processPattern(location.settings.label, datasources, currentDatasourceIndex);
                        location.settings.labelText = location.settings.label;
                    }
                    if (location.settings.displayTooltip) {
                        location.settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.tooltipPattern);
                        location.settings.tooltipReplaceInfo = processPattern(location.settings.tooltipPattern, datasources, currentDatasourceIndex);
                    }

                    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);
                    }
                    latIndex = -1;
                    lngIndex = -1;
                }

            }
            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);
            }
        }

        function createTooltipContent(tooltip, data, datasources) {
            var content;
            var settings = tooltip.locationSettings;
            if (settings.useTooltipFunction && settings.tooltipFunction) {
                var dataMap = toLabelValueMap(data, datasources);
                try {
                    settings.tooltipPattern = settings.tooltipFunction(dataMap.dataMap, dataMap.dsDataMap, tooltip.dsIndex);
                } catch (e) {
                    settings.tooltipPattern = null;
                }
                if (settings.tooltipPattern) {
                    settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(datasources[tooltip.dsIndex], settings.tooltipPattern);
                    settings.tooltipReplaceInfo = processPattern(settings.tooltipPattern, datasources, tooltip.dsIndex);
                }
            }
            content = fillPattern(settings.tooltipPattern, settings.tooltipReplaceInfo, data);
            return fillPatternWithActions(content, 'onTooltipAction', tooltip.markerArgs);
        }

        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 text = createTooltipContent(tooltip, this.subscription.data, this.subscription.datasources);
                    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);
            }
        }
    }

    static settingsSchema(mapProvider, drawRoutes) {
        var schema;
        if (mapProvider === 'google-map') {
            schema = angular.copy(googleMapSettingsSchema);
        } else if (mapProvider === 'openstreet-map') {
            schema = angular.copy(openstreetMapSettingsSchema);
        } else if (mapProvider === 'image-map') {
            return imageMapSettingsSchema;
        } else if (mapProvider === 'tencent-map') {
            schema = angular.copy(tencentMapSettingsSchema);
        }
        angular.merge(schema.schema.properties, commonMapSettingsSchema.schema.properties);
        schema.schema.required = schema.schema.required.concat(commonMapSettingsSchema.schema.required);
        schema.form = schema.form.concat(commonMapSettingsSchema.form);
        if (drawRoutes) {
            angular.merge(schema.schema.properties, routeMapSettingsSchema.schema.properties);
            schema.schema.required = schema.schema.required.concat(routeMapSettingsSchema.schema.required);
            schema.form = schema.form.concat(routeMapSettingsSchema.form);
        }
        return schema;
    }

    static dataKeySettingsSchema(/*mapProvider*/) {
        return {};
    }

    static actionSources() {
        return {
            'markerClick': {
                name: 'widget-action.marker-click',
                multiple: false
            },
            'tooltipAction': {
                name: 'widget-action.tooltip-tag-action',
                multiple: true
            }
        };
    }

}

const googleMapSettingsSchema =
    {
        "schema":{
            "title":"Google Map Configuration",
            "type":"object",
            "properties":{
                "gmApiKey":{
                    "title":"Google Maps API Key",
                    "type":"string"
                },
                "gmDefaultMapType":{
                    "title":"Default map type",
                    "type":"string",
                    "default":"roadmap"
                }
            },
            "required":[
                "gmApiKey"
            ]
        },
        "form":[
            "gmApiKey",
            {
                "key":"gmDefaultMapType",
                "type":"rc-select",
                "multiple":false,
                "items":[
                    {
                        "value":"roadmap",
                        "label":"Roadmap"
                    },
                    {
                        "value":"satellite",
                        "label":"Satellite"
                    },
                    {
                        "value":"hybrid",
                        "label":"Hybrid"
                    },
                    {
                        "value":"terrain",
                        "label":"Terrain"
                    }
                ]
            }
        ]
    };
    
const tencentMapSettingsSchema =
    {
        "schema":{
            "title":"Tencent Map Configuration",
            "type":"object",
            "properties":{
                "tmApiKey":{
                    "title":"Tencent Maps API Key",
                    "type":"string"
                },
                "tmDefaultMapType":{
                    "title":"Default map type",
                    "type":"string",
                    "default":"roadmap"
                }
            },
            "required":[
                "tmApiKey"
            ]
        },
        "form":[
            "tmApiKey",
            {
                "key":"tmDefaultMapType",
                "type":"rc-select",
                "multiple":false,
                "items":[
                    {
                        "value":"roadmap",
                        "label":"Roadmap"
                    },
                    {
                        "value":"satellite",
                        "label":"Satellite"
                    },
                    {
                        "value":"hybrid",
                        "label":"Hybrid"
                    },
                ]
            }
        ]
    };
    
const openstreetMapSettingsSchema =
    {
        "schema":{
            "title":"Openstreet Map Configuration",
            "type":"object",
            "properties":{
                "mapProvider":{
                    "title":"Map provider",
                    "type":"string",
                    "default":"OpenStreetMap.Mapnik"
                }
            },
            "required":[
            ]
        },
        "form":[
            {
                "key":"mapProvider",
                "type":"rc-select",
                "multiple":false,
                "items":[
                    {
                        "value":"OpenStreetMap.Mapnik",
                        "label":"OpenStreetMap.Mapnik (Default)"
                    },
                    {
                        "value":"OpenStreetMap.BlackAndWhite",
                        "label":"OpenStreetMap.BlackAndWhite"
                    },
                    {
                        "value":"OpenStreetMap.HOT",
                        "label":"OpenStreetMap.HOT"
                    },
                    {
                        "value":"Esri.WorldStreetMap",
                        "label":"Esri.WorldStreetMap"
                    },
                    {
                        "value":"Esri.WorldTopoMap",
                        "label":"Esri.WorldTopoMap"
                    },
                    {
                        "value":"CartoDB.Positron",
                        "label":"CartoDB.Positron"
                    },
                    {
                        "value":"CartoDB.DarkMatter",
                        "label":"CartoDB.DarkMatter"
                    }
                ]
            }
        ]
    };

const commonMapSettingsSchema =
    {
        "schema":{
            "title":"Map Configuration",
            "type":"object",
            "properties":{
                "defaultZoomLevel":{
                    "title":"Default map zoom level (1 - 20)",
                    "type":"number"
                },
                "fitMapBounds":{
                    "title":"Fit map bounds to cover all markers",
                    "type":"boolean",
                    "default":true
                },
                "latKeyName":{
                    "title":"Latitude key name",
                    "type":"string",
                    "default":"latitude"
                },
                "lngKeyName":{
                    "title":"Longitude key name",
                    "type":"string",
                    "default":"longitude"
                },
                "showLabel":{
                    "title":"Show label",
                    "type":"boolean",
                    "default":true
                },
                "label":{
                    "title":"Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )",
                    "type":"string",
                    "default":"${entityName}"
                },
                "useLabelFunction": {
                    "title":"Use label function",
                    "type":"boolean",
                    "default":false
                },
                "labelFunction":{
                    "title":"Label function: f(data, dsData, dsIndex)",
                    "type":"string"
                },
                "showTooltip": {
                    "title": "Show tooltip",
                    "type":"boolean",
                    "default":true
                },
                "autocloseTooltip": {
                    "title": "Auto-close tooltips",
                    "type":"boolean",
                    "default":true
                },
                "tooltipPattern":{
                    "title":"Tooltip (for ex. 'Text ${keyName} units.' or <link-act name='my-action'>Link text</link-act>')",
                    "type":"string",
                    "default":"<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}"
                },
                "useTooltipFunction": {
                    "title":"Use tooltip function",
                    "type":"boolean",
                    "default":false
                },
                "tooltipFunction":{
                    "title":"Tooltip function: f(data, dsData, dsIndex)",
                    "type":"string"
                },
                "color":{
                    "title":"Color",
                    "type":"string"
                },
                "useColorFunction":{
                    "title":"Use color function",
                    "type":"boolean",
                    "default":false
                },
                "colorFunction":{
                    "title":"Color function: f(data, dsData, dsIndex)",
                    "type":"string"
                },
                "markerImage":{
                    "title":"Custom marker image",
                    "type":"string"
                },
                "markerImageSize":{
                    "title":"Custom marker image size (px)",
                    "type":"number",
                    "default":34
                },
                "useMarkerImageFunction":{
                    "title":"Use marker image function",
                    "type":"boolean",
                    "default":false
                },
                "markerImageFunction":{
                    "title":"Marker image function: f(data, images, dsData, dsIndex)",
                    "type":"string"
                },
                "markerImages":{
                    "title":"Marker images",
                    "type":"array",
                    "items":{
                        "title":"Marker image",
                        "type":"string"
                    }
                }
            },
            "required":[]
        },
        "form":[
            "defaultZoomLevel",
            "fitMapBounds",
            "latKeyName",
            "lngKeyName",
            "showLabel",
            "label",
            "useLabelFunction",
            {
                "key":"labelFunction",
                "type":"javascript"
            },
            "showTooltip",
            "autocloseTooltip",
            {
                "key": "tooltipPattern",
                "type": "textarea"
            },
            "useTooltipFunction",
            {
                "key":"tooltipFunction",
                "type":"javascript"
            },
            {
                "key":"color",
                "type":"color"
            },
            "useColorFunction",
            {
                "key":"colorFunction",
                "type":"javascript"
            },
            {
                "key":"markerImage",
                "type":"image"
            },
            "markerImageSize",
            "useMarkerImageFunction",
            {
                "key":"markerImageFunction",
                "type":"javascript"
            },
            {
                "key":"markerImages",
                "items":[
                    {
                        "key":"markerImages[]",
                        "type":"image"
                    }
                ]
            }
        ]
};

const routeMapSettingsSchema =
    {
        "schema":{
            "title":"Route Map Configuration",
            "type":"object",
            "properties":{
                "strokeWeight": {
                    "title": "Stroke weight",
                    "type": "number",
                    "default": 2
                },
                "strokeOpacity": {
                    "title": "Stroke opacity",
                    "type": "number",
                    "default": 1.0
                }
            },
            "required":[
            ]
        },
        "form":[
            "strokeWeight",
            "strokeOpacity"
        ]
    };

const imageMapSettingsSchema =
{
    "schema":{
        "title":"Image Map Configuration",
        "type":"object",
        "properties":{
            "mapImageUrl": {
                "title": "Image map background",
                "type": "string",
                "default": "data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg=="
            },
            "imageEntityAlias": {
                "title": "Image URL source entity alias",
                "type": "string",
                "default": ""
            },
            "imageUrlAttribute": {
                "title": "Image URL source entity attribute",
                "type": "string",
                "default": ""
            },
            "xPosKeyName":{
                "title":"X position key name",
                "type":"string",
                "default":"xPos"
            },
            "yPosKeyName":{
                "title":"Y position key name",
                "type":"string",
                "default":"yPos"
            },
            "showLabel":{
                "title":"Show label",
                "type":"boolean",
                "default":true
            },
            "label":{
                "title":"Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )",
                "type":"string",
                "default":"${entityName}"
            },
            "useLabelFunction": {
                "title":"Use label function",
                "type":"boolean",
                "default":false
            },
            "labelFunction":{
                "title":"Label function: f(data, dsData, dsIndex)",
                "type":"string"
            },
            "showTooltip": {
                "title": "Show tooltip",
                "type":"boolean",
                "default":true
            },
            "autocloseTooltip": {
                "title": "Auto-close tooltips",
                "type":"boolean",
                "default":true
            },
            "tooltipPattern":{
                "title":"Tooltip (for ex. 'Text ${keyName} units.' or <link-act name='my-action'>Link text</link-act>')",
                "type":"string",
                "default":"<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}"
            },
            "useTooltipFunction": {
                "title":"Use tooltip function",
                "type":"boolean",
                "default":false
            },
            "tooltipFunction":{
                "title":"Tooltip function: f(data, dsData, dsIndex)",
                "type":"string"
            },
            "color":{
                "title":"Color",
                "type":"string"
            },
            "posFunction":{
                "title":"Position conversion function: f(origXPos, origYPos), should return x,y coordinates as double from 0 to 1 each",
                "type":"string",
                "default": "return {x: origXPos, y: origYPos};"
            },
            "markerOffsetX": {
                "title": "Marker X offset relative to position",
                "type": "number",
                "default": 0.5
            },
            "markerOffsetY": {
                "title": "Marker Y offset relative to position",
                "type": "number",
                "default": 1
            },
            "useColorFunction":{
                "title":"Use color function",
                "type":"boolean",
                "default":false
            },
            "colorFunction":{
                "title":"Color function: f(data, dsData, dsIndex)",
                "type":"string"
            },
            "markerImage":{
                "title":"Custom marker image",
                "type":"string"
            },
            "markerImageSize":{
                "title":"Custom marker image size (px)",
                "type":"number",
                "default":34
            },
            "useMarkerImageFunction":{
                "title":"Use marker image function",
                "type":"boolean",
                "default":false
            },
            "markerImageFunction":{
                "title":"Marker image function: f(data, images, dsData, dsIndex)",
                "type":"string"
            },
            "markerImages":{
                "title":"Marker images",
                "type":"array",
                "items":{
                    "title":"Marker image",
                    "type":"string"
                }
            }
        },
        "required":[]
    },
    "form":[
        {
            "key": "mapImageUrl",
            "type": "image"
        },
        "imageEntityAlias",
        "imageUrlAttribute",
        "xPosKeyName",
        "yPosKeyName",
        "showLabel",
        "label",
        "useLabelFunction",
        {
            "key":"labelFunction",
            "type":"javascript"
        },
        "showTooltip",
        "autocloseTooltip",
        {
            "key": "tooltipPattern",
            "type": "textarea"
        },
        "useTooltipFunction",
        {
            "key":"tooltipFunction",
            "type":"javascript"
        },
        {
            "key":"color",
            "type":"color"
        },
        {
            "key":"posFunction",
            "type":"javascript"
        },
        "markerOffsetX",
        "markerOffsetY",
        "useColorFunction",
        {
            "key":"colorFunction",
            "type":"javascript"
        },
        {
            "key":"markerImage",
            "type":"image"
        },
        "markerImageSize",
        "useMarkerImageFunction",
        {
            "key":"markerImageFunction",
            "type":"javascript"
        },
        {
            "key":"markerImages",
            "items":[
                {
                    "key":"markerImages[]",
                    "type":"image"
                }
            ]
        }
    ]
};