Skip to content

Commit

Permalink
Implement MVP for OGC API - Coverages
Browse files Browse the repository at this point in the history
  • Loading branch information
m-mohr committed Nov 12, 2024
1 parent a06913c commit 2db1bb4
Show file tree
Hide file tree
Showing 4 changed files with 440 additions and 42 deletions.
53 changes: 35 additions & 18 deletions src/api/capabilities.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import API from '../utils/API.js';
import Utils from '../utils/utils.js';
import Coverages from './coverages.js';
const packageInfo = Utils.require('../../package.json');

export default class CapabilitiesAPI {
Expand Down Expand Up @@ -112,46 +113,62 @@ export default class CapabilitiesAPI {
type: 'application/json',
title: 'Supported API versions'
},
// STAC
{
rel: "data",
href: API.getUrl("/collections"),
type: "application/json",
title: "Datasets"
},
// OGC API - Coverages
{
rel: "http://www.opengis.net/def/rel/ogc/1.0/data",
href: API.getUrl("/collections"),
type: "application/json",
title: "Datasets"
},
// STAC and older OGC APIs
{
rel: "conformance",
href: API.getUrl("/conformance"),
type: "application/json",
title: "OGC Conformance classes"
},
// Some newer OGC APIs (including Coverages)
{
rel: "http://www.opengis.net/def/rel/ogc/1.0/conformance",
href: API.getUrl("/conformance"),
type: "application/json",
title: "OGC Conformance classes"
}
]
});
}

async getConformance(req, res) {
res.json({
"conformsTo": [
"https://api.openeo.org/1.2.0",
"https://api.stacspec.org/v1.0.0/core",
"https://api.stacspec.org/v1.0.0/collections",
"https://api.stacspec.org/v1.0.0/ogcapi-features",
"https://api.stacspec.org/v1.0.0/ogcapi-features#sort",
let conformsTo = [
"https://api.openeo.org/1.2.0",
"https://api.stacspec.org/v1.0.0/core",
"https://api.stacspec.org/v1.0.0/collections",
"https://api.stacspec.org/v1.0.0/ogcapi-features",
"https://api.stacspec.org/v1.0.0/ogcapi-features#sort",
// Item Filter
// "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter",
// "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter",
// Collection Search
// "https://api.stacspec.org/v1.0.0-rc.1/collection-search",
// "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query",
// "https://api.stacspec.org/v1.0.0-rc.1/collection-search",
// "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query",
// Collection Filter
// "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter",
// "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter",
// Collection Sorting
// "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort",
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
// "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort",
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
// CQL2 (for Item and Collection Filter)
// "http://www.opengis.net/spec/cql2/1.0/conf/cql2-text",
// "http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2",
]
});
// "http://www.opengis.net/spec/cql2/1.0/conf/cql2-text",
// "http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2",
];
conformsTo = Coverages.addConformanceClasses(conformsTo);
res.json({ conformsTo });
}

async getServices(req, res) {
Expand Down
114 changes: 104 additions & 10 deletions src/api/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Utils from '../utils/utils.js';
import Errors from '../utils/errors.js';
import GeeProcessing from '../processes/utils/processing.js';
import HttpUtils from '../utils/http.js';
import Coverages from './coverages.js';

const sortPropertyMap = {
'properties.datetime': 'system:time_start',
Expand Down Expand Up @@ -36,6 +37,8 @@ export default class Data {
server.addEndpoint('get', ['/collections/{collection_id}', '/collections/*'], this.getCollectionById.bind(this));
server.addEndpoint('get', '/collections/{collection_id}/queryables', this.getCollectionQueryables.bind(this));
server.addEndpoint('get', '/collections/{collection_id}/items', this.getCollectionItems.bind(this));
server.addEndpoint('get', '/collections/{collection_id}/schema', this.getCollectionSchema.bind(this));
server.addEndpoint('get', '/collections/{collection_id}/coverage', this.getCoverage.bind(this));
server.addEndpoint('get', '/collections/{collection_id}/items/{item_id}', this.getCollectionItemById.bind(this));
if (this.context.stacAssetDownloadSize > 0) {
server.addEndpoint('get', ['/assets/{asset_id}', '/assets/*'], this.getAssetById.bind(this));
Expand Down Expand Up @@ -106,6 +109,12 @@ export default class Data {
else if (id.endsWith('/queryables')) {
return await this.getCollectionQueryables(req, res);
}
else if (id.endsWith('/schema')) {
return await this.getCollectionSchema(req, res);
}
else if (id.endsWith('/coverage')) {
return await this.getCoverage(req, res);
}
else if (id.endsWith('/items')) {
return await this.getCollectionItems(req, res);
}
Expand All @@ -121,28 +130,113 @@ export default class Data {
res.json(collection);
}

async getCollectionQueryables(req, res) {
getCollectionId(req, endpoint) {
let id = req.params.collection_id;
// Get the ID if this was a redirect from the /collections/{collection_id} endpoint
// Get the ID if this was a redirect from another endpoint
if (req.params['*'] && !id) {
id = req.params['*'].replace(/\/queryables$/, '');
endpoint = '/' + endpoint;
id = req.params['*'];
if (id.endsWith(endpoint)) {
id = id.substring(0, req.params['*'].length - endpoint.length);
}
}
return id;
}

const queryables = this.catalog.getSchema(id);
async getCollectionQueryables(req, res) {
const id = this.getCollectionId(req, 'queryables');
const queryables = this.catalog.getQueryables(id);
if (queryables === null) {
throw new Errors.CollectionNotFound();
}

res.json(queryables);
}

async getCollectionItems(req, res) {
let id = req.params.collection_id;
// Get the ID if this was a redirect from the /collections/{collection_id} endpoint
if (req.params['*'] && !id) {
id = req.params['*'].replace(/\/items$/, '');
async getCollectionSchema(req, res) {
const id = this.getCollectionId(req, 'schema');
const schema = this.catalog.getSchema(id);
if (schema === null) {
throw new Errors.CollectionNotFound();
}
res.json(schema);
}

async getCoverage(req, res) {
if (!req.user._id) {
throw new Errors.AuthenticationRequired();
}

const id = this.getCollectionId(req, 'coverage');
const collection = this.catalog.getData(id);
if (collection === null) {
throw new Errors.CollectionNotFound();
}

const coverage = new Coverages(collection);
// Subsetting
try {
coverage.setDatetime(req.query.datetime);
} catch (e) {
throw new Errors.ParameterValueInvalid({parameter: 'datetime', reason: e.message});
}
try {
coverage.setBoundingBox(req.query.bbox, req.query['bbox-crs']);
} catch (e) {
throw new Errors.ParameterValueInvalid({parameter: 'bbox', reason: e.message});
}
try {
coverage.setSubset(req.query.subset, req.query['subset-crs']);
} catch (e) {
throw new Errors.ParameterValueInvalid({parameter: 'subset', reason: e.message});
}
// Field selection
try {
coverage.setFields(req.query.properties);
} catch (e) {
throw new Errors.ParameterValueInvalid({parameter: 'properties', reason: e.message});
}
// CRS
try {
coverage.setCrs(req.query.crs);
} catch (e) {
throw new Errors.ParameterValueInvalid({parameter: 'crs', reason: e.message});
}

const pngMedia = ['image/png'];
const gtiffMedia = ['image/tiff', 'image/tiff; application=geotiff'];
if (req.query.f) {
if (pngMedia.includes(req.query.f)) {
coverage.setFileFormat('PNG');
}
else if (gtiffMedia.includes(req.query.f)) {
coverage.setFileFormat('GTIFF');
}
else {
throw new Errors.NotAcceptableError();
}
}
else {
const isPNG = req.accepts(pngMedia);
const isGTIFF = req.accepts(gtiffMedia);
if (isGTIFF) {
coverage.setFileFormat('GTIFF');
}
else if (isPNG) {
coverage.setFileFormat('PNG');
}
else {
throw new Errors.NotAcceptableError();
}
}

const response = await coverage.execute(this.context, req);

res.header('Content-Type', response?.headers?.['content-type'] || 'application/octet-stream');
response.data.pipe(res);
}

async getCollectionItems(req, res) {
const id = this.getCollectionId(req, 'items');
const collection = this.catalog.getData(id, true);
if (collection === null) {
throw new Errors.CollectionNotFound();
Expand Down
Loading

0 comments on commit 2db1bb4

Please sign in to comment.