From 30bf12b7f86c109559742a2b55cffa27f335ae2f Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Wed, 29 Oct 2025 17:36:56 +0100 Subject: [PATCH 01/13] Add appendMultiGeometryWKT() --- .../map/src/lib/helpers/feature.helper.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/projects/map/src/lib/helpers/feature.helper.ts b/projects/map/src/lib/helpers/feature.helper.ts index d67976510..cb5bf2168 100644 --- a/projects/map/src/lib/helpers/feature.helper.ts +++ b/projects/map/src/lib/helpers/feature.helper.ts @@ -2,7 +2,7 @@ import { FeatureModelType } from '../models/feature-model.type'; import { Feature } from 'ol'; import { GeoJSON, WKT } from 'ol/format'; import { FeatureModel, FeatureModelAttributes } from '@tailormap-viewer/api'; -import { Circle, Geometry, Point } from 'ol/geom'; +import { Circle, Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon } from 'ol/geom'; import { fromCircle, fromExtent } from 'ol/geom/Polygon'; import { MapSizeHelper } from '../helpers/map-size.helper'; import { MapUnitEnum } from '../models/map-unit.enum'; @@ -209,4 +209,47 @@ export class FeatureHelper { const circle = new Circle(point.getCoordinates(), radius); return FeatureHelper.getWKT(circle); } + + public static appendMultiGeometryWKT(currentWkt: string | null, newGeometryWkt: string) { + const newGeom = FeatureHelper.fromWKT(newGeometryWkt); + if (!currentWkt) { + return newGeom; + } + const currentMultiGeom = this.ensureMultiGeometry(FeatureHelper.fromWKT(currentWkt)); + + if (currentMultiGeom instanceof MultiPoint && newGeom instanceof Point) { + currentMultiGeom.appendPoint(newGeom); + } else if (currentMultiGeom instanceof MultiLineString && newGeom instanceof LineString) { + currentMultiGeom.appendLineString(newGeom); + } else if (currentMultiGeom instanceof MultiPolygon && newGeom instanceof Polygon) { + currentMultiGeom.appendPolygon(newGeom); + } else { + // In case of incompatible geometry types, just return the new geometry + // TODO, maybe create a GeometryCollection? + return newGeom; + } + return currentMultiGeom; + } + + private static ensureMultiGeometry(geometry: Geometry) { + if (geometry instanceof MultiPoint || geometry instanceof MultiLineString || geometry instanceof MultiPolygon) { + return geometry; + } + if (geometry instanceof Point) { + const multiPoint = new MultiPoint([]); + multiPoint.appendPoint(geometry); + return multiPoint; + } + if (geometry instanceof LineString) { + const multiLineString = new MultiLineString([]); + multiLineString.appendLineString(geometry); + return multiLineString; + } + if (geometry instanceof Polygon) { + const multiPolygon = new MultiPolygon([]); + multiPolygon.appendPolygon(geometry); + return multiPolygon; + } + throw new Error('Unsupported geometry type'); + } } From 394dbef4d2a3315c338ce1055840d17cff03bd8e Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Wed, 29 Oct 2025 17:37:15 +0100 Subject: [PATCH 02/13] Add getFeaturesForLayer$() --- .../feature-info/feature-info.service.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/projects/core/src/lib/components/feature-info/feature-info.service.ts b/projects/core/src/lib/components/feature-info/feature-info.service.ts index fdd92be2c..3bcbcc4cc 100644 --- a/projects/core/src/lib/components/feature-info/feature-info.service.ts +++ b/projects/core/src/lib/components/feature-info/feature-info.service.ts @@ -86,6 +86,30 @@ export class FeatureInfoService { ); } + public getFeaturesForLayer$(coordinates: [ number, number ], selectedLayer: string | null, pointerType?: string): Observable { + return combineLatest([ + this.store$.select(selectVisibleLayersWithAttributes), + this.store$.select(selectVisibleWMSLayersWithoutAttributes), + this.store$.select(selectViewerId), + this.mapService.getMapViewDetails$(), + ]) + .pipe( + take(1), + switchMap(([ layers, wmsLayers, viewerId, mapViewDetails ]) => { + if (!viewerId) { + return of(null); + } + if (layers.some(l => l.id === selectedLayer)) { + return this.getFeatureInfoFromApi$(selectedLayer!, coordinates, viewerId, mapViewDetails, false, pointerType); + } else if (wmsLayers.some(l => l.id === selectedLayer)) { + return this.getWmsGetFeatureInfo$(selectedLayer!, coordinates); + } else { + return of(null); + } + }), + ); + } + public getEditableFeatures$(coordinates: [ number, number ], selectedLayer?: string | null, pointerType?: string): Observable { return combineLatest([ this.store$.select(selectEditableLayers), From de12795ccd75688bcb262b1096b6b460a50e4af0 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Wed, 29 Oct 2025 17:39:33 +0100 Subject: [PATCH 03/13] WIP update edit component to enable copying other layers' geometry --- .../core/assets/locale/messages.core.nl.xlf | 4 +- .../edit/edit-dialog/edit-dialog.component.ts | 6 +- .../components/edit/edit/edit.component.css | 12 ++- .../components/edit/edit/edit.component.html | 76 ++++++++++-------- .../components/edit/edit/edit.component.ts | 48 ++++++++++-- .../edit/services/edit-map-tool.service.ts | 51 +++++++++--- .../lib/components/edit/state/edit.actions.ts | 22 +++++- .../lib/components/edit/state/edit.effects.ts | 37 ++++++--- .../lib/components/edit/state/edit.reducer.ts | 77 ++++++++++++++++++- .../components/edit/state/edit.selectors.ts | 16 +++- .../lib/components/edit/state/edit.state.ts | 6 ++ 11 files changed, 282 insertions(+), 73 deletions(-) diff --git a/projects/core/assets/locale/messages.core.nl.xlf b/projects/core/assets/locale/messages.core.nl.xlf index de6a022e0..baff40e4d 100644 --- a/projects/core/assets/locale/messages.core.nl.xlf +++ b/projects/core/assets/locale/messages.core.nl.xlf @@ -472,11 +472,11 @@ Select which feature to edit - Selecteer een object om te bewerken + Object om te bewerken Select layer to edit - Selecteer een laag om te bewerken + Laag om te bewerken True diff --git a/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.ts b/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.ts index 636a0bbec..183f62c3e 100644 --- a/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.ts +++ b/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.ts @@ -96,12 +96,10 @@ export class EditDialogComponent { this.resetChanges(); }); - this.editMapToolService.editedGeometry$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(geometry => { + this.editMapToolService.allEditGeometry$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(geometry => { this.geometryChanged(geometry, false); }); - this.editMapToolService.createdGeometry$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(geometry => { - this.geometryChanged(geometry, true); - }); + ComponentConfigHelper.useInitialConfigForComponent( this.store$, BaseComponentTypeEnum.EDIT, diff --git a/projects/core/src/lib/components/edit/edit/edit.component.css b/projects/core/src/lib/components/edit/edit/edit.component.css index ef5fc476d..7817a582e 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.css +++ b/projects/core/src/lib/components/edit/edit/edit.component.css @@ -18,12 +18,18 @@ border-radius: 3px; } -.map-control-button--add-feature { +.edit-wrapper { + flex: 1; +} + +.edit-controls button { left: 3px; } -.edit-wrapper { - flex: 1; +.edit-controls { + display: flex; + flex-direction: column; + height: 100%; } .edit-container mat-form-field { diff --git a/projects/core/src/lib/components/edit/edit/edit.component.html b/projects/core/src/lib/components/edit/edit/edit.component.html index ee6f5cbe2..265ccf83f 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.html +++ b/projects/core/src/lib/components/edit/edit/edit.component.html @@ -28,38 +28,54 @@ } - } - @if (!isLine() && !isPoint()) { - - - @if (isPolygon()) { - - - +
+ @if (!isLine() && !isPoint()) { + + + @if (isPolygon()) { + + + + } @else { + + + + + + } + } @else { - - - - - + + } + @if (layersToCreateNewFeaturesFrom().length > 0) { + + + @for (layer of layersToCreateNewFeaturesFrom(); track layer) { + + } + } - - } @else { - +
} } diff --git a/projects/core/src/lib/components/edit/edit/edit.component.ts b/projects/core/src/lib/components/edit/edit/edit.component.ts index 41140898e..411810486 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.ts +++ b/projects/core/src/lib/components/edit/edit/edit.component.ts @@ -1,18 +1,20 @@ -import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject, signal } from '@angular/core'; import { selectEditActive, selectSelectedEditLayer } from '../state/edit.selectors'; import { Store } from '@ngrx/store'; import { combineLatest, of, take } from 'rxjs'; -import { setEditActive, setEditCreateNewFeatureActive, setSelectedEditLayer } from '../state/edit.actions'; +import { + setEditActive, setEditCopyOtherLayerFeaturesActive, setEditCreateNewFeatureActive, setSelectedEditLayer, +} from '../state/edit.actions'; import { FormControl } from '@angular/forms'; -import { selectEditableLayers } from '../../../map/state/map.selectors'; +import { selectEditableLayers, selectOrderedVisibleLayersWithServices } from '../../../map/state/map.selectors'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { switchMap, withLatestFrom } from 'rxjs/operators'; import { hideFeatureInfoDialog } from '../../feature-info/state/feature-info.actions'; import { ApplicationLayerService } from '../../../map/services/application-layer.service'; -import { AttributeType, AuthenticatedUserService, GeometryType } from '@tailormap-viewer/api'; +import { AppLayerModel, AttributeType, AuthenticatedUserService, GeometryType } from '@tailormap-viewer/api'; import { activateTool } from '../../toolbar/state/toolbar.actions'; import { ToolbarComponentEnum } from '../../toolbar/models/toolbar-component.enum'; -import { DrawingType } from '@tailormap-viewer/map'; +import { DrawingType, MapService, ScaleHelper } from '@tailormap-viewer/map'; @Component({ selector: 'tm-edit', @@ -26,13 +28,15 @@ export class EditComponent implements OnInit { private destroyRef = inject(DestroyRef); private applicationLayerService = inject(ApplicationLayerService); private authenticatedUserService = inject(AuthenticatedUserService); - + private mapService = inject(MapService); public active$ = this.store$.select(selectEditActive); public editableLayers$ = this.store$.select(selectEditableLayers); public layer = new FormControl(); public editGeometryType: GeometryType | null = null; + public layersToCreateNewFeaturesFrom = signal([]); + private defaultTooltip = $localize `:@@core.edit.edit-feature-tooltip:Edit feature`; private notLoggedInTooltip = $localize `:@@core.edit.require-login-tooltip:You must be logged in to edit.`; private noLayersTooltip = $localize `:@@core.edit.no-editable-layers-tooltip:There are no editable layers. Enable a layer to start editing.`; @@ -72,6 +76,17 @@ export class EditComponent implements OnInit { this.toggle(true); } }); + + combineLatest([ this.store$.select(selectSelectedEditLayer), + this.store$.select(selectOrderedVisibleLayersWithServices), + this.mapService.getMapViewDetails$() ]).pipe( + takeUntilDestroyed(this.destroyRef), + ).subscribe(([ selectedEditLayerId, visibleLayers, mapViewDetails ]) => { + const layers = selectedEditLayerId == null ? [] : visibleLayers.filter(layer => + layer.id !== selectedEditLayerId + && ScaleHelper.isInScale(mapViewDetails.scale, layer.minScale, layer.maxScale)); + this.layersToCreateNewFeaturesFrom.set(layers); + }); } public isLine() { @@ -152,4 +167,25 @@ export class EditComponent implements OnInit { } } + public createFeatureFromLayer(id: string) { + // get layer attribute details for edit form + this.applicationLayerService.getLayerDetails$(this.layer.value) + .pipe(take(1)) + .subscribe(layerDetails => { + // show edit dialog + this.store$.dispatch(setEditCopyOtherLayerFeaturesActive({ + active: true, + layerId: id, + columnMetadata: layerDetails.details.attributes.map(attribute => { + return { + layerId: layerDetails.details.id, + name: attribute.name, + type: attribute.type as unknown as AttributeType, + alias: attribute.editAlias, + }; + }, + ), + })); + }); + } } diff --git a/projects/core/src/lib/components/edit/services/edit-map-tool.service.ts b/projects/core/src/lib/components/edit/services/edit-map-tool.service.ts index 5ba66e717..04e83c4a0 100644 --- a/projects/core/src/lib/components/edit/services/edit-map-tool.service.ts +++ b/projects/core/src/lib/components/edit/services/edit-map-tool.service.ts @@ -2,7 +2,7 @@ import { DestroyRef, Injectable, OnDestroy, inject } from '@angular/core'; import { DrawingToolConfigModel, DrawingToolEvent, - DrawingToolModel, + DrawingToolModel, FeatureHelper, MapClickToolConfigModel, MapClickToolModel, MapService, @@ -12,12 +12,14 @@ import { ToolTypeEnum, } from '@tailormap-viewer/map'; import { Store } from '@ngrx/store'; -import { selectEditStatus, selectEditError$, selectNewFeatureGeometryType, selectSelectedEditFeature } from '../state/edit.selectors'; +import { + selectEditStatus, selectEditError$, selectNewFeatureGeometryType, selectSelectedEditFeature, selectCopiedFeatures, +} from '../state/edit.selectors'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { BehaviorSubject, combineLatest, concatMap, forkJoin, map, Observable, of, switchMap, take, tap } from 'rxjs'; +import { BehaviorSubject, combineLatest, concatMap, forkJoin, map, merge, Observable, of, switchMap, take, tap } from 'rxjs'; import { deregisterTool, registerTool } from '../../toolbar/state/toolbar.actions'; import { ToolbarComponentEnum } from '../../toolbar/models/toolbar-component.enum'; -import { loadEditFeatures } from '../state/edit.actions'; +import { loadCopyFeatures, loadEditFeatures } from '../state/edit.actions'; import { SnackBarMessageComponent, SnackBarMessageOptionsModel } from '@tailormap-viewer/shared'; import { MatSnackBar } from '@angular/material/snack-bar'; import { withLatestFrom } from 'rxjs/operators'; @@ -34,7 +36,6 @@ export class EditMapToolService implements OnDestroy { private applicationLayerService = inject(ApplicationLayerService); private destroyRef = inject(DestroyRef); - private static DEFAULT_ERROR_MESSAGE = $localize `:@@core.edit.error-getting-features:Something went wrong while getting editable features, please try again`; private static DEFAULT_NO_FEATURES_FOUND_MESSAGE = $localize `:@@core.edit.no-features-found:No editable features found`; @@ -43,10 +44,10 @@ export class EditMapToolService implements OnDestroy { private createGeometryToolId = ''; private createdGeometrySubject = new BehaviorSubject(null); - public createdGeometry$ = this.createdGeometrySubject.asObservable(); - private editedGeometrySubject = new BehaviorSubject(null); - public editedGeometry$ = this.editedGeometrySubject.asObservable(); + private readonly copiedGeometry$; + + public allEditGeometry$; constructor() { @@ -88,6 +89,27 @@ export class EditMapToolService implements OnDestroy { this.handleEditGeometryModified(modifiedGeometry); }); + this.copiedGeometry$ = this.store$.select(selectCopiedFeatures).pipe( + map(copiedFeatures => { + if (copiedFeatures.length === 0) { + return null; + } + return copiedFeatures + .map(feature => feature.geometry!) + .reduce((previousValue, currentValue) => + FeatureHelper.getWKT(FeatureHelper.appendMultiGeometryWKT(previousValue, currentValue))); + })); + + this.allEditGeometry$ = merge( + this.editedGeometrySubject.asObservable(), + this.createdGeometrySubject.asObservable(), + this.copiedGeometry$, + ); + + this.mapService.renderFeatures$("copied-features-geometry", this.copiedGeometry$, style) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(); + this.mapService.renderFeatures$("create-feature-geometry", this.createdGeometrySubject.asObservable(), style) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); @@ -125,11 +147,11 @@ export class EditMapToolService implements OnDestroy { toolManager.disableTool(this.editMapClickToolId, true); toolManager.disableTool(this.editGeometryToolId, true); toolManager.disableTool(this.createGeometryToolId, false); - } else if(editStatus === 'active') { + } else if(editStatus === 'active' || editStatus === 'copy_features') { toolManager.disableTool(this.editGeometryToolId, true); toolManager.disableTool(this.createGeometryToolId, true); toolManager.enableTool(this.editMapClickToolId, true); - } else if(editStatus === 'edit_feature') { + } else if(editStatus === 'edit_feature') { toolManager.disableTool(this.createGeometryToolId, true); toolManager.enableTool(this.editMapClickToolId, true); toolManager.enableTool(this.editGeometryToolId, false, { geometry: editGeometry }); @@ -167,7 +189,14 @@ export class EditMapToolService implements OnDestroy { } private handleMapClick(evt: { mapCoordinates: [number, number]; mouseCoordinates: [number, number]; pointerType?: string }) { - this.store$.dispatch(loadEditFeatures({ coordinates: evt.mapCoordinates, pointerType: evt.pointerType })); + this.store$.select(selectEditStatus).pipe(take(1)) + .subscribe(status => { + if (status === 'active') { + this.store$.dispatch(loadEditFeatures({ coordinates: evt.mapCoordinates, mouseCoordinates: evt.mouseCoordinates, pointerType: evt.pointerType })); + } else if (status === 'copy_features') { + this.store$.dispatch(loadCopyFeatures({ coordinates: evt.mapCoordinates, mouseCoordinates: evt.mouseCoordinates, pointerType: evt.pointerType })); + } + }); this.store$.pipe(selectEditError$) .subscribe(error => { if (!error || error.error === 'none') { diff --git a/projects/core/src/lib/components/edit/state/edit.actions.ts b/projects/core/src/lib/components/edit/state/edit.actions.ts index ab3a94608..db79efdea 100644 --- a/projects/core/src/lib/components/edit/state/edit.actions.ts +++ b/projects/core/src/lib/components/edit/state/edit.actions.ts @@ -17,6 +17,11 @@ export const setEditCreateNewFeatureActive = createAction( props<{ active: boolean; geometryType: DrawingType; columnMetadata: FeatureInfoColumnMetadataModel[] }>(), ); +export const setEditCopyOtherLayerFeaturesActive = createAction( + `${editActionsPrefix} Set Copy Other Layer Features Active`, + props<{ active: boolean; layerId: string; columnMetadata: FeatureInfoColumnMetadataModel[] }>(), +); + export const setSelectedEditLayer = createAction( `${editActionsPrefix} Set Selected Layer`, props<{ layer: string | null }>(), @@ -24,7 +29,12 @@ export const setSelectedEditLayer = createAction( export const loadEditFeatures = createAction( `${editActionsPrefix} Load Edit Features`, - props<{ coordinates: [number, number]; pointerType?: string }>(), + props<{ coordinates: [number, number]; mouseCoordinates: [number, number]; pointerType?: string }>(), +); + +export const loadCopyFeatures = createAction( + `${editActionsPrefix} Load Copy Features`, + props<{ coordinates: [number, number]; mouseCoordinates: [number, number]; pointerType?: string }>(), ); export const loadEditFeaturesSuccess = createAction( @@ -37,6 +47,16 @@ export const loadEditFeaturesFailed = createAction( props<{ errorMessage?: string }>(), ); +export const loadCopyFeaturesSuccess = createAction( + `${editActionsPrefix} Load Copy Features Success`, + props<{ featureInfo: FeatureInfoResponseModel[] }>(), +); + +export const loadCopyFeaturesFailed = createAction( + `${editActionsPrefix} Load Copy Features Failed`, + props<{ errorMessage?: string }>(), +); + export const setSelectedEditFeature = createAction( `${editActionsPrefix} Set Selected Edit Feature`, props<{ fid: string | null }>(), diff --git a/projects/core/src/lib/components/edit/state/edit.effects.ts b/projects/core/src/lib/components/edit/state/edit.effects.ts index 32289d581..06cdbe9d2 100644 --- a/projects/core/src/lib/components/edit/state/edit.effects.ts +++ b/projects/core/src/lib/components/edit/state/edit.effects.ts @@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import * as EditActions from './edit.actions'; import { filter, map, switchMap } from 'rxjs'; -import { FeatureInfoService } from '../../feature-info/feature-info.service'; +import { FeatureInfoService } from '../../feature-info'; import { withLatestFrom } from 'rxjs/operators'; import { selectEditActive, selectSelectedEditLayer } from './edit.selectors'; import { Store } from '@ngrx/store'; @@ -16,25 +16,40 @@ export class EditEffects { private store$ = inject(Store); private featureInfoService = inject(FeatureInfoService); - public loadEditFeatures$ = createEffect(() => { return this.actions$.pipe( ofType(EditActions.loadEditFeatures), withLatestFrom(this.store$.select(selectSelectedEditLayer)), switchMap(([ action, editLayer ]) => { - return this.featureInfoService.getEditableFeatures$(action.coordinates, editLayer, action.pointerType) - .pipe( - map(result => { - if (!result) { - return EditActions.loadEditFeaturesFailed({}); - } - return EditActions.loadEditFeaturesSuccess({ featureInfo: result }); - }), - ); + return this.featureInfoService.getEditableFeatures$(action.coordinates, editLayer, action.pointerType).pipe( + map(result => { + if (!result) { + return EditActions.loadEditFeaturesFailed({}); + } + return EditActions.loadEditFeaturesSuccess({ featureInfo: result }); + }), + ); }), ); }); + public loadCopyFeatures$ = createEffect(() => { + return this.actions$.pipe( + ofType(EditActions.loadCopyFeatures), + withLatestFrom(this.store$.select(selectSelectedEditLayer)), + switchMap(([ action, editLayer ]) => { + return this.featureInfoService.getFeaturesForLayer$(action.coordinates, editLayer, action.pointerType).pipe( + map(result => { + if (!result) { + return EditActions.loadCopyFeaturesFailed({}); + } + return EditActions.loadCopyFeaturesSuccess({ featureInfo: [result] }); + }), + ); + }), + ); + }); + public activeTool$ = createEffect(() => { return this.actions$.pipe( ofType(activateTool), diff --git a/projects/core/src/lib/components/edit/state/edit.reducer.ts b/projects/core/src/lib/components/edit/state/edit.reducer.ts index a296b511c..a4d77f622 100644 --- a/projects/core/src/lib/components/edit/state/edit.reducer.ts +++ b/projects/core/src/lib/components/edit/state/edit.reducer.ts @@ -37,7 +37,15 @@ const onLoadFeatureInfo = ( mapCoordinates: payload.coordinates, loadStatus: LoadingStateEnum.LOADING, features: [], - dialogVisible: false, +}); + +const onLoadCopyFeatureInfo = ( + state: EditState, + payload: ReturnType, +): EditState => ({ + ...state, + mapCoordinates: payload.coordinates, + loadStatus: LoadingStateEnum.LOADING, }); const onLoadEditFeaturesSuccess = ( @@ -57,6 +65,56 @@ const onLoadEditFeaturesSuccess = ( }; }; +const onLoadCopyFeaturesSuccess = ( + state: EditState, + payload: ReturnType, +): EditState => { + state = { + ...state, + loadStatus: LoadingStateEnum.LOADED, + }; + const geometry = payload.featureInfo[0]?.features[0]?.geometry; + if (!geometry) { + return state; + } + + // Deselect a feature by checking if the geometry WKT is already in the copiedFeatures array (can't search by fid) + const sameGeometryIndex = state.copiedFeatures.findIndex(f => f.geometry === geometry); + const copiedFeatures = sameGeometryIndex !== -1 + ? state.copiedFeatures.filter((_, idx) => idx !== sameGeometryIndex) + : [ ...state.copiedFeatures, payload.featureInfo[0].features[0] ]; + + return { + ...state, + features: [{ + layerId: state.columnMetadata[0].layerId, + __fid: 'new', + attributes: {}, + }], + copiedFeatures, + }; +}; + +const onSetCopyOtherLayerFeaturesActive = ( + state: EditState, + payload: ReturnType, +): EditState => ({ + ...state, + isCopyOtherLayerFeaturesActive: payload.active, + isCreateNewFeatureActive: false, + selectedCopyLayer: payload.layerId, + dialogVisible: payload.active, + selectedFeature: 'new', + features: [{ + layerId: payload.columnMetadata[0].layerId, + __fid: 'new', + attributes: {}, + }], + columnMetadata: payload.columnMetadata, + loadStatus: LoadingStateEnum.LOADED, + dialogCollapsed: false, +}); + const onSetCreateNewFeatureActive = ( state: EditState, payload: ReturnType, @@ -98,6 +156,15 @@ const onLoadEditFeaturesFailed = ( loadStatus: LoadingStateEnum.FAILED, }); +const onLoadCopyFeaturesFailed = ( + state: EditState, + payload: ReturnType, +): EditState => ({ + ...state, + errorMessage: payload.errorMessage, + loadStatus: LoadingStateEnum.FAILED, +}); + const onSetSelectedEditFeature = ( state: EditState, payload: ReturnType, @@ -167,21 +234,27 @@ const onEditNewlyCreatedFeature = ( ): EditState => { return { ...state, + isCopyOtherLayerFeaturesActive: false, + selectedCopyLayer: null, + copiedFeatures: [], features: [payload.feature], selectedFeature: payload.feature.__fid, isCreateNewFeatureActive: false, }; - }; const editReducerImpl = createReducer( initialEditState, on(EditActions.setEditActive, onSetIsActive), + on(EditActions.setEditCopyOtherLayerFeaturesActive, onSetCopyOtherLayerFeaturesActive), on(EditActions.setEditCreateNewFeatureActive, onSetCreateNewFeatureActive), on(EditActions.setSelectedEditLayer, onSetSelectedLayer), on(EditActions.loadEditFeatures, onLoadFeatureInfo), + on(EditActions.loadCopyFeatures, onLoadCopyFeatureInfo), on(EditActions.loadEditFeaturesSuccess, onLoadEditFeaturesSuccess), on(EditActions.loadEditFeaturesFailed, onLoadEditFeaturesFailed), + on(EditActions.loadCopyFeaturesSuccess, onLoadCopyFeaturesSuccess), + on(EditActions.loadCopyFeaturesFailed, onLoadCopyFeaturesFailed), on(EditActions.setSelectedEditFeature, onSetSelectedEditFeature), on(EditActions.setLoadedEditFeature, onSetLoadedEditFeature), on(EditActions.showEditDialog, onShowEditDialog), diff --git a/projects/core/src/lib/components/edit/state/edit.selectors.ts b/projects/core/src/lib/components/edit/state/edit.selectors.ts index 31397bac6..b810c5215 100644 --- a/projects/core/src/lib/components/edit/state/edit.selectors.ts +++ b/projects/core/src/lib/components/edit/state/edit.selectors.ts @@ -10,9 +10,15 @@ const selectEditState = createFeatureSelector(editStateKey); export const selectEditActive = createSelector(selectEditState, state => state.isActive); export const selectEditSelectedFeature = createSelector(selectEditState, state => state.selectedFeature); -export const selectEditCreateNewFeatureActive = createSelector(selectEditState, state => state.isCreateNewFeatureActive); +export const selectEditCopyOtherLayerFeaturesActive = createSelector(selectEditState, state => state.isCopyOtherLayerFeaturesActive); +export const selectEditCreateNewFeatureActive = createSelector(selectEditState, + state => state.isCreateNewFeatureActive || state.isCopyOtherLayerFeaturesActive); export const selectNewFeatureGeometryType = createSelector(selectEditState, state => state.newGeometryType); -export const selectSelectedEditLayer = createSelector(selectEditState, state => state.selectedLayer); + +export const selectSelectedEditLayer = createSelector(selectEditState, + state => state.isCopyOtherLayerFeaturesActive ? state.selectedCopyLayer : state.selectedLayer); +export const selectCopiedFeatures = createSelector(selectEditState, state => state.copiedFeatures); + export const selectEditMapCoordinates = createSelector(selectEditState, state => state.mapCoordinates); export const selectEditLoadStatus = createSelector(selectEditState, state => state.loadStatus); export const selectEditErrorMessage = createSelector(selectEditState, state => state.errorMessage); @@ -34,12 +40,16 @@ export const selectEditActiveWithSelectedLayer = createSelector( export const selectEditStatus = createSelector( selectEditActiveWithSelectedLayer, + selectEditCopyOtherLayerFeaturesActive, selectEditSelectedFeature, selectEditCreateNewFeatureActive, - (editWithLayerActive, editSelectedFeatureActive, editCreateNewFeatureActive) => { + (editWithLayerActive, editCopyOtherLayerFeaturesActive, editSelectedFeatureActive, editCreateNewFeatureActive) => { if (!editWithLayerActive) { return 'inactive'; } + if (editCopyOtherLayerFeaturesActive) { + return 'copy_features'; + } if (editCreateNewFeatureActive) { return 'create_feature'; } diff --git a/projects/core/src/lib/components/edit/state/edit.state.ts b/projects/core/src/lib/components/edit/state/edit.state.ts index 95a32d2fd..dd5855eca 100644 --- a/projects/core/src/lib/components/edit/state/edit.state.ts +++ b/projects/core/src/lib/components/edit/state/edit.state.ts @@ -7,9 +7,12 @@ export const editStateKey = 'edit'; export interface EditState { isActive: boolean; + isCopyOtherLayerFeaturesActive: boolean; isCreateNewFeatureActive: boolean; newGeometryType: DrawingType | null; selectedLayer: string | null; + selectedCopyLayer: string | null; + copiedFeatures: FeatureInfoFeatureModel[]; mapCoordinates?: [number, number]; dialogVisible: boolean; dialogCollapsed: boolean; @@ -23,9 +26,12 @@ export interface EditState { export const initialEditState: EditState = { isActive: false, + isCopyOtherLayerFeaturesActive: false, isCreateNewFeatureActive: false, newGeometryType: null, selectedLayer: null, + selectedCopyLayer: null, + copiedFeatures: [], dialogVisible: false, dialogCollapsed: false, loadStatus: LoadingStateEnum.INITIAL, From 9ad22eaf2c213750ce599fa7145db423e32205c2 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Thu, 30 Oct 2025 11:46:54 +0100 Subject: [PATCH 04/13] Disable copy features when hiding edit dialog --- projects/core/src/lib/components/edit/state/edit.reducer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/core/src/lib/components/edit/state/edit.reducer.ts b/projects/core/src/lib/components/edit/state/edit.reducer.ts index a4d77f622..a954466dd 100644 --- a/projects/core/src/lib/components/edit/state/edit.reducer.ts +++ b/projects/core/src/lib/components/edit/state/edit.reducer.ts @@ -202,6 +202,9 @@ const onHideEditDialog = (state: EditState): EditState => ({ dialogCollapsed: false, selectedFeature: null, isCreateNewFeatureActive: false, + isCopyOtherLayerFeaturesActive: false, + copiedFeatures: [], + selectedCopyLayer: null, }); const onExpandCollapseEditDialog = (state: EditState): EditState => ({ From cf1faa990c2f649c9b1b7084fd09c782807daa14 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Thu, 30 Oct 2025 15:47:29 +0100 Subject: [PATCH 05/13] Show active state for new geometry/copy features, show number of copied features in badge, show new/edit title in edit dialog, small fixes --- .../core/assets/locale/messages.core.de.xlf | 4 ---- .../core/assets/locale/messages.core.en.xlf | 3 --- .../core/assets/locale/messages.core.nl.xlf | 4 ---- .../components/edit/edit-component.module.ts | 3 ++- .../edit-dialog/edit-dialog.component.html | 3 +-- .../edit/edit-dialog/edit-dialog.component.ts | 9 +++++++-- .../components/edit/edit/edit.component.css | 4 ++-- .../components/edit/edit/edit.component.html | 19 ++++++++++++------- .../components/edit/edit/edit.component.ts | 13 ++++++++++--- .../lib/components/edit/state/edit.effects.ts | 8 ++++---- .../lib/components/edit/state/edit.reducer.ts | 14 ++++++-------- .../components/edit/state/edit.selectors.ts | 9 +++++---- .../lib/components/edit/state/edit.state.ts | 10 +++++++--- .../components/dialog/dialog.component.ts | 2 +- 14 files changed, 57 insertions(+), 48 deletions(-) diff --git a/projects/core/assets/locale/messages.core.de.xlf b/projects/core/assets/locale/messages.core.de.xlf index 4a0c35998..d1fcece99 100644 --- a/projects/core/assets/locale/messages.core.de.xlf +++ b/projects/core/assets/locale/messages.core.de.xlf @@ -427,10 +427,6 @@ Löschen des Features fehlgeschlagen
- Edit - Bearbeiten - - Edit feature Objekt bearbeiten diff --git a/projects/core/assets/locale/messages.core.en.xlf b/projects/core/assets/locale/messages.core.en.xlf index 03d703323..2c9599872 100644 --- a/projects/core/assets/locale/messages.core.en.xlf +++ b/projects/core/assets/locale/messages.core.en.xlf @@ -321,9 +321,6 @@ Delete feature failed - Edit - - Edit feature diff --git a/projects/core/assets/locale/messages.core.nl.xlf b/projects/core/assets/locale/messages.core.nl.xlf index baff40e4d..596a0c3a9 100644 --- a/projects/core/assets/locale/messages.core.nl.xlf +++ b/projects/core/assets/locale/messages.core.nl.xlf @@ -427,10 +427,6 @@ Object verwijderen mislukt - Edit - Bewerken - - Edit feature Object bewerken diff --git a/projects/core/src/lib/components/edit/edit-component.module.ts b/projects/core/src/lib/components/edit/edit-component.module.ts index dc24ccca5..60fb2ffcf 100644 --- a/projects/core/src/lib/components/edit/edit-component.module.ts +++ b/projects/core/src/lib/components/edit/edit-component.module.ts @@ -13,7 +13,7 @@ import { ApplicationMapModule } from '../../map/application-map.module'; import { EditSelectFeatureComponent } from './edit-select-feature/edit-select-feature.component'; import { SelectFieldComponent } from './fields/select-field/select-field.component'; import { CoreSharedModule } from '../../shared'; - +import { MatBadge } from '@angular/material/badge'; @NgModule({ declarations: [ @@ -30,6 +30,7 @@ import { CoreSharedModule } from '../../shared'; EffectsModule.forFeature([EditEffects]), ApplicationMapModule, CoreSharedModule, + MatBadge, ], exports: [ EditComponent, diff --git a/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.html b/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.html index 953091dad..fe1cf0b89 100644 --- a/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.html +++ b/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.html @@ -7,8 +7,7 @@ [allowCollapse]="true" [collapsed]="dialogCollapsed$ | async" (expandCollapseDialog)="expandCollapseDialog()" - dialogTitle="Edit" - i18n-dialogTitle="@@core.edit.edit"> + [dialogTitle]="dialogTitle$ | async"> @let currentFeature = currentFeature$ | async; @let layerDetails = layerDetails$ | async; @if (currentFeature && layerDetails) { diff --git a/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.ts b/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.ts index 183f62c3e..40c472a72 100644 --- a/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.ts +++ b/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inje import { Store } from '@ngrx/store'; import { ConfirmDialogService, CssHelper } from '@tailormap-viewer/shared'; import { - selectEditCreateNewFeatureActive, selectEditDialogCollapsed, selectEditDialogVisible, selectEditFeatures, selectEditMapCoordinates, + selectEditCreateNewOrCopyFeatureActive, selectEditDialogCollapsed, selectEditDialogVisible, selectEditFeatures, selectEditMapCoordinates, selectEditOpenedFromFeatureInfo, selectLoadingEditFeatures, selectSelectedEditFeature, } from '../state/edit.selectors'; import { combineLatest, concatMap, filter, map, of, switchMap, take } from 'rxjs'; @@ -44,6 +44,7 @@ export class EditDialogComponent { public dialogCollapsed$; public isCreateFeature$; public currentFeature$; + public dialogTitle$; public layerDetails$; public selectableFeature$; @@ -70,7 +71,11 @@ export class EditDialogComponent { this.loadingEditFeatureInfo$ = this.store$.select(selectLoadingEditFeatures); this.editCoordinates$ = this.store$.select(selectEditMapCoordinates); this.currentFeature$ = this.store$.select(selectSelectedEditFeature); - this.isCreateFeature$ = this.store$.select(selectEditCreateNewFeatureActive); + this.dialogTitle$ = this.currentFeature$.pipe( + map(feature => feature?.feature.__fid !== 'new' + ? $localize `:@@core.edit.edit:Edit feature` + : $localize `:@@core.edit.add-new-feature:Add new feature`)); + this.isCreateFeature$ = this.store$.select(selectEditCreateNewOrCopyFeatureActive); this.selectableFeature$ = combineLatest([ this.store$.select(selectEditFeatures), this.store$.select(selectSelectedEditFeature), diff --git a/projects/core/src/lib/components/edit/edit/edit.component.css b/projects/core/src/lib/components/edit/edit/edit.component.css index 7817a582e..7be6b319e 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.css +++ b/projects/core/src/lib/components/edit/edit/edit.component.css @@ -8,8 +8,8 @@ } .edit-container--is-active { - padding: 5px 6px 5px 0; - width: 300px; + padding: 5px 11px 5px 0; + width: 305px; height: 70px; max-width: var(--max-control-width); } diff --git a/projects/core/src/lib/components/edit/edit/edit.component.html b/projects/core/src/lib/components/edit/edit/edit.component.html index 265ccf83f..b155f5bd0 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.html +++ b/projects/core/src/lib/components/edit/edit/edit.component.html @@ -11,8 +11,7 @@ @if (active$ | async) { -
+
@if (editableLayers.length > 0) { @@ -35,7 +34,8 @@ tmTooltip="Add new feature" i18n-tmTooltip="@@core.edit.add-new-feature" class="map-control-button map-control-button--add-feature" - mat-flat-button> + mat-flat-button + [class.toggle-button--active]="createNewFeatureActive$ | async"> @@ -57,16 +57,21 @@ i18n-tmTooltip="@@core.edit.add-new-feature" class="map-control-button map-control-button--add-feature" (click)="createFeatureIfSingleGeometryType()" - mat-flat-button> + mat-flat-button + [class.toggle-button--active]="createNewFeatureActive$ | async"> } @if (layersToCreateNewFeaturesFrom().length > 0) { diff --git a/projects/core/src/lib/components/edit/edit/edit.component.ts b/projects/core/src/lib/components/edit/edit/edit.component.ts index 411810486..69412ee74 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.ts +++ b/projects/core/src/lib/components/edit/edit/edit.component.ts @@ -1,7 +1,11 @@ import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject, signal } from '@angular/core'; -import { selectEditActive, selectSelectedEditLayer } from '../state/edit.selectors'; +import { + selectCopiedFeatures, + selectEditActive, selectEditCopyOtherLayerFeaturesActive, selectEditCreateNewFeatureActive, selectEditCreateNewOrCopyFeatureActive, + selectSelectedEditLayer, +} from '../state/edit.selectors'; import { Store } from '@ngrx/store'; -import { combineLatest, of, take } from 'rxjs'; +import { combineLatest, map, of, take } from 'rxjs'; import { setEditActive, setEditCopyOtherLayerFeaturesActive, setEditCreateNewFeatureActive, setSelectedEditLayer, } from '../state/edit.actions'; @@ -31,13 +35,16 @@ export class EditComponent implements OnInit { private mapService = inject(MapService); public active$ = this.store$.select(selectEditActive); + public createNewFeatureActive$ = this.store$.select(selectEditCreateNewFeatureActive); + public copyActive$ = this.store$.select(selectEditCopyOtherLayerFeaturesActive); + public copiedFeaturesCount$ = this.store$.select(selectCopiedFeatures).pipe(map(features => features.length)); public editableLayers$ = this.store$.select(selectEditableLayers); public layer = new FormControl(); public editGeometryType: GeometryType | null = null; public layersToCreateNewFeaturesFrom = signal([]); - private defaultTooltip = $localize `:@@core.edit.edit-feature-tooltip:Edit feature`; + private defaultTooltip = $localize `:@@core.edit.edit:Edit feature`; private notLoggedInTooltip = $localize `:@@core.edit.require-login-tooltip:You must be logged in to edit.`; private noLayersTooltip = $localize `:@@core.edit.no-editable-layers-tooltip:There are no editable layers. Enable a layer to start editing.`; diff --git a/projects/core/src/lib/components/edit/state/edit.effects.ts b/projects/core/src/lib/components/edit/state/edit.effects.ts index 06cdbe9d2..de2a01dfc 100644 --- a/projects/core/src/lib/components/edit/state/edit.effects.ts +++ b/projects/core/src/lib/components/edit/state/edit.effects.ts @@ -4,7 +4,7 @@ import * as EditActions from './edit.actions'; import { filter, map, switchMap } from 'rxjs'; import { FeatureInfoService } from '../../feature-info'; import { withLatestFrom } from 'rxjs/operators'; -import { selectEditActive, selectSelectedEditLayer } from './edit.selectors'; +import { selectEditActive, selectSelectedCopyLayer, selectSelectedEditLayer } from './edit.selectors'; import { Store } from '@ngrx/store'; import { activateTool } from '../../toolbar/state/toolbar.actions'; import { ToolbarComponentEnum } from '../../toolbar/models/toolbar-component.enum'; @@ -36,9 +36,9 @@ export class EditEffects { public loadCopyFeatures$ = createEffect(() => { return this.actions$.pipe( ofType(EditActions.loadCopyFeatures), - withLatestFrom(this.store$.select(selectSelectedEditLayer)), - switchMap(([ action, editLayer ]) => { - return this.featureInfoService.getFeaturesForLayer$(action.coordinates, editLayer, action.pointerType).pipe( + withLatestFrom(this.store$.select(selectSelectedCopyLayer)), + switchMap(([ action, copyLayer ]) => { + return this.featureInfoService.getFeaturesForLayer$(action.coordinates, copyLayer, action.pointerType).pipe( map(result => { if (!result) { return EditActions.loadCopyFeaturesFailed({}); diff --git a/projects/core/src/lib/components/edit/state/edit.reducer.ts b/projects/core/src/lib/components/edit/state/edit.reducer.ts index a954466dd..caf2605c8 100644 --- a/projects/core/src/lib/components/edit/state/edit.reducer.ts +++ b/projects/core/src/lib/components/edit/state/edit.reducer.ts @@ -1,6 +1,6 @@ import * as EditActions from './edit.actions'; import { Action, createReducer, on } from '@ngrx/store'; -import { EditState, initialEditState } from './edit.state'; +import { EditState, initialEditCopyState, initialEditState } from './edit.state'; import { LoadingStateEnum } from '@tailormap-viewer/shared'; import { FeatureInfoFeatureModel } from '../../feature-info/models/feature-info-feature.model'; import { FeatureInfoColumnMetadataModel } from '../../feature-info/models/feature-info-column-metadata.model'; @@ -12,6 +12,7 @@ const onSetIsActive = ( ...state, isActive: payload.active, isCreateNewFeatureActive: payload.active ? state.isCreateNewFeatureActive : false, + ...(payload.active ? {} : initialEditCopyState), dialogVisible: false, selectedFeature: null, openedFromFeatureInfo: false, @@ -111,7 +112,7 @@ const onSetCopyOtherLayerFeaturesActive = ( attributes: {}, }], columnMetadata: payload.columnMetadata, - loadStatus: LoadingStateEnum.LOADED, + loadStatus: LoadingStateEnum.INITIAL, dialogCollapsed: false, }); @@ -130,6 +131,7 @@ const onSetCreateNewFeatureActive = ( return { ...state, isCreateNewFeatureActive: payload.active, + ...initialEditCopyState, newGeometryType: payload.geometryType, dialogVisible: payload.active, selectedFeature: 'new', @@ -202,9 +204,7 @@ const onHideEditDialog = (state: EditState): EditState => ({ dialogCollapsed: false, selectedFeature: null, isCreateNewFeatureActive: false, - isCopyOtherLayerFeaturesActive: false, - copiedFeatures: [], - selectedCopyLayer: null, + ...initialEditCopyState, }); const onExpandCollapseEditDialog = (state: EditState): EditState => ({ @@ -237,9 +237,7 @@ const onEditNewlyCreatedFeature = ( ): EditState => { return { ...state, - isCopyOtherLayerFeaturesActive: false, - selectedCopyLayer: null, - copiedFeatures: [], + ...initialEditCopyState, features: [payload.feature], selectedFeature: payload.feature.__fid, isCreateNewFeatureActive: false, diff --git a/projects/core/src/lib/components/edit/state/edit.selectors.ts b/projects/core/src/lib/components/edit/state/edit.selectors.ts index b810c5215..5039550f4 100644 --- a/projects/core/src/lib/components/edit/state/edit.selectors.ts +++ b/projects/core/src/lib/components/edit/state/edit.selectors.ts @@ -11,12 +11,13 @@ const selectEditState = createFeatureSelector(editStateKey); export const selectEditActive = createSelector(selectEditState, state => state.isActive); export const selectEditSelectedFeature = createSelector(selectEditState, state => state.selectedFeature); export const selectEditCopyOtherLayerFeaturesActive = createSelector(selectEditState, state => state.isCopyOtherLayerFeaturesActive); -export const selectEditCreateNewFeatureActive = createSelector(selectEditState, +export const selectEditCreateNewFeatureActive = createSelector(selectEditState, state => state.isCreateNewFeatureActive); +export const selectEditCreateNewOrCopyFeatureActive = createSelector(selectEditState, state => state.isCreateNewFeatureActive || state.isCopyOtherLayerFeaturesActive); export const selectNewFeatureGeometryType = createSelector(selectEditState, state => state.newGeometryType); -export const selectSelectedEditLayer = createSelector(selectEditState, - state => state.isCopyOtherLayerFeaturesActive ? state.selectedCopyLayer : state.selectedLayer); +export const selectSelectedEditLayer = createSelector(selectEditState, state => state.selectedLayer); +export const selectSelectedCopyLayer = createSelector(selectEditState, state => state.selectedCopyLayer); export const selectCopiedFeatures = createSelector(selectEditState, state => state.copiedFeatures); export const selectEditMapCoordinates = createSelector(selectEditState, state => state.mapCoordinates); @@ -42,7 +43,7 @@ export const selectEditStatus = createSelector( selectEditActiveWithSelectedLayer, selectEditCopyOtherLayerFeaturesActive, selectEditSelectedFeature, - selectEditCreateNewFeatureActive, + selectEditCreateNewOrCopyFeatureActive, (editWithLayerActive, editCopyOtherLayerFeaturesActive, editSelectedFeatureActive, editCreateNewFeatureActive) => { if (!editWithLayerActive) { return 'inactive'; diff --git a/projects/core/src/lib/components/edit/state/edit.state.ts b/projects/core/src/lib/components/edit/state/edit.state.ts index dd5855eca..c16ad285e 100644 --- a/projects/core/src/lib/components/edit/state/edit.state.ts +++ b/projects/core/src/lib/components/edit/state/edit.state.ts @@ -24,14 +24,18 @@ export interface EditState { openedFromFeatureInfo?: boolean; } +export const initialEditCopyState = { + isCopyOtherLayerFeaturesActive: false, + selectedCopyLayer: null, + copiedFeatures: [], +}; + export const initialEditState: EditState = { isActive: false, - isCopyOtherLayerFeaturesActive: false, + ...initialEditCopyState, isCreateNewFeatureActive: false, newGeometryType: null, selectedLayer: null, - selectedCopyLayer: null, - copiedFeatures: [], dialogVisible: false, dialogCollapsed: false, loadStatus: LoadingStateEnum.INITIAL, diff --git a/projects/core/src/lib/shared/components/dialog/dialog.component.ts b/projects/core/src/lib/shared/components/dialog/dialog.component.ts index d7486e829..d52499aba 100644 --- a/projects/core/src/lib/shared/components/dialog/dialog.component.ts +++ b/projects/core/src/lib/shared/components/dialog/dialog.component.ts @@ -34,7 +34,7 @@ export class DialogComponent implements OnInit, OnChanges, OnDestroy { public open: boolean | null = false; @Input() - public dialogTitle = ''; + public dialogTitle: string | null = ''; @Input() public hidden: boolean | null = false; From c8ff5e31a1bfa8284a6789bdeac008c3976e81e3 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Thu, 30 Oct 2025 16:55:01 +0100 Subject: [PATCH 06/13] Fix tests --- .../edit/edit-dialog/edit-dialog.component.spec.ts | 12 +++++++++--- .../lib/components/edit/edit/edit.component.spec.ts | 4 ++-- .../src/lib/components/edit/edit/edit.component.ts | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.spec.ts b/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.spec.ts index 9a911d677..6687e49e5 100644 --- a/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.spec.ts +++ b/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.spec.ts @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/angular'; import { EditDialogComponent } from './edit-dialog.component'; import { provideMockStore } from '@ngrx/store/testing'; -import { SharedModule } from '@tailormap-viewer/shared'; +import { LoadingStateEnum, SharedModule } from '@tailormap-viewer/shared'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { AttributeType, getAppLayerModel, getFeatureModel, UniqueValuesService } from '@tailormap-viewer/api'; import { MatIconTestingModule } from '@angular/material/icon/testing'; @@ -15,6 +15,8 @@ import { of } from 'rxjs'; import { ViewerLayoutService } from '../../../services/viewer-layout/viewer-layout.service'; import { CoreSharedModule } from '../../../shared'; import { getMapServiceMock } from '../../../test-helpers/map-service.mock.spec'; +import { EditMapToolService } from '../services/edit-map-tool.service'; +import { coreStateKey, initialCoreState, selectViewerLoadingState, ViewerState } from '../../../state'; const getFeatureInfo = (): FeatureWithMetadataModel => { return { @@ -42,9 +44,13 @@ const setup = async (getLayerDetails = false, selectors: any[] = []) => { }, { provide: EditFeatureService, useValue: {} }, getMapServiceMock().provider, - provideMockStore({ initialState: { [editStateKey]: { ...initialEditState } }, selectors }), + provideMockStore({ initialState: { + [editStateKey]: { ...initialEditState }, + [coreStateKey]: { ...initialCoreState, viewer: { components: [] } as ViewerState }, + }, selectors }), { provide: UniqueValuesService, useValue: { clearCaches: jest.fn() } }, { provide: ViewerLayoutService, useValue: { setLeftPadding: jest.fn(), setRightPadding: jest.fn() } }, + { provide: EditMapToolService, useValue: { allEditGeometry$: of() } }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }); @@ -63,7 +69,7 @@ describe('EditDialogComponent', () => { { selector: selectSelectedEditFeature, value: getFeatureInfo() }, { selector: selectEditDialogVisible, value: true }, ]); - expect(await screen.findByText('Edit')).toBeInTheDocument(); + expect(await screen.findByText('Edit feature')).toBeInTheDocument(); expect(await screen.findByText('Close')).toBeInTheDocument(); expect(await screen.findByText('Save')).toBeInTheDocument(); expect(await screen.findByText('Delete')).toBeInTheDocument(); diff --git a/projects/core/src/lib/components/edit/edit/edit.component.spec.ts b/projects/core/src/lib/components/edit/edit/edit.component.spec.ts index e2b6ff334..7d5daa6f3 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.spec.ts +++ b/projects/core/src/lib/components/edit/edit/edit.component.spec.ts @@ -8,12 +8,14 @@ import { selectEditActive, selectSelectedEditLayer } from "../state/edit.selecto import { SharedModule } from "@tailormap-viewer/shared"; import { MatIconTestingModule } from "@angular/material/icon/testing"; import { AuthenticatedUserTestHelper } from '../../../test-helpers/authenticated-user-test.helper'; +import { HttpXsrfTokenExtractor } from '@angular/common/http'; const setup = async (hasLayers: boolean, authenticated: boolean) => { await render(EditComponent, { imports: [ SharedModule, MatIconTestingModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA], providers: [ + { provide: HttpXsrfTokenExtractor, useValue: {} as HttpXsrfTokenExtractor }, { provide: TAILORMAP_API_V1_SERVICE, useClass: TailormapApiV1MockService }, AuthenticatedUserTestHelper.provideAuthenticatedUserService(authenticated, []), provideMockStore({ @@ -35,8 +37,6 @@ describe('EditComponent', () => { const buttons = screen.getAllByRole('button'); expect(buttons[0]).toBeVisible(); expect(buttons[0]).not.toHaveClass("disabled"); - expect(buttons[1]).toBeVisible(); - expect(buttons[1]).toHaveClass("disabled"); }); test('should be disabled when user is not logged in button', async () => { diff --git a/projects/core/src/lib/components/edit/edit/edit.component.ts b/projects/core/src/lib/components/edit/edit/edit.component.ts index 69412ee74..b94ab6278 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.ts +++ b/projects/core/src/lib/components/edit/edit/edit.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject, signal } from '@angular/core'; import { selectCopiedFeatures, - selectEditActive, selectEditCopyOtherLayerFeaturesActive, selectEditCreateNewFeatureActive, selectEditCreateNewOrCopyFeatureActive, + selectEditActive, selectEditCopyOtherLayerFeaturesActive, selectEditCreateNewFeatureActive, selectSelectedEditLayer, } from '../state/edit.selectors'; import { Store } from '@ngrx/store'; From da18bf81a4372ca7e76a337bb7ef91e4f3aa39e0 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Fri, 31 Oct 2025 15:16:02 +0100 Subject: [PATCH 07/13] Show the selected copy layer in different style, deactivate copy layer when clicking selected copy layer again --- .../components/edit/edit/edit.component.css | 4 ++ .../components/edit/edit/edit.component.html | 7 ++- .../components/edit/edit/edit.component.ts | 56 ++++++++++--------- .../lib/components/edit/state/edit.actions.ts | 5 +- .../lib/components/edit/state/edit.reducer.ts | 20 +++++-- 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/projects/core/src/lib/components/edit/edit/edit.component.css b/projects/core/src/lib/components/edit/edit/edit.component.css index 7be6b319e..a5f3e43c3 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.css +++ b/projects/core/src/lib/components/edit/edit/edit.component.css @@ -37,6 +37,10 @@ padding-top: 6px; } +.mat-mdc-menu-item.selected { + color: var(--primary-color); +} + @media screen and (max-width: 730px) { .edit-container--is-active { padding: 0; diff --git a/projects/core/src/lib/components/edit/edit/edit.component.html b/projects/core/src/lib/components/edit/edit/edit.component.html index b155f5bd0..dd935d27f 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.html +++ b/projects/core/src/lib/components/edit/edit/edit.component.html @@ -75,8 +75,13 @@ + @let selectedCopyLayerId = selectedCopyLayer$ | async; @for (layer of layersToCreateNewFeaturesFrom(); track layer) { - + @let selected = layer.id === selectedCopyLayerId; + } } diff --git a/projects/core/src/lib/components/edit/edit/edit.component.ts b/projects/core/src/lib/components/edit/edit/edit.component.ts index b94ab6278..329a04220 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.ts +++ b/projects/core/src/lib/components/edit/edit/edit.component.ts @@ -1,13 +1,14 @@ import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject, signal } from '@angular/core'; import { selectCopiedFeatures, - selectEditActive, selectEditCopyOtherLayerFeaturesActive, selectEditCreateNewFeatureActive, + selectEditActive, selectEditCopyOtherLayerFeaturesActive, selectEditCreateNewFeatureActive, selectSelectedCopyLayer, selectSelectedEditLayer, } from '../state/edit.selectors'; import { Store } from '@ngrx/store'; import { combineLatest, map, of, take } from 'rxjs'; import { - setEditActive, setEditCopyOtherLayerFeaturesActive, setEditCreateNewFeatureActive, setSelectedEditLayer, + setEditActive, setEditCopyOtherLayerFeaturesActive, setEditCopyOtherLayerFeaturesDisabled, setEditCreateNewFeatureActive, + setSelectedEditLayer, } from '../state/edit.actions'; import { FormControl } from '@angular/forms'; import { selectEditableLayers, selectOrderedVisibleLayersWithServices } from '../../../map/state/map.selectors'; @@ -38,6 +39,7 @@ export class EditComponent implements OnInit { public createNewFeatureActive$ = this.store$.select(selectEditCreateNewFeatureActive); public copyActive$ = this.store$.select(selectEditCopyOtherLayerFeaturesActive); public copiedFeaturesCount$ = this.store$.select(selectCopiedFeatures).pipe(map(features => features.length)); + public selectedCopyLayer$ = this.store$.select(selectSelectedCopyLayer); public editableLayers$ = this.store$.select(selectEditableLayers); public layer = new FormControl(); public editGeometryType: GeometryType | null = null; @@ -144,25 +146,22 @@ export class EditComponent implements OnInit { if (!this.layer.value) { return; } - // get layer attribute details for edit form - this.applicationLayerService.getLayerDetails$(this.layer.value) - .pipe(take(1)) - .subscribe(layerDetails => { - // show edit dialog - this.store$.dispatch(setEditCreateNewFeatureActive({ - active: true, - geometryType, - columnMetadata: layerDetails.details.attributes.map(attribute => { - return { - layerId: layerDetails.details.id, - name: attribute.name, - type: attribute.type as unknown as AttributeType, - alias: attribute.editAlias, - }; - }, - ), - })); - }); + + this.applicationLayerService.getLayerDetails$(this.layer.value).pipe(take(1)).subscribe(layerDetails => { + this.store$.dispatch(setEditCreateNewFeatureActive({ + active: true, + geometryType, + columnMetadata: layerDetails.details.attributes.map(attribute => { + return { + layerId: layerDetails.details.id, + name: attribute.name, + type: attribute.type as unknown as AttributeType, + alias: attribute.editAlias, + }; + }, + ), + })); + }); } public createFeatureIfSingleGeometryType() { @@ -175,13 +174,15 @@ export class EditComponent implements OnInit { } public createFeatureFromLayer(id: string) { - // get layer attribute details for edit form - this.applicationLayerService.getLayerDetails$(this.layer.value) - .pipe(take(1)) - .subscribe(layerDetails => { - // show edit dialog + + this.selectedCopyLayer$.pipe(take(1)).subscribe(selectedCopyLayer => { + if (id == selectedCopyLayer) { + this.store$.dispatch(setEditCopyOtherLayerFeaturesDisabled()); + return; + } + + this.applicationLayerService.getLayerDetails$(this.layer.value).pipe(take(1)).subscribe(layerDetails => { this.store$.dispatch(setEditCopyOtherLayerFeaturesActive({ - active: true, layerId: id, columnMetadata: layerDetails.details.attributes.map(attribute => { return { @@ -194,5 +195,6 @@ export class EditComponent implements OnInit { ), })); }); + }); } } diff --git a/projects/core/src/lib/components/edit/state/edit.actions.ts b/projects/core/src/lib/components/edit/state/edit.actions.ts index db79efdea..333869b92 100644 --- a/projects/core/src/lib/components/edit/state/edit.actions.ts +++ b/projects/core/src/lib/components/edit/state/edit.actions.ts @@ -19,9 +19,12 @@ export const setEditCreateNewFeatureActive = createAction( export const setEditCopyOtherLayerFeaturesActive = createAction( `${editActionsPrefix} Set Copy Other Layer Features Active`, - props<{ active: boolean; layerId: string; columnMetadata: FeatureInfoColumnMetadataModel[] }>(), + props<{ layerId: string; columnMetadata: FeatureInfoColumnMetadataModel[] }>(), ); +export const setEditCopyOtherLayerFeaturesDisabled = createAction( + `${editActionsPrefix} Set Copy Other Layer Features Disabled`); + export const setSelectedEditLayer = createAction( `${editActionsPrefix} Set Selected Layer`, props<{ layer: string | null }>(), diff --git a/projects/core/src/lib/components/edit/state/edit.reducer.ts b/projects/core/src/lib/components/edit/state/edit.reducer.ts index caf2605c8..6181e5c47 100644 --- a/projects/core/src/lib/components/edit/state/edit.reducer.ts +++ b/projects/core/src/lib/components/edit/state/edit.reducer.ts @@ -101,19 +101,30 @@ const onSetCopyOtherLayerFeaturesActive = ( payload: ReturnType, ): EditState => ({ ...state, - isCopyOtherLayerFeaturesActive: payload.active, + isCopyOtherLayerFeaturesActive: true, isCreateNewFeatureActive: false, + dialogVisible: true, + dialogCollapsed: false, selectedCopyLayer: payload.layerId, - dialogVisible: payload.active, + columnMetadata: payload.columnMetadata, + // Do not reset copiedFeatures, so features from different layers can be copied selectedFeature: 'new', features: [{ layerId: payload.columnMetadata[0].layerId, __fid: 'new', attributes: {}, }], - columnMetadata: payload.columnMetadata, loadStatus: LoadingStateEnum.INITIAL, - dialogCollapsed: false, +}); + +const onSetCopyOtherLayerFeaturesDisabled = ( + state: EditState, +): EditState => ({ + ...state, + isCreateNewFeatureActive: false, + dialogVisible: false, + dialogCollapsed: true, + ...initialEditCopyState, }); const onSetCreateNewFeatureActive = ( @@ -248,6 +259,7 @@ const editReducerImpl = createReducer( initialEditState, on(EditActions.setEditActive, onSetIsActive), on(EditActions.setEditCopyOtherLayerFeaturesActive, onSetCopyOtherLayerFeaturesActive), + on(EditActions.setEditCopyOtherLayerFeaturesDisabled, onSetCopyOtherLayerFeaturesDisabled), on(EditActions.setEditCreateNewFeatureActive, onSetCreateNewFeatureActive), on(EditActions.setSelectedEditLayer, onSetSelectedLayer), on(EditActions.loadEditFeatures, onLoadFeatureInfo), From 5bbe7ee620a69021e799aa2b6817c2fc6279fcb6 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Tue, 4 Nov 2025 15:03:28 +0100 Subject: [PATCH 08/13] Fix form cleared when (de)selecting feature to copy --- projects/core/src/lib/components/edit/state/edit.reducer.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/projects/core/src/lib/components/edit/state/edit.reducer.ts b/projects/core/src/lib/components/edit/state/edit.reducer.ts index 6181e5c47..afa8cac71 100644 --- a/projects/core/src/lib/components/edit/state/edit.reducer.ts +++ b/projects/core/src/lib/components/edit/state/edit.reducer.ts @@ -87,11 +87,6 @@ const onLoadCopyFeaturesSuccess = ( return { ...state, - features: [{ - layerId: state.columnMetadata[0].layerId, - __fid: 'new', - attributes: {}, - }], copiedFeatures, }; }; @@ -215,6 +210,7 @@ const onHideEditDialog = (state: EditState): EditState => ({ dialogCollapsed: false, selectedFeature: null, isCreateNewFeatureActive: false, + features: [], ...initialEditCopyState, }); From 76d2d4a3659c9b63dad3de5f4faa1f5ba57d3ec0 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Tue, 4 Nov 2025 15:16:48 +0100 Subject: [PATCH 09/13] Deactivate copy features when switchting edit layer --- projects/core/src/lib/components/edit/state/edit.reducer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/core/src/lib/components/edit/state/edit.reducer.ts b/projects/core/src/lib/components/edit/state/edit.reducer.ts index afa8cac71..c6c716a40 100644 --- a/projects/core/src/lib/components/edit/state/edit.reducer.ts +++ b/projects/core/src/lib/components/edit/state/edit.reducer.ts @@ -28,6 +28,7 @@ const onSetSelectedLayer = ( dialogCollapsed: false, selectedFeature: null, isCreateNewFeatureActive: false, + ...initialEditCopyState, }); const onLoadFeatureInfo = ( From 5964a827b7e7a777376957ff36297d80e4aa47b1 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Wed, 5 Nov 2025 15:26:25 +0100 Subject: [PATCH 10/13] Add config for available copy layers --- .../assets/locale/messages.admin-core.de.xlf | 20 +++--- .../assets/locale/messages.admin-core.en.xlf | 15 +++-- .../assets/locale/messages.admin-core.nl.xlf | 20 +++--- .../components/components.module.ts | 16 +++-- .../edit-component-config.component.css | 7 ++ .../edit-component-config.component.html | 17 +++++ .../edit-component-config.component.ts | 66 +++++++++++++++++-- .../extended-app-tree-layer-node.model.ts | 12 ++++ .../state/application.selectors.ts | 31 +++++++++ .../component-config/edit-config.model.ts | 1 + 10 files changed, 172 insertions(+), 33 deletions(-) create mode 100644 projects/admin-core/src/lib/application/models/extended-app-tree-layer-node.model.ts diff --git a/projects/admin-core/assets/locale/messages.admin-core.de.xlf b/projects/admin-core/assets/locale/messages.admin-core.de.xlf index b4110b3d9..e05feaa8d 100644 --- a/projects/admin-core/assets/locale/messages.admin-core.de.xlf +++ b/projects/admin-core/assets/locale/messages.admin-core.de.xlf @@ -10,6 +10,14 @@ Single-layer filter Einzel-Ebenen-Filter + + Created by at + Erstellt von + + + Last updated by at + Zuletzt aktualisiert von + Add application Anwendung hinzufügen @@ -142,6 +150,10 @@ Copy application Anwendung kopieren + + Select one or more layers that are available for copying features from. If no layers are selected, all layers will be available. Note that incompatible layer geometry types may prevent successful copying. + Wählen Sie eine oder mehrere Ebenen aus, von denen Features kopiert werden können. Wenn keine Ebenen ausgewählt sind, stehen alle Ebenen zur Verfügung. Beachten Sie, dass inkompatible Geometrietypen der Ebenen ein erfolgreiches Kopieren verhindern können. + Create application Anwendung erstellen @@ -2413,14 +2425,6 @@ Valid until Gültig bis - - Created by - Erstellt von - - - Last updated by - Zuletzt aktualisiert von - diff --git a/projects/admin-core/assets/locale/messages.admin-core.en.xlf b/projects/admin-core/assets/locale/messages.admin-core.en.xlf index b070b01e1..6ef839a9f 100644 --- a/projects/admin-core/assets/locale/messages.admin-core.en.xlf +++ b/projects/admin-core/assets/locale/messages.admin-core.en.xlf @@ -8,6 +8,12 @@ Single-layer filter + + Created by at + + + Last updated by at + Add application @@ -107,6 +113,9 @@ Copy application + + Select one or more layers that are available for copying features from. If no layers are selected, all layers will be available. Note that incompatible layer geometry types may prevent successful copying. + Create application @@ -1775,12 +1784,6 @@ Valid until - - Created by - - - Last updated by - diff --git a/projects/admin-core/assets/locale/messages.admin-core.nl.xlf b/projects/admin-core/assets/locale/messages.admin-core.nl.xlf index b002d37cb..a34899962 100644 --- a/projects/admin-core/assets/locale/messages.admin-core.nl.xlf +++ b/projects/admin-core/assets/locale/messages.admin-core.nl.xlf @@ -10,6 +10,14 @@ Single-layer filter Enkele laag filter + + Created by at + Aangemaakt door + + + Last updated by at + Laatst bijgewerkt door + Add application Applicatie toevoegen @@ -142,6 +150,10 @@ Copy application Kopieer applicatie + + Select one or more layers that are available for copying features from. If no layers are selected, all layers will be available. Note that incompatible layer geometry types may prevent successful copying. + Selecteer één of meerdere lagen waarvan objecten kunnen worden gekopiëerd. Als geen lagen zijn aangevinkt is dit mogelijk van alle lagen. Let op dat door incompatibele geometrie-types een object mogelijk niet gekopiëerd kan worden. + Create application Applicatie aanmaken @@ -2397,14 +2409,6 @@ Valid until Geldig tot - - Created by - Aangemaakt door - - - Last updated by - Laatst bijgewerkt door - diff --git a/projects/admin-core/src/lib/application/components/components.module.ts b/projects/admin-core/src/lib/application/components/components.module.ts index 5b90ee54a..c2d7cc257 100644 --- a/projects/admin-core/src/lib/application/components/components.module.ts +++ b/projects/admin-core/src/lib/application/components/components.module.ts @@ -16,6 +16,7 @@ import { EditComponentConfigComponent } from './edit-config/edit-component-confi import { GeolocationConfigComponent } from './geolocation-config/geolocation-config.component'; import { InfoConfigComponent } from './info-config/info-config.component'; import { DrawingConfigComponent } from './drawing-config/drawing-config.component'; +import { SharedAdminComponentsModule } from '../../shared/components'; @NgModule({ declarations: [ @@ -31,13 +32,14 @@ import { DrawingConfigComponent } from './drawing-config/drawing-config.componen InfoConfigComponent, DrawingConfigComponent, ], - imports: [ - CommonModule, - SharedModule, - BaseComponentConfigComponent, - SelectUploadModule, - MarkdownEditorComponent, - ], + imports: [ + CommonModule, + SharedModule, + BaseComponentConfigComponent, + SelectUploadModule, + MarkdownEditorComponent, + SharedAdminComponentsModule, + ], exports: [ ComponentsListComponent, ComponentConfigRendererComponent, diff --git a/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.css b/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.css index e69de29bb..3fec1dd83 100644 --- a/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.css +++ b/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.css @@ -0,0 +1,7 @@ +div { + margin-bottom: 8px; +} + +.copy-layers { + max-width: 500px; +} diff --git a/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.html b/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.html index e49a26a57..cfbe05e34 100644 --- a/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.html +++ b/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.html @@ -6,5 +6,22 @@
Close window after adding new feature + +
+
+ Select one or more layers that are available for copying features from. If no layers are selected, all layers will be available. Note that incompatible layer geometry types may prevent successful copying. +
+ + + @for (layer of filteredLayerList(); track layer.id) { + +
{{ layer.label }}
+
+ } +
+
diff --git a/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.ts b/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.ts index f61d91f4b..430c44275 100644 --- a/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.ts +++ b/projects/admin-core/src/lib/application/components/edit-config/edit-component-config.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, DestroyRef, Input, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, DestroyRef, Input, inject, signal, OnInit, computed, effect } from '@angular/core'; import { BaseComponentTypeEnum, EditConfigModel, } from '@tailormap-viewer/api'; @@ -6,7 +6,13 @@ import { FormControl, FormGroup } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ComponentConfigurationService } from '../../services/component-configuration.service'; import { ConfigurationComponentModel } from '../configuration-component.model'; -import { debounceTime } from 'rxjs'; +import { debounceTime, take } from 'rxjs'; +import { MatSelectionListChange } from '@angular/material/list'; +import { selectExtendedAppLayerNodesForSelectedApplication } from '../../state/application.selectors'; +import { Store } from '@ngrx/store'; +import { FilterHelper, LoadingStateEnum } from '@tailormap-viewer/shared'; +import { selectCatalogLoadStatus } from '../../../catalog/state/catalog.selectors'; +import { loadCatalog } from '../../../catalog/state/catalog.actions'; @Component({ selector: 'tm-admin-edit-component-config', @@ -15,10 +21,10 @@ import { debounceTime } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, }) -export class EditComponentConfigComponent implements ConfigurationComponentModel { +export class EditComponentConfigComponent implements ConfigurationComponentModel, OnInit { private componentConfigService = inject(ComponentConfigurationService); private destroyRef = inject(DestroyRef); - + private store$ = inject(Store); @Input() public type: BaseComponentTypeEnum | undefined; @@ -39,8 +45,36 @@ export class EditComponentConfigComponent implements ConfigurationComponentModel public formGroup = new FormGroup({ closeAfterAddFeature: new FormControl(false), }); + public selectedCopyLayers = signal([]); + + // Not in formGroup for config properties, used for layer filter control + public copyLayerFilter = new FormControl(''); + public copyLayerFilterSignal = signal(''); + public allLayers = this.store$.selectSignal(selectExtendedAppLayerNodesForSelectedApplication); + + public filteredLayerList = computed(() => { + const allLayers = this.allLayers(); + const selectedLayerIds = this.selectedCopyLayers(); + const filterTerm = this.copyLayerFilterSignal(); + const layersWithSelected = allLayers.map(layer => ({ + ...layer, + selected: selectedLayerIds.includes(layer.id), + })); + if (filterTerm) { + return FilterHelper.filterByTerm(layersWithSelected, filterTerm, l => l.label); + } + return layersWithSelected; + }); constructor() { + this.store$.select(selectCatalogLoadStatus) + .pipe(take(1)) + .subscribe(loadStatus => { + if (loadStatus === LoadingStateEnum.INITIAL || loadStatus === LoadingStateEnum.FAILED) { + this.store$.dispatch(loadCatalog()); + } + }); + this.formGroup.valueChanges .pipe(takeUntilDestroyed(this.destroyRef), debounceTime(250)) .subscribe(() => { @@ -49,14 +83,38 @@ export class EditComponentConfigComponent implements ConfigurationComponentModel } this.saveConfig(); }); + + effect(() => { + this.componentConfigService.updateConfigForKey(this.type, 'copyLayerIds', this.selectedCopyLayers()); + }); + } + + public ngOnInit(): void { + this.copyLayerFilter.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(filterTerm => { + this.copyLayerFilterSignal.set(filterTerm || ''); + }); } public initForm(config: EditConfigModel | undefined) { this.formGroup.patchValue({ closeAfterAddFeature: config?.closeAfterAddFeature ?? false }, { emitEvent: false }); + this.selectedCopyLayers.set(config?.copyLayerIds ?? []); } private saveConfig() { this.componentConfigService.updateConfigForKey(this.type, 'closeAfterAddFeature', this.formGroup.value.closeAfterAddFeature); } + public onCopyLayerSelectionChange($event: MatSelectionListChange) { + const selectedLayers = [...this.selectedCopyLayers()]; + $event.options.forEach(option => { + if (option.selected) { + selectedLayers.push(option.value); + } else { + selectedLayers.splice(selectedLayers.indexOf(option.value), 1); + } + }); + this.selectedCopyLayers.set(selectedLayers); + } } diff --git a/projects/admin-core/src/lib/application/models/extended-app-tree-layer-node.model.ts b/projects/admin-core/src/lib/application/models/extended-app-tree-layer-node.model.ts new file mode 100644 index 000000000..270e26a67 --- /dev/null +++ b/projects/admin-core/src/lib/application/models/extended-app-tree-layer-node.model.ts @@ -0,0 +1,12 @@ +import { ExtendedGeoServiceModel } from '../../catalog/models/extended-geo-service.model'; +import { ExtendedFeatureTypeModel } from '../../catalog/models/extended-feature-type.model'; +import { ExtendedGeoServiceLayerModel } from '../../catalog/models/extended-geo-service-layer.model'; +import { AppLayerSettingsModel, AppTreeLayerNodeModel } from '@tailormap-admin/admin-api'; + +export interface ExtendedAppTreeLayerNodeModel extends AppTreeLayerNodeModel { + label: string; + appLayerSettings: AppLayerSettingsModel; + geoService: ExtendedGeoServiceModel | undefined; + geoServiceLayer: ExtendedGeoServiceLayerModel | undefined; + featureType: ExtendedFeatureTypeModel | undefined; +} diff --git a/projects/admin-core/src/lib/application/state/application.selectors.ts b/projects/admin-core/src/lib/application/state/application.selectors.ts index 3f9d23788..87b995757 100644 --- a/projects/admin-core/src/lib/application/state/application.selectors.ts +++ b/projects/admin-core/src/lib/application/state/application.selectors.ts @@ -11,6 +11,7 @@ import { ApplicationModelHelper } from '../helpers/application-model.helper'; import { GeoServiceLayerInApplicationModel } from '../models/geo-service-layer-in-application.model'; import { ExtendedGeoServiceLayerModel } from '../../catalog/models/extended-geo-service-layer.model'; import { ExtendedFilterGroupModel } from '../models/extended-filter-group.model'; +import { ExtendedAppTreeLayerNodeModel } from '../models/extended-app-tree-layer-node.model'; const selectApplicationState = createFeatureSelector(applicationStateKey); @@ -205,6 +206,36 @@ export const selectStylingConfig = createSelector(selectDraftApplication, applic export const selectFilterGroups = createSelector(selectDraftApplication, application => application?.settings?.filterGroups || []); +export const selectExtendedAppLayerNodesForSelectedApplication = createSelector( + selectAppLayerNodesForSelectedApplication, + selectGeoServices, + selectGeoServiceLayers, + selectSelectedApplicationLayerSettings, + selectFeatureTypes, + (appLayerTreeNodes, geoServices, geoServiceLayers, layerSettings, featureTypes) => { + const geoServiceLayerMap = ApplicationTreeHelper.getLayerMap(geoServiceLayers); + return appLayerTreeNodes + .filter(node => ApplicationModelHelper.isLayerTreeNode(node)) + .map((appLayerNode): ExtendedAppTreeLayerNodeModel => { + const geoServiceLayer = geoServiceLayerMap.get(ApplicationTreeHelper.getLayerMapKey(appLayerNode.layerName, appLayerNode.serviceId)); + const geoService = geoServices.find(service => service.id === geoServiceLayer?.serviceId); + const appLayerSettings = layerSettings[appLayerNode.id]; + const featureType = featureTypes.find(ft => { + return ft.featureSourceId === geoServiceLayer?.layerSettings?.featureType?.featureSourceId.toString() + && ft.name === geoServiceLayer.layerSettings?.featureType?.featureTypeName; + }); + return { + ...appLayerNode, + label: ApplicationTreeHelper.getTreeModelLabel(appLayerNode, geoServiceLayerMap, layerSettings, 'layer'), + appLayerSettings, + geoService, + geoServiceLayer, + featureType, + }; + }); + }, +); + export const selectFilterableLayersForApplication = createSelector( selectAppLayerNodesForSelectedApplication, selectGeoServiceLayers, diff --git a/projects/api/src/lib/models/component-config/edit-config.model.ts b/projects/api/src/lib/models/component-config/edit-config.model.ts index 10b256f2a..04428842c 100644 --- a/projects/api/src/lib/models/component-config/edit-config.model.ts +++ b/projects/api/src/lib/models/component-config/edit-config.model.ts @@ -2,4 +2,5 @@ import { ComponentBaseConfigModel } from '../component-base-config.model'; export interface EditConfigModel extends ComponentBaseConfigModel { closeAfterAddFeature: boolean; + copyLayerIds?: string[]; } From 07318275183e5fdf910e8884156c58f9b622d2b8 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Wed, 5 Nov 2025 15:37:11 +0100 Subject: [PATCH 11/13] Use available copy layers config --- .../lib/components/edit/edit/edit.component.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/projects/core/src/lib/components/edit/edit/edit.component.ts b/projects/core/src/lib/components/edit/edit/edit.component.ts index 329a04220..33aeefd5a 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.ts +++ b/projects/core/src/lib/components/edit/edit/edit.component.ts @@ -16,10 +16,13 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { switchMap, withLatestFrom } from 'rxjs/operators'; import { hideFeatureInfoDialog } from '../../feature-info/state/feature-info.actions'; import { ApplicationLayerService } from '../../../map/services/application-layer.service'; -import { AppLayerModel, AttributeType, AuthenticatedUserService, GeometryType } from '@tailormap-viewer/api'; +import { + AppLayerModel, AttributeType, AuthenticatedUserService, BaseComponentTypeEnum, EditConfigModel, GeometryType, +} from '@tailormap-viewer/api'; import { activateTool } from '../../toolbar/state/toolbar.actions'; import { ToolbarComponentEnum } from '../../toolbar/models/toolbar-component.enum'; import { DrawingType, MapService, ScaleHelper } from '@tailormap-viewer/map'; +import { ComponentConfigHelper } from '../../../shared'; @Component({ selector: 'tm-edit', @@ -53,6 +56,8 @@ export class EditComponent implements OnInit { public tooltip = this.defaultTooltip; public disabled = false; + private selectedCopyLayerIds: string[] = []; + public ngOnInit(): void { this.store$.select(selectSelectedEditLayer) .pipe(takeUntilDestroyed(this.destroyRef)) @@ -86,6 +91,14 @@ export class EditComponent implements OnInit { } }); + ComponentConfigHelper.useInitialConfigForComponent( + this.store$, + BaseComponentTypeEnum.EDIT, + config => { + this.selectedCopyLayerIds = config.copyLayerIds || []; + }, + ); + combineLatest([ this.store$.select(selectSelectedEditLayer), this.store$.select(selectOrderedVisibleLayersWithServices), this.mapService.getMapViewDetails$() ]).pipe( @@ -93,7 +106,8 @@ export class EditComponent implements OnInit { ).subscribe(([ selectedEditLayerId, visibleLayers, mapViewDetails ]) => { const layers = selectedEditLayerId == null ? [] : visibleLayers.filter(layer => layer.id !== selectedEditLayerId - && ScaleHelper.isInScale(mapViewDetails.scale, layer.minScale, layer.maxScale)); + && ScaleHelper.isInScale(mapViewDetails.scale, layer.minScale, layer.maxScale) + && this.selectedCopyLayerIds.length == 0 || this.selectedCopyLayerIds.includes(layer.id)); this.layersToCreateNewFeaturesFrom.set(layers); }); } From c60f32f5ab406a8b537f193c68ab131d95d1a555 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Wed, 5 Nov 2025 15:49:56 +0100 Subject: [PATCH 12/13] Address review comments --- .../core/src/lib/components/edit/edit/edit.component.ts | 4 ++-- .../core/src/lib/components/edit/state/edit.reducer.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/projects/core/src/lib/components/edit/edit/edit.component.ts b/projects/core/src/lib/components/edit/edit/edit.component.ts index 33aeefd5a..7305310b4 100644 --- a/projects/core/src/lib/components/edit/edit/edit.component.ts +++ b/projects/core/src/lib/components/edit/edit/edit.component.ts @@ -104,7 +104,7 @@ export class EditComponent implements OnInit { this.mapService.getMapViewDetails$() ]).pipe( takeUntilDestroyed(this.destroyRef), ).subscribe(([ selectedEditLayerId, visibleLayers, mapViewDetails ]) => { - const layers = selectedEditLayerId == null ? [] : visibleLayers.filter(layer => + const layers = selectedEditLayerId === null ? [] : visibleLayers.filter(layer => layer.id !== selectedEditLayerId && ScaleHelper.isInScale(mapViewDetails.scale, layer.minScale, layer.maxScale) && this.selectedCopyLayerIds.length == 0 || this.selectedCopyLayerIds.includes(layer.id)); @@ -190,7 +190,7 @@ export class EditComponent implements OnInit { public createFeatureFromLayer(id: string) { this.selectedCopyLayer$.pipe(take(1)).subscribe(selectedCopyLayer => { - if (id == selectedCopyLayer) { + if (id === selectedCopyLayer) { this.store$.dispatch(setEditCopyOtherLayerFeaturesDisabled()); return; } diff --git a/projects/core/src/lib/components/edit/state/edit.reducer.ts b/projects/core/src/lib/components/edit/state/edit.reducer.ts index c6c716a40..bbd1bb1a4 100644 --- a/projects/core/src/lib/components/edit/state/edit.reducer.ts +++ b/projects/core/src/lib/components/edit/state/edit.reducer.ts @@ -80,12 +80,13 @@ const onLoadCopyFeaturesSuccess = ( return state; } - // Deselect a feature by checking if the geometry WKT is already in the copiedFeatures array (can't search by fid) + // Deselect a feature by checking if the geometry WKT is already in the copiedFeatures array. + // We can't deselect by checking the FID, because WMS GetFeatureInfo responses have a different FID every time (generated by us), and we + // want to have this work for WMS GFI responses as well. const sameGeometryIndex = state.copiedFeatures.findIndex(f => f.geometry === geometry); const copiedFeatures = sameGeometryIndex !== -1 - ? state.copiedFeatures.filter((_, idx) => idx !== sameGeometryIndex) + ? [ ...state.copiedFeatures.slice(0, sameGeometryIndex), ...state.copiedFeatures.slice(sameGeometryIndex + 1) ] : [ ...state.copiedFeatures, payload.featureInfo[0].features[0] ]; - return { ...state, copiedFeatures, From bed32e2bb3a4247ecfe52201489e178a0540c730 Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Thu, 6 Nov 2025 16:18:00 +0100 Subject: [PATCH 13/13] Merge fix --- .../src/lib/application/components/components.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/admin-core/src/lib/application/components/components.module.ts b/projects/admin-core/src/lib/application/components/components.module.ts index 9517c043d..7e47482b9 100644 --- a/projects/admin-core/src/lib/application/components/components.module.ts +++ b/projects/admin-core/src/lib/application/components/components.module.ts @@ -17,6 +17,7 @@ import { GeolocationConfigComponent } from './geolocation-config/geolocation-con import { InfoConfigComponent } from './info-config/info-config.component'; import { DrawingConfigComponent } from './drawing-config/drawing-config.component'; import { TocComponentConfigComponent } from './toc-config/toc-component-config.component'; +import { SharedAdminComponentsModule } from '../../shared/components'; @NgModule({ declarations: [ @@ -39,6 +40,7 @@ import { TocComponentConfigComponent } from './toc-config/toc-component-config.c BaseComponentConfigComponent, SelectUploadModule, MarkdownEditorComponent, + SharedAdminComponentsModule, ], exports: [ ComponentsListComponent,