Skip to content

Commit

Permalink
Merge pull request #875 from geonetwork/DH_WFS_Custom_URL
Browse files Browse the repository at this point in the history
[Datahub] Allow building custom URL for WFS service
  • Loading branch information
ronitjadhav authored May 21, 2024
2 parents 3222f97 + 4420fc5 commit 22f4da7
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 44 deletions.
3 changes: 2 additions & 1 deletion libs/ui/elements/src/lib/api-card/api-card.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export class ApiCardComponent implements OnInit, OnChanges {

ngOnInit() {
this.displayApiFormButton =
this.link.accessServiceProtocol === 'ogcFeatures' ? true : false
this.link.accessServiceProtocol === 'ogcFeatures' ||
this.link.accessServiceProtocol === 'wfs'
}

ngOnChanges(changes: SimpleChanges) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,31 @@
</div>
</div>
</div>
<div class="flex flex-col gap-3">
<p class="text-sm" translate>record.metadata.api.form.offset</p>
<gn-ui-text-input
class="w-20"
[value]="offset$ | async"
(valueChange)="setOffset($event)"
hint=""
>
</gn-ui-text-input>
<div class="flex flex-col gap-3 relative">
<p class="text-sm" [class.text-gray-600]="!supportOffset" translate>
record.metadata.api.form.offset
</p>
<div class="flex items-center">
<gn-ui-text-input
class="w-20"
[value]="offset$ | async"
[disabled]="!supportOffset"
(valueChange)="supportOffset ? setOffset($event) : null"
hint=""
>
</gn-ui-text-input>
<div
*ngIf="!supportOffset"
class="flex items-center gap-2 text-orange-500 z-10 ml-3"
>
<span
class="material-symbols-outlined"
matTooltip="Not supported on this service"
>
warning
</span>
</div>
</div>
</div>
<div class="flex flex-col gap-3">
<p class="text-sm" translate>record.metadata.api.form.type</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@ jest.mock('@camptocamp/ogc-client', () => ({
})
}
},
WfsEndpoint: class {
constructor(private url) {}
async isReady() {
return Promise.resolve(true)
}
getFeatureUrl(featureType, options) {
return `${this.url}?type=${featureType}&options=${JSON.stringify(
options
)}`
}
getServiceInfo() {
return Promise.resolve({
outputFormats: [
'application/geo+json',
'application/json',
'text/csv',
'application/json',
],
})
}
supportsStartIndex() {
return true
}
},
}))

describe('RecordApFormComponent', () => {
Expand Down Expand Up @@ -122,6 +146,29 @@ describe('RecordApFormComponent', () => {
])
})
})

describe('When panel is opened and accessServiceProtocol is wfs', () => {
beforeEach(() => {
component.apiLink = {
...mockDatasetServiceDistribution,
accessServiceProtocol: 'wfs',
}
fixture.detectChanges()
})

it('should set the links and initial values correctly', async () => {
expect(component.apiBaseUrl).toBe('https://api.example.com/data')
expect(component.accessServiceProtocol).toBe('wfs')
expect(component.offset$.getValue()).toBe('')
expect(component.limit$.getValue()).toBe('-1')
expect(component.format$.getValue()).toBe('json')
const url = await firstValueFrom(component.apiQueryUrl$)
expect(url).toBe(
'https://api.example.com/data?type=undefined&options={"outputFormat":"json","startIndex":0}'
)
})
})

describe('When apiLink input is undefined', () => {
it('should not call parseOutputFormats()', () => {
const spy = jest.spyOn(component, 'parseOutputFormats')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { OgcApiEndpoint } from '@camptocamp/ogc-client'
import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record'
import { OgcApiEndpoint, WfsEndpoint } from '@camptocamp/ogc-client'
import {
DatasetServiceDistribution,
ServiceProtocol,
} from '@geonetwork-ui/common/domain/model/record'
import { mimeTypeToFormat } from '@geonetwork-ui/util/shared'
import { BehaviorSubject, combineLatest, map } from 'rxjs'
import { BehaviorSubject, combineLatest, map, switchMap } from 'rxjs'

const DEFAULT_PARAMS = {
OFFSET: '',
Expand All @@ -18,19 +21,26 @@ const DEFAULT_PARAMS = {
export class RecordApiFormComponent {
@Input() set apiLink(value: DatasetServiceDistribution) {
this.outputFormats = [{ value: 'json', label: 'JSON' }]
this.accessServiceProtocol = value ? value.accessServiceProtocol : undefined
this.apiFeatureType = value ? value.name : undefined
if (value) {
this.apiBaseUrl = value.url.href
this.parseOutputFormats()
}
this.resetUrl()
}

offset$ = new BehaviorSubject('')
limit$ = new BehaviorSubject('')
format$ = new BehaviorSubject('')
apiBaseUrl: string
apiFeatureType: string
supportOffset = true
accessServiceProtocol: ServiceProtocol | undefined
outputFormats = [{ value: 'json', label: 'JSON' }]

apiQueryUrl$ = combineLatest([this.offset$, this.limit$, this.format$]).pipe(
map(([offset, limit, format]) => {
switchMap(async ([offset, limit, format]) => {
let outputUrl
if (this.apiBaseUrl) {
const url = new URL(this.apiBaseUrl)
Expand All @@ -44,6 +54,20 @@ export class RecordApiFormComponent {
}
outputUrl = url.toString()
}

if (this.accessServiceProtocol === 'wfs') {
const wfsEndpoint = new WfsEndpoint(this.apiBaseUrl)
if (await wfsEndpoint.isReady()) {
const options = {
outputFormat: format,
startIndex: Number(offset),
}
if (limit !== '-1') {
options['maxFeatures'] = Number(limit)
}
outputUrl = wfsEndpoint.getFeatureUrl(this.apiFeatureType, options)
}
}
return outputUrl
})
)
Expand Down Expand Up @@ -80,32 +104,49 @@ export class RecordApiFormComponent {
? this.apiBaseUrl.slice(0, -1)
: this.apiBaseUrl

this.getOutputFormats(apiUrl).then((outputFormats) => {
const formatsList = outputFormats.itemFormats.map((format) => {
const normalizedFormat = mimeTypeToFormat(format)
if (normalizedFormat) {
return {
label: normalizedFormat?.toUpperCase(),
value: normalizedFormat,
}
this.getOutputFormats(apiUrl, this.accessServiceProtocol).then(
(outputFormats) => {
let formatsList = []
if ('itemFormats' in outputFormats) {
formatsList = this.mapFormats(outputFormats.itemFormats)
} else if ('outputFormats' in outputFormats) {
formatsList = this.mapFormats(outputFormats.outputFormats)
}
return null
})
this.outputFormats = this.outputFormats.concat(
formatsList.filter(Boolean)
)
this.outputFormats = this.outputFormats
.filter(
(format, index, self) =>
index === self.findIndex((t) => t.value === format.value)
this.outputFormats = this.outputFormats.concat(
formatsList.filter(Boolean)
)
.sort((a, b) => a.label.localeCompare(b.label))
this.outputFormats = this.outputFormats
.filter(
(format, index, self) =>
index === self.findIndex((t) => t.value === format.value)
)
.sort((a, b) => a.label.localeCompare(b.label))
}
)
}

mapFormats(formats: any[]) {
return formats.map((format) => {
const normalizedFormat = mimeTypeToFormat(format)
if (normalizedFormat) {
return {
label: normalizedFormat.toUpperCase(),
value: normalizedFormat,
}
}
return null
})
}

async getOutputFormats(url) {
const endpoint = await new OgcApiEndpoint(url)
const firstCollection = (await endpoint.featureCollections)[0]
return endpoint.getCollectionInfo(firstCollection)
async getOutputFormats(url: string, accessServiceProtocol: string) {
if (accessServiceProtocol === 'wfs') {
const endpoint = await new WfsEndpoint(url).isReady()
this.supportOffset = endpoint.supportsStartIndex()
return endpoint.getServiceInfo()
} else {
const endpoint = await new OgcApiEndpoint(url)
const firstCollection = (await endpoint.featureCollections)[0]
return endpoint.getCollectionInfo(firstCollection)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
[placeholder]="hint"
[attr.aria-label]="hint"
[attr.required]="required || null"
[disabled]="disabled"
/>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const Primary: StoryObj<TextInputComponent> = {
value: '',
hint: 'Put something here!',
required: false,
disabled: false,
},
argTypes: {
valueChange: {
Expand Down
1 change: 1 addition & 0 deletions libs/ui/inputs/src/lib/text-input/text-input.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class TextInputComponent implements AfterViewInit {
@Input() extraClass = ''
@Input() hint: string
@Input() required = false
@Input() disabled: boolean
rawChange = new Subject<string>()
@Output() valueChange = this.rawChange.pipe(distinctUntilChanged())
@ViewChild('input') input
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.1-dev.a0aadb6",
"@camptocamp/ogc-client": "^1.1.1-dev.ddbb5b0",
"@geospatial-sdk/geocoding": "^0.0.5-alpha.2",
"@ltd/j-toml": "~1.35.2",
"@messageformat/core": "^3.0.1",
Expand Down

0 comments on commit 22f4da7

Please sign in to comment.