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": "",
"dataKeySettingsSchema": "{}\n",
"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": [
{

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

@ -17,7 +17,7 @@
"latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings",
"hasBasicMode": true,
"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": [
{
@ -32,4 +32,4 @@
"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.')) {
this.mobileService.handleMobileNavigation(path, params);
this.mobileService.handleMobileNavigation(path, params, lastChild.queryParams);
return of(false);
}
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) {
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;
}
.tb-latest-chart-shape {
flex: 1;
flex: 1 1 100%;
min-width: 10px;
min-height: 10px;
display: flex;
@ -95,8 +95,14 @@
&.legend-right, &.legend-left {
gap: 24px;
.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;
justify-content: center;
justify-content: stretch;
align-items: stretch;
.tb-latest-chart-legend-item {
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() {
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.chartLegend.nativeElement, 'flex', null);
}
const shapeWidth = this.chartShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.chartShape.nativeElement.getBoundingClientRect().height;
const size = Math.min(shapeWidth, shapeHeight);
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.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">
<label class="tb-title" translate>entity.columns-to-display</label>
<mat-checkbox [(ngModel)]="column.display" *ngFor="let column of (columns | selectableColumns)"
(ngModelChange)="update()">
{{ column.title }}
<mat-checkbox
[checked]="allColumnsVisible"
[indeterminate]="someColumnsVisible"
(change)="toggleAllColumns($event)">
{{'entity.show-all-columns' | translate}}
</mat-checkbox>
<mat-divider/>
@for (column of columns; track column.def) {
<mat-checkbox [(ngModel)]="column.display" (ngModelChange)="update()">
{{ column.title }}
</mat-checkbox>
}
</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 { isDefinedAndNotNull } from '@core/utils';
import { SelectableColumnsPipe } from '@shared/public-api';
import { DisplayColumn } from '@home/components/widget/lib/table-widget.models';
export const DISPLAY_COLUMNS_PANEL_DATA = new InjectionToken<any>('DisplayColumnsPanelData');
@ -33,8 +35,28 @@ export class DisplayColumnsPanelComponent {
columns: DisplayColumn[];
constructor(@Inject(DISPLAY_COLUMNS_PANEL_DATA) public data: DisplayColumnsPanelData) {
this.columns = this.data.columns;
constructor(@Inject(DISPLAY_COLUMNS_PANEL_DATA) public data: DisplayColumnsPanelData,
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() {

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 { ImagePipe } from '@shared/pipe/image.pipe';
export type ShapePatternStorage = {[id: string]: {
pattern: L.TB.Pattern;
refCount: number;
}};
export type ShapePatternStorage = {
[id: string]: {
pattern: L.TB.Pattern;
refCount: number;
}
};
interface ShapePatternInfo {
type: ShapeFillType;
@ -311,6 +313,23 @@ export abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L exte
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> {
return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe(
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>};
interface PointItem {
export interface PointItem {
point: L.CircleMarker;
tooltip?: L.Popup;
}

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

@ -24,7 +24,11 @@ import {
TbPolygonCoordinate,
TbPolygonCoordinates,
TbPolygonRawCoordinate,
TbPolygonRawCoordinates
TbPolygonRawCoordinates,
TbPolylineCoordinate,
TbPolylineCoordinates,
TbPolylineRawCoordinate,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common';
@ -179,5 +183,29 @@ export class TbGeoMap extends TbMap<GeoMapSettings> {
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,
loadImageWithAspect,
MapZoomAction,
TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates
TbCircleData,
TbPolygonCoordinate,
TbPolygonCoordinates,
TbPolygonRawCoordinate,
TbPolygonRawCoordinates, TbPolylineCoordinate, TbPolylineCoordinates, TbPolylineRawCoordinate,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models';
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 {
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 {
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>');

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

@ -27,7 +27,9 @@ import {
TbCircleData,
TbMapDatasource,
TbPolygonCoordinates,
TbPolygonRawCoordinates
TbPolygonRawCoordinates,
TbPolylineCoordinates,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models';
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 { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
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 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> {
@ -124,6 +127,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected addRectangleButton: L.TB.ToolbarButton;
protected addPolygonButton: L.TB.ToolbarButton;
protected addCircleButton: L.TB.ToolbarButton;
protected addPolylineButton: L.TB.ToolbarButton;
protected timeLineComponentRef: ComponentRef<MapTimelinePanelComponent>;
protected timeLineComponent: MapTimelinePanelComponent;
@ -132,6 +136,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected addMarkerDataLayers: TbLatestMapDataLayer<any>[];
protected addPolygonDataLayers: TbLatestMapDataLayer<any>[];
protected addCircleDataLayers: TbLatestMapDataLayer<any>[];
protected addPolylineDataLayers: TbLatestMapDataLayer<any>[];
protected shapePatternStorage: ShapePatternStorage = {};
@ -274,6 +279,11 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.dataLayers.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) {
const tripsDataLayers = this.settings.trips.map(settings => new TbTripsDataLayer(this, settings));
this.dataLayers.push(...tripsDataLayers);
@ -319,7 +329,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
let datasources: TbMapDatasource[];
for (const layerType of mapDataLayerTypes) {
const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType)
.map(dl => dl.getDataSources()).flat();
.map(dl => dl.getDataSources()).flat();
if (!datasources) {
datasources = typeDatasources;
} else {
@ -419,88 +429,100 @@ export abstract class TbMap<S extends BaseMapSettings> {
private setupEditMode() {
this.editToolbar = L.TB.bottomToolbar({
mapElement: $(this.mapElement),
closeTitle: this.ctx.translate.instant('action.cancel'),
onClose: () => {
return this.deselectItem(true);
}
});
this.map.on('click', () => {
this.deselectItem();
});
if (this.latestDataLayers.some(dl => dl.isEditable())) {
this.map.pm.setGlobalOptions({ snappable: false });
this.map.pm.applyGlobalOptions();
}
const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled());
const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length;
const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled());
if (showDragModeButton || addSupportedDataLayers.length) {
const drawToolbar = L.TB.toolbar({
position: this.settings.controlsPosition
}).addTo(this.map);
if (showDragModeButton) {
this.dragModeButton = drawToolbar.toolbarButton({
id: 'dragMode',
title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'),
iconClass: 'tb-drag-mode',
click: (e, button) => {
this.toggleDragMode(e, button);
}
});
}
this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers');
if (this.addMarkerDataLayers.length) {
this.addMarkerButton = drawToolbar.toolbarButton({
id: 'addMarker',
title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'),
iconClass: 'tb-place-marker',
click: (e, button) => {
this.placeMarker(e, button);
}
});
this.addMarkerButton.setDisabled(true);
this.setPlaceMarkerStyle();
}
this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons');
if (this.addPolygonDataLayers.length) {
this.addRectangleButton = drawToolbar.toolbarButton({
id: 'addRectangle',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'),
iconClass: 'tb-draw-rectangle',
click: (e, button) => {
this.drawRectangle(e, button);
}
});
this.addRectangleButton.setDisabled(true);
this.addPolygonButton = drawToolbar.toolbarButton({
id: 'addPolygon',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'),
iconClass: 'tb-draw-polygon',
click: (e, button) => {
this.drawPolygon(e, button);
}
});
this.addPolygonButton.setDisabled(true);
}
this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles');
if (this.addCircleDataLayers.length) {
this.addCircleButton = drawToolbar.toolbarButton({
id: 'addCircle',
title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'),
iconClass: 'tb-draw-circle',
click: (e, button) => {
this.drawCircle(e, button);
}
});
this.addCircleButton.setDisabled(true);
}
}
this.editToolbar = L.TB.bottomToolbar({
mapElement: $(this.mapElement),
closeTitle: this.ctx.translate.instant('action.cancel'),
onClose: () => {
return this.deselectItem(true);
}
});
this.map.on('click', () => {
this.deselectItem();
});
if (this.latestDataLayers.some(dl => dl.isEditable())) {
this.map.pm.setGlobalOptions({snappable: false});
this.map.pm.applyGlobalOptions();
}
const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled());
const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length;
const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled());
if (showDragModeButton || addSupportedDataLayers.length) {
const drawToolbar = L.TB.toolbar({
position: this.settings.controlsPosition
}).addTo(this.map);
if (showDragModeButton) {
this.dragModeButton = drawToolbar.toolbarButton({
id: 'dragMode',
title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'),
iconClass: 'tb-drag-mode',
click: (e, button) => {
this.toggleDragMode(e, button);
}
});
}
this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers');
if (this.addMarkerDataLayers.length) {
this.addMarkerButton = drawToolbar.toolbarButton({
id: 'addMarker',
title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'),
iconClass: 'tb-place-marker',
click: (e, button) => {
this.placeMarker(e, button);
}
});
this.addMarkerButton.setDisabled(true);
this.setPlaceMarkerStyle();
}
this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons');
if (this.addPolygonDataLayers.length) {
this.addRectangleButton = drawToolbar.toolbarButton({
id: 'addRectangle',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'),
iconClass: 'tb-draw-rectangle',
click: (e, button) => {
this.drawRectangle(e, button);
}
});
this.addRectangleButton.setDisabled(true);
this.addPolygonButton = drawToolbar.toolbarButton({
id: 'addPolygon',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'),
iconClass: 'tb-draw-polygon',
click: (e, button) => {
this.drawPolygon(e, button);
}
});
this.addPolygonButton.setDisabled(true);
}
this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles');
if (this.addCircleDataLayers.length) {
this.addCircleButton = drawToolbar.toolbarButton({
id: 'addCircle',
title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'),
iconClass: 'tb-draw-circle',
click: (e, button) => {
this.drawCircle(e, button);
}
});
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 {
@ -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[],
prepareDrawMode: (entity: UnplacedMapDataItem) => void): void {
if (this.isPlacingItem) {
@ -678,6 +707,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
case MapItemType.circle:
this.createCircle(actionData);
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) {
const actionId = 'id' in actionData.action ? actionData.action.id : 'map-button';
if (this.createMapItemActionId === actionId) {
@ -765,7 +808,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
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) {
case MapItemType.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 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();
}
private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle', tooltipsTranslation: Record<string, string>) {
this.map.pm.setLang('en', { tooltips: tooltipsTranslation }, 'en');
private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle' | 'Line', tooltipsTranslation: Record<string, string>) {
this.map.pm.setLang('en', {tooltips: tooltipsTranslation}, 'en');
this.map.pm.enableDraw(shape);
// @ts-ignore
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 = [];
}
$(root)
.find('a[role="button"]:not(.leaflet-pm-action)')
.each((_index, element) => {
let title: string;
if (element.title) {
title = element.title;
$(element).removeAttr('title');
} else if (element.parentElement.title) {
title = element.parentElement.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);
.find('a[role="button"]:not(.leaflet-pm-action)')
.each((_index, element) => {
let title: string;
if (element.title) {
title = element.title;
$(element).removeAttr('title');
} else if (element.parentElement.title) {
title = element.parentElement.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);
}
});
});
});
});
}
@ -899,6 +951,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.updateTripsAnchors();
this.updateBounds();
this.updateEditButtonsStates();
}
private updateTrips(subscription: IWidgetSubscription) {
@ -943,6 +996,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
private updateTripsAppearance() {
this.tripDataLayers.forEach(dl => dl.updateAppearance());
}
private updateTripsTime() {
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)
&& !mapBounds.contains(bounds)
)
)
{
) {
this.bounds = bounds;
if (!this.ignoreUpdateBounds && !this.isPlacingItem) {
this.fitBounds(bounds);
@ -1023,6 +1076,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.addCircleButton && this.addCircleButton !== this.currentEditButton) {
this.addCircleButton.setDisabled(true);
}
if (this.addPolylineButton && this.addPolylineButton !== this.currentEditButton) {
this.addPolylineButton.setDisabled(true);
}
this.customActionsToolbar?.setDisabled(true);
} else {
if (this.dragModeButton) {
@ -1040,6 +1096,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.addCircleButton) {
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);
}
}
@ -1150,7 +1209,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
$event.preventDefault();
$event.stopPropagation();
}
const { entityId, entityName, entityLabel, entityType } = data.$datasource;
const {entityId, entityName, entityLabel, entityType} = data.$datasource;
this.ctx.actionsApi.handleWidgetAction($event, action, {
entityType,
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;
let dataKeys = datasource.dataKeys;
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 coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates;
public abstract polylineDataToCoordinates(coordinates: TbPolylineRawCoordinates): TbPolylineRawCoordinates;
public abstract coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates;
public abstract circleDataToCoordinates(circle: TbCircleData): 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="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>
<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>
<button mat-raised-button color="primary" (click)="takePhoto()" *ngIf="!textMessage">
{{ "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,
Inject,
Input,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ImageService } from '@app/core/public-api';
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 { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
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 { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable } from 'rxjs';
import { isString } from '@core/utils';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
import { map, Observable, of, switchMap } from 'rxjs';
interface PhotoCameraInputWidgetSettings {
widgetTitle: string;
saveToGallery: boolean;
usePublicGalleryLink: boolean;
imageQuality: number;
imageFormat: string;
maxWidth: number;
@ -59,9 +60,7 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
constructor(@Inject(WINDOW) private window: Window,
protected store: Store<AppState>,
private elementRef: ElementRef,
private ngZone: NgZone,
private overlay: Overlay,
private imageService: ImageService,
private utils: UtilsService,
private attributeService: AttributeService,
private sanitizer: DomSanitizer
@ -119,6 +118,9 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
lastPhoto: SafeUrl;
datasourceDetected = false;
private mimeType: string;
private quality: number;
private static hasGetUserMedia(): boolean {
return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia);
}
@ -160,6 +162,9 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
this.dataKeyDetected = true;
}
}
this.mimeType = this.settings.imageFormat || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
this.quality = this.settings.imageQuality || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.detectAvailableDevices();
}
@ -169,8 +174,8 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
private updateWidgetData(data: Array<DatasourceData>) {
const keyData = data[0].data;
if (keyData?.length && isString(keyData[0][1]) && keyData[0][1].startsWith('data:image/')) {
this.lastPhoto = this.sanitizer.bypassSecurityTrustUrl(keyData[0][1]);
if (keyData?.length && isString(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() {
this.updatePhoto = true;
let task: Observable<any>;
const entityId: EntityId = {
entityType: this.datasource.entityType,
id: this.datasource.entityId
};
const saveData = [{
key: this.datasource.dataKeys[0].name,
value: this.previewPhoto
}];
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
task = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData);
} else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
task = this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData);
}
task.subscribe(() => {
this.isPreviewPhoto = false;
this.updatePhoto = false;
this.closeCamera();
}, () => {
this.updatePhoto = false;
const uploadImage$ = this.settings.saveToGallery ? this.saveImageToGallery() : of(this.previewPhoto);
uploadImage$.pipe(
switchMap((image) => {
const saveData = [{
key: this.datasource.dataKeys[0].name,
value: image
}];
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
return this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData);
} else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
return this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData);
}
})
).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.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight);
const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
const quality: number = this.settings.imageQuality ? this.settings.imageQuality : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality);
this.previewPhoto = this.canvasElement.toDataURL(this.mimeType, this.quality);
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) {
if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
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">
{{ 'widgets.table.use-entity-label-tab-name' | translate }}
</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 class="tb-form-panel tb-slide-toggle">
<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 { AppState } from '@core/core.state';
import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models';
import { TabSortKey } from '@app/modules/home/components/widget/lib/timeseries-table-widget.component'
@Component({
selector: 'tb-timeseries-table-widget-settings',
@ -28,6 +29,8 @@ import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widge
})
export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent {
TabSortKey = TabSortKey;
timeseriesTableWidgetSettingsForm: UntypedFormGroup;
pageStepSizeValues = [];
@ -58,12 +61,14 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
hideEmptyLines: false,
disableStickyHeader: false,
useRowStyleFunction: false,
rowStyleFunction: ''
rowStyleFunction: '',
tabSortKey: TabSortKey.TIMESTAMP
};
}
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
settings.pageStepIncrement = settings.pageStepIncrement ?? settings.defaultPageSize;
settings.tabSortKey = settings.tabSortKey ?? TabSortKey.TIMESTAMP;
this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement);
return settings;
}
@ -93,7 +98,8 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
hideEmptyLines: [settings.hideEmptyLines, []],
disableStickyHeader: [settings.disableStickyHeader, []],
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>
</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>
</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();
attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x});
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});
} else if (mapItemType === 'Circle') {
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')"
formControlName="circleKey">
</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
formControlName="additionalDataKeys"
appearance="outline"
@ -443,6 +463,8 @@
<tb-shape-fill-image-settings formControlName="fillImage"></tb-shape-fill-image-settings>
</div>
</div>
</ng-container>
<ng-container *ngIf="['polygons', 'circles', 'polylines'].includes(dataLayerType)">
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.stroke</div>
<div class="flex flex-row items-center gap-2">
@ -455,7 +477,9 @@
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').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>
</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,
pathDecoratorSymbols,
pathDecoratorSymbolTranslationMap,
PolygonsDataLayerSettings,
PolygonsDataLayerSettings, PolylinesDataLayerSettings,
ShapeDataLayerSettings, ShapeFillType,
TripsDataLayerSettings,
updateDataKeyToNewDsType
@ -113,6 +113,8 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
return 'widget/lib/map/polygon_label_fn';
case 'circles':
return 'widget/lib/map/circle_label_fn';
case 'polylines':
return 'widget/lib/map/polyline_label_fn';
default:
return 'widget/lib/map/label_fn';
}
@ -128,6 +130,8 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
return 'widget/lib/map/polygon_tooltip_fn';
case 'circles':
return 'widget/lib/map/circle_tooltip_fn';
case 'polylines':
return 'widget/lib/map/polyline_tooltip_fn';
default:
return 'widget/lib/map/tooltip_fn';
}
@ -138,18 +142,21 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
const editEnabledActions: DataLayerEditAction[] =
this.dataLayerFormGroup.get('edit').get('enabledActions').value;
if (editEnabledActions && editEnabledActions.length) {
switch (this.dataLayerType) {
case 'markers':
const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value;
const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value;
return (xKey?.type === DataKeyType.attribute || yKey?.type === DataKeyType.attribute);
case 'polygons':
const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value;
return polygonKey?.type === DataKeyType.attribute;
case 'circles':
const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value;
return circleKey?.type === DataKeyType.attribute;
}
switch (this.dataLayerType) {
case 'markers':
const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value;
const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value;
return (xKey?.type === DataKeyType.attribute || yKey?.type === DataKeyType.attribute);
case 'polygons':
const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value;
return polygonKey?.type === DataKeyType.attribute;
case 'circles':
const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value;
return circleKey?.type === DataKeyType.attribute;
case 'polylines':
const polylineKey: DataKey = this.dataLayerFormGroup.get('polylineKey').value;
return polylineKey?.type === DataKeyType.attribute;
}
} else {
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('pointTooltip', this.fb.control(tripsDataLayer.pointTooltip));
merge(this.dataLayerFormGroup.get('showMarker').valueChanges,
this.dataLayerFormGroup.get('markerType').valueChanges,
this.dataLayerFormGroup.get('rotateMarker').valueChanges,
this.dataLayerFormGroup.get('showPath').valueChanges,
this.dataLayerFormGroup.get('usePathDecorator').valueChanges,
this.dataLayerFormGroup.get('showPoints').valueChanges).pipe(
this.dataLayerFormGroup.get('markerType').valueChanges,
this.dataLayerFormGroup.get('rotateMarker').valueChanges,
this.dataLayerFormGroup.get('showPath').valueChanges,
this.dataLayerFormGroup.get('usePathDecorator').valueChanges,
this.dataLayerFormGroup.get('showPoints').valueChanges).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() =>
this.updateValidators()
@ -291,6 +298,15 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
this.updateValidators()
);
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(
takeUntilDestroyed(this.destroyRef)
@ -325,6 +341,12 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
this.dataLayerFormGroup.get('circleKey').patchValue(circleKey, {emitEvent: false});
}
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;
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;
this.context.editKey(targetDataKey,
this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value,
@ -511,6 +533,12 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
dataKeys.push(circleKey);
}
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;
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')"
formControlName="circleKey">
</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">
<button type="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,
MapType,
MarkersDataLayerSettings,
PolygonsDataLayerSettings,
PolygonsDataLayerSettings, PolylinesDataLayerSettings,
TripsDataLayerSettings,
updateDataKeyToNewDsType
} 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.dataLayerFormGroup.addControl('circleKey', this.fb.control(null, Validators.required));
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(
takeUntilDestroyed(this.destroyRef)
@ -225,12 +230,20 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
}, {emitEvent: false}
);
break;
case 'polylines':
const polylinesDataLayer = value as PolylinesDataLayerSettings;
this.dataLayerFormGroup.patchValue(
{
polylineKey: polylinesDataLayer?.polylineKey
}, {emitEvent: false}
);
break;
}
this.updateValidators();
this.cd.markForCheck();
}
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') {
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey' | 'polylineKey') {
const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value;
this.context.editKey(targetDataKey,
this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value,
@ -296,6 +309,13 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
updateModel = true;
}
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();
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 *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 === '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>
<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.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles';
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({
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="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>
//todo translation
<tb-toggle-option value="polylines" [error]="mapSettingsFormGroup.get('polylines').invalid ? ('common.required-fields' | translate) : null"> Polylines </tb-toggle-option>
</tb-toggle-select>
</div>
<tb-map-data-layers *ngIf="trip"
@ -68,6 +70,11 @@
dataLayerType="circles"
[context]="context"
[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 class="tb-form-panel">
<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, []],
polygons: [null, []],
circles: [null, []],
polylines: [null, []],
additionalDataSources: [null, []],
controlsPosition: [null, []],
zoomActions: [null, []],
@ -180,7 +181,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
});
merge(this.mapSettingsFormGroup.get('markers').valueChanges,
this.mapSettingsFormGroup.get('polygons').valueChanges,
this.mapSettingsFormGroup.get('circles').valueChanges
this.mapSettingsFormGroup.get('circles').valueChanges,
this.mapSettingsFormGroup.get('polylines').valueChanges
).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
@ -281,6 +283,10 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value;
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) {
const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value;
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.
-->
<section class="tb-widget-settings" [formGroup]="photoCameraInputWidgetSettingsForm">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.general-settings</legend>
<mat-form-field class="mat-block flex-1">
<mat-label translate>widgets.input-widgets.widget-title</mat-label>
<input matInput formControlName="widgetTitle">
</mat-form-field>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.image-settings</legend>
<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="mat-block flex-1">
<mat-label translate>widgets.input-widgets.image-format</mat-label>
<section class="flex flex-col gap-4" [formGroup]="photoCameraInputWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.input-widgets.general-settings</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>widgets.input-widgets.widget-title</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="widgetTitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.input-widgets.save-image</div>
<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-option [value]="'image/jpeg'">
{{ 'widgets.input-widgets.image-format-jpeg' | translate }}
@ -40,20 +53,33 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block flex-1">
<mat-label translate>widgets.input-widgets.image-quality</mat-label>
<input matInput type="number" min="0" max="1" formControlName="imageQuality">
</mat-form-field>
</section>
<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="mat-block flex-1">
<mat-label translate>widgets.input-widgets.max-image-width</mat-label>
<input matInput type="number" min="1" formControlName="maxWidth">
</mat-form-field>
<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">
</mat-form-field>
</section>
</fieldset>
</div>
@if (photoCameraInputWidgetSettingsForm.get('imageFormat').value !== 'image/png') {
<div class="tb-form-row space-between">
<div translate>widgets.input-widgets.image-quality</div>
<mat-form-field class="number" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" max="100" formControlName="imageQuality">
<span matSuffix>%</span>
</mat-form-field>
</div>
}
<div class="tb-form-row space-between column-xs">
<div class="fixed-title-width !min-w-30">Size</div>
<div class="flex flex-row items-center justify-star gap-2">
<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>

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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
@Component({
selector: 'tb-photo-camera-input-widget-settings',
@ -42,6 +42,8 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo
return {
widgetTitle: '',
saveToGallery: false,
usePublicGalleryLink: true,
imageFormat: 'image/png',
imageQuality: 0.92,
maxWidth: 640,
@ -57,11 +59,28 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo
widgetTitle: [settings.widgetTitle, []],
// Image settings
saveToGallery: [settings.saveToGallery],
usePublicGalleryLink: [settings.usePublicGalleryLink],
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)]],
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 mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString();
const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
const cssString =
'.mat-mdc-input-element::placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-input-element::-moz-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-input-element::-webkit-input-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-input-element:-ms-input-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'mat-toolbar.mat-mdc-table-toolbar {\n' +
'color: ' + mdDark + ';\n' +
'}\n' +
'mat-toolbar.mat-mdc-table-toolbar:not([color="primary"]) button.mat-mdc-icon-button mat-icon {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-tab .mdc-tab__text-label {\n' +
'color: ' + mdDark + ';\n' +
'}\n' +
'.mat-mdc-tab-header-pagination-chevron {\n' +
'border-color: ' + mdDark + ';\n' +
'}\n' +
'.mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron {\n' +
'border-color: ' + mdDarkDisabled2 + ';\n' +
'}\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' +
'}';
const cssString = ` {
--mat-toolbar-container-text-color: ${mdDark};
--mat-tab-header-active-label-text-color: ${mdDark};
--mat-tab-header-inactive-label-text-color: ${mdDark};
--mat-tab-header-pagination-icon-color: ${mdDark};
--mat-tab-header-pagination-disabled-icon-color: ${mdDarkDisabled2};
--mat-table-header-headline-color: ${mdDarkSecondary};
--mat-table-row-item-label-text-color: ${mdDark};
--mat-icon-color: ${mdDarkSecondary};
--mdc-icon-button-disabled-icon-color: ${mdDarkDisabled};
--mat-divider-color: ${mdDarkDivider};
--mat-paginator-container-text-color: ${mdDarkSecondary};
--mdc-icon-button-icon-color: ${mdDarkSecondary};
--mat-paginator-enabled-icon-color: ${mdDarkSecondary};
--mat-paginator-disabled-icon-color: ${mdDarkDisabled};
--mat-select-enabled-trigger-text-color: ${mdDarkSecondary};
--mat-select-disabled-trigger-text-color: ${mdDarkDisabled};
--mat-table-row-item-outline-color: ${mdDarkDivider};
--mdc-checkbox-unselected-focus-icon-color: ${mdDarkSecondary};
--tb-orig-background-color: ${origBackgroundColor};
--tb-current-entity-color: ${currentEntityColor};
--tb-current-entity-sticky-color: ${currentEntityStickyColor};
--tb-hover-color: ${hoverColor};
--tb-hover-sticky-color: ${hoverStickyColor};
--tb-selected-color: ${selectedColor};
--tb-selected-sticky-color: ${selectedStickyColor};
}
`;
return cssString;
}

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

@ -44,7 +44,53 @@
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) {

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

@ -41,7 +41,7 @@
class="flex-1"
mat-stretch-tabs="false"
[(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)">
<div class="table-container flex-1">
<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 { 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 {
showTimestamp: boolean;
showMilliseconds: boolean;
hideEmptyLines: boolean;
dateFormat: DateFormatSettings;
tabSortKey: TabSortKey;
}
interface TimeseriesWidgetLatestDataKeySettings extends TableWidgetDataKeySettings {
@ -152,6 +159,7 @@ interface TimeseriesTableSource {
timeseriesDatasource: TimeseriesDatasource;
header: TimeseriesHeader[];
rowDataTemplate: {[key: string]: any};
displayName: string;
}
@Component({
@ -392,12 +400,33 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.updateDatasources();
}
public getTabLabel(source: TimeseriesTableSource){
if (this.useEntityLabel) {
return source.datasource.entityLabel || source.datasource.entityName;
} else {
return source.datasource.entityName;
private getTabLabel(source: Datasource, entityLabelCache:Map<string,string>):string {
if (entityLabelCache.has(source.entityId)) {
return entityLabelCache.get(source.entityId);
}
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() {
@ -405,9 +434,11 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.sourceIndex = 0;
let keyOffset = 0;
let latestKeyOffset = 0;
const entityLabelCache = new Map<string,string>();
const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
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 source = {} as TimeseriesTableSource;
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.rowDataTemplate = {};
source.rowDataTemplate.Timestamp = null;
source.displayName = entityLabelCache.get(datasource.entityId);
if (this.showTimestamp) {
source.displayedColumns.push('0');
}

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

@ -58,12 +58,14 @@
[enableFlagsSelect]="true"
formControlName="phone">
</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-select formControlName="language">
<mat-option *ngFor="let lang of languageList" [value]="lang">
{{ lang ? ('language.locales.' + lang | translate) : ''}}
</mat-option>
<mat-select formControlName="language"
[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">

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 { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions';
import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
@ -52,7 +51,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
private route: ActivatedRoute,
private userService: UserService,
private authService: AuthService,
private translate: TranslateService,
private unitService: UnitService,
private fb: UntypedFormBuilder) {
super(store);
@ -82,9 +80,13 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
if (!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.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)) {
this.user.additionalInfo.unitSystem = this.profile.get('unitSystem').value;
} else {
@ -105,7 +107,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
id: user.id,
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.authService.refreshJwtToken(false);
}
@ -115,7 +117,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
private userLoaded(user: User) {
this.user = user;
this.profile.reset(user);
let lang;
let lang: string = null;
let homeDashboardId;
let homeDashboardHideToolbar = true;
let unitSystem: UnitSystem = null;
@ -131,9 +133,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
unitSystem = user.additionalInfo.unitSystem;
}
}
if (!lang) {
lang = this.translate.currentLang;
}
this.profile.get('language').setValue(lang);
this.profile.get('unitSystem').setValue(unitSystem);
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 mat-dialog-content>
<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-select [ngModelOptions]="{standalone: true}" [(ngModel)]="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.tenantId = new TenantId(this.data.tenantId);
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;
this.userService.saveUser(this.user, sendActivationEmail).subscribe(
(user) => {

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

@ -74,7 +74,7 @@
<div class="mat-padding flex flex-col">
<form [formGroup]="entityForm">
<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>
<input matInput type="email" formControlName="email" required>
<mat-error *ngIf="entityForm.get('email').hasError('email')">
@ -84,30 +84,52 @@
{{ 'user.email-required' | translate }}
</mat-error>
</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>
<input matInput formControlName="firstName">
</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>
<input matInput formControlName="lastName">
</mat-form-field>
<tb-phone-input [required]="false"
label="{{ 'contact.phone' | translate }}"
[enableFlagsSelect]="true"
appearance="outline"
formControlName="phone">
</tb-phone-input>
<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>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
<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
class="flex-1"
label="{{ 'user.default-dashboard' | translate }}"
formControlName="defaultDashboardId"
appearance="outline"
[dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
[tenantId]="entity?.tenantId?.id"
[customerId]="entity?.customerId?.id"
@ -117,11 +139,12 @@
{{ 'user.always-fullscreen' | translate }}
</mat-checkbox>
</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
class="flex-1"
label="{{ 'dashboard.home-dashboard' | translate }}"
formControlName="homeDashboardId"
appearance="outline"
[dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
[tenantId]="entity?.tenantId?.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 { ActionNotificationShow } from '@app/core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { environment as env } from '@env/environment';
import { UnitSystems } from '@shared/models/unit.models';
@Component({
selector: 'tb-user',
@ -36,6 +38,8 @@ import { TranslateService } from '@ngx-translate/core';
export class UserComponent extends EntityComponent<User> {
authority = Authority;
languageList = env.supportedLangs;
UnitSystems = UnitSystems;
loginAsUserEnabled$ = this.store.pipe(
select(selectAuth),
@ -77,6 +81,8 @@ export class UserComponent extends EntityComponent<User> {
additionalInfo: this.fb.group(
{
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],
defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false],
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({phone: entity.phone});
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:
{defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}});
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.customerId = new CustomerId(this.customerId);
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);
}

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

@ -30,7 +30,7 @@
</tb-copy-button>
</mat-form-field>
<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>
</mat-form-field>
</div>

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

@ -659,7 +659,8 @@ export enum MapItemType {
marker = 'marker',
polygon = 'polygon',
rectangle = 'rectangle',
circle = 'circle'
circle = 'circle',
polyline = 'polyline'
}
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.rectangle, 'widget-action.map-item.rectangle' ],
[ MapItemType.circle, 'widget-action.map-item.circle' ],
[ MapItemType.polyline, 'widget-action.map-item.polyline' ]
]
)
@ -826,6 +828,8 @@ export interface MapItemTooltips {
finishRect?: string;
startCircle?: string;
finishCircle?: string;
startPolyline?: string;
finishPolyline?: string;
}
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',
finishRect: 'widgets.maps.data-layer.polygon.finish-rectangle-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 {
@ -1145,5 +1151,4 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
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,
dataLayerType: MapDataLayerType): DataKey[] => {
dataLayerType: MapDataLayerType): DataKey[] => {
const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : [];
switch (dataLayerType) {
case 'trips':
@ -96,6 +96,9 @@ const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
case 'circles':
dataKeys.push((settings as CirclesDataLayerSettings).circleKey);
break;
case 'polylines':
dataKeys.push((settings as PolylinesDataLayerSettings).polylineKey);
break;
}
return dataKeys;
};
@ -159,6 +162,7 @@ export interface DataLayerEditSettings {
snappable: boolean;
}
export interface MapDataLayerSettings extends MapDataSourceSettings {
additionalDataSources?: MapDataSourceSettings[];
additionalDataKeys?: DataKey[];
@ -166,7 +170,7 @@ export interface MapDataLayerSettings extends MapDataSourceSettings {
tooltip: DataLayerTooltipSettings;
click: WidgetAction;
groups?: string[];
edit: DataLayerEditSettings;
edit: DataLayerEditSettings;
}
export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataLayerSettings> => ({
@ -182,7 +186,7 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataL
type: DataLayerPatternType.pattern,
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>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,
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 => {
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':
const markersDataLayer = dataLayer as MarkersDataLayerSettings;
if (!markersDataLayer.xKey?.type || !markersDataLayer.xKey?.name ||
!markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) {
!markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) {
return false;
}
break;
@ -232,6 +236,12 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData
return false;
}
break;
case 'polylines':
const polylinesDataLayer = dataLayer as PolylinesDataLayerSettings;
if (!polylinesDataLayer.polylineKey?.type || !polylinesDataLayer.polylineKey?.name) {
return false;
}
break;
case 'circles':
const circlesDataLayer = dataLayer as CirclesDataLayerSettings;
if (!circlesDataLayer.circleKey?.type || !circlesDataLayer.circleKey?.name) {
@ -300,6 +310,7 @@ export interface MarkerIconSettings extends BaseMarkerShapeSettings {
iconContainer?: MarkerIconContainer;
icon: string;
}
export interface MarkerClusteringSettings {
enable: boolean;
zoomOnClick: boolean;
@ -600,8 +611,11 @@ export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial<
color: '#3388ff',
},
strokeWeight: 3
} 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>)
} 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>)
export interface CirclesDataLayerSettings extends ShapeDataLayerSettings {
circleKey: DataKey;
@ -651,8 +665,45 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial<C
color: '#3388ff',
},
strokeWeight: 3
} 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>)
} 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>)
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 => {
switch (dataLayerType) {
@ -664,6 +715,8 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map
return defaultPolygonsDataLayerSettings(mapType, functionsOnly);
case 'circles':
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;
case 'circles':
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 => {
const dataSource: AdditionalMapDataSourceSettings = control.value;
if (!additionalMapDataSourceValid(dataSource)) {
return {
dataSource: true
};
}
return null;
const dataSource: AdditionalMapDataSourceSettings = control.value;
if (!additionalMapDataSourceValid(dataSource)) {
return {
dataSource: true
};
}
return null;
};
export const defaultAdditionalMapDataSourceSettings = (functionsOnly = false): AdditionalMapDataSourceSettings => {
@ -815,6 +870,7 @@ export interface BaseMapSettings {
markers: MarkersDataLayerSettings[];
polygons: PolygonsDataLayerSettings[];
circles: CirclesDataLayerSettings[];
polylines: PolylinesDataLayerSettings[];
additionalDataSources: AdditionalMapDataSourceSettings[];
controlsPosition: MapControlsPosition;
zoomActions: MapZoomAction[];
@ -839,6 +895,7 @@ export const defaultBaseMapSettings: BaseMapSettings = {
markers: [],
polygons: [],
circles: [],
polylines: [],
additionalDataSources: [],
controlsPosition: MapControlsPosition.topleft,
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 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 {
latitude: number;
longitude: number;

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

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

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

@ -98,7 +98,8 @@
"settings": {
"showTimestamp": true,
"displayPagination": true,
"defaultPageSize": 10
"defaultPageSize": 10,
"tabSortKey": "NAME_ASC"
},
"title": "{i18n:api-usage.exceptions}",
"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();
attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x});
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});
} else if (mapItemType === 'Circle') {
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:
<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>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>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>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/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>
Note: The coordinates will be automatically converted according to the selected map type.
</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*
A JavaScript function used to compute color of the trip path point.
A JavaScript function used to compute color of the path point.
**Parameters:**
@ -15,7 +15,7 @@ A JavaScript function used to compute color of the trip path point.
**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.

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

@ -5,7 +5,7 @@
*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:**

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",
"no-entities-prompt": "No entities found",
"no-data": "No data to display",
"show-all-columns":"Show All",
"columns-to-display": "Columns to Display",
"type-api-usage-state": "API Usage State",
"type-edge": "Edge",
@ -7135,7 +7136,8 @@
"marker": "Marker",
"polygon": "Polygon",
"rectangle": "Rectangle",
"circle": "Circle"
"circle": "Circle",
"polyline": "Polyline"
},
"place-map-item": "Place map item",
"map-item-tooltip": {
@ -7147,7 +7149,9 @@
"continue-draw-polygon": "Continue draw polygon",
"finish-draw-polygon": "Finish draw polygon",
"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": {
@ -8163,14 +8167,14 @@
"attribute-scope-server": "Server attribute",
"attribute-scope-shared": "Shared attribute",
"value-required": "Value required",
"image-settings": "Image settings",
"image-settings": "Image output settings",
"image-format": "Image format",
"image-format-jpeg": "JPEG",
"image-format-png": "PNG",
"image-format-webp": "WEBP",
"image-quality": "Image quality that use lossy compression such as jpeg and webp",
"max-image-width": "Maximum image width",
"max-image-height": "Maximum image height",
"image-quality": "Image quality",
"max-image-width": "Max width",
"max-image-height": "Max height",
"action-buttons": "Action buttons",
"show-action-buttons": "Show action buttons",
"update-all-values": "Update all values, not only modified",
@ -8251,7 +8255,10 @@
"add-radio-option": "Add radio option",
"radio-label-position": "Label position",
"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",
"qr-code": {
@ -8546,7 +8553,8 @@
"trips": "Trips",
"markers": "Markers",
"polygons": "Polygons",
"circles": "Circles"
"circles": "Circles",
"polylines": "Polylines"
},
"data-layer": {
"source": "Source",
@ -8743,6 +8751,25 @@
"finish-circle-hint-with-entity": "Circle for '{{entityName}}': click to finish and save circle",
"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-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",
"table-tabs": "Table tabs",
"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": {
"total": "Total",
@ -9807,6 +9838,7 @@
"items-per-page-separator": "of"
},
"language": {
"auto": "Auto",
"language": "Language",
"locales": {
"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 {
button.mat-mdc-icon-button {
.mat-icon {
color: rgba(0, 0, 0, .54);
color:var(--mat-icon-color, rgba(0, 0, 0, .54));
}
&[disabled][disabled] {
.mat-icon {
@ -760,11 +760,11 @@ pre.tb-highlight {
transition: background-color .2s;
&:hover:not(.tb-current-entity) {
background-color: #f4f4f4;
background-color: var(--tb-hover-color, #f4f4f4);
}
&.tb-current-entity {
background-color: #e9e9e9;
background-color: var(--tb-current-entity-color, #e9e9e9);
}
&.tb-pointer {
@ -816,7 +816,7 @@ pre.tb-highlight {
vertical-align: middle;
border-width: 0;
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;
text-overflow: ellipsis;
touch-action: auto !important;

Loading…
Cancel
Save