diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json
index 3e12e5a662..d0d0741e21 100644
--- a/application/src/main/data/json/system/widget_bundles/maps.json
+++ b/application/src/main/data/json/system/widget_bundles/maps.json
@@ -12,6 +12,7 @@
"map",
"image_map",
"trip_map",
+ "route_map",
"maps_v2.openstreetmap",
"maps_v2.google_maps",
"maps_v2.image_map",
diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json
index 2d66ca43da..2b7d56957b 100644
--- a/application/src/main/data/json/system/widget_types/image_map.json
+++ b/application/src/main/data/json/system/widget_types/image_map.json
@@ -17,7 +17,7 @@
"settingsDirective": "tb-map-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-map-basic-config",
- "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"layers\":[],\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null},\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\" \",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Image Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[]}}"
+ "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null},\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\" \",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[]},\"title\":\"Image Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}"
},
"resources": [
{
diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json
index 379ae10b0a..9f246a215a 100644
--- a/application/src/main/data/json/system/widget_types/map.json
+++ b/application/src/main/data/json/system/widget_types/map.json
@@ -17,7 +17,7 @@
"settingsDirective": "tb-map-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-map-basic-config",
- "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.HOT\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldStreetMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldTopoMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.Positron\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.DarkMatter\"}],\"imageSource\":null,\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\"\\n\",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"mapPageSize\":16384,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{\"tooltipAction\":[]}}"
+ "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\"\\n\",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[]},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}"
},
"resources": [
{
diff --git a/application/src/main/data/json/system/widget_types/route_map.json b/application/src/main/data/json/system/widget_types/route_map.json
new file mode 100644
index 0000000000..6d9590fe90
--- /dev/null
+++ b/application/src/main/data/json/system/widget_types/route_map.json
@@ -0,0 +1,80 @@
+{
+ "fqn": "route_map",
+ "name": "Route Map",
+ "deprecated": false,
+ "image": "tb-image;/api/images/system/route_map_openstreet_system_widget_image.png",
+ "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Highly customizable via custom markers, marker tooltips, and widget actions.",
+ "descriptor": {
+ "type": "timeseries",
+ "sizeX": 8.5,
+ "sizeY": 6,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}",
+ "settingsForm": [],
+ "dataKeySettingsForm": [],
+ "latestDataKeySettingsForm": [],
+ "settingsDirective": "tb-map-widget-settings",
+ "dataKeySettingsDirective": "",
+ "latestDataKeySettingsDirective": "",
+ "hasBasicMode": true,
+ "basicModeDirective": "tb-map-basic-config",
+ "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"trips\":[{\"dsType\":\"function\",\"dsLabel\":\"First route\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.17490048149347315,\"funcBody\":\"var value = prevValue;\\nif (!value) {\\n value = 45;\\n}\\nif (time % 500 < 500) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null,\"tagActions\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"tripMarkerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"arrow_forward\",\"size\":48,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/tripShape1.svg\",\"imageSize\":34,\"imageFunction\":\"var speed = data.Speed;\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"rotateMarker\":true,\"offsetAngle\":0,\"showPath\":true,\"pathStrokeWeight\":4,\"pathStrokeColor\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var speed = data.Speed;\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).setAlpha(0.65).toRgbString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).setAlpha(0.65).toRgbString();\\n }\\n}\"},\"usePathDecorator\":false,\"pathDecoratorSymbol\":\"arrowHead\",\"pathDecoratorSymbolSize\":10,\"pathDecoratorSymbolColor\":\"#307FE5\",\"pathDecoratorOffset\":20,\"pathEndDecoratorOffset\":20,\"pathDecoratorRepeat\":20,\"showPoints\":false,\"pointSize\":10,\"pointColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"pointTooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-1}}],\"tripTimeline\":{\"showTimelineControl\":false}},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
+ },
+ "resources": [
+ {
+ "link": "/api/images/system/map_marker_image_0.png",
+ "title": "Map marker image 0",
+ "type": "IMAGE",
+ "subType": "IMAGE",
+ "fileName": "map_marker_image_0.png",
+ "publicResourceKey": "LPbcriZ2v053mkWb33T5JdK7Agkt1jGg",
+ "mediaType": "image/png",
+ "data": "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",
+ "public": true
+ },
+ {
+ "link": "/api/images/system/map_marker_image_1.png",
+ "title": "Map marker image 1",
+ "type": "IMAGE",
+ "subType": "IMAGE",
+ "fileName": "map_marker_image_1.png",
+ "publicResourceKey": "TwKYnwJfaCIgDbJsetgcj3q7AYK4HUSA",
+ "mediaType": "image/png",
+ "data": "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",
+ "public": true
+ },
+ {
+ "link": "/api/images/system/map_marker_image_2.png",
+ "title": "Map marker image 2",
+ "type": "IMAGE",
+ "subType": "IMAGE",
+ "fileName": "map_marker_image_2.png",
+ "publicResourceKey": "FazBQsEp1uSeIsT1XL31o2npLAx5s3zJ",
+ "mediaType": "image/png",
+ "data": "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",
+ "public": true
+ },
+ {
+ "link": "/api/images/system/route_map_openstreet_system_widget_image.png",
+ "title": "\"Route Map - OpenStreet\" system widget image",
+ "type": "IMAGE",
+ "subType": "IMAGE",
+ "fileName": "route_map_openstreet_system_widget_image.png",
+ "publicResourceKey": "VTTAajdw2NRpCUbQLGVFCMBybEtJfwFK",
+ "mediaType": "image/png",
+ "data": "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",
+ "public": true
+ }
+ ],
+ "scada": false,
+ "tags": [
+ "mapping",
+ "gps",
+ "navigation",
+ "geolocation",
+ "satellite",
+ "directions"
+ ]
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/trip_map.json b/application/src/main/data/json/system/widget_types/trip_map.json
index e64d491779..09eba54978 100644
--- a/application/src/main/data/json/system/widget_types/trip_map.json
+++ b/application/src/main/data/json/system/widget_types/trip_map.json
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}",
"settingsForm": [],
"dataKeySettingsForm": [],
"latestDataKeySettingsForm": [],
@@ -20,7 +20,7 @@
"latestDataKeySettingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-map-basic-config",
- "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.HOT\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldStreetMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldTopoMap\"},{\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.Positron\"},{\"provider\":\"openstreet\",\"layerType\":\"CartoDB.DarkMatter\"}],\"trips\":[],\"markers\":[],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"minZoomLevel\":16,\"mapPageSize\":16384,\"tripTimeline\":{\"showTimelineControl\":true,\"timeStep\":1000,\"speedOptions\":[1,5,10,15,25],\"showTimestamp\":true,\"timestampFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\",\"lastUpdateAgo\":false,\"custom\":false,\"auto\":false},\"snapToRealLocation\":false,\"locationSnapFilter\":\"return true;\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Trip Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
+ "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"trips\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 0 : (value + 2) % gpsData.length)];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 1 : (value + 2) % gpsData.length)];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"tripMarkerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"arrow_forward\",\"size\":48,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"/assets/markers/tripShape1.svg\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":0.5,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null},\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-0.5},\"click\":{\"type\":\"doNothing\"},\"edit\":{\"enabledActions\":[],\"snappable\":false},\"rotateMarker\":true,\"offsetAngle\":0,\"showPath\":true,\"pathStrokeWeight\":2,\"pathStrokeColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"usePathDecorator\":false,\"pathDecoratorSymbol\":\"arrowHead\",\"pathDecoratorSymbolSize\":10,\"pathDecoratorSymbolColor\":\"#307FE5\",\"pathDecoratorOffset\":20,\"pathEndDecoratorOffset\":20,\"pathDecoratorRepeat\":20,\"showPoints\":false,\"pointSize\":10,\"pointColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"pointTooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}
Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-1}}],\"tripTimeline\":{\"showTimelineControl\":true}},\"title\":\"Trip Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
},
"resources": [
{
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts
index d83292642a..140e4f770b 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts
@@ -17,7 +17,7 @@
import {
CirclesDataLayerSettings,
defaultBaseCirclesDataLayerSettings,
- isJSON,
+ isJSON, MapDataLayerType,
TbCircleData,
TbMapDatasource
} from '@home/components/widget/lib/maps/models/map.models';
@@ -28,13 +28,12 @@ import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
import { isNotEmptyStr } from '@core/utils';
import {
- MapDataLayerType,
- TbDataLayerItem,
+ TbLatestDataLayerItem,
UnplacedMapDataItem
-} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
+} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { map } from 'rxjs/operators';
-class TbCircleDataLayerItem extends TbDataLayerItem {
+class TbCircleDataLayerItem extends TbLatestDataLayerItem {
private circle: L.Circle;
private circleStyle: L.PathOptions;
@@ -47,6 +46,14 @@ class TbCircleDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): L.Layer {
const circleData = this.dataLayer.extractCircleCoordinates(data);
const center = new L.LatLng(circleData.latitude, circleData.longitude);
@@ -141,14 +148,6 @@ class TbCircleDataLayerItem extends TbDataLayerItem) {
+ const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]);
+ return this.map.circleDataToCoordinates(circleData);
+ }
+
+ public saveCircleCoordinates(data: FormattedData, center: L.LatLng, radius: number): Observable {
+ const converted = center ? this.map.coordinatesToCircleData(center, radius) : null;
+ const circleData = [
+ {
+ dataKey: this.settings.circleKey,
+ value: converted
+ }
+ ];
+ return this.map.saveItemData(data.$datasource, circleData).pipe(
+ map(() => converted)
+ );
+ }
+
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.circleKey);
return datasource;
@@ -213,25 +230,8 @@ export class TbCirclesDataLayer extends TbShapesDataLayer, dsData: FormattedData[]): TbDataLayerItem {
+ protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbLatestDataLayerItem {
return new TbCircleDataLayerItem(data, dsData, this.settings, this);
}
- public extractCircleCoordinates(data: FormattedData) {
- const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]);
- return this.map.circleDataToCoordinates(circleData);
- }
-
- public saveCircleCoordinates(data: FormattedData, center: L.LatLng, radius: number): Observable {
- const converted = center ? this.map.coordinatesToCircleData(center, radius) : null;
- const circleData = [
- {
- dataKey: this.settings.circleKey,
- value: converted
- }
- ];
- return this.map.saveItemData(data.$datasource, circleData).pipe(
- map(() => converted)
- );
- }
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts
new file mode 100644
index 0000000000..fe8639f7a0
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts
@@ -0,0 +1,415 @@
+///
+/// Copyright © 2016-2025 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 {
+ DataLayerEditAction,
+ MapDataLayerSettings,
+ TbMapDatasource
+} from '@home/components/widget/lib/maps/models/map.models';
+import { TbMap } from '@home/components/widget/lib/maps/map';
+import { FormattedData, WidgetActionType } from '@shared/models/widget.models';
+import { Observable } from 'rxjs';
+import L from 'leaflet';
+import { createTooltip, updateTooltip } from './data-layer-utils';
+import { TbDataLayerItem, TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
+
+export abstract class TbLatestDataLayerItem = TbLatestMapDataLayer, L extends L.Layer = L.Layer> extends TbDataLayerItem {
+
+ protected tooltip: L.Popup;
+ protected data: FormattedData;
+ protected selected = false;
+
+ protected constructor(data: FormattedData,
+ dsData: FormattedData[],
+ settings: S,
+ dataLayer: D) {
+ super(settings, dataLayer);
+ this.data = data;
+ this.layer = this.create(data, dsData);
+ if (this.settings.tooltip?.show) {
+ this.tooltip = createTooltip(this.dataLayer.getMap(),
+ this.layer, this.settings.tooltip, this.data, () => {
+ return !this.isEditing();
+ });
+ updateTooltip(this.dataLayer.getMap(), this.tooltip,
+ this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData);
+ }
+ this.bindEvents();
+ try {
+ this.dataLayer.getDataLayerContainer().addLayer(this.layer);
+ this.editModeUpdated();
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+
+ public invalidateCoordinates(): void {
+ this.doInvalidateCoordinates(this.data, this.dataLayer.getMap().getData());
+ }
+
+ public select(): L.TB.ToolbarButtonOptions[] {
+ if (!this.selected) {
+ this.selected = true;
+ this.disableEdit();
+ this.updateSelectedState();
+ const buttons = this.onSelected();
+ if (this.dataLayer.isRemoveEnabled()) {
+ buttons.push({
+ id: 'remove',
+ title: this.removeDataItemTitle(),
+ click: () => {
+ this.removeDataItem().subscribe(
+ () => this.dataLayer.removeItem(this.data.entityId)
+ );
+ },
+ iconClass: 'tb-remove'
+ });
+ }
+ return buttons;
+ } else {
+ return [];
+ }
+ }
+
+ public deselect(cancel = false, force = false): boolean {
+ if (this.selected) {
+ if (this.canDeselect(cancel) || force) {
+ this.selected = false;
+ this.layer.closePopup();
+ this.updateSelectedState();
+ this.onDeselected();
+ this.editModeUpdated();
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public isSelected() {
+ return this.selected;
+ }
+
+ public editModeUpdated() {
+ if (this.dataLayer.isEditMode() && !this.selected) {
+ this.enableEdit();
+ } else {
+ this.disableEdit();
+ }
+ this.updateSelectedState();
+ this.updateBubblingMouseEvents();
+ }
+
+ public update(data: FormattedData, dsData: FormattedData[]): void {
+ this.data = data;
+ this.doUpdate(data, dsData);
+ }
+
+ public remove() {
+ if (this.selected) {
+ this.dataLayer.getMap().deselectItem(false, true);
+ }
+ this.dataLayer.getDataLayerContainer().removeLayer(this.layer);
+ this.layer.off();
+ }
+
+ public isEditing() {
+ return false;
+ }
+
+ protected bindEvents(): void {
+ if (this.dataLayer.isSelectable()) {
+ this.layer.on('click', () => {
+ if (!this.isEditing()) {
+ this.dataLayer.getMap().selectItem(this);
+ }
+ });
+ }
+ const clickAction = this.settings.click;
+ if (clickAction && clickAction.type !== WidgetActionType.doNothing) {
+ this.layer.on('click', (event) => {
+ this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.data);
+ });
+ }
+ }
+
+ protected enableEdit(): void {
+ if (this.dataLayer.isHoverable()) {
+ this.addItemClass('tb-hoverable');
+ }
+ if (this.dataLayer.isDragEnabled()) {
+ this.enableDrag();
+ this.addItemClass('tb-draggable');
+ }
+ }
+
+ protected disableEdit(): void {
+ if (this.dataLayer.isHoverable()) {
+ this.removeItemClass('tb-hoverable');
+ }
+ if (this.dataLayer.isDragEnabled()) {
+ this.disableDrag();
+ this.removeItemClass('tb-draggable');
+ }
+ }
+
+ protected updateSelectedState() {
+ if (this.selected) {
+ this.addItemClass('tb-selected');
+ } else {
+ this.removeItemClass('tb-selected');
+ }
+ }
+
+ protected updateTooltip(data: FormattedData, dsData: FormattedData[]) {
+ if (this.settings.tooltip.show) {
+ updateTooltip(this.dataLayer.getMap(), this.tooltip,
+ this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData);
+ }
+ }
+
+ protected updateLabel(data: FormattedData, dsData: FormattedData[]) {
+ if (this.settings.label.show) {
+ this.unbindLabel();
+ const label = this.dataLayer.dataLayerLabelProcessor.processPattern(data, dsData);
+ const labelColor = this.dataLayer.getCtx().widgetConfig.color;
+ const content: L.Content = `${label}
`;
+ this.bindLabel(content);
+ }
+ }
+
+ protected canDeselect(cancel = false): boolean {
+ return true;
+ }
+
+ protected onSelected(): L.TB.ToolbarButtonOptions[] {
+ return [];
+ }
+
+ protected onDeselected(): void {}
+
+ protected abstract create(data: FormattedData, dsData: FormattedData[]): L;
+
+ protected abstract doUpdate(data: FormattedData, dsData: FormattedData[]): void;
+
+ protected abstract doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void;
+
+ protected abstract unbindLabel(): void;
+
+ protected abstract bindLabel(content: L.Content): void;
+
+ protected abstract addItemClass(clazz: string): void;
+
+ protected abstract removeItemClass(clazz: string): void;
+
+ protected abstract enableDrag(): void;
+
+ protected abstract disableDrag(): void;
+
+ protected abstract updateBubblingMouseEvents(): void;
+
+ protected abstract removeDataItemTitle(): string;
+
+ protected abstract removeDataItem(): Observable;
+
+}
+
+export interface UnplacedMapDataItem {
+ entity: FormattedData;
+ dataLayer: TbLatestMapDataLayer;
+}
+
+export abstract class TbLatestMapDataLayer = any, L extends L.Layer = L.Layer> extends TbMapDataLayer> implements L.TB.DataLayer {
+
+ protected addEnabled = false;
+ protected dragEnabled = false;
+ protected editEnabled = false;
+ protected removeEnabled = false;
+
+ protected editable = false;
+ protected selectable = false;
+ protected hoverable = false;
+
+ private editMode = false;
+
+ private unplacedItems: UnplacedMapDataItem[] = [];
+
+ protected constructor(map: TbMap,
+ inputSettings: S) {
+ super(map, inputSettings);
+ if (this.settings.edit?.enabledActions) {
+ this.addEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.add);
+ this.dragEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.move);
+ this.editEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.edit);
+ this.removeEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.remove);
+
+ this.editable = this.addEnabled || this.dragEnabled || this.editEnabled || this.removeEnabled;
+ this.selectable = this.removeEnabled || this.editEnabled;
+ this.hoverable = this.selectable || this.dragEnabled;
+ }
+ this.snappable = this.settings.edit?.snappable;
+ this.enableEditMode();
+ }
+
+ public isEditMode(): boolean {
+ return this.editMode;
+ }
+
+ public isAddEnabled(): boolean {
+ return this.addEnabled;
+ }
+
+ public isDragEnabled(): boolean {
+ return this.dragEnabled;
+ }
+
+ public isEditEnabled(): boolean {
+ return this.editEnabled;
+ }
+
+ public isRemoveEnabled(): boolean {
+ return this.removeEnabled;
+ }
+
+ public isEditable(): boolean {
+ return this.editable;
+ }
+
+ public isHoverable(): boolean {
+ return this.hoverable;
+ }
+
+ public isSelectable(): boolean {
+ return this.selectable;
+ }
+
+ public isSnappable(): boolean {
+ return this.snappable;
+ }
+
+ public updateData(dsData: FormattedData[]) {
+ this.unplacedItems.length = 0;
+ const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId));
+ const toDelete = new Set(Array.from(this.layerItems.keys()));
+ const updatedItems: TbLatestDataLayerItem[] = [];
+ layerData.forEach((data) => {
+ if (this.isValidLayerData(data)) {
+ let layerItem = this.layerItems.get(data.entityId);
+ if (layerItem) {
+ layerItem.update(data, dsData);
+ updatedItems.push(layerItem);
+ } else {
+ layerItem = this.createLayerItem(data, dsData);
+ this.layerItems.set(data.entityId, layerItem);
+ }
+ toDelete.delete(data.entityId);
+ } else {
+ this.unplacedItems.push({
+ entity: data,
+ dataLayer: this
+ });
+ }
+ });
+ toDelete.forEach((key) => {
+ this.removeItem(key);
+ });
+ if (updatedItems.length) {
+ this.layerItemsUpdated(updatedItems);
+ }
+ }
+
+ public hasUnplacedItems(): boolean {
+ return !!this.unplacedItems.length;
+ }
+
+ public getUnplacedItems(): UnplacedMapDataItem[] {
+ return this.prepareUnplacedItems();
+ }
+
+ public enableEditMode() {
+ if (this.editable) {
+ if (!this.editMode) {
+ this.editMode = true;
+ this.updateItemsEditMode();
+ }
+ }
+ }
+
+ public disableEditMode() {
+ if (this.editMode) {
+ this.editMode = false;
+ this.updateItemsEditMode();
+ }
+ }
+
+ protected createDataLayerContainer(): L.FeatureGroup {
+ return L.featureGroup([], {snapIgnore: !this.settings.edit?.snappable});
+ }
+
+ protected onDataLayerEnabled(): void {
+ this.updateItemsEditMode();
+ }
+
+ protected onDataLayerDisabled(): void {
+ for (const item of this.layerItems) {
+ if (item[1].isSelected()) {
+ this.getMap().deselectItem(false, true);
+ break;
+ }
+ }
+ }
+
+ protected createItemFromUnplaced(unplacedItem: UnplacedMapDataItem): void {
+ const index = this.unplacedItems.indexOf(unplacedItem);
+ if (index > -1) {
+ this.unplacedItems.splice(index, 1);
+ const layerItem = this.createLayerItem(unplacedItem.entity, this.map.getData());
+ this.layerItems.set(unplacedItem.entity.entityId, layerItem);
+ this.map.enabledDataLayersUpdated();
+ }
+ }
+
+ protected layerItemsUpdated(_updatedItems: TbLatestDataLayerItem[]): void {
+ }
+
+ private prepareUnplacedItems(): UnplacedMapDataItem[] {
+ const div = document.createElement('div');
+ for (const item of this.unplacedItems) {
+ if (!item.entity.entityDisplayName) {
+ if (this.settings.label.show) {
+ div.innerHTML = this.dataLayerLabelProcessor.processPattern(item.entity, this.getMap().getData());
+ item.entity.entityDisplayName = div.textContent || div.innerText || '';
+ } else {
+ item.entity.entityDisplayName = item.entity.entityName;
+ }
+ }
+ }
+ return this.unplacedItems;
+ }
+
+ private updateItemsEditMode() {
+ this.layerItems.forEach(item => item.editModeUpdated());
+ }
+
+ public abstract placeItem(item: UnplacedMapDataItem, layer: L.Layer): void;
+
+ protected abstract isValidLayerData(layerData: FormattedData): boolean;
+
+ protected abstract createLayerItem(data: FormattedData, dsData: FormattedData[]): TbLatestDataLayerItem;
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
index e3f326b729..ce3f9862de 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
@@ -15,260 +15,34 @@
///
import {
- DataLayerColorSettings,
- DataLayerColorType,
- DataLayerEditAction,
- DataLayerPatternSettings,
- DataLayerPatternType,
- DataLayerTooltipTrigger,
- MapDataLayerSettings,
- mapDataSourceSettingsToDatasource,
- MapStringFunction,
- MapType,
- processTooltipTemplate,
+ DataLayerColorSettings, DataLayerColorType,
+ DataLayerPatternSettings, DataLayerPatternType,
+ MapDataLayerSettings, MapDataLayerType, mapDataSourceSettingsToDatasource,
+ MapStringFunction, MapType,
TbMapDatasource
} from '@home/components/widget/lib/maps/models/map.models';
-import { TbMap } from '@home/components/widget/lib/maps/map';
-import { FormattedData, WidgetActionType } from '@shared/models/widget.models';
-import { forkJoin, Observable, of } from 'rxjs';
import {
createLabelFromPattern,
- guid,
- isDefined,
+ guid, isDefined,
mergeDeepIgnoreArray,
parseTbFunction,
safeExecuteTbFunction
} from '@core/utils';
import L from 'leaflet';
import { CompiledTbFunction } from '@shared/models/js-function.models';
+import { forkJoin, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
-import { WidgetContext } from '@home/models/widget-component.models';
+import { FormattedData } from '@shared/models/widget.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
-import { createTooltip, updateTooltip } from './data-layer-utils';
-
-export abstract class TbDataLayerItem = TbMapDataLayer, L extends L.Layer = L.Layer> {
-
- protected layer: L;
- protected tooltip: L.Popup;
- protected data: FormattedData;
- protected selected = false;
-
- protected constructor(data: FormattedData,
- dsData: FormattedData[],
- protected settings: S,
- protected dataLayer: D) {
- this.data = data;
- this.layer = this.create(data, dsData);
- if (this.settings.tooltip?.show) {
- this.tooltip = createTooltip(this.dataLayer.getMap(),
- this.layer, this.settings.tooltip, this.data, () => {
- return !this.isEditing();
- });
- updateTooltip(this.dataLayer.getMap(), this.tooltip,
- this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData);
- }
- this.bindEvents();
- try {
- this.dataLayer.getDataLayerContainer().addLayer(this.layer);
- this.editModeUpdated();
- } catch (e) {
- console.warn(e);
- }
- }
-
- protected abstract create(data: FormattedData, dsData: FormattedData[]): L;
-
- protected abstract doUpdate(data: FormattedData, dsData: FormattedData[]): void;
-
- protected abstract doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void;
-
- protected abstract unbindLabel(): void;
-
- protected abstract bindLabel(content: L.Content): void;
-
- protected abstract addItemClass(clazz: string): void;
-
- protected abstract removeItemClass(clazz: string): void;
-
- protected abstract enableDrag(): void;
-
- protected abstract disableDrag(): void;
-
- protected abstract updateBubblingMouseEvents(): void;
-
- protected bindEvents(): void {
- if (this.dataLayer.isSelectable()) {
- this.layer.on('click', () => {
- if (!this.isEditing()) {
- this.dataLayer.getMap().selectItem(this);
- }
- });
- }
- const clickAction = this.settings.click;
- if (clickAction && clickAction.type !== WidgetActionType.doNothing) {
- this.layer.on('click', (event) => {
- this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.data);
- });
- }
- }
-
- protected enableEdit(): void {
- if (this.dataLayer.isHoverable()) {
- this.addItemClass('tb-hoverable');
- }
- if (this.dataLayer.isDragEnabled()) {
- this.enableDrag();
- this.addItemClass('tb-draggable');
- }
- }
-
- protected disableEdit(): void {
- if (this.dataLayer.isHoverable()) {
- this.removeItemClass('tb-hoverable');
- }
- if (this.dataLayer.isDragEnabled()) {
- this.disableDrag();
- this.removeItemClass('tb-draggable');
- }
- }
-
- protected updateSelectedState() {
- if (this.selected) {
- this.addItemClass('tb-selected');
- } else {
- this.removeItemClass('tb-selected');
- }
- }
-
- public invalidateCoordinates(): void {
- this.doInvalidateCoordinates(this.data, this.dataLayer.getMap().getData());
- }
-
- public select(): L.TB.ToolbarButtonOptions[] {
- if (!this.selected) {
- this.selected = true;
- this.disableEdit();
- this.updateSelectedState();
- const buttons = this.onSelected();
- if (this.dataLayer.isRemoveEnabled()) {
- buttons.push({
- id: 'remove',
- title: this.removeDataItemTitle(),
- click: () => {
- this.removeDataItem().subscribe(
- () => this.dataLayer.removeItem(this.data.entityId)
- );
- },
- iconClass: 'tb-remove'
- });
- }
- return buttons;
- } else {
- return [];
- }
- }
-
- public deselect(cancel = false, force = false): boolean {
- if (this.selected) {
- if (this.canDeselect(cancel) || force) {
- this.selected = false;
- this.layer.closePopup();
- this.updateSelectedState();
- this.onDeselected();
- this.editModeUpdated();
- } else {
- return false;
- }
- }
- return true;
- }
-
- public isSelected() {
- return this.selected;
- }
-
- public editModeUpdated() {
- if (this.dataLayer.isEditMode() && !this.selected) {
- this.enableEdit();
- } else {
- this.disableEdit();
- }
- this.updateSelectedState();
- this.updateBubblingMouseEvents();
- }
-
- public update(data: FormattedData, dsData: FormattedData[]): void {
- this.data = data;
- this.doUpdate(data, dsData);
- }
-
- public remove() {
- if (this.selected) {
- this.dataLayer.getMap().deselectItem(false, true);
- }
- this.dataLayer.getDataLayerContainer().removeLayer(this.layer);
- this.layer.off();
- }
-
- public getLayer(): L {
- return this.layer;
- }
-
- public getDataLayer(): D {
- return this.dataLayer;
- }
-
- public isEditing() {
- return false;
- }
-
- protected updateTooltip(data: FormattedData, dsData: FormattedData[]) {
- if (this.settings.tooltip.show) {
- updateTooltip(this.dataLayer.getMap(), this.tooltip,
- this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData);
- }
- }
-
- protected updateLabel(data: FormattedData, dsData: FormattedData[]) {
- if (this.settings.label.show) {
- this.unbindLabel();
- const label = this.dataLayer.dataLayerLabelProcessor.processPattern(data, dsData);
- const labelColor = this.dataLayer.getCtx().widgetConfig.color;
- const content: L.Content = `${label}
`;
- this.bindLabel(content);
- }
- }
-
- protected canDeselect(cancel = false): boolean {
- return true;
- }
-
- protected onSelected(): L.TB.ToolbarButtonOptions[] {
- return [];
- }
-
- protected onDeselected(): void {}
-
- protected abstract removeDataItemTitle(): string;
-
- protected abstract removeDataItem(): Observable;
-
-}
-
-export enum MapDataLayerType {
- trip = 'trip',
- marker = 'marker',
- polygon = 'polygon',
- circle = 'circle'
-}
+import { TbMap } from '@home/components/widget/lib/maps/map';
+import { WidgetContext } from '@home/models/widget-component.models';
export class DataLayerPatternProcessor {
private patternFunction: CompiledTbFunction;
private pattern: string;
- constructor(private dataLayer: TbMapDataLayer,
+ constructor(private dataLayer: TbMapDataLayer,
private settings: DataLayerPatternSettings) {}
public setup(): Observable {
@@ -304,7 +78,7 @@ export class DataLayerColorProcessor {
private colorFunction: CompiledTbFunction;
private color: string;
- constructor(private dataLayer: TbMapDataLayer,
+ constructor(private dataLayer: TbMapDataLayer,
private settings: DataLayerColorSettings) {}
public setup(): Observable {
@@ -333,12 +107,28 @@ export class DataLayerColorProcessor {
}
-export interface UnplacedMapDataItem {
- entity: FormattedData;
- dataLayer: TbMapDataLayer;
+export abstract class TbDataLayerItem {
+
+ protected layer: L;
+
+ protected constructor(protected settings: S,
+ protected dataLayer: D) {}
+
+ public getLayer(): L {
+ return this.layer;
+ }
+
+ public getDataLayer(): D {
+ return this.dataLayer;
+ }
+
+ public abstract remove(): void;
+
+ public abstract invalidateCoordinates(): void;
+
}
-export abstract class TbMapDataLayer = any, L extends L.Layer = L.Layer> implements L.TB.DataLayer {
+export abstract class TbMapDataLayer {
protected settings: S;
@@ -348,26 +138,14 @@ export abstract class TbMapDataLayer>();
+ protected layerItems = new Map();
protected groupsState: {[group: string]: boolean} = {};
protected enabled = true;
- protected addEnabled = false;
- protected dragEnabled = false;
- protected editEnabled = false;
- protected removeEnabled = false;
-
- protected editable = false;
- protected selectable = false;
- protected hoverable = false;
protected snappable = false;
- private editMode = false;
-
- private unplacedItems: UnplacedMapDataItem[] = [];
-
public dataLayerLabelProcessor: DataLayerPatternProcessor;
public dataLayerTooltipProcessor: DataLayerPatternProcessor;
@@ -379,24 +157,10 @@ export abstract class TbMapDataLayer {
@@ -412,56 +176,44 @@ export abstract class TbMapDataLayer item.invalidateCoordinates());
}
- public isDragEnabled(): boolean {
- return this.dragEnabled;
+ public getCtx(): WidgetContext {
+ return this.map.getCtx();
}
- public isEditEnabled(): boolean {
- return this.editEnabled;
+ public getMap(): TbMap {
+ return this.map;
}
- public isRemoveEnabled(): boolean {
- return this.removeEnabled;
+ public mapType(): MapType {
+ return this.map.type();
}
- public isEditable(): boolean {
- return this.editable;
+ public getDatasource(): TbMapDatasource {
+ return this.datasource;
}
- public isHoverable(): boolean {
- return this.hoverable;
+ public getDataLayerContainer(): L.FeatureGroup {
+ return this.dataLayerContainer;
}
- public isSelectable(): boolean {
- return this.selectable;
+ public getBounds(): L.LatLngBounds {
+ return this.dataLayerContainer.getBounds();
}
- public isSnappable(): boolean {
- return this.snappable;
+ public isEnabled(): boolean {
+ return this.enabled;
}
public getGroups(): string[] {
@@ -476,14 +228,9 @@ export abstract class TbMapDataLayer[]) {
- this.unplacedItems.length = 0;
- const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId));
- const toDelete = new Set(Array.from(this.layerItems.keys()));
- const updatedItems: TbDataLayerItem[] = [];
- layerData.forEach((data) => {
- if (this.isValidLayerData(data)) {
- let layerItem = this.layerItems.get(data.entityId);
- if (layerItem) {
- layerItem.update(data, dsData);
- updatedItems.push(layerItem);
- } else {
- layerItem = this.createLayerItem(data, dsData);
- this.layerItems.set(data.entityId, layerItem);
- }
- toDelete.delete(data.entityId);
- } else {
- this.unplacedItems.push({
- entity: data,
- dataLayer: this
- });
- }
- });
- toDelete.forEach((key) => {
- this.removeItem(key);
- });
- if (updatedItems.length) {
- this.layerItemsUpdated(updatedItems);
- }
- }
-
- public removeItem(key: string): void {
- const item = this.layerItems.get(key);
- if (item) {
- item.remove();
- this.layerItems.delete(key);
- }
- }
-
- protected createItemFromUnplaced(unplacedItem: UnplacedMapDataItem): void {
- const index = this.unplacedItems.indexOf(unplacedItem);
- if (index > -1) {
- this.unplacedItems.splice(index, 1);
- const layerItem = this.createLayerItem(unplacedItem.entity, this.map.getData());
- this.layerItems.set(unplacedItem.entity.entityId, layerItem);
- this.map.enabledDataLayersUpdated();
- }
- }
-
- public invalidateCoordinates(): void {
- this.layerItems.forEach(item => item.invalidateCoordinates());
- }
-
- public getCtx(): WidgetContext {
- return this.map.getCtx();
- }
-
- public getMap(): TbMap {
- return this.map;
- }
-
- public hasUnplacedItems(): boolean {
- return !!this.unplacedItems.length;
- }
-
- private prepareUnplacedItems(): UnplacedMapDataItem[] {
- const div = document.createElement('div');
- for (const item of this.unplacedItems) {
- if (!item.entity.entityDisplayName) {
- if (this.settings.label.show) {
- div.innerHTML = this.dataLayerLabelProcessor.processPattern(item.entity, this.getMap().getData());
- item.entity.entityDisplayName = div.textContent || div.innerText || '';
- } else {
- item.entity.entityDisplayName = item.entity.entityName;
- }
- }
- }
- return this.unplacedItems;
- }
-
- public getUnplacedItems(): UnplacedMapDataItem[] {
- return this.prepareUnplacedItems();
- }
-
protected createDataLayerContainer(): L.FeatureGroup {
- return L.featureGroup([], {snapIgnore: !this.snappable});
+ return L.featureGroup([], {snapIgnore: true});
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
return datasource;
}
- protected layerItemsUpdated(_updatedItems: TbDataLayerItem[]): void {
- }
+ protected onDataLayerEnabled(): void {}
- protected mapType(): MapType {
- return this.map.type();
- }
-
- public enableEditMode() {
- if (this.editable) {
- if (!this.editMode) {
- this.editMode = true;
- this.updateItemsEditMode();
- }
- }
- }
-
- public disableEditMode() {
- if (this.editMode) {
- this.editMode = false;
- this.updateItemsEditMode();
- }
- }
-
- private updateItemsEditMode() {
- this.layerItems.forEach(item => item.editModeUpdated());
- }
+ protected onDataLayerDisabled(): void {}
public abstract dataLayerType(): MapDataLayerType;
- public abstract placeItem(item: UnplacedMapDataItem, layer: L.Layer): void;
-
protected abstract defaultBaseSettings(map: TbMap): Partial;
protected abstract doSetup(): Observable;
- protected abstract isValidLayerData(layerData: FormattedData): boolean;
-
- protected abstract createLayerItem(data: FormattedData, dsData: FormattedData[]): TbDataLayerItem;
-
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts
index e99974cae4..5e9517b7fc 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts
@@ -20,7 +20,7 @@ import {
DataLayerColorType,
defaultBaseMarkersDataLayerSettings,
isValidLatLng,
- loadImageWithAspect,
+ loadImageWithAspect, MapDataLayerType,
MapStringFunction,
MapType,
MarkerIconInfo,
@@ -59,208 +59,159 @@ import {
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import {
- MapDataLayerType,
- TbDataLayerItem,
- TbMapDataLayer, UnplacedMapDataItem
-} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
+ TbLatestDataLayerItem,
+ TbLatestMapDataLayer, UnplacedMapDataItem
+} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { TbImageMap } from '@home/components/widget/lib/maps/image-map';
+import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
-class TbMarkerDataLayerItem extends TbDataLayerItem {
+export class MarkerDataProcessor {
- private marker: L.Marker;
- private labelOffset: L.PointTuple;
- private iconClassList: string[];
- private moving = false;
-
- constructor(data: FormattedData,
- dsData: FormattedData[],
- protected settings: MarkersDataLayerSettings,
- protected dataLayer: TbMarkersDataLayer) {
- super(data, dsData, settings, dataLayer);
- }
-
- protected create(data: FormattedData, dsData: FormattedData[]): L.Marker {
- this.iconClassList = [];
- const location = this.dataLayer.extractLocation(data, dsData);
- this.marker = L.marker(location, {
- tbMarkerData: data,
- snapIgnore: !this.dataLayer.isSnappable(),
- bubblingMouseEvents: !this.dataLayer.isEditMode()
- });
- this.updateMarkerIcon(data, dsData);
- return this.marker;
- }
-
- protected unbindLabel() {
- this.marker.unbindTooltip();
- }
-
- protected bindLabel(content: L.Content): void {
- this.marker.bindTooltip(content, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset });
- }
-
- protected doUpdate(data: FormattedData, dsData: FormattedData[]): void {
- this.marker.options.tbMarkerData = data;
- this.updateMarkerLocation(data, dsData);
- this.updateTooltip(data, dsData);
- this.updateMarkerIcon(data, dsData);
- }
+ private positionFunction: CompiledTbFunction;
+ private markerIconProcessor: MarkerIconProcessor;
- protected doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void {
- this.updateMarkerLocation(data, dsData);
+ constructor(public dataLayer: TbMapDataLayer,
+ private settings: S,
+ public markerOffset: L.LatLngTuple,
+ public tooltipOffset: L.LatLngTuple,
+ private trip = false) {
}
- protected addItemClass(clazz: string): void {
- if (!this.iconClassList.includes(clazz)) {
- this.iconClassList.push(clazz);
- this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className);
- if ((this.marker as any)._icon) {
- L.DomUtil.addClass((this.marker as any)._icon, clazz);
- }
+ public setup(): Observable {
+ this.markerIconProcessor = MarkerIconProcessor.fromSettings(this, this.settings);
+ const setup$: Observable[] = [this.markerIconProcessor.setup()];
+ if (this.dataLayer.mapType() === MapType.image) {
+ setup$.push(
+ parseTbFunction(this.dataLayer.getCtx().http, this.settings.positionFunction, ['origXPos', 'origYPos', 'data', 'dsData', 'aspect']).pipe(
+ map((parsed) => {
+ this.positionFunction = parsed;
+ return null;
+ })
+ )
+ );
}
+ return forkJoin(setup$).pipe(map(() => null));
}
- protected removeItemClass(clazz: string): void {
- const index = this.iconClassList.indexOf(clazz);
- if (index !== -1) {
- this.iconClassList.splice(index, 1);
- this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className, clazz);
- if ((this.marker as any)._icon) {
- L.DomUtil.removeClass((this.marker as any)._icon, clazz);
+ public extractLocation(data: FormattedData, dsData: FormattedData[]): L.LatLng {
+ let locationData = this.extractLocationData(data);
+ if (locationData) {
+ if (this.dataLayer.mapType() === MapType.image && this.positionFunction) {
+ const imageMap = this.dataLayer.getMap() as TbImageMap;
+ locationData = this.positionFunction.execute(locationData.x, locationData.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0};
}
- }
- }
-
- protected enableDrag(): void {
- if (this.settings.markerClustering?.enable) {
- this.marker.options.draggable = true;
- this.marker.on('dragstart', () => {
- this.moving = true;
- });
- this.marker.on('dragend', () => {
- this.saveMarkerLocation();
- this.moving = false;
- });
+ return this.dataLayer.getMap().locationDataToLatLng(locationData);
} else {
- this.marker.pm.setOptions({
- snappable: this.dataLayer.isSnappable()
- });
- this.marker.pm.enableLayerDrag();
- this.marker.on('pm:dragstart', () => {
- this.moving = true;
- });
- this.marker.on('pm:dragend', () => {
- this.saveMarkerLocation();
- this.moving = false;
- });
+ return null;
}
}
- protected disableDrag(): void {
- if (this.settings.markerClustering?.enable) {
- this.marker.options.draggable = false;
- this.marker.off('dragstart');
- this.marker.off('dragend');
+ public extractLocationData(data: FormattedData): {x: number; y: number} {
+ if (data) {
+ const xKeyVal = data[this.settings.xKey.label];
+ const yKeyVal = data[this.settings.yKey.label];
+ switch (this.dataLayer.mapType()) {
+ case MapType.geoMap:
+ if (!isValidLatLng(xKeyVal, yKeyVal)) {
+ return null;
+ }
+ break;
+ case MapType.image:
+ if (!isDefinedAndNotNull(xKeyVal) || isEmptyStr(xKeyVal) || isNaN(xKeyVal) || !isDefinedAndNotNull(yKeyVal) || isEmptyStr(yKeyVal) || isNaN(yKeyVal)) {
+ return null;
+ }
+ break;
+ }
+ return {x: xKeyVal, y: yKeyVal};
} else {
- this.marker.pm.disableLayerDrag();
- this.marker.off('pm:dragstart');
- this.marker.off('pm:dragend');
+ return null;
}
}
- protected removeDataItemTitle(): string {
- return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.marker.remove-marker-for', {entityName: this.data.entityName});
- }
-
- protected removeDataItem(): Observable {
- return this.dataLayer.saveMarkerLocation(this.data, null);
- }
-
- public isEditing() {
- return this.moving;
- }
-
- public updateBubblingMouseEvents() {
- this.marker.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
- }
-
- private saveMarkerLocation() {
- const location = this.marker.getLatLng();
- this.dataLayer.saveMarkerLocation(this.data, location).subscribe();
+ public createMarkerIcon(data: FormattedData,
+ dsData: FormattedData[],
+ rotationAngle?: number): Observable {
+ return this.markerIconProcessor.createMarkerIcon(data, dsData, rotationAngle);
}
- private updateMarkerLocation(data: FormattedData, dsData: FormattedData[]) {
- const location = this.dataLayer.extractLocation(data, dsData);
- if (!this.marker.getLatLng().equals(location) && !this.moving) {
- this.marker.setLatLng(location);
- }
+ public createDefaultMarkerIcon(rotationAngle = 0): Observable {
+ const color = this.settings.markerShape?.color?.color || '#307FE5';
+ return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color), rotationAngle);
}
- private updateMarkerIcon(data: FormattedData, dsData: FormattedData[]) {
- this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe(
- (iconInfo) => {
- let icon: L.Icon | L.DivIcon;
- const options = deepClone(iconInfo.icon.options);
- options.className = this.updateIconClasses(options.className);
- if (iconInfo.icon instanceof L.DivIcon) {
- icon = L.divIcon(options);
+ public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable {
+ return createColorMarkerShapeURI(this.dataLayer.getCtx().$injector.get(MatIconRegistry), this.dataLayer.getCtx().$injector.get(DomSanitizer), shape, color).pipe(
+ map((iconUrl) => {
+ let icon: L.Icon;
+ if (rotationAngle === 0) {
+ icon = L.icon({
+ iconUrl,
+ iconSize: [size, size],
+ iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]],
+ popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]]
+ });
} else {
- icon = L.icon(options as L.IconOptions);
+ const style = `background-image: url(${iconUrl}); transform: rotate(${rotationAngle}deg); height: ${size}px; width: ${size}px;`;
+ icon = L.divIcon({
+ html: ``,
+ className: 'tb-marker-div-icon',
+ iconSize: [size, size],
+ iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]],
+ popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]]
+ });
}
- this.marker.setIcon(icon);
- const anchor = options.iconAnchor;
- if (anchor && Array.isArray(anchor)) {
- this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]];
- } else {
- this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10];
+ return {
+ size: [size, size],
+ icon
}
- this.updateLabel(data, dsData);
- this.editModeUpdated();
- }
+ })
);
}
- private updateIconClasses(className: string, toRemove?: string): string {
- const classes: string[] = [];
- if (className?.length) {
- classes.push(...className.split(' '));
- }
- if (toRemove?.length) {
- const index = classes.indexOf(toRemove);
- if (index !== -1) {
- classes.splice(index, 1);
- }
- }
- this.iconClassList.forEach(clazz => {
- if (!classes.includes(clazz)) {
- classes.push(clazz);
- }
- });
- return classes.join(' ');
+ public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable {
+ return createColorMarkerIconElement(this.dataLayer.getCtx().$injector.get(MatIconRegistry), this.dataLayer.getCtx().$injector.get(DomSanitizer), icon, color, this.trip).pipe(
+ map((element) => {
+ if (rotationAngle !== 0) {
+ element.style.transform = `rotate(${rotationAngle}deg)`;
+ }
+ return {
+ size: [size, size],
+ icon: L.divIcon({
+ html: element.outerHTML,
+ className: 'tb-marker-div-icon',
+ iconSize: [size, size],
+ iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]],
+ popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]]
+ })
+ };
+ })
+ );
}
}
+
abstract class MarkerIconProcessor {
- static fromSettings(dataLayer: TbMarkersDataLayer,
+ static fromSettings(dataProcessor: MarkerDataProcessor,
settings: MarkersDataLayerSettings): MarkerIconProcessor {
switch (settings.markerType) {
case MarkerType.shape:
- return new ShapeMarkerIconProcessor(dataLayer, settings.markerShape);
+ return new ShapeMarkerIconProcessor(dataProcessor, settings.markerShape);
case MarkerType.icon:
- return new IconMarkerIconProcessor(dataLayer, settings.markerIcon);
+ return new IconMarkerIconProcessor(dataProcessor, settings.markerIcon);
case MarkerType.image:
- return new ImageMarkerIconProcessor(dataLayer, settings.markerImage);
+ return new ImageMarkerIconProcessor(dataProcessor, settings.markerImage);
}
}
- protected constructor(protected dataLayer: TbMarkersDataLayer,
+ protected constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: S) {}
public abstract setup(): Observable;
public abstract createMarkerIcon(data: FormattedData,
- dsData: FormattedData[]): Observable;
+ dsData: FormattedData[],
+ rotationAngle?: number): Observable;
}
@@ -270,15 +221,15 @@ abstract class BaseColorMarkerShapeProcessor
private defaultMarkerIconInfo: MarkerIconInfo;
- protected constructor(protected dataLayer: TbMarkersDataLayer,
+ protected constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: S) {
- super(dataLayer, settings);
+ super(dataProcessor, settings);
}
public setup(): Observable {
const colorSettings = this.settings.color;
if (colorSettings.type === DataLayerColorType.function) {
- return parseTbFunction(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe(
+ return parseTbFunction(this.dataProcessor.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe(
map((parsed) => {
this.markerColorFunction = parsed;
return null;
@@ -286,7 +237,7 @@ abstract class BaseColorMarkerShapeProcessor
);
} else {
const color = tinycolor(colorSettings.color);
- return this.createMarkerShape(color, this.settings.size).pipe(
+ return this.createMarkerShape(color, 0, this.settings.size).pipe(
map((info) => {
this.defaultMarkerIconInfo = info;
return null;
@@ -295,47 +246,50 @@ abstract class BaseColorMarkerShapeProcessor
}
}
- public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable {
+ public createMarkerIcon(data: FormattedData, dsData: FormattedData[], rotationAngle = 0): Observable {
const colorSettings = this.settings.color;
+ let color: tinycolor.Instance;
if (colorSettings.type === DataLayerColorType.function) {
const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]);
- let color: tinycolor.Instance;
if (isDefinedAndNotNull(functionColor)) {
color = tinycolor(functionColor);
} else {
color = tinycolor(colorSettings.color);
}
- return this.createMarkerShape(color, this.settings.size);
- } else {
+ return this.createMarkerShape(color, rotationAngle, this.settings.size);
+ } else if (rotationAngle === 0) {
return of(this.defaultMarkerIconInfo);
+ } else {
+ color = tinycolor(colorSettings.color);
+ return this.createMarkerShape(color, rotationAngle, this.settings.size);
}
}
- protected abstract createMarkerShape(color: tinycolor.Instance, size: number): Observable;
+ protected abstract createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable;
}
class ShapeMarkerIconProcessor extends BaseColorMarkerShapeProcessor {
- constructor(protected dataLayer: TbMarkersDataLayer,
+ constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: MarkerShapeSettings) {
- super(dataLayer, settings);
+ super(dataProcessor, settings);
}
- protected createMarkerShape(color: tinycolor.Instance, size: number): Observable {
- return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, size);
+ protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable {
+ return this.dataProcessor.createColoredMarkerShape(this.settings.shape, color, rotationAngle, size);
}
}
class IconMarkerIconProcessor extends BaseColorMarkerShapeProcessor {
- constructor(protected dataLayer: TbMarkersDataLayer,
+ constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: MarkerIconSettings) {
- super(dataLayer, settings);
+ super(dataProcessor, settings);
}
- protected createMarkerShape(color: tinycolor.Instance, size: number): Observable {
- return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, size);
+ protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable {
+ return this.dataProcessor.createColoredMarkerIcon(this.settings.icon, color, rotationAngle, size);
}
}
@@ -346,14 +300,14 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor
private defaultMarkerIconInfo: MarkerIconInfo;
- constructor(protected dataLayer: TbMarkersDataLayer,
+ constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: MarkerImageSettings) {
- super(dataLayer, settings);
+ super(dataProcessor, settings);
}
public setup(): Observable {
if (this.settings.type === MarkerImageType.function) {
- return parseTbFunction(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe(
+ return parseTbFunction(this.dataProcessor.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe(
map((parsed) => {
this.markerImageFunction = parsed;
return null;
@@ -364,7 +318,7 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor
url: this.settings.image,
size: this.settings.imageSize || 34
};
- return this.loadMarkerIconInfo(currentImage).pipe(
+ return this.loadMarkerIconInfo(currentImage, 0).pipe(
map((iconInfo) => {
this.defaultMarkerIconInfo = iconInfo;
return null;
@@ -373,18 +327,24 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor
}
}
- public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable {
+ public createMarkerIcon(data: FormattedData, dsData: FormattedData[], rotationAngle = 0): Observable {
if (this.settings.type === MarkerImageType.function) {
const currentImage: MarkerImageInfo = safeExecuteTbFunction(this.markerImageFunction, [data, this.settings.images, dsData]);
- return this.loadMarkerIconInfo(currentImage);
- } else {
+ return this.loadMarkerIconInfo(currentImage, rotationAngle);
+ } else if (rotationAngle === 0) {
return of(this.defaultMarkerIconInfo);
+ } else {
+ const currentImage: MarkerImageInfo = {
+ url: this.settings.image,
+ size: this.settings.imageSize || 34
+ };
+ return this.loadMarkerIconInfo(currentImage, rotationAngle);
}
}
- private loadMarkerIconInfo(image: MarkerImageInfo): Observable {
+ private loadMarkerIconInfo(image: MarkerImageInfo, rotationAngle = 0): Observable {
if (image && image.url) {
- return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe(
+ return loadImageWithAspect(this.dataProcessor.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe(
switchMap((aspectImage) => {
if (aspectImage?.aspect) {
let width: number;
@@ -399,45 +359,230 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor
let iconAnchor = image.markerOffset;
let popupAnchor = image.tooltipOffset;
if (!iconAnchor) {
- iconAnchor = [width * this.dataLayer.markerOffset[0], height * this.dataLayer.markerOffset[1]];
+ iconAnchor = [width * this.dataProcessor.markerOffset[0], height * this.dataProcessor.markerOffset[1]];
}
if (!popupAnchor) {
- popupAnchor = [width * this.dataLayer.tooltipOffset[0], height * this.dataLayer.tooltipOffset[1]];
+ popupAnchor = [width * this.dataProcessor.tooltipOffset[0], height * this.dataProcessor.tooltipOffset[1]];
+ }
+ let icon: L.Icon;
+ if (rotationAngle === 0) {
+ icon = L.icon({
+ iconUrl: aspectImage.url,
+ iconSize: [width, height],
+ iconAnchor,
+ popupAnchor
+ });
+ } else {
+ const style = `background-image: url(${aspectImage.url}); background-size: contain; transform: rotate(${rotationAngle}deg); height: ${height}px; width: ${width}px;`;
+ icon = L.divIcon({
+ html: ``,
+ className: 'tb-marker-div-icon',
+ iconSize: [width, height],
+ iconAnchor,
+ popupAnchor
+ });
}
- const icon = L.icon({
- iconUrl: aspectImage.url,
- iconSize: [width, height],
- iconAnchor,
- popupAnchor
- });
const iconInfo: MarkerIconInfo = {
size: [width, height],
icon
};
return of(iconInfo);
} else {
- return this.dataLayer.createDefaultMarkerIcon();
+ return this.dataProcessor.createDefaultMarkerIcon(rotationAngle);
}
}),
- catchError(() => this.dataLayer.createDefaultMarkerIcon())
+ catchError(() => this.dataProcessor.createDefaultMarkerIcon(rotationAngle))
);
} else {
- return this.dataLayer.createDefaultMarkerIcon();
+ return this.dataProcessor.createDefaultMarkerIcon(rotationAngle);
+ }
+ }
+}
+
+class TbMarkerDataLayerItem extends TbLatestDataLayerItem {
+
+ private marker: L.Marker;
+ private labelOffset: L.PointTuple;
+ private iconClassList: string[];
+ private moving = false;
+
+ constructor(data: FormattedData,
+ dsData: FormattedData[],
+ protected settings: MarkersDataLayerSettings,
+ protected dataLayer: TbMarkersDataLayer) {
+ super(data, dsData, settings, dataLayer);
+ }
+
+ public isEditing() {
+ return this.moving;
+ }
+
+ public updateBubblingMouseEvents() {
+ this.marker.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
+ }
+
+ protected create(data: FormattedData, dsData: FormattedData[]): L.Marker {
+ this.iconClassList = [];
+ const location = this.dataLayer.dataProcessor.extractLocation(data, dsData);
+ this.marker = L.marker(location, {
+ tbMarkerData: data,
+ snapIgnore: !this.dataLayer.isSnappable(),
+ bubblingMouseEvents: !this.dataLayer.isEditMode()
+ });
+ this.updateMarkerIcon(data, dsData);
+ return this.marker;
+ }
+
+ protected unbindLabel() {
+ this.marker.unbindTooltip();
+ }
+
+ protected bindLabel(content: L.Content): void {
+ this.marker.bindTooltip(content, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset });
+ }
+
+ protected doUpdate(data: FormattedData, dsData: FormattedData[]): void {
+ this.marker.options.tbMarkerData = data;
+ this.updateMarkerLocation(data, dsData);
+ this.updateTooltip(data, dsData);
+ this.updateMarkerIcon(data, dsData);
+ }
+
+ protected doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void {
+ this.updateMarkerLocation(data, dsData);
+ }
+
+ protected addItemClass(clazz: string): void {
+ if (!this.iconClassList.includes(clazz)) {
+ this.iconClassList.push(clazz);
+ this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className);
+ if ((this.marker as any)._icon) {
+ L.DomUtil.addClass((this.marker as any)._icon, clazz);
+ }
+ }
+ }
+
+ protected removeItemClass(clazz: string): void {
+ const index = this.iconClassList.indexOf(clazz);
+ if (index !== -1) {
+ this.iconClassList.splice(index, 1);
+ this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className, clazz);
+ if ((this.marker as any)._icon) {
+ L.DomUtil.removeClass((this.marker as any)._icon, clazz);
+ }
+ }
+ }
+
+ protected enableDrag(): void {
+ if (this.settings.markerClustering?.enable) {
+ this.marker.options.draggable = true;
+ this.marker.on('dragstart', () => {
+ this.moving = true;
+ });
+ this.marker.on('dragend', () => {
+ this.saveMarkerLocation();
+ this.moving = false;
+ });
+ } else {
+ this.marker.pm.setOptions({
+ snappable: this.dataLayer.isSnappable()
+ });
+ this.marker.pm.enableLayerDrag();
+ this.marker.on('pm:dragstart', () => {
+ this.moving = true;
+ });
+ this.marker.on('pm:dragend', () => {
+ this.saveMarkerLocation();
+ this.moving = false;
+ });
+ }
+ }
+
+ protected disableDrag(): void {
+ if (this.settings.markerClustering?.enable) {
+ this.marker.options.draggable = false;
+ this.marker.off('dragstart');
+ this.marker.off('dragend');
+ } else {
+ this.marker.pm.disableLayerDrag();
+ this.marker.off('pm:dragstart');
+ this.marker.off('pm:dragend');
}
}
+ protected removeDataItemTitle(): string {
+ return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.marker.remove-marker-for', {entityName: this.data.entityName});
+ }
+
+ protected removeDataItem(): Observable {
+ return this.dataLayer.saveMarkerLocation(this.data, null);
+ }
+
+ private saveMarkerLocation() {
+ const location = this.marker.getLatLng();
+ this.dataLayer.saveMarkerLocation(this.data, location).subscribe();
+ }
+
+ private updateMarkerLocation(data: FormattedData, dsData: FormattedData[]) {
+ const location = this.dataLayer.dataProcessor.extractLocation(data, dsData);
+ if (!this.marker.getLatLng().equals(location) && !this.moving) {
+ this.marker.setLatLng(location);
+ }
+ }
+
+ private updateMarkerIcon(data: FormattedData, dsData: FormattedData[]) {
+ this.dataLayer.dataProcessor.createMarkerIcon(data, dsData).subscribe(
+ (iconInfo) => {
+ let icon: L.Icon | L.DivIcon;
+ const options = deepClone(iconInfo.icon.options);
+ options.className = this.updateIconClasses(options.className);
+ if (iconInfo.icon instanceof L.DivIcon) {
+ icon = L.divIcon(options);
+ } else {
+ icon = L.icon(options as L.IconOptions);
+ }
+ this.marker.setIcon(icon);
+ const anchor = options.iconAnchor;
+ if (anchor && Array.isArray(anchor)) {
+ this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]];
+ } else {
+ this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10];
+ }
+ this.updateLabel(data, dsData);
+ this.editModeUpdated();
+ }
+ );
+ }
+
+ private updateIconClasses(className: string, toRemove?: string): string {
+ const classes: string[] = [];
+ if (className?.length) {
+ classes.push(...className.split(' '));
+ }
+ if (toRemove?.length) {
+ const index = classes.indexOf(toRemove);
+ if (index !== -1) {
+ classes.splice(index, 1);
+ }
+ }
+ this.iconClassList.forEach(clazz => {
+ if (!classes.includes(clazz)) {
+ classes.push(clazz);
+ }
+ });
+ return classes.join(' ');
+ }
}
-export class TbMarkersDataLayer extends TbMapDataLayer {
+export class TbMarkersDataLayer extends TbLatestMapDataLayer {
- public markerIconProcessor: MarkerIconProcessor;
+ public dataProcessor: MarkerDataProcessor;
public markerOffset: L.LatLngTuple;
public tooltipOffset: L.LatLngTuple;
private markersClusterContainer: L.MarkerClusterGroup;
private clusterMarkerColorFunction: CompiledTbFunction;
- private positionFunction: CompiledTbFunction;
constructor(protected map: TbMap,
inputSettings: MarkersDataLayerSettings) {
@@ -445,7 +590,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer, position: L.LatLng): Observable<{x: number; y: number}> {
+ const converted = this.map.latLngToLocationData(position);
+ const locationData = [
+ {
+ dataKey: this.settings.xKey,
+ value: converted.x
+ },
+ {
+ dataKey: this.settings.yKey,
+ value: converted.y
+ }
+ ];
+ return this.map.saveItemData(data.$datasource, locationData).pipe(
+ map(() => converted)
+ );
+ }
+
protected createDataLayerContainer(): FeatureGroup {
if (this.settings.markerClustering?.enable) {
return this.createMarkersClusterContainer();
@@ -489,8 +651,8 @@ export class TbMarkersDataLayer extends TbMapDataLayer[] = [];
+ this.dataProcessor = new MarkerDataProcessor(this, this.settings, this.markerOffset, this.tooltipOffset);
+ const setup$: Observable[] = [this.dataProcessor.setup()];
if (this.settings.markerClustering?.enable && this.settings.markerClustering.useClusterMarkerColorFunction) {
setup$.push(
parseTbFunction(this.getCtx().http, this.settings.markerClustering.clusterMarkerColorFunction, ['data', 'childCount']).pipe(
@@ -501,29 +663,18 @@ export class TbMarkersDataLayer extends TbMapDataLayer(this.getCtx().http, this.settings.positionFunction, ['origXPos', 'origYPos', 'data', 'dsData', 'aspect']).pipe(
- map((parsed) => {
- this.positionFunction = parsed;
- return null;
- })
- )
- );
- }
- setup$.push(this.markerIconProcessor.setup());
return forkJoin(setup$).pipe(map(() => null));
}
protected isValidLayerData(layerData: FormattedData): boolean {
- return !!this.extractLocationData(layerData);
+ return !!this.dataProcessor.extractLocationData(layerData);
}
protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbMarkerDataLayerItem {
return new TbMarkerDataLayerItem(data, dsData, this.settings, this);
}
- protected layerItemsUpdated(updatedItems: TbDataLayerItem[]) {
+ protected layerItemsUpdated(updatedItems: TbLatestDataLayerItem[]) {
if (this.settings.markerClustering?.enable) {
this.markersClusterContainer.refreshClusters(updatedItems.map(item => item.getLayer()));
}
@@ -538,7 +689,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer {
@@ -583,94 +734,4 @@ export class TbMarkersDataLayer extends TbMapDataLayer): {x: number; y: number} {
- if (data) {
- const xKeyVal = data[this.settings.xKey.label];
- const yKeyVal = data[this.settings.yKey.label];
- switch (this.mapType()) {
- case MapType.geoMap:
- if (!isValidLatLng(xKeyVal, yKeyVal)) {
- return null;
- }
- break;
- case MapType.image:
- if (!isDefinedAndNotNull(xKeyVal) || isEmptyStr(xKeyVal) || isNaN(xKeyVal) || !isDefinedAndNotNull(yKeyVal) || isEmptyStr(yKeyVal) || isNaN(yKeyVal)) {
- return null;
- }
- break;
- }
- return {x: xKeyVal, y: yKeyVal};
- } else {
- return null;
- }
- }
-
- public createDefaultMarkerIcon(): Observable {
- const color = this.settings.markerShape?.color?.color || '#307FE5';
- return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color));
- }
-
- public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, size = 34): Observable {
- return createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), shape, color).pipe(
- map((iconUrl) => {
- return {
- size: [size, size],
- icon: L.icon({
- iconUrl,
- iconSize: [size, size],
- iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]],
- popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]]
- })
- };
- })
- );
- }
-
- public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, size = 34): Observable {
- return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color).pipe(
- map((element) => {
- return {
- size: [size, size],
- icon: L.divIcon({
- html: element.outerHTML,
- className: 'tb-marker-div-icon',
- iconSize: [size, size],
- iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]],
- popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]]
- })
- };
- })
- );
- }
-
- public extractLocation(data: FormattedData, dsData: FormattedData[]): L.LatLng {
- let locationData = this.extractLocationData(data);
- if (locationData) {
- if (this.map.type() === MapType.image && this.positionFunction) {
- const imageMap = this.map as TbImageMap;
- locationData = this.positionFunction.execute(locationData.x, locationData.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0};
- }
- return this.map.locationDataToLatLng(locationData);
- } else {
- return null;
- }
- }
-
- public saveMarkerLocation(data: FormattedData, position: L.LatLng): Observable<{x: number; y: number}> {
- const converted = this.map.latLngToLocationData(position);
- const locationData = [
- {
- dataKey: this.settings.xKey,
- value: converted.x
- },
- {
- dataKey: this.settings.yKey,
- value: converted.y
- }
- ];
- return this.map.saveItemData(data.$datasource, locationData).pipe(
- map(() => converted)
- );
- }
-
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts
index a2a8ba09a5..5d4f9a6ae3 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts
@@ -16,7 +16,7 @@
import {
defaultBasePolygonsDataLayerSettings,
- isCutPolygon, isJSON,
+ isCutPolygon, isJSON, MapDataLayerType,
PolygonsDataLayerSettings,
TbMapDatasource, TbPolyData, TbPolygonCoordinates, TbPolygonRawCoordinates
} from '@home/components/widget/lib/maps/models/map.models';
@@ -27,13 +27,12 @@ import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
import { isNotEmptyStr, isString } from '@core/utils';
import {
- MapDataLayerType,
- TbDataLayerItem,
+ TbLatestDataLayerItem,
UnplacedMapDataItem
-} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
+} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { map } from 'rxjs/operators';
-class TbPolygonDataLayerItem extends TbDataLayerItem {
+class TbPolygonDataLayerItem extends TbLatestDataLayerItem {
private polygonContainer: L.FeatureGroup;
private polygon: L.Polygon;
@@ -47,6 +46,14 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): L.Layer {
const polyData = this.dataLayer.extractPolygonCoordinates(data);
const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle;
@@ -199,14 +206,6 @@ class TbPolygonDataLayerItem extends TbDataLayerItem this.editing = true);
this.polygon.on('pm:markerdragend', () => setTimeout(() => {
@@ -364,7 +363,7 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer): TbPolygonRawCoordinates {
+ let rawPolyData = data[this.settings.polygonKey.label];
+ if (isString(rawPolyData)) {
+ rawPolyData = JSON.parse(rawPolyData);
+ }
+ return this.map.polygonDataToCoordinates(rawPolyData);
+ }
+
+ public savePolygonCoordinates(data: FormattedData, coordinates: TbPolygonCoordinates): Observable {
+ const converted = coordinates ? this.map.coordinatesToPolygonData(coordinates) : null;
+ const polygonData = [
+ {
+ dataKey: this.settings.polygonKey,
+ value: converted
+ }
+ ];
+ return this.map.saveItemData(data.$datasource, polygonData).pipe(
+ map(() => converted)
+ );
+ }
+
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.polygonKey);
return datasource;
@@ -412,24 +432,4 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer): TbPolygonRawCoordinates {
- let rawPolyData = data[this.settings.polygonKey.label];
- if (isString(rawPolyData)) {
- rawPolyData = JSON.parse(rawPolyData);
- }
- return this.map.polygonDataToCoordinates(rawPolyData);
- }
-
- public savePolygonCoordinates(data: FormattedData, coordinates: TbPolygonCoordinates): Observable {
- const converted = coordinates ? this.map.coordinatesToPolygonData(coordinates) : null;
- const polygonData = [
- {
- dataKey: this.settings.polygonKey,
- value: converted
- }
- ];
- return this.map.saveItemData(data.$datasource, polygonData).pipe(
- map(() => converted)
- );
- }
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
index b45c9f2ecf..39c703b00a 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
@@ -19,9 +19,10 @@ import L from 'leaflet';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { forkJoin, Observable } from 'rxjs';
import { FormattedData } from '@shared/models/widget.models';
-import { DataLayerColorProcessor, TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
+import { TbLatestMapDataLayer } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
+import { DataLayerColorProcessor } from './map-data-layer';
-export abstract class TbShapesDataLayer> extends TbMapDataLayer {
+export abstract class TbShapesDataLayer> extends TbLatestMapDataLayer {
public fillColorProcessor: DataLayerColorProcessor;
public strokeColorProcessor: DataLayerColorProcessor;
@@ -31,12 +32,6 @@ export abstract class TbShapesDataLayer {
- this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor);
- this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor);
- return forkJoin([this.fillColorProcessor.setup(), this.strokeColorProcessor.setup()]);
- }
-
public getShapeStyle(data: FormattedData, dsData: FormattedData[]): L.PathOptions {
const fill = this.fillColorProcessor.processColor(data, dsData);
const stroke = this.strokeColorProcessor.processColor(data, dsData);
@@ -49,4 +44,11 @@ export abstract class TbShapesDataLayer {
+ this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor);
+ this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor);
+ return forkJoin([this.fillColorProcessor.setup(), this.strokeColorProcessor.setup()]);
+ }
+
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
index 45aa31def5..27d726f15e 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
@@ -15,75 +15,39 @@
///
import {
- DataLayerColorProcessor,
- DataLayerPatternProcessor,
- MapDataLayerType,
- TbDataLayerItem,
- TbMapDataLayer,
- UnplacedMapDataItem
-} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
-import {
- BaseMarkerShapeSettings,
calculateInterpolationRatio,
calculateLastPoints,
- DataLayerColorType,
defaultBaseTripsDataLayerSettings,
findRotationAngle,
interpolateLineSegment,
- isValidLatLng,
- loadImageWithAspect,
- MapStringFunction,
- MapType,
- MarkerIconInfo,
- MarkerIconSettings,
- MarkerImageFunction,
- MarkerImageInfo,
- MarkerImageSettings,
- MarkerImageType,
- MarkerPositionFunction,
- MarkerShapeSettings,
- MarkerType,
+ MapDataLayerType,
TbMapDatasource,
TripsDataLayerSettings
} from '@home/components/widget/lib/maps/models/map.models';
-import { forkJoin, Observable, of } from 'rxjs';
+import { forkJoin, Observable } from 'rxjs';
import { FormattedData, WidgetActionType } from '@shared/models/widget.models';
-import {
- createColorMarkerIconElement,
- createColorMarkerShapeURI,
- MarkerShape
-} from '@home/components/widget/lib/maps/models/marker-shape.models';
-import tinycolor from 'tinycolor2';
-import { MatIconRegistry } from '@angular/material/icon';
-import { DomSanitizer } from '@angular/platform-browser';
-import { catchError, map, switchMap } from 'rxjs/operators';
+import { map } from 'rxjs/operators';
import L from 'leaflet';
-import { CompiledTbFunction } from '@shared/models/js-function.models';
-import {
- deepClone,
- isDefined,
- isDefinedAndNotNull,
- isEmptyStr,
- isUndefined,
- parseTbFunction,
- safeExecuteTbFunction
-} from '@core/utils';
-import { ImagePipe } from '@shared/pipe/image.pipe';
+import { deepClone, isDefined, isUndefined } from '@core/utils';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import moment from 'moment/moment';
-import { TbImageMap } from '@home/components/widget/lib/maps/image-map';
import { createTooltip, updateTooltip } from '@home/components/widget/lib/maps/data-layer/data-layer-utils';
import _ from 'lodash';
+import {
+ DataLayerColorProcessor,
+ DataLayerPatternProcessor,
+ TbDataLayerItem,
+ TbMapDataLayer
+} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
+import { MarkerDataProcessor } from '@home/components/widget/lib/maps/data-layer/markers-data-layer';
type TripRouteData = {[time: number]: FormattedData};
-class TbTripDataItem {
+class TbTripDataItem extends TbDataLayerItem {
private tripRouteData: TripRouteData;
- private layer: L.FeatureGroup;
-
private marker: L.Marker;
private markerTooltip: L.Popup;
private labelOffset: L.PointTuple;
@@ -98,29 +62,13 @@ class TbTripDataItem {
constructor(private rawRouteData: FormattedData[],
private latestData: FormattedData,
- private settings: TripsDataLayerSettings,
- private dataLayer: TbTripsDataLayer) {
+ settings: TripsDataLayerSettings,
+ dataLayer: TbTripsDataLayer) {
+ super(settings, dataLayer);
this.tripRouteData = this.prepareTripRouteData();
this.create();
}
- private create() {
- this.updateCurrentPosition();
- this.layer = L.featureGroup();
- this.pointData = this.currentPositionData;
- if (this.latestData) {
- this.pointData = {...this.pointData, ...this.latestData};
- }
- this.createPath();
- this.updatePoints();
- this.createMarker();
- try {
- this.dataLayer.getDataLayerContainer().addLayer(this.layer);
- } catch (e) {
- console.warn(e);
- }
- }
-
public update(rawRouteData: FormattedData[]) {
this.rawRouteData = rawRouteData;
this.tripRouteData = this.prepareTripRouteData();
@@ -176,20 +124,33 @@ class TbTripDataItem {
this.layer.off();
}
- public getLayer(): L.Layer {
- return this.layer;
- }
-
public calculateAnchors(): number[] {
const entries = Object.entries(this.tripRouteData);
const dsData = this.dataLayer.getMap().getData();
return entries.filter(data => this.dataLayer.getMap().getLocationSnapFilterFunction().execute(data[1], dsData))
- .map(data => parseInt(data[0], 10));
+ .map(data => parseInt(data[0], 10));
+ }
+
+ private create() {
+ this.updateCurrentPosition();
+ this.layer = L.featureGroup();
+ this.pointData = this.currentPositionData;
+ if (this.latestData) {
+ this.pointData = {...this.pointData, ...this.latestData};
+ }
+ this.createPath();
+ this.updatePoints();
+ this.createMarker();
+ try {
+ this.dataLayer.getDataLayerContainer().addLayer(this.layer);
+ } catch (e) {
+ console.warn(e);
+ }
}
private createMarker() {
const dsData = this.dataLayer.getMap().getData();
- const location = this.dataLayer.extractLocation(this.pointData, dsData);
+ const location = this.dataLayer.dataProcessor.extractLocation(this.pointData, dsData);
this.marker = L.marker(location, {
tbMarkerData: this.pointData,
snapIgnore: true
@@ -225,7 +186,7 @@ class TbTripDataItem {
if (this.settings.showPath) {
const formattedRouteData = _.values(this.tripRouteData);
const dsData = this.dataLayer.getMap().getData();
- const locations = formattedRouteData.map(data => this.dataLayer.extractLocation(data, dsData));
+ const locations = formattedRouteData.map(data => this.dataLayer.dataProcessor.extractLocation(data, dsData));
const pathStyle = this.dataLayer.getPathStyle(this.pointData, dsData);
this.polyline = L.polyline(locations, pathStyle);
this.polyline.addTo(this.layer);
@@ -240,7 +201,7 @@ class TbTripDataItem {
if (this.settings.showPath) {
const formattedRouteData = _.values(this.tripRouteData);
const dsData = this.dataLayer.getMap().getData();
- const locations = formattedRouteData.map(data => this.dataLayer.extractLocation(data, dsData));
+ const locations = formattedRouteData.map(data => this.dataLayer.dataProcessor.extractLocation(data, dsData));
this.polyline.setLatLngs(locations);
if (this.settings.usePathDecorator) {
this.polylineDecorator.setPaths(this.polyline);
@@ -258,14 +219,14 @@ class TbTripDataItem {
this.points.clearLayers();
const formattedRouteData = _.values(this.tripRouteData);
const dsData = this.dataLayer.getMap().getData();
- const pointsList = formattedRouteData.filter(data => !!this.dataLayer.extractLocationData(data));
+ const pointsList = formattedRouteData.filter(data => !!this.dataLayer.dataProcessor.extractLocationData(data));
for (const pData of pointsList) {
let pointData = pData;
if (this.latestData) {
pointData = {...pointData, ...this.latestData};
}
const pointColor = this.dataLayer.pointColorProcessor.processColor(pointData, dsData);
- const point = L.circleMarker(this.dataLayer.extractLocation(pointData, dsData), {
+ const point = L.circleMarker(this.dataLayer.dataProcessor.extractLocation(pointData, dsData), {
stroke: false,
fillOpacity: 1,
fillColor: pointColor,
@@ -294,14 +255,14 @@ class TbTripDataItem {
}
private updateMarkerLocation(data: FormattedData, dsData: FormattedData[]) {
- const location = this.dataLayer.extractLocation(data, dsData);
+ const location = this.dataLayer.dataProcessor.extractLocation(data, dsData);
if (!this.marker.getLatLng().equals(location)) {
this.marker.setLatLng(location);
}
}
private updateMarkerIcon(data: FormattedData, dsData: FormattedData[]) {
- this.dataLayer.tripMarkerIconProcessor.createMarkerIcon(data, dsData).subscribe(
+ this.dataLayer.dataProcessor.createMarkerIcon(data, dsData, data.rotationAngle).subscribe(
(iconInfo) => {
const options = deepClone(iconInfo.icon.options);
this.marker.setIcon(iconInfo.icon);
@@ -362,8 +323,8 @@ class TbTripDataItem {
}
}
if (this.settings.rotateMarker) {
- const startPoint = this.dataLayer.extractLocation(result[timeStamp[i]], dsData);
- const endPoint = this.dataLayer.extractLocation(result[timeStamp[i + 1]], dsData);
+ const startPoint = this.dataLayer.dataProcessor.extractLocation(result[timeStamp[i]], dsData);
+ const endPoint = this.dataLayer.dataProcessor.extractLocation(result[timeStamp[i + 1]], dsData);
result[timeStamp[i]].rotationAngle += findRotationAngle(startPoint, endPoint);
}
}
@@ -400,183 +361,9 @@ class TbTripDataItem {
}
-abstract class TripMarkerIconProcessor {
-
- static fromSettings(dataLayer: TbTripsDataLayer,
- settings: TripsDataLayerSettings): TripMarkerIconProcessor {
- switch (settings.markerType) {
- case MarkerType.shape:
- return new ShapeTripMarkerIconProcessor(dataLayer, settings.markerShape);
- case MarkerType.icon:
- return new IconTripMarkerIconProcessor(dataLayer, settings.markerIcon);
- case MarkerType.image:
- return new ImageTripMarkerIconProcessor(dataLayer, settings.markerImage);
- }
- }
-
- protected constructor(protected dataLayer: TbTripsDataLayer,
- protected settings: S) {}
+export class TbTripsDataLayer extends TbMapDataLayer {
- public abstract setup(): Observable;
-
- public abstract createMarkerIcon(data: FormattedData,
- dsData: FormattedData[]): Observable;
-
-}
-
-abstract class BaseColorTripMarkerShapeProcessor extends TripMarkerIconProcessor {
-
- private markerColorFunction: CompiledTbFunction;
-
- protected constructor(protected dataLayer: TbTripsDataLayer,
- protected settings: S) {
- super(dataLayer, settings);
- }
-
- public setup(): Observable {
- const colorSettings = this.settings.color;
- if (colorSettings.type === DataLayerColorType.function) {
- return parseTbFunction(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe(
- map((parsed) => {
- this.markerColorFunction = parsed;
- return null;
- })
- );
- } else {
- return of(null);
- }
- }
-
- public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable {
- const colorSettings = this.settings.color;
- let color: tinycolor.Instance;
- if (colorSettings.type === DataLayerColorType.function) {
- const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]);
- if (isDefinedAndNotNull(functionColor)) {
- color = tinycolor(functionColor);
- } else {
- color = tinycolor(colorSettings.color);
- }
- } else {
- color = tinycolor(colorSettings.color);
- }
- return this.createMarkerShape(color, data.rotationAngle, this.settings.size);
- }
-
- protected abstract createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable;
-}
-
-class ShapeTripMarkerIconProcessor extends BaseColorTripMarkerShapeProcessor {
-
- constructor(protected dataLayer: TbTripsDataLayer,
- protected settings: MarkerShapeSettings) {
- super(dataLayer, settings);
- }
-
- protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable {
- return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, rotationAngle, size);
- }
-
-}
-
-class IconTripMarkerIconProcessor extends BaseColorTripMarkerShapeProcessor {
-
- constructor(protected dataLayer: TbTripsDataLayer,
- protected settings: MarkerIconSettings) {
- super(dataLayer, settings);
- }
-
- protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable {
- return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, rotationAngle, size);
- }
-
-}
-
-class ImageTripMarkerIconProcessor extends TripMarkerIconProcessor {
-
- private markerImageFunction: CompiledTbFunction;
-
- constructor(protected dataLayer: TbTripsDataLayer,
- protected settings: MarkerImageSettings) {
- super(dataLayer, settings);
- }
-
- public setup(): Observable {
- if (this.settings.type === MarkerImageType.function) {
- return parseTbFunction(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe(
- map((parsed) => {
- this.markerImageFunction = parsed;
- return null;
- })
- );
- } else {
- return of(null);
- }
- }
-
- public createMarkerIcon(data: FormattedData