diff --git a/packages/geo/src/lib/search/search-results/save-feature-dialog.component.html b/packages/geo/src/lib/search/search-results/save-feature-dialog.component.html new file mode 100644 index 0000000000..dade16b92c --- /dev/null +++ b/packages/geo/src/lib/search/search-results/save-feature-dialog.component.html @@ -0,0 +1,48 @@ +

{{'igo.geo.layer.saveFeatureInLayer' | translate}}

+ +
+
+ + + + + +
+ + + + +

{{layer.title}}

+
+
+
+
+
+
+ +
+
+ + +
+
\ No newline at end of file diff --git a/packages/geo/src/lib/search/search-results/save-feature-dialog.component.scss b/packages/geo/src/lib/search/search-results/save-feature-dialog.component.scss new file mode 100644 index 0000000000..e2feb76b12 --- /dev/null +++ b/packages/geo/src/lib/search/search-results/save-feature-dialog.component.scss @@ -0,0 +1,30 @@ +mat-form-field { + width: 100%; + } + + .create-layer-button-top-padding { + padding-top: 25px; + } + + .igo-form { + padding: 10px 5px 5px; + } + + .igo-form-button-group { + text-align: center; + } + + button { + cursor: pointer; + } + + button#createLayerBtnDialog[disabled=true] { + cursor: default; + background-color: rgba(0,0,0,.12); + color: rgba(0,0,0,.26); + } + + .error { + color: red; + } + \ No newline at end of file diff --git a/packages/geo/src/lib/search/search-results/save-feature-dialog.component.ts b/packages/geo/src/lib/search/search-results/save-feature-dialog.component.ts new file mode 100644 index 0000000000..f46462ebf7 --- /dev/null +++ b/packages/geo/src/lib/search/search-results/save-feature-dialog.component.ts @@ -0,0 +1,64 @@ +import { LanguageService } from '@igo2/core'; +import { Component, OnInit, Optional, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Layer } from '../../layer'; +import { SearchResult } from '../shared'; +import { Observable } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; + +@Component({ + selector: 'igo-save-feature-dialog', + templateUrl: './save-feature-dialog.component.html', + styleUrls: ['./save-feature-dialog.component.scss'] +}) +export class SaveFeatureDialogComponent implements OnInit { + + public form: UntypedFormGroup; + feature: SearchResult; + layers: Layer[] = []; + filteredLayers: Observable; + + constructor( + private formBuilder: UntypedFormBuilder, + public languageService: LanguageService, + public dialogRef: MatDialogRef, + @Optional() + @Inject(MAT_DIALOG_DATA) + public data: { feature: SearchResult; layers: Layer[]} + ) { + this.form = this.formBuilder.group({ + layerName: ['', [Validators.required]], + }); + } + + ngOnInit() { + this.feature = this.data.feature; + this.layers = this.data.layers; + this.filteredLayers = this.form.controls['layerName'].valueChanges.pipe( + startWith(''), + map(val => this.filter(val)) + ); + } + + private filter(val): Layer[] { + if(typeof val !== 'string'){ + return; + } + return this.layers.map(l => l).filter(layer => + layer?.title?.toLowerCase().includes(val.toLowerCase())); + } + + displayFn(layer: Layer): string { + return layer && layer.title ? layer.title: ''; + } + + save() { + const data: {layer: string | Layer, feature: SearchResult} = {layer: this.form.value.layerName, feature: this.feature}; + this.dialogRef.close(data); + } + + cancel() { + this.dialogRef.close(); + } +} diff --git a/packages/geo/src/lib/search/search-results/search-results-add-button.component.html b/packages/geo/src/lib/search/search-results/search-results-add-button.component.html index 429a0b7c2f..b7a0ed2c98 100644 --- a/packages/geo/src/lib/search/search-results/search-results-add-button.component.html +++ b/packages/geo/src/lib/search/search-results/search-results-add-button.component.html @@ -21,3 +21,23 @@ [svgIcon]="(isPreview$ | async) ? 'plus' : added ? 'delete' : 'plus'"> + + diff --git a/packages/geo/src/lib/search/search-results/search-results-add-button.component.ts b/packages/geo/src/lib/search/search-results/search-results-add-button.component.ts index 975f3d065a..3c0c403acb 100644 --- a/packages/geo/src/lib/search/search-results/search-results-add-button.component.ts +++ b/packages/geo/src/lib/search/search-results/search-results-add-button.component.ts @@ -3,7 +3,7 @@ import { Input, ChangeDetectionStrategy, OnInit, - OnDestroy + OnDestroy, } from '@angular/core'; import { SearchResult } from '../shared/search.interfaces'; @@ -11,7 +11,29 @@ import { IgoMap } from '../../map/shared/map'; import { LayerOptions } from '../../layer/shared/layers/layer.interface'; import { LayerService } from '../../layer/shared/layer.service'; import { LAYER } from '../../layer/shared/layer.enums'; -import { Subscription, BehaviorSubject } from 'rxjs'; +import { Subscription, BehaviorSubject, take } from 'rxjs'; +import { SaveFeatureDialogComponent } from './save-feature-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { VectorLayer } from '../../layer/shared/layers/vector-layer'; +import { DataSourceService, FeatureDataSource } from '../../datasource'; +import { + Feature, + FeatureMotion, + FeatureStore, + FeatureStoreLoadingStrategy, + FeatureStoreSelectionStrategy, + tryAddLoadingStrategy, + tryAddSelectionStrategy, + tryBindStoreLayer +} from '../../feature'; +import { EntityStore } from '@igo2/common'; +import { getTooltipsOfOlGeometry } from '../../measure'; +import OlOverlay from 'ol/Overlay'; +import { VectorSourceEvent as OlVectorSourceEvent } from 'ol/source/Vector'; +import { default as OlGeometry } from 'ol/geom/Geometry'; +import { QueryableDataSourceOptions } from '../../query'; +import { createOverlayDefaultStyle } from '../../overlay'; + @Component({ selector: 'igo-search-add-button', @@ -19,7 +41,14 @@ import { Subscription, BehaviorSubject } from 'rxjs'; styleUrls: ['./search-results-add-button.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class SearchResultAddButtonComponent implements OnInit, OnDestroy { +export class SearchResultAddButtonComponent implements OnInit, OnDestroy{ + public tooltip$: BehaviorSubject = new BehaviorSubject( + 'igo.geo.catalog.layer.addToMap' + ); + + public addFeatureToLayerTooltip$: BehaviorSubject = new BehaviorSubject( + 'igo.geo.search.addToLayer' + ); private resolution$$: Subscription; private layers$$: Subscription; @@ -38,6 +67,8 @@ export class SearchResultAddButtonComponent implements OnInit, OnDestroy { @Input() layer: SearchResult; + @Input() store: EntityStore; + /** * Whether the layer is already added to the map */ @@ -57,7 +88,18 @@ export class SearchResultAddButtonComponent implements OnInit, OnDestroy { } private _color = 'primary'; - constructor(private layerService: LayerService) {} + @Input() stores: FeatureStore[] = []; + + get allLayers() { + return this.map.layers.filter((layer) => + layer.id.includes('igo-search-layer') + ); + } + + constructor( + private layerService: LayerService, + private dialog: MatDialog, + private dataSourceService: DataSourceService) {} /** * @internal @@ -230,4 +272,138 @@ export class SearchResultAddButtonComponent implements OnInit, OnDestroy { : 'igo.geo.catalog.layer.addToMapOutRange'; } } + + addFeatureToLayer() { + if (this.layer.meta.dataType !== 'Feature') { + return; + } + + const selectedFeature = this.layer; + const dialogRef = this.dialog.open(SaveFeatureDialogComponent, { + width: '700px', + data: { + feature: selectedFeature, + layers: this.allLayers + } + }); + + dialogRef.afterClosed().subscribe((data: {layer: string | any, feature: SearchResult}) => { + if (data) { + if(this.stores.length > 0) { + this.stores.map((store) => { + store.state.updateAll({selected: false}); + (store?.layer).visible = false; + return store; + }); + } + // check if is new layer + if (typeof data.layer === 'string') { + this.createLayer(data.layer, data.feature); + } else { + const activeStore = this.stores.find(store => store.layer.id === data.layer.id); + activeStore.layer.visible = true; + activeStore.layer.opacity = 1; + this.addFeature(data.feature, activeStore); + } + } + }); + } + + createLayer(layerTitle: string, selectedFeature: SearchResult) { + + const activeStore: FeatureStore = new FeatureStore([], { + map: this.map + }); + + // set layer id + let layerCounterID: number = 0; + for (const layer of this.allLayers) { + let numberId = Number(layer.id.replace('igo-search-layer','')); + layerCounterID = Math.max(numberId,layerCounterID); + } + + this.dataSourceService + .createAsyncDataSource({ + type: 'vector', + queryable: true + } as QueryableDataSourceOptions) + .pipe(take(1)) + .subscribe((dataSource: FeatureDataSource) => { + let searchLayer: VectorLayer = new VectorLayer({ + isIgoInternalLayer: true, + id: 'igo-search-layer' + ++layerCounterID, + title: layerTitle, + source: dataSource, + style: createOverlayDefaultStyle({ + text: '', + strokeWidth: 1, + fillColor: 'rgba(255,255,255,0.4)', + strokeColor: 'rgba(143,7,7,1)' + }), + showInLayerList: true, + exportable: true, + workspace: { + enabled: true + } + }); + + tryBindStoreLayer(activeStore, searchLayer); + tryAddLoadingStrategy( + activeStore, + new FeatureStoreLoadingStrategy({ + motion: FeatureMotion.None + }) + ); + + tryAddSelectionStrategy( + activeStore, + new FeatureStoreSelectionStrategy({ + map: this.map, + motion: FeatureMotion.None, + many: true + }) + ); + + activeStore.layer.visible = true; + activeStore.source.ol.on( + 'removefeature', + (event: OlVectorSourceEvent) => { + const olGeometry = event.feature.getGeometry(); + this.clearLabelsOfOlGeometry(olGeometry); + } + ); + + this.addFeature(selectedFeature, activeStore); + this.stores.push(activeStore); + }); + } + + addFeature(feature: SearchResult, activeStore: FeatureStore) { + const newFeature = { + type: feature.data.type, + geometry: { + coordinates: feature.data.geometry.coordinates, + type: feature.data.geometry.type + }, + projection: feature.data.projection, + properties: feature.data.properties, + meta: { + id: feature.meta.id + } + }; + delete newFeature.properties.Route; + activeStore.update(newFeature); + activeStore.setLayerExtent(); + activeStore.layer.ol.getSource().refresh(); + } + + private clearLabelsOfOlGeometry(olGeometry) { + getTooltipsOfOlGeometry(olGeometry).forEach( + (olTooltip: OlOverlay | undefined) => { + if (olTooltip && olTooltip.getMap()) { + this.map.ol.removeOverlay(olTooltip); + } + } + ); + } } diff --git a/packages/geo/src/lib/search/search-results/search-results.component.ts b/packages/geo/src/lib/search/search-results/search-results.component.ts index d6b44349e1..ec30ed98cd 100644 --- a/packages/geo/src/lib/search/search-results/search-results.component.ts +++ b/packages/geo/src/lib/search/search-results/search-results.component.ts @@ -142,7 +142,7 @@ export class SearchResultsComponent implements OnInit, OnDestroy { constructor(private cdRef: ChangeDetectorRef, private searchService: SearchService, - private configService: ConfigService + private configService: ConfigService, ) {} /** diff --git a/packages/geo/src/lib/search/search-results/search-results.module.ts b/packages/geo/src/lib/search/search-results/search-results.module.ts index 56669d379d..529232716a 100644 --- a/packages/geo/src/lib/search/search-results/search-results.module.ts +++ b/packages/geo/src/lib/search/search-results/search-results.module.ts @@ -6,6 +6,12 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { IgoCollapsibleModule, @@ -19,6 +25,7 @@ import { IgoMetadataModule } from './../../metadata/metadata.module'; import { SearchResultsComponent } from './search-results.component'; import { SearchResultsItemComponent } from './search-results-item.component'; import { SearchResultAddButtonComponent } from './search-results-add-button.component'; +import { SaveFeatureDialogComponent } from './save-feature-dialog.component'; /** * @ignore @@ -31,12 +38,18 @@ import { SearchResultAddButtonComponent } from './search-results-add-button.comp MatIconModule, MatListModule, MatButtonModule, + MatDialogModule, IgoCollapsibleModule, IgoListModule, IgoStopPropagationModule, IgoLanguageModule, IgoMatBadgeIconModule, IgoMetadataModule, + MatFormFieldModule, + ReactiveFormsModule, + MatInputModule, + MatSelectModule, + MatAutocompleteModule, ], exports: [ SearchResultsComponent, @@ -45,7 +58,8 @@ import { SearchResultAddButtonComponent } from './search-results-add-button.comp declarations: [ SearchResultsComponent, SearchResultsItemComponent, - SearchResultAddButtonComponent + SearchResultAddButtonComponent, + SaveFeatureDialogComponent ] }) export class IgoSearchResultsModule {} diff --git a/packages/geo/src/locale/en.geo.json b/packages/geo/src/locale/en.geo.json index cead2cbf90..c8ea144be7 100644 --- a/packages/geo/src/locale/en.geo.json +++ b/packages/geo/src/locale/en.geo.json @@ -263,7 +263,11 @@ "style": { "styleModal": "Edit style", "styleModalTooltip": "Edit the style of the selected entities " - } + }, + "saveBtn": "Save", + "cancelBtn": "Cancel", + "saveFeatureInLayer": "Save selection in layer", + "chooseOrSet": "Choose a layer or set a new layer name" }, "download": { "action": "Download data", @@ -426,6 +430,7 @@ "layer.placeholder": "Search for a layer", "ichercheReverse.name": "Search by coordinates", "clearSearch": "Clear search", + "addToLayer": "Add to layer", "ilayer": { "name": "Layers", "properties": { diff --git a/packages/geo/src/locale/fr.geo.json b/packages/geo/src/locale/fr.geo.json index 6690654cd9..a29fb65115 100644 --- a/packages/geo/src/locale/fr.geo.json +++ b/packages/geo/src/locale/fr.geo.json @@ -262,7 +262,11 @@ "style": { "styleModal": "Modifier le style", "styleModalTooltip": "Éditer le style des entités sélectionnées" - } + }, + "saveBtn": "Sauvegarder", + "cancelBtn": "Cancel", + "saveFeatureInLayer": "Ajouter la sélection dans une couche", + "chooseOrSet": "Choisir une couche ou définir un nouveau nom" }, "download": { "action": "Télécharger les données associées", @@ -425,6 +429,7 @@ "layer.placeholder": "Rechercher une couche de données.", "ichercheReverse.name": "Recherche par coordonnées", "clearSearch": "Effacer la recherche", + "addToLayer": "Ajouter à une couche", "ilayer": { "name": "Couches", "properties": { diff --git a/packages/integration/src/lib/draw/draw.state.ts b/packages/integration/src/lib/draw/draw.state.ts index 1707a50d4b..2e54bf6fc6 100644 --- a/packages/integration/src/lib/draw/draw.state.ts +++ b/packages/integration/src/lib/draw/draw.state.ts @@ -11,6 +11,7 @@ import { MapState } from '../map/map.state'; }) export class DrawState { + public searchLayerStores: FeatureStore[] = []; public stores: FeatureStore[] = []; public layersID: string[] = []; public drawControls: [string, DrawControl][] = []; diff --git a/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.html b/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.html index 552195e186..3b9e7837d7 100644 --- a/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.html +++ b/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.html @@ -30,10 +30,13 @@

{{ 'igo.integration.searchResultsTool.noResults' | translate }} + (moreResults)="onSearch($event)" + [map]="map"> diff --git a/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.ts b/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.ts index 0aea1f580b..a073108dd6 100644 --- a/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.ts +++ b/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.ts @@ -41,7 +41,9 @@ import { getCommonVectorSelectedStyle, computeOlFeaturesExtent, featuresAreOutOfView, - roundCoordTo + roundCoordTo, + FeatureWithDraw, + FeatureStore } from '@igo2/geo'; import { MapState } from '../../map/map.state'; @@ -49,6 +51,7 @@ import { MapState } from '../../map/map.state'; import { SearchState } from '../search.state'; import { ToolState } from '../../tool/tool.state'; import { DirectionState } from '../../directions/directions.state'; +import { DrawState } from '../../draw'; /** * Tool to browse the search results @@ -148,13 +151,18 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { private format = new olFormatGeoJSON(); + get stores(): FeatureStore[] { + return this.drawState.searchLayerStores; + } + constructor( private mapState: MapState, private searchState: SearchState, private elRef: ElementRef, public toolState: ToolState, private directionState: DirectionState, - configService: ConfigService + configService: ConfigService, + private drawState: DrawState ) { this.hasFeatureEmphasisOnSelection = configService.getConfig( 'hasFeatureEmphasisOnSelection'