From 46dce5ed7ca0b67d0534269e685c2b600c2077dc Mon Sep 17 00:00:00 2001 From: Olivia Date: Mon, 29 Apr 2024 12:59:45 +0200 Subject: [PATCH 1/2] OGC API: add helper to json bulk download link --- src/ogc-api/endpoint.spec.ts | 5 +++++ src/ogc-api/endpoint.ts | 15 ++++++++------- src/ogc-api/info.ts | 14 ++++++++++++++ src/ogc-api/model.ts | 2 ++ src/shared/mime-type.spec.ts | 16 ++++++++++++++-- src/shared/mime-type.ts | 4 ++++ 6 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/ogc-api/endpoint.spec.ts b/src/ogc-api/endpoint.spec.ts index 546f190..9264e32 100644 --- a/src/ogc-api/endpoint.spec.ts +++ b/src/ogc-api/endpoint.spec.ts @@ -225,6 +225,7 @@ describe('OgcApiEndpoint', () => { 'application/vnd.ogc.fg+json;compatibility=geojson', ], bulkDownloadLinks: {}, + jsonDownloadLink: null, extent: { spatial: { bbox: [ @@ -280,6 +281,7 @@ describe('OgcApiEndpoint', () => { 'text/html', ], bulkDownloadLinks: {}, + jsonDownloadLink: null, keywords: ['netherlands', 'open data', 'georegister'], extent: { spatial: { @@ -358,6 +360,7 @@ describe('OgcApiEndpoint', () => { 'text/html', ], bulkDownloadLinks: {}, + jsonDownloadLink: null, extent: { spatial: { bbox: [ @@ -1783,6 +1786,8 @@ The document at http://local/nonexisting?f=json could not be fetched.` 'text/csv;charset=UTF-8': 'https://my.server.org/sample-data-2/collections/aires-covoiturage/items?f=csv&limit=-1', }, + jsonDownloadLink: + 'https://my.server.org/sample-data-2/collections/aires-covoiturage/items?f=geojson&limit=-1', id: 'aires-covoiturage', itemType: 'feature', queryables: [], diff --git a/src/ogc-api/endpoint.ts b/src/ogc-api/endpoint.ts index 490c983..bd77282 100644 --- a/src/ogc-api/endpoint.ts +++ b/src/ogc-api/endpoint.ts @@ -27,7 +27,11 @@ import { } from './link-utils.js'; import { EndpointError } from '../shared/errors.js'; import { BoundingBox, CrsCode, MimeType } from '../shared/models.js'; -import { isMimeTypeJson, isMimeTypeJsonFg } from '../shared/mime-type.js'; +import { + isMimeTypeGeoJson, + isMimeTypeJson, + isMimeTypeJsonFg, +} from '../shared/mime-type.js'; /** * Represents an OGC API endpoint advertising various collections and services. @@ -313,13 +317,10 @@ ${e.message}`); ); let url: URL; if (options.asJson) { - // try json-fg - linkWithFormat = itemLinks.find((link) => - isMimeTypeJsonFg(link.type) - ); - // try geojson + // try json-fg, geojson and json linkWithFormat = - linkWithFormat ?? + itemLinks.find((link) => isMimeTypeJsonFg(link.type)) || + itemLinks.find((link) => isMimeTypeGeoJson(link.type)) || itemLinks.find((link) => isMimeTypeJson(link.type)); } if (options?.outputFormat && !linkWithFormat) { diff --git a/src/ogc-api/info.ts b/src/ogc-api/info.ts index 80bc71e..38ece2e 100644 --- a/src/ogc-api/info.ts +++ b/src/ogc-api/info.ts @@ -9,6 +9,11 @@ import { } from './model.js'; import { assertHasLinks } from './link-utils.js'; import { EndpointError } from '../shared/errors.js'; +import { + isMimeTypeGeoJson, + isMimeTypeJson, + isMimeTypeJsonFg, +} from '../shared/mime-type.js'; export function parseEndpointInfo(rootDoc: OgcApiDocument): OgcApiEndpointInfo { try { @@ -108,9 +113,18 @@ export function parseBaseCollectionInfo( acc[link.type] = link.href; return acc; }, {}); + const mimeTypes = Object.keys(bulkDownloadLinks); + const jsonMimeType = + mimeTypes.find(isMimeTypeJsonFg) || + mimeTypes.find(isMimeTypeGeoJson) || + mimeTypes.find(isMimeTypeJson); + const jsonDownloadLink = jsonMimeType + ? bulkDownloadLinks[jsonMimeType] + : null; return { itemFormats: itemFormats, bulkDownloadLinks, + jsonDownloadLink, ...props, } as OgcApiCollectionInfo; } diff --git a/src/ogc-api/model.ts b/src/ogc-api/model.ts index 69c76a5..f6b5765 100644 --- a/src/ogc-api/model.ts +++ b/src/ogc-api/model.ts @@ -35,6 +35,7 @@ export interface CollectionParameter { * @property itemFormats These mime types are available through the `/items` endpoint; * use the `getCollectionItemsUrl` function to generate a URL using one of those formats * @property bulkDownloadLinks Map between formats and bulk download links (no filtering, pagination etc.) + * @property jsonDownloadLink Link to the first bulk download link using JSON-FG or GeoJSON; null if no link found * @property crs * @property storageCrs * @property itemCount @@ -54,6 +55,7 @@ export interface OgcApiCollectionInfo { itemType: 'feature' | 'record'; itemFormats: MimeType[]; bulkDownloadLinks: Record; + jsonDownloadLink: string; crs: CrsCode[]; storageCrs?: CrsCode; itemCount: number; diff --git a/src/shared/mime-type.spec.ts b/src/shared/mime-type.spec.ts index f1a77a2..2437635 100644 --- a/src/shared/mime-type.spec.ts +++ b/src/shared/mime-type.spec.ts @@ -1,7 +1,11 @@ -import { isMimeTypeJson, isMimeTypeJsonFg } from './mime-type.js'; +import { + isMimeTypeGeoJson, + isMimeTypeJson, + isMimeTypeJsonFg, +} from './mime-type.js'; describe('mime type utils', () => { - it('isMimeTypeGeoJson', () => { + it('isMimeTypeJson', () => { expect(isMimeTypeJson('application/geo+json')).toBe(true); expect(isMimeTypeJson('application/vnd.geo+json')).toBe(true); expect(isMimeTypeJson('geo+json')).toBe(true); @@ -9,6 +13,14 @@ describe('mime type utils', () => { expect(isMimeTypeJson('application/json')).toBe(true); expect(isMimeTypeJson('json')).toBe(true); }); + it('isMimeTypeGeoJson', () => { + expect(isMimeTypeGeoJson('application/geo+json')).toBe(true); + expect(isMimeTypeGeoJson('application/vnd.geo+json')).toBe(true); + expect(isMimeTypeGeoJson('geo+json')).toBe(true); + expect(isMimeTypeGeoJson('geojson')).toBe(true); + expect(isMimeTypeGeoJson('application/json')).toBe(false); + expect(isMimeTypeGeoJson('json')).toBe(false); + }); it('isMimeTypeJsonFg', () => { expect(isMimeTypeJsonFg('application/vnd.ogc.fg+json')).toBe(true); expect(isMimeTypeJsonFg('fg+json')).toBe(true); diff --git a/src/shared/mime-type.ts b/src/shared/mime-type.ts index 2349df8..dc45b1d 100644 --- a/src/shared/mime-type.ts +++ b/src/shared/mime-type.ts @@ -2,6 +2,10 @@ export function isMimeTypeJson(mimeType: string): boolean { return mimeType.toLowerCase().indexOf('json') > -1; } +export function isMimeTypeGeoJson(mimeType: string): boolean { + return /geo.?json/.test(mimeType); +} + export function isMimeTypeJsonFg(mimeType: string): boolean { return /json.?fg|fg.?json/.test(mimeType); } From 40b8cca21b024df90983cbcbe970c1ea5eb55aa2 Mon Sep 17 00:00:00 2001 From: Olivia Date: Mon, 29 Apr 2024 12:59:59 +0200 Subject: [PATCH 2/2] better handle root path lookup if query params present --- src/ogc-api/endpoint.spec.ts | 19 +++++++++++++++++++ src/ogc-api/link-utils.spec.ts | 8 ++++++++ src/ogc-api/link-utils.ts | 4 +++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/ogc-api/endpoint.spec.ts b/src/ogc-api/endpoint.spec.ts index 9264e32..a7e7d69 100644 --- a/src/ogc-api/endpoint.spec.ts +++ b/src/ogc-api/endpoint.spec.ts @@ -1810,4 +1810,23 @@ The document at http://local/nonexisting?f=json could not be fetched.` }); }); }); + + describe('url with trailing ?', () => { + beforeEach(() => { + endpoint = new OgcApiEndpoint( + 'http://local/sample-data/collections/airports/items?' + ); + }); + describe('#info', () => { + it('returns endpoint info', async () => { + await expect(endpoint.info).resolves.toEqual({ + title: 'OS Open Zoomstack', + description: + 'OS Open Zoomstack is a comprehensive vector basemap showing coverage of Great Britain at a national level, right down to street-level detail.', + attribution: + 'Contains OS data © Crown copyright and database right 2021.', + }); + }); + }); + }); }); diff --git a/src/ogc-api/link-utils.spec.ts b/src/ogc-api/link-utils.spec.ts index d4548f4..63fc8c7 100644 --- a/src/ogc-api/link-utils.spec.ts +++ b/src/ogc-api/link-utils.spec.ts @@ -18,5 +18,13 @@ describe('link utils', () => { 'http://example.com/foo' ); }); + it('should keep query params', () => { + expect(getParentPath('http://example.com/foo/bar?aa=bb')).toBe( + 'http://example.com/foo?aa=bb' + ); + expect(getParentPath('http://example.com/foo/bar?')).toBe( + 'http://example.com/foo?' + ); + }); }); }); diff --git a/src/ogc-api/link-utils.ts b/src/ogc-api/link-utils.ts index da57e76..0c6aca6 100644 --- a/src/ogc-api/link-utils.ts +++ b/src/ogc-api/link-utils.ts @@ -42,7 +42,9 @@ export function fetchRoot(url: string): Promise { } // if there is a collections array, we expect the parent path to end with slash if ('collections' in doc) { - parentUrl = `${parentUrl}/`; + const urlObj = new URL(parentUrl); + urlObj.pathname = `${urlObj.pathname}/`; + parentUrl = urlObj.toString(); } return fetchRoot(parentUrl); }