diff --git a/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.css b/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.html b/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.html new file mode 100644 index 0000000000..a022248950 --- /dev/null +++ b/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.html @@ -0,0 +1,37 @@ +
+ + +
+ +
+ {{ errorMessage }} +
+ +
+

map.loading.service

+
+ +
+

map.layers.available

+ +
+

+ {{ layer.title }} +

+ map.layer.add +
+
+
diff --git a/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.spec.ts b/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.spec.ts new file mode 100644 index 0000000000..e11cf446e9 --- /dev/null +++ b/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.spec.ts @@ -0,0 +1,165 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { AddLayerFromWfsComponent } from './add-layer-from-wfs.component' +import { MapFacade } from '../+state/map.facade' +import { TranslateModule } from '@ngx-translate/core' +import { By } from '@angular/platform-browser' + +jest.mock('@camptocamp/ogc-client', () => ({ + WfsEndpoint: class { + constructor(private url) {} + isReady() { + if (this.url.indexOf('error') > -1) { + return Promise.reject(new Error('Something went wrong')) + } + if (this.url.indexOf('wait') > -1) { + return new Promise(() => { + // do nothing + }) + } + return Promise.resolve(this) + } + getFeatureTypes() { + return [ + { + name: 'ft1', + title: 'Feature Type 1', + }, + { + name: 'ft2', + title: 'Feature Type 2', + }, + { + name: 'ft3', + title: 'Feature Type 3', + }, + ] + } + }, +})) + +class MapFacadeMock { + addLayer = jest.fn() +} + +describe('AddLayerFromWfsComponent', () => { + let component: AddLayerFromWfsComponent + let fixture: ComponentFixture + let mapFacade: MapFacade + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [AddLayerFromWfsComponent], + providers: [ + { + provide: MapFacade, + useClass: MapFacadeMock, + }, + ], + }).compileComponents() + + mapFacade = TestBed.inject(MapFacade) + fixture = TestBed.createComponent(AddLayerFromWfsComponent) + component = fixture.componentInstance + }) + + it('should create', () => { + fixture.detectChanges() + expect(component).toBeTruthy() + expect(component.errorMessage).toBeFalsy() + expect(component.loading).toBe(false) + expect(component.layers.length).toBe(0) + }) + + describe('loadLayers', () => { + describe('while layers are loading', () => { + beforeEach(() => { + component.wfsUrl = 'http://my.service.org/wait' + component.loadLayers() + }) + it('shows only a "loading" message', () => { + expect(component.errorMessage).toBeFalsy() + expect(component.loading).toBe(true) + expect(component.layers).toEqual([]) + }) + }) + describe('valid WFS service', () => { + beforeEach(() => { + component.wfsUrl = 'http://my.service.org/wfs' + component.loadLayers() + }) + it('shows all layers', () => { + expect(component.errorMessage).toBeFalsy() + expect(component.loading).toBe(false) + expect(component.layers).toEqual([ + { + name: 'ft1', + title: 'Feature Type 1', + }, + { + name: 'ft2', + title: 'Feature Type 2', + }, + { + name: 'ft3', + title: 'Feature Type 3', + }, + ]) + }) + it('should show a Add button for each layer', () => { + fixture.detectChanges() + const layerElts = fixture.debugElement.queryAll( + By.css('.layer-item-tree') + ) + expect(layerElts.length).toBe(3) + const hasButtons = layerElts.map( + (layerElt) => !!layerElt.query(By.css('.layer-add-btn')) + ) + expect(hasButtons).toEqual([true, true, true]) + }) + }) + describe('error loading layers', () => { + beforeEach(() => { + component.wfsUrl = 'http://my.service.org/error' + component.loadLayers() + }) + it('shows an error message', () => { + expect(component.errorMessage).toBeTruthy() + expect(component.loading).toBe(false) + expect(component.layers.length).toBe(0) + }) + }) + describe('error and then valid service', () => { + beforeEach(async () => { + component.wfsUrl = 'http://my.service.org/error' + await component.loadLayers().catch(() => { + // do nothing + }) + component.wfsUrl = 'http://my.service.org/wfs' + await component.loadLayers() + }) + it('shows no error', () => { + expect(component.errorMessage).toBeFalsy() + expect(component.loading).toBe(false) + expect(component.layers).not.toEqual([]) + }) + }) + }) + describe('addLayer', () => { + beforeEach(() => { + component.wfsUrl = 'http://my.service.org/wfs' + component.addLayer({ + name: 'ft1', + title: 'Feature Type 1', + }) + }) + it('should add the selected layer in the current map context', () => { + expect(mapFacade.addLayer).toHaveBeenCalledWith({ + name: 'ft1', + title: 'Feature Type 1', + url: 'http://my.service.org/wfs', + type: 'wfs', + }) + }) + }) +}) diff --git a/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.ts b/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.ts new file mode 100644 index 0000000000..a3ec798089 --- /dev/null +++ b/libs/feature/map/src/lib/add-layer-from-wfs/add-layer-from-wfs.component.ts @@ -0,0 +1,64 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core' +import { WfsEndpoint, WfsFeatureTypeBrief } from '@camptocamp/ogc-client' +import { Subject } from 'rxjs' +import { + MapContextLayerModel, + MapContextLayerTypeEnum, +} from '../map-context/map-context.model' +import { MapFacade } from '../+state/map.facade' +import { debounceTime } from 'rxjs/operators' + +@Component({ + selector: 'gn-ui-add-layer-from-wfs', + templateUrl: './add-layer-from-wfs.component.html', + styleUrls: ['./add-layer-from-wfs.component.css'], +}) +export class AddLayerFromWfsComponent implements OnInit { + wfsUrl = '' + loading = false + layers: WfsFeatureTypeBrief[] = [] + wfsEndpoint: WfsEndpoint | null = null + urlChange = new Subject() + errorMessage: string | null = null + + constructor( + private mapFacade: MapFacade, + private changeDetectorRef: ChangeDetectorRef + ) {} + + ngOnInit() { + this.urlChange.pipe(debounceTime(700)).subscribe(() => this.loadLayers()) + } + + async loadLayers() { + this.errorMessage = null + try { + this.loading = true + + if (this.wfsUrl.trim() === '') { + this.layers = [] + return + } + + this.wfsEndpoint = await new WfsEndpoint(this.wfsUrl).isReady() + this.layers = this.wfsEndpoint.getFeatureTypes() + console.log(this.layers) + } catch (error) { + const err = error as Error + this.layers = [] + this.errorMessage = 'Error loading layers: ' + err.message + } finally { + this.loading = false + this.changeDetectorRef.markForCheck() + } + } + + addLayer(layer: WfsFeatureTypeBrief) { + const layerToAdd: MapContextLayerModel = { + name: layer.name, + url: this.wfsUrl.toString(), + type: MapContextLayerTypeEnum.WFS, + } + this.mapFacade.addLayer({ ...layerToAdd, title: layer.title }) + } +} diff --git a/libs/feature/map/src/lib/feature-map.module.ts b/libs/feature/map/src/lib/feature-map.module.ts index facd91d161..8945b5e4c4 100644 --- a/libs/feature/map/src/lib/feature-map.module.ts +++ b/libs/feature/map/src/lib/feature-map.module.ts @@ -21,6 +21,7 @@ import { AddLayerRecordPreviewComponent } from './add-layer-from-catalog/add-lay import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { AddLayerFromWmsComponent } from './add-layer-from-wms/add-layer-from-wms.component' +import { AddLayerFromWfsComponent } from './add-layer-from-wfs/add-layer-from-wfs.component' @NgModule({ declarations: [ @@ -31,6 +32,7 @@ import { AddLayerFromWmsComponent } from './add-layer-from-wms/add-layer-from-wm MapContainerComponent, AddLayerRecordPreviewComponent, AddLayerFromWmsComponent, + AddLayerFromWfsComponent, ], exports: [ MapContextComponent, diff --git a/libs/feature/map/src/lib/layers-panel/layers-panel.component.html b/libs/feature/map/src/lib/layers-panel/layers-panel.component.html index 61454e4e23..d85dd8b330 100644 --- a/libs/feature/map/src/lib/layers-panel/layers-panel.component.html +++ b/libs/feature/map/src/lib/layers-panel/layers-panel.component.html @@ -36,7 +36,9 @@ -
Add from WFS
+
+ +
Add from file
diff --git a/libs/feature/map/src/lib/map-context/map-context.service.spec.ts b/libs/feature/map/src/lib/map-context/map-context.service.spec.ts index 0bc5494d30..8862473d28 100644 --- a/libs/feature/map/src/lib/map-context/map-context.service.spec.ts +++ b/libs/feature/map/src/lib/map-context/map-context.service.spec.ts @@ -137,7 +137,7 @@ describe('MapContextService', () => { const source = layer.getSource() const urlLoader = source.getUrl() expect(urlLoader([10, 20, 30, 40])).toBe( - 'https://www.geograndest.fr/geoserver/region-grand-est/ows?service=WFS&version=1.1.0&request=GetFeature&outputFormat=application%2Fjson&typename=ms%3Acommune_actuelle_3857&srsname=EPSG%3A3857&bbox=10%2C20%2C30%2C40%2CEPSG%3A3857' + 'https://www.geograndest.fr/geoserver/region-grand-est/ows?service=WFS&version=1.1.0&request=GetFeature&outputFormat=application%2Fjson&typename=ms%3Acommune_actuelle_3857&srsname=EPSG%3A3857&bbox=10%2C20%2C30%2C40%2CEPSG%3A3857&maxFeatures=10000' ) }) }) diff --git a/libs/feature/map/src/lib/map-context/map-context.service.ts b/libs/feature/map/src/lib/map-context/map-context.service.ts index b717585757..8f8c316c31 100644 --- a/libs/feature/map/src/lib/map-context/map-context.service.ts +++ b/libs/feature/map/src/lib/map-context/map-context.service.ts @@ -40,6 +40,8 @@ export const DEFAULT_VIEW: MapContextViewModel = { zoom: 2, } +export const WFS_MAX_FEATURES = 10000 + @Injectable({ providedIn: 'root', }) @@ -111,6 +113,10 @@ export class MapContextService { urlObj.searchParams.set('typename', layerModel.name) urlObj.searchParams.set('srsname', 'EPSG:3857') urlObj.searchParams.set('bbox', `${extent.join(',')},EPSG:3857`) + urlObj.searchParams.set( + 'maxFeatures', + WFS_MAX_FEATURES.toString() + ) return urlObj.toString() }, strategy: bboxStrategy, diff --git a/translations/de.json b/translations/de.json index 7d6c9899f6..4c0982ba5a 100644 --- a/translations/de.json +++ b/translations/de.json @@ -183,6 +183,7 @@ "map.loading.service": "", "map.navigation.message": "Bitte verwenden Sie STRG + Maus (oder zwei Finger auf einem Mobilgerät), um die Karte zu navigieren", "map.select.layer": "Datenquelle", + "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", "multiselect.filter.placeholder": "Suche", "nav.back": "Zurück", diff --git a/translations/en.json b/translations/en.json index 976bb6cc8a..6b93f4c6ac 100644 --- a/translations/en.json +++ b/translations/en.json @@ -183,6 +183,7 @@ "map.loading.service": "Loading service...", "map.navigation.message": "Please use CTRL + mouse (or two fingers on mobile) to navigate the map", "map.select.layer": "Data source", + "map.wfs.urlInput.hint": "Enter WFS service URL", "map.wms.urlInput.hint": "Enter WMS service URL", "multiselect.filter.placeholder": "Search", "nav.back": "Back", diff --git a/translations/es.json b/translations/es.json index e379e5ae57..97736afc48 100644 --- a/translations/es.json +++ b/translations/es.json @@ -183,6 +183,7 @@ "map.loading.service": "", "map.navigation.message": "", "map.select.layer": "", + "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", "multiselect.filter.placeholder": "", "nav.back": "", diff --git a/translations/fr.json b/translations/fr.json index 993c73bd1f..1e1476f587 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -183,6 +183,7 @@ "map.loading.service": "", "map.navigation.message": "Veuillez utiliser CTRL + souris (ou deux doigts sur mobile) pour naviguer sur la carte", "map.select.layer": "Source de données", + "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", "multiselect.filter.placeholder": "Rechercher", "nav.back": "Retour", diff --git a/translations/it.json b/translations/it.json index d35a909296..15716290b0 100644 --- a/translations/it.json +++ b/translations/it.json @@ -183,6 +183,7 @@ "map.loading.service": "", "map.navigation.message": "Si prega di utilizzare CTRL + mouse (o due dita su mobile) per navigare sulla mappa", "map.select.layer": "Sorgente dati", + "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", "multiselect.filter.placeholder": "Cerca", "nav.back": "Indietro", diff --git a/translations/nl.json b/translations/nl.json index 6c51ad7e03..043643ae7d 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -183,6 +183,7 @@ "map.loading.service": "", "map.navigation.message": "", "map.select.layer": "", + "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", "multiselect.filter.placeholder": "", "nav.back": "", diff --git a/translations/pt.json b/translations/pt.json index a625d6eb14..e0bedb86ee 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -183,6 +183,7 @@ "map.loading.service": "", "map.navigation.message": "", "map.select.layer": "", + "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", "multiselect.filter.placeholder": "", "nav.back": "", diff --git a/translations/sk.json b/translations/sk.json index 1124148783..637d467975 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -183,6 +183,7 @@ "map.loading.service": "", "map.navigation.message": "Použite prosím CTRL + myš (alebo dva prsty na mobilnom zariadení) na navigáciu po mape", "map.select.layer": "Zdroj dát", + "map.wfs.urlInput.hint": "", "map.wms.urlInput.hint": "", "multiselect.filter.placeholder": "Hľadať", "nav.back": "Späť",