UI: Map editor toolbar.

This commit is contained in:
Igor Kulikov 2025-01-27 17:56:33 +02:00
parent bd86e47f52
commit 4ab9fb9f7b
8 changed files with 425 additions and 65 deletions

View File

@ -45,6 +45,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
const center = new L.LatLng(circleData.latitude, circleData.longitude);
this.circleStyle = this.dataLayer.getShapeStyle(data, dsData);
this.circle = L.circle(center, {
bubblingMouseEvents: false,
radius: circleData.radius,
...this.circleStyle,
snapIgnore: !this.dataLayer.isSnappable()
@ -54,7 +55,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
}
protected createEventListeners(data: FormattedData<TbMapDatasource>, _dsData: FormattedData<TbMapDatasource>[]): void {
this.dataLayer.getMap().circleClick(this.circle, data.$datasource);
this.dataLayer.getMap().circleClick(this, data.$datasource);
}
protected unbindLabel() {
@ -62,7 +63,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
}
protected bindLabel(content: L.Content): void {
this.circle.bindTooltip(content, { className: 'tb-polygon-label', permanent: true, direction: 'center'})
this.circle.bindTooltip(content, { className: 'tb-circle-label', permanent: true, direction: 'center'})
.openTooltip(this.circle.getLatLng());
}
@ -110,6 +111,14 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
this.circle.off('pm:dragend');
}
protected removeDataItem(): void {
this.dataLayer.saveCircleCoordinates(this.data, null, null);
}
public isEditing() {
return this.editing;
}
private saveCircleCoordinates() {
const center = this.circle.getLatLng();
const radius = this.circle.getRadius();
@ -169,7 +178,7 @@ export class TbCirclesDataLayer extends TbShapesDataLayer<CirclesDataLayerSettin
}
public saveCircleCoordinates(data: FormattedData<TbMapDatasource>, center: L.LatLng, radius: number): void {
const converted = this.map.coordinatesToCircleData(center, radius);
const converted = center ? this.map.coordinatesToCircleData(center, radius) : null;
const circleData = [
{
dataKey: this.settings.circleKey,

View File

@ -39,17 +39,19 @@ import {
parseTbFunction,
safeExecuteTbFunction
} from '@core/utils';
import L, { LatLngBounds } from 'leaflet';
import L from 'leaflet';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { map } from 'rxjs/operators';
import { WidgetContext } from '@home/models/widget-component.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
export abstract class TbDataLayerItem<S extends MapDataLayerSettings, D extends TbMapDataLayer<S,D>, L extends L.Layer = L.Layer> {
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>[],
@ -61,6 +63,7 @@ export abstract class TbDataLayerItem<S extends MapDataLayerSettings, D extends
this.createTooltip(data.$datasource);
this.updateTooltip(data, dsData);
}
this.bindEvents();
this.createEventListeners(data, dsData);
try {
this.dataLayer.getDataLayerContainer().addLayer(this.layer);
@ -90,6 +93,21 @@ export abstract class TbDataLayerItem<S extends MapDataLayerSettings, D extends
protected abstract disableDrag(): void;
protected bindEvents(): void {
if (this.dataLayer.isSelectable()) {
this.layer.on('click', () => {
if (!this.isEditing()) {
this.dataLayer.getMap().selectItem(this);
}
});
this.layer.on('remove', () => {
if (this.selected) {
this.dataLayer.getMap().deselectItem();
}
});
}
}
protected enableEdit(): void {
if (this.dataLayer.isHoverable()) {
this.addItemClass('tb-hoverable');
@ -110,16 +128,57 @@ export abstract class TbDataLayerItem<S extends MapDataLayerSettings, D extends
}
}
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.updateSelectedState();
const buttons: L.TB.ToolbarButtonOptions[] = [];
if (this.dataLayer.isRemoveEnabled()) {
buttons.push({
title: this.dataLayer.getCtx().translate.instant('action.remove'),
click: () => {
this.removeDataItem();
},
iconClass: 'tb-remove'
});
}
return buttons;
} else {
return [];
}
}
public deselect() {
if (this.selected) {
this.selected = false;
this.layer.closePopup();
this.updateSelectedState();
}
}
public isSelected() {
return this.selected;
}
public editModeUpdated() {
if (this.dataLayer.isEditMode()) {
this.enableEdit();
} else {
this.disableEdit();
}
this.updateSelectedState();
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
@ -128,14 +187,25 @@ export abstract class TbDataLayerItem<S extends MapDataLayerSettings, D extends
}
public remove() {
this.layer.off();
if (this.selected) {
this.dataLayer.getMap().deselectItem();
}
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) {
let tooltipTemplate = this.dataLayer.dataLayerTooltipProcessor.processPattern(data, dsData);
@ -157,13 +227,25 @@ export abstract class TbDataLayerItem<S extends MapDataLayerSettings, D extends
}
}
protected abstract removeDataItem(): void;
private createTooltip(datasource: TbMapDatasource) {
this.tooltip = L.popup();
this.layer.bindPopup(this.tooltip, {autoClose: this.settings.tooltip.autoclose, closeOnClick: false});
if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.hover) {
this.layer.off('click');
this.layer.off('click');
if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.click) {
this.layer.on('click', () => {
if (this.tooltip.isOpen()) {
this.layer.closePopup();
} else if (!this.isEditing()) {
this.layer.openPopup();
}
});
} else if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.hover) {
this.layer.on('mouseover', () => {
this.layer.openPopup();
if (!this.isEditing()) {
this.layer.openPopup();
}
});
this.layer.on('mousemove', (e) => {
this.tooltip.setLatLng(e.latlng);
@ -345,7 +427,7 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
return this.dataLayerContainer;
}
public getBounds(): LatLngBounds {
public getBounds(): L.LatLngBounds {
return this.dataLayerContainer.getBounds();
}

View File

@ -39,7 +39,14 @@ import L, { FeatureGroup } from 'leaflet';
import { FormattedData } from '@shared/models/widget.models';
import { forkJoin, Observable, of } from 'rxjs';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { isDefined, isDefinedAndNotNull, isEmptyStr, parseTbFunction, safeExecuteTbFunction } from '@core/utils';
import {
deepClone,
isDefined,
isDefinedAndNotNull,
isEmptyStr,
parseTbFunction,
safeExecuteTbFunction
} from '@core/utils';
import { catchError, map, switchMap } from 'rxjs/operators';
import tinycolor from 'tinycolor2';
import { ImagePipe } from '@shared/pipe/image.pipe';
@ -77,16 +84,15 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
const location = this.dataLayer.extractLocation(data, dsData);
this.marker = L.marker(location, {
tbMarkerData: data,
snapIgnore: !this.dataLayer.isSnappable()
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: false
});
this.updateMarkerIcon(data, dsData);
return this.marker;
}
protected createEventListeners(data: FormattedData<TbMapDatasource>, _dsData: FormattedData<TbMapDatasource>[]): void {
this.dataLayer.getMap().markerClick(this.marker, data.$datasource);
this.dataLayer.getMap().markerClick(this, data.$datasource);
}
protected unbindLabel() {
@ -122,7 +128,7 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
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);
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);
}
@ -166,6 +172,14 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
}
}
protected removeDataItem(): void {
this.dataLayer.saveMarkerLocation(this.data, null);
}
public isEditing() {
return this.moving;
}
private saveMarkerLocation() {
const location = this.marker.getLatLng();
this.dataLayer.saveMarkerLocation(this.data, location);
@ -181,9 +195,16 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
private updateMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe(
(iconInfo) => {
iconInfo.icon.options.className = this.updateIconClasses(iconInfo.icon.options.className);
this.marker.setIcon(iconInfo.icon);
const anchor = iconInfo.icon.options.iconAnchor;
let icon: L.Icon | L.DivIcon;
const options = deepClone(iconInfo.icon.options);
options.className = this.updateIconClasses(options.className);
if (iconInfo.icon instanceof L.Icon) {
icon = L.icon(options as L.IconOptions);
} else {
icon = L.divIcon(options);
}
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 {
@ -195,11 +216,17 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
);
}
private updateIconClasses(className: string): string {
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);

View File

@ -48,7 +48,8 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
this.polygonStyle = this.dataLayer.getShapeStyle(data, dsData);
this.polygon = polyConstructor(polyData as (TbPolygonRawCoordinates & L.LatLngTuple[]), {
...this.polygonStyle,
snapIgnore: !this.dataLayer.isSnappable()
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: false
});
this.polygonContainer = L.featureGroup();
@ -59,7 +60,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
}
protected createEventListeners(data: FormattedData<TbMapDatasource>, _dsData: FormattedData<TbMapDatasource>[]): void {
this.dataLayer.getMap().polygonClick(this.polygonContainer, data.$datasource);
this.dataLayer.getMap().polygonClick(this, data.$datasource);
}
protected unbindLabel() {
@ -103,7 +104,12 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
this.polygon.on('pm:dragstart', () => {
this.editing = true;
});
this.polygon.on('pm:dragend', () => {
this.polygon.on('pm:drag', () => {
if (this.tooltip?.isOpen()) {
this.tooltip.setLatLng(this.polygon.getBounds().getCenter());
}
});
this.polygon.on('pm:dragend', (e) => {
this.savePolygonCoordinates();
this.editing = false;
});
@ -115,6 +121,14 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
this.polygon.off('pm:dragend');
}
protected removeDataItem(): void {
this.dataLayer.savePolygonCoordinates(this.data, null);
}
public isEditing() {
return this.editing;
}
private savePolygonCoordinates() {
let coordinates: TbPolygonCoordinates = this.polygon.getLatLngs();
if (coordinates.length === 1) {
@ -140,7 +154,8 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
this.polygonContainer.removeLayer(this.polygon);
this.polygon = L.polygon(polyData, {
...this.polygonStyle,
snapIgnore: !this.dataLayer.isSnappable()
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: false
});
this.polygon.addTo(this.polygonContainer);
this.editModeUpdated();
@ -197,7 +212,7 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer<PolygonsDataLayerSett
}
public savePolygonCoordinates(data: FormattedData<TbMapDatasource>, coordinates: TbPolygonCoordinates): void {
const converted = this.map.coordinatesToPolygonData(coordinates);
const converted = coordinates ? this.map.coordinatesToPolygonData(coordinates) : null;
const polygonData = [
{
dataKey: this.settings.polygonKey,

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import L, { Coords, TB, TileLayerOptions } from 'leaflet';
import L, { TB } from 'leaflet';
import { guid } from '@core/utils';
import 'leaflet-providers';
import '@geoman-io/leaflet-geoman-free';
@ -276,6 +276,81 @@ class GroupsControl extends SidebarPaneControl<TB.GroupsControlOptions> {
}
}
class ToolbarButton extends L.Control<TB.ToolbarButtonOptions> {
private readonly button: JQuery<HTMLElement>;
constructor(options: TB.ToolbarButtonOptions) {
super(options);
this.button = $("<a>")
.attr('class', 'tb-control-button')
.attr('href', '#')
.attr('role', 'button')
.attr('title', this.options.title)
.html('<div class="'+this.options.iconClass+'"></div>');
this.button.on('click', (e) => {
e.stopPropagation();
e.preventDefault();
this.options.click(e.originalEvent, this);
});
}
addToToolbar(toolbar: BottomToolbarControl): void {
this.button.appendTo(toolbar.container);
}
}
class BottomToolbarControl extends L.Control<TB.BottomToolbarControlOptions> {
private readonly buttonContainer: JQuery<HTMLElement>;
container: HTMLElement;
constructor(options: TB.BottomToolbarControlOptions) {
super(options);
const controlContainer = $('.leaflet-control-container', options.mapElement);
const toolbar = $('<div class="tb-map-bottom-toolbar leaflet-bottom"></div>');
toolbar.appendTo(controlContainer);
this.buttonContainer = $('<div class="leaflet-bar leaflet-control"></div>');
this.buttonContainer.appendTo(toolbar);
this.container = this.buttonContainer[0];
}
addTo(map: L.Map): this {
return this;
}
open(buttons: TB.ToolbarButtonOptions[]): void {
buttons.forEach(buttonOption => {
const button = new ToolbarButton(buttonOption);
button.addToToolbar(this);
});
const closeButton = $("<a>")
.attr('class', 'tb-control-button')
.attr('href', '#')
.attr('role', 'button')
.attr('title', this.options.closeTitle)
.html('<div class="tb-close"></div>');
closeButton.on('click', (e) => {
e.stopPropagation();
e.preventDefault();
this.close();
});
closeButton.appendTo(this.buttonContainer);
}
close(): void {
this.buttonContainer.empty();
if (this.options.onClose) {
this.options.onClose();
}
}
}
const sidebar = (options: TB.SidebarControlOptions): SidebarControl => {
return new SidebarControl(options);
}
@ -292,6 +367,10 @@ const groups = (options: TB.GroupsControlOptions): GroupsControl => {
return new GroupsControl(options);
}
const bottomToolbar = (options: TB.BottomToolbarControlOptions): BottomToolbarControl => {
return new BottomToolbarControl(options);
}
class ChinaProvider extends L.TileLayer {
static chinaProviders: L.TB.TileLayer.ChinaProvidersData = {
@ -303,7 +382,7 @@ class ChinaProvider extends L.TileLayer {
}
};
constructor(type: string, options?: TileLayerOptions) {
constructor(type: string, options?: L.TileLayerOptions) {
options = options || {};
const parts = type.split('.');
@ -316,7 +395,7 @@ class ChinaProvider extends L.TileLayer {
super(url, options);
}
getTileUrl(coords: Coords): string {
getTileUrl(coords: L.Coords): string {
const data = {
s: this._getSubdomain(coords),
x: coords.x,
@ -338,7 +417,7 @@ class ChinaProvider extends L.TileLayer {
}
}
const chinaProvider = (type: string, options?: TileLayerOptions): ChinaProvider => {
const chinaProvider = (type: string, options?: L.TileLayerOptions): ChinaProvider => {
return new ChinaProvider(type, options);
}
@ -347,10 +426,13 @@ L.TB = L.TB || {
SidebarPaneControl,
LayersControl,
GroupsControl,
ToolbarButton,
BottomToolbarControl,
sidebar,
sidebarPane,
layers,
groups,
bottomToolbar,
TileLayer: {
ChinaProvider
},

View File

@ -17,6 +17,7 @@
//$map-element-hover-color: #307FE5;
$map-element-hover-color: rgba(0,0,0,0.56);
$map-element-selected-color: #307FE5;
.tb-map-layout {
display: flex;
@ -79,6 +80,17 @@ $map-element-hover-color: rgba(0,0,0,0.56);
}
}
}
.tb-map-bottom-toolbar {
left: 0;
right: 0;
display: flex;
flex-direction: row;
justify-content: center;
.leaflet-bar {
display: flex;
flex-direction: row;
}
}
}
.leaflet-control {
.tb-control-button {
@ -100,6 +112,27 @@ $map-element-hover-color: rgba(0,0,0,0.56);
&.tb-groups {
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M6 13.5C5.5875 13.5 5.2345 13.3533 4.941 13.0597C4.6475 12.7662 4.5005 12.413 4.5 12V3C4.5 2.5875 4.647 2.2345 4.941 1.941C5.235 1.6475 5.588 1.5005 6 1.5H15C15.4125 1.5 15.7657 1.647 16.0597 1.941C16.3538 2.235 16.5005 2.588 16.5 3V12C16.5 12.4125 16.3533 12.7657 16.0597 13.0597C15.7662 13.3538 15.413 13.5005 15 13.5H6ZM6 4.5H15V3H6V4.5ZM3 16.5C2.5875 16.5 2.2345 16.3533 1.941 16.0597C1.6475 15.7662 1.5005 15.413 1.5 15V4.5H3V15H13.5V16.5H3Z"/></svg>');
}
&.tb-remove {
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M4.5 14.25C4.5 14.6478 4.65804 15.0294 4.93934 15.3107C5.22064 15.592 5.60218 15.75 6 15.75H12C12.3978 15.75 12.7794 15.592 13.0607 15.3107C13.342 15.0294 13.5 14.6478 13.5 14.25V5.25H4.5V14.25ZM6 6.75H12V14.25H6V6.75ZM11.625 3L10.875 2.25H7.125L6.375 3H3.75V4.5H14.25V3H11.625Z"/></svg>');
}
&.tb-close {
background: #D12730;
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M14.25 4.8075L13.1925 3.75L9 7.9425L4.8075 3.75L3.75 4.8075L7.9425 9L3.75 13.1925L4.8075 14.25L9 10.0575L13.1925 14.25L14.25 13.1925L10.0575 9L14.25 4.8075Z"/></svg>');
}
}
}
}
.leaflet-map-pane:not(.leaflet-zoom-anim) {
.leaflet-marker-icon {
&.tb-hoverable:not(.tb-selected) {
svg {
transition: filter 0.2s;
}
}
}
img.leaflet-marker-icon, path {
&.tb-hoverable:not(.tb-selected) {
transition: filter 0.2s;
}
}
}
@ -110,28 +143,35 @@ $map-element-hover-color: rgba(0,0,0,0.56);
&.tb-draggable {
cursor: move;
}
&.tb-hoverable {
svg {
transition: filter 0.2s;
}
&.tb-hoverable:not(.tb-selected) {
&:hover {
svg {
filter: drop-shadow( 0 0 4px $map-element-hover-color);
//filter: drop-shadow( 0 0 4px $map-element-hover-color);
filter: brightness(0.8) drop-shadow( 0 0 4px $map-element-hover-color);
}
}
}
&.tb-selected {
svg {
filter: brightness(0.8);
//animation: tb-selected-animation 0.5s linear 0s infinite alternate;
}
}
}
}
img.leaflet-marker-icon, path {
&.tb-draggable {
cursor: move;
}
&.tb-hoverable {
transition: filter 0.2s;
&.tb-hoverable:not(.tb-selected) {
&:hover {
filter: drop-shadow( 0 0 4px $map-element-hover-color);
filter: brightness(0.8) drop-shadow( 0 0 4px $map-element-hover-color);
}
}
&.tb-selected {
filter: brightness(0.8);
//animation: tb-selected-animation 0.5s linear 0s infinite alternate;
}
}
.tb-cluster-marker-container {
border: none;
@ -144,6 +184,11 @@ $map-element-hover-color: rgba(0,0,0,0.56);
width: 40px;
height: 40px;
}
.tb-marker-label, .tb-polygon-label, .tb-circle-label {
border: none;
background: none;
box-shadow: none;
}
}
.tb-map-sidebar {
.tb-layers, .tb-groups {
@ -267,3 +312,14 @@ $map-element-hover-color: rgba(0,0,0,0.56);
}
}
}
@keyframes tb-selected-animation {
0% {
//filter: drop-shadow( 0 0 2px $map-element-selected-color);
filter: brightness(1);
}
100% {
//filter: drop-shadow( 0 0 4px $map-element-selected-color) drop-shadow( 0 0 4px $map-element-selected-color);
filter: brightness(0.8);
}
}

View File

@ -32,7 +32,11 @@ import L from 'leaflet';
import { forkJoin, Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import '@home/components/widget/lib/maps/leaflet/leaflet-tb';
import { MapDataLayerType, TbMapDataLayer, } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import {
MapDataLayerType,
TbDataLayerItem,
TbMapDataLayer,
} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { FormattedData, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models';
import { EntityDataPageLink } from '@shared/models/query/query.models';
@ -44,6 +48,9 @@ import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]};
export abstract class TbMap<S extends BaseMapSettings> {
@ -59,10 +66,14 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected dataLayers: TbMapDataLayer<any,any>[];
protected dsData: FormattedData<TbMapDatasource>[];
protected selectedDataItem: TbDataLayerItem;
protected mapElement: HTMLElement;
protected sidebar: L.TB.SidebarControl;
protected editToolbar: L.TB.BottomToolbarControl;
private readonly mapResize$: ResizeObserver;
private readonly tooltipActions: { [name: string]: MapActionHandler };
@ -70,7 +81,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
private readonly polygonClickActions: { [name: string]: MapActionHandler };
private readonly circleClickActions: { [name: string]: MapActionHandler };
private tooltipInstances: ITooltipsterInstance[] = [];
private tooltipInstances: TooltipInstancesData[] = [];
protected constructor(protected ctx: WidgetContext,
protected inputSettings: DeepPartial<S>,
@ -133,7 +144,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
this.setupDataLayers();
this.setupEditMode();
this.createdControlButtonTooltip();
this.createdControlButtonTooltip(this.mapElement, ['topleft', 'bottomleft'].includes(this.settings.controlsPosition) ? 'right' : 'left');
}
private setupDataLayers() {
@ -231,21 +242,36 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
private setupEditMode() {
const dragEnabled = this.dataLayers.some(dl => dl.isDragEnabled());
if (dragEnabled) {
//this.map.pm.enableGlobalDragMode();
}
this.editToolbar = L.TB.bottomToolbar({
mapElement: $(this.mapElement),
closeTitle: this.ctx.translate.instant('action.cancel'),
onClose: () => {
this.deselectItem();
}
}).addTo(this.map);
this.map.on('click', () => {
this.deselectItem();
});
}
private createdControlButtonTooltip() {
private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) {
import('tooltipster').then(() => {
let tooltipData = this.tooltipInstances.find(d => d.root === root);
if (!tooltipData) {
tooltipData = {
root,
instances: []
}
this.tooltipInstances.push(tooltipData);
}
if ($.tooltipster) {
this.tooltipInstances.forEach((instance) => {
tooltipData.instances.forEach((instance) => {
instance.destroy();
});
this.tooltipInstances = [];
tooltipData.instances = [];
}
$(this.mapElement)
$(root)
.find('a[role="button"]:not(.leaflet-pm-action)')
.each((_index, element) => {
let title: string;
@ -267,7 +293,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
scroll: true,
mouseleave: true
},
side: ['topleft', 'bottomleft'].includes(this.settings.controlsPosition) ? 'right' : 'left',
side,
distance: 2,
trackOrigin: true,
functionBefore: (_instance, helper) => {
@ -277,7 +303,14 @@ export abstract class TbMap<S extends BaseMapSettings> {
},
}
);
this.tooltipInstances.push(tooltip.tooltipster('instance'));
const instance = tooltip.tooltipster('instance');
tooltipData.instances.push(instance);
instance.on('destroyed', () => {
const index = tooltipData.instances.indexOf(instance);
if (index > -1) {
tooltipData.instances.splice(index, 1);
}
});
});
});
}
@ -385,36 +418,64 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
}
public markerClick(marker: L.Layer, datasource: TbMapDatasource): void {
public markerClick(marker: TbDataLayerItem, datasource: TbMapDatasource): void {
if (Object.keys(this.markerClickActions).length) {
marker.on('click', (event: L.LeafletMouseEvent) => {
for (const action in this.markerClickActions) {
this.markerClickActions[action](event.originalEvent, datasource);
marker.getLayer().on('click', (event: L.LeafletMouseEvent) => {
if (!marker.isEditing()) {
for (const action in this.markerClickActions) {
this.markerClickActions[action](event.originalEvent, datasource);
}
}
});
}
}
public polygonClick(polygon: L.Layer, datasource: TbMapDatasource): void {
public polygonClick(polygon: TbDataLayerItem, datasource: TbMapDatasource): void {
if (Object.keys(this.polygonClickActions).length) {
polygon.on('click', (event: L.LeafletMouseEvent) => {
for (const action in this.polygonClickActions) {
this.polygonClickActions[action](event.originalEvent, datasource);
polygon.getLayer().on('click', (event: L.LeafletMouseEvent) => {
if (!polygon.isEditing()) {
for (const action in this.polygonClickActions) {
this.polygonClickActions[action](event.originalEvent, datasource);
}
}
});
}
}
public circleClick(circle: L.Layer, datasource: TbMapDatasource): void {
public circleClick(circle: TbDataLayerItem, datasource: TbMapDatasource): void {
if (Object.keys(this.circleClickActions).length) {
circle.on('click', (event: L.LeafletMouseEvent) => {
for (const action in this.circleClickActions) {
this.circleClickActions[action](event.originalEvent, datasource);
circle.getLayer().on('click', (event: L.LeafletMouseEvent) => {
if (!circle.isEditing()) {
for (const action in this.circleClickActions) {
this.circleClickActions[action](event.originalEvent, datasource);
}
}
});
}
}
public selectItem(item: TbDataLayerItem): void {
if (this.selectedDataItem) {
this.selectedDataItem.deselect();
this.selectedDataItem = null;
this.editToolbar.close();
}
this.selectedDataItem = item;
if (this.selectedDataItem) {
const buttons = this.selectedDataItem.select();
this.editToolbar.open(buttons);
this.createdControlButtonTooltip(this.editToolbar.container, 'top');
}
}
public deselectItem(): void {
this.selectItem(null);
}
public getSelectedDataItem(): TbDataLayerItem {
return this.selectedDataItem;
}
public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable<any> {
const attributeService = this.ctx.$injector.get(AttributeService);
const attributes: AttributeData[] = [];
@ -466,8 +527,10 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.map) {
this.map.remove();
}
this.tooltipInstances.forEach((instance) => {
instance.destroy();
this.tooltipInstances.forEach((data) => {
data.instances.forEach(instance => {
instance.destroy();
})
});
}

View File

@ -15,7 +15,7 @@
///
import { FormattedData } from '@shared/models/widget.models';
import L from 'leaflet';
import L, { Control, ControlOptions } from 'leaflet';
import { TbMapDatasource } from '@home/components/widget/lib/maps/models/map.models';
// redeclare module, maintains compatibility with @types/leaflet
@ -89,6 +89,30 @@ declare module 'leaflet' {
constructor(options: GroupsControlOptions);
}
interface ToolbarButtonOptions extends ControlOptions{
title: string;
click: (e: MouseEvent, button: ToolbarButton) => void;
iconClass: string;
}
class ToolbarButton extends Control<ToolbarButtonOptions>{
constructor(options: ToolbarButtonOptions);
addToToolbar(toolbar: BottomToolbarControl): void;
}
interface BottomToolbarControlOptions extends ControlOptions {
mapElement: JQuery<HTMLElement>;
closeTitle: string;
onClose: () => void;
}
class BottomToolbarControl extends Control<BottomToolbarControlOptions> {
constructor(options: BottomToolbarControlOptions);
open(buttons: ToolbarButtonOptions[]): void;
close(): void;
container: HTMLElement;
}
function sidebar(options: SidebarControlOptions): SidebarControl;
function sidebarPane<O extends SidebarPaneControlOptions>(options: O): SidebarPaneControl<O>;
@ -97,6 +121,8 @@ declare module 'leaflet' {
function groups(options: GroupsControlOptions): GroupsControl;
function bottomToolbar(options: BottomToolbarControlOptions): BottomToolbarControl;
namespace TileLayer {
interface ChinaProvidersData {