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 b6b394e0d..baf303b50 100644 --- a/projects/admin-core/assets/locale/messages.admin-core.de.xlf +++ b/projects/admin-core/assets/locale/messages.admin-core.de.xlf @@ -150,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 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 7788a2edd..db3b19a64 100644 --- a/projects/admin-core/assets/locale/messages.admin-core.en.xlf +++ b/projects/admin-core/assets/locale/messages.admin-core.en.xlf @@ -113,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 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 ded9ff2ba..a7dfa55f4 100644 --- a/projects/admin-core/assets/locale/messages.admin-core.nl.xlf +++ b/projects/admin-core/assets/locale/messages.admin-core.nl.xlf @@ -150,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 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 20dd2e3fb..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, @@ -50,7 +52,7 @@ export class ComponentsModule { const configurationComponentService = inject(ConfigurationComponentRegistryService); /* eslint-disable max-len */ - configurationComponentService.registerConfigurationComponents(BaseComponentTypeEnum.TOC, $localize `:@@admin-core.application.component-table-of-contents:Table of contents`, TocComponentConfigComponent); + configurationComponentService.registerConfigurationComponents(BaseComponentTypeEnum.TOC, $localize `:@@admin-core.application.component-table-of-contents:Table of contents`, BaseComponentConfigComponent); configurationComponentService.registerConfigurationComponents(BaseComponentTypeEnum.LEGEND, $localize `:@@admin-core.application.component-legend:Legend`, BaseComponentConfigComponent); configurationComponentService.registerConfigurationComponents(BaseComponentTypeEnum.DRAWING, $localize `:@@admin-core.application.component-drawing:Drawing`, DrawingConfigComponent); configurationComponentService.registerConfigurationComponents(BaseComponentTypeEnum.PRINT, $localize `:@@admin-core.application.component-print:Print`, BaseComponentConfigComponent); 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[]; } diff --git a/projects/core/assets/locale/messages.core.de.xlf b/projects/core/assets/locale/messages.core.de.xlf index ae107d8ee..7b662bd14 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 d1d4be459..76dac75b3 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 c8095d022..17f873988 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 @@ -472,11 +468,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-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.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-dialog/edit-dialog.component.ts b/projects/core/src/lib/components/edit/edit-dialog/edit-dialog.component.ts index 636a0bbec..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), @@ -96,12 +101,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..a5f3e43c3 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); } @@ -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 { @@ -31,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 ee6f5cbe2..dd935d27f 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) { @@ -28,38 +27,65 @@ }
- } - @if (!isLine() && !isPoint()) { - - - @if (isPolygon()) { - - - +
+ @if (!isLine() && !isPoint()) { + + + @if (isPolygon()) { + + + + } @else { + + + + + + } + } @else { - - - - - + + } + @if (layersToCreateNewFeaturesFrom().length > 0) { + + + @let selectedCopyLayerId = selectedCopyLayer$ | async; + @for (layer of layersToCreateNewFeaturesFrom(); track layer) { + @let selected = layer.id === selectedCopyLayerId; + + } + } - - } @else { - +
}
} 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 41140898e..7305310b4 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,28 @@ -import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject } from '@angular/core'; -import { selectEditActive, selectSelectedEditLayer } from '../state/edit.selectors'; +import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject, signal } from '@angular/core'; +import { + selectCopiedFeatures, + selectEditActive, selectEditCopyOtherLayerFeaturesActive, selectEditCreateNewFeatureActive, selectSelectedCopyLayer, + 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 { combineLatest, map, of, take } from 'rxjs'; +import { + setEditActive, setEditCopyOtherLayerFeaturesActive, setEditCopyOtherLayerFeaturesDisabled, 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, BaseComponentTypeEnum, EditConfigModel, 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'; +import { ComponentConfigHelper } from '../../../shared'; @Component({ selector: 'tm-edit', @@ -26,20 +36,28 @@ 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 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; - private defaultTooltip = $localize `:@@core.edit.edit-feature-tooltip:Edit feature`; + public layersToCreateNewFeaturesFrom = signal([]); + + 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.`; public tooltip = this.defaultTooltip; public disabled = false; + private selectedCopyLayerIds: string[] = []; + public ngOnInit(): void { this.store$.select(selectSelectedEditLayer) .pipe(takeUntilDestroyed(this.destroyRef)) @@ -72,6 +90,26 @@ export class EditComponent implements OnInit { this.toggle(true); } }); + + ComponentConfigHelper.useInitialConfigForComponent( + this.store$, + BaseComponentTypeEnum.EDIT, + config => { + this.selectedCopyLayerIds = config.copyLayerIds || []; + }, + ); + + 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.selectedCopyLayerIds.length == 0 || this.selectedCopyLayerIds.includes(layer.id)); + this.layersToCreateNewFeaturesFrom.set(layers); + }); } public isLine() { @@ -122,14 +160,44 @@ 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, + + 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() { + if (this.isPoint()) { + this.createFeature('point'); + } + if (this.isLine()) { + this.createFeature('line'); + } + } + + public createFeatureFromLayer(id: string) { + + 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({ + layerId: id, columnMetadata: layerDetails.details.attributes.map(attribute => { return { layerId: layerDetails.details.id, @@ -141,15 +209,6 @@ export class EditComponent implements OnInit { ), })); }); + }); } - - public createFeatureIfSingleGeometryType() { - if (this.isPoint()) { - this.createFeature('point'); - } - if (this.isLine()) { - this.createFeature('line'); - } - } - } 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..333869b92 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,14 @@ export const setEditCreateNewFeatureActive = createAction( props<{ active: boolean; geometryType: DrawingType; columnMetadata: FeatureInfoColumnMetadataModel[] }>(), ); +export const setEditCopyOtherLayerFeaturesActive = createAction( + `${editActionsPrefix} Set Copy Other Layer Features Active`, + 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 }>(), @@ -24,7 +32,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 +50,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..de2a01dfc 100644 --- a/projects/core/src/lib/components/edit/state/edit.effects.ts +++ b/projects/core/src/lib/components/edit/state/edit.effects.ts @@ -2,9 +2,9 @@ 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 { 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'; @@ -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(selectSelectedCopyLayer)), + switchMap(([ action, copyLayer ]) => { + return this.featureInfoService.getFeaturesForLayer$(action.coordinates, copyLayer, 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..bbd1bb1a4 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, @@ -27,6 +28,7 @@ const onSetSelectedLayer = ( dialogCollapsed: false, selectedFeature: null, isCreateNewFeatureActive: false, + ...initialEditCopyState, }); const onLoadFeatureInfo = ( @@ -37,7 +39,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 +67,63 @@ 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. + // 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.slice(0, sameGeometryIndex), ...state.copiedFeatures.slice(sameGeometryIndex + 1) ] + : [ ...state.copiedFeatures, payload.featureInfo[0].features[0] ]; + return { + ...state, + copiedFeatures, + }; +}; + +const onSetCopyOtherLayerFeaturesActive = ( + state: EditState, + payload: ReturnType, +): EditState => ({ + ...state, + isCopyOtherLayerFeaturesActive: true, + isCreateNewFeatureActive: false, + dialogVisible: true, + dialogCollapsed: false, + selectedCopyLayer: payload.layerId, + 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: {}, + }], + loadStatus: LoadingStateEnum.INITIAL, +}); + +const onSetCopyOtherLayerFeaturesDisabled = ( + state: EditState, +): EditState => ({ + ...state, + isCreateNewFeatureActive: false, + dialogVisible: false, + dialogCollapsed: true, + ...initialEditCopyState, +}); + const onSetCreateNewFeatureActive = ( state: EditState, payload: ReturnType, @@ -72,6 +139,7 @@ const onSetCreateNewFeatureActive = ( return { ...state, isCreateNewFeatureActive: payload.active, + ...initialEditCopyState, newGeometryType: payload.geometryType, dialogVisible: payload.active, selectedFeature: 'new', @@ -98,6 +166,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, @@ -135,6 +212,8 @@ const onHideEditDialog = (state: EditState): EditState => ({ dialogCollapsed: false, selectedFeature: null, isCreateNewFeatureActive: false, + features: [], + ...initialEditCopyState, }); const onExpandCollapseEditDialog = (state: EditState): EditState => ({ @@ -167,21 +246,26 @@ const onEditNewlyCreatedFeature = ( ): EditState => { return { ...state, + ...initialEditCopyState, features: [payload.feature], selectedFeature: payload.feature.__fid, isCreateNewFeatureActive: false, }; - }; 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), + 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..5039550f4 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,16 @@ 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, 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.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); export const selectEditLoadStatus = createSelector(selectEditState, state => state.loadStatus); export const selectEditErrorMessage = createSelector(selectEditState, state => state.errorMessage); @@ -34,12 +41,16 @@ export const selectEditActiveWithSelectedLayer = createSelector( export const selectEditStatus = createSelector( selectEditActiveWithSelectedLayer, + selectEditCopyOtherLayerFeaturesActive, selectEditSelectedFeature, - selectEditCreateNewFeatureActive, - (editWithLayerActive, editSelectedFeatureActive, editCreateNewFeatureActive) => { + selectEditCreateNewOrCopyFeatureActive, + (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..c16ad285e 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; @@ -21,8 +24,15 @@ export interface EditState { openedFromFeatureInfo?: boolean; } +export const initialEditCopyState = { + isCopyOtherLayerFeaturesActive: false, + selectedCopyLayer: null, + copiedFeatures: [], +}; + export const initialEditState: EditState = { isActive: false, + ...initialEditCopyState, isCreateNewFeatureActive: false, newGeometryType: null, selectedLayer: null, 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), 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; 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'); + } }