Browse Source

UI: Map layers drag mode.

pull/12723/head
Igor Kulikov 2 years ago
parent
commit
5d3cafc65d
  1. 57
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts
  2. 114
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
  3. 115
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts
  4. 78
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts
  5. 63
      ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts
  6. 79
      ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts
  7. 27
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss
  8. 86
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  9. 71
      ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts
  10. 16
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html
  11. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts
  12. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -31,6 +31,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
private circle: L.Circle; private circle: L.Circle;
private circleStyle: L.PathOptions; private circleStyle: L.PathOptions;
private editing = false;
constructor(data: FormattedData<TbMapDatasource>, constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[],
@ -45,7 +46,8 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
this.circleStyle = this.dataLayer.getShapeStyle(data, dsData); this.circleStyle = this.dataLayer.getShapeStyle(data, dsData);
this.circle = L.circle(center, { this.circle = L.circle(center, {
radius: circleData.radius, radius: circleData.radius,
...this.circleStyle ...this.circleStyle,
snapIgnore: !this.dataLayer.isSnappable()
}); });
this.updateLabel(data, dsData); this.updateLabel(data, dsData);
return this.circle; return this.circle;
@ -76,7 +78,45 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
this.updateCircleShape(data); this.updateCircleShape(data);
} }
protected addItemClass(clazz: string): void {
if ((this.circle as any)._path) {
L.DomUtil.addClass((this.circle as any)._path, clazz);
}
}
protected removeItemClass(clazz: string): void {
if ((this.circle as any)._path) {
L.DomUtil.removeClass((this.circle as any)._path, clazz);
}
}
protected enableDrag(): void {
this.circle.pm.enableLayerDrag();
this.circle.on('pm:dragstart', () => {
this.editing = true;
});
this.circle.on('pm:dragend', () => {
this.saveCircleCoordinates();
this.editing = false;
});
}
protected disableDrag(): void {
this.circle.pm.disableLayerDrag();
this.circle.off('pm:dragstart');
this.circle.off('pm:dragend');
}
private saveCircleCoordinates() {
const center = this.circle.getLatLng();
const radius = this.circle.getRadius();
this.dataLayer.saveCircleCoordinates(this.data, center, radius);
}
private updateCircleShape(data: FormattedData<TbMapDatasource>) { private updateCircleShape(data: FormattedData<TbMapDatasource>) {
if (this.editing) {
return;
}
const circleData = this.dataLayer.extractCircleCoordinates(data); const circleData = this.dataLayer.extractCircleCoordinates(data);
const center = new L.LatLng(circleData.latitude, circleData.longitude); const center = new L.LatLng(circleData.latitude, circleData.longitude);
if (!this.circle.getLatLng().equals(center)) { if (!this.circle.getLatLng().equals(center)) {
@ -117,13 +157,22 @@ export class TbCirclesDataLayer extends TbShapesDataLayer<CirclesDataLayerSettin
} }
protected createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbDataLayerItem<CirclesDataLayerSettings, TbCirclesDataLayer> { protected createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbDataLayerItem<CirclesDataLayerSettings, TbCirclesDataLayer> {
throw new TbCircleDataLayerItem(data, dsData, this.settings, this); return new TbCircleDataLayerItem(data, dsData, this.settings, this);
} }
public extractCircleCoordinates(data: FormattedData<TbMapDatasource>) { public extractCircleCoordinates(data: FormattedData<TbMapDatasource>) {
const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]); const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]);
return this.map.convertCircleData(circleData); return this.map.circleDataToCoordinates(circleData);
} }
public saveCircleCoordinates(data: FormattedData<TbMapDatasource>, center: L.LatLng, radius: number): void {
const converted = this.map.coordinatesToCircleData(center, radius);
const circleData = [
{
dataKey: this.settings.circleKey,
value: converted
}
];
this.map.saveItemData(data.$datasource, circleData).subscribe();
}
} }

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

@ -17,6 +17,7 @@
import { import {
DataLayerColorSettings, DataLayerColorSettings,
DataLayerColorType, DataLayerColorType,
DataLayerEditAction,
DataLayerPatternSettings, DataLayerPatternSettings,
DataLayerPatternType, DataLayerPatternType,
DataLayerTooltipTrigger, DataLayerTooltipTrigger,
@ -63,6 +64,7 @@ export abstract class TbDataLayerItem<S extends MapDataLayerSettings, D extends
this.createEventListeners(data, dsData); this.createEventListeners(data, dsData);
try { try {
this.dataLayer.getDataLayerContainer().addLayer(this.layer); this.dataLayer.getDataLayerContainer().addLayer(this.layer);
this.editModeUpdated();
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
@ -80,10 +82,46 @@ export abstract class TbDataLayerItem<S extends MapDataLayerSettings, D extends
protected abstract createEventListeners(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void; protected abstract createEventListeners(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void;
protected abstract addItemClass(clazz: string): void;
protected abstract removeItemClass(clazz: string): void;
protected abstract enableDrag(): void;
protected abstract disableDrag(): void;
protected enableEdit(): void {
if (this.dataLayer.isHoverable()) {
this.addItemClass('tb-hoverable');
}
if (this.dataLayer.isDragEnabled()) {
this.enableDrag();
this.addItemClass('tb-draggable');
}
}
protected disableEdit(): void {
if (this.dataLayer.isHoverable()) {
this.removeItemClass('tb-hoverable');
}
if (this.dataLayer.isDragEnabled()) {
this.disableDrag();
this.removeItemClass('tb-draggable');
}
}
public invalidateCoordinates(): void { public invalidateCoordinates(): void {
this.doInvalidateCoordinates(this.data, this.dataLayer.getMap().getData()); this.doInvalidateCoordinates(this.data, this.dataLayer.getMap().getData());
} }
public editModeUpdated() {
if (this.dataLayer.isEditMode()) {
this.enableEdit();
} else {
this.disableEdit();
}
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void { public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.data = data; this.data = data;
this.doUpdate(data, dsData); this.doUpdate(data, dsData);
@ -245,6 +283,17 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
protected enabled = true; protected enabled = true;
protected addEnabled = false;
protected dragEnabled = false;
protected editEnabled = false;
protected removeEnabled = false;
protected selectable = false;
protected hoverable = false;
protected snappable = false;
private editMode = false;
public dataLayerLabelProcessor: DataLayerPatternProcessor; public dataLayerLabelProcessor: DataLayerPatternProcessor;
public dataLayerTooltipProcessor: DataLayerPatternProcessor; public dataLayerTooltipProcessor: DataLayerPatternProcessor;
@ -256,10 +305,22 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
this.groupsState[group] = true; this.groupsState[group] = true;
}); });
} }
if (this.settings.edit?.enabledActions) {
this.addEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.add);
this.dragEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.move);
this.editEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.edit);
this.removeEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.remove);
this.selectable = this.removeEnabled || this.editEnabled;
this.hoverable = this.selectable || this.dragEnabled;
}
this.dataLayerContainer = this.createDataLayerContainer(); this.dataLayerContainer = this.createDataLayerContainer();
this.dataLayerLabelProcessor = this.settings.label.show ? new DataLayerPatternProcessor(this, this.settings.label) : null; this.dataLayerLabelProcessor = this.settings.label.show ? new DataLayerPatternProcessor(this, this.settings.label) : null;
this.dataLayerTooltipProcessor = this.settings.tooltip.show ? new DataLayerPatternProcessor(this, this.settings.tooltip): null; this.dataLayerTooltipProcessor = this.settings.tooltip.show ? new DataLayerPatternProcessor(this, this.settings.tooltip): null;
this.map.getMap().addLayer(this.dataLayerContainer); this.map.getMap().addLayer(this.dataLayerContainer);
this.enableEditMode();
} }
public setup(): Observable<any> { public setup(): Observable<any> {
@ -291,6 +352,38 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
return this.enabled; return this.enabled;
} }
public isEditMode(): boolean {
return this.editMode;
}
public isAddEnabled(): boolean {
return this.addEnabled;
}
public isDragEnabled(): boolean {
return this.dragEnabled;
}
public isEditEnabled(): boolean {
return this.editEnabled;
}
public isRemoveEnabled(): boolean {
return this.removeEnabled;
}
public isHoverable(): boolean {
return this.hoverable;
}
public isSelectable(): boolean {
return this.selectable;
}
public isSnappable(): boolean {
return this.snappable;
}
public getGroups(): string[] { public getGroups(): string[] {
return this.settings.groups || []; return this.settings.groups || [];
} }
@ -303,6 +396,7 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
this.enabled = enabled; this.enabled = enabled;
if (this.enabled) { if (this.enabled) {
this.map.getMap().addLayer(this.dataLayerContainer); this.map.getMap().addLayer(this.dataLayerContainer);
this.updateItemsEditMode();
} else { } else {
this.map.getMap().removeLayer(this.dataLayerContainer); this.map.getMap().removeLayer(this.dataLayerContainer);
} }
@ -350,7 +444,7 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
} }
protected createDataLayerContainer(): L.FeatureGroup { protected createDataLayerContainer(): L.FeatureGroup {
return L.featureGroup(); return L.featureGroup([], {snapIgnore: !this.snappable});
} }
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
@ -364,6 +458,24 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
return this.map.type(); return this.map.type();
} }
public enableEditMode() {
if (!this.editMode) {
this.editMode = true;
this.updateItemsEditMode();
}
}
public disableEditMode() {
if (this.editMode) {
this.editMode = false;
this.updateItemsEditMode();
}
}
private updateItemsEditMode() {
this.layerItems.forEach(item => item.editModeUpdated());
}
public abstract dataLayerType(): MapDataLayerType; public abstract dataLayerType(): MapDataLayerType;
protected abstract defaultBaseSettings(map: TbMap<any>): Partial<S>; protected abstract defaultBaseSettings(map: TbMap<any>): Partial<S>;

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

@ -60,9 +60,10 @@ import { TbImageMap } from '@home/components/widget/lib/maps/image-map';
class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, TbMarkersDataLayer, L.Marker> { class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, TbMarkersDataLayer, L.Marker> {
private location: L.LatLng;
private marker: L.Marker; private marker: L.Marker;
private labelOffset: L.PointTuple; private labelOffset: L.PointTuple;
private iconClassList: string[];
private moving = false;
constructor(data: FormattedData<TbMapDatasource>, constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[],
@ -72,8 +73,9 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
} }
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Marker { protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Marker {
this.location = this.dataLayer.extractLocation(data, dsData); this.iconClassList = [];
this.marker = L.marker(this.location, { const location = this.dataLayer.extractLocation(data, dsData);
this.marker = L.marker(location, {
tbMarkerData: data tbMarkerData: data
}); });
@ -96,26 +98,69 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
protected doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void { protected doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.marker.options.tbMarkerData = data; this.marker.options.tbMarkerData = data;
this.updateMarkerPosition(data, dsData); this.updateMarkerLocation(data, dsData);
this.updateTooltip(data, dsData); this.updateTooltip(data, dsData);
this.updateMarkerIcon(data, dsData); this.updateMarkerIcon(data, dsData);
} }
protected doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void { protected doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.updateMarkerPosition(data, dsData); this.updateMarkerLocation(data, dsData);
} }
private updateMarkerPosition(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) { protected addItemClass(clazz: string): void {
const position = this.dataLayer.extractLocation(data, dsData); if (!this.iconClassList.includes(clazz)) {
if (!this.marker.getLatLng().equals(position)) { this.iconClassList.push(clazz);
this.location = position; this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className);
this.marker.setLatLng(position); if ((this.marker as any)._icon) {
L.DomUtil.addClass((this.marker as any)._icon, clazz);
}
}
}
protected removeItemClass(clazz: string): void {
const index = this.iconClassList.indexOf(clazz);
if (index !== -1) {
this.iconClassList.splice(index, 1);
this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className);
if ((this.marker as any)._icon) {
L.DomUtil.removeClass((this.marker as any)._icon, clazz);
}
}
}
protected enableDrag(): void {
this.marker.options.draggable = true;
this.marker.on('dragstart', () => {
this.moving = true;
});
this.marker.on('dragend', () => {
this.saveMarkerLocation();
this.moving = false;
});
}
protected disableDrag(): void {
this.marker.options.draggable = false;
this.marker.off('dragstart');
this.marker.off('dragend');
}
private saveMarkerLocation() {
const location = this.marker.getLatLng();
this.dataLayer.saveMarkerLocation(this.data, location);
}
private updateMarkerLocation(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
const location = this.dataLayer.extractLocation(data, dsData);
if (!this.marker.getLatLng().equals(location) && !this.moving) {
this.marker.setLatLng(location);
} }
} }
private updateMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) { private updateMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe( this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe(
(iconInfo) => { (iconInfo) => {
iconInfo.icon.options.className = this.updateIconClasses(iconInfo.icon.options.className);
this.marker.setIcon(iconInfo.icon); this.marker.setIcon(iconInfo.icon);
const anchor = iconInfo.icon.options.iconAnchor; const anchor = iconInfo.icon.options.iconAnchor;
if (anchor && Array.isArray(anchor)) { if (anchor && Array.isArray(anchor)) {
@ -124,9 +169,23 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10]; this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10];
} }
this.updateLabel(data, dsData); this.updateLabel(data, dsData);
this.editModeUpdated();
} }
); );
} }
private updateIconClasses(className: string): string {
const classes: string[] = [];
if (className?.length) {
classes.push(...className.split(' '));
}
this.iconClassList.forEach(clazz => {
if (!classes.includes(clazz)) {
classes.push(clazz);
}
});
return classes.join(' ');
}
} }
abstract class MarkerIconProcessor<S> { abstract class MarkerIconProcessor<S> {
@ -390,7 +449,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
} }
protected isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean { protected isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean {
return !!this.extractPosition(layerData); return !!this.extractLocationData(layerData);
} }
protected createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbMarkerDataLayerItem { protected createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbMarkerDataLayerItem {
@ -412,13 +471,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
removeOutsideVisibleBounds: this.settings.markerClustering?.lazyLoad, removeOutsideVisibleBounds: this.settings.markerClustering?.lazyLoad,
animate: this.settings.markerClustering?.zoomAnimation, animate: this.settings.markerClustering?.zoomAnimation,
chunkedLoading: this.settings.markerClustering?.chunkedLoad, chunkedLoading: this.settings.markerClustering?.chunkedLoad,
pmIgnore: true, snapIgnore: !this.snappable
spiderLegPolylineOptions: {
pmIgnore: true
},
polygonOptions: {
pmIgnore: true
}
}; };
if (this.settings.markerClustering?.useClusterMarkerColorFunction) { if (this.settings.markerClustering?.useClusterMarkerColorFunction) {
markerClusterOptions.iconCreateFunction = (cluster) => { markerClusterOptions.iconCreateFunction = (cluster) => {
@ -463,7 +516,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
return this.markersClusterContainer; return this.markersClusterContainer;
} }
private extractPosition(data: FormattedData<TbMapDatasource>): {x: number; y: number} { private extractLocationData(data: FormattedData<TbMapDatasource>): {x: number; y: number} {
if (data) { if (data) {
const xKeyVal = data[this.settings.xKey.label]; const xKeyVal = data[this.settings.xKey.label];
const yKeyVal = data[this.settings.yKey.label]; const yKeyVal = data[this.settings.yKey.label];
@ -524,15 +577,31 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
} }
public extractLocation(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.LatLng { public extractLocation(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.LatLng {
let position = this.extractPosition(data); let locationData = this.extractLocationData(data);
if (position) { if (locationData) {
if (this.map.type() === MapType.image && this.positionFunction) { if (this.map.type() === MapType.image && this.positionFunction) {
const imageMap = this.map as TbImageMap; const imageMap = this.map as TbImageMap;
position = this.positionFunction.execute(position.x, position.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0}; locationData = this.positionFunction.execute(locationData.x, locationData.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0};
} }
return this.map.positionToLatLng(position); return this.map.locationDataToLatLng(locationData);
} else { } else {
return null; return null;
} }
} }
public saveMarkerLocation(data: FormattedData<TbMapDatasource>, position: L.LatLng): void {
const converted = this.map.latLngToLocationData(position);
const locationData = [
{
dataKey: this.settings.xKey,
value: converted.x
},
{
dataKey: this.settings.yKey,
value: converted.y
}
];
this.map.saveItemData(data.$datasource, locationData).subscribe();
}
} }

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

@ -18,7 +18,7 @@ import {
defaultBasePolygonsDataLayerSettings, defaultBasePolygonsDataLayerSettings,
isCutPolygon, isJSON, isCutPolygon, isJSON,
PolygonsDataLayerSettings, PolygonsDataLayerSettings,
TbMapDatasource TbMapDatasource, TbPolyData, TbPolygonCoordinates, TbPolygonRawCoordinates
} from '@home/components/widget/lib/maps/models/map.models'; } from '@home/components/widget/lib/maps/models/map.models';
import L from 'leaflet'; import L from 'leaflet';
import { FormattedData } from '@shared/models/widget.models'; import { FormattedData } from '@shared/models/widget.models';
@ -33,6 +33,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
private polygonContainer: L.FeatureGroup; private polygonContainer: L.FeatureGroup;
private polygon: L.Polygon; private polygon: L.Polygon;
private polygonStyle: L.PathOptions; private polygonStyle: L.PathOptions;
private editing = false;
constructor(data: FormattedData<TbMapDatasource>, constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[],
@ -45,8 +46,9 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
const polyData = this.dataLayer.extractPolygonCoordinates(data); const polyData = this.dataLayer.extractPolygonCoordinates(data);
const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle; const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle;
this.polygonStyle = this.dataLayer.getShapeStyle(data, dsData); this.polygonStyle = this.dataLayer.getShapeStyle(data, dsData);
this.polygon = polyConstructor(polyData, { this.polygon = polyConstructor(polyData as (TbPolygonRawCoordinates & L.LatLngTuple[]), {
...this.polygonStyle ...this.polygonStyle,
snapIgnore: !this.dataLayer.isSnappable()
}); });
this.polygonContainer = L.featureGroup(); this.polygonContainer = L.featureGroup();
@ -81,20 +83,69 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
this.updatePolygonShape(data); this.updatePolygonShape(data);
} }
protected addItemClass(clazz: string): void {
if ((this.polygon as any)._path) {
L.DomUtil.addClass((this.polygon as any)._path, clazz);
}
}
protected removeItemClass(clazz: string): void {
if ((this.polygon as any)._path) {
L.DomUtil.removeClass((this.polygon as any)._path, clazz);
}
}
protected enableDrag(): void {
this.polygon.pm.enableLayerDrag();
this.polygon.on('pm:dragstart', () => {
this.editing = true;
});
this.polygon.on('pm:dragend', () => {
this.savePolygonCoordinates();
this.editing = false;
});
}
protected disableDrag(): void {
this.polygon.pm.disableLayerDrag();
this.polygon.off('pm:dragstart');
this.polygon.off('pm:dragend');
}
private savePolygonCoordinates() {
let coordinates: TbPolygonCoordinates = this.polygon.getLatLngs();
if (coordinates.length === 1) {
coordinates = coordinates[0] as TbPolygonCoordinates;
}
if (this.polygon instanceof L.Rectangle && !isCutPolygon(coordinates)) {
const bounds = this.polygon.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.savePolygonCoordinates(this.data, coordinates);
}
private updatePolygonShape(data: FormattedData<TbMapDatasource>) { private updatePolygonShape(data: FormattedData<TbMapDatasource>) {
const polyData = this.dataLayer.extractPolygonCoordinates(data); if (this.editing) {
return;
}
const polyData = this.dataLayer.extractPolygonCoordinates(data) as TbPolyData;
if (isCutPolygon(polyData) || polyData.length !== 2) { if (isCutPolygon(polyData) || polyData.length !== 2) {
if (this.polygon instanceof L.Rectangle) { if (this.polygon instanceof L.Rectangle) {
this.polygonContainer.removeLayer(this.polygon); this.polygonContainer.removeLayer(this.polygon);
this.polygon = L.polygon(polyData, { this.polygon = L.polygon(polyData, {
...this.polygonStyle ...this.polygonStyle,
snapIgnore: !this.dataLayer.isSnappable()
}); });
this.polygon.addTo(this.polygonContainer); this.polygon.addTo(this.polygonContainer);
this.editModeUpdated();
} else { } else {
this.polygon.setLatLngs(polyData); this.polygon.setLatLngs(polyData);
} }
} else if (polyData.length === 2) { } else if (polyData.length === 2) {
const bounds = new L.LatLngBounds(polyData); const bounds = new L.LatLngBounds(polyData as L.LatLngTuple[]);
(this.polygon as L.Rectangle).setBounds(bounds); (this.polygon as L.Rectangle).setBounds(bounds);
} }
} }
@ -134,11 +185,22 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer<PolygonsDataLayerSett
return new TbPolygonDataLayerItem(data, dsData, this.settings, this); return new TbPolygonDataLayerItem(data, dsData, this.settings, this);
} }
public extractPolygonCoordinates(data: FormattedData<TbMapDatasource>) { public extractPolygonCoordinates(data: FormattedData<TbMapDatasource>): TbPolygonRawCoordinates {
let rawPolyData = data[this.settings.polygonKey.label]; let rawPolyData = data[this.settings.polygonKey.label];
if (isString(rawPolyData)) { if (isString(rawPolyData)) {
rawPolyData = JSON.parse(rawPolyData); rawPolyData = JSON.parse(rawPolyData);
} }
return this.map.toPolygonCoordinates(rawPolyData); return this.map.polygonDataToCoordinates(rawPolyData);
}
public savePolygonCoordinates(data: FormattedData<TbMapDatasource>, coordinates: TbPolygonCoordinates): void {
const converted = this.map.coordinatesToPolygonData(coordinates);
const polygonData = [
{
dataKey: this.settings.polygonKey,
value: converted
}
];
this.map.saveItemData(data.$datasource, polygonData).subscribe();
} }
} }

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

@ -15,15 +15,22 @@
/// ///
import { import {
checkLngLat,
DEFAULT_ZOOM_LEVEL, DEFAULT_ZOOM_LEVEL,
defaultGeoMapSettings, defaultGeoMapSettings,
GeoMapSettings, latLngPointToBounds, GeoMapSettings,
MapZoomAction, TbCircleData latLngPointToBounds,
MapZoomAction,
TbCircleData,
TbPolygonCoordinate,
TbPolygonCoordinates,
TbPolygonRawCoordinate,
TbPolygonRawCoordinates
} from '@home/components/widget/lib/maps/models/map.models'; } from '@home/components/widget/lib/maps/models/map.models';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common'; import { DeepPartial } from '@shared/models/common';
import { forkJoin, Observable, of } from 'rxjs'; import { forkJoin, Observable, of } from 'rxjs';
import L, { LatLngBounds, LatLngTuple } from 'leaflet'; import L from 'leaflet';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer';
import { TbMap } from '@home/components/widget/lib/maps/map'; import { TbMap } from '@home/components/widget/lib/maps/map';
@ -53,7 +60,7 @@ export class TbGeoMap extends TbMap<GeoMapSettings> {
protected onResize(): void {} protected onResize(): void {}
protected fitBounds(bounds: LatLngBounds) { protected fitBounds(bounds: L.LatLngBounds) {
if (bounds.isValid()) { if (bounds.isValid()) {
if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) { if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) {
this.map.setZoom(this.settings.defaultZoomLevel, { animate: false }); this.map.setZoom(this.settings.defaultZoomLevel, { animate: false });
@ -115,27 +122,63 @@ export class TbGeoMap extends TbMap<GeoMapSettings> {
); );
} }
public positionToLatLng(position: {x: number; y: number}): L.LatLng { public locationDataToLatLng(position: {x: number; y: number}): L.LatLng {
return L.latLng(position.x, position.y) as L.LatLng; return L.latLng(position.x, position.y) as L.LatLng;
} }
public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { public latLngToLocationData(position: L.LatLng): {x: number; y: number} {
return (expression).map((el) => { position = position ? checkLngLat(position, this.southWest, this.northEast, 0) : {lat: null, lng: null} as L.LatLng;
if (!Array.isArray(el[0]) && el.length === 2) { return {
x: position.lat,
y: position.lng
}
}
public polygonDataToCoordinates(expression: TbPolygonRawCoordinates): TbPolygonRawCoordinates {
return (expression).map((el: TbPolygonRawCoordinate) => {
if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) {
return el; return el;
} else if (Array.isArray(el) && el.length) { } else if (Array.isArray(el) && el.length) {
return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); return this.polygonDataToCoordinates(el as TbPolygonRawCoordinates) as TbPolygonRawCoordinate;
} else { } else {
return null; return null;
} }
}).filter(el => !!el); }).filter(el => !!el);
} }
public convertCircleData(circle: TbCircleData): TbCircleData { public coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates {
if (coordinates.length) {
return coordinates.map((point: TbPolygonCoordinate) => {
if (Array.isArray(point)) {
return this.coordinatesToPolygonData(point) as TbPolygonRawCoordinate;
} else {
const convertPoint = checkLngLat(point, this.southWest, this.northEast);
return [convertPoint.lat, convertPoint.lng];
}
});
}
return [];
}
public circleDataToCoordinates(circle: TbCircleData): TbCircleData {
const centerPoint = latLngPointToBounds(new L.LatLng(circle.latitude, circle.longitude), this.southWest, this.northEast); const centerPoint = latLngPointToBounds(new L.LatLng(circle.latitude, circle.longitude), this.southWest, this.northEast);
circle.latitude = centerPoint.lat; circle.latitude = centerPoint.lat;
circle.longitude = centerPoint.lng; circle.longitude = centerPoint.lng;
return circle; return circle;
} }
public coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData {
let circleData: TbCircleData = null;
if (center) {
const position = checkLngLat(center, this.southWest, this.northEast);
circleData = {
latitude: position.lat,
longitude: position.lng,
radius
};
}
return circleData;
}
} }

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

@ -15,6 +15,7 @@
/// ///
import { import {
calculateNewPointCoordinate,
defaultImageMapSettings, defaultImageMapSettings,
defaultImageMapSourceSettings, defaultImageMapSourceSettings,
ImageMapSettings, ImageMapSettings,
@ -22,12 +23,12 @@ import {
ImageSourceType, ImageSourceType,
loadImageWithAspect, loadImageWithAspect,
MapZoomAction, MapZoomAction,
TbCircleData TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates
} from '@home/components/widget/lib/maps/models/map.models'; } from '@home/components/widget/lib/maps/models/map.models';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common'; import { DeepPartial } from '@shared/models/common';
import { Observable, of, ReplaySubject, switchMap } from 'rxjs'; import { Observable, of, ReplaySubject, switchMap } from 'rxjs';
import L, { LatLngBounds, LatLngLiteral, LatLngTuple } from 'leaflet'; import L from 'leaflet';
import { TbMap } from '@home/components/widget/lib/maps/map'; import { TbMap } from '@home/components/widget/lib/maps/map';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@ -115,38 +116,62 @@ export class TbImageMap extends TbMap<ImageMapSettings> {
} }
} }
protected fitBounds(_bounds: LatLngBounds) {} protected fitBounds(_bounds: L.LatLngBounds) {}
public positionToLatLng(position: {x: number; y: number}): L.LatLng { public locationDataToLatLng(position: {x: number; y: number}): L.LatLng {
return this.pointToLatLng( return this.pointToLatLng(
position.x * this.width, position.x * this.width,
position.y * this.height); position.y * this.height);
} }
public pointToLatLng(x: number, y: number): L.LatLng { public latLngToLocationData(position: L.LatLng): {x: number; y: number} {
return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1); if (!position) {
} return {
x: null,
private latLngToPoint(latLng: LatLngLiteral): L.Point { y: null
return L.CRS.Simple.latLngToPoint(latLng, this.maxZoom - 1); };
}
const point = this.latLngToPoint(position);
const posX = calculateNewPointCoordinate(point.x, this.width);
const posY = calculateNewPointCoordinate(point.y, this.height);
return {
x: posX,
y: posY
};
} }
public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { public polygonDataToCoordinates(expression: TbPolygonRawCoordinates): TbPolygonRawCoordinates {
return (expression).map((el) => { return expression.map((el: TbPolygonRawCoordinate) => {
if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) {
return this.pointToLatLng( const latLng = this.pointToLatLng(
el[0] * this.width, el[0] * this.width,
el[1] * this.height el[1] * this.height
); );
return [latLng.lat, latLng.lng] as TbPolygonRawCoordinate;
} else if (Array.isArray(el) && el.length) { } else if (Array.isArray(el) && el.length) {
return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); return this.polygonDataToCoordinates(el as TbPolygonRawCoordinates) as TbPolygonRawCoordinate;
} else { } else {
return null; return null;
} }
}).filter(el => !!el); }).filter(el => !!el);
} }
public convertCircleData(circle: TbCircleData): TbCircleData { public coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates {
if (coordinates.length) {
return coordinates.map((point: TbPolygonCoordinate) => {
if (Array.isArray(point)) {
return this.coordinatesToPolygonData(point) as TbPolygonRawCoordinate;
} else {
const pos = this.latLngToPoint(point);
return [calculateNewPointCoordinate(pos.x, this.width), calculateNewPointCoordinate(pos.y, this.height)];
}
});
} else {
return [];
}
}
public circleDataToCoordinates(circle: TbCircleData): TbCircleData {
const centerPoint = this.pointToLatLng(circle.latitude * this.width, circle.longitude * this.height); const centerPoint = this.pointToLatLng(circle.latitude * this.width, circle.longitude * this.height);
circle.latitude = centerPoint.lat; circle.latitude = centerPoint.lat;
circle.longitude = centerPoint.lng; circle.longitude = centerPoint.lng;
@ -154,10 +179,34 @@ export class TbImageMap extends TbMap<ImageMapSettings> {
return circle; return circle;
} }
public coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData {
let circleData: TbCircleData = null;
if (center) {
const point = this.latLngToPoint(center);
const posX = calculateNewPointCoordinate(point.x, this.width);
const posY = calculateNewPointCoordinate(point.y, this.height);
const convertedRadius = calculateNewPointCoordinate(radius, this.width);
circleData = {
latitude: posX,
longitude: posY,
radius: convertedRadius
};
}
return circleData;
}
public getAspect(): number { public getAspect(): number {
return this.imageLayerData.aspect; return this.imageLayerData.aspect;
} }
private pointToLatLng(x: number, y: number): L.LatLng {
return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1);
}
private latLngToPoint(latLng: L.LatLngLiteral): L.Point {
return L.CRS.Simple.latLngToPoint(latLng, this.maxZoom - 1);
}
private doCreateMap(updateImage?: boolean) { private doCreateMap(updateImage?: boolean) {
if (!this.map && this.imageLayerData.aspect > 0) { if (!this.map && this.imageLayerData.aspect > 0) {
const center = this.pointToLatLng(this.width / 2, this.height / 2); const center = this.pointToLatLng(this.width / 2, this.height / 2);

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

@ -15,6 +15,9 @@
*/ */
@import '../../../../../../../scss/constants'; @import '../../../../../../../scss/constants';
//$map-element-hover-color: #307FE5;
$map-element-hover-color: rgba(0,0,0,0.56);
.tb-map-layout { .tb-map-layout {
display: flex; display: flex;
width: 100%; width: 100%;
@ -104,6 +107,30 @@
&.tb-marker-div-icon { &.tb-marker-div-icon {
background: none; background: none;
border: none; border: none;
&.tb-draggable {
cursor: move;
}
&.tb-hoverable {
svg {
transition: filter 0.2s;
}
&:hover {
svg {
filter: drop-shadow( 0 0 4px $map-element-hover-color);
}
}
}
}
}
img.leaflet-marker-icon, path {
&.tb-draggable {
cursor: move;
}
&.tb-hoverable {
transition: filter 0.2s;
&:hover {
filter: drop-shadow( 0 0 4px $map-element-hover-color);
}
} }
} }
.tb-cluster-marker-container { .tb-cluster-marker-container {

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

@ -17,17 +17,18 @@
import { import {
additionalMapDataSourcesToDatasources, additionalMapDataSourcesToDatasources,
BaseMapSettings, BaseMapSettings,
DataKeyValuePair,
MapActionHandler, MapActionHandler,
MapType, MapType,
mergeMapDatasources, mergeMapDatasources,
parseCenterPosition, parseCenterPosition,
TbCircleData, TbCircleData,
TbMapDatasource TbMapDatasource, TbPolygonCoordinates, TbPolygonRawCoordinates
} from '@home/components/widget/lib/maps/models/map.models'; } from '@home/components/widget/lib/maps/models/map.models';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils';
import { DeepPartial } from '@shared/models/common'; import { DeepPartial } from '@shared/models/common';
import L, { LatLngBounds, LatLngTuple, LeafletMouseEvent, Projection } from 'leaflet'; import L from 'leaflet';
import { forkJoin, Observable, of } from 'rxjs'; import { forkJoin, Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb';
@ -39,6 +40,9 @@ import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbMarkersDataLayer } from '@home/components/widget/lib/maps/data-layer/markers-data-layer'; import { TbMarkersDataLayer } from '@home/components/widget/lib/maps/data-layer/markers-data-layer';
import { TbPolygonsDataLayer } from '@home/components/widget/lib/maps/data-layer/polygons-data-layer'; import { TbPolygonsDataLayer } from '@home/components/widget/lib/maps/data-layer/polygons-data-layer';
import { TbCirclesDataLayer } from '@home/components/widget/lib/maps/data-layer/circles-data-layer'; import { TbCirclesDataLayer } from '@home/components/widget/lib/maps/data-layer/circles-data-layer';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
export abstract class TbMap<S extends BaseMapSettings> { export abstract class TbMap<S extends BaseMapSettings> {
@ -49,8 +53,8 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected defaultCenterPosition: [number, number]; protected defaultCenterPosition: [number, number];
protected ignoreUpdateBounds = false; protected ignoreUpdateBounds = false;
protected southWest = new L.LatLng(-Projection.SphericalMercator['MAX_LATITUDE'], -180); protected southWest = new L.LatLng(-L.Projection.SphericalMercator['MAX_LATITUDE'], -180);
protected northEast = new L.LatLng(Projection.SphericalMercator['MAX_LATITUDE'], 180); protected northEast = new L.LatLng(L.Projection.SphericalMercator['MAX_LATITUDE'], 180);
protected dataLayers: TbMapDataLayer<any,any>[]; protected dataLayers: TbMapDataLayer<any,any>[];
protected dsData: FormattedData<TbMapDatasource>[]; protected dsData: FormattedData<TbMapDatasource>[];
@ -128,6 +132,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.map.panTo(this.defaultCenterPosition); this.map.panTo(this.defaultCenterPosition);
} }
this.setupDataLayers(); this.setupDataLayers();
this.setupEditMode();
this.createdControlButtonTooltip(); this.createdControlButtonTooltip();
} }
@ -225,6 +230,13 @@ export abstract class TbMap<S extends BaseMapSettings> {
} }
} }
private setupEditMode() {
const dragEnabled = this.dataLayers.some(dl => dl.isDragEnabled());
if (dragEnabled) {
//this.map.pm.enableGlobalDragMode();
}
}
private createdControlButtonTooltip() { private createdControlButtonTooltip() {
import('tooltipster').then(() => { import('tooltipster').then(() => {
if ($.tooltipster) { if ($.tooltipster) {
@ -326,7 +338,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected abstract onResize(): void; protected abstract onResize(): void;
protected abstract fitBounds(bounds: LatLngBounds): void; protected abstract fitBounds(bounds: L.LatLngBounds): void;
protected doSetupControls(): Observable<any> { protected doSetupControls(): Observable<any> {
return of(null); return of(null);
@ -375,7 +387,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
public markerClick(marker: L.Layer, datasource: TbMapDatasource): void { public markerClick(marker: L.Layer, datasource: TbMapDatasource): void {
if (Object.keys(this.markerClickActions).length) { if (Object.keys(this.markerClickActions).length) {
marker.on('click', (event: LeafletMouseEvent) => { marker.on('click', (event: L.LeafletMouseEvent) => {
for (const action in this.markerClickActions) { for (const action in this.markerClickActions) {
this.markerClickActions[action](event.originalEvent, datasource); this.markerClickActions[action](event.originalEvent, datasource);
} }
@ -385,7 +397,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
public polygonClick(polygon: L.Layer, datasource: TbMapDatasource): void { public polygonClick(polygon: L.Layer, datasource: TbMapDatasource): void {
if (Object.keys(this.polygonClickActions).length) { if (Object.keys(this.polygonClickActions).length) {
polygon.on('click', (event: LeafletMouseEvent) => { polygon.on('click', (event: L.LeafletMouseEvent) => {
for (const action in this.polygonClickActions) { for (const action in this.polygonClickActions) {
this.polygonClickActions[action](event.originalEvent, datasource); this.polygonClickActions[action](event.originalEvent, datasource);
} }
@ -395,7 +407,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
public circleClick(circle: L.Layer, datasource: TbMapDatasource): void { public circleClick(circle: L.Layer, datasource: TbMapDatasource): void {
if (Object.keys(this.circleClickActions).length) { if (Object.keys(this.circleClickActions).length) {
circle.on('click', (event: LeafletMouseEvent) => { circle.on('click', (event: L.LeafletMouseEvent) => {
for (const action in this.circleClickActions) { for (const action in this.circleClickActions) {
this.circleClickActions[action](event.originalEvent, datasource); this.circleClickActions[action](event.originalEvent, datasource);
} }
@ -403,6 +415,50 @@ export abstract class TbMap<S extends BaseMapSettings> {
} }
} }
public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable<any> {
const attributeService = this.ctx.$injector.get(AttributeService);
const attributes: AttributeData[] = [];
const timeseries: AttributeData[] = [];
const entityId: EntityId = {
entityType: datasource.entityType,
id: datasource.entityId
};
data.forEach(pair => {
const key = pair.dataKey;
if (key.type === DataKeyType.attribute) {
attributes.push({
key: key.name,
value: pair.value
});
} else if (key.type === DataKeyType.timeseries) {
timeseries.push({
key: key.name,
value: pair.value
});
}
});
const observables: Observable<any>[] = [];
if (timeseries.length) {
observables.push(attributeService.saveEntityTimeseries(
entityId,
LatestTelemetry.LATEST_TELEMETRY,
timeseries
));
}
if (attributes.length) {
observables.push(attributeService.saveEntityAttributes(
entityId,
AttributeScope.SERVER_SCOPE,
attributes
));
}
if (observables.length) {
return forkJoin(observables);
} else {
return of(null);
}
}
public destroy() { public destroy() {
if (this.mapResize$) { if (this.mapResize$) {
this.mapResize$.disconnect(); this.mapResize$.disconnect();
@ -415,10 +471,18 @@ export abstract class TbMap<S extends BaseMapSettings> {
}); });
} }
public abstract positionToLatLng(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 polygonDataToCoordinates(coordinates: TbPolygonRawCoordinates): TbPolygonRawCoordinates;
public abstract coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates;
public abstract circleDataToCoordinates(circle: TbCircleData): TbCircleData;
public abstract coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData;
public abstract toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any;
public abstract convertCircleData(circle: TbCircleData): TbCircleData;
} }

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

@ -19,7 +19,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { guid, hashCode, isDefinedAndNotNull, isNotEmptyStr, isString, mergeDeep } from '@core/utils'; import { guid, hashCode, isDefinedAndNotNull, isNotEmptyStr, isString, mergeDeep } from '@core/utils';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { materialColors } from '@shared/models/material.models'; import { materialColors } from '@shared/models/material.models';
import L from 'leaflet'; import L, { LatLngExpression } from 'leaflet';
import { TbFunction } from '@shared/models/js-function.models'; import { TbFunction } from '@shared/models/js-function.models';
import { Observable, Observer, of, switchMap } from 'rxjs'; import { Observable, Observer, of, switchMap } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -89,11 +89,35 @@ export interface DataLayerTooltipSettings extends DataLayerPatternSettings {
offsetY: number; offsetY: number;
} }
export enum DataLayerEditAction {
add = 'add',
edit = 'edit',
move = 'move',
remove = 'remove'
}
export const dataLayerEditActions = Object.keys(DataLayerEditAction) as DataLayerEditAction[];
export const dataLayerEditActionTranslationMap = new Map<DataLayerEditAction, string>(
[
[DataLayerEditAction.add, 'widgets.maps.data-layer.action-add'],
[DataLayerEditAction.edit, 'widgets.maps.data-layer.action-edit'],
[DataLayerEditAction.move, 'widgets.maps.data-layer.action-move'],
[DataLayerEditAction.remove, 'widgets.maps.data-layer.action-remove']
]
);
export interface DataLayerEditSettings {
enabledActions: DataLayerEditAction[];
snappable: boolean;
}
export interface MapDataLayerSettings extends MapDataSourceSettings { export interface MapDataLayerSettings extends MapDataSourceSettings {
additionalDataKeys?: DataKey[]; additionalDataKeys?: DataKey[];
label: DataLayerPatternSettings; label: DataLayerPatternSettings;
tooltip: DataLayerTooltipSettings; tooltip: DataLayerTooltipSettings;
groups?: string[]; groups?: string[];
edit: DataLayerEditSettings;
} }
export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataLayerSettings> => ({ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataLayerSettings> => ({
@ -112,6 +136,10 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataL
: '<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}<br/><b>Temperature:</b> ${temperature} °C<br/><small>See tooltip settings for details</small>', : '<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}<br/><b>Temperature:</b> ${temperature} °C<br/><small>See tooltip settings for details</small>',
offsetX: 0, offsetX: 0,
offsetY: -1 offsetY: -1
},
edit: {
enabledActions: [],
snappable: false
} }
}) })
@ -864,12 +892,23 @@ export type ClusterMarkerColorFunction = (data: FormattedData<TbMapDatasource>[]
export type MarkerPositionFunction = (origXPos: number, origYPos: number, data: FormattedData<TbMapDatasource>, export type MarkerPositionFunction = (origXPos: number, origYPos: number, data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[], aspect: number) => { x: number, y: number }; dsData: FormattedData<TbMapDatasource>[], aspect: number) => { x: number, y: number };
export type TbPolygonRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][];
export type TbPolygonRawCoordinates = TbPolygonRawCoordinate[];
export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][];
export type TbPolygonCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][];
export type TbPolygonCoordinates = TbPolygonCoordinate[];
export interface TbCircleData { export interface TbCircleData {
latitude: number; latitude: number;
longitude: number; longitude: number;
radius: number; radius: number;
} }
export type DataKeyValuePair = {
dataKey: DataKey;
value: any;
}
export const isJSON = (data: string): boolean => { export const isJSON = (data: string): boolean => {
try { try {
const parseData = JSON.parse(data); const parseData = JSON.parse(data);
@ -892,7 +931,7 @@ export const isValidLongitude = (longitude: any): boolean =>
export const isValidLatLng = (latitude: any, longitude: any): boolean => export const isValidLatLng = (latitude: any, longitude: any): boolean =>
isValidLatitude(latitude) && isValidLongitude(longitude); isValidLatitude(latitude) && isValidLongitude(longitude);
export const isCutPolygon = (data): boolean => { export const isCutPolygon = (data: TbPolygonCoordinates | TbPolygonRawCoordinates): boolean => {
return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || data[0][0] instanceof L.LatLng); return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || data[0][0] instanceof L.LatLng);
} }
@ -1069,3 +1108,31 @@ export const processTooltipTemplate = (template: string): string => {
return template; return template;
} }
export function calculateNewPointCoordinate(coordinate: number, imageSize: number): number {
let pointCoordinate = coordinate / imageSize;
if (pointCoordinate < 0) {
pointCoordinate = 0;
} else if (pointCoordinate > 1) {
pointCoordinate = 1;
}
return pointCoordinate;
}
export function checkLngLat(point: L.LatLng, southWest: L.LatLng, northEast: L.LatLng, offset = 0): L.LatLng {
const maxLngMap = northEast.lng - offset;
const minLngMap = southWest.lng + offset;
const maxLatMap = northEast.lat - offset;
const minLatMap = southWest.lat + offset;
if (point.lng > maxLngMap) {
point.lng = maxLngMap;
} else if (point.lng < minLngMap) {
point.lng = minLngMap;
}
if (point.lat > maxLatMap) {
point.lat = maxLatMap;
} else if (point.lat < minLatMap) {
point.lat = minLatMap;
}
return point;
}

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

@ -262,6 +262,22 @@
formControlName="groups"> formControlName="groups">
</tb-string-items-list> </tb-string-items-list>
</div> </div>
<div class="tb-form-panel" formGroupName="edit">
<div class="tb-form-panel-title">{{ dataLayerEditTitle | translate }}</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widgets.maps.data-layer.edit-instruments</div>
<mat-chip-listbox multiple formControlName="enabledActions">
<mat-chip-option *ngFor="let action of dataLayerEditActions" [value]="action">
{{ dataLayerEditActionTranslationMap.get(action) | translate }}
</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="snappable">
{{ 'widgets.maps.data-layer.enable-snapping' | translate }}
</mat-slide-toggle>
</div>
</div>
<tb-marker-clustering-settings <tb-marker-clustering-settings
*ngIf="dataLayerType === 'markers'" *ngIf="dataLayerType === 'markers'"
formControlName="markerClustering"> formControlName="markerClustering">

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

@ -18,10 +18,12 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { import {
CirclesDataLayerSettings, CirclesDataLayerSettings,
DataLayerEditAction, dataLayerEditActions,
dataLayerEditActionTranslationMap,
defaultBaseMapDataLayerSettings, defaultBaseMapDataLayerSettings,
MapDataLayerSettings, MapDataLayerSettings,
MapDataLayerType, MapDataLayerType,
MapType, MapType, mapZoomActions, mapZoomActionTranslationMap,
MarkersDataLayerSettings, MarkersDataLayerSettings,
MarkerType, MarkerType,
PolygonsDataLayerSettings, PolygonsDataLayerSettings,
@ -71,6 +73,10 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
datasourceTypes: Array<DatasourceType> = []; datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap; datasourceTypesTranslations = datasourceTypeTranslationMap;
dataLayerEditTitle: string;
dataLayerEditActions: Array<DataLayerEditAction> = [];
dataLayerEditActionTranslationMap = dataLayerEditActionTranslationMap;
dataLayerFormGroup: UntypedFormGroup; dataLayerFormGroup: UntypedFormGroup;
settings = this.data.settings; settings = this.data.settings;
@ -111,13 +117,19 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
additionalDataKeys: [this.settings.additionalDataKeys, []], additionalDataKeys: [this.settings.additionalDataKeys, []],
label: [this.settings.label, []], label: [this.settings.label, []],
tooltip: [this.settings.tooltip, []], tooltip: [this.settings.tooltip, []],
groups: [this.settings.groups, []] groups: [this.settings.groups, []],
edit: this.fb.group({
enabledActions: [this.settings.edit?.enabledActions, []],
snappable: [this.settings.edit?.snappable, []]
})
}); });
switch (this.dataLayerType) { switch (this.dataLayerType) {
case 'markers': case 'markers':
const markersDataLayer = this.settings as MarkersDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.marker.marker-configuration'; this.dialogTitle = 'widgets.maps.data-layer.marker.marker-configuration';
this.dataLayerEditTitle = 'widgets.maps.data-layer.marker.edit';
this.dataLayerEditActions = [DataLayerEditAction.add, DataLayerEditAction.move, DataLayerEditAction.remove];
const markersDataLayer = this.settings as MarkersDataLayerSettings;
this.dataLayerFormGroup.addControl('xKey', this.fb.control(markersDataLayer.xKey, Validators.required)); this.dataLayerFormGroup.addControl('xKey', this.fb.control(markersDataLayer.xKey, Validators.required));
this.dataLayerFormGroup.addControl('yKey', this.fb.control(markersDataLayer.yKey, Validators.required)) this.dataLayerFormGroup.addControl('yKey', this.fb.control(markersDataLayer.yKey, Validators.required))
this.dataLayerFormGroup.addControl('markerType', this.fb.control(markersDataLayer.markerType, Validators.required)); this.dataLayerFormGroup.addControl('markerType', this.fb.control(markersDataLayer.markerType, Validators.required));
@ -138,17 +150,20 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
break; break;
case 'polygons': case 'polygons':
case 'circles': case 'circles':
this.dataLayerEditActions = dataLayerEditActions;
const shapeDataLayer = this.settings as ShapeDataLayerSettings; const shapeDataLayer = this.settings as ShapeDataLayerSettings;
this.dataLayerFormGroup.addControl('fillColor', this.fb.control(shapeDataLayer.fillColor, Validators.required)); this.dataLayerFormGroup.addControl('fillColor', this.fb.control(shapeDataLayer.fillColor, Validators.required));
this.dataLayerFormGroup.addControl('strokeColor', this.fb.control(shapeDataLayer.strokeColor, Validators.required)); this.dataLayerFormGroup.addControl('strokeColor', this.fb.control(shapeDataLayer.strokeColor, Validators.required));
this.dataLayerFormGroup.addControl('strokeWeight', this.fb.control(shapeDataLayer.strokeWeight, [Validators.required, Validators.min(0)])); this.dataLayerFormGroup.addControl('strokeWeight', this.fb.control(shapeDataLayer.strokeWeight, [Validators.required, Validators.min(0)]));
if (this.dataLayerType === 'polygons') { if (this.dataLayerType === 'polygons') {
const polygonsDataLayer = this.settings as PolygonsDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.polygon.polygon-configuration'; this.dialogTitle = 'widgets.maps.data-layer.polygon.polygon-configuration';
this.dataLayerEditTitle = 'widgets.maps.data-layer.marker.edit';
const polygonsDataLayer = this.settings as PolygonsDataLayerSettings;
this.dataLayerFormGroup.addControl('polygonKey', this.fb.control(polygonsDataLayer.polygonKey, Validators.required)); this.dataLayerFormGroup.addControl('polygonKey', this.fb.control(polygonsDataLayer.polygonKey, Validators.required));
} else { } else {
const circlesDataLayer = this.settings as CirclesDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.circle.circle-configuration'; this.dialogTitle = 'widgets.maps.data-layer.circle.circle-configuration';
this.dataLayerEditTitle = 'widgets.maps.data-layer.circle.edit';
const circlesDataLayer = this.settings as CirclesDataLayerSettings;
this.dataLayerFormGroup.addControl('circleKey', this.fb.control(circlesDataLayer.circleKey, Validators.required)); this.dataLayerFormGroup.addControl('circleKey', this.fb.control(circlesDataLayer.circleKey, Validators.required));
} }
break; break;
@ -304,4 +319,6 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
} }
protected readonly mapProvider = MapProviders; protected readonly mapProvider = MapProviders;
protected readonly mapZoomActions = mapZoomActions;
protected readonly mapZoomActionTranslationMap = mapZoomActionTranslationMap;
} }

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

@ -7664,6 +7664,12 @@
"tooltip-offset": "Tooltip offset", "tooltip-offset": "Tooltip offset",
"tooltip-offset-horizontal": "Horizontal", "tooltip-offset-horizontal": "Horizontal",
"tooltip-offset-vertical": "Vertical", "tooltip-offset-vertical": "Vertical",
"action-add": "Add",
"action-edit": "Edit",
"action-move": "Move",
"action-remove": "Remove",
"edit-instruments": "Instruments",
"enable-snapping": "Enable snapping to other vertices for precision drawing",
"marker": { "marker": {
"latitude-key": "Latitude key", "latitude-key": "Latitude key",
"longitude-key": "Longitude key", "longitude-key": "Longitude key",
@ -7709,7 +7715,8 @@
"lazy-load": "Use lazy load for adding markers", "lazy-load": "Use lazy load for adding markers",
"use-cluster-marker-color-function": "Use cluster markers color function", "use-cluster-marker-color-function": "Use cluster markers color function",
"marker-color-function": "Marker color function" "marker-color-function": "Marker color function"
} },
"edit": "Edit marker"
}, },
"polygon": { "polygon": {
"polygon-key": "Polygon key", "polygon-key": "Polygon key",
@ -7717,7 +7724,8 @@
"no-polygons": "No polygons configured", "no-polygons": "No polygons configured",
"add-polygon": "Add polygon", "add-polygon": "Add polygon",
"polygon-configuration": "Polygon configuration", "polygon-configuration": "Polygon configuration",
"remove-polygon": "Remove polygon" "remove-polygon": "Remove polygon",
"edit": "Edit polygon"
}, },
"circle": { "circle": {
"circle-key": "Circle key", "circle-key": "Circle key",
@ -7725,7 +7733,8 @@
"no-circles": "No circles configured", "no-circles": "No circles configured",
"add-circle": "Add circle", "add-circle": "Add circle",
"circle-configuration": "Circle configuration", "circle-configuration": "Circle configuration",
"remove-circle": "Remove circle" "remove-circle": "Remove circle",
"edit": "Edit circle"
} }
}, },
"select-entity": "Select entity", "select-entity": "Select entity",

Loading…
Cancel
Save