Skip to content

Commit

Permalink
Merge pull request geonetwork#871 from geonetwork/mv-add-ogc-api-tiles
Browse files Browse the repository at this point in the history
[Map-Viewer] Added support for OGC API Tiles
  • Loading branch information
ronitjadhav authored May 21, 2024
2 parents 8918855 + 5382b5e commit 3222f97
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.dropdown-content {
display: none;
}

.relative:hover .dropdown-content {
display: block;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
(valueChange)="urlChange.next($event)"
[hint]="'map.ogc.urlInput.hint' | translate"
class="w-96"
>
</gn-ui-text-input>
></gn-ui-text-input>
</div>

<div *ngIf="errorMessage" class="text-red-500 mt-2">
Expand All @@ -16,21 +15,36 @@
<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
<ng-container *ngFor="let layer of layers">
<div
*ngIf="shouldDisplayLayer(layer)"
class="flex items-center justify-between my-2 layer-item-tree"
>
<div class="flex flex-col items-start w-full">
<p
class="max-w-xs overflow-hidden overflow-ellipsis whitespace-nowrap"
[title]="layer.name"
>
{{ layer.name }}
</p>
<div class="flex justify-between items-center w-full">
<gn-ui-dropdown-selector
[title]="'Add Layer As' | translate"
[choices]="getLayerChoices(layer)"
(selectValue)="onLayerTypeSelect(layer.name, $event)"
[selected]="selectedLayerTypes[layer.name]"
extraBtnClass="w-6 h-5 !text-sm !px-2 !py-1"
></gn-ui-dropdown-selector>
<gn-ui-button
class="layer-add-btn"
type="primary"
(buttonClick)="addLayer(layer.name, selectedLayerTypes[layer.name])"
extraClass="text-sm !px-2 !py-1"
translate
>
<span translate>map.layer.add</span>
</gn-ui-button>
</div>
</div>
</ng-container>
</div>
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'))
}
Expand All @@ -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`
)
}
},
}))

Expand Down Expand Up @@ -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 () => {
Expand All @@ -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',
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -30,33 +30,30 @@ export class AddLayerFromOgcApiComponent implements OnInit {
@Output() layerAdded = new EventEmitter<MapLayer>()

urlChange = new Subject<string>()
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
})
}

async loadLayers() {
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 = []
Expand All @@ -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 })
}
}
1 change: 1 addition & 0 deletions libs/feature/map/src/lib/map-context/map-context.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface MapContextLayerOgcapiModel {
type: 'ogcapi'
url: string
name: string
layerType: 'feature' | 'vectorTiles' | 'mapTiles' | 'record'
}

interface LayerXyzModel {
Expand Down
34 changes: 26 additions & 8 deletions libs/feature/map/src/lib/map-context/map-context.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down
Loading

0 comments on commit 3222f97

Please sign in to comment.