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 }}
+
+
+
+
+ 0">
+
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äť",