Commit 3d52420d17b4fbc9ff7e24416ed9e13167756272
1 parent
3bc534ec
Fixed map functions. Update map helps.
Showing
15 changed files
with
126 additions
and
245 deletions
@@ -25,7 +25,7 @@ import { | @@ -25,7 +25,7 @@ import { | ||
25 | SubscriptionTimewindow | 25 | SubscriptionTimewindow |
26 | } from '@shared/models/time/time.models'; | 26 | } from '@shared/models/time/time.models'; |
27 | import { UtilsService } from '@core/services/utils.service'; | 27 | import { UtilsService } from '@core/services/utils.service'; |
28 | -import { deepClone, isNumeric } from '@core/utils'; | 28 | +import { deepClone, isNumber, isNumeric } from '@core/utils'; |
29 | import Timeout = NodeJS.Timeout; | 29 | import Timeout = NodeJS.Timeout; |
30 | 30 | ||
31 | export declare type onAggregatedData = (data: SubscriptionData, detectChanges: boolean) => void; | 31 | export declare type onAggregatedData = (data: SubscriptionData, detectChanges: boolean) => void; |
@@ -92,20 +92,36 @@ declare type AggFunction = (aggData: AggData, value?: any) => void; | @@ -92,20 +92,36 @@ declare type AggFunction = (aggData: AggData, value?: any) => void; | ||
92 | 92 | ||
93 | const avg: AggFunction = (aggData: AggData, value?: any) => { | 93 | const avg: AggFunction = (aggData: AggData, value?: any) => { |
94 | aggData.count++; | 94 | aggData.count++; |
95 | - aggData.sum += value; | ||
96 | - aggData.aggValue = aggData.sum / aggData.count; | 95 | + if (isNumber(value)) { |
96 | + aggData.sum += value; | ||
97 | + aggData.aggValue = aggData.sum / aggData.count; | ||
98 | + } else { | ||
99 | + aggData.aggValue = value; | ||
100 | + } | ||
97 | }; | 101 | }; |
98 | 102 | ||
99 | const min: AggFunction = (aggData: AggData, value?: any) => { | 103 | const min: AggFunction = (aggData: AggData, value?: any) => { |
100 | - aggData.aggValue = Math.min(aggData.aggValue, value); | 104 | + if (isNumber(value)) { |
105 | + aggData.aggValue = Math.min(aggData.aggValue, value); | ||
106 | + } else { | ||
107 | + aggData.aggValue = value; | ||
108 | + } | ||
101 | }; | 109 | }; |
102 | 110 | ||
103 | const max: AggFunction = (aggData: AggData, value?: any) => { | 111 | const max: AggFunction = (aggData: AggData, value?: any) => { |
104 | - aggData.aggValue = Math.max(aggData.aggValue, value); | 112 | + if (isNumber(value)) { |
113 | + aggData.aggValue = Math.max(aggData.aggValue, value); | ||
114 | + } else { | ||
115 | + aggData.aggValue = value; | ||
116 | + } | ||
105 | }; | 117 | }; |
106 | 118 | ||
107 | const sum: AggFunction = (aggData: AggData, value?: any) => { | 119 | const sum: AggFunction = (aggData: AggData, value?: any) => { |
108 | - aggData.aggValue = aggData.aggValue + value; | 120 | + if (isNumber(value)) { |
121 | + aggData.aggValue = aggData.aggValue + value; | ||
122 | + } else { | ||
123 | + aggData.aggValue = value; | ||
124 | + } | ||
109 | }; | 125 | }; |
110 | 126 | ||
111 | const count: AggFunction = (aggData: AggData) => { | 127 | const count: AggFunction = (aggData: AggData) => { |
@@ -409,7 +425,7 @@ export class DataAggregator { | @@ -409,7 +425,7 @@ export class DataAggregator { | ||
409 | } | 425 | } |
410 | 426 | ||
411 | private convertValue(val: string): any { | 427 | private convertValue(val: string): any { |
412 | - if (!this.noAggregation || val && isNumeric(val) && Number(val).toString() === val) { | 428 | + if (val && isNumeric(val) && (!this.noAggregation || this.noAggregation && Number(val).toString() === val)) { |
413 | return Number(val); | 429 | return Number(val); |
414 | } | 430 | } |
415 | return val; | 431 | return val; |
@@ -16,7 +16,7 @@ | @@ -16,7 +16,7 @@ | ||
16 | 16 | ||
17 | import { FormattedData, MapProviders, ReplaceInfo } from '@home/components/widget/lib/maps/map-models'; | 17 | import { FormattedData, MapProviders, ReplaceInfo } from '@home/components/widget/lib/maps/map-models'; |
18 | import { | 18 | import { |
19 | - createLabelFromDatasource, deepClone, | 19 | + createLabelFromDatasource, |
20 | hashCode, | 20 | hashCode, |
21 | isDefined, | 21 | isDefined, |
22 | isDefinedAndNotNull, | 22 | isDefinedAndNotNull, |
@@ -127,7 +127,6 @@ export function aspectCache(imageUrl: string): Observable<number> { | @@ -127,7 +127,6 @@ export function aspectCache(imageUrl: string): Observable<number> { | ||
127 | 127 | ||
128 | export type TranslateFunc = (key: string, defaultTranslation?: string) => string; | 128 | export type TranslateFunc = (key: string, defaultTranslation?: string) => string; |
129 | 129 | ||
130 | -const varsRegex = /\${([^}]*)}/g; | ||
131 | const linkActionRegex = /<link-act name=['"]([^['"]*)['"]>([^<]*)<\/link-act>/g; | 130 | const linkActionRegex = /<link-act name=['"]([^['"]*)['"]>([^<]*)<\/link-act>/g; |
132 | const buttonActionRegex = /<button-act name=['"]([^['"]*)['"]>([^<]*)<\/button-act>/g; | 131 | const buttonActionRegex = /<button-act name=['"]([^['"]*)['"]>([^<]*)<\/button-act>/g; |
133 | 132 | ||
@@ -304,7 +303,8 @@ export const parseWithTranslation = { | @@ -304,7 +303,8 @@ export const parseWithTranslation = { | ||
304 | if (this.translateFn) { | 303 | if (this.translateFn) { |
305 | return this.translateFn(key, defaultTranslation); | 304 | return this.translateFn(key, defaultTranslation); |
306 | } else { | 305 | } else { |
307 | - throw console.error('Translate not assigned'); | 306 | + console.error('Translate not assigned'); |
307 | + throw Error('Translate not assigned'); | ||
308 | } | 308 | } |
309 | }, | 309 | }, |
310 | parseTemplate(template: string, data: object, forceTranslate = false): string { | 310 | parseTemplate(template: string, data: object, forceTranslate = false): string { |
@@ -334,7 +334,7 @@ export function parseData(input: DatasourceData[], dataIndex?: number): Formatte | @@ -334,7 +334,7 @@ export function parseData(input: DatasourceData[], dataIndex?: number): Formatte | ||
334 | if (!obj.hasOwnProperty(el.dataKey.label) || el.data[dataIndex][1] !== '') { | 334 | if (!obj.hasOwnProperty(el.dataKey.label) || el.data[dataIndex][1] !== '') { |
335 | obj[el.dataKey.label] = el.data[dataIndex][1]; | 335 | obj[el.dataKey.label] = el.data[dataIndex][1]; |
336 | obj[el.dataKey.label + '|ts'] = el.data[dataIndex][0]; | 336 | obj[el.dataKey.label + '|ts'] = el.data[dataIndex][0]; |
337 | - if (el.dataKey.label === 'type') { | 337 | + if (el.dataKey.label.toLowerCase() === 'type') { |
338 | obj.deviceType = el.data[dataIndex][1]; | 338 | obj.deviceType = el.data[dataIndex][1]; |
339 | } | 339 | } |
340 | } | 340 | } |
@@ -360,28 +360,35 @@ export function flatData(input: FormattedData[]): FormattedData { | @@ -360,28 +360,35 @@ export function flatData(input: FormattedData[]): FormattedData { | ||
360 | } | 360 | } |
361 | 361 | ||
362 | export function parseArray(input: DatasourceData[]): FormattedData[][] { | 362 | export function parseArray(input: DatasourceData[]): FormattedData[][] { |
363 | - return _(input).groupBy(el => el?.datasource?.entityName) | ||
364 | - .values().value().map((entityArray) => | ||
365 | - entityArray[0].data.map((el, i) => { | ||
366 | - const obj: FormattedData = { | ||
367 | - entityName: entityArray[0]?.datasource?.entityName, | ||
368 | - entityId: entityArray[0]?.datasource?.entityId, | ||
369 | - entityType: entityArray[0]?.datasource?.entityType, | ||
370 | - $datasource: entityArray[0]?.datasource, | ||
371 | - dsIndex: i, | ||
372 | - time: el[0], | ||
373 | - deviceType: null | ||
374 | - }; | ||
375 | - entityArray.filter(e => e.data.length && e.data[i]).forEach(entity => { | ||
376 | - obj[entity?.dataKey?.label] = entity?.data[i][1]; | ||
377 | - obj[entity?.dataKey?.label + '|ts'] = entity?.data[0][0]; | ||
378 | - if (entity?.dataKey?.label === 'type') { | ||
379 | - obj.deviceType = entity?.data[0][1]; | 363 | + return _(input).groupBy(el => el.datasource.entityName) |
364 | + .values().value().map((entityArray, dsIndex) => { | ||
365 | + const timeDataMap: {[time: number]: FormattedData} = {}; | ||
366 | + entityArray.filter(e => e.data.length).forEach(entity => { | ||
367 | + entity.data.forEach(tsData => { | ||
368 | + const time = tsData[0]; | ||
369 | + const value = tsData[1]; | ||
370 | + let data = timeDataMap[time]; | ||
371 | + if (!data) { | ||
372 | + data = { | ||
373 | + entityName: entity.datasource.entityName, | ||
374 | + entityId: entity.datasource.entityId, | ||
375 | + entityType: entity.datasource.entityType, | ||
376 | + $datasource: entity.datasource, | ||
377 | + dsIndex, | ||
378 | + time, | ||
379 | + deviceType: null | ||
380 | + }; | ||
381 | + timeDataMap[time] = data; | ||
382 | + } | ||
383 | + data[entity.dataKey.label] = value; | ||
384 | + data[entity.dataKey.label + '|ts'] = time; | ||
385 | + if (entity.dataKey.label.toLowerCase() === 'type') { | ||
386 | + data.deviceType = value; | ||
380 | } | 387 | } |
381 | }); | 388 | }); |
382 | - return obj; | ||
383 | - }) | ||
384 | - ); | 389 | + }); |
390 | + return _.values(timeDataMap); | ||
391 | + }); | ||
385 | } | 392 | } |
386 | 393 | ||
387 | export function parseFunction(source: any, params: string[] = ['def']): (...args: any[]) => any { | 394 | export function parseFunction(source: any, params: string[] = ['def']): (...args: any[]) => any { |
@@ -474,9 +474,10 @@ export default abstract class LeafletMap { | @@ -474,9 +474,10 @@ export default abstract class LeafletMap { | ||
474 | this.showPolygon = showPolygon; | 474 | this.showPolygon = showPolygon; |
475 | if (this.map) { | 475 | if (this.map) { |
476 | const data = this.ctx.data; | 476 | const data = this.ctx.data; |
477 | - const formattedData = parseData(this.ctx.data); | 477 | + const formattedData = parseData(data); |
478 | if (drawRoutes) { | 478 | if (drawRoutes) { |
479 | - this.updatePolylines(parseArray(data), false); | 479 | + const polyData = parseArray(data); |
480 | + this.updatePolylines(polyData, formattedData, false); | ||
480 | } | 481 | } |
481 | if (showPolygon) { | 482 | if (showPolygon) { |
482 | this.updatePolygons(formattedData, false); | 483 | this.updatePolygons(formattedData, false); |
@@ -633,7 +634,8 @@ export default abstract class LeafletMap { | @@ -633,7 +634,8 @@ export default abstract class LeafletMap { | ||
633 | return polygon; | 634 | return polygon; |
634 | } | 635 | } |
635 | 636 | ||
636 | - updatePoints(pointsData: FormattedData[][], getTooltip: (point: FormattedData) => string) { | 637 | + updatePoints(pointsData: FormattedData[][], |
638 | + getTooltip: (point: FormattedData, points: FormattedData[]) => string) { | ||
637 | if (pointsData.length) { | 639 | if (pointsData.length) { |
638 | if (this.points) { | 640 | if (this.points) { |
639 | this.map.removeLayer(this.points); | 641 | this.map.removeLayer(this.points); |
@@ -642,21 +644,25 @@ export default abstract class LeafletMap { | @@ -642,21 +644,25 @@ export default abstract class LeafletMap { | ||
642 | } | 644 | } |
643 | let pointColor = this.options.pointColor; | 645 | let pointColor = this.options.pointColor; |
644 | for (const pointsList of pointsData) { | 646 | for (const pointsList of pointsData) { |
645 | - pointsList.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { | ||
646 | - if (this.options.useColorPointFunction) { | ||
647 | - pointColor = safeExecute(this.options.colorPointFunction, [data, pointsData, data.dsIndex]); | ||
648 | - } | ||
649 | - const point = L.circleMarker(this.convertPosition(data), { | ||
650 | - color: pointColor, | ||
651 | - radius: this.options.pointSize | ||
652 | - }); | ||
653 | - if (!this.options.pointTooltipOnRightPanel) { | ||
654 | - point.on('click', () => getTooltip(data)); | ||
655 | - } else { | ||
656 | - createTooltip(point, this.options, data.$datasource, getTooltip(data)); | 647 | + for (let tsIndex = 0; tsIndex < pointsList.length; tsIndex++) { |
648 | + const pdata = pointsList[tsIndex]; | ||
649 | + if (!!this.convertPosition(pdata)) { | ||
650 | + const dsData = pointsData.map(ds => ds[tsIndex]); | ||
651 | + if (this.options.useColorPointFunction) { | ||
652 | + pointColor = safeExecute(this.options.colorPointFunction, [pdata, dsData, pdata.dsIndex]); | ||
653 | + } | ||
654 | + const point = L.circleMarker(this.convertPosition(pdata), { | ||
655 | + color: pointColor, | ||
656 | + radius: this.options.pointSize | ||
657 | + }); | ||
658 | + if (!this.options.pointTooltipOnRightPanel) { | ||
659 | + point.on('click', () => getTooltip(pdata, dsData)); | ||
660 | + } else { | ||
661 | + createTooltip(point, this.options, pdata.$datasource, getTooltip(pdata, dsData)); | ||
662 | + } | ||
663 | + this.points.addLayer(point); | ||
657 | } | 664 | } |
658 | - this.points.addLayer(point); | ||
659 | - }); | 665 | + } |
660 | } | 666 | } |
661 | if (pointsData.length) { | 667 | if (pointsData.length) { |
662 | this.map.addLayer(this.points); | 668 | this.map.addLayer(this.points); |
@@ -665,15 +671,15 @@ export default abstract class LeafletMap { | @@ -665,15 +671,15 @@ export default abstract class LeafletMap { | ||
665 | 671 | ||
666 | // Polyline | 672 | // Polyline |
667 | 673 | ||
668 | - updatePolylines(polyData: FormattedData[][], updateBounds = true, activePolyline?: FormattedData) { | 674 | + updatePolylines(polyData: FormattedData[][], dsData: FormattedData[], updateBounds = true) { |
669 | const keys: string[] = []; | 675 | const keys: string[] = []; |
670 | - polyData.forEach((dataSource: FormattedData[]) => { | ||
671 | - const data = activePolyline || dataSource[0]; | ||
672 | - if (dataSource.length && data.entityName === dataSource[0].entityName) { | 676 | + polyData.forEach((tsData: FormattedData[], index) => { |
677 | + const data = dsData[index]; | ||
678 | + if (tsData.length && data.entityName === tsData[0].entityName) { | ||
673 | if (this.polylines.get(data.entityName)) { | 679 | if (this.polylines.get(data.entityName)) { |
674 | - this.updatePolyline(data, dataSource, this.options, updateBounds); | 680 | + this.updatePolyline(data, tsData, dsData, this.options, updateBounds); |
675 | } else { | 681 | } else { |
676 | - this.createPolyline(data, dataSource, this.options, updateBounds); | 682 | + this.createPolyline(data, tsData, dsData, this.options, updateBounds); |
677 | } | 683 | } |
678 | keys.push(data.entityName); | 684 | keys.push(data.entityName); |
679 | } | 685 | } |
@@ -689,9 +695,9 @@ export default abstract class LeafletMap { | @@ -689,9 +695,9 @@ export default abstract class LeafletMap { | ||
689 | }); | 695 | }); |
690 | } | 696 | } |
691 | 697 | ||
692 | - createPolyline(data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings, updateBounds = true) { | 698 | + createPolyline(data: FormattedData, tsData: FormattedData[], dsData: FormattedData[], settings: PolylineSettings, updateBounds = true) { |
693 | const poly = new Polyline(this.map, | 699 | const poly = new Polyline(this.map, |
694 | - dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); | 700 | + tsData.map(el => this.convertPosition(el)).filter(el => !!el), data, dsData, settings); |
695 | if (updateBounds) { | 701 | if (updateBounds) { |
696 | const bounds = poly.leafletPoly.getBounds(); | 702 | const bounds = poly.leafletPoly.getBounds(); |
697 | this.fitBounds(bounds); | 703 | this.fitBounds(bounds); |
@@ -699,10 +705,10 @@ export default abstract class LeafletMap { | @@ -699,10 +705,10 @@ export default abstract class LeafletMap { | ||
699 | this.polylines.set(data.entityName, poly); | 705 | this.polylines.set(data.entityName, poly); |
700 | } | 706 | } |
701 | 707 | ||
702 | - updatePolyline(data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings, updateBounds = true) { | 708 | + updatePolyline(data: FormattedData, tsData: FormattedData[], dsData: FormattedData[], settings: PolylineSettings, updateBounds = true) { |
703 | const poly = this.polylines.get(data.entityName); | 709 | const poly = this.polylines.get(data.entityName); |
704 | const oldBounds = poly.leafletPoly.getBounds(); | 710 | const oldBounds = poly.leafletPoly.getBounds(); |
705 | - poly.updatePolyline(dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); | 711 | + poly.updatePolyline(tsData.map(el => this.convertPosition(el)).filter(el => !!el), data, dsData, settings); |
706 | const newBounds = poly.leafletPoly.getBounds(); | 712 | const newBounds = poly.leafletPoly.getBounds(); |
707 | if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { | 713 | if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { |
708 | this.fitBounds(newBounds); | 714 | this.fitBounds(newBounds); |
@@ -866,7 +866,7 @@ export const pathSchema = | @@ -866,7 +866,7 @@ export const pathSchema = | ||
866 | 'useColorFunction', | 866 | 'useColorFunction', |
867 | { | 867 | { |
868 | key: 'colorFunction', | 868 | key: 'colorFunction', |
869 | - helpId: 'widget/lib/map/trip_path_color_fn', | 869 | + helpId: 'widget/lib/map/path_color_fn', |
870 | type: 'javascript', | 870 | type: 'javascript', |
871 | condition: 'model.useColorFunction === true' | 871 | condition: 'model.useColorFunction === true' |
872 | }, | 872 | }, |
@@ -961,7 +961,7 @@ export const pointSchema = | @@ -961,7 +961,7 @@ export const pointSchema = | ||
961 | 'useColorPointFunction', | 961 | 'useColorPointFunction', |
962 | { | 962 | { |
963 | key: 'colorPointFunction', | 963 | key: 'colorPointFunction', |
964 | - helpId: 'widget/lib/map/trip_path_point_color_fn', | 964 | + helpId: 'widget/lib/map/path_point_color_fn', |
965 | type: 'javascript', | 965 | type: 'javascript', |
966 | condition: 'model.useColorPointFunction === true' | 966 | condition: 'model.useColorPointFunction === true' |
967 | }, | 967 | }, |
@@ -1150,7 +1150,7 @@ export const tripAnimationSchema = { | @@ -1150,7 +1150,7 @@ export const tripAnimationSchema = { | ||
1150 | { | 1150 | { |
1151 | key: 'labelFunction', | 1151 | key: 'labelFunction', |
1152 | type: 'javascript', | 1152 | type: 'javascript', |
1153 | - helpId: 'widget/lib/map/trip_label_fn', | 1153 | + helpId: 'widget/lib/map/label_fn', |
1154 | condition: 'model.showLabel === true && model.useLabelFunction === true' | 1154 | condition: 'model.showLabel === true && model.useLabelFunction === true' |
1155 | }, | 1155 | }, |
1156 | 'showTooltip', | 1156 | 'showTooltip', |
@@ -1184,7 +1184,7 @@ export const tripAnimationSchema = { | @@ -1184,7 +1184,7 @@ export const tripAnimationSchema = { | ||
1184 | { | 1184 | { |
1185 | key: 'tooltipFunction', | 1185 | key: 'tooltipFunction', |
1186 | type: 'javascript', | 1186 | type: 'javascript', |
1187 | - helpId: 'widget/lib/map/trip_tooltip_fn', | 1187 | + helpId: 'widget/lib/map/tooltip_fn', |
1188 | condition: 'model.showTooltip === true && model.useTooltipFunction === true' | 1188 | condition: 'model.showTooltip === true && model.useTooltipFunction === true' |
1189 | }, | 1189 | }, |
1190 | 'rotationAngle', | 1190 | 'rotationAngle', |
@@ -1201,7 +1201,7 @@ export const tripAnimationSchema = { | @@ -1201,7 +1201,7 @@ export const tripAnimationSchema = { | ||
1201 | { | 1201 | { |
1202 | key: 'markerImageFunction', | 1202 | key: 'markerImageFunction', |
1203 | type: 'javascript', | 1203 | type: 'javascript', |
1204 | - helpId: 'widget/lib/map/trip_marker_image_fn', | 1204 | + helpId: 'widget/lib/map/marker_image_fn', |
1205 | condition: 'model.useMarkerImageFunction === true' | 1205 | condition: 'model.useMarkerImageFunction === true' |
1206 | }, | 1206 | }, |
1207 | { | 1207 | { |
@@ -16,9 +16,7 @@ | @@ -16,9 +16,7 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <div class="trip-animation-widget"> | 18 | <div class="trip-animation-widget"> |
19 | - <div class="trip-animation-label-container" *ngIf="settings.showLabel"> | ||
20 | - {{label}} | ||
21 | - </div> | 19 | + <div class="trip-animation-label-container" *ngIf="settings.showLabel" [innerHTML]="label"></div> |
22 | <div class="trip-animation-container" fxLayout="column"> | 20 | <div class="trip-animation-container" fxLayout="column"> |
23 | <div class="map" #map></div> | 21 | <div class="map" #map></div> |
24 | <div class="trip-animation-info-panel" fxLayout="row"> | 22 | <div class="trip-animation-info-panel" fxLayout="row"> |
@@ -30,7 +30,7 @@ import { | @@ -30,7 +30,7 @@ import { | ||
30 | import { FormattedData, MapProviders, TripAnimationSettings } from '@home/components/widget/lib/maps/map-models'; | 30 | import { FormattedData, MapProviders, TripAnimationSettings } from '@home/components/widget/lib/maps/map-models'; |
31 | import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils'; | 31 | import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils'; |
32 | import { mapPolygonSchema, pathSchema, pointSchema, tripAnimationSchema } from '@home/components/widget/lib/maps/schemes'; | 32 | import { mapPolygonSchema, pathSchema, pointSchema, tripAnimationSchema } from '@home/components/widget/lib/maps/schemes'; |
33 | -import { DomSanitizer } from '@angular/platform-browser'; | 33 | +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; |
34 | import { WidgetContext } from '@app/modules/home/models/widget-component.models'; | 34 | import { WidgetContext } from '@app/modules/home/models/widget-component.models'; |
35 | import { | 35 | import { |
36 | findAngle, getProviderSchema, | 36 | findAngle, getProviderSchema, |
@@ -70,13 +70,14 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | @@ -70,13 +70,14 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | ||
70 | mapWidget: MapWidgetInterface; | 70 | mapWidget: MapWidgetInterface; |
71 | historicalData: FormattedData[][]; | 71 | historicalData: FormattedData[][]; |
72 | normalizationStep: number; | 72 | normalizationStep: number; |
73 | - interpolatedTimeData = []; | 73 | + interpolatedTimeData: {[time: number]: FormattedData}[] = []; |
74 | + formattedInterpolatedTimeData: FormattedData[][] = []; | ||
74 | widgetConfig: WidgetConfig; | 75 | widgetConfig: WidgetConfig; |
75 | settings: TripAnimationSettings; | 76 | settings: TripAnimationSettings; |
76 | mainTooltips = []; | 77 | mainTooltips = []; |
77 | visibleTooltip = false; | 78 | visibleTooltip = false; |
78 | activeTrip: FormattedData; | 79 | activeTrip: FormattedData; |
79 | - label: string; | 80 | + label: SafeHtml; |
80 | minTime: number; | 81 | minTime: number; |
81 | maxTime: number; | 82 | maxTime: number; |
82 | anchors: number[] = []; | 83 | anchors: number[] = []; |
@@ -117,6 +118,8 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | @@ -117,6 +118,8 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | ||
117 | const subscription = this.ctx.defaultSubscription; | 118 | const subscription = this.ctx.defaultSubscription; |
118 | subscription.callbacks.onDataUpdated = () => { | 119 | subscription.callbacks.onDataUpdated = () => { |
119 | this.historicalData = parseArray(this.ctx.data).filter(arr => arr.length); | 120 | this.historicalData = parseArray(this.ctx.data).filter(arr => arr.length); |
121 | + this.interpolatedTimeData.length = 0; | ||
122 | + this.formattedInterpolatedTimeData.length = 0; | ||
120 | if (this.historicalData.length) { | 123 | if (this.historicalData.length) { |
121 | this.calculateIntervals(); | 124 | this.calculateIntervals(); |
122 | this.timeUpdated(this.minTime); | 125 | this.timeUpdated(this.minTime); |
@@ -146,6 +149,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | @@ -146,6 +149,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | ||
146 | 149 | ||
147 | timeUpdated(time: number) { | 150 | timeUpdated(time: number) { |
148 | this.currentTime = time; | 151 | this.currentTime = time; |
152 | + // get point for each datasource associated with time | ||
149 | const currentPosition = this.interpolatedTimeData | 153 | const currentPosition = this.interpolatedTimeData |
150 | .map(dataSource => dataSource[time]); | 154 | .map(dataSource => dataSource[time]); |
151 | for (let j = 0; j < this.interpolatedTimeData.length; j++) { | 155 | for (let j = 0; j < this.interpolatedTimeData.length; j++) { |
@@ -171,20 +175,20 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | @@ -171,20 +175,20 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | ||
171 | currentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time); | 175 | currentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time); |
172 | } | 176 | } |
173 | } | 177 | } |
174 | - this.calcLabel(); | 178 | + this.calcLabel(currentPosition); |
175 | this.calcMainTooltip(currentPosition); | 179 | this.calcMainTooltip(currentPosition); |
176 | if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) { | 180 | if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) { |
177 | - const formattedInterpolatedTimeData = this.interpolatedTimeData.map(ds => _.values(ds)); | ||
178 | - this.mapWidget.map.updatePolylines(formattedInterpolatedTimeData, true); | 181 | + this.mapWidget.map.updatePolylines(this.formattedInterpolatedTimeData, currentPosition, true); |
179 | if (this.settings.showPolygon) { | 182 | if (this.settings.showPolygon) { |
180 | - this.mapWidget.map.updatePolygons(this.interpolatedTimeData); | 183 | + this.mapWidget.map.updatePolygons(currentPosition); |
181 | } | 184 | } |
182 | if (this.settings.showPoints) { | 185 | if (this.settings.showPoints) { |
183 | - this.mapWidget.map.updatePoints(formattedInterpolatedTimeData.map(ds => _.union(ds)), this.calcTooltip); | 186 | + this.mapWidget.map.updatePoints(this.formattedInterpolatedTimeData, this.calcTooltip); |
184 | } | 187 | } |
185 | this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => { | 188 | this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => { |
186 | this.activeTrip = trip; | 189 | this.activeTrip = trip; |
187 | this.timeUpdated(this.currentTime); | 190 | this.timeUpdated(this.currentTime); |
191 | + this.cd.markForCheck(); | ||
188 | }); | 192 | }); |
189 | } | 193 | } |
190 | } | 194 | } |
@@ -215,41 +219,44 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | @@ -215,41 +219,44 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy | ||
215 | this.maxTime = dataSource[dataSource.length - 1]?.time || -Infinity; | 219 | this.maxTime = dataSource[dataSource.length - 1]?.time || -Infinity; |
216 | this.interpolatedTimeData[index] = this.interpolateArray(dataSource); | 220 | this.interpolatedTimeData[index] = this.interpolateArray(dataSource); |
217 | }); | 221 | }); |
222 | + this.formattedInterpolatedTimeData = this.interpolatedTimeData.map(ds => _.values(ds)); | ||
218 | if (!this.activeTrip) { | 223 | if (!this.activeTrip) { |
219 | this.activeTrip = this.interpolatedTimeData.map(dataSource => dataSource[this.minTime]).filter(ds => ds)[0]; | 224 | this.activeTrip = this.interpolatedTimeData.map(dataSource => dataSource[this.minTime]).filter(ds => ds)[0]; |
220 | } | 225 | } |
221 | if (this.useAnchors) { | 226 | if (this.useAnchors) { |
222 | const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]); | 227 | const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]); |
223 | this.anchors = anchorDate | 228 | this.anchors = anchorDate |
224 | - .filter((data: [string, FormattedData]) => safeExecute(this.settings.pointAsAnchorFunction, [data[1], anchorDate, data[1].dsIndex])) | 229 | + .filter((data: [string, FormattedData], tsIndex) => safeExecute(this.settings.pointAsAnchorFunction, [data[1], |
230 | + this.formattedInterpolatedTimeData.map(ds => ds[tsIndex]), data[1].dsIndex])) | ||
225 | .map(data => parseInt(data[0], 10)); | 231 | .map(data => parseInt(data[0], 10)); |
226 | } | 232 | } |
227 | } | 233 | } |
228 | 234 | ||
229 | - calcTooltip = (point: FormattedData): string => { | 235 | + calcTooltip = (point: FormattedData, points: FormattedData[]): string => { |
230 | const data = point ? point : this.activeTrip; | 236 | const data = point ? point : this.activeTrip; |
231 | const tooltipPattern: string = this.settings.useTooltipFunction ? | 237 | const tooltipPattern: string = this.settings.useTooltipFunction ? |
232 | - safeExecute(this.settings.tooltipFunction, [data, this.historicalData, point.dsIndex]) : this.settings.tooltipPattern; | 238 | + safeExecute(this.settings.tooltipFunction, |
239 | + [data, points, point.dsIndex]) : this.settings.tooltipPattern; | ||
233 | return parseWithTranslation.parseTemplate(tooltipPattern, data, true); | 240 | return parseWithTranslation.parseTemplate(tooltipPattern, data, true); |
234 | } | 241 | } |
235 | 242 | ||
236 | private calcMainTooltip(points: FormattedData[]): void { | 243 | private calcMainTooltip(points: FormattedData[]): void { |
237 | const tooltips = []; | 244 | const tooltips = []; |
238 | for (const point of points) { | 245 | for (const point of points) { |
239 | - tooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, this.calcTooltip(point))); | 246 | + tooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, this.calcTooltip(point, points))); |
240 | } | 247 | } |
241 | this.mainTooltips = tooltips; | 248 | this.mainTooltips = tooltips; |
242 | } | 249 | } |
243 | 250 | ||
244 | - calcLabel() { | ||
245 | - const data = this.activeTrip; | 251 | + calcLabel(points: FormattedData[]) { |
252 | + const data = points[this.activeTrip.dsIndex]; | ||
246 | const labelText: string = this.settings.useLabelFunction ? | 253 | const labelText: string = this.settings.useLabelFunction ? |
247 | - safeExecute(this.settings.labelFunction, [data, this.historicalData, data.dsIndex]) : this.settings.label; | ||
248 | - this.label = (parseWithTranslation.parseTemplate(labelText, data, true)); | 254 | + safeExecute(this.settings.labelFunction, [data, points, data.dsIndex]) : this.settings.label; |
255 | + this.label = this.sanitizer.bypassSecurityTrustHtml(parseWithTranslation.parseTemplate(labelText, data, true)); | ||
249 | } | 256 | } |
250 | 257 | ||
251 | - interpolateArray(originData: FormattedData[]) { | ||
252 | - const result = {}; | 258 | + interpolateArray(originData: FormattedData[]): {[time: number]: FormattedData} { |
259 | + const result: {[time: number]: FormattedData} = {}; | ||
253 | const latKeyName = this.settings.latKeyName; | 260 | const latKeyName = this.settings.latKeyName; |
254 | const lngKeyName = this.settings.lngKeyName; | 261 | const lngKeyName = this.settings.lngKeyName; |
255 | for (const data of originData) { | 262 | for (const data of originData) { |
1 | - <li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a></code> - A <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> object associated with marker.<br/> | 1 | + <li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a></code> - A <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> object associated with marker or data point of the route.<br/> |
2 | Represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration. | 2 | Represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration. |
3 | </li> | 3 | </li> |
4 | - <li><b>dsData:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData[]</a></code> - All available widget data (entities) as array of <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> objects<br/> | 4 | + <li><b>dsData:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData[]</a></code> - All available data associated with markers or routes data points as array of <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> objects<br/> |
5 | resolved from configured datasources. Each object represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/> | 5 | resolved from configured datasources. Each object represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/> |
6 | and provides access to other entity attributes/timeseries declared in widget datasource configuration. | 6 | and provides access to other entity attributes/timeseries declared in widget datasource configuration. |
7 | </li> | 7 | </li> |
8 | - <li><b>dsIndex</b> <code>number</code> - index of the current marker data in <code>dsData</code> array.<br> | 8 | + <li><b>dsIndex</b> <code>number</code> - index of the current marker data or route data point in <code>dsData</code> array.<br> |
9 | <strong>Note: </strong> The <code>data</code> argument is equivalent to <code>dsData[dsIndex]</code> expression. | 9 | <strong>Note: </strong> The <code>data</code> argument is equivalent to <code>dsData[dsIndex]</code> expression. |
10 | </li> | 10 | </li> |
ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md
renamed from
ui-ngx/src/assets/help/en_US/widget/lib/map/trip_path_color_fn.md
@@ -10,7 +10,7 @@ A JavaScript function used to compute color of the trip path. | @@ -10,7 +10,7 @@ A JavaScript function used to compute color of the trip path. | ||
10 | **Parameters:** | 10 | **Parameters:** |
11 | 11 | ||
12 | <ul> | 12 | <ul> |
13 | - {% include widget/lib/map/trip_fn_args %} | 13 | + {% include widget/lib/map/map_fn_args %} |
14 | </ul> | 14 | </ul> |
15 | 15 | ||
16 | **Returns:** | 16 | **Returns:** |
ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md
renamed from
ui-ngx/src/assets/help/en_US/widget/lib/map/trip_path_point_color_fn.md
@@ -10,7 +10,7 @@ A JavaScript function used to compute color of the trip path point. | @@ -10,7 +10,7 @@ A JavaScript function used to compute color of the trip path point. | ||
10 | **Parameters:** | 10 | **Parameters:** |
11 | 11 | ||
12 | <ul> | 12 | <ul> |
13 | - {% include widget/lib/map/trip_fn_args %} | 13 | + {% include widget/lib/map/map_fn_args %} |
14 | </ul> | 14 | </ul> |
15 | 15 | ||
16 | **Returns:** | 16 | **Returns:** |
@@ -5,7 +5,7 @@ | @@ -5,7 +5,7 @@ | ||
5 | 5 | ||
6 | *function (data, dsData, dsIndex): string* | 6 | *function (data, dsData, dsIndex): string* |
7 | 7 | ||
8 | -A JavaScript function used to compute text or HTML code to be displayed in the marker tooltip. | 8 | +A JavaScript function used to compute text or HTML code to be displayed in the marker, point or polygon tooltip. |
9 | 9 | ||
10 | **Parameters:** | 10 | **Parameters:** |
11 | 11 | ||
@@ -15,7 +15,7 @@ A JavaScript function used to compute text or HTML code to be displayed in the m | @@ -15,7 +15,7 @@ A JavaScript function used to compute text or HTML code to be displayed in the m | ||
15 | 15 | ||
16 | **Returns:** | 16 | **Returns:** |
17 | 17 | ||
18 | -Should return string value presenting text or HTML for the marker tooltip. | 18 | +Should return string value presenting text or HTML for the tooltip. |
19 | 19 | ||
20 | <div class="divider"></div> | 20 | <div class="divider"></div> |
21 | 21 |
ui-ngx/src/assets/help/en_US/widget/lib/map/trip_fn_args.md
deleted
100644 → 0
1 | - <li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a></code> - A <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> object associated with the point of the trip.<br/> | ||
2 | - Represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration. | ||
3 | - </li> | ||
4 | - <li><b>dsData:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData[][]</a></code> - two-dimensional array of <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> objects presenting<br/> | ||
5 | - array of trips (entities with timeseries data) resolved from configured datasources.<br/> | ||
6 | - Each element of array is particular entity trip data presented as array of <b>FormattedData</b> object (trip point).<br/> | ||
7 | - Each object represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/> | ||
8 | - and provides access to other entity attributes/timeseries declared in widget datasource configuration. | ||
9 | - </li> | ||
10 | - <li><b>dsIndex</b> <code>number</code> - index of the current trip data in <code>dsData</code> array. | ||
11 | - </li> |
ui-ngx/src/assets/help/en_US/widget/lib/map/trip_label_fn.md
deleted
100644 → 0
1 | -#### Trip animation widget label function | ||
2 | - | ||
3 | -<div class="divider"></div> | ||
4 | -<br/> | ||
5 | - | ||
6 | -*function (data, dsData, dsIndex): string* | ||
7 | - | ||
8 | -A JavaScript function used to compute text or HTML code of the marker label. | ||
9 | - | ||
10 | -**Parameters:** | ||
11 | - | ||
12 | -<ul> | ||
13 | - {% include widget/lib/map/trip_fn_args %} | ||
14 | -</ul> | ||
15 | - | ||
16 | -**Returns:** | ||
17 | - | ||
18 | -Should return string value presenting text or HTML of the marker label. | ||
19 | - | ||
20 | -<div class="divider"></div> | ||
21 | - | ||
22 | -##### Examples | ||
23 | - | ||
24 | -* Display styled label with corresponding latest telemetry data for `energy meter` or `thermometer` device types: | ||
25 | - | ||
26 | -```javascript | ||
27 | -var deviceType = data['Type']; | ||
28 | -if (typeof deviceType !== undefined) { | ||
29 | - if (deviceType == "energy meter") { | ||
30 | - return '<span style="color:orange;">${entityName}, ${energy:2} kWt</span>'; | ||
31 | - } else if (deviceType == "thermometer") { | ||
32 | - return '<span style="color:blue;">${entityName}, ${temperature:2} °C</span>'; | ||
33 | - } | ||
34 | -} | ||
35 | -return data.entityName; | ||
36 | -{:copy-code} | ||
37 | -``` | ||
38 | - | ||
39 | -<br> | ||
40 | -<br> |
ui-ngx/src/assets/help/en_US/widget/lib/map/trip_marker_image_fn.md
deleted
100644 → 0
1 | -#### Marker image function | ||
2 | - | ||
3 | -<div class="divider"></div> | ||
4 | -<br/> | ||
5 | - | ||
6 | -*function (data, images, dsData, dsIndex): {url: string, size: number}* | ||
7 | - | ||
8 | -A JavaScript function used to compute marker image. | ||
9 | - | ||
10 | -**Parameters:** | ||
11 | - | ||
12 | -<ul> | ||
13 | - {% include widget/lib/map/trip_fn_args %} | ||
14 | -</ul> | ||
15 | - | ||
16 | -**Returns:** | ||
17 | - | ||
18 | -Should return marker image data having the following structure: | ||
19 | - | ||
20 | -```typescript | ||
21 | -{ | ||
22 | - url: string, | ||
23 | - size: number | ||
24 | -} | ||
25 | -``` | ||
26 | - | ||
27 | -- *url* - marker image url; | ||
28 | -- *size* - marker image size; | ||
29 | - | ||
30 | -In case no data is returned, default marker image will be used. | ||
31 | - | ||
32 | -<div class="divider"></div> | ||
33 | - | ||
34 | -##### Examples | ||
35 | - | ||
36 | -<ul> | ||
37 | -<li> | ||
38 | -Calculate image url depending on <code>temperature</code> telemetry value for <code>thermometer</code> device type.<br> | ||
39 | -Let's assume 4 images are defined in <b>Marker images</b> section. Each image corresponds to particular temperature level: | ||
40 | -</li> | ||
41 | -</ul> | ||
42 | - | ||
43 | -```javascript | ||
44 | -var type = data['Type']; | ||
45 | -if (type == 'thermometer') { | ||
46 | - var res = { | ||
47 | - url: images[0], | ||
48 | - size: 40 | ||
49 | - } | ||
50 | - var temperature = data['temperature']; | ||
51 | - if (typeof temperature !== undefined) { | ||
52 | - var percent = (temperature + 60)/120; | ||
53 | - var index = Math.min(3, Math.floor(4 * percent)); | ||
54 | - res.url = images[index]; | ||
55 | - } | ||
56 | - return res; | ||
57 | -} | ||
58 | -{:copy-code} | ||
59 | -``` | ||
60 | - | ||
61 | -<br> | ||
62 | -<br> |
@@ -10,7 +10,7 @@ A JavaScript function evaluating whether to use trip point as time anchor used i | @@ -10,7 +10,7 @@ A JavaScript function evaluating whether to use trip point as time anchor used i | ||
10 | **Parameters:** | 10 | **Parameters:** |
11 | 11 | ||
12 | <ul> | 12 | <ul> |
13 | - {% include widget/lib/map/trip_fn_args %} | 13 | + {% include widget/lib/map/map_fn_args %} |
14 | </ul> | 14 | </ul> |
15 | 15 | ||
16 | **Returns:** | 16 | **Returns:** |
ui-ngx/src/assets/help/en_US/widget/lib/map/trip_tooltip_fn.md
deleted
100644 → 0
1 | -#### Trip animation widget tooltip function | ||
2 | - | ||
3 | -<div class="divider"></div> | ||
4 | -<br/> | ||
5 | - | ||
6 | -*function (data, dsData, dsIndex): string* | ||
7 | - | ||
8 | -A JavaScript function used to compute text or HTML code to be displayed in the marker tooltip. | ||
9 | - | ||
10 | -**Parameters:** | ||
11 | - | ||
12 | -<ul> | ||
13 | - {% include widget/lib/map/trip_fn_args %} | ||
14 | -</ul> | ||
15 | - | ||
16 | -**Returns:** | ||
17 | - | ||
18 | -Should return string value presenting text or HTML for the marker tooltip. | ||
19 | - | ||
20 | -<div class="divider"></div> | ||
21 | - | ||
22 | -##### Examples | ||
23 | - | ||
24 | -* Display details with corresponding telemetry data for `thermostat` device type: | ||
25 | - | ||
26 | -```javascript | ||
27 | -var deviceType = data['Type']; | ||
28 | -if (typeof deviceType !== undefined) { | ||
29 | - if (deviceType == "energy meter") { | ||
30 | - return '<b>${entityName}</b><br/><b>Energy:</b> ${energy:2} kWt<br/>'; | ||
31 | - } else if (deviceType == "thermometer") { | ||
32 | - return '<b>${entityName}</b><br/><b>Temperature:</b> ${temperature:2} °C<br/>'; | ||
33 | - } | ||
34 | -} | ||
35 | -return data.entityName; | ||
36 | -{:copy-code} | ||
37 | -``` | ||
38 | - | ||
39 | -<br> | ||
40 | -<br> |