Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Map-Viewer] Add layer from WFS service #776

Merged
merged 2 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div class="flex items-center mb-5">
<gn-ui-text-input
[(value)]="wfsUrl"
(valueChange)="urlChange.next($event)"
[hint]="'map.wfs.urlInput.hint' | translate"
class="w-96"
>
</gn-ui-text-input>
</div>

<div *ngIf="errorMessage" class="text-red-500 mt-2">
{{ errorMessage }}
</div>

<div *ngIf="loading">
<p class="loading-message" translate>map.loading.service</p>
</div>

<div *ngIf="!loading && layers.length > 0">
<h2 class="font-bold" translate>map.layers.available</h2>
<ng-container *ngFor="let layer of layers">
<div class="flex items-center justify-between my-2 layer-item-tree">
<p class="max-w-xs overflow-hidden overflow-ellipsis whitespace-nowrap">
{{ layer.title }}
</p>
<gn-ui-button
*ngIf="layer.name"
class="layer-add-btn"
type="primary"
(buttonClick)="addLayer(layer)"
extraClass="text-sm !px-2 !py-1"
translate
><span translate> map.layer.add </span></gn-ui-button
>
</div>
</ng-container>
</div>
Original file line number Diff line number Diff line change
@@ -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<AddLayerFromWfsComponent>
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',
})
})
})
})
Original file line number Diff line number Diff line change
@@ -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<string>()
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line can be removed I think 🙂 did we forget a console.log in WMS as well?

} 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 })
}
}
2 changes: 2 additions & 0 deletions libs/feature/map/src/lib/feature-map.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -31,6 +32,7 @@ import { AddLayerFromWmsComponent } from './add-layer-from-wms/add-layer-from-wm
MapContainerComponent,
AddLayerRecordPreviewComponent,
AddLayerFromWmsComponent,
AddLayerFromWfsComponent,
],
exports: [
MapContextComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
</div>
</mat-tab>
<mat-tab [label]="'map.add.layer.wfs' | translate" bodyClass="h-full">
<div class="p-3 h-full">Add from WFS</div>
<div class="p-3">
<gn-ui-add-layer-from-wfs></gn-ui-add-layer-from-wfs>
</div>
</mat-tab>
<mat-tab [label]="'map.add.layer.file' | translate" bodyClass="h-full">
<div class="p-3 h-full">Add from file</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)
})
})
Expand Down
6 changes: 6 additions & 0 deletions libs/feature/map/src/lib/map-context/map-context.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const DEFAULT_VIEW: MapContextViewModel = {
zoom: 2,
}

export const WFS_MAX_FEATURES = 10000

@Injectable({
providedIn: 'root',
})
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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",
Expand Down
1 change: 1 addition & 0 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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",
Expand Down
1 change: 1 addition & 0 deletions translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"map.loading.service": "",
"map.navigation.message": "",
"map.select.layer": "",
"map.wfs.urlInput.hint": "",
"map.wms.urlInput.hint": "",
"multiselect.filter.placeholder": "",
"nav.back": "",
Expand Down
1 change: 1 addition & 0 deletions translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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",
Expand Down
1 change: 1 addition & 0 deletions translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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",
Expand Down
1 change: 1 addition & 0 deletions translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"map.loading.service": "",
"map.navigation.message": "",
"map.select.layer": "",
"map.wfs.urlInput.hint": "",
"map.wms.urlInput.hint": "",
"multiselect.filter.placeholder": "",
"nav.back": "",
Expand Down
1 change: 1 addition & 0 deletions translations/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"map.loading.service": "",
"map.navigation.message": "",
"map.select.layer": "",
"map.wfs.urlInput.hint": "",
"map.wms.urlInput.hint": "",
"multiselect.filter.placeholder": "",
"nav.back": "",
Expand Down
1 change: 1 addition & 0 deletions translations/sk.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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äť",
Expand Down
Loading