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/package-lock.json b/package-lock.json
index 31dbb3b45a..39816ce869 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,7 @@
"@angular/router": "16.1.7",
"@bartholomej/ngx-translate-extract": "^8.0.2",
"@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
- "@camptocamp/ogc-client": "^1.1.0",
+ "@camptocamp/ogc-client": "^1.1.1-dev.a0aadb6",
"@geospatial-sdk/geocoding": "^0.0.5-alpha.2",
"@ltd/j-toml": "~1.35.2",
"@messageformat/core": "^3.0.1",
@@ -3652,9 +3652,9 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@camptocamp/ogc-client": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@camptocamp/ogc-client/-/ogc-client-1.1.0.tgz",
- "integrity": "sha512-+Vj4G1D6YNxqRsKtdCA6fWHlFjNJxdK8xRbnXlgJwfRNtFxK78qkPeAuN82hxjgZrEmAOQzPZWgELDAjDq2UAQ==",
+ "version": "1.1.1-dev.a0aadb6",
+ "resolved": "https://registry.npmjs.org/@camptocamp/ogc-client/-/ogc-client-1.1.1-dev.a0aadb6.tgz",
+ "integrity": "sha512-wQsCe7GHYoUEr71vO8ov74GoNrHbCS+cfvimJ055OTHcEOIjrFC3Hs88P0WmgXmdUuGoFZLpJ6EF1eeYisGL+Q==",
"dependencies": {
"@rgrove/parse-xml": "^4.1.0"
},
@@ -17891,9 +17891,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/ejs": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
- "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
+ "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"dependencies": {
"jake": "^10.8.5"
},
diff --git a/package.json b/package.json
index 49058af098..e01ac3607e 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
"@angular/router": "16.1.7",
"@bartholomej/ngx-translate-extract": "^8.0.2",
"@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
- "@camptocamp/ogc-client": "^1.1.0",
+ "@camptocamp/ogc-client": "^1.1.1-dev.a0aadb6",
"@geospatial-sdk/geocoding": "^0.0.5-alpha.2",
"@ltd/j-toml": "~1.35.2",
"@messageformat/core": "^3.0.1",