UI: Maps - introduce button to toggle drag-drop mode.

This commit is contained in:
Igor Kulikov 2025-03-05 12:36:57 +02:00
parent 02008a95a1
commit dabd7cca5b
8 changed files with 158 additions and 25 deletions

View File

@ -114,6 +114,18 @@ export abstract class TbLatestDataLayerItem<S extends MapDataLayerSettings = Map
this.updateBubblingMouseEvents();
}
public dragModeUpdated() {
if (this.dataLayer.isEditMode() && !this.selected) {
if (this.dataLayer.allowDrag()) {
this.enableDrag();
this.addItemClass('tb-draggable');
} else {
this.disableDrag();
this.removeItemClass('tb-draggable');
}
}
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.data = data;
this.doUpdate(data, dsData);
@ -151,7 +163,7 @@ export abstract class TbLatestDataLayerItem<S extends MapDataLayerSettings = Map
if (this.dataLayer.isHoverable()) {
this.addItemClass('tb-hoverable');
}
if (this.dataLayer.isDragEnabled()) {
if (this.dataLayer.allowDrag()) {
this.enableDrag();
this.addItemClass('tb-draggable');
}
@ -278,6 +290,10 @@ export abstract class TbLatestMapDataLayer<S extends MapDataLayerSettings = MapD
return this.dragEnabled;
}
public allowDrag(): boolean {
return this.dragEnabled && (!this.map.useDragModeButton() || this.map.dragModeEnabled());
}
public isEditEnabled(): boolean {
return this.editEnabled;
}
@ -357,6 +373,10 @@ export abstract class TbLatestMapDataLayer<S extends MapDataLayerSettings = MapD
}
}
public dragModeUpdated() {
this.updateItemsDragMode();
}
protected createDataLayerContainer(): L.FeatureGroup {
return L.featureGroup([], {snapIgnore: !this.settings.edit?.snappable});
}
@ -406,6 +426,10 @@ export abstract class TbLatestMapDataLayer<S extends MapDataLayerSettings = MapD
this.layerItems.forEach(item => item.editModeUpdated());
}
private updateItemsDragMode() {
this.layerItems.forEach(item => item.dragModeUpdated());
}
public abstract placeItem(item: UnplacedMapDataItem, layer: L.Layer): void;
protected abstract isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean;

View File

@ -161,7 +161,7 @@ class TbGoogleMapLayer extends TbMapLayer<GoogleMapLayerSettings> {
}
private loadGoogle(): Observable<boolean> {
const apiKey = this.settings.apiKey;
const apiKey = this.settings.apiKey || defaultGoogleMapLayerSettings.apiKey;
if (TbGoogleMapLayer.loadedApiKeysGlobal[apiKey]) {
return of(true);
} else {
@ -213,7 +213,8 @@ class TbHereMapLayer extends TbMapLayer<HereMapLayerSettings> {
}
protected createLayer(): Observable<L.Layer> {
const layer = L.tileLayer.provider(this.settings.layerType, {useV3: true, apiKey: this.settings.apiKey} as any);
const apiKey = this.settings.apiKey || defaultHereMapLayerSettings.apiKey;
const layer = L.tileLayer.provider(this.settings.layerType, {useV3: true, apiKey} as any);
return of(layer);
}

View File

@ -138,6 +138,9 @@
&.tb-rotate {
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M1.77 1.7625C2.8575 0.675 4.35 0 6.0075 0C9.3225 0 12 2.685 12 6C12 9.315 9.3225 12 6.0075 12C3.21 12 0.8775 10.0875 0.21 7.5H1.77C2.385 9.2475 4.05 10.5 6.0075 10.5C8.49 10.5 10.5075 8.4825 10.5075 6C10.5075 3.5175 8.49 1.5 6.0075 1.5C4.7625 1.5 3.6525 2.0175 2.8425 2.835L5.2575 5.25H0.00749922V0L1.77 1.7625Z"/></svg>');
}
&.tb-drag-mode {
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z"></path></svg>');
}
&.tb-place-marker {
mask-image: url('data:image/svg+xml,<svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg"><path d="M6 0.5C3.0975 0.5 0.75 2.8475 0.75 5.75C0.75 9.6875 6 15.5 6 15.5C6 15.5 11.25 9.6875 11.25 5.75C11.25 2.8475 8.9025 0.5 6 0.5ZM6 7.625C4.965 7.625 4.125 6.785 4.125 5.75C4.125 4.715 4.965 3.875 6 3.875C7.035 3.875 7.875 4.715 7.875 5.75C7.875 6.785 7.035 7.625 6 7.625Z"/></svg>');
}

View File

@ -109,6 +109,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected customActionsToolbar: L.TB.TopToolbarControl;
protected editToolbar: L.TB.BottomToolbarControl;
protected dragModeButton: L.TB.ToolbarButton;
protected addMarkerButton: L.TB.ToolbarButton;
protected addRectangleButton: L.TB.ToolbarButton;
protected addPolygonButton: L.TB.ToolbarButton;
@ -127,10 +128,12 @@ export abstract class TbMap<S extends BaseMapSettings> {
private tooltipInstances: TooltipInstancesData[] = [];
private currentPopover: TbPopoverComponent;
private currentAddButton: L.TB.ToolbarButton;
private currentEditButton: L.TB.ToolbarButton;
private dragMode = true;
private get isPlacingItem(): boolean {
return !!this.currentAddButton;
return !!this.currentEditButton;
}
protected constructor(protected ctx: WidgetContext,
@ -188,6 +191,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.map.zoomControl) {
this.map.zoomControl.setPosition(this.settings.controlsPosition);
}
this.dragMode = !this.settings.dragModeButton;
const setup = [this.doSetupControls()];
if (this.timeline && this.settings.tripTimeline.snapToRealLocation) {
setup.push(parseTbFunction<MapBooleanFunction>(this.getCtx().http, this.settings.tripTimeline.locationSnapFilter, ['data', 'dsData']).pipe(
@ -393,12 +397,24 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.map.pm.applyGlobalOptions();
}
const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled());
const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length;
const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled());
if (addSupportedDataLayers.length) {
if (showDragModeButton || addSupportedDataLayers.length) {
const drawToolbar = L.TB.toolbar({
position: this.settings.controlsPosition
}).addTo(this.map);
if (showDragModeButton) {
this.dragModeButton = drawToolbar.toolbarButton({
id: 'dragMode',
title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'),
iconClass: 'tb-drag-mode',
click: (e, button) => {
this.toggleDragMode(e, button);
}
});
}
this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers');
if (this.addMarkerDataLayers.length) {
this.addMarkerButton = drawToolbar.toolbarButton({
@ -448,6 +464,32 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
}
private toggleDragMode(e: MouseEvent, button: L.TB.ToolbarButton): void {
if (this.dragMode) {
this.disableDragMode();
} else {
this.dragMode = true;
this.latestDataLayers.forEach(dl => dl.dragModeUpdated());
this.updatePlaceItemState(button);
this.editToolbar.open([
{
id: 'cancel',
iconClass: 'tb-close',
title: this.ctx.translate.instant('action.cancel'),
showText: true,
click: this.disableDragMode
}
], false);
}
}
private disableDragMode = () => {
this.dragMode = false;
this.latestDataLayers.forEach(dl => dl.dragModeUpdated());
this.updatePlaceItemState();
this.editToolbar.close();
}
private placeMarker(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addMarkerDataLayers, (entity) => this.prepareDrawMode('Marker', {
placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint-with-entity', {entityName: entity.entity.entityDisplayName})
@ -479,6 +521,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbLatestMapDataLayer[],
prepareDrawMode: (entity: UnplacedMapDataItem) => void): void {
if (this.isPlacingItem) {
this.finishAdd();
return;
}
this.updatePlaceItemState(button);
@ -692,6 +735,10 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
private finishAdd = () => {
if (this.currentPopover) {
this.currentPopover.hide();
this.currentPopover = null;
}
this.map.off('pm:create');
this.map.pm.disableDraw();
this.latestDataLayers.forEach(dl => dl.enableEditMode());
@ -706,15 +753,15 @@ export abstract class TbMap<S extends BaseMapSettings> {
L.DomUtil.addClass(this.map.pm.Draw[shape]._hintMarker.getTooltip()._container, 'tb-place-item-label');
}
private updatePlaceItemState(addButton?: L.TB.ToolbarButton, disabled = false): void {
if (addButton) {
private updatePlaceItemState(editButton?: L.TB.ToolbarButton, disabled = false): void {
if (editButton) {
this.deselectItem(false, true);
addButton.setActive(true);
} else if (this.currentAddButton) {
this.currentAddButton.setActive(false);
editButton.setActive(true);
} else if (this.currentEditButton) {
this.currentEditButton.setActive(false);
}
this.currentAddButton = addButton;
this.updateAddButtonsStates(disabled);
this.currentEditButton = editButton;
this.updateEditButtonsStates(disabled);
}
private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) {
@ -784,7 +831,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.updateTripsAppearance();
this.updateTripsAnchors();
this.updateBounds();
this.updateAddButtonsStates();
this.updateEditButtonsStates();
}
private updateTrips(subscription: IWidgetSubscription) {
@ -886,22 +933,28 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
}
private updateAddButtonsStates(disabled = false) {
if (this.currentAddButton || disabled) {
if (this.addMarkerButton && this.addMarkerButton !== this.currentAddButton) {
private updateEditButtonsStates(disabled = false) {
if (this.currentEditButton || disabled) {
if (this.dragModeButton && this.dragModeButton !== this.currentEditButton) {
this.dragModeButton.setDisabled(true);
}
if (this.addMarkerButton && this.addMarkerButton !== this.currentEditButton) {
this.addMarkerButton.setDisabled(true);
}
if (this.addRectangleButton && this.addRectangleButton !== this.currentAddButton) {
if (this.addRectangleButton && this.addRectangleButton !== this.currentEditButton) {
this.addRectangleButton.setDisabled(true);
}
if (this.addPolygonButton && this.addPolygonButton !== this.currentAddButton) {
if (this.addPolygonButton && this.addPolygonButton !== this.currentEditButton) {
this.addPolygonButton.setDisabled(true);
}
if (this.addCircleButton && this.addCircleButton !== this.currentAddButton) {
if (this.addCircleButton && this.addCircleButton !== this.currentEditButton) {
this.addCircleButton.setDisabled(true);
}
this.customActionsToolbar.setDisabled(true);
this.customActionsToolbar?.setDisabled(true);
} else {
if (this.dragModeButton) {
this.dragModeButton.setDisabled(false);
}
if (this.addMarkerButton) {
this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
@ -914,7 +967,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.addCircleButton) {
this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
this.customActionsToolbar.setDisabled(false);
this.customActionsToolbar?.setDisabled(false);
}
}
@ -979,7 +1032,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
public enabledDataLayersUpdated() {
this.updateAddButtonsStates();
this.updateEditButtonsStates();
this.updateTripsAnchors();
}
@ -1027,6 +1080,14 @@ export abstract class TbMap<S extends BaseMapSettings> {
return this.editToolbar;
}
public useDragModeButton(): boolean {
return this.settings.dragModeButton;
}
public dragModeEnabled(): boolean {
return this.dragMode;
}
public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable<any> {
const attributeService = this.ctx.$injector.get(AttributeService);
const attributes: AttributeData[] = [];

View File

@ -660,6 +660,7 @@ export interface BaseMapSettings {
additionalDataSources: AdditionalMapDataSourceSettings[];
controlsPosition: MapControlsPosition;
zoomActions: MapZoomAction[];
dragModeButton: boolean;
fitMapBounds: boolean;
useDefaultCenterPosition: boolean;
defaultCenterPosition?: string;
@ -682,6 +683,7 @@ export const defaultBaseMapSettings: BaseMapSettings = {
additionalDataSources: [],
controlsPosition: MapControlsPosition.topleft,
zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons],
dragModeButton: false,
fitMapBounds: true,
useDefaultCenterPosition: false,
defaultCenterPosition: '0,0',

View File

@ -99,6 +99,11 @@
</mat-chip-option>
</mat-chip-listbox>
</div>
<div *ngIf="showDragButtonModeButtonSettings" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="dragModeButton">
{{ 'widgets.maps.control.switch-to-drag-mode-using-button' | translate }}
</mat-slide-toggle>
</div>
<tb-trip-timeline-settings *ngIf="trip"
formControlName="tripTimeline"></tb-trip-timeline-settings>
</div>

View File

@ -26,10 +26,13 @@ import {
Validators
} from '@angular/forms';
import {
DataLayerEditAction,
defaultImageMapSourceSettings,
ImageMapSourceSettings, imageMapSourceSettingsValidator,
ImageMapSourceSettings,
imageMapSourceSettingsValidator,
mapControlPositions,
mapControlsPositionTranslationMap,
MapDataLayerSettings,
MapDataLayerType,
MapSetting,
MapType,
@ -109,6 +112,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
dataLayerMode: MapDataLayerType = 'markers';
showDragButtonModeButtonSettings = false;
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private destroyRef: DestroyRef) {
@ -139,6 +144,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
additionalDataSources: [null, []],
controlsPosition: [null, []],
zoomActions: [null, []],
dragModeButton: [null, []],
fitMapBounds: [null, []],
useDefaultCenterPosition: [null, []],
defaultCenterPosition: [null, []],
@ -167,6 +173,14 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
).subscribe((mapType: MapType) => {
this.mapTypeChanged(mapType);
});
merge(this.mapSettingsFormGroup.get('markers').valueChanges,
this.mapSettingsFormGroup.get('polygons').valueChanges,
this.mapSettingsFormGroup.get('circles').valueChanges
).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateDragButtonModeSettings();
});
}
registerOnChange(fn: any): void {
@ -192,6 +206,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
value, {emitEvent: false}
);
this.updateValidators();
this.updateDragButtonModeSettings();
}
public validate(_c: UntypedFormControl) {
@ -237,6 +252,26 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
}
}
private updateDragButtonModeSettings() {
const markers: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('markers').value;
const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value;
let dragModeButtonSettingsEnabled = markers.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move));
if (!dragModeButtonSettingsEnabled) {
const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value;
dragModeButtonSettingsEnabled = polygons.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move));
}
if (!dragModeButtonSettingsEnabled) {
const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value;
dragModeButtonSettingsEnabled = circles.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move));
}
this.showDragButtonModeButtonSettings = dragModeButtonSettingsEnabled;
if (dragModeButtonSettingsEnabled) {
this.mapSettingsFormGroup.get('dragModeButton').enable({emitEvent: false});
} else {
this.mapSettingsFormGroup.get('dragModeButton').disable({emitEvent: false});
}
}
private updateModel() {
this.modelValue = this.mapSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);

View File

@ -7704,7 +7704,8 @@
"zoom-actions": "Zoom actions",
"zoom-scroll": "Scroll",
"zoom-double-click": "Double click",
"zoom-control-buttons": "Control buttons"
"zoom-control-buttons": "Control buttons",
"switch-to-drag-mode-using-button": "Switch to drag mode using button"
},
"timeline": {
"control-panel": "Timeline control panel",
@ -7839,6 +7840,7 @@
"action-remove": "Remove",
"edit-instruments": "Instruments",
"enable-snapping": "Enable snapping to other vertices for precision drawing",
"drag-drop-mode": "Drag-drop mode",
"trip": {
"no-trips": "No trips configured",
"add-trip": "Add trip",