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 7060499f24..3e12e5a662 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -11,6 +11,7 @@ "widgetTypeFqns": [ "map", "image_map", + "trip_map", "maps_v2.openstreetmap", "maps_v2.google_maps", "maps_v2.image_map", 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 new file mode 100644 index 0000000000..e64d491779 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/trip_map.json @@ -0,0 +1,47 @@ +{ + "fqn": "trip_map", + "name": "Trip Map", + "deprecated": false, + "image": "tb-image;/api/images/system/trip_animation_system_widget_image.png", + "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Allows to scroll and animate the movement of the entity. 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 };\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\",\"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\"}" + }, + "resources": [ + { + "link": "/api/images/system/trip_animation_system_widget_image.png", + "title": "\"Trip Animation\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "trip_animation_system_widget_image.png", + "publicResourceKey": "3UKAE6mZvW6bnIhr7NAtYTG8FbIDFY1e", + "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/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 41a7c588ed..83c253f64c 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -512,18 +512,20 @@ export function formattedDataFormDatasourceData el.datasource.entityName) +export function formattedDataArrayFromDatasourceData(input: DatasourceData[], + groupFunction: (el: DatasourceData) => any = + (el) => el.datasource.entityName + el.datasource.entityType): FormattedData[][] { + return _(input).groupBy(groupFunction) .values().value().map((entityArray, dsIndex) => { - const timeDataMap: {[time: number]: FormattedData} = {}; + const timeDataMap: {[time: number]: FormattedData} = {}; entityArray.filter(e => e.data.length).forEach(entity => { entity.data.forEach(tsData => { const time = tsData[0]; const value = tsData[1]; let data = timeDataMap[time]; if (!data) { - const datasource = entity.datasource; - data = formattedDataFromDatasource(datasource, dsIndex); + const datasource = entity.datasource as D; + data = formattedDataFromDatasource(datasource, dsIndex); data.time = time; timeDataMap[time] = data; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html index b2803c4d50..dcefd50801 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html @@ -16,7 +16,10 @@ --> + + , protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -46,6 +52,14 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { return this.mapWidgetConfigForm; } + protected setupConfig(widgetConfig: WidgetConfigComponentData) { + const params = widgetConfig.typeParameters as any; + if (isDefinedAndNotNull(params.trip)) { + this.trip = params.trip === true; + } + super.setupConfig(widgetConfig); + } + protected setupDefaults(configData: WidgetConfigComponentData) { const settings = configData.config.settings as MapWidgetSettings; if (settings?.markers?.length) { @@ -57,6 +71,11 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { if (settings?.circles?.length) { settings.circles = []; } + if (this.trip) { + if (settings?.trips?.length) { + settings.trips = []; + } + } } protected onConfigSet(configData: WidgetConfigComponentData) { @@ -85,10 +104,15 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { actions: [configData.config.actions || {}, []] }); + if (this.trip) { + this.mapWidgetConfigForm.addControl('timewindowConfig', this.fb.control(getTimewindowConfig(configData.config))) + } } protected prepareOutputConfig(config: any): WidgetConfigComponentData { - + if (this.trip) { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + } this.widgetConfig.config.settings = config.mapSettings || {}; this.widgetConfig.config.showTitle = config.showTitle; 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 2d29de3e79..8916931d2b 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 @@ -306,12 +306,13 @@ export abstract class TbDataLayerItem; private pattern: string; 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 new file mode 100644 index 0000000000..354fa4eb0f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -0,0 +1,416 @@ +/// +/// 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 { + DataLayerColorProcessor, + DataLayerPatternProcessor, + MapDataLayerType, TbDataLayerItem, + TbMapDataLayer, + UnplacedMapDataItem +} from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { + BaseMarkerShapeSettings, ClusterMarkerColorFunction, + DataLayerColorType, defaultBaseMarkersDataLayerSettings, defaultBaseTripsDataLayerSettings, + loadImageWithAspect, + MapStringFunction, MapType, + MarkerIconInfo, + MarkerIconSettings, + MarkerImageFunction, + MarkerImageInfo, + MarkerImageSettings, + MarkerImageType, + MarkerPositionFunction, MarkersDataLayerSettings, + MarkerShapeSettings, + MarkerType, + TbMapDatasource, + TripsDataLayerSettings +} from '@home/components/widget/lib/maps/models/map.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { FormattedData } 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 L from 'leaflet'; +import { CompiledTbFunction } from '@shared/models/js-function.models'; +import { isDefined, isDefinedAndNotNull, parseTbFunction, safeExecuteTbFunction } from '@core/utils'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +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) {} + + 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, dsData: FormattedData[]): Observable { + if (this.settings.type === MarkerImageType.function) { + const currentImage: MarkerImageInfo = safeExecuteTbFunction(this.markerImageFunction, [data, this.settings.images, dsData]); + return this.loadMarkerIconInfo(currentImage, data.rotationAngle); + } else { + const currentImage: MarkerImageInfo = { + url: this.settings.image, + size: this.settings.imageSize || 34 + }; + return this.loadMarkerIconInfo(currentImage, data.rotationAngle); + } + } + + private loadMarkerIconInfo(image: MarkerImageInfo, rotationAngle = 0): Observable { + if (image && image.url) { + return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe( + switchMap((aspectImage) => { + if (aspectImage?.aspect) { + let width: number; + let height: number; + if (aspectImage.aspect > 1) { + width = image.size; + height = image.size / aspectImage.aspect; + } else { + width = image.size * aspectImage.aspect; + height = image.size; + } + let iconAnchor = image.markerOffset; + let popupAnchor = image.tooltipOffset; + if (!iconAnchor) { + iconAnchor = [width * this.dataLayer.markerOffset[0], height * this.dataLayer.markerOffset[1]]; + } + if (!popupAnchor) { + popupAnchor = [width * this.dataLayer.tooltipOffset[0], height * this.dataLayer.tooltipOffset[1]]; + } + const style = `background-image: url(${aspectImage.url}); transform: rotate(${rotationAngle}deg); height: ${height}px; width: ${width}px;`; + const icon = L.divIcon({ + html: `
`, + className: 'tb-marker-div-icon', + iconSize: [width, height], + iconAnchor, + popupAnchor + }); + const iconInfo: MarkerIconInfo = { + size: [width, height], + icon + }; + return of(iconInfo); + } else { + return this.dataLayer.createDefaultMarkerIcon(rotationAngle); + } + }), + catchError(() => this.dataLayer.createDefaultMarkerIcon(rotationAngle)) + ); + } else { + return this.dataLayer.createDefaultMarkerIcon(rotationAngle); + } + } +} + +export class TbTripsDataLayer extends TbMapDataLayer { + + public tripMarkerIconProcessor: TripMarkerIconProcessor; + + public markerOffset: L.LatLngTuple; + public tooltipOffset: L.LatLngTuple; + + public pathStrokeColorProcessor: DataLayerColorProcessor; + public pointColorProcessor: DataLayerColorProcessor; + public pointTooltipProcessor: DataLayerPatternProcessor; + + private positionFunction: CompiledTbFunction; + + private rawTripsData: FormattedData[][]; + private latestTripsData: FormattedData[]; + + constructor(protected map: TbMap, + inputSettings: TripsDataLayerSettings) { + super(map, inputSettings); + } + + public dataLayerType(): MapDataLayerType { + return MapDataLayerType.trip; + } + + public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void { + throw new Error('Not implemented!'); + } + + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { + datasource.dataKeys = [this.settings.xKey, this.settings.yKey]; + if (this.settings.additionalDataKeys?.length) { + const tsKeys = this.settings.additionalDataKeys.filter(key => key.type === DataKeyType.timeseries); + const latestKeys = this.settings.additionalDataKeys.filter(key => key.type !== DataKeyType.timeseries); + datasource.dataKeys.push(...tsKeys); + if (latestKeys.length) { + datasource.latestDataKeys = latestKeys; + } + } + return datasource; + } + + protected defaultBaseSettings(map: TbMap): Partial { + return defaultBaseTripsDataLayerSettings(map.type()); + } + + protected doSetup(): Observable { + this.markerOffset = [ + isDefined(this.settings.markerOffsetX) ? this.settings.markerOffsetX : 0.5, + isDefined(this.settings.markerOffsetY) ? this.settings.markerOffsetY : 1, + ]; + this.tooltipOffset = [ + isDefined(this.settings.tooltip?.offsetX) ? this.settings.tooltip?.offsetX : 0, + isDefined(this.settings.tooltip?.offsetY) ? this.settings.tooltip?.offsetY : -1, + ]; + this.tripMarkerIconProcessor = TripMarkerIconProcessor.fromSettings(this, this.settings); + const setup$: Observable[] = []; + if (this.map.type() === MapType.image) { + setup$.push( + parseTbFunction(this.getCtx().http, this.settings.positionFunction, ['origXPos', 'origYPos', 'data', 'dsData', 'aspect']).pipe( + map((parsed) => { + this.positionFunction = parsed; + return null; + }) + ) + ); + } + setup$.push(this.tripMarkerIconProcessor.setup()); + + if (this.settings.showPath) { + this.pathStrokeColorProcessor = new DataLayerColorProcessor(this, this.settings.pathStrokeColor); + setup$.push(this.pathStrokeColorProcessor.setup()); + } + + if (this.settings.showPoints) { + this.pointColorProcessor = new DataLayerColorProcessor(this, this.settings.pointColor); + setup$.push(this.pointColorProcessor.setup()); + if (this.settings.pointTooltip?.show) { + this.pointTooltipProcessor = new DataLayerPatternProcessor(this, this.settings.pointTooltip); + setup$.push(this.pointTooltipProcessor.setup()); + } + } + return forkJoin(setup$).pipe(map(() => null)); + } + + protected isValidLayerData(_layerData: FormattedData): boolean { + throw new Error('Not implemented!'); + } + + protected createLayerItem(_data: FormattedData, _dsData: FormattedData[]): TbDataLayerItem { + throw new Error('Not implemented!'); + } + + public updateData(_dsData: FormattedData[]) { + throw new Error('Not implemented!'); + } + + public prepareTripsData(tripsData: FormattedData[][], tripsLatestData: FormattedData[]): {minTime: number; maxTime: number} { + let minTime = Infinity; + let maxTime = -Infinity; + this.rawTripsData = + tripsData.filter(d => d[0].$datasource.mapDataIds.includes(this.mapDataId)).map( + item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length); + this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); + this.rawTripsData.forEach((dataSource) => { + minTime = Math.min(dataSource[0].time, minTime); + maxTime = Math.max(dataSource[dataSource.length - 1].time, maxTime); + }); + + return {minTime, maxTime}; + } + + public updateTrips(minTime: number, maxTime: number) { + console.log(`Update trips: min(${minTime}), max(${maxTime})`); + } + + public updateTripsLatestData(tripsLatestData: FormattedData[]) { + this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); + } + + private clearIncorrectFirsLastDatapoint(dataSource: FormattedData[]): FormattedData[] { + const firstHistoricalDataIndexCoordinate = dataSource.findIndex(this.findFirstHistoricalDataIndexCoordinate); + if (firstHistoricalDataIndexCoordinate === -1) { + return []; + } + let lastIndex = dataSource.length - 1; + for (lastIndex; lastIndex > 0; lastIndex--) { + if (this.findFirstHistoricalDataIndexCoordinate(dataSource[lastIndex])) { + lastIndex++; + break; + } + } + if (firstHistoricalDataIndexCoordinate > 0 || lastIndex < dataSource.length) { + return dataSource.slice(firstHistoricalDataIndexCoordinate, lastIndex); + } + return dataSource; + } + + private findFirstHistoricalDataIndexCoordinate = (item: FormattedData): boolean => { + return isDefined(item[this.settings.xKey.label]) && isDefined(item[this.settings.yKey.label]); + } + + public createDefaultMarkerIcon(rotationAngle = 0): Observable { + const color = this.settings.markerShape?.color?.color || '#307FE5'; + return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color), rotationAngle); + } + + public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable { + return createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), shape, color).pipe( + map((iconUrl) => { + const style = `background-image: url(${iconUrl}); transform: rotate(${rotationAngle}deg); height: ${size}px; width: ${size}px;`; + return { + size: [size, size], + 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]] + }) + }; + }) + ); + } + + public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable { + return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color).pipe( + map((element) => { + 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]] + }) + }; + }) + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 1baeaffb87..ac8684e2bb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -15,7 +15,7 @@ */ @import '../../../../../../../scss/constants'; -.tb-map-layout { +.tb-map-container, .tb-map-layout { position: relative; display: flex; width: 100%; @@ -23,6 +23,13 @@ min-width: 0; min-height: 0; flex: 1; +} + +.tb-map-container { + flex-direction: column; +} + +.tb-map-layout { &.tb-sidebar-left { flex-direction: row-reverse; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index b3b2ebe343..968f911c1f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -28,7 +28,13 @@ import { TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; +import { + formattedDataArrayFromDatasourceData, + formattedDataFormDatasourceData, + isDefined, + isDefinedAndNotNull, + mergeDeepIgnoreArray +} from '@core/utils'; import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; @@ -61,6 +67,9 @@ import { DomSanitizer } from '@angular/platform-browser'; import tinycolor from 'tinycolor2'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; +import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component'; +import { ComponentRef } from '@angular/core'; +import { TbTripsDataLayer } from '@home/components/widget/lib/maps/data-layer/trips-data-layer'; type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; @@ -76,11 +85,13 @@ export abstract class TbMap { protected southWest = new L.LatLng(-L.Projection.SphericalMercator['MAX_LATITUDE'], -180); protected northEast = new L.LatLng(L.Projection.SphericalMercator['MAX_LATITUDE'], 180); - protected dataLayers: TbMapDataLayer[]; + protected dataLayers: TbMapDataLayer[]; + protected tripDataLayers: TbTripsDataLayer[]; protected dsData: FormattedData[]; protected selectedDataItem: TbDataLayerItem; + protected mapLayoutElement: HTMLElement; protected mapElement: HTMLElement; protected sidebar: L.TB.SidebarControl; @@ -93,6 +104,9 @@ export abstract class TbMap { protected addPolygonButton: L.TB.ToolbarButton; protected addCircleButton: L.TB.ToolbarButton; + protected timeLineComponentRef: ComponentRef; + protected timeLineComponent: MapTimelinePanelComponent; + protected addMarkerDataLayers: TbMapDataLayer[]; protected addPolygonDataLayers: TbMapDataLayer[]; protected addCircleDataLayers: TbMapDataLayer[]; @@ -114,9 +128,27 @@ export abstract class TbMap { this.settings = mergeDeepIgnoreArray({} as S, this.defaultSettings(), this.inputSettings as S); $(containerElement).empty(); - $(containerElement).addClass('tb-map-layout'); + $(containerElement).addClass('tb-map-container'); + const mapLayoutElement = $('
'); + this.mapLayoutElement = mapLayoutElement[0]; + $(containerElement).append(mapLayoutElement); + + if (this.settings.tripTimeline?.showTimelineControl) { + this.timeLineComponentRef = this.ctx.widgetContentContainer.createComponent(MapTimelinePanelComponent); + this.timeLineComponent = this.timeLineComponentRef.instance; + this.timeLineComponent.settings = this.settings.tripTimeline; + this.timeLineComponent.timeChanged.subscribe((time) => { + console.log(`Time updated: ${time}`); + }); + const parentElement = this.timeLineComponentRef.instance.element.nativeElement; + const content = parentElement.firstChild; + parentElement.removeChild(content); + parentElement.style.display = 'none'; + containerElement.append(content); + } + const mapElement = $('
'); - $(containerElement).append(mapElement); + mapLayoutElement.append(mapElement); this.mapResize$ = new ResizeObserver(() => { this.resize(); @@ -173,6 +205,7 @@ export abstract class TbMap { private setupDataLayers() { this.dataLayers = []; + this.tripDataLayers = []; if (this.settings.markers) { this.dataLayers.push(...this.settings.markers.map(settings => new TbMarkersDataLayer(this, settings))); } @@ -182,10 +215,14 @@ export abstract class TbMap { if (this.settings.circles) { this.dataLayers.push(...this.settings.circles.map(settings => new TbCirclesDataLayer(this, settings))); } - if (this.dataLayers.length) { + if (this.settings.trips) { + this.tripDataLayers.push(...this.settings.trips.map(settings => new TbTripsDataLayer(this, settings))); + } + if (this.dataLayers.length || this.tripDataLayers.length) { const groupsMap = new Map(); const customTranslate = this.ctx.$injector.get(CustomTranslatePipe); - this.dataLayers.forEach(dl => { + const allDataLayers = [...this.dataLayers, ...this.tripDataLayers]; + allDataLayers.forEach(dl => { dl.getGroups().forEach(group => { let groupData = groupsMap.get(group); if (!groupData) { @@ -217,7 +254,7 @@ export abstract class TbMap { }); } - const setup = this.dataLayers.map(dl => dl.setup()); + const setup = allDataLayers.map(dl => dl.setup()); forkJoin(setup).subscribe( () => { let datasources: TbMapDatasource[]; @@ -231,35 +268,77 @@ export abstract class TbMap { } const additionalDatasources = additionalMapDataSourcesToDatasources(this.settings.additionalDataSources); datasources = mergeMapDatasources(datasources, additionalDatasources); - const dataLayersSubscriptionOptions: WidgetSubscriptionOptions = { - datasources, - hasDataPageLink: true, - useDashboardTimewindow: false, - type: widgetType.latest, - callbacks: { - onDataUpdated: (subscription) => { - this.update(subscription); + if (datasources.length) { + const dataLayersSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + hasDataPageLink: true, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + this.update(subscription); + } } - } - }; - this.ctx.subscriptionApi.createSubscription(dataLayersSubscriptionOptions, false).subscribe( - (dataLayersSubscription) => { - let pageSize = this.settings.mapPageSize; - if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { - pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); + }; + this.ctx.subscriptionApi.createSubscription(dataLayersSubscriptionOptions, false).subscribe( + (dataLayersSubscription) => { + let pageSize = this.settings.mapPageSize; + if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { + pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); + } + const pageLink: EntityDataPageLink = { + page: 0, + pageSize, + textSearch: null, + dynamic: true + }; + dataLayersSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { + // this.map.resetState(); + }); + dataLayersSubscription.subscribeAllForPaginatedData(pageLink, null); } - const pageLink: EntityDataPageLink = { - page: 0, - pageSize, - textSearch: null, - dynamic: true - }; - dataLayersSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { - // this.map.resetState(); - }); - dataLayersSubscription.subscribeAllForPaginatedData(pageLink, null); + ); + } + if (this.tripDataLayers.length) { + const tripDatasources = this.tripDataLayers.map(dl => dl.getDatasource()); + const tripDataLayersSubscriptionOptions: WidgetSubscriptionOptions = { + datasources: tripDatasources, + hasDataPageLink: true, + useDashboardTimewindow: isDefined(this.ctx.widgetConfig.useDashboardTimewindow) + ? this.ctx.widgetConfig.useDashboardTimewindow : true, + type: widgetType.timeseries, + callbacks: { + onDataUpdated: (subscription) => { + this.updateTrips(subscription); + }, + onLatestDataUpdated: (subscription) => { + this.updateTripsWithLatestData(subscription); + } + } + }; + if (!tripDataLayersSubscriptionOptions.useDashboardTimewindow) { + tripDataLayersSubscriptionOptions.timeWindowConfig = this.ctx.widgetConfig.timewindow; } - ); + + this.ctx.subscriptionApi.createSubscription(tripDataLayersSubscriptionOptions, false).subscribe( + (tripDataLayersSubscription) => { + let pageSize = this.settings.mapPageSize; + if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { + pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); + } + const pageLink: EntityDataPageLink = { + page: 0, + pageSize, + textSearch: null, + dynamic: true + }; + tripDataLayersSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { + // this.map.resetState(); + }); + tripDataLayersSubscription.subscribeAllForPaginatedData(pageLink, null); + } + ); + } } ); } @@ -582,10 +661,51 @@ export abstract class TbMap { this.dsData = formattedDataFormDatasourceData(subscription.data, undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); this.dataLayers.forEach(dl => dl.updateData(this.dsData)); + this.updateTripsAppearance(); + this.updateTripsTimeline(); this.updateBounds(); this.updateAddButtonsStates(); } + private updateTrips(subscription: IWidgetSubscription) { + const tripsData = formattedDataArrayFromDatasourceData(subscription.data, el => el.datasource.entityId + el.datasource.mapDataIds[0]); + const tripsLatestData = formattedDataFormDatasourceData(subscription.latestData, + undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); + + let minTime = Infinity; + let maxTime = -Infinity; + for (const tripsDataLayer of this.tripDataLayers) { + const minMax = tripsDataLayer.prepareTripsData(tripsData, tripsLatestData); + minTime = Math.min(minMax.minTime, minTime); + maxTime = Math.max(minMax.maxTime, maxTime); + } + this.tripDataLayers.forEach(dl => dl.updateTrips(minTime, maxTime)); + this.updateTripsTimeline(minTime, maxTime); + this.updateBounds(); + } + + private updateTripsWithLatestData(subscription: IWidgetSubscription) { + const tripsLatestData = formattedDataFormDatasourceData(subscription.latestData, + undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); + this.tripDataLayers.forEach(dl => dl.updateTripsLatestData(tripsLatestData)); + this.updateTripsAppearance(); + this.updateTripsTimeline(); + } + + private updateTripsAppearance() {} + + private updateTripsTimeline(minTime?: number, maxTime?: number) { + if (this.settings.tripTimeline?.showTimelineControl) { + if (isDefinedAndNotNull(minTime) && isDefinedAndNotNull(maxTime)) { + this.timeLineComponent.min = minTime; + this.timeLineComponent.max = maxTime; + } + if (this.settings.tripTimeline.snapToRealLocation) { + // Recalculate anchors only for enabled layers + } + } + } + private resize() { this.onResize(); this.map?.invalidateSize(); @@ -659,7 +779,7 @@ export abstract class TbMap { protected getSidebar(): L.TB.SidebarControl { if (!this.sidebar) { this.sidebar = L.TB.sidebar({ - container: $(this.containerElement), + container: $(this.mapLayoutElement), position: this.settings.controlsPosition, paneWidth: 220 }).addTo(this.map); @@ -685,6 +805,7 @@ export abstract class TbMap { public enabledDataLayersUpdated() { this.updateAddButtonsStates(); + this.updateTripsTimeline(); } public dataItemClick($event: Event, action: WidgetAction, entityInfo: TbMapDatasource) { @@ -787,6 +908,9 @@ export abstract class TbMap { instance.destroy(); }) }); + if (this.timeLineComponentRef) { + this.timeLineComponentRef.destroy(); + } } public abstract locationDataToLatLng(position: {x: number; y: number}): L.LatLng; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 31428c682c..69c38bfbf7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -33,6 +33,7 @@ import { map } from 'rxjs/operators'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models'; import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models'; export enum MapType { geoMap = 'geoMap', @@ -368,6 +369,83 @@ export const defaultBaseMarkersDataLayerSettings = (mapType: MapType): Partial( + [ + [PathDecoratorSymbol.arrowHead, 'widgets.maps.data-layer.decorator-symbol-arrow-head'], + [PathDecoratorSymbol.dash, 'widgets.maps.data-layer.decorator-symbol-dash'] + ] +); + +export interface TripsDataLayerSettings extends MarkersDataLayerSettings { + rotateMarker: boolean; + offsetAngle: number; + showPath: boolean; + pathStrokeWeight?: number; + pathStrokeColor?: DataLayerColorSettings; + usePathDecorator?: boolean; + pathDecoratorSymbol?: PathDecoratorSymbol; + pathDecoratorSymbolSize?: number; + pathDecoratorSymbolColor?: string; + pathDecoratorOffset?: number; + pathEndDecoratorOffset?: number; + pathDecoratorRepeat?: number; + showPoints: boolean; + pointSize?: number; + pointColor?: DataLayerColorSettings; + pointTooltip?: DataLayerTooltipSettings; +} + +export const defaultBaseTripsDataLayerSettings = (mapType: MapType): Partial => mergeDeep( + defaultBaseMarkersDataLayerSettings(mapType), + { + tooltip: { + pattern: mapType === MapType.geoMap ? + '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}' + : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
End Time: ${maxTime}
Start Time: ${minTime}', + }, + rotateMarker: true, + offsetAngle: 0, + markerOffsetX: 0.5, + markerOffsetY: 0.5, + showPath: true, + pathStrokeWeight: 2, + pathStrokeColor: { + type: DataLayerColorType.constant, + color: '#307FE5', + }, + usePathDecorator: false, + pathDecoratorSymbol: PathDecoratorSymbol.arrowHead, + pathDecoratorSymbolSize: 10, + pathDecoratorSymbolColor: '#307FE5', + pathDecoratorOffset: 20, + pathEndDecoratorOffset: 20, + pathDecoratorRepeat: 20, + showPoints: false, + pointSize: 10, + pointColor: { + type: DataLayerColorType.constant, + color: '#307FE5', + }, + pointTooltip: { + show: true, + trigger: DataLayerTooltipTrigger.click, + autoclose: true, + type: DataLayerPatternType.pattern, + pattern: mapType === MapType.geoMap ? + '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}' + : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
End Time: ${maxTime}
Start Time: ${minTime}', + offsetX: 0, + offsetY: -1 + }, + } as TripsDataLayerSettings); + export interface ShapeDataLayerSettings extends MapDataLayerSettings { fillColor: DataLayerColorSettings; strokeColor: DataLayerColorSettings; @@ -525,8 +603,19 @@ export const mapZoomActionTranslationMap = new Map( ] ); +export interface TripTimelineSettings { + showTimelineControl: boolean; + timeStep: number; + speedOptions: number[]; + showTimestamp: boolean; + timestampFormat: DateFormatSettings; + snapToRealLocation: boolean; + locationSnapFilter: TbFunction; +} + export interface BaseMapSettings { mapType: MapType; + trips?: TripsDataLayerSettings[]; markers: MarkersDataLayerSettings[]; polygons: PolygonsDataLayerSettings[]; circles: CirclesDataLayerSettings[]; @@ -539,6 +628,7 @@ export interface BaseMapSettings { defaultZoomLevel: number; minZoomLevel: number; mapPageSize: number; + tripTimeline?: TripTimelineSettings; } export const DEFAULT_MAP_PAGE_SIZE = 16384; @@ -546,6 +636,7 @@ export const DEFAULT_ZOOM_LEVEL = 8; export const defaultBaseMapSettings: BaseMapSettings = { mapType: MapType.geoMap, + trips: [], markers: [], polygons: [], circles: [], @@ -557,7 +648,16 @@ export const defaultBaseMapSettings: BaseMapSettings = { defaultCenterPosition: '0,0', defaultZoomLevel: null, minZoomLevel: 16, - mapPageSize: DEFAULT_MAP_PAGE_SIZE + mapPageSize: DEFAULT_MAP_PAGE_SIZE, + tripTimeline: { + showTimelineControl: false, + timeStep: 1000, + speedOptions: [1,5,10,15,25], + showTimestamp: true, + timestampFormat: simpleDateFormat('yyyy-MM-dd HH:mm:ss'), + snapToRealLocation: false, + locationSnapFilter: 'return true;' + } }; export enum MapProvider { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html new file mode 100644 index 0000000000..543f07a3d7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html @@ -0,0 +1,26 @@ + +
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss new file mode 100644 index 0000000000..9e38394deb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.scss @@ -0,0 +1,19 @@ +/** + * 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. + */ +.tb-map-timeline-panel { + display: flex; + flex-direction: column; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts new file mode 100644 index 0000000000..33cac7c012 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts @@ -0,0 +1,68 @@ +/// +/// 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 { + Component, + ElementRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { TripTimelineSettings } from '@home/components/widget/lib/maps/models/map.models'; + +@Component({ + selector: 'tb-map-timeline-panel', + templateUrl: './map-timeline-panel.component.html', + styleUrls: ['./map-timeline-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MapTimelinePanelComponent implements OnInit, OnDestroy { + + @Input() + settings: TripTimelineSettings; + + @Input() + disabled = false; + + @Input() + min = 0; + + @Input() + max = 10000; + + @Output() + timeChanged = new EventEmitter(); + + currentTime = 0; + + constructor(public element: ElementRef) { + } + + ngOnInit() { + } + + ngOnDestroy() { + } + + public onTimeChange() { + //this.updateValueText(); + this.timeChanged.next(this.currentTime); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index e92a39b863..710af3478b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -79,6 +79,10 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid MapType = MapType; + @Input() + @coerceBoolean() + trip = false; + @Input() disabled: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 58a3144f9e..cdc829a96f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -93,6 +93,7 @@ import { MapWidgetComponent } from '@home/components/widget/lib/maps/map-widget. import { SelectMapEntityPanelComponent } from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component'; +import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component'; @NgModule({ declarations: [ @@ -149,6 +150,7 @@ import { NotificationTypeFilterPanelComponent, ScadaSymbolWidgetComponent, SelectMapEntityPanelComponent, + MapTimelinePanelComponent, MapWidgetComponent ], imports: [ diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 9943f1ec83..a3d924fe08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -278,6 +278,12 @@ + + + +
widget-config.data-settings
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index aece3d525f..dfe60066e0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7858,6 +7858,10 @@ "place-marker": "Place marker", "place-marker-hint": "Click to place '{{entityName}}' entity" }, + "path": { + "decorator-symbol-arrow-head": "Arrow", + "decorator-symbol-dash": "Dash" + }, "polygon": { "polygon-key": "Polygon key", "polygon-key-required": "Polygon key required",