diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts index 0cabfe9fef..a730c74adf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts @@ -15,8 +15,8 @@ /// import { + BaseMarkerShapeSettings, CirclesDataLayerSettings, - createColorMarkerURI, DataLayerColorSettings, DataLayerColorType, DataLayerPatternSettings, @@ -34,18 +34,22 @@ import { MapStringFunction, MapType, MarkerIconInfo, + MarkerIconSettings, MarkerImageFunction, MarkerImageInfo, MarkerImageSettings, MarkerImageType, MarkersDataLayerSettings, + MarkerShapeSettings, MarkerType, - PolygonsDataLayerSettings, processTooltipTemplate, ShapeDataLayerSettings, + PolygonsDataLayerSettings, + processTooltipTemplate, + ShapeDataLayerSettings, TbCircleData, TbMapDatasource } from '@home/components/widget/lib/maps/map.models'; import { TbMap } from '@home/components/widget/lib/maps/map'; -import { Datasource, FormattedData } from '@shared/models/widget.models'; +import { FormattedData } from '@shared/models/widget.models'; import { forkJoin, Observable, of } from 'rxjs'; import { createLabelFromPattern, @@ -59,13 +63,20 @@ import { parseTbFunction, safeExecuteTbFunction } from '@core/utils'; -import L, { LatLngBounds, PathOptions } from 'leaflet'; +import L, { divIcon, LatLngBounds } from 'leaflet'; import { CompiledTbFunction } from '@shared/models/js-function.models'; -import { catchError, map } from 'rxjs/operators'; +import { catchError, map, switchMap } from 'rxjs/operators'; import tinycolor from 'tinycolor2'; import { WidgetContext } from '@home/models/widget-component.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import { + createColorMarkerIconElement, + createColorMarkerShapeURI, + MarkerShape +} from '@home/components/widget/lib/maps/marker-shape.models'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; abstract class TbDataLayerItem> { @@ -317,7 +328,7 @@ export abstract class TbMapDataLayer d.$datasource.mapDataIds.includes(this.mapDataId)); const rawItems = layerData.filter(d => this.isValidLayerData(d)); const toDelete = new Set(Array.from(this.layerItems.keys())); - rawItems.forEach((data, index) => { + rawItems.forEach((data) => { let layerItem = this.layerItems.get(data.entityId); if (layerItem) { layerItem.update(data, dsData); @@ -428,8 +439,10 @@ abstract class MarkerIconProcessor { static fromSettings(dataLayer: TbMarkersDataLayer, settings: MarkersDataLayerSettings): MarkerIconProcessor { switch (settings.markerType) { - case MarkerType.default: - return new ColorMarkerIconProcessor(dataLayer, settings.markerColor); + case MarkerType.shape: + return new ShapeMarkerIconProcessor(dataLayer, settings.markerShape); + case MarkerType.icon: + return new IconMarkerIconProcessor(dataLayer, settings.markerIcon); case MarkerType.image: return new ImageMarkerIconProcessor(dataLayer, settings.markerImage); } @@ -445,46 +458,80 @@ abstract class MarkerIconProcessor { } -class ColorMarkerIconProcessor extends MarkerIconProcessor { +abstract class BaseColorMarkerShapeProcessor extends MarkerIconProcessor { private markerColorFunction: CompiledTbFunction; private defaultMarkerIconInfo: MarkerIconInfo; - constructor(protected dataLayer: TbMarkersDataLayer, - protected settings: DataLayerColorSettings) { + protected constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: S) { super(dataLayer, settings); } public setup(): Observable { - if (this.settings.type === DataLayerColorType.function) { - return parseTbFunction(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe( + const colorSettings = this.settings.color; + if (colorSettings.type === DataLayerColorType.function) { + return parseTbFunction(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe( map((parsed) => { this.markerColorFunction = parsed; return null; }) ); } else { - const color = tinycolor(this.settings.color); - this.defaultMarkerIconInfo = this.dataLayer.createColoredMarkerIcon(color); - return of(null) + const color = tinycolor(colorSettings.color); + return this.createMarkerShape(color, this.settings.size).pipe( + map((info) => { + this.defaultMarkerIconInfo = info; + return null; + } + )); } } public createMarkerIcon(data: FormattedData, dsData: FormattedData[]): Observable { - if (this.settings.type === DataLayerColorType.function) { + const colorSettings = this.settings.color; + if (colorSettings.type === DataLayerColorType.function) { const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]); let color: tinycolor.Instance; if (isDefinedAndNotNull(functionColor)) { color = tinycolor(functionColor); } else { - color = tinycolor(this.settings.color); + color = tinycolor(colorSettings.color); } - return of(this.dataLayer.createColoredMarkerIcon(color)); + return this.createMarkerShape(color, this.settings.size); } else { return of(this.defaultMarkerIconInfo); } } + + protected abstract createMarkerShape(color: tinycolor.Instance, size: number): Observable; +} + +class ShapeMarkerIconProcessor extends BaseColorMarkerShapeProcessor { + + constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: MarkerShapeSettings) { + super(dataLayer, settings); + } + + protected createMarkerShape(color: tinycolor.Instance, size: number): Observable { + return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, size); + } + +} + +class IconMarkerIconProcessor extends BaseColorMarkerShapeProcessor { + + constructor(protected dataLayer: TbMarkersDataLayer, + protected settings: MarkerIconSettings) { + super(dataLayer, settings); + } + + protected createMarkerShape(color: tinycolor.Instance, size: number): Observable { + return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, size); + } + } class ImageMarkerIconProcessor extends MarkerIconProcessor { @@ -532,7 +579,7 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor private loadMarkerIconInfo(image: MarkerImageInfo): Observable { if (image && image.url) { return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe( - map((aspectImage) => { + switchMap((aspectImage) => { if (aspectImage?.aspect) { let width: number; let height: number; @@ -561,15 +608,15 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor size: [width, height], icon }; - return iconInfo; + return of(iconInfo); } else { return this.dataLayer.createDefaultMarkerIcon(); } }), - catchError(() => of(this.dataLayer.createDefaultMarkerIcon())) + catchError(() => this.dataLayer.createDefaultMarkerIcon()) ); } else { - return of(this.dataLayer.createDefaultMarkerIcon()); + return this.dataLayer.createDefaultMarkerIcon(); } } @@ -644,24 +691,42 @@ export class TbMarkersDataLayer extends TbMapDataLayer { + const color = this.settings.markerShape?.color?.color || '#307FE5'; + return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color)); } - public createColoredMarkerIcon(color: tinycolor.Instance): MarkerIconInfo { - return { - size: [21, 34], - icon: L.icon({ - iconUrl: createColorMarkerURI(color), - iconSize: [21, 34], - iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]], - popupAnchor: [21 * this.tooltipOffset[0], 34 * this.tooltipOffset[1]], - shadowUrl: 'assets/shadow.png', - shadowSize: [40, 37], - shadowAnchor: [12, 35] + public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, size = 34): Observable { + return createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), shape, color).pipe( + map((iconUrl) => { + return { + size: [size, size], + icon: L.icon({ + iconUrl, + iconSize: [size, size], + iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], + popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] + }) + }; }) - }; + ); + } + + public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, size = 34): Observable { + return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color).pipe( + map((element) => { + return { + size: [size, size], + icon: L.divIcon({ + html: element.outerHTML, + className: 'tb-marker-div-icon', + iconSize: [size, size], + iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], + popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] + }) + }; + }) + ); } public extractLocation(data: FormattedData): L.LatLng { @@ -757,7 +822,7 @@ abstract class TbShapesDataLayer, dsData: FormattedData[]): L.PathOptions { const fill = this.fillColorProcessor.processColor(data, dsData); const stroke = this.strokeColorProcessor.processColor(data, dsData); - const style: L.PathOptions = { + return { fill: true, fillColor: fill, color: stroke, @@ -765,7 +830,6 @@ abstract class TbShapesDataLayer { - const svg = `` + - ``; - return 'data:image/svg+xml;base64,' + btoa(svg); -} +import { MarkerShape } from '@home/components/widget/lib/maps/marker-shape.models'; export enum MapType { geoMap = 'geoMap', @@ -183,7 +170,8 @@ export const mapDataLayerValidator = (type: MapDataLayerType): ValidatorFn => { }; export enum MarkerType { - default = 'default', + shape = 'shape', + icon = 'icon', image = 'image' } @@ -211,11 +199,25 @@ export interface MarkerImageSettings { images?: string[]; } +export interface BaseMarkerShapeSettings { + size: number; + color: DataLayerColorSettings; +} + +export interface MarkerShapeSettings extends BaseMarkerShapeSettings { + shape: MarkerShape; +} + +export interface MarkerIconSettings extends BaseMarkerShapeSettings { + icon: string; +} + export interface MarkersDataLayerSettings extends MapDataLayerSettings { xKey: DataKey; yKey: DataKey; markerType: MarkerType; - markerColor: DataLayerColorSettings; + markerShape?: MarkerShapeSettings; + markerIcon?: MarkerIconSettings; markerImage?: MarkerImageSettings; markerOffsetX: number; markerOffsetY: number; @@ -267,14 +269,26 @@ export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly } as MarkersDataLayerSettings, defaultBaseMarkersDataLayerSettings as MarkersDataLayerSettings); export const defaultBaseMarkersDataLayerSettings: Partial = mergeDeep({ - markerType: MarkerType.default, - markerColor: { - type: DataLayerColorType.constant, - color: '#307FE5', + markerType: MarkerType.shape, + markerShape: { + shape: MarkerShape.markerShape1, + size: 34, + color: { + type: DataLayerColorType.constant, + color: '#307FE5', + } + }, + markerIcon: { + icon: 'mdi:lightbulb-on', + size: 48, + color: { + type: DataLayerColorType.constant, + color: '#307FE5', + } }, markerImage: { type: MarkerImageType.image, - image: createColorMarkerURI(tinycolor('#307FE5')), + image: '/assets/markers/shape1.svg', imageSize: 34 }, markerOffsetX: 0.5, @@ -720,7 +734,7 @@ export interface MarkerImageInfo { } export interface MarkerIconInfo { - icon: Icon; + icon: L.Icon; size: [number, number]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 65cc297eac..2928f6205a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -95,6 +95,12 @@ } } } + .leaflet-marker-icon { + &.tb-marker-div-icon { + background: none; + border: none; + } + } } .tb-map-sidebar { .tb-layers, .tb-groups { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/marker-shape.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/marker-shape.models.ts new file mode 100644 index 0000000000..8de7e3d013 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/marker-shape.models.ts @@ -0,0 +1,143 @@ +/// +/// Copyright © 2016-2024 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 tinycolor from 'tinycolor2'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; +import { Observable, of, switchMap } from 'rxjs'; +import { catchError, map, take } from 'rxjs/operators'; +import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; +import { Element, Text, G } from '@svgdotjs/svg.js'; + +export enum MarkerShape { + markerShape1 = 'markerShape1', + markerShape2 = 'markerShape2', + markerShape3 = 'markerShape3', + markerShape4 = 'markerShape4', + markerShape5 = 'markerShape5', + markerShape6 = 'markerShape6', + markerShape7 = 'markerShape7', + markerShape8 = 'markerShape8', + markerShape9 = 'markerShape9', + markerShape10 = 'markerShape10' +} + +export const markerShapeMap = new Map( + [ + [MarkerShape.markerShape1, '/assets/markers/shape1.svg'], + [MarkerShape.markerShape2, '/assets/markers/shape2.svg'], + [MarkerShape.markerShape3, '/assets/markers/shape3.svg'], + [MarkerShape.markerShape4, '/assets/markers/shape4.svg'], + [MarkerShape.markerShape5, '/assets/markers/shape5.svg'], + [MarkerShape.markerShape6, '/assets/markers/shape6.svg'], + [MarkerShape.markerShape7, '/assets/markers/shape7.svg'], + [MarkerShape.markerShape8, '/assets/markers/shape8.svg'], + [MarkerShape.markerShape9, '/assets/markers/shape9.svg'], + [MarkerShape.markerShape10, '/assets/markers/shape10.svg'] + ] +); + +const createColorMarkerShape = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, shape: MarkerShape, color: tinycolor.Instance): Observable => { + const markerAssetUrl = markerShapeMap.get(shape); + const safeUrl = domSanitizer.bypassSecurityTrustResourceUrl(markerAssetUrl); + return iconRegistry.getSvgIconFromUrl(safeUrl).pipe( + map((svgElement) => { + const colorElements = Array.from(svgElement.getElementsByClassName('marker-color')); + colorElements.forEach(el => { + el.setAttribute('fill', '#'+color.toHex()); + }); + const strokeElements = Array.from(svgElement.getElementsByClassName('marker-stroke')); + strokeElements.forEach(el => { + el.setAttribute('stroke', '#'+color.toHex()); + }); + return svgElement; + }) + ); +} + + +export const createColorMarkerShapeURI = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, shape: MarkerShape, color: tinycolor.Instance): Observable => { + return createColorMarkerShape(iconRegistry, domSanitizer, shape, color).pipe( + map((svgElement) => { + const svg = svgElement.outerHTML; + return 'data:image/svg+xml;base64,' + btoa(svg); + }) + ); +} + +const createIconElement = (iconRegistry: MatIconRegistry, icon: string, size: number, color: tinycolor.Instance): Observable => { + const isSvg = isSvgIcon(icon); + const iconColor = tinycolor.mix(color, tinycolor('rgba(0,0,0,0.38)')); + if (isSvg) { + const [namespace, iconName] = splitIconName(icon); + return iconRegistry + .getNamedSvgIcon(iconName, namespace) + .pipe( + take(1), + map((svgElement) => { + const element = new Element(svgElement.firstChild); + element.fill('#'+iconColor.toHex()); + //const box = element.bbox(); + const scale = size / 24;//box.height; + element.scale(scale); + return element; + }), + catchError(() => of(null)) + ); + } else { + const iconName = splitIconName(icon)[1]; + const textElement = new Text(document.createElementNS('http://www.w3.org/2000/svg', 'text')); + const fontSetClasses = ( + iconRegistry.getDefaultFontSetClass() + ).filter(className => className.length > 0); + fontSetClasses.forEach(className => textElement.addClass(className)); + textElement.font({size: `${size}px`}); + textElement.attr({ + style: `font-size: ${size}px`, + 'text-anchor': 'start' + }); + textElement.fill('#'+iconColor.toHex()); + const tspan = textElement.tspan(iconName); + tspan.attr({ + 'dominant-baseline': 'hanging' + }); + return of(textElement); + } +} + +export const createColorMarkerIconElement = (iconRegistry: MatIconRegistry, domSanitizer: DomSanitizer, icon: string, color: tinycolor.Instance): Observable => { + return createColorMarkerShape(iconRegistry, domSanitizer, MarkerShape.markerShape6, color).pipe( + switchMap((svgElement) => { + return createIconElement(iconRegistry, icon, 12, color).pipe( + map((iconElement) => { + let elements = svgElement.getElementsByClassName('marker-icon-container'); + if (iconElement && elements.length) { + const iconContainer = new G(elements[0] as SVGGElement); + iconContainer.add(iconElement); + const box = iconElement.bbox(); + iconElement.translate(-box.cx, -box.cy); + } + elements = svgElement.getElementsByClassName('marker-icon-background'); + if (elements.length) { + (elements[0] as SVGGElement).style.display = ''; + } + return svgElement; + }) + ); + }) + ); +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 9ecaac60a7..b84d61f354 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -181,13 +181,18 @@
{{ 'widgets.maps.data-layer.marker.marker-type' | translate }}
- {{ 'widgets.maps.data-layer.marker.marker-type-default' | translate }} + {{ 'widgets.maps.data-layer.marker.marker-type-shape' | translate }} + {{ 'widgets.maps.data-layer.marker.marker-type-icon' | translate }} {{ 'widgets.maps.data-layer.marker.marker-type-image' | translate }}
-
-
widgets.maps.data-layer.color
- +
+
widgets.maps.data-layer.marker.shape
+ +
+
+
widgets.maps.data-layer.marker.icon
+
widgets.maps.data-layer.marker.image
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 6b15b49580..52bec3c42f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -114,7 +114,8 @@ export class MapDataLayerDialogComponent extends DialogComponent +
+ + +
px
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts new file mode 100644 index 0000000000..cd8ddb25b9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts @@ -0,0 +1,204 @@ +/// +/// Copyright © 2016-2024 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 { + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + Input, + OnInit, + Renderer2, + ViewContainerRef +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MarkerIconSettings, MarkerShapeSettings, MarkerType } from '@home/components/widget/lib/maps/map.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Observable } from 'rxjs'; +import { DomSanitizer, SafeHtml, SafeUrl } from '@angular/platform-browser'; +import { MatIconRegistry } from '@angular/material/icon'; +import { + createColorMarkerIconElement, + createColorMarkerShapeURI +} from '@home/components/widget/lib/maps/marker-shape.models'; +import tinycolor from 'tinycolor2'; +import { map, share } from 'rxjs/operators'; +import { MarkerShapesComponent } from '@home/components/widget/lib/settings/common/map/marker-shapes.component'; +import { MaterialIconsComponent } from '@shared/components/material-icons.component'; + +@Component({ + selector: 'tb-marker-shape-settings', + templateUrl: './marker-shape-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkerShapeSettingsComponent), + multi: true + } + ] +}) +export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnInit { + + MarkerType = MarkerType; + + @Input() + disabled: boolean; + + @Input() + markerType: MarkerType; + + modelValue: MarkerShapeSettings | MarkerIconSettings; + + public shapeSettingsFormGroup: UntypedFormGroup; + + public shapePreview$: Observable; + public iconPreview$: Observable; + + private propagateChange: (v: any) => void = () => { }; + + constructor(private popoverService: TbPopoverService, + private fb: UntypedFormBuilder, + private destroyRef: DestroyRef, + private iconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + this.shapeSettingsFormGroup = this.fb.group({ + size: [null, [Validators.required, Validators.min(1)]], + color: [null, [Validators.required]] + }); + if (this.markerType === MarkerType.shape) { + this.shapeSettingsFormGroup.addControl('shape', this.fb.control(null, [Validators.required])); + } + if (this.markerType === MarkerType.icon) { + this.shapeSettingsFormGroup.addControl('icon', this.fb.control(null, [Validators.required])); + } + this.shapeSettingsFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.shapeSettingsFormGroup.disable({emitEvent: false}); + } else { + this.shapeSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MarkerShapeSettings | MarkerIconSettings): void { + this.modelValue = value; + this.shapeSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updatePreview(); + } + + openShapePopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + if (this.markerType === MarkerType.shape) { + const ctx: any = { + shape: (this.modelValue as MarkerShapeSettings).shape, + color: this.modelValue.color.color + }; + const markerShapesPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MarkerShapesComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + markerShapesPopover.tbComponentRef.instance.popover = markerShapesPopover; + markerShapesPopover.tbComponentRef.instance.markerShapeSelected.subscribe((shape) => { + markerShapesPopover.hide(); + this.shapeSettingsFormGroup.get('shape').patchValue( + shape + ); + }); + } else if (this.markerType === MarkerType.icon) { + const ctx: any = { + selectedIcon: (this.modelValue as MarkerIconSettings).icon, + iconClearButton: false + }; + const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MaterialIconsComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + materialIconsPopover.tbComponentRef.instance.popover = materialIconsPopover; + materialIconsPopover.tbComponentRef.instance.iconSelected.subscribe((icon) => { + materialIconsPopover.hide(); + this.shapeSettingsFormGroup.get('icon').patchValue( + icon + ); + }); + } + } + } + + private updateModel() { + this.modelValue = this.shapeSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + this.updatePreview(); + } + + private updatePreview() { + const color = this.modelValue.color.color; + if (this.markerType === MarkerType.shape) { + const shape = (this.modelValue as MarkerShapeSettings).shape; + this.shapePreview$ = createColorMarkerShapeURI(this.iconRegistry, this.domSanitizer, shape, tinycolor(color)).pipe( + map((url) => { + return this.domSanitizer.bypassSecurityTrustUrl(url); + }), + share() + ); + } else if (this.markerType === MarkerType.icon) { + const icon = (this.modelValue as MarkerIconSettings).icon; + this.iconPreview$ = createColorMarkerIconElement(this.iconRegistry, this.domSanitizer, icon, tinycolor(color)).pipe( + map((element) => { + return this.domSanitizer.bypassSecurityTrustHtml(element.outerHTML); + }), + share() + ); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html new file mode 100644 index 0000000000..09d7b654e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html @@ -0,0 +1,38 @@ + +
+
widgets.maps.data-layer.marker.marker-shapes
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.scss new file mode 100644 index 0000000000..3e9dfb9a52 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-marker-shapes-panel { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + .tb-marker-shapes-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + button.mat-mdc-button-base.tb-select-shape-button { + width: 42px; + min-width: 42px; + height: 42px; + padding: 4px; + img.tb-marker-shape { + width: 34px; + height: 34px; + object-fit: contain; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts new file mode 100644 index 0000000000..b225bf535c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts @@ -0,0 +1,84 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { createColorMarkerShapeURI, MarkerShape } from '@home/components/widget/lib/maps/marker-shape.models'; +import { Observable } from 'rxjs'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { MatIconRegistry } from '@angular/material/icon'; +import tinycolor from 'tinycolor2'; +import { map, share } from 'rxjs/operators'; + +interface MarkerShapeInfo { + shape: MarkerShape; + url$: Observable; +} + +@Component({ + selector: 'tb-marker-shapes', + templateUrl: './marker-shapes.component.html', + providers: [], + styleUrls: ['./marker-shapes.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MarkerShapesComponent extends PageComponent implements OnInit { + + @Input() + shape: MarkerShape; + + @Input() + color: string; + + @Input() + popover: TbPopoverComponent; + + @Output() + markerShapeSelected = new EventEmitter(); + + shapes: MarkerShapeInfo[]; + + constructor(protected store: Store, + private iconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer) { + super(store); + } + + ngOnInit(): void { + this.shapes = (Object.keys(MarkerShape) as MarkerShape[]).map((shape) => { + return { + shape, + url$: createColorMarkerShapeURI(this.iconRegistry, this.domSanitizer, shape, tinycolor(this.color)).pipe( + map((url) => { + return this.domSanitizer.bypassSecurityTrustUrl(url); + }), + share() + ) + }; + }); + } + + cancel() { + this.popover?.hide(); + } + + selectShape(shape: MarkerShape) { + this.markerShapeSelected.emit(shape); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 91186e45ac..583f3e399c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -219,6 +219,10 @@ import { import { DataLayerPatternSettingsComponent } from '@home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component'; +import { + MarkerShapeSettingsComponent +} from '@home/components/widget/lib/settings/common/map/marker-shape-settings.component'; +import { MarkerShapesComponent } from '@home/components/widget/lib/settings/common/map/marker-shapes.component'; @NgModule({ declarations: [ @@ -295,6 +299,8 @@ import { DataLayerColorSettingsComponent, DataLayerColorSettingsPanelComponent, DataLayerPatternSettingsComponent, + MarkerShapeSettingsComponent, + MarkerShapesComponent, MarkerImageSettingsComponent, MarkerImageSettingsPanelComponent, MapDataLayerDialogComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 413a409b65..b01a2598d9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6911,9 +6911,13 @@ "marker-configuration": "Marker configuration", "remove-marker": "Remove marker", "marker-type": "Marker type", - "marker-type-default": "Default", + "marker-type-shape": "Shape", + "marker-type-icon": "Icon", "marker-type-image": "Image", + "shape": "Shape", + "icon": "Icon", "image": "Image", + "marker-shapes": "Marker shapes", "marker-image": "Marker image", "marker-image-type-image": "Image", "marker-image-type-function": "Function", diff --git a/ui-ngx/src/assets/markers/shape1.svg b/ui-ngx/src/assets/markers/shape1.svg new file mode 100644 index 0000000000..fafd5638e7 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape1.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape10.svg b/ui-ngx/src/assets/markers/shape10.svg new file mode 100644 index 0000000000..f0d8b68d2f --- /dev/null +++ b/ui-ngx/src/assets/markers/shape10.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape2.svg b/ui-ngx/src/assets/markers/shape2.svg new file mode 100644 index 0000000000..6777c1810e --- /dev/null +++ b/ui-ngx/src/assets/markers/shape2.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape3.svg b/ui-ngx/src/assets/markers/shape3.svg new file mode 100644 index 0000000000..eb2fa563b7 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape3.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape4.svg b/ui-ngx/src/assets/markers/shape4.svg new file mode 100644 index 0000000000..5303d47ba6 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape4.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape5.svg b/ui-ngx/src/assets/markers/shape5.svg new file mode 100644 index 0000000000..db3c2e0118 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape5.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape6.svg b/ui-ngx/src/assets/markers/shape6.svg new file mode 100644 index 0000000000..f4de6d140d --- /dev/null +++ b/ui-ngx/src/assets/markers/shape6.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape7.svg b/ui-ngx/src/assets/markers/shape7.svg new file mode 100644 index 0000000000..c7df181b78 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape7.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape8.svg b/ui-ngx/src/assets/markers/shape8.svg new file mode 100644 index 0000000000..1fecf058d9 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape8.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/shape9.svg b/ui-ngx/src/assets/markers/shape9.svg new file mode 100644 index 0000000000..f44a6b3626 --- /dev/null +++ b/ui-ngx/src/assets/markers/shape9.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + +