Browse Source

UI: Maps refactoring.

pull/12866/head
Igor Kulikov 2 years ago
parent
commit
dd5ec734d3
  1. 1
      application/src/main/data/json/system/widget_bundles/maps.json
  2. 2
      application/src/main/data/json/system/widget_types/image_map.json
  3. 2
      application/src/main/data/json/system/widget_types/map.json
  4. 80
      application/src/main/data/json/system/widget_types/route_map.json
  5. 4
      application/src/main/data/json/system/widget_types/trip_map.json
  6. 64
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts
  7. 415
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts
  8. 484
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
  9. 669
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts
  10. 68
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts
  11. 18
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
  12. 568
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
  13. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts
  14. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts
  15. 99
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  16. 79
      ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts
  17. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts
  18. 4
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  19. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

1
application/src/main/data/json/system/widget_bundles/maps.json

@ -12,6 +12,7 @@
"map",
"image_map",
"trip_map",
"route_map",
"maps_v2.openstreetmap",
"maps_v2.google_maps",
"maps_v2.image_map",

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

80
application/src/main/data/json/system/widget_types/route_map.json

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -17,7 +17,7 @@
import {
CirclesDataLayerSettings,
defaultBaseCirclesDataLayerSettings,
isJSON,
isJSON, MapDataLayerType,
TbCircleData,
TbMapDatasource
} from '@home/components/widget/lib/maps/models/map.models';
@ -28,13 +28,12 @@ import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
import { isNotEmptyStr } from '@core/utils';
import {
MapDataLayerType,
TbDataLayerItem,
TbLatestDataLayerItem,
UnplacedMapDataItem
} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { map } from 'rxjs/operators';
class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, TbCirclesDataLayer> {
class TbCircleDataLayerItem extends TbLatestDataLayerItem<CirclesDataLayerSettings, TbCirclesDataLayer> {
private circle: L.Circle;
private circleStyle: L.PathOptions;
@ -47,6 +46,14 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
super(data, dsData, settings, dataLayer);
}
public isEditing() {
return this.editing;
}
public updateBubblingMouseEvents() {
this.circle.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Layer {
const circleData = this.dataLayer.extractCircleCoordinates(data);
const center = new L.LatLng(circleData.latitude, circleData.longitude);
@ -141,14 +148,6 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
return this.dataLayer.saveCircleCoordinates(this.data, null, null);
}
public isEditing() {
return this.editing;
}
public updateBubblingMouseEvents() {
this.circle.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
private saveCircleCoordinates() {
const center = this.circle.getLatLng();
const radius = this.circle.getRadius();
@ -178,7 +177,7 @@ export class TbCirclesDataLayer extends TbShapesDataLayer<CirclesDataLayerSettin
}
public dataLayerType(): MapDataLayerType {
return MapDataLayerType.circle;
return 'circles';
}
public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void {
@ -196,6 +195,24 @@ export class TbCirclesDataLayer extends TbShapesDataLayer<CirclesDataLayerSettin
}
}
public extractCircleCoordinates(data: FormattedData<TbMapDatasource>) {
const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]);
return this.map.circleDataToCoordinates(circleData);
}
public saveCircleCoordinates(data: FormattedData<TbMapDatasource>, center: L.LatLng, radius: number): Observable<TbCircleData> {
const converted = center ? this.map.coordinatesToCircleData(center, radius) : null;
const circleData = [
{
dataKey: this.settings.circleKey,
value: converted
}
];
return this.map.saveItemData(data.$datasource, circleData).pipe(
map(() => converted)
);
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.circleKey);
return datasource;
@ -213,25 +230,8 @@ export class TbCirclesDataLayer extends TbShapesDataLayer<CirclesDataLayerSettin
return layerData && isNotEmptyStr(layerData[this.settings.circleKey.label]) && isJSON(layerData[this.settings.circleKey.label]);
}
protected createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbDataLayerItem<CirclesDataLayerSettings, TbCirclesDataLayer> {
protected createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbLatestDataLayerItem<CirclesDataLayerSettings, TbCirclesDataLayer> {
return new TbCircleDataLayerItem(data, dsData, this.settings, this);
}
public extractCircleCoordinates(data: FormattedData<TbMapDatasource>) {
const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]);
return this.map.circleDataToCoordinates(circleData);
}
public saveCircleCoordinates(data: FormattedData<TbMapDatasource>, center: L.LatLng, radius: number): Observable<TbCircleData> {
const converted = center ? this.map.coordinatesToCircleData(center, radius) : null;
const circleData = [
{
dataKey: this.settings.circleKey,
value: converted
}
];
return this.map.saveItemData(data.$datasource, circleData).pipe(
map(() => converted)
);
}
}

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

@ -0,0 +1,415 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
DataLayerEditAction,
MapDataLayerSettings,
TbMapDatasource
} from '@home/components/widget/lib/maps/models/map.models';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { FormattedData, WidgetActionType } from '@shared/models/widget.models';
import { Observable } from 'rxjs';
import L from 'leaflet';
import { createTooltip, updateTooltip } from './data-layer-utils';
import { TbDataLayerItem, TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
export abstract class TbLatestDataLayerItem<S extends MapDataLayerSettings = MapDataLayerSettings,
D extends TbLatestMapDataLayer<S,D> = TbLatestMapDataLayer<any>, L extends L.Layer = L.Layer> extends TbDataLayerItem<S,D,L> {
protected tooltip: L.Popup;
protected data: FormattedData<TbMapDatasource>;
protected selected = false;
protected constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
settings: S,
dataLayer: D) {
super(settings, dataLayer);
this.data = data;
this.layer = this.create(data, dsData);
if (this.settings.tooltip?.show) {
this.tooltip = createTooltip(this.dataLayer.getMap(),
this.layer, this.settings.tooltip, this.data, () => {
return !this.isEditing();
});
updateTooltip(this.dataLayer.getMap(), this.tooltip,
this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData);
}
this.bindEvents();
try {
this.dataLayer.getDataLayerContainer().addLayer(this.layer);
this.editModeUpdated();
} catch (e) {
console.warn(e);
}
}
public invalidateCoordinates(): void {
this.doInvalidateCoordinates(this.data, this.dataLayer.getMap().getData());
}
public select(): L.TB.ToolbarButtonOptions[] {
if (!this.selected) {
this.selected = true;
this.disableEdit();
this.updateSelectedState();
const buttons = this.onSelected();
if (this.dataLayer.isRemoveEnabled()) {
buttons.push({
id: 'remove',
title: this.removeDataItemTitle(),
click: () => {
this.removeDataItem().subscribe(
() => this.dataLayer.removeItem(this.data.entityId)
);
},
iconClass: 'tb-remove'
});
}
return buttons;
} else {
return [];
}
}
public deselect(cancel = false, force = false): boolean {
if (this.selected) {
if (this.canDeselect(cancel) || force) {
this.selected = false;
this.layer.closePopup();
this.updateSelectedState();
this.onDeselected();
this.editModeUpdated();
} else {
return false;
}
}
return true;
}
public isSelected() {
return this.selected;
}
public editModeUpdated() {
if (this.dataLayer.isEditMode() && !this.selected) {
this.enableEdit();
} else {
this.disableEdit();
}
this.updateSelectedState();
this.updateBubblingMouseEvents();
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.data = data;
this.doUpdate(data, dsData);
}
public remove() {
if (this.selected) {
this.dataLayer.getMap().deselectItem(false, true);
}
this.dataLayer.getDataLayerContainer().removeLayer(this.layer);
this.layer.off();
}
public isEditing() {
return false;
}
protected bindEvents(): void {
if (this.dataLayer.isSelectable()) {
this.layer.on('click', () => {
if (!this.isEditing()) {
this.dataLayer.getMap().selectItem(this);
}
});
}
const clickAction = this.settings.click;
if (clickAction && clickAction.type !== WidgetActionType.doNothing) {
this.layer.on('click', (event) => {
this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.data);
});
}
}
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');
}
}
protected updateSelectedState() {
if (this.selected) {
this.addItemClass('tb-selected');
} else {
this.removeItemClass('tb-selected');
}
}
protected updateTooltip(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
if (this.settings.tooltip.show) {
updateTooltip(this.dataLayer.getMap(), this.tooltip,
this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData);
}
}
protected updateLabel(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
if (this.settings.label.show) {
this.unbindLabel();
const label = this.dataLayer.dataLayerLabelProcessor.processPattern(data, dsData);
const labelColor = this.dataLayer.getCtx().widgetConfig.color;
const content: L.Content = `<div style="color: ${labelColor};"><b>${label}</b></div>`;
this.bindLabel(content);
}
}
protected canDeselect(cancel = false): boolean {
return true;
}
protected onSelected(): L.TB.ToolbarButtonOptions[] {
return [];
}
protected onDeselected(): void {}
protected abstract create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L;
protected abstract doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void;
protected abstract doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void;
protected abstract unbindLabel(): void;
protected abstract bindLabel(content: L.Content): void;
protected abstract addItemClass(clazz: string): void;
protected abstract removeItemClass(clazz: string): void;
protected abstract enableDrag(): void;
protected abstract disableDrag(): void;
protected abstract updateBubblingMouseEvents(): void;
protected abstract removeDataItemTitle(): string;
protected abstract removeDataItem(): Observable<any>;
}
export interface UnplacedMapDataItem {
entity: FormattedData<TbMapDatasource>;
dataLayer: TbLatestMapDataLayer;
}
export abstract class TbLatestMapDataLayer<S extends MapDataLayerSettings = MapDataLayerSettings,
D extends TbLatestMapDataLayer<S,D> = any, L extends L.Layer = L.Layer> extends TbMapDataLayer<S, TbLatestDataLayerItem<S,D,L>> implements L.TB.DataLayer {
protected addEnabled = false;
protected dragEnabled = false;
protected editEnabled = false;
protected removeEnabled = false;
protected editable = false;
protected selectable = false;
protected hoverable = false;
private editMode = false;
private unplacedItems: UnplacedMapDataItem[] = [];
protected constructor(map: TbMap<any>,
inputSettings: S) {
super(map, inputSettings);
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.editable = this.addEnabled || this.dragEnabled || this.editEnabled || this.removeEnabled;
this.selectable = this.removeEnabled || this.editEnabled;
this.hoverable = this.selectable || this.dragEnabled;
}
this.snappable = this.settings.edit?.snappable;
this.enableEditMode();
}
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 isEditable(): boolean {
return this.editable;
}
public isHoverable(): boolean {
return this.hoverable;
}
public isSelectable(): boolean {
return this.selectable;
}
public isSnappable(): boolean {
return this.snappable;
}
public updateData(dsData: FormattedData<TbMapDatasource>[]) {
this.unplacedItems.length = 0;
const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId));
const toDelete = new Set(Array.from(this.layerItems.keys()));
const updatedItems: TbLatestDataLayerItem<S,D,L>[] = [];
layerData.forEach((data) => {
if (this.isValidLayerData(data)) {
let layerItem = this.layerItems.get(data.entityId);
if (layerItem) {
layerItem.update(data, dsData);
updatedItems.push(layerItem);
} else {
layerItem = this.createLayerItem(data, dsData);
this.layerItems.set(data.entityId, layerItem);
}
toDelete.delete(data.entityId);
} else {
this.unplacedItems.push({
entity: data,
dataLayer: this
});
}
});
toDelete.forEach((key) => {
this.removeItem(key);
});
if (updatedItems.length) {
this.layerItemsUpdated(updatedItems);
}
}
public hasUnplacedItems(): boolean {
return !!this.unplacedItems.length;
}
public getUnplacedItems(): UnplacedMapDataItem[] {
return this.prepareUnplacedItems();
}
public enableEditMode() {
if (this.editable) {
if (!this.editMode) {
this.editMode = true;
this.updateItemsEditMode();
}
}
}
public disableEditMode() {
if (this.editMode) {
this.editMode = false;
this.updateItemsEditMode();
}
}
protected createDataLayerContainer(): L.FeatureGroup {
return L.featureGroup([], {snapIgnore: !this.settings.edit?.snappable});
}
protected onDataLayerEnabled(): void {
this.updateItemsEditMode();
}
protected onDataLayerDisabled(): void {
for (const item of this.layerItems) {
if (item[1].isSelected()) {
this.getMap().deselectItem(false, true);
break;
}
}
}
protected createItemFromUnplaced(unplacedItem: UnplacedMapDataItem): void {
const index = this.unplacedItems.indexOf(unplacedItem);
if (index > -1) {
this.unplacedItems.splice(index, 1);
const layerItem = this.createLayerItem(unplacedItem.entity, this.map.getData());
this.layerItems.set(unplacedItem.entity.entityId, layerItem);
this.map.enabledDataLayersUpdated();
}
}
protected layerItemsUpdated(_updatedItems: TbLatestDataLayerItem<S,D,L>[]): void {
}
private prepareUnplacedItems(): UnplacedMapDataItem[] {
const div = document.createElement('div');
for (const item of this.unplacedItems) {
if (!item.entity.entityDisplayName) {
if (this.settings.label.show) {
div.innerHTML = this.dataLayerLabelProcessor.processPattern(item.entity, this.getMap().getData());
item.entity.entityDisplayName = div.textContent || div.innerText || '';
} else {
item.entity.entityDisplayName = item.entity.entityName;
}
}
}
return this.unplacedItems;
}
private updateItemsEditMode() {
this.layerItems.forEach(item => item.editModeUpdated());
}
public abstract placeItem(item: UnplacedMapDataItem, layer: L.Layer): void;
protected abstract isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean;
protected abstract createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbLatestDataLayerItem<S,D,L>;
}

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

@ -15,260 +15,34 @@
///
import {
DataLayerColorSettings,
DataLayerColorType,
DataLayerEditAction,
DataLayerPatternSettings,
DataLayerPatternType,
DataLayerTooltipTrigger,
MapDataLayerSettings,
mapDataSourceSettingsToDatasource,
MapStringFunction,
MapType,
processTooltipTemplate,
DataLayerColorSettings, DataLayerColorType,
DataLayerPatternSettings, DataLayerPatternType,
MapDataLayerSettings, MapDataLayerType, mapDataSourceSettingsToDatasource,
MapStringFunction, MapType,
TbMapDatasource
} from '@home/components/widget/lib/maps/models/map.models';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { FormattedData, WidgetActionType } from '@shared/models/widget.models';
import { forkJoin, Observable, of } from 'rxjs';
import {
createLabelFromPattern,
guid,
isDefined,
guid, isDefined,
mergeDeepIgnoreArray,
parseTbFunction,
safeExecuteTbFunction
} from '@core/utils';
import L from 'leaflet';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { forkJoin, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { WidgetContext } from '@home/models/widget-component.models';
import { FormattedData } from '@shared/models/widget.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { createTooltip, updateTooltip } from './data-layer-utils';
export abstract class TbDataLayerItem<S extends MapDataLayerSettings = MapDataLayerSettings,
D extends TbMapDataLayer<S,D> = TbMapDataLayer<any, any>, L extends L.Layer = L.Layer> {
protected layer: L;
protected tooltip: L.Popup;
protected data: FormattedData<TbMapDatasource>;
protected selected = false;
protected constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
protected settings: S,
protected dataLayer: D) {
this.data = data;
this.layer = this.create(data, dsData);
if (this.settings.tooltip?.show) {
this.tooltip = createTooltip(this.dataLayer.getMap(),
this.layer, this.settings.tooltip, this.data, () => {
return !this.isEditing();
});
updateTooltip(this.dataLayer.getMap(), this.tooltip,
this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData);
}
this.bindEvents();
try {
this.dataLayer.getDataLayerContainer().addLayer(this.layer);
this.editModeUpdated();
} catch (e) {
console.warn(e);
}
}
protected abstract create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L;
protected abstract doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void;
protected abstract doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void;
protected abstract unbindLabel(): void;
protected abstract bindLabel(content: L.Content): void;
protected abstract addItemClass(clazz: string): void;
protected abstract removeItemClass(clazz: string): void;
protected abstract enableDrag(): void;
protected abstract disableDrag(): void;
protected abstract updateBubblingMouseEvents(): void;
protected bindEvents(): void {
if (this.dataLayer.isSelectable()) {
this.layer.on('click', () => {
if (!this.isEditing()) {
this.dataLayer.getMap().selectItem(this);
}
});
}
const clickAction = this.settings.click;
if (clickAction && clickAction.type !== WidgetActionType.doNothing) {
this.layer.on('click', (event) => {
this.dataLayer.getMap().dataItemClick(event.originalEvent, clickAction, this.data);
});
}
}
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');
}
}
protected updateSelectedState() {
if (this.selected) {
this.addItemClass('tb-selected');
} else {
this.removeItemClass('tb-selected');
}
}
public invalidateCoordinates(): void {
this.doInvalidateCoordinates(this.data, this.dataLayer.getMap().getData());
}
public select(): L.TB.ToolbarButtonOptions[] {
if (!this.selected) {
this.selected = true;
this.disableEdit();
this.updateSelectedState();
const buttons = this.onSelected();
if (this.dataLayer.isRemoveEnabled()) {
buttons.push({
id: 'remove',
title: this.removeDataItemTitle(),
click: () => {
this.removeDataItem().subscribe(
() => this.dataLayer.removeItem(this.data.entityId)
);
},
iconClass: 'tb-remove'
});
}
return buttons;
} else {
return [];
}
}
public deselect(cancel = false, force = false): boolean {
if (this.selected) {
if (this.canDeselect(cancel) || force) {
this.selected = false;
this.layer.closePopup();
this.updateSelectedState();
this.onDeselected();
this.editModeUpdated();
} else {
return false;
}
}
return true;
}
public isSelected() {
return this.selected;
}
public editModeUpdated() {
if (this.dataLayer.isEditMode() && !this.selected) {
this.enableEdit();
} else {
this.disableEdit();
}
this.updateSelectedState();
this.updateBubblingMouseEvents();
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.data = data;
this.doUpdate(data, dsData);
}
public remove() {
if (this.selected) {
this.dataLayer.getMap().deselectItem(false, true);
}
this.dataLayer.getDataLayerContainer().removeLayer(this.layer);
this.layer.off();
}
public getLayer(): L {
return this.layer;
}
public getDataLayer(): D {
return this.dataLayer;
}
public isEditing() {
return false;
}
protected updateTooltip(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
if (this.settings.tooltip.show) {
updateTooltip(this.dataLayer.getMap(), this.tooltip,
this.settings.tooltip, this.dataLayer.dataLayerTooltipProcessor, data, dsData);
}
}
protected updateLabel(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
if (this.settings.label.show) {
this.unbindLabel();
const label = this.dataLayer.dataLayerLabelProcessor.processPattern(data, dsData);
const labelColor = this.dataLayer.getCtx().widgetConfig.color;
const content: L.Content = `<div style="color: ${labelColor};"><b>${label}</b></div>`;
this.bindLabel(content);
}
}
protected canDeselect(cancel = false): boolean {
return true;
}
protected onSelected(): L.TB.ToolbarButtonOptions[] {
return [];
}
protected onDeselected(): void {}
protected abstract removeDataItemTitle(): string;
protected abstract removeDataItem(): Observable<any>;
}
export enum MapDataLayerType {
trip = 'trip',
marker = 'marker',
polygon = 'polygon',
circle = 'circle'
}
import { TbMap } from '@home/components/widget/lib/maps/map';
import { WidgetContext } from '@home/models/widget-component.models';
export class DataLayerPatternProcessor {
private patternFunction: CompiledTbFunction<MapStringFunction>;
private pattern: string;
constructor(private dataLayer: TbMapDataLayer<any, any>,
constructor(private dataLayer: TbMapDataLayer,
private settings: DataLayerPatternSettings) {}
public setup(): Observable<void> {
@ -304,7 +78,7 @@ export class DataLayerColorProcessor {
private colorFunction: CompiledTbFunction<MapStringFunction>;
private color: string;
constructor(private dataLayer: TbMapDataLayer<any, any>,
constructor(private dataLayer: TbMapDataLayer,
private settings: DataLayerColorSettings) {}
public setup(): Observable<void> {
@ -333,12 +107,28 @@ export class DataLayerColorProcessor {
}
export interface UnplacedMapDataItem {
entity: FormattedData<TbMapDatasource>;
dataLayer: TbMapDataLayer;
export abstract class TbDataLayerItem<S extends MapDataLayerSettings = MapDataLayerSettings, D extends TbMapDataLayer = TbMapDataLayer, L extends L.Layer = L.Layer> {
protected layer: L;
protected constructor(protected settings: S,
protected dataLayer: D) {}
public getLayer(): L {
return this.layer;
}
public getDataLayer(): D {
return this.dataLayer;
}
public abstract remove(): void;
public abstract invalidateCoordinates(): void;
}
export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLayerSettings, D extends TbMapDataLayer<S,D> = any, L extends L.Layer = L.Layer> implements L.TB.DataLayer {
export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLayerSettings, I extends TbDataLayerItem = any> {
protected settings: S;
@ -348,26 +138,14 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
protected dataLayerContainer: L.FeatureGroup;
protected layerItems = new Map<string, TbDataLayerItem<S,D,L>>();
protected layerItems = new Map<string, I>();
protected groupsState: {[group: string]: boolean} = {};
protected enabled = true;
protected addEnabled = false;
protected dragEnabled = false;
protected editEnabled = false;
protected removeEnabled = false;
protected editable = false;
protected selectable = false;
protected hoverable = false;
protected snappable = false;
private editMode = false;
private unplacedItems: UnplacedMapDataItem[] = [];
public dataLayerLabelProcessor: DataLayerPatternProcessor;
public dataLayerTooltipProcessor: DataLayerPatternProcessor;
@ -379,24 +157,10 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
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.editable = this.addEnabled || this.dragEnabled || this.editEnabled || this.removeEnabled;
this.selectable = this.removeEnabled || this.editEnabled;
this.hoverable = this.selectable || this.dragEnabled;
this.snappable = this.settings.edit.snappable;
}
this.dataLayerContainer = this.createDataLayerContainer();
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.map.getMap().addLayer(this.dataLayerContainer);
this.enableEditMode();
}
public setup(): Observable<any> {
@ -412,56 +176,44 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
]);
}
public getDatasource(): TbMapDatasource {
return this.datasource;
}
public getDataLayerContainer(): L.FeatureGroup {
return this.dataLayerContainer;
}
public getBounds(): L.LatLngBounds {
return this.dataLayerContainer.getBounds();
}
public isEnabled(): boolean {
return this.enabled;
}
public isEditMode(): boolean {
return this.editMode;
public removeItem(key: string): void {
const item = this.layerItems.get(key);
if (item) {
item.remove();
this.layerItems.delete(key);
}
}
public isAddEnabled(): boolean {
return this.addEnabled;
public invalidateCoordinates(): void {
this.layerItems.forEach(item => item.invalidateCoordinates());
}
public isDragEnabled(): boolean {
return this.dragEnabled;
public getCtx(): WidgetContext {
return this.map.getCtx();
}
public isEditEnabled(): boolean {
return this.editEnabled;
public getMap(): TbMap<any> {
return this.map;
}
public isRemoveEnabled(): boolean {
return this.removeEnabled;
public mapType(): MapType {
return this.map.type();
}
public isEditable(): boolean {
return this.editable;
public getDatasource(): TbMapDatasource {
return this.datasource;
}
public isHoverable(): boolean {
return this.hoverable;
public getDataLayerContainer(): L.FeatureGroup {
return this.dataLayerContainer;
}
public isSelectable(): boolean {
return this.selectable;
public getBounds(): L.LatLngBounds {
return this.dataLayerContainer.getBounds();
}
public isSnappable(): boolean {
return this.snappable;
public isEnabled(): boolean {
return this.enabled;
}
public getGroups(): string[] {
@ -476,14 +228,9 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
this.enabled = enabled;
if (this.enabled) {
this.map.getMap().addLayer(this.dataLayerContainer);
this.updateItemsEditMode();
this.onDataLayerEnabled();
} else {
for (const item of this.layerItems) {
if (item[1].isSelected()) {
this.getMap().deselectItem(false, true);
break;
}
}
this.onDataLayerDisabled();
this.map.getMap().removeLayer(this.dataLayerContainer);
}
this.map.enabledDataLayersUpdated();
@ -493,136 +240,23 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
return false;
}
public updateData(dsData: FormattedData<TbMapDatasource>[]) {
this.unplacedItems.length = 0;
const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId));
const toDelete = new Set(Array.from(this.layerItems.keys()));
const updatedItems: TbDataLayerItem<S,D,L>[] = [];
layerData.forEach((data) => {
if (this.isValidLayerData(data)) {
let layerItem = this.layerItems.get(data.entityId);
if (layerItem) {
layerItem.update(data, dsData);
updatedItems.push(layerItem);
} else {
layerItem = this.createLayerItem(data, dsData);
this.layerItems.set(data.entityId, layerItem);
}
toDelete.delete(data.entityId);
} else {
this.unplacedItems.push({
entity: data,
dataLayer: this
});
}
});
toDelete.forEach((key) => {
this.removeItem(key);
});
if (updatedItems.length) {
this.layerItemsUpdated(updatedItems);
}
}
public removeItem(key: string): void {
const item = this.layerItems.get(key);
if (item) {
item.remove();
this.layerItems.delete(key);
}
}
protected createItemFromUnplaced(unplacedItem: UnplacedMapDataItem): void {
const index = this.unplacedItems.indexOf(unplacedItem);
if (index > -1) {
this.unplacedItems.splice(index, 1);
const layerItem = this.createLayerItem(unplacedItem.entity, this.map.getData());
this.layerItems.set(unplacedItem.entity.entityId, layerItem);
this.map.enabledDataLayersUpdated();
}
}
public invalidateCoordinates(): void {
this.layerItems.forEach(item => item.invalidateCoordinates());
}
public getCtx(): WidgetContext {
return this.map.getCtx();
}
public getMap(): TbMap<any> {
return this.map;
}
public hasUnplacedItems(): boolean {
return !!this.unplacedItems.length;
}
private prepareUnplacedItems(): UnplacedMapDataItem[] {
const div = document.createElement('div');
for (const item of this.unplacedItems) {
if (!item.entity.entityDisplayName) {
if (this.settings.label.show) {
div.innerHTML = this.dataLayerLabelProcessor.processPattern(item.entity, this.getMap().getData());
item.entity.entityDisplayName = div.textContent || div.innerText || '';
} else {
item.entity.entityDisplayName = item.entity.entityName;
}
}
}
return this.unplacedItems;
}
public getUnplacedItems(): UnplacedMapDataItem[] {
return this.prepareUnplacedItems();
}
protected createDataLayerContainer(): L.FeatureGroup {
return L.featureGroup([], {snapIgnore: !this.snappable});
return L.featureGroup([], {snapIgnore: true});
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
return datasource;
}
protected layerItemsUpdated(_updatedItems: TbDataLayerItem<S,D,L>[]): void {
}
protected onDataLayerEnabled(): void {}
protected mapType(): MapType {
return this.map.type();
}
public enableEditMode() {
if (this.editable) {
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());
}
protected onDataLayerDisabled(): void {}
public abstract dataLayerType(): MapDataLayerType;
public abstract placeItem(item: UnplacedMapDataItem, layer: L.Layer): void;
protected abstract defaultBaseSettings(map: TbMap<any>): Partial<S>;
protected abstract doSetup(): Observable<any>;
protected abstract isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean;
protected abstract createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbDataLayerItem<S,D,L>;
}

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

@ -20,7 +20,7 @@ import {
DataLayerColorType,
defaultBaseMarkersDataLayerSettings,
isValidLatLng,
loadImageWithAspect,
loadImageWithAspect, MapDataLayerType,
MapStringFunction,
MapType,
MarkerIconInfo,
@ -59,208 +59,159 @@ import {
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import {
MapDataLayerType,
TbDataLayerItem,
TbMapDataLayer, UnplacedMapDataItem
} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
TbLatestDataLayerItem,
TbLatestMapDataLayer, UnplacedMapDataItem
} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { TbImageMap } from '@home/components/widget/lib/maps/image-map';
import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, TbMarkersDataLayer, L.Marker> {
export class MarkerDataProcessor<S extends MarkersDataLayerSettings = MarkersDataLayerSettings> {
private marker: L.Marker;
private labelOffset: L.PointTuple;
private iconClassList: string[];
private moving = false;
constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
protected settings: MarkersDataLayerSettings,
protected dataLayer: TbMarkersDataLayer) {
super(data, dsData, settings, dataLayer);
}
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Marker {
this.iconClassList = [];
const location = this.dataLayer.extractLocation(data, dsData);
this.marker = L.marker(location, {
tbMarkerData: data,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.updateMarkerIcon(data, dsData);
return this.marker;
}
protected unbindLabel() {
this.marker.unbindTooltip();
}
protected bindLabel(content: L.Content): void {
this.marker.bindTooltip(content, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset });
}
protected doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.marker.options.tbMarkerData = data;
this.updateMarkerLocation(data, dsData);
this.updateTooltip(data, dsData);
this.updateMarkerIcon(data, dsData);
}
private positionFunction: CompiledTbFunction<MarkerPositionFunction>;
private markerIconProcessor: MarkerIconProcessor<any>;
protected doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.updateMarkerLocation(data, dsData);
constructor(public dataLayer: TbMapDataLayer,
private settings: S,
public markerOffset: L.LatLngTuple,
public tooltipOffset: L.LatLngTuple,
private trip = false) {
}
protected addItemClass(clazz: string): void {
if (!this.iconClassList.includes(clazz)) {
this.iconClassList.push(clazz);
this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className);
if ((this.marker as any)._icon) {
L.DomUtil.addClass((this.marker as any)._icon, clazz);
}
public setup(): Observable<any> {
this.markerIconProcessor = MarkerIconProcessor.fromSettings(this, this.settings);
const setup$: Observable<void>[] = [this.markerIconProcessor.setup()];
if (this.dataLayer.mapType() === MapType.image) {
setup$.push(
parseTbFunction<MarkerPositionFunction>(this.dataLayer.getCtx().http, this.settings.positionFunction, ['origXPos', 'origYPos', 'data', 'dsData', 'aspect']).pipe(
map((parsed) => {
this.positionFunction = parsed;
return null;
})
)
);
}
return forkJoin(setup$).pipe(map(() => null));
}
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, clazz);
if ((this.marker as any)._icon) {
L.DomUtil.removeClass((this.marker as any)._icon, clazz);
public extractLocation(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.LatLng {
let locationData = this.extractLocationData(data);
if (locationData) {
if (this.dataLayer.mapType() === MapType.image && this.positionFunction) {
const imageMap = this.dataLayer.getMap() as TbImageMap;
locationData = this.positionFunction.execute(locationData.x, locationData.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0};
}
}
}
protected enableDrag(): void {
if (this.settings.markerClustering?.enable) {
this.marker.options.draggable = true;
this.marker.on('dragstart', () => {
this.moving = true;
});
this.marker.on('dragend', () => {
this.saveMarkerLocation();
this.moving = false;
});
return this.dataLayer.getMap().locationDataToLatLng(locationData);
} else {
this.marker.pm.setOptions({
snappable: this.dataLayer.isSnappable()
});
this.marker.pm.enableLayerDrag();
this.marker.on('pm:dragstart', () => {
this.moving = true;
});
this.marker.on('pm:dragend', () => {
this.saveMarkerLocation();
this.moving = false;
});
return null;
}
}
protected disableDrag(): void {
if (this.settings.markerClustering?.enable) {
this.marker.options.draggable = false;
this.marker.off('dragstart');
this.marker.off('dragend');
public extractLocationData(data: FormattedData<TbMapDatasource>): {x: number; y: number} {
if (data) {
const xKeyVal = data[this.settings.xKey.label];
const yKeyVal = data[this.settings.yKey.label];
switch (this.dataLayer.mapType()) {
case MapType.geoMap:
if (!isValidLatLng(xKeyVal, yKeyVal)) {
return null;
}
break;
case MapType.image:
if (!isDefinedAndNotNull(xKeyVal) || isEmptyStr(xKeyVal) || isNaN(xKeyVal) || !isDefinedAndNotNull(yKeyVal) || isEmptyStr(yKeyVal) || isNaN(yKeyVal)) {
return null;
}
break;
}
return {x: xKeyVal, y: yKeyVal};
} else {
this.marker.pm.disableLayerDrag();
this.marker.off('pm:dragstart');
this.marker.off('pm:dragend');
return null;
}
}
protected removeDataItemTitle(): string {
return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.marker.remove-marker-for', {entityName: this.data.entityName});
}
protected removeDataItem(): Observable<any> {
return this.dataLayer.saveMarkerLocation(this.data, null);
}
public isEditing() {
return this.moving;
}
public updateBubblingMouseEvents() {
this.marker.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
private saveMarkerLocation() {
const location = this.marker.getLatLng();
this.dataLayer.saveMarkerLocation(this.data, location).subscribe();
public createMarkerIcon(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
rotationAngle?: number): Observable<MarkerIconInfo> {
return this.markerIconProcessor.createMarkerIcon(data, dsData, rotationAngle);
}
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);
}
public createDefaultMarkerIcon(rotationAngle = 0): Observable<MarkerIconInfo> {
const color = this.settings.markerShape?.color?.color || '#307FE5';
return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color), rotationAngle);
}
private updateMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe(
(iconInfo) => {
let icon: L.Icon | L.DivIcon;
const options = deepClone(iconInfo.icon.options);
options.className = this.updateIconClasses(options.className);
if (iconInfo.icon instanceof L.DivIcon) {
icon = L.divIcon(options);
public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable<MarkerIconInfo> {
return createColorMarkerShapeURI(this.dataLayer.getCtx().$injector.get(MatIconRegistry), this.dataLayer.getCtx().$injector.get(DomSanitizer), shape, color).pipe(
map((iconUrl) => {
let icon: L.Icon<L.BaseIconOptions>;
if (rotationAngle === 0) {
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]]
});
} else {
icon = L.icon(options as L.IconOptions);
const style = `background-image: url(${iconUrl}); transform: rotate(${rotationAngle}deg); height: ${size}px; width: ${size}px;`;
icon = L.divIcon({
html: `<div style="${style}"></div>`,
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]]
});
}
this.marker.setIcon(icon);
const anchor = options.iconAnchor;
if (anchor && Array.isArray(anchor)) {
this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]];
} else {
this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10];
return {
size: [size, size],
icon
}
this.updateLabel(data, dsData);
this.editModeUpdated();
}
})
);
}
private updateIconClasses(className: string, toRemove?: string): string {
const classes: string[] = [];
if (className?.length) {
classes.push(...className.split(' '));
}
if (toRemove?.length) {
const index = classes.indexOf(toRemove);
if (index !== -1) {
classes.splice(index, 1);
}
}
this.iconClassList.forEach(clazz => {
if (!classes.includes(clazz)) {
classes.push(clazz);
}
});
return classes.join(' ');
public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable<MarkerIconInfo> {
return createColorMarkerIconElement(this.dataLayer.getCtx().$injector.get(MatIconRegistry), this.dataLayer.getCtx().$injector.get(DomSanitizer), icon, color, this.trip).pipe(
map((element) => {
if (rotationAngle !== 0) {
element.style.transform = `rotate(${rotationAngle}deg)`;
}
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]]
})
};
})
);
}
}
abstract class MarkerIconProcessor<S> {
static fromSettings(dataLayer: TbMarkersDataLayer,
static fromSettings(dataProcessor: MarkerDataProcessor,
settings: MarkersDataLayerSettings): MarkerIconProcessor<any> {
switch (settings.markerType) {
case MarkerType.shape:
return new ShapeMarkerIconProcessor(dataLayer, settings.markerShape);
return new ShapeMarkerIconProcessor(dataProcessor, settings.markerShape);
case MarkerType.icon:
return new IconMarkerIconProcessor(dataLayer, settings.markerIcon);
return new IconMarkerIconProcessor(dataProcessor, settings.markerIcon);
case MarkerType.image:
return new ImageMarkerIconProcessor(dataLayer, settings.markerImage);
return new ImageMarkerIconProcessor(dataProcessor, settings.markerImage);
}
}
protected constructor(protected dataLayer: TbMarkersDataLayer,
protected constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: S) {}
public abstract setup(): Observable<void>;
public abstract createMarkerIcon(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo>;
dsData: FormattedData<TbMapDatasource>[],
rotationAngle?: number): Observable<MarkerIconInfo>;
}
@ -270,15 +221,15 @@ abstract class BaseColorMarkerShapeProcessor<S extends BaseMarkerShapeSettings>
private defaultMarkerIconInfo: MarkerIconInfo;
protected constructor(protected dataLayer: TbMarkersDataLayer,
protected constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: S) {
super(dataLayer, settings);
super(dataProcessor, settings);
}
public setup(): Observable<void> {
const colorSettings = this.settings.color;
if (colorSettings.type === DataLayerColorType.function) {
return parseTbFunction<MapStringFunction>(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe(
return parseTbFunction<MapStringFunction>(this.dataProcessor.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe(
map((parsed) => {
this.markerColorFunction = parsed;
return null;
@ -286,7 +237,7 @@ abstract class BaseColorMarkerShapeProcessor<S extends BaseMarkerShapeSettings>
);
} else {
const color = tinycolor(colorSettings.color);
return this.createMarkerShape(color, this.settings.size).pipe(
return this.createMarkerShape(color, 0, this.settings.size).pipe(
map((info) => {
this.defaultMarkerIconInfo = info;
return null;
@ -295,47 +246,50 @@ abstract class BaseColorMarkerShapeProcessor<S extends BaseMarkerShapeSettings>
}
}
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo> {
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], rotationAngle = 0): Observable<MarkerIconInfo> {
const colorSettings = this.settings.color;
let color: tinycolor.Instance;
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(colorSettings.color);
}
return this.createMarkerShape(color, this.settings.size);
} else {
return this.createMarkerShape(color, rotationAngle, this.settings.size);
} else if (rotationAngle === 0) {
return of(this.defaultMarkerIconInfo);
} else {
color = tinycolor(colorSettings.color);
return this.createMarkerShape(color, rotationAngle, this.settings.size);
}
}
protected abstract createMarkerShape(color: tinycolor.Instance, size: number): Observable<MarkerIconInfo>;
protected abstract createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable<MarkerIconInfo>;
}
class ShapeMarkerIconProcessor extends BaseColorMarkerShapeProcessor<MarkerShapeSettings> {
constructor(protected dataLayer: TbMarkersDataLayer,
constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: MarkerShapeSettings) {
super(dataLayer, settings);
super(dataProcessor, settings);
}
protected createMarkerShape(color: tinycolor.Instance, size: number): Observable<MarkerIconInfo> {
return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, size);
protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable<MarkerIconInfo> {
return this.dataProcessor.createColoredMarkerShape(this.settings.shape, color, rotationAngle, size);
}
}
class IconMarkerIconProcessor extends BaseColorMarkerShapeProcessor<MarkerIconSettings> {
constructor(protected dataLayer: TbMarkersDataLayer,
constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: MarkerIconSettings) {
super(dataLayer, settings);
super(dataProcessor, settings);
}
protected createMarkerShape(color: tinycolor.Instance, size: number): Observable<MarkerIconInfo> {
return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, size);
protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable<MarkerIconInfo> {
return this.dataProcessor.createColoredMarkerIcon(this.settings.icon, color, rotationAngle, size);
}
}
@ -346,14 +300,14 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor<MarkerImageSettings>
private defaultMarkerIconInfo: MarkerIconInfo;
constructor(protected dataLayer: TbMarkersDataLayer,
constructor(protected dataProcessor: MarkerDataProcessor,
protected settings: MarkerImageSettings) {
super(dataLayer, settings);
super(dataProcessor, settings);
}
public setup(): Observable<void> {
if (this.settings.type === MarkerImageType.function) {
return parseTbFunction<MarkerImageFunction>(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe(
return parseTbFunction<MarkerImageFunction>(this.dataProcessor.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe(
map((parsed) => {
this.markerImageFunction = parsed;
return null;
@ -364,7 +318,7 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor<MarkerImageSettings>
url: this.settings.image,
size: this.settings.imageSize || 34
};
return this.loadMarkerIconInfo(currentImage).pipe(
return this.loadMarkerIconInfo(currentImage, 0).pipe(
map((iconInfo) => {
this.defaultMarkerIconInfo = iconInfo;
return null;
@ -373,18 +327,24 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor<MarkerImageSettings>
}
}
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo> {
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], rotationAngle = 0): Observable<MarkerIconInfo> {
if (this.settings.type === MarkerImageType.function) {
const currentImage: MarkerImageInfo = safeExecuteTbFunction(this.markerImageFunction, [data, this.settings.images, dsData]);
return this.loadMarkerIconInfo(currentImage);
} else {
return this.loadMarkerIconInfo(currentImage, rotationAngle);
} else if (rotationAngle === 0) {
return of(this.defaultMarkerIconInfo);
} else {
const currentImage: MarkerImageInfo = {
url: this.settings.image,
size: this.settings.imageSize || 34
};
return this.loadMarkerIconInfo(currentImage, rotationAngle);
}
}
private loadMarkerIconInfo(image: MarkerImageInfo): Observable<MarkerIconInfo> {
private loadMarkerIconInfo(image: MarkerImageInfo, rotationAngle = 0): Observable<MarkerIconInfo> {
if (image && image.url) {
return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe(
return loadImageWithAspect(this.dataProcessor.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe(
switchMap((aspectImage) => {
if (aspectImage?.aspect) {
let width: number;
@ -399,45 +359,230 @@ class ImageMarkerIconProcessor extends MarkerIconProcessor<MarkerImageSettings>
let iconAnchor = image.markerOffset;
let popupAnchor = image.tooltipOffset;
if (!iconAnchor) {
iconAnchor = [width * this.dataLayer.markerOffset[0], height * this.dataLayer.markerOffset[1]];
iconAnchor = [width * this.dataProcessor.markerOffset[0], height * this.dataProcessor.markerOffset[1]];
}
if (!popupAnchor) {
popupAnchor = [width * this.dataLayer.tooltipOffset[0], height * this.dataLayer.tooltipOffset[1]];
popupAnchor = [width * this.dataProcessor.tooltipOffset[0], height * this.dataProcessor.tooltipOffset[1]];
}
let icon: L.Icon<L.BaseIconOptions>;
if (rotationAngle === 0) {
icon = L.icon({
iconUrl: aspectImage.url,
iconSize: [width, height],
iconAnchor,
popupAnchor
});
} else {
const style = `background-image: url(${aspectImage.url}); background-size: contain; transform: rotate(${rotationAngle}deg); height: ${height}px; width: ${width}px;`;
icon = L.divIcon({
html: `<div style="${style}"></div>`,
className: 'tb-marker-div-icon',
iconSize: [width, height],
iconAnchor,
popupAnchor
});
}
const icon = L.icon({
iconUrl: aspectImage.url,
iconSize: [width, height],
iconAnchor,
popupAnchor
});
const iconInfo: MarkerIconInfo = {
size: [width, height],
icon
};
return of(iconInfo);
} else {
return this.dataLayer.createDefaultMarkerIcon();
return this.dataProcessor.createDefaultMarkerIcon(rotationAngle);
}
}),
catchError(() => this.dataLayer.createDefaultMarkerIcon())
catchError(() => this.dataProcessor.createDefaultMarkerIcon(rotationAngle))
);
} else {
return this.dataLayer.createDefaultMarkerIcon();
return this.dataProcessor.createDefaultMarkerIcon(rotationAngle);
}
}
}
class TbMarkerDataLayerItem extends TbLatestDataLayerItem<MarkersDataLayerSettings, TbMarkersDataLayer, L.Marker> {
private marker: L.Marker;
private labelOffset: L.PointTuple;
private iconClassList: string[];
private moving = false;
constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
protected settings: MarkersDataLayerSettings,
protected dataLayer: TbMarkersDataLayer) {
super(data, dsData, settings, dataLayer);
}
public isEditing() {
return this.moving;
}
public updateBubblingMouseEvents() {
this.marker.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Marker {
this.iconClassList = [];
const location = this.dataLayer.dataProcessor.extractLocation(data, dsData);
this.marker = L.marker(location, {
tbMarkerData: data,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.updateMarkerIcon(data, dsData);
return this.marker;
}
protected unbindLabel() {
this.marker.unbindTooltip();
}
protected bindLabel(content: L.Content): void {
this.marker.bindTooltip(content, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset });
}
protected doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.marker.options.tbMarkerData = data;
this.updateMarkerLocation(data, dsData);
this.updateTooltip(data, dsData);
this.updateMarkerIcon(data, dsData);
}
protected doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.updateMarkerLocation(data, dsData);
}
protected addItemClass(clazz: string): void {
if (!this.iconClassList.includes(clazz)) {
this.iconClassList.push(clazz);
this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className);
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, clazz);
if ((this.marker as any)._icon) {
L.DomUtil.removeClass((this.marker as any)._icon, clazz);
}
}
}
protected enableDrag(): void {
if (this.settings.markerClustering?.enable) {
this.marker.options.draggable = true;
this.marker.on('dragstart', () => {
this.moving = true;
});
this.marker.on('dragend', () => {
this.saveMarkerLocation();
this.moving = false;
});
} else {
this.marker.pm.setOptions({
snappable: this.dataLayer.isSnappable()
});
this.marker.pm.enableLayerDrag();
this.marker.on('pm:dragstart', () => {
this.moving = true;
});
this.marker.on('pm:dragend', () => {
this.saveMarkerLocation();
this.moving = false;
});
}
}
protected disableDrag(): void {
if (this.settings.markerClustering?.enable) {
this.marker.options.draggable = false;
this.marker.off('dragstart');
this.marker.off('dragend');
} else {
this.marker.pm.disableLayerDrag();
this.marker.off('pm:dragstart');
this.marker.off('pm:dragend');
}
}
protected removeDataItemTitle(): string {
return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.marker.remove-marker-for', {entityName: this.data.entityName});
}
protected removeDataItem(): Observable<any> {
return this.dataLayer.saveMarkerLocation(this.data, null);
}
private saveMarkerLocation() {
const location = this.marker.getLatLng();
this.dataLayer.saveMarkerLocation(this.data, location).subscribe();
}
private updateMarkerLocation(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
const location = this.dataLayer.dataProcessor.extractLocation(data, dsData);
if (!this.marker.getLatLng().equals(location) && !this.moving) {
this.marker.setLatLng(location);
}
}
private updateMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
this.dataLayer.dataProcessor.createMarkerIcon(data, dsData).subscribe(
(iconInfo) => {
let icon: L.Icon | L.DivIcon;
const options = deepClone(iconInfo.icon.options);
options.className = this.updateIconClasses(options.className);
if (iconInfo.icon instanceof L.DivIcon) {
icon = L.divIcon(options);
} else {
icon = L.icon(options as L.IconOptions);
}
this.marker.setIcon(icon);
const anchor = options.iconAnchor;
if (anchor && Array.isArray(anchor)) {
this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]];
} else {
this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10];
}
this.updateLabel(data, dsData);
this.editModeUpdated();
}
);
}
private updateIconClasses(className: string, toRemove?: string): string {
const classes: string[] = [];
if (className?.length) {
classes.push(...className.split(' '));
}
if (toRemove?.length) {
const index = classes.indexOf(toRemove);
if (index !== -1) {
classes.splice(index, 1);
}
}
this.iconClassList.forEach(clazz => {
if (!classes.includes(clazz)) {
classes.push(clazz);
}
});
return classes.join(' ');
}
}
export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings, TbMarkersDataLayer> {
export class TbMarkersDataLayer extends TbLatestMapDataLayer<MarkersDataLayerSettings, TbMarkersDataLayer> {
public markerIconProcessor: MarkerIconProcessor<any>;
public dataProcessor: MarkerDataProcessor;
public markerOffset: L.LatLngTuple;
public tooltipOffset: L.LatLngTuple;
private markersClusterContainer: L.MarkerClusterGroup;
private clusterMarkerColorFunction: CompiledTbFunction<ClusterMarkerColorFunction>;
private positionFunction: CompiledTbFunction<MarkerPositionFunction>;
constructor(protected map: TbMap<any>,
inputSettings: MarkersDataLayerSettings) {
@ -445,7 +590,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
}
public dataLayerType(): MapDataLayerType {
return MapDataLayerType.marker;
return 'markers';
}
public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void {
@ -463,6 +608,23 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
}
}
public saveMarkerLocation(data: FormattedData<TbMapDatasource>, position: L.LatLng): Observable<{x: number; y: number}> {
const converted = this.map.latLngToLocationData(position);
const locationData = [
{
dataKey: this.settings.xKey,
value: converted.x
},
{
dataKey: this.settings.yKey,
value: converted.y
}
];
return this.map.saveItemData(data.$datasource, locationData).pipe(
map(() => converted)
);
}
protected createDataLayerContainer(): FeatureGroup {
if (this.settings.markerClustering?.enable) {
return this.createMarkersClusterContainer();
@ -489,8 +651,8 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
isDefined(this.settings.tooltip?.offsetX) ? this.settings.tooltip?.offsetX : 0,
isDefined(this.settings.tooltip?.offsetY) ? this.settings.tooltip?.offsetY : -1,
];
this.markerIconProcessor = MarkerIconProcessor.fromSettings(this, this.settings);
const setup$: Observable<void>[] = [];
this.dataProcessor = new MarkerDataProcessor(this, this.settings, this.markerOffset, this.tooltipOffset);
const setup$: Observable<void>[] = [this.dataProcessor.setup()];
if (this.settings.markerClustering?.enable && this.settings.markerClustering.useClusterMarkerColorFunction) {
setup$.push(
parseTbFunction<ClusterMarkerColorFunction>(this.getCtx().http, this.settings.markerClustering.clusterMarkerColorFunction, ['data', 'childCount']).pipe(
@ -501,29 +663,18 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
)
);
}
if (this.map.type() === MapType.image) {
setup$.push(
parseTbFunction<MarkerPositionFunction>(this.getCtx().http, this.settings.positionFunction, ['origXPos', 'origYPos', 'data', 'dsData', 'aspect']).pipe(
map((parsed) => {
this.positionFunction = parsed;
return null;
})
)
);
}
setup$.push(this.markerIconProcessor.setup());
return forkJoin(setup$).pipe(map(() => null));
}
protected isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean {
return !!this.extractLocationData(layerData);
return !!this.dataProcessor.extractLocationData(layerData);
}
protected createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbMarkerDataLayerItem {
return new TbMarkerDataLayerItem(data, dsData, this.settings, this);
}
protected layerItemsUpdated(updatedItems: TbDataLayerItem<MarkersDataLayerSettings, TbMarkersDataLayer, L.Marker>[]) {
protected layerItemsUpdated(updatedItems: TbLatestDataLayerItem<MarkersDataLayerSettings, TbMarkersDataLayer, L.Marker>[]) {
if (this.settings.markerClustering?.enable) {
this.markersClusterContainer.refreshClusters(updatedItems.map(item => item.getLayer()));
}
@ -538,7 +689,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
removeOutsideVisibleBounds: this.settings.markerClustering?.lazyLoad,
animate: this.settings.markerClustering?.zoomAnimation,
chunkedLoading: this.settings.markerClustering?.chunkedLoad,
snapIgnore: !this.snappable
snapIgnore: !this.settings.edit?.snappable
};
if (this.settings.markerClustering?.useClusterMarkerColorFunction) {
markerClusterOptions.iconCreateFunction = (cluster) => {
@ -583,94 +734,4 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
return this.markersClusterContainer;
}
private extractLocationData(data: FormattedData<TbMapDatasource>): {x: number; y: number} {
if (data) {
const xKeyVal = data[this.settings.xKey.label];
const yKeyVal = data[this.settings.yKey.label];
switch (this.mapType()) {
case MapType.geoMap:
if (!isValidLatLng(xKeyVal, yKeyVal)) {
return null;
}
break;
case MapType.image:
if (!isDefinedAndNotNull(xKeyVal) || isEmptyStr(xKeyVal) || isNaN(xKeyVal) || !isDefinedAndNotNull(yKeyVal) || isEmptyStr(yKeyVal) || isNaN(yKeyVal)) {
return null;
}
break;
}
return {x: xKeyVal, y: yKeyVal};
} else {
return null;
}
}
public createDefaultMarkerIcon(): Observable<MarkerIconInfo> {
const color = this.settings.markerShape?.color?.color || '#307FE5';
return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color));
}
public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, size = 34): Observable<MarkerIconInfo> {
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<MarkerIconInfo> {
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<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.LatLng {
let locationData = this.extractLocationData(data);
if (locationData) {
if (this.map.type() === MapType.image && this.positionFunction) {
const imageMap = this.map as TbImageMap;
locationData = this.positionFunction.execute(locationData.x, locationData.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0};
}
return this.map.locationDataToLatLng(locationData);
} else {
return null;
}
}
public saveMarkerLocation(data: FormattedData<TbMapDatasource>, position: L.LatLng): Observable<{x: number; y: number}> {
const converted = this.map.latLngToLocationData(position);
const locationData = [
{
dataKey: this.settings.xKey,
value: converted.x
},
{
dataKey: this.settings.yKey,
value: converted.y
}
];
return this.map.saveItemData(data.$datasource, locationData).pipe(
map(() => converted)
);
}
}

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

@ -16,7 +16,7 @@
import {
defaultBasePolygonsDataLayerSettings,
isCutPolygon, isJSON,
isCutPolygon, isJSON, MapDataLayerType,
PolygonsDataLayerSettings,
TbMapDatasource, TbPolyData, TbPolygonCoordinates, TbPolygonRawCoordinates
} from '@home/components/widget/lib/maps/models/map.models';
@ -27,13 +27,12 @@ import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
import { isNotEmptyStr, isString } from '@core/utils';
import {
MapDataLayerType,
TbDataLayerItem,
TbLatestDataLayerItem,
UnplacedMapDataItem
} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { map } from 'rxjs/operators';
class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings, TbPolygonsDataLayer> {
class TbPolygonDataLayerItem extends TbLatestDataLayerItem<PolygonsDataLayerSettings, TbPolygonsDataLayer> {
private polygonContainer: L.FeatureGroup;
private polygon: L.Polygon;
@ -47,6 +46,14 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
super(data, dsData, settings, dataLayer);
}
public isEditing() {
return this.editing;
}
public updateBubblingMouseEvents() {
this.polygon.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Layer {
const polyData = this.dataLayer.extractPolygonCoordinates(data);
const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle;
@ -199,14 +206,6 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
return this.dataLayer.savePolygonCoordinates(this.data, null);
}
public isEditing() {
return this.editing;
}
public updateBubblingMouseEvents() {
this.polygon.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
private enablePolygonEditMode() {
this.polygon.on('pm:markerdragstart', () => this.editing = true);
this.polygon.on('pm:markerdragend', () => setTimeout(() => {
@ -364,7 +363,7 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer<PolygonsDataLayerSett
}
public dataLayerType(): MapDataLayerType {
return MapDataLayerType.polygon;
return 'polygons';
}
public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void {
@ -390,6 +389,27 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer<PolygonsDataLayerSett
}
}
public extractPolygonCoordinates(data: FormattedData<TbMapDatasource>): TbPolygonRawCoordinates {
let rawPolyData = data[this.settings.polygonKey.label];
if (isString(rawPolyData)) {
rawPolyData = JSON.parse(rawPolyData);
}
return this.map.polygonDataToCoordinates(rawPolyData);
}
public savePolygonCoordinates(data: FormattedData<TbMapDatasource>, coordinates: TbPolygonCoordinates): Observable<TbPolygonRawCoordinates> {
const converted = coordinates ? this.map.coordinatesToPolygonData(coordinates) : null;
const polygonData = [
{
dataKey: this.settings.polygonKey,
value: converted
}
];
return this.map.saveItemData(data.$datasource, polygonData).pipe(
map(() => converted)
);
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.polygonKey);
return datasource;
@ -412,24 +432,4 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer<PolygonsDataLayerSett
return new TbPolygonDataLayerItem(data, dsData, this.settings, this);
}
public extractPolygonCoordinates(data: FormattedData<TbMapDatasource>): TbPolygonRawCoordinates {
let rawPolyData = data[this.settings.polygonKey.label];
if (isString(rawPolyData)) {
rawPolyData = JSON.parse(rawPolyData);
}
return this.map.polygonDataToCoordinates(rawPolyData);
}
public savePolygonCoordinates(data: FormattedData<TbMapDatasource>, coordinates: TbPolygonCoordinates): Observable<TbPolygonRawCoordinates> {
const converted = coordinates ? this.map.coordinatesToPolygonData(coordinates) : null;
const polygonData = [
{
dataKey: this.settings.polygonKey,
value: converted
}
];
return this.map.saveItemData(data.$datasource, polygonData).pipe(
map(() => converted)
);
}
}

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

@ -19,9 +19,10 @@ import L from 'leaflet';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { forkJoin, Observable } from 'rxjs';
import { FormattedData } from '@shared/models/widget.models';
import { DataLayerColorProcessor, TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import { TbLatestMapDataLayer } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { DataLayerColorProcessor } from './map-data-layer';
export abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L extends TbMapDataLayer<S,L>> extends TbMapDataLayer<S, L> {
export abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L extends TbLatestMapDataLayer<S,L>> extends TbLatestMapDataLayer<S, L> {
public fillColorProcessor: DataLayerColorProcessor;
public strokeColorProcessor: DataLayerColorProcessor;
@ -31,12 +32,6 @@ export abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L exte
super(map, inputSettings);
}
protected doSetup(): Observable<any> {
this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor);
this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor);
return forkJoin([this.fillColorProcessor.setup(), this.strokeColorProcessor.setup()]);
}
public getShapeStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.PathOptions {
const fill = this.fillColorProcessor.processColor(data, dsData);
const stroke = this.strokeColorProcessor.processColor(data, dsData);
@ -49,4 +44,11 @@ export abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L exte
opacity: 1
};
}
protected doSetup(): Observable<any> {
this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor);
this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor);
return forkJoin([this.fillColorProcessor.setup(), this.strokeColorProcessor.setup()]);
}
}

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

@ -15,75 +15,39 @@
///
import {
DataLayerColorProcessor,
DataLayerPatternProcessor,
MapDataLayerType,
TbDataLayerItem,
TbMapDataLayer,
UnplacedMapDataItem
} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import {
BaseMarkerShapeSettings,
calculateInterpolationRatio,
calculateLastPoints,
DataLayerColorType,
defaultBaseTripsDataLayerSettings,
findRotationAngle,
interpolateLineSegment,
isValidLatLng,
loadImageWithAspect,
MapStringFunction,
MapType,
MarkerIconInfo,
MarkerIconSettings,
MarkerImageFunction,
MarkerImageInfo,
MarkerImageSettings,
MarkerImageType,
MarkerPositionFunction,
MarkerShapeSettings,
MarkerType,
MapDataLayerType,
TbMapDatasource,
TripsDataLayerSettings
} from '@home/components/widget/lib/maps/models/map.models';
import { forkJoin, Observable, of } from 'rxjs';
import { forkJoin, Observable } from 'rxjs';
import { FormattedData, WidgetActionType } from '@shared/models/widget.models';
import {
createColorMarkerIconElement,
createColorMarkerShapeURI,
MarkerShape
} from '@home/components/widget/lib/maps/models/marker-shape.models';
import tinycolor from 'tinycolor2';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import { catchError, map, switchMap } from 'rxjs/operators';
import { map } from 'rxjs/operators';
import L from 'leaflet';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import {
deepClone,
isDefined,
isDefinedAndNotNull,
isEmptyStr,
isUndefined,
parseTbFunction,
safeExecuteTbFunction
} from '@core/utils';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { deepClone, isDefined, isUndefined } from '@core/utils';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import moment from 'moment/moment';
import { TbImageMap } from '@home/components/widget/lib/maps/image-map';
import { createTooltip, updateTooltip } from '@home/components/widget/lib/maps/data-layer/data-layer-utils';
import _ from 'lodash';
import {
DataLayerColorProcessor,
DataLayerPatternProcessor,
TbDataLayerItem,
TbMapDataLayer
} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import { MarkerDataProcessor } from '@home/components/widget/lib/maps/data-layer/markers-data-layer';
type TripRouteData = {[time: number]: FormattedData<TbMapDatasource>};
class TbTripDataItem {
class TbTripDataItem extends TbDataLayerItem<TripsDataLayerSettings, TbTripsDataLayer, L.FeatureGroup> {
private tripRouteData: TripRouteData;
private layer: L.FeatureGroup;
private marker: L.Marker;
private markerTooltip: L.Popup;
private labelOffset: L.PointTuple;
@ -98,29 +62,13 @@ class TbTripDataItem {
constructor(private rawRouteData: FormattedData<TbMapDatasource>[],
private latestData: FormattedData<TbMapDatasource>,
private settings: TripsDataLayerSettings,
private dataLayer: TbTripsDataLayer) {
settings: TripsDataLayerSettings,
dataLayer: TbTripsDataLayer) {
super(settings, dataLayer);
this.tripRouteData = this.prepareTripRouteData();
this.create();
}
private create() {
this.updateCurrentPosition();
this.layer = L.featureGroup();
this.pointData = this.currentPositionData;
if (this.latestData) {
this.pointData = {...this.pointData, ...this.latestData};
}
this.createPath();
this.updatePoints();
this.createMarker();
try {
this.dataLayer.getDataLayerContainer().addLayer(this.layer);
} catch (e) {
console.warn(e);
}
}
public update(rawRouteData: FormattedData<TbMapDatasource>[]) {
this.rawRouteData = rawRouteData;
this.tripRouteData = this.prepareTripRouteData();
@ -176,20 +124,33 @@ class TbTripDataItem {
this.layer.off();
}
public getLayer(): L.Layer {
return this.layer;
}
public calculateAnchors(): number[] {
const entries = Object.entries(this.tripRouteData);
const dsData = this.dataLayer.getMap().getData();
return entries.filter(data => this.dataLayer.getMap().getLocationSnapFilterFunction().execute(data[1], dsData))
.map(data => parseInt(data[0], 10));
.map(data => parseInt(data[0], 10));
}
private create() {
this.updateCurrentPosition();
this.layer = L.featureGroup();
this.pointData = this.currentPositionData;
if (this.latestData) {
this.pointData = {...this.pointData, ...this.latestData};
}
this.createPath();
this.updatePoints();
this.createMarker();
try {
this.dataLayer.getDataLayerContainer().addLayer(this.layer);
} catch (e) {
console.warn(e);
}
}
private createMarker() {
const dsData = this.dataLayer.getMap().getData();
const location = this.dataLayer.extractLocation(this.pointData, dsData);
const location = this.dataLayer.dataProcessor.extractLocation(this.pointData, dsData);
this.marker = L.marker(location, {
tbMarkerData: this.pointData,
snapIgnore: true
@ -225,7 +186,7 @@ class TbTripDataItem {
if (this.settings.showPath) {
const formattedRouteData = _.values(this.tripRouteData);
const dsData = this.dataLayer.getMap().getData();
const locations = formattedRouteData.map(data => this.dataLayer.extractLocation(data, dsData));
const locations = formattedRouteData.map(data => this.dataLayer.dataProcessor.extractLocation(data, dsData));
const pathStyle = this.dataLayer.getPathStyle(this.pointData, dsData);
this.polyline = L.polyline(locations, pathStyle);
this.polyline.addTo(this.layer);
@ -240,7 +201,7 @@ class TbTripDataItem {
if (this.settings.showPath) {
const formattedRouteData = _.values(this.tripRouteData);
const dsData = this.dataLayer.getMap().getData();
const locations = formattedRouteData.map(data => this.dataLayer.extractLocation(data, dsData));
const locations = formattedRouteData.map(data => this.dataLayer.dataProcessor.extractLocation(data, dsData));
this.polyline.setLatLngs(locations);
if (this.settings.usePathDecorator) {
this.polylineDecorator.setPaths(this.polyline);
@ -258,14 +219,14 @@ class TbTripDataItem {
this.points.clearLayers();
const formattedRouteData = _.values(this.tripRouteData);
const dsData = this.dataLayer.getMap().getData();
const pointsList = formattedRouteData.filter(data => !!this.dataLayer.extractLocationData(data));
const pointsList = formattedRouteData.filter(data => !!this.dataLayer.dataProcessor.extractLocationData(data));
for (const pData of pointsList) {
let pointData = pData;
if (this.latestData) {
pointData = {...pointData, ...this.latestData};
}
const pointColor = this.dataLayer.pointColorProcessor.processColor(pointData, dsData);
const point = L.circleMarker(this.dataLayer.extractLocation(pointData, dsData), {
const point = L.circleMarker(this.dataLayer.dataProcessor.extractLocation(pointData, dsData), {
stroke: false,
fillOpacity: 1,
fillColor: pointColor,
@ -294,14 +255,14 @@ class TbTripDataItem {
}
private updateMarkerLocation(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
const location = this.dataLayer.extractLocation(data, dsData);
const location = this.dataLayer.dataProcessor.extractLocation(data, dsData);
if (!this.marker.getLatLng().equals(location)) {
this.marker.setLatLng(location);
}
}
private updateMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
this.dataLayer.tripMarkerIconProcessor.createMarkerIcon(data, dsData).subscribe(
this.dataLayer.dataProcessor.createMarkerIcon(data, dsData, data.rotationAngle).subscribe(
(iconInfo) => {
const options = deepClone(iconInfo.icon.options);
this.marker.setIcon(iconInfo.icon);
@ -362,8 +323,8 @@ class TbTripDataItem {
}
}
if (this.settings.rotateMarker) {
const startPoint = this.dataLayer.extractLocation(result[timeStamp[i]], dsData);
const endPoint = this.dataLayer.extractLocation(result[timeStamp[i + 1]], dsData);
const startPoint = this.dataLayer.dataProcessor.extractLocation(result[timeStamp[i]], dsData);
const endPoint = this.dataLayer.dataProcessor.extractLocation(result[timeStamp[i + 1]], dsData);
result[timeStamp[i]].rotationAngle += findRotationAngle(startPoint, endPoint);
}
}
@ -400,183 +361,9 @@ class TbTripDataItem {
}
abstract class TripMarkerIconProcessor<S> {
static fromSettings(dataLayer: TbTripsDataLayer,
settings: TripsDataLayerSettings): TripMarkerIconProcessor<any> {
switch (settings.markerType) {
case MarkerType.shape:
return new ShapeTripMarkerIconProcessor(dataLayer, settings.markerShape);
case MarkerType.icon:
return new IconTripMarkerIconProcessor(dataLayer, settings.markerIcon);
case MarkerType.image:
return new ImageTripMarkerIconProcessor(dataLayer, settings.markerImage);
}
}
protected constructor(protected dataLayer: TbTripsDataLayer,
protected settings: S) {}
export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbTripDataItem> {
public abstract setup(): Observable<void>;
public abstract createMarkerIcon(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo>;
}
abstract class BaseColorTripMarkerShapeProcessor<S extends BaseMarkerShapeSettings> extends TripMarkerIconProcessor<S> {
private markerColorFunction: CompiledTbFunction<MapStringFunction>;
protected constructor(protected dataLayer: TbTripsDataLayer,
protected settings: S) {
super(dataLayer, settings);
}
public setup(): Observable<void> {
const colorSettings = this.settings.color;
if (colorSettings.type === DataLayerColorType.function) {
return parseTbFunction<MapStringFunction>(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe(
map((parsed) => {
this.markerColorFunction = parsed;
return null;
})
);
} else {
return of(null);
}
}
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo> {
const colorSettings = this.settings.color;
let color: tinycolor.Instance;
if (colorSettings.type === DataLayerColorType.function) {
const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]);
if (isDefinedAndNotNull(functionColor)) {
color = tinycolor(functionColor);
} else {
color = tinycolor(colorSettings.color);
}
} else {
color = tinycolor(colorSettings.color);
}
return this.createMarkerShape(color, data.rotationAngle, this.settings.size);
}
protected abstract createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable<MarkerIconInfo>;
}
class ShapeTripMarkerIconProcessor extends BaseColorTripMarkerShapeProcessor<MarkerShapeSettings> {
constructor(protected dataLayer: TbTripsDataLayer,
protected settings: MarkerShapeSettings) {
super(dataLayer, settings);
}
protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable<MarkerIconInfo> {
return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, rotationAngle, size);
}
}
class IconTripMarkerIconProcessor extends BaseColorTripMarkerShapeProcessor<MarkerIconSettings> {
constructor(protected dataLayer: TbTripsDataLayer,
protected settings: MarkerIconSettings) {
super(dataLayer, settings);
}
protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable<MarkerIconInfo> {
return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, rotationAngle, size);
}
}
class ImageTripMarkerIconProcessor extends TripMarkerIconProcessor<MarkerImageSettings> {
private markerImageFunction: CompiledTbFunction<MarkerImageFunction>;
constructor(protected dataLayer: TbTripsDataLayer,
protected settings: MarkerImageSettings) {
super(dataLayer, settings);
}
public setup(): Observable<void> {
if (this.settings.type === MarkerImageType.function) {
return parseTbFunction<MarkerImageFunction>(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe(
map((parsed) => {
this.markerImageFunction = parsed;
return null;
})
);
} else {
return of(null);
}
}
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo> {
if (this.settings.type === MarkerImageType.function) {
const currentImage: MarkerImageInfo = safeExecuteTbFunction(this.markerImageFunction, [data, this.settings.images, dsData]);
return this.loadMarkerIconInfo(currentImage, data.rotationAngle);
} else {
const currentImage: MarkerImageInfo = {
url: this.settings.image,
size: this.settings.imageSize || 34
};
return this.loadMarkerIconInfo(currentImage, data.rotationAngle);
}
}
private loadMarkerIconInfo(image: MarkerImageInfo, rotationAngle = 0): Observable<MarkerIconInfo> {
if (image && image.url) {
return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe(
switchMap((aspectImage) => {
if (aspectImage?.aspect) {
let width: number;
let height: number;
if (aspectImage.aspect > 1) {
width = image.size;
height = image.size / aspectImage.aspect;
} else {
width = image.size * aspectImage.aspect;
height = image.size;
}
let iconAnchor = image.markerOffset;
let popupAnchor = image.tooltipOffset;
if (!iconAnchor) {
iconAnchor = [width * this.dataLayer.markerOffset[0], height * this.dataLayer.markerOffset[1]];
}
if (!popupAnchor) {
popupAnchor = [width * this.dataLayer.tooltipOffset[0], height * this.dataLayer.tooltipOffset[1]];
}
const style = `background-image: url(${aspectImage.url}); background-size: contain; transform: rotate(${rotationAngle}deg); height: ${height}px; width: ${width}px;`;
const icon = L.divIcon({
html: `<div style="${style}"></div>`,
className: 'tb-marker-div-icon',
iconSize: [width, height],
iconAnchor,
popupAnchor
});
const iconInfo: MarkerIconInfo = {
size: [width, height],
icon
};
return of(iconInfo);
} else {
return this.dataLayer.createDefaultMarkerIcon(rotationAngle);
}
}),
catchError(() => this.dataLayer.createDefaultMarkerIcon(rotationAngle))
);
} else {
return this.dataLayer.createDefaultMarkerIcon(rotationAngle);
}
}
}
export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbTripsDataLayer> {
public tripMarkerIconProcessor: TripMarkerIconProcessor<any>;
public dataProcessor: MarkerDataProcessor;
public markerOffset: L.LatLngTuple;
public tooltipOffset: L.LatLngTuple;
@ -585,10 +372,6 @@ export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbT
public pointColorProcessor: DataLayerColorProcessor;
public pointTooltipProcessor: DataLayerPatternProcessor;
private tripItems = new Map<string,TbTripDataItem>();
private positionFunction: CompiledTbFunction<MarkerPositionFunction>;
private rawTripsData: FormattedData<TbMapDatasource>[][];
private latestTripsData: FormattedData<TbMapDatasource>[];
@ -598,79 +381,7 @@ export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbT
}
public dataLayerType(): MapDataLayerType {
return MapDataLayerType.trip;
}
public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void {
throw new Error('Not implemented!');
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys = [this.settings.xKey, this.settings.yKey];
if (this.settings.additionalDataKeys?.length) {
const tsKeys = this.settings.additionalDataKeys.filter(key => key.type === DataKeyType.timeseries);
const latestKeys = this.settings.additionalDataKeys.filter(key => key.type !== DataKeyType.timeseries);
datasource.dataKeys.push(...tsKeys);
if (latestKeys.length) {
datasource.latestDataKeys = latestKeys;
}
}
return datasource;
}
protected defaultBaseSettings(map: TbMap<any>): Partial<TripsDataLayerSettings> {
return defaultBaseTripsDataLayerSettings(map.type());
}
protected doSetup(): Observable<void> {
this.markerOffset = [
isDefined(this.settings.markerOffsetX) ? this.settings.markerOffsetX : 0.5,
isDefined(this.settings.markerOffsetY) ? this.settings.markerOffsetY : 1,
];
this.tooltipOffset = [
isDefined(this.settings.tooltip?.offsetX) ? this.settings.tooltip?.offsetX : 0,
isDefined(this.settings.tooltip?.offsetY) ? this.settings.tooltip?.offsetY : -1,
];
this.tripMarkerIconProcessor = TripMarkerIconProcessor.fromSettings(this, this.settings);
const setup$: Observable<void>[] = [];
if (this.map.type() === MapType.image) {
setup$.push(
parseTbFunction<MarkerPositionFunction>(this.getCtx().http, this.settings.positionFunction, ['origXPos', 'origYPos', 'data', 'dsData', 'aspect']).pipe(
map((parsed) => {
this.positionFunction = parsed;
return null;
})
)
);
}
setup$.push(this.tripMarkerIconProcessor.setup());
if (this.settings.showPath) {
this.pathStrokeColorProcessor = new DataLayerColorProcessor(this, this.settings.pathStrokeColor);
setup$.push(this.pathStrokeColorProcessor.setup());
}
if (this.settings.showPoints) {
this.pointColorProcessor = new DataLayerColorProcessor(this, this.settings.pointColor);
setup$.push(this.pointColorProcessor.setup());
if (this.settings.pointTooltip?.show) {
this.pointTooltipProcessor = new DataLayerPatternProcessor(this, this.settings.pointTooltip);
setup$.push(this.pointTooltipProcessor.setup());
}
}
return forkJoin(setup$).pipe(map(() => null));
}
protected isValidLayerData(_layerData: FormattedData<TbMapDatasource>): boolean {
throw new Error('Not implemented!');
}
protected createLayerItem(_data: FormattedData<TbMapDatasource>, _dsData: FormattedData<TbMapDatasource>[]): TbDataLayerItem<TripsDataLayerSettings,TbTripsDataLayer> {
throw new Error('Not implemented!');
}
public updateData(_dsData: FormattedData<TbMapDatasource>[]) {
throw new Error('Not implemented!');
return 'trips';
}
public prepareTripsData(tripsData: FormattedData<TbMapDatasource>[][], tripsLatestData: FormattedData<TbMapDatasource>[]): {minTime: number; maxTime: number} {
@ -688,16 +399,16 @@ export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbT
}
public updateTrips() {
const toDelete = new Set(Array.from(this.tripItems.keys()));
const toDelete = new Set(Array.from(this.layerItems.keys()));
this.rawTripsData.forEach(rawTripData => {
const entityId = rawTripData[0].entityId;
let tripItem = this.tripItems.get(entityId);
let tripItem = this.layerItems.get(entityId);
if (tripItem) {
tripItem.update(rawTripData);
} else {
const latestData = this.latestTripsData.find(d => d.entityId === entityId);
tripItem = new TbTripDataItem(rawTripData, latestData, this.settings, this);
this.tripItems.set(entityId, tripItem);
this.layerItems.set(entityId, tripItem);
}
toDelete.delete(entityId);
});
@ -706,145 +417,35 @@ export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbT
});
}
public removeItem(key: string): void {
const item = this.tripItems.get(key);
if (item) {
item.remove();
this.tripItems.delete(key);
}
}
public invalidateCoordinates(): void {
this.tripItems.forEach(item => item.invalidateCoordinates());
}
public updateTripsLatestData(tripsLatestData: FormattedData<TbMapDatasource>[]) {
this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId));
this.tripItems.forEach((item, entityId) => {
this.layerItems.forEach((item, entityId) => {
const latestData = this.latestTripsData.find(d => d.entityId === entityId);
item.updateLatestData(latestData);
});
}
public updateCurrentTime() {
this.tripItems.forEach(item => {
this.layerItems.forEach(item => {
item.updateCurrentTime();
});
}
public updateAppearance() {
this.tripItems.forEach(item => {
this.layerItems.forEach(item => {
item.updateAppearance();
});
}
public calculateAnchors(): number[] {
let anchors: number[] = [];
this.tripItems.forEach(item => {
this.layerItems.forEach(item => {
const tripAnchors = item.calculateAnchors();
anchors = [...new Set([...anchors, ...tripAnchors])];
});
return anchors;
}
private clearIncorrectFirsLastDatapoint(dataSource: FormattedData<TbMapDatasource>[]): FormattedData<TbMapDatasource>[] {
const firstHistoricalDataIndexCoordinate = dataSource.findIndex(this.findFirstHistoricalDataIndexCoordinate);
if (firstHistoricalDataIndexCoordinate === -1) {
return [];
}
let lastIndex = dataSource.length - 1;
for (lastIndex; lastIndex > 0; lastIndex--) {
if (this.findFirstHistoricalDataIndexCoordinate(dataSource[lastIndex])) {
lastIndex++;
break;
}
}
if (firstHistoricalDataIndexCoordinate > 0 || lastIndex < dataSource.length) {
return dataSource.slice(firstHistoricalDataIndexCoordinate, lastIndex);
}
return dataSource;
}
private findFirstHistoricalDataIndexCoordinate = (item: FormattedData<TbMapDatasource>): boolean => {
return isDefined(item[this.settings.xKey.label]) && isDefined(item[this.settings.yKey.label]);
}
public createDefaultMarkerIcon(rotationAngle = 0): Observable<MarkerIconInfo> {
const color = this.settings.markerShape?.color?.color || '#307FE5';
return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color), rotationAngle);
}
public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable<MarkerIconInfo> {
return createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), shape, color).pipe(
map((iconUrl) => {
const style = `background-image: url(${iconUrl}); transform: rotate(${rotationAngle}deg); height: ${size}px; width: ${size}px;`;
return {
size: [size, size],
icon: L.divIcon({
html: `<div style="${style}"></div>`,
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 createColoredMarkerIcon(icon: string, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable<MarkerIconInfo> {
return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color, true).pipe(
map((element) => {
element.style.transform = `rotate(${rotationAngle}deg)`;
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 extractLocationData(data: FormattedData<TbMapDatasource>): {x: number; y: number} {
if (data) {
const xKeyVal = data[this.settings.xKey.label];
const yKeyVal = data[this.settings.yKey.label];
switch (this.mapType()) {
case MapType.geoMap:
if (!isValidLatLng(xKeyVal, yKeyVal)) {
return null;
}
break;
case MapType.image:
if (!isDefinedAndNotNull(xKeyVal) || isEmptyStr(xKeyVal) || isNaN(xKeyVal) || !isDefinedAndNotNull(yKeyVal) || isEmptyStr(yKeyVal) || isNaN(yKeyVal)) {
return null;
}
break;
}
return {x: xKeyVal, y: yKeyVal};
} else {
return null;
}
}
public extractLocation(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.LatLng {
let locationData = this.extractLocationData(data);
if (locationData) {
if (this.map.type() === MapType.image && this.positionFunction) {
const imageMap = this.map as TbImageMap;
locationData = this.positionFunction.execute(locationData.x, locationData.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0};
}
return this.map.locationDataToLatLng(locationData);
} else {
return null;
}
}
public getPathStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.PolylineOptions {
const pathStroke = this.pathStrokeColorProcessor.processColor(data, dsData);
return {
@ -877,4 +478,69 @@ export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbT
};
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys = [this.settings.xKey, this.settings.yKey];
if (this.settings.additionalDataKeys?.length) {
const tsKeys = this.settings.additionalDataKeys.filter(key => key.type === DataKeyType.timeseries);
const latestKeys = this.settings.additionalDataKeys.filter(key => key.type !== DataKeyType.timeseries);
datasource.dataKeys.push(...tsKeys);
if (latestKeys.length) {
datasource.latestDataKeys = latestKeys;
}
}
return datasource;
}
protected defaultBaseSettings(map: TbMap<any>): Partial<TripsDataLayerSettings> {
return defaultBaseTripsDataLayerSettings(map.type());
}
protected doSetup(): Observable<void> {
this.markerOffset = [
isDefined(this.settings.markerOffsetX) ? this.settings.markerOffsetX : 0.5,
isDefined(this.settings.markerOffsetY) ? this.settings.markerOffsetY : 0.5,
];
this.tooltipOffset = [
isDefined(this.settings.tooltip?.offsetX) ? this.settings.tooltip?.offsetX : 0,
isDefined(this.settings.tooltip?.offsetY) ? this.settings.tooltip?.offsetY : -0.5,
];
this.dataProcessor = new MarkerDataProcessor(this, this.settings, this.markerOffset, this.tooltipOffset, true);
const setup$: Observable<void>[] = [this.dataProcessor.setup()];
if (this.settings.showPath) {
this.pathStrokeColorProcessor = new DataLayerColorProcessor(this, this.settings.pathStrokeColor);
setup$.push(this.pathStrokeColorProcessor.setup());
}
if (this.settings.showPoints) {
this.pointColorProcessor = new DataLayerColorProcessor(this, this.settings.pointColor);
setup$.push(this.pointColorProcessor.setup());
if (this.settings.pointTooltip?.show) {
this.pointTooltipProcessor = new DataLayerPatternProcessor(this, this.settings.pointTooltip);
setup$.push(this.pointTooltipProcessor.setup());
}
}
return forkJoin(setup$).pipe(map(() => null));
}
private clearIncorrectFirsLastDatapoint(dataSource: FormattedData<TbMapDatasource>[]): FormattedData<TbMapDatasource>[] {
const firstHistoricalDataIndexCoordinate = dataSource.findIndex(this.findFirstHistoricalDataIndexCoordinate);
if (firstHistoricalDataIndexCoordinate === -1) {
return [];
}
let lastIndex = dataSource.length - 1;
for (lastIndex; lastIndex > 0; lastIndex--) {
if (this.findFirstHistoricalDataIndexCoordinate(dataSource[lastIndex])) {
lastIndex++;
break;
}
}
if (firstHistoricalDataIndexCoordinate > 0 || lastIndex < dataSource.length) {
return dataSource.slice(firstHistoricalDataIndexCoordinate, lastIndex);
}
return dataSource;
}
private findFirstHistoricalDataIndexCoordinate = (item: FormattedData<TbMapDatasource>): boolean => {
return isDefined(item[this.settings.xKey.label]) && isDefined(item[this.settings.yKey.label]);
}
}

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

@ -15,7 +15,6 @@
///
import {
checkLngLat,
DEFAULT_ZOOM_LEVEL,
defaultGeoMapSettings,
GeoMapSettings,
@ -127,7 +126,7 @@ export class TbGeoMap extends TbMap<GeoMapSettings> {
}
public latLngToLocationData(position: L.LatLng): {x: number; y: number} {
position = position ? checkLngLat(position, this.southWest, this.northEast, 0) : {lat: null, lng: null} as L.LatLng;
position = position ? latLngPointToBounds(position, this.southWest, this.northEast, 0) : {lat: null, lng: null} as L.LatLng;
return {
x: position.lat,
y: position.lng
@ -152,7 +151,7 @@ export class TbGeoMap extends TbMap<GeoMapSettings> {
if (Array.isArray(point)) {
return this.coordinatesToPolygonData(point) as TbPolygonRawCoordinate;
} else {
const convertPoint = checkLngLat(point, this.southWest, this.northEast);
const convertPoint = latLngPointToBounds(point, this.southWest, this.northEast);
return [convertPoint.lat, convertPoint.lng];
}
});
@ -170,7 +169,7 @@ export class TbGeoMap extends TbMap<GeoMapSettings> {
public coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData {
let circleData: TbCircleData = null;
if (center) {
const position = checkLngLat(center, this.southWest, this.northEast);
const position = latLngPointToBounds(center, this.southWest, this.northEast);
circleData = {
latitude: position.lat,
longitude: position.lng,

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

@ -166,7 +166,7 @@ class TbGoogleMapLayer extends TbMapLayer<GoogleMapLayerSettings> {
return of(true);
} else {
const resourceService = this.ctx.$injector.get(ResourcesService);
return resourceService.loadResource(`https://maps.googleapis.com/maps/api/js?key=${apiKey}`).pipe(
return resourceService.loadResource(`https://maps.googleapis.com/maps/api/js?key=${apiKey}&loading=async`).pipe(
map(() => {
TbGoogleMapLayer.loadedApiKeysGlobal[apiKey] = true;
return true;

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

@ -18,10 +18,11 @@ import {
additionalMapDataSourcesToDatasources,
BaseMapSettings,
CustomActionData,
DataKeyValuePair, MapBooleanFunction, MapStringFunction,
DataKeyValuePair,
MapBooleanFunction,
mapDataLayerTypes,
MapType,
mergeMapDatasources,
mergeUnplacedDataItemsArrays,
parseCenterPosition,
TbCircleData,
TbMapDatasource,
@ -33,8 +34,10 @@ import {
formattedDataArrayFromDatasourceData,
formattedDataFormDatasourceData,
isDefined,
isDefinedAndNotNull, isUndefined,
mergeDeepIgnoreArray, parseTbFunction
isDefinedAndNotNull,
isUndefined,
mergeDeepIgnoreArray,
parseTbFunction
} from '@core/utils';
import { DeepPartial } from '@shared/models/common';
import L from 'leaflet';
@ -42,11 +45,10 @@ import { forkJoin, Observable, of } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
import '@home/components/widget/lib/maps/leaflet/leaflet-tb';
import {
MapDataLayerType,
TbDataLayerItem,
TbMapDataLayer,
TbLatestDataLayerItem,
TbLatestMapDataLayer,
UnplacedMapDataItem,
} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { FormattedData, MapItemType, WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models';
import { EntityDataPageLink } from '@shared/models/query/query.models';
@ -66,12 +68,13 @@ import { createColorMarkerShapeURI, MarkerShape } from '@home/components/widget/
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import tinycolor from 'tinycolor2';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component';
import { ComponentRef } from '@angular/core';
import { TbTripsDataLayer } from '@home/components/widget/lib/maps/data-layer/trips-data-layer';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]};
@ -87,7 +90,8 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected southWest = new L.LatLng(-L.Projection.SphericalMercator['MAX_LATITUDE'], -180);
protected northEast = new L.LatLng(L.Projection.SphericalMercator['MAX_LATITUDE'], 180);
protected dataLayers: TbMapDataLayer<any>[];
protected dataLayers: TbMapDataLayer[];
protected latestDataLayers: TbLatestMapDataLayer[];
protected tripDataLayers: TbTripsDataLayer[];
protected dsData: FormattedData<TbMapDatasource>[] = [];
@ -97,7 +101,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected timeStep: number;
protected currentTime: number;
protected selectedDataItem: TbDataLayerItem;
protected selectedDataItem: TbLatestDataLayerItem;
protected mapLayoutElement: HTMLElement;
protected mapElement: HTMLElement;
@ -116,9 +120,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected timeLineComponent: MapTimelinePanelComponent;
protected locationSnapFilterFunction: CompiledTbFunction<MapBooleanFunction>;
protected addMarkerDataLayers: TbMapDataLayer<any>[];
protected addPolygonDataLayers: TbMapDataLayer<any>[];
protected addCircleDataLayers: TbMapDataLayer<any>[];
protected addMarkerDataLayers: TbLatestMapDataLayer<any>[];
protected addPolygonDataLayers: TbLatestMapDataLayer<any>[];
protected addCircleDataLayers: TbLatestMapDataLayer<any>[];
private readonly mapResize$: ResizeObserver;
@ -226,24 +230,32 @@ export abstract class TbMap<S extends BaseMapSettings> {
private setupDataLayers() {
this.dataLayers = [];
this.latestDataLayers = [];
this.tripDataLayers = [];
if (this.settings.markers) {
this.dataLayers.push(...this.settings.markers.map(settings => new TbMarkersDataLayer(this, settings)));
const markersDataLayers = this.settings.markers.map(settings => new TbMarkersDataLayer(this, settings));
this.dataLayers.push(...markersDataLayers);
this.latestDataLayers.push(...markersDataLayers);
}
if (this.settings.polygons) {
this.dataLayers.push(...this.settings.polygons.map(settings => new TbPolygonsDataLayer(this, settings)));
const polygonsDataLayers = this.settings.polygons.map(settings => new TbPolygonsDataLayer(this, settings));
this.dataLayers.push(...polygonsDataLayers);
this.latestDataLayers.push(...polygonsDataLayers);
}
if (this.settings.circles) {
this.dataLayers.push(...this.settings.circles.map(settings => new TbCirclesDataLayer(this, settings)));
const circlesDataLayers = this.settings.circles.map(settings => new TbCirclesDataLayer(this, settings));
this.dataLayers.push(...circlesDataLayers);
this.latestDataLayers.push(...circlesDataLayers);
}
if (this.settings.trips) {
this.tripDataLayers.push(...this.settings.trips.map(settings => new TbTripsDataLayer(this, settings)));
const tripsDataLayers = this.settings.trips.map(settings => new TbTripsDataLayer(this, settings));
this.dataLayers.push(...tripsDataLayers);
this.tripDataLayers.push(...tripsDataLayers);
}
if (this.dataLayers.length || this.tripDataLayers.length) {
if (this.dataLayers.length) {
const groupsMap = new Map<string, L.TB.GroupData>();
const customTranslate = this.ctx.$injector.get(CustomTranslatePipe);
const allDataLayers = [...this.dataLayers, ...this.tripDataLayers];
allDataLayers.forEach(dl => {
this.dataLayers.forEach(dl => {
dl.getGroups().forEach(group => {
let groupData = groupsMap.get(group);
if (!groupData) {
@ -274,12 +286,12 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.updateBounds();
});
}
const setup = allDataLayers.map(dl => dl.setup());
const setup = this.dataLayers.map(dl => dl.setup());
forkJoin(setup).subscribe(
() => {
let datasources: TbMapDatasource[];
for (const layerType of (Object.keys(MapDataLayerType) as MapDataLayerType[])) {
const typeDatasources = this.dataLayers.filter(dl => dl.dataLayerType() === layerType).map(dl => dl.getDatasource());
for (const layerType of mapDataLayerTypes) {
const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType).map(dl => dl.getDatasource());
if (!datasources) {
datasources = typeDatasources;
} else {
@ -377,18 +389,18 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.deselectItem();
});
if (this.dataLayers.some(dl => dl.isEditable())) {
if (this.latestDataLayers.some(dl => dl.isEditable())) {
this.map.pm.setGlobalOptions({ snappable: false });
this.map.pm.applyGlobalOptions();
}
const addSupportedDataLayers = this.dataLayers.filter(dl => dl.isAddEnabled());
const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled());
if (addSupportedDataLayers.length) {
const drawToolbar = L.TB.toolbar({
position: this.settings.controlsPosition
}).addTo(this.map);
this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.marker);
this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers');
if (this.addMarkerDataLayers.length) {
this.addMarkerButton = drawToolbar.toolbarButton({
id: 'addMarker',
@ -401,7 +413,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.addMarkerButton.setDisabled(true);
this.setPlaceMarkerStyle();
}
this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.polygon);
this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons');
if (this.addPolygonDataLayers.length) {
this.addRectangleButton = drawToolbar.toolbarButton({
id: 'addRectangle',
@ -422,7 +434,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
});
this.addPolygonButton.setDisabled(true);
}
this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.circle);
this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles');
if (this.addCircleDataLayers.length) {
this.addCircleButton = drawToolbar.toolbarButton({
id: 'addCircle',
@ -465,13 +477,20 @@ export abstract class TbMap<S extends BaseMapSettings> {
}));
}
private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbMapDataLayer[],
private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbLatestMapDataLayer[],
prepareDrawMode: (entity: UnplacedMapDataItem) => void): void {
if (this.isPlacingItem) {
return;
}
this.updatePlaceItemState(button);
const items = mergeUnplacedDataItemsArrays(dataLayers.filter(dl => dl.isEnabled()).map(dl => dl.getUnplacedItems())).sort((entity1, entity2) => {
const itemsMap = new Map<string, UnplacedMapDataItem>();
const dataItemsArrays = dataLayers.filter(dl => dl.isEnabled()).map(dl => dl.getUnplacedItems());
dataItemsArrays.forEach(dataItems => {
dataItems.forEach(dataItem => {
itemsMap.set(dataItem.entity.$datasource.entityId, dataItem);
});
});
const items = Array.from(itemsMap.values()).sort((entity1, entity2) => {
return entity1.entity.entityDisplayName.localeCompare(entity2.entity.entityDisplayName);
});
this.selectEntityToPlace(e, items).subscribe((entity) => {
@ -486,7 +505,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
prepareDrawMode(entity);
this.dataLayers.forEach(dl => dl.disableEditMode());
this.latestDataLayers.forEach(dl => dl.disableEditMode());
this.editToolbar.open([
{
@ -605,7 +624,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
}));
}
private createItem(e: MouseEvent, actionData: CustomActionData, prepareDrawMode: () => void) {
private createItem(_e: MouseEvent, actionData: CustomActionData, prepareDrawMode: () => void) {
if (this.isPlacingItem) {
return;
}
@ -625,7 +644,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
prepareDrawMode();
this.dataLayers.forEach(dl => dl.disableEditMode());
this.latestDataLayers.forEach(dl => dl.disableEditMode());
this.editToolbar.open([
{
@ -671,7 +690,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
private finishAdd = () => {
this.map.off('pm:create');
this.map.pm.disableDraw();
this.dataLayers.forEach(dl => dl.enableEditMode());
this.latestDataLayers.forEach(dl => dl.enableEditMode());
this.updatePlaceItemState();
this.editToolbar.close();
}
@ -757,7 +776,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
private update(subscription: IWidgetSubscription) {
this.dsData = formattedDataFormDatasourceData<TbMapDatasource>(subscription.data,
undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]);
this.dataLayers.forEach(dl => dl.updateData(this.dsData));
this.latestDataLayers.forEach(dl => dl.updateData(this.dsData));
this.updateTripsAppearance();
this.updateTripsAnchors();
this.updateBounds();
@ -848,9 +867,6 @@ export abstract class TbMap<S extends BaseMapSettings> {
private updateBounds() {
const enabledDataLayers = this.dataLayers.filter(dl => dl.isEnabled());
const dataLayersBounds = enabledDataLayers.map(dl => dl.getBounds()).filter(b => b.isValid());
const enabledTripsDataLayers = this.tripDataLayers.filter(dl => dl.isEnabled());
const tripsDataLayersBounds = enabledTripsDataLayers.map(dl => dl.getBounds()).filter(b => b.isValid());
dataLayersBounds.push(...tripsDataLayersBounds);
let bounds: L.LatLngBounds;
if (dataLayersBounds.length) {
bounds = new L.LatLngBounds(null, null);
@ -929,7 +945,6 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected invalidateDataLayersCoordinates(): void {
this.dataLayers.forEach(dl => dl.invalidateCoordinates());
this.tripDataLayers.forEach(dl => dl.invalidateCoordinates());
}
protected getSidebar(): L.TB.SidebarControl {
@ -976,7 +991,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
}, entityName, data, entityLabel);
}
public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean {
public selectItem(item: TbLatestDataLayerItem, cancel = false, force = false): boolean {
if (this.isPlacingItem) {
return false;
}

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

@ -29,7 +29,6 @@ import {
isDefinedAndNotNull,
isNotEmptyStr,
isString,
isUndefined,
isUndefinedOrNull,
mergeDeep
} from '@core/utils';
@ -41,7 +40,6 @@ import { Observable, Observer, of, switchMap } from 'rxjs';
import { map } from 'rxjs/operators';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models';
import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import { DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models';
export enum MapType {
@ -168,6 +166,8 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataL
export type MapDataLayerType = 'trips' | 'markers' | 'polygons' | 'circles';
export const mapDataLayerTypes: MapDataLayerType[] = ['trips', 'markers', 'polygons', 'circles'];
export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapDataLayerType): boolean => {
if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) {
return false;
@ -744,10 +744,10 @@ export const mapLayerValid = (layer: MapLayerSettings): boolean => {
return !!openStreetLayer.layerType;
case MapProvider.google:
const googleLayer = layer as GoogleMapLayerSettings;
return !!googleLayer.layerType && !!googleLayer.apiKey;
return !!googleLayer.layerType;
case MapProvider.here:
const hereLayer = layer as HereMapLayerSettings;
return !!hereLayer.layerType && !!hereLayer.apiKey;
return !!hereLayer.layerType;
case MapProvider.tencent:
const tencentLayer = layer as TencentMapLayerSettings;
return !!tencentLayer.layerType;
@ -934,30 +934,23 @@ export const defaultMapLayerSettings = (provider: MapProvider): MapLayerSettings
}
};
export const defaultMapLayers: MapLayerSettings[] = (Object.keys(OpenStreetLayerType) as OpenStreetLayerType[]).map(type => ({
provider: MapProvider.openstreet,
layerType: OpenStreetLayerType[type]
} as MapLayerSettings));
/*.concat((Object.keys(GoogleLayerType) as GoogleLayerType[]).map(type =>
mergeDeep({} as MapLayerSettings, defaultGoogleMapLayerSettings, {layerType: GoogleLayerType[type]} as GoogleMapLayerSettings)).concat(
(Object.keys(TencentLayerType) as TencentLayerType[]).map(type => ({
provider: MapProvider.tencent,
layerType: TencentLayerType[type]
} as MapLayerSettings))
)).concat(
(Object.keys(HereLayerType) as HereLayerType[]).map(type =>
mergeDeep({} as MapLayerSettings, defaultHereMapLayerSettings, {layerType: HereLayerType[type]} as HereMapLayerSettings))
).concat([
mergeDeep({} as MapLayerSettings, defaultCustomMapLayerSettings, {label: 'Custom 1'} as CustomMapLayerSettings),
mergeDeep({} as MapLayerSettings, defaultCustomMapLayerSettings, {
tileUrl: 'http://a.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png',
label: 'Custom 2'
} as CustomMapLayerSettings),
mergeDeep({} as MapLayerSettings, defaultCustomMapLayerSettings, {
tileUrl: 'http://b.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png',
label: 'Custom 3'
} as CustomMapLayerSettings)
]);*/
export const defaultMapLayers: MapLayerSettings[] = [
{
label: '{i18n:widgets.maps.layer.roadmap}',
provider: MapProvider.openstreet,
layerType: OpenStreetLayerType.openStreetMapnik,
} as OpenStreetMapLayerSettings,
{
label: '{i18n:widgets.maps.layer.satellite}',
provider: MapProvider.openstreet,
layerType: OpenStreetLayerType.esriWorldImagery,
} as OpenStreetMapLayerSettings,
{
label: '{i18n:widgets.maps.layer.hybrid}',
provider: MapProvider.google,
layerType: GoogleLayerType.hybrid,
} as GoogleMapLayerSettings
];
export interface GeoMapSettings extends BaseMapSettings {
layers?: MapLayerSettings[];
@ -1103,24 +1096,6 @@ export const isCutPolygon = (data: TbPolygonCoordinates | TbPolygonRawCoordinate
return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || data[0][0] instanceof L.LatLng);
}
export const latLngPointToBounds = (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;
}
export const parseCenterPosition = (position: string | [number, number]): [number, number] => {
if (typeof (position) === 'string') {
const parts = position.split(',');
@ -1187,16 +1162,6 @@ const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): T
return target;
}
export const mergeUnplacedDataItemsArrays = (dataItemsArrays: UnplacedMapDataItem[][]): UnplacedMapDataItem[] => {
const itemsMap = new Map<string, UnplacedMapDataItem>();
dataItemsArrays.forEach(dataItems => {
dataItems.forEach(dataItem => {
itemsMap.set(dataItem.entity.$datasource.entityId, dataItem);
});
});
return Array.from(itemsMap.values());
}
const imageAspectMap: {[key: string]: ImageWithAspect} = {};
const imageLoader = (imageUrl: string): Observable<HTMLImageElement> => new Observable((observer: Observer<HTMLImageElement>) => {
@ -1297,7 +1262,7 @@ export const calculateNewPointCoordinate = (coordinate: number, imageSize: numbe
return pointCoordinate;
}
export const checkLngLat = (point: L.LatLng, southWest: L.LatLng, northEast: L.LatLng, offset = 0): L.LatLng => {
export const latLngPointToBounds = (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;

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts

@ -20,7 +20,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
@Component({
selector: 'tb-select-map-entity-panel',

4
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -233,8 +233,8 @@
<textarea matInput #descriptionInput
[(ngModel)]="widget.description"
(ngModelChange)="isDirty = true"
rows="2" maxlength="255"></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint>
rows="2" maxlength="1024"></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/1024</mat-hint>
</mat-form-field>
<tb-string-items-list
label="{{ 'widget.tags' | translate }}"

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

@ -7743,6 +7743,9 @@
"layer-settings": "Layer settings",
"remove-layer": "Remove layer",
"no-layers": "No layers configured",
"roadmap": "Roadmap",
"satellite": "Satellite",
"hybrid": "Hybrid",
"provider": {
"provider": "Provider",
"openstreet": {
@ -7754,7 +7757,6 @@
"esri-imagery": "WorldImagery",
"cartodb-positron": "Positron",
"cartodb-dark-matter": "DarkMatter"
},
"google": {
"title": "Google",

Loading…
Cancel
Save