Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into feature/entity-alarm-rules

pull/14291/head
Viacheslav Klimov 11 months ago
parent
commit
6804ff0397
  1. 2
      application/src/main/data/json/system/widget_types/photo_camera_input.json
  2. 4
      application/src/main/data/json/system/widget_types/timeseries_table.json
  3. 97
      ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch
  4. 2
      ui-ngx/src/app/core/guards/auth.guard.ts
  5. 4
      ui-ngx/src/app/core/services/mobile.service.ts
  6. 10
      ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss
  7. 4
      ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts
  8. 14
      ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html
  9. 26
      ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts
  10. 466
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts
  11. 27
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
  12. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
  13. 30
      ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts
  14. 39
      ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts
  15. 3
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss
  16. 330
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  17. 2
      ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html
  18. 105
      ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts
  19. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html
  20. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts
  21. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html
  22. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw
  23. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html
  24. 66
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts
  25. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html
  26. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts
  27. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html
  28. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts
  29. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html
  30. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts
  31. 84
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html
  32. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts
  33. 131
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  34. 46
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss
  35. 2
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  36. 44
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  37. 12
      ui-ngx/src/app/modules/home/pages/profile/profile.component.html
  38. 15
      ui-ngx/src/app/modules/home/pages/profile/profile.component.ts
  39. 2
      ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html
  40. 6
      ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts
  41. 35
      ui-ngx/src/app/modules/home/pages/user/user.component.html
  42. 10
      ui-ngx/src/app/modules/home/pages/user/user.component.ts
  43. 6
      ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts
  44. 2
      ui-ngx/src/app/shared/components/color-picker/hex-input.component.html
  45. 11
      ui-ngx/src/app/shared/models/widget.models.ts
  46. 97
      ui-ngx/src/app/shared/models/widget/maps/map.models.ts
  47. 1
      ui-ngx/src/app/shared/shared.module.ts
  48. 3
      ui-ngx/src/assets/dashboard/api_usage.json
  49. 2
      ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md
  50. 5
      ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md
  51. 4
      ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md
  52. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md
  53. 22
      ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md
  54. 24
      ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md
  55. 22
      ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md
  56. 50
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  57. 8
      ui-ngx/src/styles.scss

2
application/src/main/data/json/system/widget_types/photo_camera_input.json

@ -15,7 +15,7 @@
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-photo-camera-input-widget-settings", "settingsDirective": "tb-photo-camera-input-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"\",\"saveToGallery\":true,\"usePublicGalleryLink\":false,\"imageFormat\":\"image/png\",\"imageQuality\":0.92,\"maxWidth\":640,\"maxHeight\":480},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}"
}, },
"resources": [ "resources": [
{ {

4
application/src/main/data/json/system/widget_types/timeseries_table.json

@ -17,7 +17,7 @@
"latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings",
"hasBasicMode": true, "hasBasicMode": true,
"basicModeDirective": "tb-timeseries-table-basic-config", "basicModeDirective": "tb-timeseries-table-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"showTimestamp\":true,\"dateFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\"},\"displayPagination\":true,\"useEntityLabel\":false,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"hideEmptyLines\":false,\"disableStickyHeader\":false,\"useRowStyleFunction\":false,\"rowStyleFunction\":\"\",\"tabSortKey\":\"timestamp\"},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}"
}, },
"resources": [ "resources": [
{ {
@ -32,4 +32,4 @@
"public": true "public": true
} }
] ]
} }

97
ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch

File diff suppressed because one or more lines are too long

2
ui-ngx/src/app/core/guards/auth.guard.ts

@ -131,7 +131,7 @@ export class AuthGuard {
} }
} }
if (this.mobileService.isMobileApp() && !path.startsWith('dashboard.')) { if (this.mobileService.isMobileApp() && !path.startsWith('dashboard.')) {
this.mobileService.handleMobileNavigation(path, params); this.mobileService.handleMobileNavigation(path, params, lastChild.queryParams);
return of(false); return of(false);
} }
if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) { if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) {

4
ui-ngx/src/app/core/services/mobile.service.ts

@ -106,9 +106,9 @@ export class MobileService {
} }
} }
public handleMobileNavigation(path?: string, params?: Params) { public handleMobileNavigation(path?: string, params?: Params, queryParams?: Params) {
if (this.mobileApp) { if (this.mobileApp) {
this.mobileChannel.callHandler(navigationHandler, path, params); this.mobileChannel.callHandler(navigationHandler, path, params, queryParams);
} }
} }

10
ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss

@ -51,7 +51,7 @@
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.tb-latest-chart-shape { .tb-latest-chart-shape {
flex: 1; flex: 1 1 100%;
min-width: 10px; min-width: 10px;
min-height: 10px; min-height: 10px;
display: flex; display: flex;
@ -95,8 +95,14 @@
&.legend-right, &.legend-left { &.legend-right, &.legend-left {
gap: 24px; gap: 24px;
.tb-latest-chart-legend { .tb-latest-chart-legend {
flex: 0 1 auto;
overflow-y: auto;
max-height: 100%;
flex-wrap: nowrap;
min-width: fit-content;
max-width: 100%;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: stretch;
align-items: stretch; align-items: stretch;
.tb-latest-chart-legend-item { .tb-latest-chart-legend-item {
flex-direction: row; flex-direction: row;

4
ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts

@ -181,17 +181,13 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit {
private onResize() { private onResize() {
if (this.legendHorizontal) { if (this.legendHorizontal) {
this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', null);
this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', null); this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', null);
this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', null);
} }
const shapeWidth = this.chartShape.nativeElement.getBoundingClientRect().width; const shapeWidth = this.chartShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.chartShape.nativeElement.getBoundingClientRect().height; const shapeHeight = this.chartShape.nativeElement.getBoundingClientRect().height;
const size = Math.min(shapeWidth, shapeHeight); const size = Math.min(shapeWidth, shapeHeight);
if (this.legendHorizontal) { if (this.legendHorizontal) {
this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', `${size}px`);
this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', `${size}px`); this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', `${size}px`);
this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', '1');
} }
} }

14
ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html

@ -17,8 +17,16 @@
--> -->
<div class="mat-content mat-padding flex flex-col"> <div class="mat-content mat-padding flex flex-col">
<label class="tb-title" translate>entity.columns-to-display</label> <label class="tb-title" translate>entity.columns-to-display</label>
<mat-checkbox [(ngModel)]="column.display" *ngFor="let column of (columns | selectableColumns)" <mat-checkbox
(ngModelChange)="update()"> [checked]="allColumnsVisible"
{{ column.title }} [indeterminate]="someColumnsVisible"
(change)="toggleAllColumns($event)">
{{'entity.show-all-columns' | translate}}
</mat-checkbox> </mat-checkbox>
<mat-divider/>
@for (column of columns; track column.def) {
<mat-checkbox [(ngModel)]="column.display" (ngModelChange)="update()">
{{ column.title }}
</mat-checkbox>
}
</div> </div>

26
ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts

@ -15,6 +15,8 @@
/// ///
import { Component, Inject, InjectionToken } from '@angular/core'; import { Component, Inject, InjectionToken } from '@angular/core';
import { isDefinedAndNotNull } from '@core/utils';
import { SelectableColumnsPipe } from '@shared/public-api';
import { DisplayColumn } from '@home/components/widget/lib/table-widget.models'; import { DisplayColumn } from '@home/components/widget/lib/table-widget.models';
export const DISPLAY_COLUMNS_PANEL_DATA = new InjectionToken<any>('DisplayColumnsPanelData'); export const DISPLAY_COLUMNS_PANEL_DATA = new InjectionToken<any>('DisplayColumnsPanelData');
@ -33,8 +35,28 @@ export class DisplayColumnsPanelComponent {
columns: DisplayColumn[]; columns: DisplayColumn[];
constructor(@Inject(DISPLAY_COLUMNS_PANEL_DATA) public data: DisplayColumnsPanelData) { constructor(@Inject(DISPLAY_COLUMNS_PANEL_DATA) public data: DisplayColumnsPanelData,
this.columns = this.data.columns; private selectableColumnsPipe: SelectableColumnsPipe ) {
this.columns = this.selectableColumnsPipe.transform(this.data.columns);
}
get allColumnsVisible(): boolean {
return isDefinedAndNotNull(this.columns) && this.columns.every(column => column.display);
}
get someColumnsVisible(): boolean {
const filtredColumns = this.columns.filter(item => item.display);
return filtredColumns.length !== 0 && this.columns.length !== filtredColumns.length;
}
public toggleAllColumns(event: any): void {
const isChecked = event.checked;
this.columns.forEach(column => {
column.display = isChecked;
});
this.update();
} }
public update() { public update() {

466
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts

@ -0,0 +1,466 @@
///
/// 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 {
defaultBasePolylinesDataLayerSettings,
isCutPolygon,
isJSON,
MapDataLayerType,
PolylinesDataLayerSettings,
TbMapDatasource,
TbPolylineCoordinates,
TbPolylineData,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models';
import L from 'leaflet';
import { DataKey, FormattedData } from '@shared/models/widget.models';
import { ShapeStyleInfo, TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
import { isNotEmptyStr, isString } from '@core/utils';
import {
TbLatestDataLayerItem,
UnplacedMapDataItem
} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { map } from 'rxjs/operators';
class TbPolylineDataLayerItem extends TbLatestDataLayerItem<PolylinesDataLayerSettings, TbPolylineDataLayer> {
private polylineContainer: L.FeatureGroup;
private polyline: L.Polyline;
private polylineStyleInfo: ShapeStyleInfo;
private editing = false;
constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
protected settings: PolylinesDataLayerSettings,
protected dataLayer: TbPolylineDataLayer) {
super(data, dsData, settings, dataLayer);
}
public isEditing() {
return this.editing;
}
public updateBubblingMouseEvents() {
this.polyline.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
public remove() {
super.remove();
if (this.polylineStyleInfo?.patternId) {
this.dataLayer.getMap().unUseShapePattern(this.polylineStyleInfo.patternId);
}
}
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Layer {
const polyData = this.dataLayer.extractPolylineCoordinates(data);
const polyConstructor = L.polyline;
this.polyline = polyConstructor(polyData as (TbPolylineRawCoordinates & L.LatLngTuple[]), {
noClip: true,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => {
this.polylineStyleInfo = styleInfo;
if (this.polyline) {
this.polyline.setStyle(this.polylineStyleInfo.style);
}
});
this.polylineContainer = L.featureGroup();
this.polyline.addTo(this.polylineContainer);
this.updateLabel(data, dsData);
return this.polylineContainer;
}
protected unbindLabel() {
this.polylineContainer.unbindTooltip();
}
protected bindLabel(content: L.Content): void {
this.polylineContainer.bindTooltip(content, {className: 'tb-polyline-label', permanent: true, direction: 'center'})
.openTooltip(this.polylineContainer.getBounds().getCenter());
}
protected doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => {
this.polylineStyleInfo = styleInfo;
this.updatePolylineShape(data);
this.updateTooltip(data, dsData);
this.updateLabel(data, dsData);
if (!this.editing || !this.dataLayer.getMap().getMap().pm.globalCutModeEnabled()) {
this.polyline.setStyle(this.polylineStyleInfo.style);
}
});
}
protected doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, _dsData: FormattedData<TbMapDatasource>[]): void {
this.updatePolylineShape(data);
}
protected addItemClass(clazz: string): void {
if ((this.polyline as any)._path) {
L.DomUtil.addClass((this.polyline as any)._path, clazz);
}
}
protected removeItemClass(clazz: string): void {
if ((this.polyline as any)._path) {
L.DomUtil.removeClass((this.polyline as any)._path, clazz);
}
}
protected enableDrag(): void {
this.polyline.pm.setOptions({
snappable: this.dataLayer.isSnappable()
});
this.polyline.pm.enableLayerDrag();
this.polyline.on('pm:dragstart', () => {
this.editing = true;
});
this.polyline.on('pm:drag', () => {
if (this.tooltip?.isOpen()) {
this.tooltip.setLatLng(this.polyline.getBounds().getCenter());
}
});
this.polyline.on('pm:dragend', () => {
this.savePolylineCoordinates();
this.editing = false;
});
}
protected disableDrag(): void {
this.polyline.pm.disableLayerDrag();
this.polyline.off('pm:dragstart');
this.polyline.off('pm:dragend');
}
protected onSelected(): L.TB.ToolbarButtonOptions[] {
const buttons: L.TB.ToolbarButtonOptions[] = [];
if (this.dataLayer.isEditEnabled()) {
this.enablePolylineEditMode();
buttons.push(
{
id: 'cut',
title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.cut'),
iconClass: 'tb-cut',
click: (e, button) => {
const map = this.dataLayer.getMap().getMap();
if (!map.pm.globalCutModeEnabled()) {
this.disablePolylineRotateMode();
this.disablePolylineEditMode();
this.enablePolylineCutMode(button);
} else {
this.disablePolylineCutMode(button);
this.enablePolylineEditMode();
}
}
},
{
id: 'rotate',
title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.rotate'),
iconClass: 'tb-rotate',
click: (e, button) => {
if (!this.polyline.pm.rotateEnabled()) {
this.disablePolylineCutMode();
this.disablePolylineEditMode();
this.enablePolylineRotateMode(button);
} else {
this.disablePolylineRotateMode(button);
this.enablePolylineEditMode();
}
}
}
);
}
return buttons;
}
protected onDeselected(): void {
if (this.dataLayer.isEditEnabled()) {
this.disablePolylineEditMode();
this.disablePolylineRotateMode();
}
}
protected canDeselect(cancel = false): boolean {
const map = this.dataLayer.getMap().getMap();
if (map.pm.globalCutModeEnabled()) {
if (cancel) {
this.disablePolylineCutMode();
}
return false;
} else if (this.polyline.pm.rotateEnabled()) {
if (cancel) {
this.disablePolylineRotateMode();
}
return false;
} else if (this.editing) {
return false;
}
return true;
}
protected removeDataItemTitle(): string {
return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.polyline.remove-polyline-for', {entityName: this.data.entityName});
}
protected removeDataItem(): Observable<any> {
return this.dataLayer.savePolylineCoordinates(this.data, null);
}
private enablePolylineEditMode() {
this.polyline.on('pm:markerdragstart', () => this.editing = true);
this.polyline.on('pm:markerdragend', () => setTimeout(() => {
this.editing = false;
}));
this.polyline.on('pm:edit', () => this.savePolylineCoordinates());
this.polyline.pm.enable();
const map = this.dataLayer.getMap();
map.getEditToolbar().getButton('remove')?.setDisabled(false);
}
private disablePolylineEditMode() {
this.polyline.pm.disable();
this.polyline.off('pm:markerdragstart');
this.polyline.off('pm:markerdragend');
this.polyline.off('pm:edit');
const map = this.dataLayer.getMap();
map.getEditToolbar().getButton('remove')?.setDisabled(true);
}
private enablePolylineCutMode(cutButton?: L.TB.ToolbarButton) {
this.polylineContainer.closePopup();
this.editing = true;
this.polyline.options.bubblingMouseEvents = true;
this.polyline.setStyle({
...this.polylineStyleInfo.style, dashArray: '5 5', weight: 3,
color: '#3388ff', opacity: 1, fillColor: '#3388ff', fillOpacity: 0.2
});
this.addItemClass('tb-cut-mode');
this.polyline.once('pm:cut', (e) => {
if (e.layer instanceof L.Polyline) {
this.polyline = L.polyline(e.layer.getLatLngs() as L.LatLngExpression[] | L.LatLngExpression[][] , {
...this.polylineStyleInfo.style,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.polyline.addTo(this.polylineContainer);
} else if (e.layer instanceof L.LayerGroup) {
const parts: L.LatLngExpression[][] = [];
if (e.layer && typeof e.layer.getLayers === 'function') {
const segments: L.Polyline[] = e.layer.getLayers() as L.Polyline[];
segments.forEach(segment => {
parts.push(segment.getLatLngs() as L.LatLngExpression[]);
});
} else if (e.layer instanceof L.Polyline) {
parts.push(e.layer.getLatLngs() as L.LatLngExpression[]);
}
if (parts.length > 0) {
this.polyline = L.polyline(parts, {
...this.polylineStyleInfo.style,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.polyline.addTo(this.polylineContainer);
}
}
// @ts-ignore
e.layer._pmTempLayer = true;
e.layer.remove();
this.polylineContainer.removeLayer(this.polyline);
// @ts-ignore
this.polyline._pmTempLayer = false;
this.polyline.addTo(this.polylineContainer);
this.updateSelectedState();
cutButton?.setActive(false);
this.savePolylineCoordinates();
});
const map = this.dataLayer.getMap().getMap();
map.pm.setLang('en', {
tooltips: {
firstVertex: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-cut-hint'),
finishLine: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.finish-polyline-cut-hint')
}
}, 'en');
map.pm.enableGlobalCutMode({
// @ts-ignore
layersToCut: [this.polyline]
});
// @ts-ignore
L.DomUtil.addClass(map.pm.Draw.Cut._hintMarker.getTooltip()._container, 'tb-place-item-label');
cutButton?.setActive(true);
map.once('pm:globalcutmodetoggled', (e) => {
if (!e.enabled) {
this.disablePolylineCutMode(cutButton);
this.enablePolylineEditMode();
}
});
}
private disablePolylineCutMode(cutButton?: L.TB.ToolbarButton) {
this.editing = false;
this.polyline.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
this.polyline.setStyle({...this.polylineStyleInfo.style, dashArray: null});
this.removeItemClass('tb-cut-mode');
this.polyline.off('pm:cut');
const map = this.dataLayer.getMap().getMap();
map.pm.disableGlobalCutMode();
cutButton?.setActive(false);
}
private enablePolylineRotateMode(rotateButton?: L.TB.ToolbarButton) {
this.polylineContainer.closePopup();
this.editing = true;
this.polyline.on('pm:rotateend', () => {
this.savePolylineCoordinates();
});
this.polyline.pm.enableRotate();
rotateButton?.setActive(true);
this.polyline.on('pm:rotatedisable', () => {
this.disablePolylineRotateMode(rotateButton);
this.enablePolylineEditMode();
});
}
private disablePolylineRotateMode(rotateButton?: L.TB.ToolbarButton) {
this.editing = false;
this.polyline.pm.disableRotate();
this.polyline.off('pm:rotateend');
this.polyline.off('pm:rotatedisable');
rotateButton?.setActive(false);
}
private savePolylineCoordinates() {
let coordinates: TbPolylineCoordinates = this.polyline.getLatLngs();
if (coordinates.length === 1) {
coordinates = coordinates[0] as TbPolylineCoordinates;
}
if (this.polyline instanceof L.Rectangle && !isCutPolygon(coordinates)) {
const bounds = this.polyline.getBounds();
const boundsArray = [bounds.getNorthWest(), bounds.getNorthEast(), bounds.getSouthWest(), bounds.getSouthEast()];
if (coordinates.every(point => boundsArray.find(boundPoint => boundPoint.equals(point as L.LatLng)) !== undefined)) {
coordinates = [bounds.getNorthWest(), bounds.getSouthEast()];
}
}
this.dataLayer.savePolylineCoordinates(this.data, coordinates).subscribe();
}
private updatePolylineShape(data: FormattedData<TbMapDatasource>) {
if (this.editing) {
return;
}
const polyData = this.dataLayer.extractPolylineCoordinates(data) as TbPolylineData;
if (this.polyline instanceof L.Polyline && !(this.polyline instanceof L.Rectangle)) {
this.polyline.setLatLngs(polyData as L.LatLngExpression[] | L.LatLngExpression[][]);
} else if (this.polyline instanceof L.Rectangle && polyData.length === 2) {
const bounds = new L.LatLngBounds(polyData as L.LatLngTuple[]);
this.polyline.setBounds(bounds);
} else {
this.polylineContainer.removeLayer(this.polyline);
if (polyData.length === 2 && isCutPolygon(polyData)) {
this.polyline = L.polyline(polyData as L.LatLngExpression[] | L.LatLngExpression[][], {
...this.polylineStyleInfo.style,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode(),
noClip: true
});
}
this.polyline.addTo(this.polylineContainer);
this.editModeUpdated();
}
}
}
export class TbPolylineDataLayer extends TbShapesDataLayer<PolylinesDataLayerSettings, TbPolylineDataLayer> {
constructor(protected map: TbMap<any>,
inputSettings: PolylinesDataLayerSettings) {
super(map, inputSettings);
}
public dataLayerType(): MapDataLayerType {
return 'polylines';
}
public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void {
if (layer instanceof L.Polyline) {
let coordinates: TbPolylineCoordinates;
coordinates = layer.getLatLngs();
if (coordinates.length === 1) {
coordinates = coordinates[0] as TbPolylineCoordinates;
}
this.savePolylineCoordinates(item.entity, coordinates).subscribe(
(converted) => {
item.entity[this.settings.polylineKey.label] = JSON.stringify(converted);
this.createItemFromUnplaced(item);
}
);
} else {
console.warn('Unable to place item, layer is not a polyline.');
}
}
public extractPolylineCoordinates(data: FormattedData<TbMapDatasource>): TbPolylineRawCoordinates {
let rawPolyData = data[this.settings.polylineKey.label];
if (isString(rawPolyData)) {
rawPolyData = JSON.parse(rawPolyData);
}
return this.map.polylineDataToCoordinates(rawPolyData);
}
public savePolylineCoordinates(data: FormattedData<TbMapDatasource>, coordinates: TbPolylineCoordinates): Observable<TbPolylineRawCoordinates> {
const converted = coordinates ? this.map.coordinatesToPolylineData(coordinates) : null;
const polylineData = [
{
dataKey: this.settings.polylineKey,
value: converted
}
];
return this.map.saveItemData(data.$datasource, polylineData, this.settings.edit?.attributeScope).pipe(
map(() => converted)
);
}
protected getDataKeys(): DataKey[] {
return [this.settings.polylineKey];
}
protected defaultBaseSettings(map: TbMap<any>): Partial<PolylinesDataLayerSettings> {
return defaultBasePolylinesDataLayerSettings(map.type());
}
protected doSetup(): Observable<any> {
return super.doSetup();
}
protected isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean {
return layerData && ((isNotEmptyStr(layerData[this.settings.polylineKey.label]) && !isJSON(layerData[this.settings.polylineKey.label])
|| Array.isArray(layerData[this.settings.polylineKey.label])));
}
protected createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbPolylineDataLayerItem {
return new TbPolylineDataLayerItem(data, dsData, this.settings, this);
}
}

27
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts

@ -37,10 +37,12 @@ import { isDefinedAndNotNull, objectHashCode, parseTbFunction, safeExecuteTbFunc
import { CompiledTbFunction } from '@shared/models/js-function.models'; import { CompiledTbFunction } from '@shared/models/js-function.models';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
export type ShapePatternStorage = {[id: string]: { export type ShapePatternStorage = {
pattern: L.TB.Pattern; [id: string]: {
refCount: number; pattern: L.TB.Pattern;
}}; refCount: number;
}
};
interface ShapePatternInfo { interface ShapePatternInfo {
type: ShapeFillType; type: ShapeFillType;
@ -311,6 +313,23 @@ export abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L exte
super(map, inputSettings); super(map, inputSettings);
} }
public getStrokeStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], fillPatternId: string): Observable<ShapeStyleInfo> {
return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe(
map((patternWithId) => {
const stroke = this.strokeColorProcessor.processColor(data, dsData);
const style: L.PathOptions = {
color: stroke,
weight: this.settings.strokeWeight,
opacity: 1
};
return {
patternId: patternWithId.patternId,
style
}
})
);
}
public getShapeStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], fillPatternId: string): Observable<ShapeStyleInfo> { public getShapeStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], fillPatternId: string): Observable<ShapeStyleInfo> {
return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe( return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe(
map((patternWithId) => { map((patternWithId) => {

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts

@ -44,7 +44,7 @@ import { MarkerDataProcessor } from '@home/components/widget/lib/maps/data-layer
type TripRouteData = {[time: number]: FormattedData<TbMapDatasource>}; type TripRouteData = {[time: number]: FormattedData<TbMapDatasource>};
interface PointItem { export interface PointItem {
point: L.CircleMarker; point: L.CircleMarker;
tooltip?: L.Popup; tooltip?: L.Popup;
} }

30
ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts

@ -24,7 +24,11 @@ import {
TbPolygonCoordinate, TbPolygonCoordinate,
TbPolygonCoordinates, TbPolygonCoordinates,
TbPolygonRawCoordinate, TbPolygonRawCoordinate,
TbPolygonRawCoordinates TbPolygonRawCoordinates,
TbPolylineCoordinate,
TbPolylineCoordinates,
TbPolylineRawCoordinate,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common'; import { DeepPartial } from '@shared/models/common';
@ -179,5 +183,29 @@ export class TbGeoMap extends TbMap<GeoMapSettings> {
return circleData; return circleData;
} }
public polylineDataToCoordinates(expression: TbPolylineRawCoordinates): TbPolylineRawCoordinates {
return (expression).map((el: TbPolylineRawCoordinate) => {
if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) {
return el;
} else if (Array.isArray(el) && el.length) {
return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate;
} else {
return null;
}
}).filter(el => !!el);
}
public coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates {
if (coordinates.length) {
return coordinates.map((point: TbPolylineCoordinate) => {
if (Array.isArray(point)) {
return this.coordinatesToPolylineData(point) as TbPolylineRawCoordinate;
} else {
const convertPoint = latLngPointToBounds(point, this.southWest, this.northEast);
return [convertPoint.lat, convertPoint.lng];
}
});
}
return [];
}
} }

39
ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts

@ -23,7 +23,12 @@ import {
ImageSourceType, ImageSourceType,
loadImageWithAspect, loadImageWithAspect,
MapZoomAction, MapZoomAction,
TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates TbCircleData,
TbPolygonCoordinate,
TbPolygonCoordinates,
TbPolygonRawCoordinate,
TbPolygonRawCoordinates, TbPolylineCoordinate, TbPolylineCoordinates, TbPolylineRawCoordinate,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common'; import { DeepPartial } from '@shared/models/common';
@ -339,4 +344,36 @@ export class TbImageMap extends TbMap<ImageMapSettings> {
); );
} }
public polylineDataToCoordinates(expression: TbPolylineRawCoordinates): TbPolylineRawCoordinates{
return expression.map((el: TbPolylineRawCoordinate) => {
if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) {
const latLng = this.pointToLatLng(
el[0] * this.width,
el[1] * this.height
);
return [latLng.lat, latLng.lng] as TbPolylineRawCoordinate;
} else if (Array.isArray(el) && el.length) {
return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate;
}
else {
return null;
}
}).filter(el => !!el);
}
public coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates{
if (coordinates.length) {
return coordinates.map((point: TbPolylineCoordinate) => {
if (Array.isArray(point)) {
return this.coordinatesToPolylineData(point) as TbPolylineRawCoordinate;
} else {
const pos = this.latLngToPoint(point);
return [calculateNewPointCoordinate(pos.x, this.width), calculateNewPointCoordinate(pos.y, this.height)];
}
});
} else {
return [];
}
}
} }

3
ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss

@ -169,6 +169,9 @@ div.tb-widget .tb-widget-content.tb-no-interaction {
&.tb-draw-circle { &.tb-draw-circle {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 6.75H14.9775C14.025 4.0575 11.4825 2.25 8.625 2.25C6.83479 2.25 5.1179 2.96116 3.85203 4.22703C2.58616 5.4929 1.875 7.20979 1.875 9C1.875 12.75 4.8975 15.75 8.625 15.75C11.4825 15.75 14.025 13.95 15 11.25H16.5M15 8.25V9.75H13.5V8.25M13.365 11.25C12.495 13.08 10.65 14.25 8.625 14.25C5.73 14.25 3.375 11.9025 3.375 9C3.375 6.105 5.73 3.75 8.625 3.75C10.65 3.75 12.495 4.9275 13.3575 6.75H12V11.25"/></svg>'); mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 6.75H14.9775C14.025 4.0575 11.4825 2.25 8.625 2.25C6.83479 2.25 5.1179 2.96116 3.85203 4.22703C2.58616 5.4929 1.875 7.20979 1.875 9C1.875 12.75 4.8975 15.75 8.625 15.75C11.4825 15.75 14.025 13.95 15 11.25H16.5M15 8.25V9.75H13.5V8.25M13.365 11.25C12.495 13.08 10.65 14.25 8.625 14.25C5.73 14.25 3.375 11.9025 3.375 9C3.375 6.105 5.73 3.75 8.625 3.75C10.65 3.75 12.495 4.9275 13.3575 6.75H12V11.25"/></svg>');
} }
&.tb-draw-polyline {
mask-image: url('data:image/svg+xml,<svg height="18px" viewBox="0 -960 960 960" width="18px" xmlns="http://www.w3.org/2000/svg"><path d="M600-96v-86L317-336H144v-216h152l112-132v-180h216v216H472L360-516v121l240 131v-48h216v216H600ZM480-720h72v-72h-72v72ZM216-408h72v-72h-72v72Zm456 240h72v-72h-72v72ZM516-755ZM252-444Zm455 240Z"/></svg>');
}
&.tb-close { &.tb-close {
background: #D12730; background: #D12730;
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M14.25 4.8075L13.1925 3.75L9 7.9425L4.8075 3.75L3.75 4.8075L7.9425 9L3.75 13.1925L4.8075 14.25L9 10.0575L13.1925 14.25L14.25 13.1925L10.0575 9L14.25 4.8075Z"/></svg>'); mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M14.25 4.8075L13.1925 3.75L9 7.9425L4.8075 3.75L3.75 4.8075L7.9425 9L3.75 13.1925L4.8075 14.25L9 10.0575L13.1925 14.25L14.25 13.1925L10.0575 9L14.25 4.8075Z"/></svg>');

330
ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts

@ -27,7 +27,9 @@ import {
TbCircleData, TbCircleData,
TbMapDatasource, TbMapDatasource,
TbPolygonCoordinates, TbPolygonCoordinates,
TbPolygonRawCoordinates TbPolygonRawCoordinates,
TbPolylineCoordinates,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { import {
@ -80,11 +82,12 @@ import { TbTripsDataLayer } from '@home/components/widget/lib/maps/data-layer/tr
import { CompiledTbFunction } from '@shared/models/js-function.models'; import { CompiledTbFunction } from '@shared/models/js-function.models';
import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { ShapePatternStorage } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { TbPolylineDataLayer } from '@home/components/widget/lib/maps/data-layer/polylines-data-layer';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
import { ShapePatternStorage } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; type TooltipInstancesData = { root: HTMLElement, instances: ITooltipsterInstance[] };
export abstract class TbMap<S extends BaseMapSettings> { export abstract class TbMap<S extends BaseMapSettings> {
@ -124,6 +127,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected addRectangleButton: L.TB.ToolbarButton; protected addRectangleButton: L.TB.ToolbarButton;
protected addPolygonButton: L.TB.ToolbarButton; protected addPolygonButton: L.TB.ToolbarButton;
protected addCircleButton: L.TB.ToolbarButton; protected addCircleButton: L.TB.ToolbarButton;
protected addPolylineButton: L.TB.ToolbarButton;
protected timeLineComponentRef: ComponentRef<MapTimelinePanelComponent>; protected timeLineComponentRef: ComponentRef<MapTimelinePanelComponent>;
protected timeLineComponent: MapTimelinePanelComponent; protected timeLineComponent: MapTimelinePanelComponent;
@ -132,6 +136,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected addMarkerDataLayers: TbLatestMapDataLayer<any>[]; protected addMarkerDataLayers: TbLatestMapDataLayer<any>[];
protected addPolygonDataLayers: TbLatestMapDataLayer<any>[]; protected addPolygonDataLayers: TbLatestMapDataLayer<any>[];
protected addCircleDataLayers: TbLatestMapDataLayer<any>[]; protected addCircleDataLayers: TbLatestMapDataLayer<any>[];
protected addPolylineDataLayers: TbLatestMapDataLayer<any>[];
protected shapePatternStorage: ShapePatternStorage = {}; protected shapePatternStorage: ShapePatternStorage = {};
@ -274,6 +279,11 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.dataLayers.push(...circlesDataLayers); this.dataLayers.push(...circlesDataLayers);
this.latestDataLayers.push(...circlesDataLayers); this.latestDataLayers.push(...circlesDataLayers);
} }
if (this.settings.polylines) {
const polylinesDataLayers = this.settings.polylines.map(settings => new TbPolylineDataLayer(this, settings));
this.dataLayers.push(...polylinesDataLayers);
this.latestDataLayers.push(...polylinesDataLayers);
}
if (this.settings.trips) { if (this.settings.trips) {
const tripsDataLayers = this.settings.trips.map(settings => new TbTripsDataLayer(this, settings)); const tripsDataLayers = this.settings.trips.map(settings => new TbTripsDataLayer(this, settings));
this.dataLayers.push(...tripsDataLayers); this.dataLayers.push(...tripsDataLayers);
@ -319,7 +329,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
let datasources: TbMapDatasource[]; let datasources: TbMapDatasource[];
for (const layerType of mapDataLayerTypes) { for (const layerType of mapDataLayerTypes) {
const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType) const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType)
.map(dl => dl.getDataSources()).flat(); .map(dl => dl.getDataSources()).flat();
if (!datasources) { if (!datasources) {
datasources = typeDatasources; datasources = typeDatasources;
} else { } else {
@ -419,88 +429,100 @@ export abstract class TbMap<S extends BaseMapSettings> {
private setupEditMode() { private setupEditMode() {
this.editToolbar = L.TB.bottomToolbar({ this.editToolbar = L.TB.bottomToolbar({
mapElement: $(this.mapElement), mapElement: $(this.mapElement),
closeTitle: this.ctx.translate.instant('action.cancel'), closeTitle: this.ctx.translate.instant('action.cancel'),
onClose: () => { onClose: () => {
return this.deselectItem(true); return this.deselectItem(true);
} }
}); });
this.map.on('click', () => { this.map.on('click', () => {
this.deselectItem(); this.deselectItem();
}); });
if (this.latestDataLayers.some(dl => dl.isEditable())) { if (this.latestDataLayers.some(dl => dl.isEditable())) {
this.map.pm.setGlobalOptions({ snappable: false }); this.map.pm.setGlobalOptions({snappable: false});
this.map.pm.applyGlobalOptions(); this.map.pm.applyGlobalOptions();
} }
const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled()); const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled());
const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length; const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length;
const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled()); const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled());
if (showDragModeButton || addSupportedDataLayers.length) { if (showDragModeButton || addSupportedDataLayers.length) {
const drawToolbar = L.TB.toolbar({ const drawToolbar = L.TB.toolbar({
position: this.settings.controlsPosition position: this.settings.controlsPosition
}).addTo(this.map); }).addTo(this.map);
if (showDragModeButton) { if (showDragModeButton) {
this.dragModeButton = drawToolbar.toolbarButton({ this.dragModeButton = drawToolbar.toolbarButton({
id: 'dragMode', id: 'dragMode',
title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'), title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'),
iconClass: 'tb-drag-mode', iconClass: 'tb-drag-mode',
click: (e, button) => { click: (e, button) => {
this.toggleDragMode(e, button); this.toggleDragMode(e, button);
} }
}); });
} }
this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers'); this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers');
if (this.addMarkerDataLayers.length) { if (this.addMarkerDataLayers.length) {
this.addMarkerButton = drawToolbar.toolbarButton({ this.addMarkerButton = drawToolbar.toolbarButton({
id: 'addMarker', id: 'addMarker',
title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'), title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'),
iconClass: 'tb-place-marker', iconClass: 'tb-place-marker',
click: (e, button) => { click: (e, button) => {
this.placeMarker(e, button); this.placeMarker(e, button);
} }
}); });
this.addMarkerButton.setDisabled(true); this.addMarkerButton.setDisabled(true);
this.setPlaceMarkerStyle(); this.setPlaceMarkerStyle();
} }
this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons'); this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons');
if (this.addPolygonDataLayers.length) { if (this.addPolygonDataLayers.length) {
this.addRectangleButton = drawToolbar.toolbarButton({ this.addRectangleButton = drawToolbar.toolbarButton({
id: 'addRectangle', id: 'addRectangle',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'), title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'),
iconClass: 'tb-draw-rectangle', iconClass: 'tb-draw-rectangle',
click: (e, button) => { click: (e, button) => {
this.drawRectangle(e, button); this.drawRectangle(e, button);
} }
}); });
this.addRectangleButton.setDisabled(true); this.addRectangleButton.setDisabled(true);
this.addPolygonButton = drawToolbar.toolbarButton({ this.addPolygonButton = drawToolbar.toolbarButton({
id: 'addPolygon', id: 'addPolygon',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'), title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'),
iconClass: 'tb-draw-polygon', iconClass: 'tb-draw-polygon',
click: (e, button) => { click: (e, button) => {
this.drawPolygon(e, button); this.drawPolygon(e, button);
} }
}); });
this.addPolygonButton.setDisabled(true); this.addPolygonButton.setDisabled(true);
} }
this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles'); this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles');
if (this.addCircleDataLayers.length) { if (this.addCircleDataLayers.length) {
this.addCircleButton = drawToolbar.toolbarButton({ this.addCircleButton = drawToolbar.toolbarButton({
id: 'addCircle', id: 'addCircle',
title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'), title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'),
iconClass: 'tb-draw-circle', iconClass: 'tb-draw-circle',
click: (e, button) => { click: (e, button) => {
this.drawCircle(e, button); this.drawCircle(e, button);
} }
}); });
this.addCircleButton.setDisabled(true); this.addCircleButton.setDisabled(true);
} }
} this.addPolylineDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polylines');
if (this.addPolylineDataLayers.length) {
this.addPolylineButton = drawToolbar.toolbarButton({
id: 'addPolyline',
title: this.ctx.translate.instant('widgets.maps.data-layer.polyline.draw-polyline'),
iconClass: 'tb-draw-polyline',
click: (e, button) => {
this.drawPolyline(e, button);
}
});
this.addPolylineButton.setDisabled(true);
}
}
} }
private toggleDragMode(_e: MouseEvent, button: L.TB.ToolbarButton): void { private toggleDragMode(_e: MouseEvent, button: L.TB.ToolbarButton): void {
@ -557,6 +579,13 @@ export abstract class TbMap<S extends BaseMapSettings> {
})); }));
} }
private drawPolyline(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addPolylineDataLayers, (entity) => this.prepareDrawMode('Line', {
startPolyline: this.ctx.translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-hint-with-entity', {entityName: entity.entity.entityDisplayName}),
finishPolyline: this.ctx.translate.instant('widgets.maps.data-layer.polyline.finish-polyline-hint-with-entity', {entityName: entity.entity.entityDisplayName}),
}));
}
private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbLatestMapDataLayer[], private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbLatestMapDataLayer[],
prepareDrawMode: (entity: UnplacedMapDataItem) => void): void { prepareDrawMode: (entity: UnplacedMapDataItem) => void): void {
if (this.isPlacingItem) { if (this.isPlacingItem) {
@ -678,6 +707,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
case MapItemType.circle: case MapItemType.circle:
this.createCircle(actionData); this.createCircle(actionData);
break; break;
case MapItemType.polyline:
this.createPolyline(actionData);
break;
} }
} }
@ -725,6 +757,17 @@ export abstract class TbMap<S extends BaseMapSettings> {
})); }));
} }
private createPolyline(actionData: PlaceMapItemActionData): void {
this.createItem(actionData, () => this.prepareDrawMode('Line', {
startPolyline: actionData.action.mapItemTooltips.startPolyline
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startPolyline)
: this.ctx.translate.instant(mapItemTooltipsTranslation.startPolyline),
finishPolyline: actionData.action.mapItemTooltips.finishPolyline
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishPolyline)
: this.ctx.translate.instant(mapItemTooltipsTranslation.finishPolyline),
}));
}
private createItem(actionData: PlaceMapItemActionData, prepareDrawMode: () => void) { private createItem(actionData: PlaceMapItemActionData, prepareDrawMode: () => void) {
const actionId = 'id' in actionData.action ? actionData.action.id : 'map-button'; const actionId = 'id' in actionData.action ? actionData.action.id : 'map-button';
if (this.createMapItemActionId === actionId) { if (this.createMapItemActionId === actionId) {
@ -765,7 +808,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.createMapItemActionId = actionId; this.createMapItemActionId = actionId;
const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): {x: number; y: number} | TbPolygonRawCoordinates | TbCircleData => { const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): { x: number; y: number } | TbPolygonRawCoordinates | TbCircleData => {
switch (type) { switch (type) {
case MapItemType.marker: case MapItemType.marker:
if (layer instanceof L.Marker) { if (layer instanceof L.Marker) {
@ -792,6 +835,15 @@ export abstract class TbMap<S extends BaseMapSettings> {
return this.coordinatesToCircleData(layer.getLatLng(), layer.getRadius()); return this.coordinatesToCircleData(layer.getLatLng(), layer.getRadius());
} }
return null; return null;
case MapItemType.polyline:
if (layer instanceof L.Polyline) {
let coordinates: any = layer.getLatLngs();
if (coordinates.length === 1) {
coordinates = coordinates[0];
}
return this.coordinatesToPolylineData(coordinates);
}
return null;
} }
} }
} }
@ -813,8 +865,8 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.editToolbar.close(); this.editToolbar.close();
} }
private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle', tooltipsTranslation: Record<string, string>) { private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle' | 'Line', tooltipsTranslation: Record<string, string>) {
this.map.pm.setLang('en', { tooltips: tooltipsTranslation }, 'en'); this.map.pm.setLang('en', {tooltips: tooltipsTranslation}, 'en');
this.map.pm.enableDraw(shape); this.map.pm.enableDraw(shape);
// @ts-ignore // @ts-ignore
L.DomUtil.addClass(this.map.pm.Draw[shape]._hintMarker.getTooltip()._container, 'tb-place-item-label'); L.DomUtil.addClass(this.map.pm.Draw[shape]._hintMarker.getTooltip()._container, 'tb-place-item-label');
@ -848,46 +900,46 @@ export abstract class TbMap<S extends BaseMapSettings> {
tooltipData.instances = []; tooltipData.instances = [];
} }
$(root) $(root)
.find('a[role="button"]:not(.leaflet-pm-action)') .find('a[role="button"]:not(.leaflet-pm-action)')
.each((_index, element) => { .each((_index, element) => {
let title: string; let title: string;
if (element.title) { if (element.title) {
title = element.title; title = element.title;
$(element).removeAttr('title'); $(element).removeAttr('title');
} else if (element.parentElement.title) { } else if (element.parentElement.title) {
title = element.parentElement.title; title = element.parentElement.title;
$(element).parent().removeAttr('title'); $(element).parent().removeAttr('title');
}
const tooltip = $(element).tooltipster(
{
content: title,
theme: 'tooltipster-shadow',
delay: 10,
triggerClose: {
click: true,
tap: true,
scroll: true,
mouseleave: true
},
side,
distance: 2,
trackOrigin: true,
functionBefore: (_instance, helper) => {
if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) {
return false;
}
},
}
);
const instance = tooltip.tooltipster('instance');
tooltipData.instances.push(instance);
instance.on('destroyed', () => {
const index = tooltipData.instances.indexOf(instance);
if (index > -1) {
tooltipData.instances.splice(index, 1);
} }
const tooltip = $(element).tooltipster(
{
content: title,
theme: 'tooltipster-shadow',
delay: 10,
triggerClose: {
click: true,
tap: true,
scroll: true,
mouseleave: true
},
side,
distance: 2,
trackOrigin: true,
functionBefore: (_instance, helper) => {
if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) {
return false;
}
},
}
);
const instance = tooltip.tooltipster('instance');
tooltipData.instances.push(instance);
instance.on('destroyed', () => {
const index = tooltipData.instances.indexOf(instance);
if (index > -1) {
tooltipData.instances.splice(index, 1);
}
});
}); });
});
}); });
} }
@ -899,6 +951,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.updateTripsAnchors(); this.updateTripsAnchors();
this.updateBounds(); this.updateBounds();
this.updateEditButtonsStates(); this.updateEditButtonsStates();
} }
private updateTrips(subscription: IWidgetSubscription) { private updateTrips(subscription: IWidgetSubscription) {
@ -943,6 +996,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
private updateTripsAppearance() { private updateTripsAppearance() {
this.tripDataLayers.forEach(dl => dl.updateAppearance()); this.tripDataLayers.forEach(dl => dl.updateAppearance());
} }
private updateTripsTime() { private updateTripsTime() {
this.tripDataLayers.forEach(dl => dl.updateCurrentTime()); this.tripDataLayers.forEach(dl => dl.updateCurrentTime());
} }
@ -995,8 +1049,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
(!this.bounds || !this.bounds.isValid() || (!this.bounds.equals(bounds) || force) && this.settings.fitMapBounds) (!this.bounds || !this.bounds.isValid() || (!this.bounds.equals(bounds) || force) && this.settings.fitMapBounds)
&& !mapBounds.contains(bounds) && !mapBounds.contains(bounds)
) )
) ) {
{
this.bounds = bounds; this.bounds = bounds;
if (!this.ignoreUpdateBounds && !this.isPlacingItem) { if (!this.ignoreUpdateBounds && !this.isPlacingItem) {
this.fitBounds(bounds); this.fitBounds(bounds);
@ -1023,6 +1076,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.addCircleButton && this.addCircleButton !== this.currentEditButton) { if (this.addCircleButton && this.addCircleButton !== this.currentEditButton) {
this.addCircleButton.setDisabled(true); this.addCircleButton.setDisabled(true);
} }
if (this.addPolylineButton && this.addPolylineButton !== this.currentEditButton) {
this.addPolylineButton.setDisabled(true);
}
this.customActionsToolbar?.setDisabled(true); this.customActionsToolbar?.setDisabled(true);
} else { } else {
if (this.dragModeButton) { if (this.dragModeButton) {
@ -1040,6 +1096,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.addCircleButton) { if (this.addCircleButton) {
this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
} }
if (this.addPolylineButton) {
this.addPolylineButton.setDisabled(!this.addPolylineDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
this.customActionsToolbar?.setDisabled(false); this.customActionsToolbar?.setDisabled(false);
} }
} }
@ -1150,7 +1209,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
} }
const { entityId, entityName, entityLabel, entityType } = data.$datasource; const {entityId, entityName, entityLabel, entityType} = data.$datasource;
this.ctx.actionsApi.handleWidgetAction($event, action, { this.ctx.actionsApi.handleWidgetAction($event, action, {
entityType, entityType,
id: entityId id: entityId
@ -1219,7 +1278,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
} }
} }
public saveLocation(data: FormattedData<TbMapDatasource>, values: {[key: string]: any}): Observable<any> { public saveLocation(data: FormattedData<TbMapDatasource>, values: { [key: string]: any }): Observable<any> {
const datasource = data.$datasource; const datasource = data.$datasource;
let dataKeys = datasource.dataKeys; let dataKeys = datasource.dataKeys;
if (datasource.latestDataKeys) { if (datasource.latestDataKeys) {
@ -1329,18 +1388,21 @@ export abstract class TbMap<S extends BaseMapSettings> {
} }
} }
public abstract locationDataToLatLng(position: {x: number; y: number}): L.LatLng; public abstract locationDataToLatLng(position: { x: number; y: number }): L.LatLng;
public abstract latLngToLocationData(position: L.LatLng): {x: number; y: number}; public abstract latLngToLocationData(position: L.LatLng): { x: number; y: number };
public abstract polygonDataToCoordinates(coordinates: TbPolygonRawCoordinates): TbPolygonRawCoordinates; public abstract polygonDataToCoordinates(coordinates: TbPolygonRawCoordinates): TbPolygonRawCoordinates;
public abstract coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates; public abstract coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates;
public abstract polylineDataToCoordinates(coordinates: TbPolylineRawCoordinates): TbPolylineRawCoordinates;
public abstract coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates;
public abstract circleDataToCoordinates(circle: TbCircleData): TbCircleData; public abstract circleDataToCoordinates(circle: TbCircleData): TbCircleData;
public abstract coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData; public abstract coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData;
} }

2
ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html

@ -20,7 +20,7 @@
<div [class.!hidden]="isShowCamera" class="flex size-full flex-col items-center justify-between"> <div [class.!hidden]="isShowCamera" class="flex size-full flex-col items-center justify-between">
<div class="tb-web-camera__last-photo flex-1"> <div class="tb-web-camera__last-photo flex-1">
<span [class.!hidden]="lastPhoto" class="tb-web-camera__last-photo_text" translate>widgets.input-widgets.no-image</span> <span [class.!hidden]="lastPhoto" class="tb-web-camera__last-photo_text" translate>widgets.input-widgets.no-image</span>
<img [class.!hidden]="!lastPhoto" class="tb-web-camera__last-photo_img" [src]="lastPhoto" alt="last photo"/> <img [class.!hidden]="!lastPhoto" class="tb-web-camera__last-photo_img" [src]="lastPhoto | image | async" alt="last photo"/>
</div> </div>
<button mat-raised-button color="primary" (click)="takePhoto()" *ngIf="!textMessage"> <button mat-raised-button color="primary" (click)="takePhoto()" *ngIf="!textMessage">
{{ "widgets.input-widgets.take-photo" | translate }} {{ "widgets.input-widgets.take-photo" | translate }}

105
ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts

@ -19,29 +19,30 @@ import {
ElementRef, ElementRef,
Inject, Inject,
Input, Input,
NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewChild, ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { WidgetContext } from '@home/models/widget-component.models'; import { ImageService } from '@app/core/public-api';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay'; import { AttributeService } from '@core/http/attribute.service';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { AttributeService } from '@core/http/attribute.service'; import { isString } from '@core/utils';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { PageComponent } from '@shared/components/page.component';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable } from 'rxjs'; import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
import { isString } from '@core/utils'; import { map, Observable, of, switchMap } from 'rxjs';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
interface PhotoCameraInputWidgetSettings { interface PhotoCameraInputWidgetSettings {
widgetTitle: string; widgetTitle: string;
saveToGallery: boolean;
usePublicGalleryLink: boolean;
imageQuality: number; imageQuality: number;
imageFormat: string; imageFormat: string;
maxWidth: number; maxWidth: number;
@ -59,9 +60,7 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
constructor(@Inject(WINDOW) private window: Window, constructor(@Inject(WINDOW) private window: Window,
protected store: Store<AppState>, protected store: Store<AppState>,
private elementRef: ElementRef, private imageService: ImageService,
private ngZone: NgZone,
private overlay: Overlay,
private utils: UtilsService, private utils: UtilsService,
private attributeService: AttributeService, private attributeService: AttributeService,
private sanitizer: DomSanitizer private sanitizer: DomSanitizer
@ -119,6 +118,9 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
lastPhoto: SafeUrl; lastPhoto: SafeUrl;
datasourceDetected = false; datasourceDetected = false;
private mimeType: string;
private quality: number;
private static hasGetUserMedia(): boolean { private static hasGetUserMedia(): boolean {
return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia); return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia);
} }
@ -160,6 +162,9 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
this.dataKeyDetected = true; this.dataKeyDetected = true;
} }
} }
this.mimeType = this.settings.imageFormat || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
this.quality = this.settings.imageQuality || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.detectAvailableDevices(); this.detectAvailableDevices();
} }
@ -169,8 +174,8 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
private updateWidgetData(data: Array<DatasourceData>) { private updateWidgetData(data: Array<DatasourceData>) {
const keyData = data[0].data; const keyData = data[0].data;
if (keyData?.length && isString(keyData[0][1]) && keyData[0][1].startsWith('data:image/')) { if (keyData?.length && isString(keyData[0][1])) {
this.lastPhoto = this.sanitizer.bypassSecurityTrustUrl(keyData[0][1]); this.lastPhoto = keyData[0][1].startsWith('data:image/') ? this.sanitizer.bypassSecurityTrustUrl(keyData[0][1]) : keyData[0][1];
} }
} }
@ -237,26 +242,34 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
savePhoto() { savePhoto() {
this.updatePhoto = true; this.updatePhoto = true;
let task: Observable<any>;
const entityId: EntityId = { const entityId: EntityId = {
entityType: this.datasource.entityType, entityType: this.datasource.entityType,
id: this.datasource.entityId id: this.datasource.entityId
}; };
const saveData = [{
key: this.datasource.dataKeys[0].name, const uploadImage$ = this.settings.saveToGallery ? this.saveImageToGallery() : of(this.previewPhoto);
value: this.previewPhoto
}]; uploadImage$.pipe(
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) { switchMap((image) => {
task = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData); const saveData = [{
} else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) { key: this.datasource.dataKeys[0].name,
task = this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData); value: image
} }];
task.subscribe(() => { if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
this.isPreviewPhoto = false; return this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData);
this.updatePhoto = false; } else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
this.closeCamera(); return this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData);
}, () => { }
this.updatePhoto = false; })
).subscribe({
next: () => {
this.isPreviewPhoto = false;
this.updatePhoto = false;
this.closeCamera();
},
error: () => {
this.updatePhoto = false;
}
}); });
} }
@ -271,12 +284,36 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
this.canvasElement.height = this.videoHeight; this.canvasElement.height = this.videoHeight;
this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight); this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight);
const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE; this.previewPhoto = this.canvasElement.toDataURL(this.mimeType, this.quality);
const quality: number = this.settings.imageQuality ? this.settings.imageQuality : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality);
this.isPreviewPhoto = true; this.isPreviewPhoto = true;
} }
private saveImageToGallery(): Observable<string> {
return new Observable<Blob>((observer) => {
this.canvasElement.toBlob(
(blob) => {
if (blob) {
observer.next(blob);
observer.complete();
} else {
observer.error('Failed to create image blob');
}
},
this.mimeType,
this.quality
);
}).pipe(
switchMap((blob) => {
const fileName = this.datasource.dataKeys[0].name;
const file = new File([blob], fileName, { type: this.mimeType });
return this.imageService.uploadImage(file, fileName);
}),
map((imageInfo) =>
this.settings.usePublicGalleryLink ? imageInfo.publicLink : imageInfo.link
)
);
}
private inititedVideoStream(deviceId?: string, init = false) { private inititedVideoStream(deviceId?: string, init = false) {
if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) { if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
const videoTrackConstraints = { const videoTrackConstraints = {

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html

@ -115,6 +115,16 @@
<mat-slide-toggle class="mat-slide" formControlName="useEntityLabel"> <mat-slide-toggle class="mat-slide" formControlName="useEntityLabel">
{{ 'widgets.table.use-entity-label-tab-name' | translate }} {{ 'widgets.table.use-entity-label-tab-name' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div class="tb-form-row justify-between">
<div class="fixed-title-width" translate>widgets.table.sort-by</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="tabSortKey" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="TabSortKey.TIMESTAMP">{{ 'widgets.table.sort-timestamp-option' | translate }}</mat-option>
<mat-option [value]="TabSortKey.NAME_ASC">{{ 'widgets.table.sort-asc' | translate }}</mat-option>
<mat-option [value]="TabSortKey.NAME_DESC">{{ 'widgets.table.sort-desc' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div> </div>
<div class="tb-form-panel tb-slide-toggle"> <div class="tb-form-panel tb-slide-toggle">
<div class="tb-form-panel-title" style="padding-bottom: 16px;" translate>widgets.table.rows</div> <div class="tb-form-panel-title" style="padding-bottom: 16px;" translate>widgets.table.rows</div>

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts

@ -20,6 +20,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models'; import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models';
import { TabSortKey } from '@app/modules/home/components/widget/lib/timeseries-table-widget.component'
@Component({ @Component({
selector: 'tb-timeseries-table-widget-settings', selector: 'tb-timeseries-table-widget-settings',
@ -28,6 +29,8 @@ import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widge
}) })
export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent { export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent {
TabSortKey = TabSortKey;
timeseriesTableWidgetSettingsForm: UntypedFormGroup; timeseriesTableWidgetSettingsForm: UntypedFormGroup;
pageStepSizeValues = []; pageStepSizeValues = [];
@ -58,12 +61,14 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
hideEmptyLines: false, hideEmptyLines: false,
disableStickyHeader: false, disableStickyHeader: false,
useRowStyleFunction: false, useRowStyleFunction: false,
rowStyleFunction: '' rowStyleFunction: '',
tabSortKey: TabSortKey.TIMESTAMP
}; };
} }
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
settings.pageStepIncrement = settings.pageStepIncrement ?? settings.defaultPageSize; settings.pageStepIncrement = settings.pageStepIncrement ?? settings.defaultPageSize;
settings.tabSortKey = settings.tabSortKey ?? TabSortKey.TIMESTAMP;
this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement); this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement);
return settings; return settings;
} }
@ -93,7 +98,8 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
hideEmptyLines: [settings.hideEmptyLines, []], hideEmptyLines: [settings.hideEmptyLines, []],
disableStickyHeader: [settings.disableStickyHeader, []], disableStickyHeader: [settings.disableStickyHeader, []],
useRowStyleFunction: [settings.useRowStyleFunction, []], useRowStyleFunction: [settings.useRowStyleFunction, []],
rowStyleFunction: [settings.rowStyleFunction, [Validators.required]] rowStyleFunction: [settings.rowStyleFunction, [Validators.required]],
tabSortKey: [settings.tabSortKey, []],
}); });
} }

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html

@ -76,6 +76,20 @@
</mat-form-field> </mat-form-field>
</div> </div>
} }
@case (MapItemType.polyline) {
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.start-draw-polyline</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.startPolyline | translate }}" formControlName="firstVertex">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.finish-draw-polyline</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.finishPolyline | translate }}" formControlName="continueLine">
</mat-form-field>
</div>
}
} }
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw

@ -79,7 +79,7 @@ function AddEntityDialogController(instance) {
const mapType = widgetContext.mapInstance.type(); const mapType = widgetContext.mapInstance.type();
attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x}); attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x});
attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y}); attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y});
} else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') { } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon' || mapItemType === 'Line' ) {
attributes.push({key: 'perimeter', value: additionalParams.coordinates}); attributes.push({key: 'perimeter', value: additionalParams.coordinates});
} else if (mapItemType === 'Circle') { } else if (mapItemType === 'Circle') {
attributes.push({key: 'circle', value: additionalParams.coordinates}); attributes.push({key: 'circle', value: additionalParams.coordinates});

26
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html

@ -163,6 +163,26 @@
(keyEdit)="editKey('circleKey')" (keyEdit)="editKey('circleKey')"
formControlName="circleKey"> formControlName="circleKey">
</tb-data-key-input> </tb-data-key-input>
<tb-data-key-input
*ngIf="dataLayerType === 'polylines'"
inlineField="false"
appearance="outline"
subscriptSizing="fixed"
label="{{ 'widgets.maps.data-layer.polyline.polyline-key' | translate }}"
required
requiredText="widgets.maps.data-layer.polyline.polyline-key-required"
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('polylineKey')"
formControlName="polylineKey">
</tb-data-key-input>
<tb-data-keys <tb-data-keys
formControlName="additionalDataKeys" formControlName="additionalDataKeys"
appearance="outline" appearance="outline"
@ -443,6 +463,8 @@
<tb-shape-fill-image-settings formControlName="fillImage"></tb-shape-fill-image-settings> <tb-shape-fill-image-settings formControlName="fillImage"></tb-shape-fill-image-settings>
</div> </div>
</div> </div>
</ng-container>
<ng-container *ngIf="['polygons', 'circles', 'polylines'].includes(dataLayerType)">
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.stroke</div> <div translate>widgets.maps.data-layer.shape.stroke</div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
@ -455,7 +477,9 @@
[dsType]="dataLayerFormGroup.get('dsType').value" [dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value" [dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/circle_stroke_color_fn' }}" formControlName="strokeColor"></tb-data-layer-color-settings> helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' :
(dataLayerType === 'circles' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/polyline_stroke_color_fn') }}"
formControlName="strokeColor"></tb-data-layer-color-settings>
</div> </div>
</div> </div>
</ng-container> </ng-container>

66
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts

@ -29,7 +29,7 @@ import {
MarkerType, MarkerType,
pathDecoratorSymbols, pathDecoratorSymbols,
pathDecoratorSymbolTranslationMap, pathDecoratorSymbolTranslationMap,
PolygonsDataLayerSettings, PolygonsDataLayerSettings, PolylinesDataLayerSettings,
ShapeDataLayerSettings, ShapeFillType, ShapeDataLayerSettings, ShapeFillType,
TripsDataLayerSettings, TripsDataLayerSettings,
updateDataKeyToNewDsType updateDataKeyToNewDsType
@ -113,6 +113,8 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
return 'widget/lib/map/polygon_label_fn'; return 'widget/lib/map/polygon_label_fn';
case 'circles': case 'circles':
return 'widget/lib/map/circle_label_fn'; return 'widget/lib/map/circle_label_fn';
case 'polylines':
return 'widget/lib/map/polyline_label_fn';
default: default:
return 'widget/lib/map/label_fn'; return 'widget/lib/map/label_fn';
} }
@ -128,6 +130,8 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
return 'widget/lib/map/polygon_tooltip_fn'; return 'widget/lib/map/polygon_tooltip_fn';
case 'circles': case 'circles':
return 'widget/lib/map/circle_tooltip_fn'; return 'widget/lib/map/circle_tooltip_fn';
case 'polylines':
return 'widget/lib/map/polyline_tooltip_fn';
default: default:
return 'widget/lib/map/tooltip_fn'; return 'widget/lib/map/tooltip_fn';
} }
@ -138,18 +142,21 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
const editEnabledActions: DataLayerEditAction[] = const editEnabledActions: DataLayerEditAction[] =
this.dataLayerFormGroup.get('edit').get('enabledActions').value; this.dataLayerFormGroup.get('edit').get('enabledActions').value;
if (editEnabledActions && editEnabledActions.length) { if (editEnabledActions && editEnabledActions.length) {
switch (this.dataLayerType) { switch (this.dataLayerType) {
case 'markers': case 'markers':
const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value; const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value;
const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value; const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value;
return (xKey?.type === DataKeyType.attribute || yKey?.type === DataKeyType.attribute); return (xKey?.type === DataKeyType.attribute || yKey?.type === DataKeyType.attribute);
case 'polygons': case 'polygons':
const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value; const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value;
return polygonKey?.type === DataKeyType.attribute; return polygonKey?.type === DataKeyType.attribute;
case 'circles': case 'circles':
const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value; const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value;
return circleKey?.type === DataKeyType.attribute; return circleKey?.type === DataKeyType.attribute;
} case 'polylines':
const polylineKey: DataKey = this.dataLayerFormGroup.get('polylineKey').value;
return polylineKey?.type === DataKeyType.attribute;
}
} else { } else {
return false; return false;
} }
@ -231,11 +238,11 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
this.dataLayerFormGroup.addControl('pointColor', this.fb.control(tripsDataLayer.pointColor, Validators.required)); this.dataLayerFormGroup.addControl('pointColor', this.fb.control(tripsDataLayer.pointColor, Validators.required));
this.dataLayerFormGroup.addControl('pointTooltip', this.fb.control(tripsDataLayer.pointTooltip)); this.dataLayerFormGroup.addControl('pointTooltip', this.fb.control(tripsDataLayer.pointTooltip));
merge(this.dataLayerFormGroup.get('showMarker').valueChanges, merge(this.dataLayerFormGroup.get('showMarker').valueChanges,
this.dataLayerFormGroup.get('markerType').valueChanges, this.dataLayerFormGroup.get('markerType').valueChanges,
this.dataLayerFormGroup.get('rotateMarker').valueChanges, this.dataLayerFormGroup.get('rotateMarker').valueChanges,
this.dataLayerFormGroup.get('showPath').valueChanges, this.dataLayerFormGroup.get('showPath').valueChanges,
this.dataLayerFormGroup.get('usePathDecorator').valueChanges, this.dataLayerFormGroup.get('usePathDecorator').valueChanges,
this.dataLayerFormGroup.get('showPoints').valueChanges).pipe( this.dataLayerFormGroup.get('showPoints').valueChanges).pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(() => ).subscribe(() =>
this.updateValidators() this.updateValidators()
@ -291,6 +298,15 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
this.updateValidators() this.updateValidators()
); );
break; break;
case 'polylines':
this.dataLayerEditActions = dataLayerEditActions;
const polylineShapeDataLayer = this.settings as PolylinesDataLayerSettings;
this.dataLayerFormGroup.addControl('strokeColor', this.fb.control(polylineShapeDataLayer.strokeColor, Validators.required));
this.dataLayerFormGroup.addControl('strokeWeight', this.fb.control(polylineShapeDataLayer.strokeWeight, [Validators.required, Validators.min(0)]));
this.dialogTitle = 'widgets.maps.data-layer.polyline.polyline-configuration';
this.dataLayerEditTitle = 'widgets.maps.data-layer.polyline.edit';
this.dataLayerFormGroup.addControl('polylineKey', this.fb.control(polylineShapeDataLayer.polylineKey, Validators.required));
break;
} }
this.dataLayerFormGroup.get('dsType').valueChanges.pipe( this.dataLayerFormGroup.get('dsType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
@ -325,6 +341,12 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
this.dataLayerFormGroup.get('circleKey').patchValue(circleKey, {emitEvent: false}); this.dataLayerFormGroup.get('circleKey').patchValue(circleKey, {emitEvent: false});
} }
break; break;
case 'polylines':
const polylineKey: DataKey = this.dataLayerFormGroup.get('polylineKey').value;
if (updateDataKeyToNewDsType(polylineKey, newDsType)) {
this.dataLayerFormGroup.get('polylineKey').patchValue(polylineKey, {emitEvent: false});
}
break;
} }
const additionalDataKeys: DataKey[] = this.dataLayerFormGroup.get('additionalDataKeys').value; const additionalDataKeys: DataKey[] = this.dataLayerFormGroup.get('additionalDataKeys').value;
if (additionalDataKeys?.length) { if (additionalDataKeys?.length) {
@ -470,7 +492,7 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
} }
} }
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') { editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey' | 'polylineKey') {
const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value; const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value;
this.context.editKey(targetDataKey, this.context.editKey(targetDataKey,
this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value, this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value,
@ -511,6 +533,12 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
dataKeys.push(circleKey); dataKeys.push(circleKey);
} }
break; break;
case 'polylines':
const polylineKey: DataKey = this.dataLayerFormGroup.get('polylineKey').value;
if (polylineKey) {
dataKeys.push(polylineKey);
}
break;
} }
const additionalKeys: DataKey[] = this.dataLayerFormGroup.get('additionalDataKeys').value; const additionalKeys: DataKey[] = this.dataLayerFormGroup.get('additionalDataKeys').value;
if (additionalKeys) { if (additionalKeys) {

18
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html

@ -116,6 +116,24 @@
(keyEdit)="editKey('circleKey')" (keyEdit)="editKey('circleKey')"
formControlName="circleKey"> formControlName="circleKey">
</tb-data-key-input> </tb-data-key-input>
<tb-data-key-input
class="tb-key-field"
*ngIf="dataLayerType === 'polylines'"
required
requiredText="widgets.maps.data-layer.polyline.polyline-key-required"
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('polylineKey')"
formControlName="polylineKey">
</tb-data-key-input>
<div class="tb-form-table-row-cell-buttons"> <div class="tb-form-table-row-cell-buttons">
<button type="button" <button type="button"
mat-icon-button mat-icon-button

24
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts

@ -40,7 +40,7 @@ import {
MapDataLayerType, MapDataLayerType,
MapType, MapType,
MarkersDataLayerSettings, MarkersDataLayerSettings,
PolygonsDataLayerSettings, PolygonsDataLayerSettings, PolylinesDataLayerSettings,
TripsDataLayerSettings, TripsDataLayerSettings,
updateDataKeyToNewDsType updateDataKeyToNewDsType
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
@ -150,6 +150,11 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
this.removeDataLayerText = 'widgets.maps.data-layer.circle.remove-circle'; this.removeDataLayerText = 'widgets.maps.data-layer.circle.remove-circle';
this.dataLayerFormGroup.addControl('circleKey', this.fb.control(null, Validators.required)); this.dataLayerFormGroup.addControl('circleKey', this.fb.control(null, Validators.required));
break; break;
case 'polylines':
this.editDataLayerText = 'widgets.maps.data-layer.polyline.polyline-configuration';
this.removeDataLayerText = 'widgets.maps.data-layer.polyline.remove-polyline';
this.dataLayerFormGroup.addControl('polylineKey', this.fb.control(null, Validators.required));
break;
} }
this.dataLayerFormGroup.valueChanges.pipe( this.dataLayerFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
@ -225,12 +230,20 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
}, {emitEvent: false} }, {emitEvent: false}
); );
break; break;
case 'polylines':
const polylinesDataLayer = value as PolylinesDataLayerSettings;
this.dataLayerFormGroup.patchValue(
{
polylineKey: polylinesDataLayer?.polylineKey
}, {emitEvent: false}
);
break;
} }
this.updateValidators(); this.updateValidators();
this.cd.markForCheck(); this.cd.markForCheck();
} }
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') { editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey' | 'polylineKey') {
const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value; const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value;
this.context.editKey(targetDataKey, this.context.editKey(targetDataKey,
this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value, this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value,
@ -296,6 +309,13 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
updateModel = true; updateModel = true;
} }
break; break;
case 'polylines':
const polylineKey: DataKey = this.dataLayerFormGroup.get('polylineKey').value;
if (updateDataKeyToNewDsType(polylineKey, newDsType)) {
this.dataLayerFormGroup.get('polylineKey').patchValue(polylineKey, {emitEvent: false});
updateModel = true;
}
break;
} }
this.updateValidators(); this.updateValidators();
if (updateModel) { if (updateModel) {

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html

@ -27,6 +27,7 @@
</div> </div>
<div *ngIf="dataLayerType === 'polygons'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.polygon.polygon-key</div> <div *ngIf="dataLayerType === 'polygons'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.polygon.polygon-key</div>
<div *ngIf="dataLayerType === 'circles'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.circle.circle-key</div> <div *ngIf="dataLayerType === 'circles'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.circle.circle-key</div>
<div *ngIf="dataLayerType === 'polylines'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.polyline.polyline-key</div>
<div class="tb-form-table-header-cell tb-actions-header"></div> <div class="tb-form-table-header-cell tb-actions-header"></div>
</div> </div>
<div *ngIf="dataLayersFormArray().controls.length; else noDataLayers" class="tb-form-table-body"> <div *ngIf="dataLayersFormArray().controls.length; else noDataLayers" class="tb-form-table-body">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts

@ -104,6 +104,10 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val
this.addDataLayerText = 'widgets.maps.data-layer.circle.add-circle'; this.addDataLayerText = 'widgets.maps.data-layer.circle.add-circle';
this.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles'; this.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles';
break; break;
case 'polylines':
this.addDataLayerText = 'widgets.maps.data-layer.polyline.add-polylines';
this.noDataLayersText = 'widgets.maps.data-layer.polyline.no-polylines';
break;
} }
this.dataLayersFormGroup = this.fb.group({ this.dataLayersFormGroup = this.fb.group({
dataLayers: [this.fb.array([]), []] dataLayers: [this.fb.array([]), []]

7
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html

@ -45,6 +45,8 @@
<tb-toggle-option value="markers" [error]="mapSettingsFormGroup.get('markers').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.markers' | translate }}</tb-toggle-option> <tb-toggle-option value="markers" [error]="mapSettingsFormGroup.get('markers').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.markers' | translate }}</tb-toggle-option>
<tb-toggle-option value="polygons" [error]="mapSettingsFormGroup.get('polygons').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.polygons' | translate }}</tb-toggle-option> <tb-toggle-option value="polygons" [error]="mapSettingsFormGroup.get('polygons').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.polygons' | translate }}</tb-toggle-option>
<tb-toggle-option value="circles" [error]="mapSettingsFormGroup.get('circles').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.circles' | translate }}</tb-toggle-option> <tb-toggle-option value="circles" [error]="mapSettingsFormGroup.get('circles').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.circles' | translate }}</tb-toggle-option>
//todo translation
<tb-toggle-option value="polylines" [error]="mapSettingsFormGroup.get('polylines').invalid ? ('common.required-fields' | translate) : null"> Polylines </tb-toggle-option>
</tb-toggle-select> </tb-toggle-select>
</div> </div>
<tb-map-data-layers *ngIf="trip" <tb-map-data-layers *ngIf="trip"
@ -68,6 +70,11 @@
dataLayerType="circles" dataLayerType="circles"
[context]="context" [context]="context"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers> [mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
<tb-map-data-layers [class.!hidden]="dataLayerMode !== 'polylines'"
formControlName="polylines"
dataLayerType="polylines"
[context]="context"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'widgets.maps.data-layer.additional-datasources-hint' | translate }}"> <div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'widgets.maps.data-layer.additional-datasources-hint' | translate }}">

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts

@ -145,6 +145,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
markers: [null, []], markers: [null, []],
polygons: [null, []], polygons: [null, []],
circles: [null, []], circles: [null, []],
polylines: [null, []],
additionalDataSources: [null, []], additionalDataSources: [null, []],
controlsPosition: [null, []], controlsPosition: [null, []],
zoomActions: [null, []], zoomActions: [null, []],
@ -180,7 +181,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
}); });
merge(this.mapSettingsFormGroup.get('markers').valueChanges, merge(this.mapSettingsFormGroup.get('markers').valueChanges,
this.mapSettingsFormGroup.get('polygons').valueChanges, this.mapSettingsFormGroup.get('polygons').valueChanges,
this.mapSettingsFormGroup.get('circles').valueChanges this.mapSettingsFormGroup.get('circles').valueChanges,
this.mapSettingsFormGroup.get('polylines').valueChanges
).pipe( ).pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(() => { ).subscribe(() => {
@ -281,6 +283,10 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value; const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value;
dragModeButtonSettingsEnabled = polygons.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); dragModeButtonSettingsEnabled = polygons.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move));
} }
if (!dragModeButtonSettingsEnabled) {
const polylines: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polylines').value;
dragModeButtonSettingsEnabled = polylines.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move));
}
if (!dragModeButtonSettingsEnabled) { if (!dragModeButtonSettingsEnabled) {
const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value; const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value;
dragModeButtonSettingsEnabled = circles.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); dragModeButtonSettingsEnabled = circles.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move));

84
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html

@ -15,19 +15,32 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="photoCameraInputWidgetSettingsForm"> <section class="flex flex-col gap-4" [formGroup]="photoCameraInputWidgetSettingsForm">
<fieldset class="fields-group"> <div class="tb-form-panel">
<legend class="group-title" translate>widgets.input-widgets.general-settings</legend> <div class="tb-form-panel-title" translate>widgets.input-widgets.general-settings</div>
<mat-form-field class="mat-block flex-1"> <div class="tb-form-row space-between">
<mat-label translate>widgets.input-widgets.widget-title</mat-label> <div class="fixed-title-width" translate>widgets.input-widgets.widget-title</div>
<input matInput formControlName="widgetTitle"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
</mat-form-field> <input matInput formControlName="widgetTitle" placeholder="{{ 'widget-config.set' | translate }}">
</fieldset> </mat-form-field>
<fieldset class="fields-group"> </div>
<legend class="group-title" translate>widgets.input-widgets.image-settings</legend> </div>
<section class="flex flex-col gt-xs:flex-row gt-xs:items-center gt-xs:justify-start gt-xs:gap-2"> <div class="tb-form-panel">
<mat-form-field class="mat-block flex-1"> <div class="tb-form-panel-title" translate>widgets.input-widgets.save-image</div>
<mat-label translate>widgets.input-widgets.image-format</mat-label> <mat-slide-toggle class="mat-slide" formControlName="saveToGallery">
{{ 'widgets.input-widgets.save-to-gallery' | translate }}
</mat-slide-toggle>
@if (photoCameraInputWidgetSettingsForm.get('saveToGallery').value) {
<mat-slide-toggle class="mat-slide" formControlName="usePublicGalleryLink">
{{ 'widgets.input-widgets.public-image' | translate }}
</mat-slide-toggle>
}
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.input-widgets.image-settings</div>
<div class="tb-form-row space-between">
<div translate>widgets.input-widgets.image-format</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="imageFormat"> <mat-select formControlName="imageFormat">
<mat-option [value]="'image/jpeg'"> <mat-option [value]="'image/jpeg'">
{{ 'widgets.input-widgets.image-format-jpeg' | translate }} {{ 'widgets.input-widgets.image-format-jpeg' | translate }}
@ -40,20 +53,33 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block flex-1"> </div>
<mat-label translate>widgets.input-widgets.image-quality</mat-label>
<input matInput type="number" min="0" max="1" formControlName="imageQuality"> @if (photoCameraInputWidgetSettingsForm.get('imageFormat').value !== 'image/png') {
</mat-form-field> <div class="tb-form-row space-between">
</section> <div translate>widgets.input-widgets.image-quality</div>
<section class="flex flex-col gt-xs:flex-row gt-xs:items-center gt-xs:justify-start gt-xs:gap-2"> <mat-form-field class="number" appearance="outline" subscriptSizing="dynamic">
<mat-form-field class="mat-block flex-1"> <input matInput type="number" min="0" max="100" formControlName="imageQuality">
<mat-label translate>widgets.input-widgets.max-image-width</mat-label> <span matSuffix>%</span>
<input matInput type="number" min="1" formControlName="maxWidth"> </mat-form-field>
</mat-form-field> </div>
<mat-form-field class="mat-block flex-1"> }
<mat-label translate>widgets.input-widgets.max-image-height</mat-label>
<input matInput type="number" min="1" formControlName="maxHeight"> <div class="tb-form-row space-between column-xs">
</mat-form-field> <div class="fixed-title-width !min-w-30">Size</div>
</section> <div class="flex flex-row items-center justify-star gap-2">
</fieldset> <div class="tb-small-label" translate>widgets.input-widgets.max-image-width</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="1" formControlName="maxWidth">
<span matSuffix>px</span>
</mat-form-field>
<mat-divider vertical class="xs:hidden"></mat-divider>
<div class="tb-small-label" translate>widgets.input-widgets.max-image-height</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="1" formControlName="maxHeight">
<span matSuffix>px</span>
</mat-form-field>
</div>
</div>
</div>
</section> </section>

27
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts

@ -15,10 +15,10 @@
/// ///
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
@Component({ @Component({
selector: 'tb-photo-camera-input-widget-settings', selector: 'tb-photo-camera-input-widget-settings',
@ -42,6 +42,8 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo
return { return {
widgetTitle: '', widgetTitle: '',
saveToGallery: false,
usePublicGalleryLink: true,
imageFormat: 'image/png', imageFormat: 'image/png',
imageQuality: 0.92, imageQuality: 0.92,
maxWidth: 640, maxWidth: 640,
@ -57,11 +59,28 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo
widgetTitle: [settings.widgetTitle, []], widgetTitle: [settings.widgetTitle, []],
// Image settings // Image settings
saveToGallery: [settings.saveToGallery],
usePublicGalleryLink: [settings.usePublicGalleryLink],
imageFormat: [settings.imageFormat, []], imageFormat: [settings.imageFormat, []],
imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(1)]], imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(100)]],
maxWidth: [settings.maxWidth, [Validators.min(1)]], maxWidth: [settings.maxWidth, [Validators.min(1)]],
maxHeight: [settings.maxHeight, [Validators.min(1)]] maxHeight: [settings.maxHeight, [Validators.min(1)]]
}); });
} }
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
return {
...settings,
saveToGallery: settings.saveToGallery ?? false,
usePublicGalleryLink: settings.usePublicGalleryLink ?? false,
imageQuality: settings.imageQuality * 100
}
}
protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings {
return {
...settings,
imageQuality: settings.imageQuality / 100
}
}
} }

131
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -451,107 +451,36 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string {
const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString(); const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString();
const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
const cssString = const cssString = ` {
'.mat-mdc-input-element::placeholder {\n' + --mat-toolbar-container-text-color: ${mdDark};
' color: ' + mdDarkSecondary + ';\n' + --mat-tab-header-active-label-text-color: ${mdDark};
'}\n' + --mat-tab-header-inactive-label-text-color: ${mdDark};
'.mat-mdc-input-element::-moz-placeholder {\n' + --mat-tab-header-pagination-icon-color: ${mdDark};
' color: ' + mdDarkSecondary + ';\n' + --mat-tab-header-pagination-disabled-icon-color: ${mdDarkDisabled2};
'}\n' + --mat-table-header-headline-color: ${mdDarkSecondary};
'.mat-mdc-input-element::-webkit-input-placeholder {\n' + --mat-table-row-item-label-text-color: ${mdDark};
' color: ' + mdDarkSecondary + ';\n' + --mat-icon-color: ${mdDarkSecondary};
'}\n' + --mdc-icon-button-disabled-icon-color: ${mdDarkDisabled};
'.mat-mdc-input-element:-ms-input-placeholder {\n' + --mat-divider-color: ${mdDarkDivider};
' color: ' + mdDarkSecondary + ';\n' + --mat-paginator-container-text-color: ${mdDarkSecondary};
'}\n' + --mdc-icon-button-icon-color: ${mdDarkSecondary};
'mat-toolbar.mat-mdc-table-toolbar {\n' + --mat-paginator-enabled-icon-color: ${mdDarkSecondary};
'color: ' + mdDark + ';\n' + --mat-paginator-disabled-icon-color: ${mdDarkDisabled};
'}\n' + --mat-select-enabled-trigger-text-color: ${mdDarkSecondary};
'mat-toolbar.mat-mdc-table-toolbar:not([color="primary"]) button.mat-mdc-icon-button mat-icon {\n' + --mat-select-disabled-trigger-text-color: ${mdDarkDisabled};
'color: ' + mdDarkSecondary + ';\n' + --mat-table-row-item-outline-color: ${mdDarkDivider};
'}\n' + --mdc-checkbox-unselected-focus-icon-color: ${mdDarkSecondary};
'.mat-mdc-tab .mdc-tab__text-label {\n' +
'color: ' + mdDark + ';\n' + --tb-orig-background-color: ${origBackgroundColor};
'}\n' + --tb-current-entity-color: ${currentEntityColor};
'.mat-mdc-tab-header-pagination-chevron {\n' + --tb-current-entity-sticky-color: ${currentEntityStickyColor};
'border-color: ' + mdDark + ';\n' + --tb-hover-color: ${hoverColor};
'}\n' + --tb-hover-sticky-color: ${hoverStickyColor};
'.mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron {\n' + --tb-selected-color: ${selectedColor};
'border-color: ' + mdDarkDisabled2 + ';\n' + --tb-selected-sticky-color: ${selectedStickyColor};
'}\n' + }
'.mat-mdc-table .mat-mdc-header-row {\n' + `;
'background-color: ' + origBackgroundColor + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-header-cell {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-cell, .mat-mdc-table .mat-mdc-header-cell {\n' +
'border-bottom-color: ' + mdDarkDivider + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-cell .mat-mdc-checkbox ' +
'.mdc-checkbox__native-control:focus:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])'+
'~.mdc-checkbox__background, ' +
'.mat-table .mat-header-cell .mat-mdc-checkbox ' +
'.mdc-checkbox__native-control:focus:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])'+
'~.mdc-checkbox__background {\n' +
'border-color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-row .mat-mdc-cell.mat-mdc-table-sticky {\n' +
'transition: background-color .2s;\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-row.tb-current-entity {\n' +
'background-color: ' + currentEntityColor + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-row.tb-current-entity .mat-mdc-cell.mat-mdc-table-sticky {\n' +
'background-color: ' + currentEntityStickyColor + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-row:hover:not(.tb-current-entity) {\n' +
'background-color: ' + hoverColor + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-row:hover:not(.tb-current-entity) .mat-mdc-cell.mat-mdc-table-sticky {\n' +
'background-color: ' + hoverStickyColor + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-row.mat-row-select.mat-selected:not(.tb-current-entity) {\n' +
'background-color: ' + selectedColor + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-row.mat-row-select.mat-selected:not(.tb-current-entity) .mat-mdc-cell.mat-mdc-table-sticky {\n' +
'background-color: ' + selectedStickyColor + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-row .mat-mdc-cell.mat-mdc-table-sticky, .mat-mdc-table .mat-mdc-header-cell.mat-mdc-table-sticky {\n' +
'background-color: ' + origBackgroundColor + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-row {\n' +
'color: ' + mdDark + ';\n' +
'background-color: rgba(0, 0, 0, 0);\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button mat-icon {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] mat-icon {\n' +
'color: ' + mdDarkDisabled + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button tb-icon {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] tb-icon {\n' +
'color: ' + mdDarkDisabled + ';\n' +
'}\n' +
'.mat-divider {\n' +
'border-top-color: ' + mdDarkDivider + ';\n' +
'}\n' +
'.mat-mdc-paginator {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-paginator button.mat-mdc-icon-button {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-paginator button.mat-mdc-icon-button[disabled][disabled] {\n' +
'color: ' + mdDarkDisabled + ';\n' +
'}\n' +
'.mat-mdc-paginator .mat-mdc-select-value {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}';
return cssString; return cssString;
} }

46
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss

@ -44,7 +44,53 @@
padding: 0 5px; padding: 0 5px;
} }
} }
.mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron {
border-color: var(--mat-tab-header-pagination-disabled-icon-color);
}
}
.mat-mdc-input-element::placeholder {
color: var(--mat-table-header-headline-color);
} }
.mat-mdc-table {
.mat-mdc-header-row {
background-color: var(--tb-orig-background-color);
}
.mat-mdc-cell, .mat-mdc-header-cell {
&.mat-mdc-table-sticky {
background-color:var(--tb-orig-background-color);
}
}
.mat-mdc-row {
background-color: rgba(0,0,0,0);
&.tb-current-entity {
background-color: var(--tb-current-entity-color);
.mat-mdc-cell.mat-mdc-table-sticky {
background-color: var(--tb-current-entity-sticky-color);
}
}
&:hover:not(.tb-current-entity){
background-color: var(--tb-hover-color);
.mat-mdc-cell.mat-mdc-table-sticky {
background-color: var(--tb-hover-sticky-color);
}
}
&.mat-row-select.mat-selected:not(.tb-current-entity){
background-color: var(--tb-selected-color);
.mat-mdc-cell.mat-mdc-table-sticky {
background-color: var(--tb-selected-sticky-color);
}
}
&, .mat-mdc-cell.mat-mdc-table-sticky {
transition: background-color .2s;
background-color:var(--tb-orig-background-color);
}
}
}
} }
:host-context(.tb-has-timewindow) { :host-context(.tb-has-timewindow) {

2
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html

@ -41,7 +41,7 @@
class="flex-1" class="flex-1"
mat-stretch-tabs="false" mat-stretch-tabs="false"
[(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()"> [(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()">
<mat-tab *ngFor="let source of sources; trackBy: trackBySourcesIndex; let index = index;" [label]="getTabLabel(source)"> <mat-tab *ngFor="let source of sources; trackBy: trackBySourcesIndex; let index = index;" [label]="source.displayName">
<ng-template [ngIf]="isActiveTab(index)"> <ng-template [ngIf]="isActiveTab(index)">
<div class="table-container flex-1"> <div class="table-container flex-1">
<table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp" <table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp"

44
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -107,11 +107,18 @@ import { FormBuilder } from '@angular/forms';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models'; import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
import { DateFormatSettings, ValueFormatProcessor } from '@shared/models/widget-settings.models'; import { DateFormatSettings, ValueFormatProcessor } from '@shared/models/widget-settings.models';
export enum TabSortKey {
NAME_ASC = 'NAME_ASC',
NAME_DESC = 'NAME_DESC',
TIMESTAMP = 'timestamp'
}
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean; showTimestamp: boolean;
showMilliseconds: boolean; showMilliseconds: boolean;
hideEmptyLines: boolean; hideEmptyLines: boolean;
dateFormat: DateFormatSettings; dateFormat: DateFormatSettings;
tabSortKey: TabSortKey;
} }
interface TimeseriesWidgetLatestDataKeySettings extends TableWidgetDataKeySettings { interface TimeseriesWidgetLatestDataKeySettings extends TableWidgetDataKeySettings {
@ -152,6 +159,7 @@ interface TimeseriesTableSource {
timeseriesDatasource: TimeseriesDatasource; timeseriesDatasource: TimeseriesDatasource;
header: TimeseriesHeader[]; header: TimeseriesHeader[];
rowDataTemplate: {[key: string]: any}; rowDataTemplate: {[key: string]: any};
displayName: string;
} }
@Component({ @Component({
@ -392,12 +400,33 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.updateDatasources(); this.updateDatasources();
} }
public getTabLabel(source: TimeseriesTableSource){ private getTabLabel(source: Datasource, entityLabelCache:Map<string,string>):string {
if (this.useEntityLabel) { if (entityLabelCache.has(source.entityId)) {
return source.datasource.entityLabel || source.datasource.entityName; return entityLabelCache.get(source.entityId);
} else { }
return source.datasource.entityName;
const value = this.useEntityLabel
? (source.entityLabel || source.entityName)
: source.entityName;
const translated = this.utils.customTranslation(value);
entityLabelCache.set(source.entityId, translated);
return translated;
}
private sortDatasources(source: Datasource[], entityLabelCache:Map<string,string>): Datasource[] {
source.forEach(ds => this.getTabLabel(ds, entityLabelCache));
if (this.settings.tabSortKey === TabSortKey.TIMESTAMP) {
return source;
} }
return source.sort((a, b) => {
const valueA = entityLabelCache.get(a.entityId);
const valueB = entityLabelCache.get(b.entityId);
return this.settings.tabSortKey === TabSortKey.NAME_ASC
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA);
});
} }
private updateDatasources() { private updateDatasources() {
@ -405,9 +434,11 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.sourceIndex = 0; this.sourceIndex = 0;
let keyOffset = 0; let keyOffset = 0;
let latestKeyOffset = 0; let latestKeyOffset = 0;
const entityLabelCache = new Map<string,string>();
const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
if (this.datasources) { if (this.datasources) {
for (const datasource of this.datasources) { const sortedDatasources = this.sortDatasources(this.datasources, entityLabelCache);
for (const datasource of sortedDatasources) {
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
const source = {} as TimeseriesTableSource; const source = {} as TimeseriesTableSource;
source.header = this.prepareHeader(datasource); source.header = this.prepareHeader(datasource);
@ -424,6 +455,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
source.pageLink = new PageLink(pageSize, 0, null, sortOrder); source.pageLink = new PageLink(pageSize, 0, null, sortOrder);
source.rowDataTemplate = {}; source.rowDataTemplate = {};
source.rowDataTemplate.Timestamp = null; source.rowDataTemplate.Timestamp = null;
source.displayName = entityLabelCache.get(datasource.entityId);
if (this.showTimestamp) { if (this.showTimestamp) {
source.displayedColumns.push('0'); source.displayedColumns.push('0');
} }

12
ui-ngx/src/app/modules/home/pages/profile/profile.component.html

@ -58,12 +58,14 @@
[enableFlagsSelect]="true" [enableFlagsSelect]="true"
formControlName="phone"> formControlName="phone">
</tb-phone-input> </tb-phone-input>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>language.language</mat-label> <mat-label translate>language.language</mat-label>
<mat-select formControlName="language"> <mat-select formControlName="language"
<mat-option *ngFor="let lang of languageList" [value]="lang"> [placeholder]="'language.auto' | translate">
{{ lang ? ('language.locales.' + lang | translate) : ''}} <mat-option [value]="null">{{ 'language.auto' | translate }}</mat-option>
</mat-option> @for(lang of languageList; track lang) {
<mat-option [value]="lang">{{ 'language.locales.' + lang | translate }}</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block" floatLabel="always"> <mat-form-field class="mat-block" floatLabel="always">

15
ui-ngx/src/app/modules/home/pages/profile/profile.component.ts

@ -25,7 +25,6 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions'; import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions';
import { environment as env } from '@env/environment'; import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
@ -52,7 +51,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
private route: ActivatedRoute, private route: ActivatedRoute,
private userService: UserService, private userService: UserService,
private authService: AuthService, private authService: AuthService,
private translate: TranslateService,
private unitService: UnitService, private unitService: UnitService,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder) {
super(store); super(store);
@ -82,9 +80,13 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
if (!this.user.additionalInfo) { if (!this.user.additionalInfo) {
this.user.additionalInfo = {}; this.user.additionalInfo = {};
} }
this.user.additionalInfo.lang = this.profile.get('language').value;
this.user.additionalInfo.homeDashboardId = this.profile.get('homeDashboardId').value; this.user.additionalInfo.homeDashboardId = this.profile.get('homeDashboardId').value;
this.user.additionalInfo.homeDashboardHideToolbar = this.profile.get('homeDashboardHideToolbar').value; this.user.additionalInfo.homeDashboardHideToolbar = this.profile.get('homeDashboardHideToolbar').value;
if (isNotEmptyStr(this.profile.get('language').value)) {
this.user.additionalInfo.lang = this.profile.get('language').value;
} else {
delete this.user.additionalInfo.lang;
}
if (isNotEmptyStr(this.profile.get('unitSystem').value)) { if (isNotEmptyStr(this.profile.get('unitSystem').value)) {
this.user.additionalInfo.unitSystem = this.profile.get('unitSystem').value; this.user.additionalInfo.unitSystem = this.profile.get('unitSystem').value;
} else { } else {
@ -105,7 +107,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
id: user.id, id: user.id,
lastName: user.lastName, lastName: user.lastName,
} })); } }));
this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang })); this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang || env.defaultLang }));
this.unitService.setUnitSystem(this.user.additionalInfo.unitSystem); this.unitService.setUnitSystem(this.user.additionalInfo.unitSystem);
this.authService.refreshJwtToken(false); this.authService.refreshJwtToken(false);
} }
@ -115,7 +117,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
private userLoaded(user: User) { private userLoaded(user: User) {
this.user = user; this.user = user;
this.profile.reset(user); this.profile.reset(user);
let lang; let lang: string = null;
let homeDashboardId; let homeDashboardId;
let homeDashboardHideToolbar = true; let homeDashboardHideToolbar = true;
let unitSystem: UnitSystem = null; let unitSystem: UnitSystem = null;
@ -131,9 +133,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
unitSystem = user.additionalInfo.unitSystem; unitSystem = user.additionalInfo.unitSystem;
} }
} }
if (!lang) {
lang = this.translate.currentLang;
}
this.profile.get('language').setValue(lang); this.profile.get('language').setValue(lang);
this.profile.get('unitSystem').setValue(unitSystem); this.profile.get('unitSystem').setValue(unitSystem);
this.profile.get('homeDashboardId').setValue(homeDashboardId); this.profile.get('homeDashboardId').setValue(homeDashboardId);

2
ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html

@ -31,7 +31,7 @@
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content>
<tb-user></tb-user> <tb-user></tb-user>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" appearance="outline">
<mat-label translate>user.activation-method</mat-label> <mat-label translate>user.activation-method</mat-label>
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="activationMethod"> <mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="activationMethod">
<mat-option *ngFor="let activationMethod of activationMethods" [value]="activationMethod"> <mat-option *ngFor="let activationMethod of activationMethods" [value]="activationMethod">

6
ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts

@ -84,6 +84,12 @@ export class AddUserDialogComponent extends DialogComponent<AddUserDialogCompone
this.user.authority = this.data.authority; this.user.authority = this.data.authority;
this.user.tenantId = new TenantId(this.data.tenantId); this.user.tenantId = new TenantId(this.data.tenantId);
this.user.customerId = new CustomerId(this.data.customerId); this.user.customerId = new CustomerId(this.data.customerId);
if (!this.user.additionalInfo.lang) {
delete this.user.additionalInfo.lang;
}
if (!this.user.additionalInfo.unitSystem) {
delete this.user.additionalInfo.unitSystem;
}
const sendActivationEmail = this.activationMethod === ActivationMethod.SEND_ACTIVATION_MAIL; const sendActivationEmail = this.activationMethod === ActivationMethod.SEND_ACTIVATION_MAIL;
this.userService.saveUser(this.user, sendActivationEmail).subscribe( this.userService.saveUser(this.user, sendActivationEmail).subscribe(
(user) => { (user) => {

35
ui-ngx/src/app/modules/home/pages/user/user.component.html

@ -74,7 +74,7 @@
<div class="mat-padding flex flex-col"> <div class="mat-padding flex flex-col">
<form [formGroup]="entityForm"> <form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit"> <fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" appearance="outline">
<mat-label translate>user.email</mat-label> <mat-label translate>user.email</mat-label>
<input matInput type="email" formControlName="email" required> <input matInput type="email" formControlName="email" required>
<mat-error *ngIf="entityForm.get('email').hasError('email')"> <mat-error *ngIf="entityForm.get('email').hasError('email')">
@ -84,30 +84,52 @@
{{ 'user.email-required' | translate }} {{ 'user.email-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" appearance="outline">
<mat-label translate>user.first-name</mat-label> <mat-label translate>user.first-name</mat-label>
<input matInput formControlName="firstName"> <input matInput formControlName="firstName">
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" appearance="outline">
<mat-label translate>user.last-name</mat-label> <mat-label translate>user.last-name</mat-label>
<input matInput formControlName="lastName"> <input matInput formControlName="lastName">
</mat-form-field> </mat-form-field>
<tb-phone-input [required]="false" <tb-phone-input [required]="false"
label="{{ 'contact.phone' | translate }}" label="{{ 'contact.phone' | translate }}"
[enableFlagsSelect]="true" [enableFlagsSelect]="true"
appearance="outline"
formControlName="phone"> formControlName="phone">
</tb-phone-input> </tb-phone-input>
<div formGroupName="additionalInfo" class="flex flex-col"> <div formGroupName="additionalInfo" class="flex flex-col">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" floatLabel="always" appearance="outline">
<mat-label translate>language.language</mat-label>
<mat-select formControlName="lang"
[placeholder]="'language.auto' | translate">
<mat-option [value]="null">{{ 'language.auto' | translate }}</mat-option>
@for(lang of languageList; track lang) {
<mat-option [value]="lang">{{ 'language.locales.' + lang | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block" floatLabel="always" appearance="outline">
<mat-label translate>unit.unit-system</mat-label>
<mat-select formControlName="unitSystem"
[placeholder]="'unit.unit-system-type.AUTO' | translate">
<mat-option [value]="null">{{ 'unit.unit-system-type.AUTO' | translate }}</mat-option>
@for(unit of UnitSystems; track unit) {
<mat-option [value]="unit">{{ 'unit.unit-system-type.' + unit | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>user.description</mat-label> <mat-label translate>user.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field> </mat-form-field>
<section class="tb-default-dashboard flex flex-1 flex-col" *ngIf="entity?.id"> <section class="tb-default-dashboard flex flex-1 flex-col" *ngIf="entity?.id">
<section class="flex flex-1 flex-col gt-sm:flex-row"> <section class="flex flex-1 flex-col items-baseline gt-sm:flex-row">
<tb-dashboard-autocomplete <tb-dashboard-autocomplete
class="flex-1" class="flex-1"
label="{{ 'user.default-dashboard' | translate }}" label="{{ 'user.default-dashboard' | translate }}"
formControlName="defaultDashboardId" formControlName="defaultDashboardId"
appearance="outline"
[dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'" [dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
[tenantId]="entity?.tenantId?.id" [tenantId]="entity?.tenantId?.id"
[customerId]="entity?.customerId?.id" [customerId]="entity?.customerId?.id"
@ -117,11 +139,12 @@
{{ 'user.always-fullscreen' | translate }} {{ 'user.always-fullscreen' | translate }}
</mat-checkbox> </mat-checkbox>
</section> </section>
<section class="flex flex-1 flex-col gt-sm:flex-row"> <section class="flex flex-1 flex-col items-baseline gt-sm:flex-row">
<tb-dashboard-autocomplete <tb-dashboard-autocomplete
class="flex-1" class="flex-1"
label="{{ 'dashboard.home-dashboard' | translate }}" label="{{ 'dashboard.home-dashboard' | translate }}"
formControlName="homeDashboardId" formControlName="homeDashboardId"
appearance="outline"
[dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'" [dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
[tenantId]="entity?.tenantId?.id" [tenantId]="entity?.tenantId?.id"
[customerId]="entity?.customerId?.id" [customerId]="entity?.customerId?.id"

10
ui-ngx/src/app/modules/home/pages/user/user.component.ts

@ -27,6 +27,8 @@ import { isDefinedAndNotNull } from '@core/utils';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { ActionNotificationShow } from '@app/core/notification/notification.actions'; import { ActionNotificationShow } from '@app/core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { environment as env } from '@env/environment';
import { UnitSystems } from '@shared/models/unit.models';
@Component({ @Component({
selector: 'tb-user', selector: 'tb-user',
@ -36,6 +38,8 @@ import { TranslateService } from '@ngx-translate/core';
export class UserComponent extends EntityComponent<User> { export class UserComponent extends EntityComponent<User> {
authority = Authority; authority = Authority;
languageList = env.supportedLangs;
UnitSystems = UnitSystems;
loginAsUserEnabled$ = this.store.pipe( loginAsUserEnabled$ = this.store.pipe(
select(selectAuth), select(selectAuth),
@ -77,6 +81,8 @@ export class UserComponent extends EntityComponent<User> {
additionalInfo: this.fb.group( additionalInfo: this.fb.group(
{ {
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
lang: [entity && entity.additionalInfo ? entity.additionalInfo.lang : null],
unitSystem: [entity && entity.additionalInfo ? entity.additionalInfo.unitSystem : null],
defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null], defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null],
defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false], defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false],
homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null], homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
@ -94,6 +100,10 @@ export class UserComponent extends EntityComponent<User> {
this.entityForm.patchValue({lastName: entity.lastName}); this.entityForm.patchValue({lastName: entity.lastName});
this.entityForm.patchValue({phone: entity.phone}); this.entityForm.patchValue({phone: entity.phone});
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
this.entityForm.patchValue({additionalInfo:
{lang: entity.additionalInfo ? entity.additionalInfo.lang : null}});
this.entityForm.patchValue({additionalInfo:
{unitSystem: entity.additionalInfo ? entity.additionalInfo.unitSystem : null}});
this.entityForm.patchValue({additionalInfo: this.entityForm.patchValue({additionalInfo:
{defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}}); {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}});
this.entityForm.patchValue({additionalInfo: this.entityForm.patchValue({additionalInfo:

6
ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts

@ -158,6 +158,12 @@ export class UsersTableConfigResolver {
user.tenantId = new TenantId(this.tenantId); user.tenantId = new TenantId(this.tenantId);
user.customerId = new CustomerId(this.customerId); user.customerId = new CustomerId(this.customerId);
user.authority = this.authority; user.authority = this.authority;
if (!user.additionalInfo.lang) {
delete user.additionalInfo.lang;
}
if (!user.additionalInfo.unitSystem) {
delete user.additionalInfo.unitSystem;
}
return this.userService.saveUser(user); return this.userService.saveUser(user);
} }

2
ui-ngx/src/app/shared/components/color-picker/hex-input.component.html

@ -30,7 +30,7 @@
</tb-copy-button> </tb-copy-button>
</mat-form-field> </mat-form-field>
<mat-form-field class="alpha-input" subscriptSizing="dynamic"> <mat-form-field class="alpha-input" subscriptSizing="dynamic">
<input matInput #hue min="0" max="100" [value]="hueValue" (keyup)="onHueInputChange($event, hue.value)"> <input matInput #hue type="number" min="0" max="100" [value]="hueValue" (keyup)="onHueInputChange($event, hue.value)">
<span matSuffix>%</span> <span matSuffix>%</span>
</mat-form-field> </mat-form-field>
</div> </div>

11
ui-ngx/src/app/shared/models/widget.models.ts

@ -659,7 +659,8 @@ export enum MapItemType {
marker = 'marker', marker = 'marker',
polygon = 'polygon', polygon = 'polygon',
rectangle = 'rectangle', rectangle = 'rectangle',
circle = 'circle' circle = 'circle',
polyline = 'polyline'
} }
export const widgetActionTypes = Object.keys(WidgetActionType) export const widgetActionTypes = Object.keys(WidgetActionType)
@ -699,6 +700,7 @@ export const mapItemTypeTranslationMap = new Map<MapItemType, string>(
[ MapItemType.polygon, 'widget-action.map-item.polygon' ], [ MapItemType.polygon, 'widget-action.map-item.polygon' ],
[ MapItemType.rectangle, 'widget-action.map-item.rectangle' ], [ MapItemType.rectangle, 'widget-action.map-item.rectangle' ],
[ MapItemType.circle, 'widget-action.map-item.circle' ], [ MapItemType.circle, 'widget-action.map-item.circle' ],
[ MapItemType.polyline, 'widget-action.map-item.polyline' ]
] ]
) )
@ -826,6 +828,8 @@ export interface MapItemTooltips {
finishRect?: string; finishRect?: string;
startCircle?: string; startCircle?: string;
finishCircle?: string; finishCircle?: string;
startPolyline?: string;
finishPolyline?: string;
} }
export const mapItemTooltipsTranslation: Required<MapItemTooltips> = Object.freeze({ export const mapItemTooltipsTranslation: Required<MapItemTooltips> = Object.freeze({
@ -836,7 +840,9 @@ export const mapItemTooltipsTranslation: Required<MapItemTooltips> = Object.free
startRect: 'widgets.maps.data-layer.polygon.rectangle-place-first-point-hint', startRect: 'widgets.maps.data-layer.polygon.rectangle-place-first-point-hint',
finishRect: 'widgets.maps.data-layer.polygon.finish-rectangle-hint', finishRect: 'widgets.maps.data-layer.polygon.finish-rectangle-hint',
startCircle: 'widgets.maps.data-layer.circle.place-circle-center-hint', startCircle: 'widgets.maps.data-layer.circle.place-circle-center-hint',
finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint' finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint',
startPolyline: 'widgets.maps.data-layer.polyline.polyline-place-first-point-hint',
finishPolyline: 'widgets.maps.data-layer.polyline.finish-polyline-hint'
}) })
export interface WidgetActionDescriptor extends WidgetAction { export interface WidgetActionDescriptor extends WidgetAction {
@ -1145,5 +1151,4 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) {
} }
} }

97
ui-ngx/src/app/shared/models/widget/maps/map.models.ts

@ -79,7 +79,7 @@ export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSetting
}; };
const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
dataLayerType: MapDataLayerType): DataKey[] => { dataLayerType: MapDataLayerType): DataKey[] => {
const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : []; const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : [];
switch (dataLayerType) { switch (dataLayerType) {
case 'trips': case 'trips':
@ -96,6 +96,9 @@ const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
case 'circles': case 'circles':
dataKeys.push((settings as CirclesDataLayerSettings).circleKey); dataKeys.push((settings as CirclesDataLayerSettings).circleKey);
break; break;
case 'polylines':
dataKeys.push((settings as PolylinesDataLayerSettings).polylineKey);
break;
} }
return dataKeys; return dataKeys;
}; };
@ -159,6 +162,7 @@ export interface DataLayerEditSettings {
snappable: boolean; snappable: boolean;
} }
export interface MapDataLayerSettings extends MapDataSourceSettings { export interface MapDataLayerSettings extends MapDataSourceSettings {
additionalDataSources?: MapDataSourceSettings[]; additionalDataSources?: MapDataSourceSettings[];
additionalDataKeys?: DataKey[]; additionalDataKeys?: DataKey[];
@ -166,7 +170,7 @@ export interface MapDataLayerSettings extends MapDataSourceSettings {
tooltip: DataLayerTooltipSettings; tooltip: DataLayerTooltipSettings;
click: WidgetAction; click: WidgetAction;
groups?: string[]; groups?: string[];
edit: DataLayerEditSettings; edit: DataLayerEditSettings;
} }
export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataLayerSettings> => ({ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataLayerSettings> => ({
@ -182,7 +186,7 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataL
type: DataLayerPatternType.pattern, type: DataLayerPatternType.pattern,
pattern: mapType === MapType.geoMap ? pattern: mapType === MapType.geoMap ?
'<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}<br/><b>Temperature:</b> ${temperature} °C<br/><small>See tooltip settings for details</small>' '<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}<br/><b>Temperature:</b> ${temperature} °C<br/><small>See tooltip settings for details</small>'
: '<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}<br/><b>Temperature:</b> ${temperature} °C<br/><small>See tooltip settings for details</small>', : '<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}<br/><b>Temperature:</b> ${temperature} °C<br/><small>See tooltip settings for details</small>',
offsetX: 0, offsetX: 0,
offsetY: -1 offsetY: -1
}, },
@ -196,9 +200,9 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataL
} }
}) })
export type MapDataLayerType = 'trips' | 'markers' | 'polygons' | 'circles'; export type MapDataLayerType = 'trips' | 'markers' | 'polygons' | 'circles' | 'polylines';
export const mapDataLayerTypes: MapDataLayerType[] = ['trips', 'markers', 'polygons', 'circles']; export const mapDataLayerTypes: MapDataLayerType[] = ['trips', 'markers', 'polygons', 'circles', 'polylines'];
export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapDataLayerType): boolean => { export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapDataLayerType): boolean => {
if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) { if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) {
@ -222,7 +226,7 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData
case 'markers': case 'markers':
const markersDataLayer = dataLayer as MarkersDataLayerSettings; const markersDataLayer = dataLayer as MarkersDataLayerSettings;
if (!markersDataLayer.xKey?.type || !markersDataLayer.xKey?.name || if (!markersDataLayer.xKey?.type || !markersDataLayer.xKey?.name ||
!markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) { !markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) {
return false; return false;
} }
break; break;
@ -232,6 +236,12 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData
return false; return false;
} }
break; break;
case 'polylines':
const polylinesDataLayer = dataLayer as PolylinesDataLayerSettings;
if (!polylinesDataLayer.polylineKey?.type || !polylinesDataLayer.polylineKey?.name) {
return false;
}
break;
case 'circles': case 'circles':
const circlesDataLayer = dataLayer as CirclesDataLayerSettings; const circlesDataLayer = dataLayer as CirclesDataLayerSettings;
if (!circlesDataLayer.circleKey?.type || !circlesDataLayer.circleKey?.name) { if (!circlesDataLayer.circleKey?.type || !circlesDataLayer.circleKey?.name) {
@ -300,6 +310,7 @@ export interface MarkerIconSettings extends BaseMarkerShapeSettings {
iconContainer?: MarkerIconContainer; iconContainer?: MarkerIconContainer;
icon: string; icon: string;
} }
export interface MarkerClusteringSettings { export interface MarkerClusteringSettings {
enable: boolean; enable: boolean;
zoomOnClick: boolean; zoomOnClick: boolean;
@ -600,8 +611,11 @@ export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial<
color: '#3388ff', color: '#3388ff',
}, },
strokeWeight: 3 strokeWeight: 3
} as Partial<PolygonsDataLayerSettings>, defaultBaseDataLayerSettings(mapType), } as Partial<PolygonsDataLayerSettings>, defaultBaseDataLayerSettings(mapType),
{label: {show: false}, tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}} as Partial<PolygonsDataLayerSettings>) {
label: {show: false},
tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}
} as Partial<PolygonsDataLayerSettings>)
export interface CirclesDataLayerSettings extends ShapeDataLayerSettings { export interface CirclesDataLayerSettings extends ShapeDataLayerSettings {
circleKey: DataKey; circleKey: DataKey;
@ -651,8 +665,45 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial<C
color: '#3388ff', color: '#3388ff',
}, },
strokeWeight: 3 strokeWeight: 3
} as Partial<CirclesDataLayerSettings>, defaultBaseDataLayerSettings(mapType), } as Partial<CirclesDataLayerSettings>, defaultBaseDataLayerSettings(mapType),
{label: {show: false}, tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}} as Partial<CirclesDataLayerSettings>) {
label: {show: false},
tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}
} as Partial<CirclesDataLayerSettings>)
export interface PolylinesDataLayerSettings extends ShapeDataLayerSettings {
polylineKey: DataKey;
}
export const defaultPolylinesDataLayerSettings = (mapType: MapType, functionsOnly = false): PolylinesDataLayerSettings => mergeDeep({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
dsLabel: functionsOnly ? 'First polyline' : '',
polylineKey: {
name: functionsOnly ? 'f(x)' : 'perimeter',
label: 'perimeter',
type: functionsOnly ? DataKeyType.function : DataKeyType.attribute,
settings: {},
color: materialColors[0].value
}
} as PolylinesDataLayerSettings, defaultBasePolylinesDataLayerSettings(mapType) as PolylinesDataLayerSettings);
export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial<PolylinesDataLayerSettings> => mergeDeep({
fillType: ShapeFillType.color,
fillColor: {
type: DataLayerColorType.constant,
color: 'rgba(51,136,255,0.2)',
},
strokeColor: {
type: DataLayerColorType.constant,
color: '#3388ff',
},
strokeWeight: 3
} as Partial<PolylinesDataLayerSettings>, defaultBaseDataLayerSettings(mapType),
{
label: {show: false},
tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}
} as Partial<PolylinesDataLayerSettings>)
export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => { export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => {
switch (dataLayerType) { switch (dataLayerType) {
@ -664,6 +715,8 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map
return defaultPolygonsDataLayerSettings(mapType, functionsOnly); return defaultPolygonsDataLayerSettings(mapType, functionsOnly);
case 'circles': case 'circles':
return defaultCirclesDataLayerSettings(mapType, functionsOnly); return defaultCirclesDataLayerSettings(mapType, functionsOnly);
case 'polylines':
return defaultPolylinesDataLayerSettings(mapType, functionsOnly);
} }
}; };
@ -677,6 +730,8 @@ export const defaultBaseMapDataLayerSettings = <T extends MapDataLayerSettings>(
return defaultBasePolygonsDataLayerSettings(mapType) as T; return defaultBasePolygonsDataLayerSettings(mapType) as T;
case 'circles': case 'circles':
return defaultBaseCirclesDataLayerSettings(mapType) as T; return defaultBaseCirclesDataLayerSettings(mapType) as T;
case 'polylines':
return defaultBasePolylinesDataLayerSettings(mapType) as T;
} }
} }
@ -727,13 +782,13 @@ export const additionalMapDataSourceValid = (dataSource: AdditionalMapDataSource
}; };
export const additionalMapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { export const additionalMapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const dataSource: AdditionalMapDataSourceSettings = control.value; const dataSource: AdditionalMapDataSourceSettings = control.value;
if (!additionalMapDataSourceValid(dataSource)) { if (!additionalMapDataSourceValid(dataSource)) {
return { return {
dataSource: true dataSource: true
}; };
} }
return null; return null;
}; };
export const defaultAdditionalMapDataSourceSettings = (functionsOnly = false): AdditionalMapDataSourceSettings => { export const defaultAdditionalMapDataSourceSettings = (functionsOnly = false): AdditionalMapDataSourceSettings => {
@ -815,6 +870,7 @@ export interface BaseMapSettings {
markers: MarkersDataLayerSettings[]; markers: MarkersDataLayerSettings[];
polygons: PolygonsDataLayerSettings[]; polygons: PolygonsDataLayerSettings[];
circles: CirclesDataLayerSettings[]; circles: CirclesDataLayerSettings[];
polylines: PolylinesDataLayerSettings[];
additionalDataSources: AdditionalMapDataSourceSettings[]; additionalDataSources: AdditionalMapDataSourceSettings[];
controlsPosition: MapControlsPosition; controlsPosition: MapControlsPosition;
zoomActions: MapZoomAction[]; zoomActions: MapZoomAction[];
@ -839,6 +895,7 @@ export const defaultBaseMapSettings: BaseMapSettings = {
markers: [], markers: [],
polygons: [], polygons: [],
circles: [], circles: [],
polylines: [],
additionalDataSources: [], additionalDataSources: [],
controlsPosition: MapControlsPosition.topleft, controlsPosition: MapControlsPosition.topleft,
zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons], zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons],
@ -1245,6 +1302,12 @@ export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][]
export type TbPolygonCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; export type TbPolygonCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][];
export type TbPolygonCoordinates = TbPolygonCoordinate[]; export type TbPolygonCoordinates = TbPolygonCoordinate[];
export type TbPolylineRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][];
export type TbPolylineRawCoordinates = TbPolylineRawCoordinate[];
export type TbPolylineData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][];
export type TbPolylineCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][];
export type TbPolylineCoordinates = TbPolylineCoordinate[];
export interface TbCircleData { export interface TbCircleData {
latitude: number; latitude: number;
longitude: number; longitude: number;

1
ui-ngx/src/app/shared/shared.module.ts

@ -237,6 +237,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
@NgModule({ @NgModule({
providers: [ providers: [
DatePipe, DatePipe,
SelectableColumnsPipe,
MillisecondsToTimeStringPipe, MillisecondsToTimeStringPipe,
EnumToArrayPipe, EnumToArrayPipe,
HighlightPipe, HighlightPipe,

3
ui-ngx/src/assets/dashboard/api_usage.json

@ -98,7 +98,8 @@
"settings": { "settings": {
"showTimestamp": true, "showTimestamp": true,
"displayPagination": true, "displayPagination": true,
"defaultPageSize": 10 "defaultPageSize": 10,
"tabSortKey": "NAME_ASC"
}, },
"title": "{i18n:api-usage.exceptions}", "title": "{i18n:api-usage.exceptions}",
"dropShadow": true, "dropShadow": true,

2
ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md

@ -79,7 +79,7 @@ function AddEntityDialogController(instance) {
const mapType = widgetContext.mapInstance.type(); const mapType = widgetContext.mapInstance.type();
attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x}); attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x});
attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y}); attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y});
} else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') { } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon' || mapItemType === 'Line') {
attributes.push({key: 'perimeter', value: additionalParams.coordinates}); attributes.push({key: 'perimeter', value: additionalParams.coordinates});
} else if (mapItemType === 'Circle') { } else if (mapItemType === 'Circle') {
attributes.push({key: 'circle', value: additionalParams.coordinates}); attributes.push({key: 'circle', value: additionalParams.coordinates});

5
ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md

@ -26,8 +26,9 @@ A JavaScript function triggered after a map item is placed. Optionally uses an H
<li><b>coordinates:</b> <code>Coordinates</code> - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item: <li><b>coordinates:</b> <code>Coordinates</code> - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item:
<ul> <ul>
<li><b>Marker:</b> <code>{x: number; y: number}</code>, where <code>x</code> represents latitude, and <code>y</code> represents longitude.</li> <li><b>Marker:</b> <code>{x: number; y: number}</code>, where <code>x</code> represents latitude, and <code>y</code> represents longitude.</li>
<li><b>Polygon, Rectangle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/61254a68507c6def8c055b7b3ae70413c456a4ac/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1099" target="_blank">TbPolygonRawCoordinates</a></code> contains an array of points defining the shape boundaries.</li> <li><b>Polygon, Rectangle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e8b2066fb6f38bcc5775ea5b51a0755dfb763df8/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1326" target="_blank">TbPolygonRawCoordinates</a></code> contains an array of points defining the shape boundaries.</li>
<li><b>Circle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/61254a68507c6def8c055b7b3ae70413c456a4ac/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1104" target="_blank">TbCircleData</a></code> contains center coordinates and radius information.</li> <li><b>Circle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e8b2066fb6f38bcc5775ea5b51a0755dfb763df8/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1338" target="_blank">TbCircleData</a></code> contains center coordinates and radius information.</li>
<li><b>Polyline:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e8b2066fb6f38bcc5775ea5b51a0755dfb763df8/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1333" target="_blank">TbPolylineRawCoordinates</a></code> contains an array of points defining the polyline.</li>
</ul> </ul>
Note: The coordinates will be automatically converted according to the selected map type. Note: The coordinates will be automatically converted according to the selected map type.
</li> </li>

4
ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md

@ -5,7 +5,7 @@
*function (data, dsData): string* *function (data, dsData): string*
A JavaScript function used to compute color of the trip path point. A JavaScript function used to compute color of the path point.
**Parameters:** **Parameters:**
@ -15,7 +15,7 @@ A JavaScript function used to compute color of the trip path point.
**Returns:** **Returns:**
Should return string value presenting color of the trip path point. Should return string value presenting color of the path point.
In case no data is returned, color value from **Color** settings field will be used. In case no data is returned, color value from **Color** settings field will be used.

2
ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md

@ -5,7 +5,7 @@
*function (data, dsData): string* *function (data, dsData): string*
A JavaScript function used to compute text or HTML code to be displayed in the trip path point tooltip. A JavaScript function used to compute text or HTML code to be displayed in the path point tooltip.
**Parameters:** **Parameters:**

22
ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md

@ -0,0 +1,22 @@
#### Polyline label function
<div class="divider"></div>
<br/>
*function (data, dsData): string*
A JavaScript function used to compute text or HTML code of the polyline label.
**Parameters:**
<ul>
{% include widget/lib/map/map_fn_args %}
</ul>
**Returns:**
Should return string value presenting text or HTML of the polyline label.
<div class="divider"></div>
{% include widget/lib/map/label_fn_examples %}

24
ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md

@ -0,0 +1,24 @@
#### Polyline stroke color function
<div class="divider"></div>
<br/>
*function (data, dsData): string*
A JavaScript function used to compute stroke color of the polyline.
**Parameters:**
<ul>
{% include widget/lib/map/map_fn_args %}
</ul>
**Returns:**
Should return string value presenting stroke color of the polyline.
In case no data is returned, color value from **Color** settings field will be used.
<div class="divider"></div>
{% include widget/lib/map/color_fn_examples %}

22
ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md

@ -0,0 +1,22 @@
#### Polyline tooltip function
<div class="divider"></div>
<br/>
*function (data, dsData): string*
A JavaScript function used to compute text or HTML code to be displayed in the polyline tooltip.
**Parameters:**
<ul>
{% include widget/lib/map/map_fn_args %}
</ul>
**Returns:**
Should return string value presenting text or HTML for the polyline tooltip.
<div class="divider"></div>
{% include widget/lib/map/tooltip_fn_examples %}

50
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2829,6 +2829,7 @@
"details": "Entity details", "details": "Entity details",
"no-entities-prompt": "No entities found", "no-entities-prompt": "No entities found",
"no-data": "No data to display", "no-data": "No data to display",
"show-all-columns":"Show All",
"columns-to-display": "Columns to Display", "columns-to-display": "Columns to Display",
"type-api-usage-state": "API Usage State", "type-api-usage-state": "API Usage State",
"type-edge": "Edge", "type-edge": "Edge",
@ -7135,7 +7136,8 @@
"marker": "Marker", "marker": "Marker",
"polygon": "Polygon", "polygon": "Polygon",
"rectangle": "Rectangle", "rectangle": "Rectangle",
"circle": "Circle" "circle": "Circle",
"polyline": "Polyline"
}, },
"place-map-item": "Place map item", "place-map-item": "Place map item",
"map-item-tooltip": { "map-item-tooltip": {
@ -7147,7 +7149,9 @@
"continue-draw-polygon": "Continue draw polygon", "continue-draw-polygon": "Continue draw polygon",
"finish-draw-polygon": "Finish draw polygon", "finish-draw-polygon": "Finish draw polygon",
"start-draw-circle": "Start draw circle", "start-draw-circle": "Start draw circle",
"finish-draw-circle": "Finish draw circle" "finish-draw-circle": "Finish draw circle",
"start-draw-polyline": "Start draw polyline",
"finish-draw-polyline": "Finish draw polyline"
} }
}, },
"widgets-bundle": { "widgets-bundle": {
@ -8163,14 +8167,14 @@
"attribute-scope-server": "Server attribute", "attribute-scope-server": "Server attribute",
"attribute-scope-shared": "Shared attribute", "attribute-scope-shared": "Shared attribute",
"value-required": "Value required", "value-required": "Value required",
"image-settings": "Image settings", "image-settings": "Image output settings",
"image-format": "Image format", "image-format": "Image format",
"image-format-jpeg": "JPEG", "image-format-jpeg": "JPEG",
"image-format-png": "PNG", "image-format-png": "PNG",
"image-format-webp": "WEBP", "image-format-webp": "WEBP",
"image-quality": "Image quality that use lossy compression such as jpeg and webp", "image-quality": "Image quality",
"max-image-width": "Maximum image width", "max-image-width": "Max width",
"max-image-height": "Maximum image height", "max-image-height": "Max height",
"action-buttons": "Action buttons", "action-buttons": "Action buttons",
"show-action-buttons": "Show action buttons", "show-action-buttons": "Show action buttons",
"update-all-values": "Update all values, not only modified", "update-all-values": "Update all values, not only modified",
@ -8251,7 +8255,10 @@
"add-radio-option": "Add radio option", "add-radio-option": "Add radio option",
"radio-label-position": "Label position", "radio-label-position": "Label position",
"radio-label-position-before": "Before", "radio-label-position-before": "Before",
"radio-label-position-after": "After" "radio-label-position-after": "After",
"save-image": "Save image",
"save-to-gallery": "Automatically store captured images in Image Gallery",
"public-image": "Makes image avaliable for any unauthorized user"
}, },
"invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type",
"qr-code": { "qr-code": {
@ -8546,7 +8553,8 @@
"trips": "Trips", "trips": "Trips",
"markers": "Markers", "markers": "Markers",
"polygons": "Polygons", "polygons": "Polygons",
"circles": "Circles" "circles": "Circles",
"polylines": "Polylines"
}, },
"data-layer": { "data-layer": {
"source": "Source", "source": "Source",
@ -8743,6 +8751,25 @@
"finish-circle-hint-with-entity": "Circle for '{{entityName}}': click to finish and save circle", "finish-circle-hint-with-entity": "Circle for '{{entityName}}': click to finish and save circle",
"finish-circle-hint": "Circle: click to finish drawing" "finish-circle-hint": "Circle: click to finish drawing"
}, },
"polyline": {
"polyline-key": "Polyline key",
"polyline-key-required": "Polyline key required",
"no-polylines": "No polylines configured",
"add-polylines": "Add polyline",
"polyline-configuration": "Polyline configuration",
"remove-polyline": "Remove polyline",
"edit": "Edit polyline",
"cut": "Cut polyline area",
"rotate": "Rotate polyline",
"remove-polyline-for": "Remove polyline for '{{entityName}}'",
"draw-polyline": "Draw polyline",
"polyline-place-first-point-hint-with-entity": "Polyline for '{{entityName}}': click to place first point",
"polyline-place-first-point-hint": "Polyline: click to place first point",
"finish-polyline-hint-with-entity": "Polyline for '{{entityName}}': click to finish drawing",
"finish-polyline-hint": "Polyline: click to finish drawing",
"polyline-place-first-point-cut-hint": "Click to place first point",
"finish-polyline-cut-hint": "Click first marker to finish and save"
},
"select-entity": "Select entity", "select-entity": "Select entity",
"select-entity-hint": "Hint: after selection click at the map to set position" "select-entity-hint": "Hint: after selection click at the map to set position"
}, },
@ -9246,7 +9273,11 @@
"alarm-column-error": "At least one alarm column should be specified", "alarm-column-error": "At least one alarm column should be specified",
"table-tabs": "Table tabs", "table-tabs": "Table tabs",
"show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode", "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode",
"disable-sorting": "Disable sorting" "disable-sorting": "Disable sorting",
"sort-by": "Sort tabs by",
"sort-asc": "Name Ascending",
"sort-desc": "Name Descending",
"sort-timestamp-option": "Created time"
}, },
"latest-chart": { "latest-chart": {
"total": "Total", "total": "Total",
@ -9807,6 +9838,7 @@
"items-per-page-separator": "of" "items-per-page-separator": "of"
}, },
"language": { "language": {
"auto": "Auto",
"language": "Language", "language": "Language",
"locales": { "locales": {
"ar_AE": "العربية (الإمارات العربية المتحدة)", "ar_AE": "العربية (الإمارات العربية المتحدة)",

8
ui-ngx/src/styles.scss

@ -692,7 +692,7 @@ pre.tb-highlight {
mat-toolbar.mat-mdc-table-toolbar:not(.mat-primary), .mat-mdc-cell, .mat-expansion-panel-header, mat-card-header.mat-mdc-card-header { mat-toolbar.mat-mdc-table-toolbar:not(.mat-primary), .mat-mdc-cell, .mat-expansion-panel-header, mat-card-header.mat-mdc-card-header {
button.mat-mdc-icon-button { button.mat-mdc-icon-button {
.mat-icon { .mat-icon {
color: rgba(0, 0, 0, .54); color:var(--mat-icon-color, rgba(0, 0, 0, .54));
} }
&[disabled][disabled] { &[disabled][disabled] {
.mat-icon { .mat-icon {
@ -760,11 +760,11 @@ pre.tb-highlight {
transition: background-color .2s; transition: background-color .2s;
&:hover:not(.tb-current-entity) { &:hover:not(.tb-current-entity) {
background-color: #f4f4f4; background-color: var(--tb-hover-color, #f4f4f4);
} }
&.tb-current-entity { &.tb-current-entity {
background-color: #e9e9e9; background-color: var(--tb-current-entity-color, #e9e9e9);
} }
&.tb-pointer { &.tb-pointer {
@ -816,7 +816,7 @@ pre.tb-highlight {
vertical-align: middle; vertical-align: middle;
border-width: 0; border-width: 0;
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-color: rgba(0, 0, 0, 0.12); border-bottom-color: var(--mat-table-row-item-outline-color,rgba(0, 0, 0, 0.12));
border-style: solid; border-style: solid;
text-overflow: ellipsis; text-overflow: ellipsis;
touch-action: auto !important; touch-action: auto !important;

Loading…
Cancel
Save