Skip to content

Commit

Permalink
Merge pull request #848 from geonetwork/mv-add-from-ogc-api
Browse files Browse the repository at this point in the history
[Map-Viewer] Add layer from OGC API service
  • Loading branch information
ronitjadhav authored Apr 16, 2024
2 parents 376e0e9 + a3e1040 commit 6e2b8be
Show file tree
Hide file tree
Showing 33 changed files with 445 additions and 428 deletions.
1 change: 0 additions & 1 deletion docs/guide/custom-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ the following settings to the `angular.json` file at the root of your project:
"@rgrove/parse-xml",
"@messageformat/core",
"rbush",
"@camptocamp/ogc-client",
"pbf",
"alasql"
// add dependencies here if other warnings show up and you want to hide them
Expand Down
2 changes: 1 addition & 1 deletion jest.preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = {
coverageReporters: ['text'],
setupFiles: ['jest-canvas-mock'],
transformIgnorePatterns: [
'node_modules/(?!(color-*|ol|@mapbox|@geospatial-sdk|.*.mjs$))',
'node_modules/(?!(color-*|ol|@mapbox|@geospatial-sdk|@camptocamp/ogc-client|.*.mjs$))',
],
transform: {
'^.+\\.(ts|mjs|js|html)$': [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ export class AddLayerRecordPreviewComponent extends RecordPreviewComponent {
name: link.name,
})
} else if (link.accessServiceProtocol === 'wmts') {
return this.mapUtils.getWmtsLayerFromCapabilities(link)
return of({
url: link.url.toString(),
type: MapContextLayerTypeEnum.WMTS,
name: link.name,
})
}
return throwError(() => 'protocol not supported')
}
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class="flex items-center mb-5">
<gn-ui-text-input
[(value)]="ogcUrl"
(valueChange)="urlChange.next($event)"
[hint]="'map.ogc.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 }}
</p>
<gn-ui-button
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,82 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { AddLayerFromOgcApiComponent } from './add-layer-from-ogc-api.component'
import { MapFacade } from '../+state/map.facade'
import { TranslateModule } from '@ngx-translate/core'
import { NO_ERRORS_SCHEMA } from '@angular/core'
import { MapContextLayerTypeEnum } from '../map-context/map-context.model'

jest.mock('@camptocamp/ogc-client', () => ({
OgcApiEndpoint: 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)
}
get featureCollections() {
if (this.url.includes('error')) {
return Promise.reject(new Error('Simulated loading error'))
}
return Promise.resolve(['layer1', 'layer2', 'layer3'])
}
getCollectionItemsUrl(collectionId) {
return Promise.resolve(
`http://example.com/collections/${collectionId}/items`
)
}
},
}))

describe('AddLayerFromOgcApiComponent', () => {
let component: AddLayerFromOgcApiComponent
let fixture: ComponentFixture<AddLayerFromOgcApiComponent>

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), AddLayerFromOgcApiComponent],
declarations: [],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents()

fixture = TestBed.createComponent(AddLayerFromOgcApiComponent)
component = fixture.componentInstance
fixture.detectChanges()
})

it('should create', () => {
expect(component).toBeTruthy()
expect(component.errorMessage).toBeFalsy()
expect(component.loading).toBe(false)
expect(component.layers.length).toBe(0)
})

describe('loadLayers', () => {
it('should clear layers if OGC URL is empty', async () => {
component.ogcUrl = ''
await component.loadLayers()
expect(component.layers.length).toBe(0)
})

it('should load layers on valid OGC API service', async () => {
component.ogcUrl = 'http://example.com/ogc'
await component.loadLayers()
expect(component.errorMessage).toBeFalsy()
expect(component.loading).toBe(false)
expect(component.layers).toEqual(['layer1', 'layer2', 'layer3'])
})

it('should handle errors while loading layers', async () => {
component.ogcUrl = 'http://example.com/error'
await component.loadLayers()
expect(component.errorMessage).toContain('Error loading layers:')
expect(component.loading).toBe(false)
expect(component.layers.length).toBe(0)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
Component,
OnInit,
Output,
EventEmitter,
ChangeDetectionStrategy,
Input,
ChangeDetectorRef,
} from '@angular/core'
import { OgcApiEndpoint } from '@camptocamp/ogc-client'
import { Subject, debounceTime } from 'rxjs'
import {
MapContextLayerModel,
MapContextLayerTypeEnum,
} from '../map-context/map-context.model'
import { TranslateModule } from '@ngx-translate/core'
import { UiInputsModule } from '@geonetwork-ui/ui/inputs'
import { CommonModule } from '@angular/common'
import { MapLayer } from '../+state/map.models'

@Component({
selector: 'gn-ui-add-layer-from-ogc-api',
templateUrl: './add-layer-from-ogc-api.component.html',
styleUrls: ['./add-layer-from-ogc-api.component.css'],
standalone: true,
imports: [CommonModule, TranslateModule, UiInputsModule],
})
export class AddLayerFromOgcApiComponent implements OnInit {
@Input() ogcUrl: string
@Output() layerAdded = new EventEmitter<MapLayer>()

urlChange = new Subject<string>()
layerUrl = ''
loading = false
layers: string[] = []
ogcEndpoint: OgcApiEndpoint = null
errorMessage: string | null = null

constructor(private changeDetectorRef: ChangeDetectorRef) {}

ngOnInit() {
this.urlChange.pipe(debounceTime(700)).subscribe(() => {
this.loadLayers()
this.changeDetectorRef.detectChanges() // manually trigger change detection
})
}

async loadLayers() {
this.errorMessage = null
try {
this.loading = true
if (this.ogcUrl.trim() === '') {
this.layers = []
return
}
this.ogcEndpoint = await new OgcApiEndpoint(this.ogcUrl)

// Currently only supports feature collections
this.layers = await this.ogcEndpoint.featureCollections
} catch (error) {
const err = error as Error
this.layers = []
this.errorMessage = 'Error loading layers: ' + err.message
} finally {
this.loading = false
this.changeDetectorRef.markForCheck()
}
}

async addLayer(layer: string) {
this.layerUrl = await this.ogcEndpoint.getCollectionItemsUrl(layer)

const layerToAdd: MapContextLayerModel = {
name: layer,
url: this.layerUrl,
type: MapContextLayerTypeEnum.OGCAPI,
}
this.layerAdded.emit({ ...layerToAdd, title: layer })
}
}
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 @@ -25,6 +25,7 @@ import { AddLayerFromFileComponent } from './add-layer-from-file/add-layer-from-
import { AddLayerFromWfsComponent } from './add-layer-from-wfs/add-layer-from-wfs.component'
import { GeocodingComponent } from './geocoding/geocoding.component'
import { GEOCODING_PROVIDER, GeocodingProvider } from './geocoding.service'
import { AddLayerFromOgcApiComponent } from './add-layer-from-ogc-api/add-layer-from-ogc-api.component'

@NgModule({
declarations: [
Expand Down Expand Up @@ -59,6 +60,7 @@ import { GEOCODING_PROVIDER, GeocodingProvider } from './geocoding.service'
EffectsModule.forFeature([MapEffects]),
UiElementsModule,
UiInputsModule,
AddLayerFromOgcApiComponent,
],
providers: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@
<gn-ui-add-layer-from-wfs></gn-ui-add-layer-from-wfs>
</div>
</mat-tab>
<mat-tab [label]="'map.add.layer.ogc.api' | translate" bodyClass="h-full">
<div class="p-3">
<gn-ui-add-layer-from-ogc-api
[ogcUrl]="ogcUrl"
(layerAdded)="addLayer($event)"
></gn-ui-add-layer-from-ogc-api>
</div>
</mat-tab>
<mat-tab [label]="'map.add.layer.file' | translate" bodyClass="h-full">
<div class="p-3">
<gn-ui-add-layer-from-file></gn-ui-add-layer-from-file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ import { MapFacade } from '../+state/map.facade'
})
export class LayersPanelComponent {
layers$ = this.mapFacade.layers$
ogcUrl = ''
constructor(private mapFacade: MapFacade) {}

deleteLayer(index: number) {
this.mapFacade.removeLayer(index)
}

addLayer(layer) {
this.mapFacade.addLayer(layer)
}
}
13 changes: 10 additions & 3 deletions libs/feature/map/src/lib/map-context/map-context.model.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { FeatureCollection } from 'geojson'
import { Coordinate } from 'ol/coordinate'
import type { Extent } from 'ol/extent'
import { Options } from 'ol/source/WMTS'

export enum MapContextLayerTypeEnum {
XYZ = 'xyz',
WMS = 'wms',
WMTS = 'wmts',
WFS = 'wfs',
GEOJSON = 'geojson',
OGCAPI = 'ogcapi',
}

export interface MapContextModel {
Expand All @@ -24,8 +24,8 @@ export interface MapContextLayerWmsModel {

export interface MapContextLayerWmtsModel {
type: 'wmts'
options: Options
extent?: Extent
url: string
name: string
}

interface MapContextLayerWfsModel {
Expand All @@ -34,6 +34,12 @@ interface MapContextLayerWfsModel {
name: string
}

export interface MapContextLayerOgcapiModel {
type: 'ogcapi'
url: string
name: string
}

interface LayerXyzModel {
type: 'xyz'
name?: string
Expand Down Expand Up @@ -71,6 +77,7 @@ export type MapContextLayerModel =
| MapContextLayerWfsModel
| MapContextLayerXyzModel
| MapContextLayerGeojsonModel
| MapContextLayerOgcapiModel

export interface MapContextViewModel {
center?: Coordinate // expressed in long/lat (EPSG:4326)
Expand Down
46 changes: 44 additions & 2 deletions libs/feature/map/src/lib/map-context/map-context.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,48 @@ const mapStyleServiceMock = {
defaultHL: DEFAULT_STYLE_HL_FIXTURE,
},
}

jest.mock('@camptocamp/ogc-client', () => ({
WmtsEndpoint: class {
constructor(private url) {}
isReady() {
return Promise.resolve({
getLayerByName: (name) => {
if (this.url.indexOf('error') > -1) {
throw new Error('Something went wrong')
}
return {
name,
latLonBoundingBox: [1.33, 48.81, 4.3, 51.1],
}
},
})
}
},
WfsEndpoint: class {
constructor(private url) {}
isReady() {
return Promise.resolve({
getLayerByName: (name) => {
if (this.url.indexOf('error') > -1) {
throw new Error('Something went wrong')
}
return {
name,
latLonBoundingBox: [1.33, 48.81, 4.3, 51.1],
}
},
getSingleFeatureTypeName: () => {
return 'ms:commune_actuelle_3857'
},
getFeatureUrl: () => {
return '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'
},
})
}
},
}))

describe('MapContextService', () => {
let service: MapContextService

Expand Down Expand Up @@ -110,7 +152,7 @@ describe('MapContextService', () => {
const urls = source.getUrls()
expect(urls.length).toBe(1)
expect(urls[0]).toBe(
'https://www.geograndest.fr/geoserver/region-grand-est/ows'
'https://www.geograndest.fr/geoserver/region-grand-est/ows?REQUEST=GetCapabilities&SERVICE=WMS'
)
})
it('set WMS gutter of 20px', () => {
Expand Down Expand Up @@ -322,7 +364,7 @@ describe('MapContextService', () => {
const layerWMSUrl = (map.getLayers().item(1) as TileLayer<TileWMS>)
.getSource()
.getUrls()[0]
expect(layerWMSUrl).toEqual('https://some-wms-server/')
expect(layerWMSUrl).toEqual('https://some-wms-server')
})
it('add one WFS layer from config on top of baselayer', () => {
const layerWFSSource = (
Expand Down
Loading

0 comments on commit 6e2b8be

Please sign in to comment.