diff --git a/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.css b/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.css
index e69de29bb2..ac88d0d289 100644
--- a/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.css
+++ b/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.css
@@ -0,0 +1,7 @@
+.dropdown-content {
+ display: none;
+}
+
+.relative:hover .dropdown-content {
+ display: block;
+}
diff --git a/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.html b/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.html
index 47d5fed454..416bb8288e 100644
--- a/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.html
+++ b/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.html
@@ -4,8 +4,7 @@
(valueChange)="urlChange.next($event)"
[hint]="'map.ogc.urlInput.hint' | translate"
class="w-96"
- >
-
+ >
0">
-
map.layers.available
-
-
-
- {{ layer }}
-
-
map.layer.add
+
+
+
+ {{ layer.name }}
+
+
+
+
+ map.layer.add
+
+
-
-
+
+
diff --git a/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.spec.ts b/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.spec.ts
index ab58f79785..8979e6e2b7 100644
--- a/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.spec.ts
+++ b/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.spec.ts
@@ -1,6 +1,5 @@
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'
@@ -9,6 +8,9 @@ jest.mock('@camptocamp/ogc-client', () => ({
OgcApiEndpoint: class {
constructor(private url) {}
isReady() {
+ if (this.url === 'http://example.com/ogc') {
+ return Promise.resolve(this)
+ }
if (this.url.indexOf('error') > -1) {
return Promise.reject(new Error('Something went wrong'))
}
@@ -19,17 +21,57 @@ jest.mock('@camptocamp/ogc-client', () => ({
}
return Promise.resolve(this)
}
- get featureCollections() {
+ get allCollections() {
+ if (this.url === 'http://example.com/ogc') {
+ return Promise.resolve([
+ {
+ name: 'NaturalEarth:physical:ne_10m_lakes_pluvial',
+ hasVectorTiles: true,
+ hasMapTiles: true,
+ },
+ {
+ name: 'NaturalEarth:physical:ne_10m_land_ocean_seams',
+ hasVectorTiles: true,
+ hasMapTiles: true,
+ },
+ ])
+ }
if (this.url.includes('error')) {
return Promise.reject(new Error('Simulated loading error'))
}
- return Promise.resolve(['layer1', 'layer2', 'layer3'])
+ return Promise.resolve([
+ {
+ name: 'NaturalEarth:physical:ne_10m_lakes_pluvial',
+ hasVectorTiles: true,
+ hasMapTiles: true,
+ },
+ {
+ name: 'NaturalEarth:physical:ne_10m_land_ocean_seams',
+ hasVectorTiles: true,
+ hasMapTiles: true,
+ },
+ ])
}
getCollectionItemsUrl(collectionId) {
+ if (this.url === 'http://example.com/ogc') {
+ return Promise.resolve(
+ `http://example.com/collections/${collectionId}/items`
+ )
+ }
return Promise.resolve(
`http://example.com/collections/${collectionId}/items`
)
}
+ getVectorTilesetUrl(collectionId) {
+ return Promise.resolve(
+ `http://example.com/collections/${collectionId}/tiles/vector`
+ )
+ }
+ getMapTilesetUrl(collectionId) {
+ return Promise.resolve(
+ `http://example.com/collections/${collectionId}/tiles/map`
+ )
+ }
},
}))
@@ -68,7 +110,18 @@ describe('AddLayerFromOgcApiComponent', () => {
await component.loadLayers()
expect(component.errorMessage).toBeFalsy()
expect(component.loading).toBe(false)
- expect(component.layers).toEqual(['layer1', 'layer2', 'layer3'])
+ expect(component.layers).toEqual([
+ {
+ name: 'NaturalEarth:physical:ne_10m_lakes_pluvial',
+ hasVectorTiles: true,
+ hasMapTiles: true,
+ },
+ {
+ name: 'NaturalEarth:physical:ne_10m_land_ocean_seams',
+ hasVectorTiles: true,
+ hasMapTiles: true,
+ },
+ ])
})
it('should handle errors while loading layers', async () => {
@@ -79,4 +132,40 @@ describe('AddLayerFromOgcApiComponent', () => {
expect(component.layers.length).toBe(0)
})
})
+
+ describe('Add Collection', () => {
+ it('should add feature type collection to map', async () => {
+ const layerAddedSpy = jest.spyOn(component.layerAdded, 'emit')
+ await component.addLayer('layer1', 'features')
+ expect(layerAddedSpy).toHaveBeenCalledWith({
+ name: 'layer1',
+ url: 'http://example.com/collections/layer1/items',
+ type: MapContextLayerTypeEnum.OGCAPI,
+ layerType: 'features',
+ title: 'layer1',
+ })
+ })
+ it('should add vector tile collection to map', async () => {
+ const layerAddedSpy = jest.spyOn(component.layerAdded, 'emit')
+ await component.addLayer('layer1', 'vectorTiles')
+ expect(layerAddedSpy).toHaveBeenCalledWith({
+ name: 'layer1',
+ url: 'http://example.com/collections/layer1/tiles/vector',
+ type: MapContextLayerTypeEnum.OGCAPI,
+ layerType: 'vectorTiles',
+ title: 'layer1',
+ })
+ })
+ it('should add map tile collection to map', async () => {
+ const layerAddedSpy = jest.spyOn(component.layerAdded, 'emit')
+ await component.addLayer('layer1', 'mapTiles')
+ expect(layerAddedSpy).toHaveBeenCalledWith({
+ name: 'layer1',
+ url: 'http://example.com/collections/layer1/tiles/map',
+ type: MapContextLayerTypeEnum.OGCAPI,
+ layerType: 'mapTiles',
+ title: 'layer1',
+ })
+ })
+ })
})
diff --git a/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.ts b/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.ts
index 6010111cd3..2ea471ce28 100644
--- a/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.ts
+++ b/libs/feature/map/src/lib/add-layer-from-ogc-api/add-layer-from-ogc-api.component.ts
@@ -14,7 +14,7 @@ import {
MapContextLayerTypeEnum,
} from '../map-context/map-context.model'
import { TranslateModule } from '@ngx-translate/core'
-import { UiInputsModule } from '@geonetwork-ui/ui/inputs'
+import { DropdownChoice, UiInputsModule } from '@geonetwork-ui/ui/inputs'
import { CommonModule } from '@angular/common'
import { MapLayer } from '../+state/map.models'
@@ -30,18 +30,16 @@ export class AddLayerFromOgcApiComponent implements OnInit {
@Output() layerAdded = new EventEmitter
()
urlChange = new Subject()
- layerUrl = ''
loading = false
- layers: string[] = []
- ogcEndpoint: OgcApiEndpoint = null
+ layers: any[] = []
errorMessage: string | null = null
+ selectedLayerTypes: { [key: string]: DropdownChoice['value'] } = {}
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit() {
this.urlChange.pipe(debounceTime(700)).subscribe(() => {
this.loadLayers()
- this.changeDetectorRef.detectChanges() // manually trigger change detection
})
}
@@ -49,14 +47,13 @@ export class AddLayerFromOgcApiComponent implements OnInit {
this.errorMessage = null
try {
this.loading = true
- if (this.ogcUrl.trim() === '') {
+ 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
+ const ogcEndpoint = await new OgcApiEndpoint(this.ogcUrl)
+ this.layers = await ogcEndpoint.allCollections
+ this.setDefaultLayerTypes()
} catch (error) {
const err = error as Error
this.layers = []
@@ -67,14 +64,72 @@ export class AddLayerFromOgcApiComponent implements OnInit {
}
}
- async addLayer(layer: string) {
- this.layerUrl = await this.ogcEndpoint.getCollectionItemsUrl(layer)
+ setDefaultLayerTypes() {
+ this.layers.forEach((layer) => {
+ const choices = this.getLayerChoices(layer)
+ if (choices.length > 0) {
+ this.selectedLayerTypes[layer.name] = choices[0].value
+ }
+ })
+ }
- const layerToAdd: MapContextLayerModel = {
- name: layer,
- url: this.layerUrl,
- type: MapContextLayerTypeEnum.OGCAPI,
+ getLayerChoices(layer: any) {
+ const choices = []
+ if (layer.hasRecords) {
+ choices.push({ label: 'Records', value: 'record' })
+ }
+ if (layer.hasFeatures) {
+ choices.push({ label: 'Features', value: 'features' })
+ }
+ if (layer.hasVectorTiles) {
+ choices.push({ label: 'Vector Tiles', value: 'vectorTiles' })
+ }
+ if (layer.hasMapTiles) {
+ choices.push({ label: 'Map Tiles', value: 'mapTiles' })
+ }
+ return choices
+ }
+
+ shouldDisplayLayer(layer: any) {
+ return (
+ layer.hasRecords ||
+ layer.hasFeatures ||
+ layer.hasVectorTiles ||
+ layer.hasMapTiles
+ )
+ }
+
+ onLayerTypeSelect(layerName: string, selectedType: any) {
+ this.selectedLayerTypes[layerName] = selectedType
+ ? selectedType
+ : this.getLayerChoices(layerName)[0]?.value
+ }
+
+ async addLayer(layer: string, layerType: any) {
+ try {
+ const ogcEndpoint = await new OgcApiEndpoint(this.ogcUrl)
+ let layerUrl: string
+
+ if (layerType === 'vectorTiles') {
+ layerUrl = await ogcEndpoint.getVectorTilesetUrl(layer)
+ } else if (layerType === 'mapTiles') {
+ layerUrl = await ogcEndpoint.getMapTilesetUrl(layer)
+ } else {
+ layerUrl = await ogcEndpoint.getCollectionItemsUrl(layer, {
+ outputFormat: 'json',
+ })
+ }
+
+ const layerToAdd: MapContextLayerModel = {
+ name: layer,
+ url: layerUrl,
+ type: MapContextLayerTypeEnum.OGCAPI,
+ layerType: layerType,
+ }
+ this.layerAdded.emit({ ...layerToAdd, title: layer })
+ } catch (error) {
+ const err = error as Error
+ console.error('Error adding layer:', err.message)
}
- this.layerAdded.emit({ ...layerToAdd, title: layer })
}
}
diff --git a/libs/feature/map/src/lib/map-context/map-context.model.ts b/libs/feature/map/src/lib/map-context/map-context.model.ts
index 12f07631ee..63d7ed833d 100644
--- a/libs/feature/map/src/lib/map-context/map-context.model.ts
+++ b/libs/feature/map/src/lib/map-context/map-context.model.ts
@@ -38,6 +38,7 @@ export interface MapContextLayerOgcapiModel {
type: 'ogcapi'
url: string
name: string
+ layerType: 'feature' | 'vectorTiles' | 'mapTiles' | 'record'
}
interface LayerXyzModel {
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 5e723badbe..8b63e20b48 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
@@ -25,6 +25,10 @@ import WMTS from 'ol/source/WMTS'
import { Geometry } from 'ol/geom'
import Feature from 'ol/Feature'
import { WfsEndpoint, WmtsEndpoint } from '@camptocamp/ogc-client'
+import OGCVectorTile from 'ol/source/OGCVectorTile.js'
+import { MVT } from 'ol/format'
+import VectorTileLayer from 'ol/layer/VectorTile'
+import OGCMapTile from 'ol/source/OGCMapTile.js'
export const DEFAULT_BASELAYER_CONTEXT: MapContextLayerXyzModel = {
type: MapContextLayerTypeEnum.XYZ,
@@ -78,14 +82,28 @@ export class MapContextService {
const style = this.styleService.styles.default
switch (type) {
case MapContextLayerTypeEnum.OGCAPI:
- return new VectorLayer({
- source: new VectorSource({
- format: new GeoJSON(),
- url: layerModel.url,
- }),
- style,
- })
-
+ if (layerModel.layerType === 'vectorTiles') {
+ return new VectorTileLayer({
+ source: new OGCVectorTile({
+ url: layerModel.url,
+ format: new MVT(),
+ }),
+ })
+ } else if (layerModel.layerType === 'mapTiles') {
+ return new TileLayer({
+ source: new OGCMapTile({
+ url: layerModel.url,
+ }),
+ })
+ } else {
+ return new VectorLayer({
+ source: new VectorSource({
+ format: new GeoJSON(),
+ url: layerModel.url,
+ }),
+ style,
+ })
+ }
case MapContextLayerTypeEnum.XYZ:
return new TileLayer({
source: new XYZ({
diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html
index fd72c253f4..4da868412e 100644
--- a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html
+++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html
@@ -54,7 +54,7 @@