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 Oct 17, 2024
1 parent ed2586f commit f1af129
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 29 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
132 changes: 122 additions & 10 deletions src/api/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ 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 runSync from './worker/sync.js';

const sortPropertyMap = {
'properties.datetime': 'system:time_start',
'id': 'system:index',
'properties.title': 'system:index'
};

// OGC:CRS84 as WKT
const CRS84 = `GEOGCS["WGS 84 (CRS84)",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["OGC","CRS84"]]`;

export default class Data {

constructor(context) {
Expand All @@ -36,6 +40,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 +112,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 +133,128 @@ 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();
}

let bbox = req.query.bbox || null;
if (bbox) {
bbox = bbox.split(",");
}
if (bbox.length !== 4) {
throw new Errors.ParameterValueInvalid({parameter: "bbox", message: "Invalid number of coordinates."});
}
const bboxCrs = req.query['bbox-crs'] || CRS84;

Check failure on line 185 in src/api/collections.js

View workflow job for this annotation

GitHub Actions / deploy (17)

'bboxCrs' is assigned a value but never used

Check failure on line 185 in src/api/collections.js

View workflow job for this annotation

GitHub Actions / deploy (lts/*)

'bboxCrs' is assigned a value but never used

Check failure on line 185 in src/api/collections.js

View workflow job for this annotation

GitHub Actions / deploy (lts/*)

'bboxCrs' is assigned a value but never used


let datetime = req.query.datetime || null;
if (typeof datetime === 'string') {
if (datetime.includes("/")) {
datetime = datetime.split("/");
}
else {
datetime = [datetime];
}
}

let subset = req.query.subset || [];
if (typeof subset === 'string') {
subset = [subset];
}
if (subset.length > 0) {
// please complain here: https://github.com/opengeospatial/ogcapi-coverages/issues/194
throw new Errors.NotSupported();
}

const scaleFactor = parseFloat(req.params['scale-factor']) || 1;

Check failure on line 207 in src/api/collections.js

View workflow job for this annotation

GitHub Actions / deploy (17)

'scaleFactor' is assigned a value but never used

Check failure on line 207 in src/api/collections.js

View workflow job for this annotation

GitHub Actions / deploy (lts/*)

'scaleFactor' is assigned a value but never used

Check failure on line 207 in src/api/collections.js

View workflow job for this annotation

GitHub Actions / deploy (lts/*)

'scaleFactor' is assigned a value but never used

const isPNG = req.accepts('image/png');
const isGTIFF = req.headers.accept && req.accepts([
'image/tiff',
'image/tiff; application=geotiff',
'image/tiff; application=geotiff; profile=cloud-optimized'
]);
if (!isPNG && !isGTIFF) {
throw new Errors.NotAcceptableError();
}

const process = {
"process_graph": {
"load_collection": {
"process_id": "load_collection",
"arguments": {
"id": id,
"spatial_extent": null,
"temporal_extent": null
}
},
"save_result": {
"process_id": "save_result",
"arguments": {
"data": {
"from_node": "load_collection"
},
"format": isGTIFF ? "GTIFF" : "PNG",
"options": {
"epsg": 4326
}
},
"result": true
}
}
};

const response = await runSync(this.context, req.user, Utils.timeId(), process, "error");

res.header('Content-Type', response?.headers?.['content-type'] || 'application/octet-stream');
res.header('Content-Crs', "EPSG:4326");
res.header('Content-Bbox', bbox.join(","));
if (req.query.datetime) {
res.header('Content-Datetime', req.query.datetime);
}
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
40 changes: 40 additions & 0 deletions src/api/coverages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import API from "../utils/API";

Check failure on line 1 in src/api/coverages.js

View workflow job for this annotation

GitHub Actions / deploy (17)

"../utils/API" is not found

Check failure on line 1 in src/api/coverages.js

View workflow job for this annotation

GitHub Actions / deploy (lts/*)

"../utils/API" is not found

Check failure on line 1 in src/api/coverages.js

View workflow job for this annotation

GitHub Actions / deploy (lts/*)

"../utils/API" is not found

export default class Coverages {

static addConformanceClasses(list) {
return list.concat([
"http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/core",
"http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/geotiff",
"http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/png",
"http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/crs",
"http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/scaling",
"http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/subsetting",
"http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/fieldselection",
]);
}

static updateCollection(collection) {

collection.links.push({
rel: "http://www.opengis.net/def/rel/ogc/1.0/schema",
href: API.getUrl(`/collections/${collection.id}/schema`),
type: "application/schema+json"
});
collection.links.push({
rel: "http://www.opengis.net/def/rel/ogc/1.0/coverage",
href: API.getUrl(`/collections/${collection.id}/schema`),
type: "image/png"
});
return collection;
}

constructor() {

}

async execute() {

}

}
51 changes: 50 additions & 1 deletion src/models/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export default class DataCatalog {
return await this.readLocalCatalog();
}

getSchema(id) {
getQueryables(id) {
const collection = this.getData(id, true);
if (!collection) {
return null;
Expand Down Expand Up @@ -211,6 +211,31 @@ export default class DataCatalog {
return jsonSchema;
}

getSchema(id) {
const collection = this.getData(id, true);
if (!collection) {
return null;
}

const jsonSchema = {
"$schema" : "https://json-schema.org/draft/2019-09/schema",
"$id" : API.getUrl(`/collections/${id}/schema`),
"title" : "Schema",
"type" : "object",
"properties" : {
"var": {
"title": "",
"type": "number",
"description": "",
"x-ogc-propertySeq": 0
}
},
"additionalProperties": false
};

return jsonSchema;
}

getData(id = null, withSchema = false) {
if (id !== null) {
if (typeof this.collections[id] !== 'undefined') {
Expand Down Expand Up @@ -257,12 +282,25 @@ export default class DataCatalog {
}
return l;
});
// STAC etc.
c.links.push({
rel: 'http://www.opengis.net/def/rel/ogc/1.0/queryables',
href: API.getUrl(`/collections/${c.id}/queryables`),
title: "Queryables",
type: "application/schema+json"
});
// OGC API - Coverages
c.links.push({
rel: "http://www.opengis.net/def/rel/ogc/1.0/schema",
href: API.getUrl(`/collections/${c.id}/schema`),
type: "application/schema+json"
});
// OGC API - Coverages
c.links.push({
rel: "http://www.opengis.net/def/rel/ogc/1.0/coverage",
href: API.getUrl(`/collections/${c.id}/schema`),
type: "image/png"
});
if (c["gee:type"] === 'image_collection') {
c.links.push({
rel: 'items',
Expand Down Expand Up @@ -538,6 +576,11 @@ export default class DataCatalog {
console.log("Invalid spatial extent for " + c.id);
}
else {
// OGC API - Coverages
c.extent.spatial.grid = [{},{}];
// c.extent.sparial.storageCrsBbox = []; // todo
c.extent.temporal.grid = {};

// spatial dimensions for all data types
const x2 = c.extent.spatial.bbox[0].length > 4 ? 3 : 2;
const y2 = c.extent.spatial.bbox[0].length > 4 ? 4 : 3;
Expand Down Expand Up @@ -578,6 +621,12 @@ export default class DataCatalog {
// Unfortunately, no other information available
c['cube:dimensions'].x.reference_system = c.summaries['proj:epsg'][0];
c['cube:dimensions'].y.reference_system = c.summaries['proj:epsg'][0];

// OGC API - Coverages
c.storageCrs = `http://www.opengis.net/def/crs/EPSG/0/${c.summaries['proj:epsg'][0]}`; // todo
c.crs = [
"EPSG:4326" // todo
];
}

if (!Utils.isObject(c.assets)) {
Expand Down

0 comments on commit f1af129

Please sign in to comment.