diff --git a/src/api/capabilities.js b/src/api/capabilities.js index 900b584..61945ce 100644 --- a/src/api/capabilities.js +++ b/src/api/capabilities.js @@ -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 { @@ -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) { diff --git a/src/api/collections.js b/src/api/collections.js index 287ff47..65bc8b0 100644 --- a/src/api/collections.js +++ b/src/api/collections.js @@ -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 runSync from './worker/sync.js'; const sortPropertyMap = { 'properties.datetime': 'system:time_start', @@ -10,6 +11,9 @@ const sortPropertyMap = { '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) { @@ -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)); @@ -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); } @@ -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; + + 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; + + 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(); diff --git a/src/api/coverages.js b/src/api/coverages.js new file mode 100644 index 0000000..0505aaf --- /dev/null +++ b/src/api/coverages.js @@ -0,0 +1,40 @@ +import API from "../utils/API"; + +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() { + + } + +} diff --git a/src/models/catalog.js b/src/models/catalog.js index cae28e9..18485dc 100644 --- a/src/models/catalog.js +++ b/src/models/catalog.js @@ -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; @@ -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') { @@ -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', @@ -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; @@ -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)) {