Comparing version 0.0.9 to 0.1.0
{ | ||
"name": "stac-js", | ||
"version": "0.0.9", | ||
"version": "0.1.0", | ||
"description": "JS drop-in classes with utilities for STAC", | ||
@@ -18,2 +18,6 @@ "author": "Matthias Mohr", | ||
}, | ||
"funding": { | ||
"type": "github", | ||
"url": "https://github.com/sponsors/m-mohr" | ||
}, | ||
"type": "module", | ||
@@ -27,3 +31,3 @@ "main": "src/index.js", | ||
"docs_lint": "npx documentation lint src/**", | ||
"lint": "eslint \"./src/**/*.js\" -c .eslintrc.json", | ||
"lint": "eslint \"./src/**/*.js\" -c eslint.config.mjs", | ||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", | ||
@@ -33,4 +37,7 @@ "check": "npm run docs_lint && npm run lint && npm test" | ||
"devDependencies": { | ||
"eslint": "^8.34.0", | ||
"eslint-plugin-jsdoc": "^40.0.0", | ||
"@eslint/eslintrc": "^3.2.0", | ||
"@eslint/js": "^9.18.0", | ||
"eslint": "^9.18.0", | ||
"eslint-plugin-jsdoc": "^50.0.0", | ||
"globals": "^15.14.0", | ||
"jest": "^29.0.0", | ||
@@ -40,5 +47,5 @@ "jest-html-reporter": "^3.3.0" | ||
"dependencies": { | ||
"@radiantearth/stac-migrate": "^1.5.0", | ||
"@radiantearth/stac-migrate": "^2.0.1", | ||
"urijs": "^1.19.11" | ||
} | ||
} |
@@ -9,3 +9,3 @@ # stac-js | ||
- **Package version:** 0.0.9 | ||
- **Package version:** 0.1.0 | ||
- **STAC versions:** >= 0.6.0 (through [stac-migrate](https://github.com/stac-utils/stac-migrate)). | ||
@@ -17,2 +17,3 @@ - **Documentation:** <https://m-mohr.github.io/stac-js/latest/> | ||
Automatically instantiate the right class through the factory: | ||
```js | ||
@@ -23,3 +24,3 @@ import create from 'stac-js'; | ||
const stac = { | ||
stac_version: "1.0.0", | ||
stac_version: "1.1.0", | ||
type: "Collection", | ||
@@ -33,2 +34,3 @@ id: "example", | ||
Directly instantiate `Asset`, `Catalog`, `Collection`, `CollectionCollection`, `Item` or `ItemCollection` through the class constructors: | ||
```js | ||
@@ -39,3 +41,3 @@ import { Collection } from 'stac-js'; // or Catalog or Item | ||
const stac = { | ||
stac_version: "1.0.0", | ||
stac_version: "1.1.0", | ||
type: "Collection", | ||
@@ -49,2 +51,3 @@ id: "example", | ||
You can then use the object, check whether it's STAC and call some methods, for example: | ||
```js | ||
@@ -66,2 +69,3 @@ import { STAC } from 'stac-js'; | ||
The classes are drop-in replacements, which means you can still access the objects as before: | ||
```js | ||
@@ -68,0 +72,0 @@ console.log(stac.id === obj.id); |
183
src/asset.js
@@ -1,4 +0,14 @@ | ||
import { getMaxForDataType, getMinForDataType, hasText, isObject, mergeArraysOfObjects } from "./utils.js"; | ||
import { getMinMaxValues, getNoDataValues, hasText, isObject } from "./utils.js"; | ||
import STACReference from './reference.js'; | ||
import Band from "./band.js"; | ||
const NO_INHERITANCE = [ | ||
'created', | ||
'updated', | ||
'published', | ||
'expires', | ||
'unpublished', | ||
'bands' | ||
]; | ||
/** | ||
@@ -23,3 +33,3 @@ * A STAC Asset or Item Asset Definition. | ||
constructor(data, key = null, context = null) { | ||
super(data, context, {}, ['_key']); | ||
super(data, context, { bands: Band.fromBands }, ['_key']); | ||
if (!this._key) { | ||
@@ -83,3 +93,3 @@ this._key = key; | ||
} | ||
if (this._context) { | ||
if (this._context && !NO_INHERITANCE.includes(field)) { | ||
return this._context.getMetadata(field); | ||
@@ -93,8 +103,6 @@ } | ||
* | ||
* This is usually a merge of eo:bands and raster:bands. | ||
* | ||
* @returns {Array.<Object>} | ||
* @returns {Array.<Band>} | ||
*/ | ||
getBands() { | ||
return mergeArraysOfObjects(this['eo:bands'], this['raster:bands']); | ||
return this.bands || []; | ||
} | ||
@@ -106,5 +114,5 @@ | ||
* @typedef {Object} VisualBands | ||
* @property {BandWithIndex} red The red band with its index | ||
* @property {BandWithIndex} green The green band with its index | ||
* @property {BandWithIndex} blue The blue band with its index | ||
* @property {Band} red The red band with its index | ||
* @property {Band} green The green band with its index | ||
* @property {Band} blue The blue band with its index | ||
*/ | ||
@@ -118,3 +126,3 @@ | ||
findVisualBands() { | ||
let rgb = { | ||
const rgb = { | ||
red: null, | ||
@@ -124,11 +132,11 @@ green: null, | ||
}; | ||
let bands = this.getBands(); | ||
for(let key in bands) { | ||
let index = parseInt(key, 10); // findIndex returns number, for loop uses strings?! | ||
let band = bands[index]; | ||
if (isObject(band) && hasText(band.common_name) && band.common_name in rgb) { | ||
rgb[band.common_name] = { index, band }; | ||
const bands = this.getBands(); | ||
for(const key in bands) { | ||
const index = parseInt(key, 10); // for loop may return strings as keys | ||
const band = bands[index]; | ||
if (isObject(band) && hasText(band['eo:common_name']) && band['eo:common_name'] in rgb) { | ||
rgb[band['eo:common_name']] = band; | ||
} | ||
} | ||
let complete = Object.values(rgb).every(o => o !== null); | ||
const complete = Object.values(rgb).every(o => o !== null); | ||
return complete ? rgb : null; | ||
@@ -144,16 +152,13 @@ } | ||
* @param {string} property The property in the bands to match against. | ||
* @param {Array.<Object>} bands For performance reasons you can provide a list of merged bands from `getBands()`. | ||
* @returns {BandWithIndex|null} | ||
* @returns {Band|null} | ||
* @see {getBands} | ||
*/ | ||
findBand(value, property = 'name', bands = null) { | ||
findBand(value, property = 'name') { | ||
if (!Array.isArray(value)) { | ||
value = [value]; | ||
} | ||
if (!isObject(bands)) { | ||
bands = this.getBands(); | ||
} | ||
let index = bands.findIndex(band => isObject(band) && value.includes(band[property])); | ||
const bands = this.getBands(); | ||
const index = bands.findIndex(band => isObject(band) && value.includes(band[property])); | ||
if (index >= 0) { | ||
return { index, band: bands[index] }; | ||
return bands[index]; | ||
} | ||
@@ -176,3 +181,3 @@ return null; | ||
} | ||
let bands = this.getBands(); | ||
const bands = this.getBands(); | ||
return bands[band] || null; | ||
@@ -182,131 +187,21 @@ } | ||
/** | ||
* Gets the reported minimum and maximum values for an asset (or band). | ||
* Gets the reported minimum and maximum values for an asset. | ||
* | ||
* Searches through different extension fields in raster, claasification, and file. | ||
* | ||
* @param {Object|number} band | ||
* @returns {Statistics} | ||
*/ | ||
getMinMaxValues(band = null) { | ||
band = this.getBand(band); | ||
/** | ||
* Statistics | ||
* | ||
* @typedef {Object} Statistics | ||
* @property {number|null} minimum Minimum value | ||
* @property {number|null} maximum Maximum value | ||
*/ | ||
const stats = { | ||
minimum: null, | ||
maximum: null | ||
}; | ||
// Checks whether the stats object is completely filled | ||
const isComplete = obj => obj.minimum !== null && obj.maximum !== null; | ||
// data sources: raster (statistics, histogram, data_type), classification, file (values, data_type) | ||
if (band) { | ||
if (isObject(band.statistics)) { | ||
if (typeof band.statistics.minimum === 'number') { | ||
stats.minimum = band.statistics.minimum; | ||
} | ||
if (typeof band.statistics.maximum === 'number') { | ||
stats.maximum = band.statistics.maximum; | ||
} | ||
if (isComplete(stats)) { | ||
return stats; | ||
} | ||
} | ||
if (isObject(band.histogram)) { | ||
if (typeof band.histogram.min === 'number') { | ||
stats.minimum = band.histogram.min; | ||
} | ||
if (typeof band.histogram.max === 'number') { | ||
stats.maximum = band.histogram.max; | ||
} | ||
if (isComplete(stats)) { | ||
return stats; | ||
} | ||
} | ||
} | ||
let classification = this.getMetadata("classification:classes"); | ||
if (Array.isArray(classification)) { | ||
classification.reduce((obj, cls) => { | ||
obj.minimum = Math.min(obj.minimum, cls.value); | ||
obj.maximum = Math.max(obj.maximum, cls.value); | ||
return obj; | ||
}, stats); | ||
if (isComplete(stats)) { | ||
return stats; | ||
} | ||
} | ||
let values = this.getMetadata("file:values"); | ||
if (Array.isArray(values)) { | ||
values.reduce((obj, map) => { | ||
obj.minimum = Math.min(obj.minimum, ...map.values); | ||
obj.maximum = Math.max(obj.maximum, ...map.values); | ||
return obj; | ||
}, stats); | ||
if (isComplete(stats)) { | ||
return stats; | ||
} | ||
} | ||
let data_type = (isObject(band) && band.data_type) || this.getMetadata("file:data_type"); | ||
if (data_type) { | ||
stats.minimum = getMinForDataType(data_type); | ||
stats.maximum = getMaxForDataType(data_type); | ||
} | ||
return stats; | ||
getMinMaxValues() { | ||
return getMinMaxValues(this); | ||
} | ||
/** | ||
* Gets the reported no-data values for an asset (or band). | ||
* Gets the reported no-data values for an asset. | ||
* | ||
* Searches through different extension fields in raster, claasification, and file. | ||
* Searches through different extension fields in nodata, classification, and file. | ||
* | ||
* @param {Object|number} band | ||
* @returns {Array.<*>} | ||
*/ | ||
getNoDataValues(band = null) { | ||
band = this.getBand(band); | ||
// data sources: raster (nodata), classification (nodata flag), file (nodata) | ||
let nodata = []; | ||
if (band && typeof band.nodata !== 'undefined') { | ||
nodata.push(band.nodata); | ||
} | ||
else { | ||
let file = this.getMetadata("file:nodata"); | ||
if (typeof file !== 'undefined') { | ||
nodata = file; | ||
} | ||
else { | ||
let classification = this.getMetadata("classification:classes"); | ||
if (Array.isArray(classification)) { | ||
nodata = classification | ||
.filter(cls => Boolean(cls.nodata)) | ||
.map(cls => cls.value); | ||
} | ||
} | ||
} | ||
return nodata.map(value => { | ||
if (value === "nan") { | ||
return NaN; | ||
} | ||
else if (value === "+inf") { | ||
return +Infinity; | ||
} | ||
else if (value === "-inf") { | ||
return -Infinity; | ||
} | ||
else { | ||
return value; | ||
} | ||
}); | ||
getNoDataValues() { | ||
return getNoDataValues(this); | ||
} | ||
@@ -313,0 +208,0 @@ |
import Asset from './asset.js'; | ||
import Band from './band.js'; | ||
import CatalogLike from './cataloglike.js'; | ||
import { isoToDate } from './datetime.js'; | ||
import { isBoundingBox, toGeoJSON } from './geo.js'; | ||
import { hasText, mergeArraysOfObjects } from './utils.js'; | ||
import { hasText } from './utils.js'; | ||
@@ -155,19 +156,13 @@ /** | ||
* | ||
* This is usually a merge of eo:bands and raster:bands from the summaries. | ||
* | ||
* @returns {Array.<Object>} | ||
* @returns {Array.<Band>} | ||
*/ | ||
getBands() { | ||
let eo = this.getSummary('eo:bands'); | ||
let raster = this.getSummary('raster:bands'); | ||
let all = [eo, raster].filter(arr => Array.isArray(arr)); | ||
if (all.length >= 2) { | ||
return mergeArraysOfObjects(...all); | ||
let bands = this.getMetadata('bands'); | ||
if (!Array.isArray(bands)) { | ||
bands = this.getSummary('bands'); | ||
} | ||
else if (all.length === 1) { | ||
return all[0]; | ||
} | ||
else { | ||
if (!Array.isArray(bands)) { | ||
return []; | ||
} | ||
return Band.fromBands(bands, this); | ||
} | ||
@@ -174,0 +169,0 @@ |
@@ -16,3 +16,3 @@ import { hasText } from "./utils.js"; | ||
return new Date(Date.UTC(dt[0], dt[1] - 1, dt[2], dt[3], dt[4], dt[5], dt[6] || 0)); | ||
} catch(error) { | ||
} catch { | ||
return null; | ||
@@ -19,0 +19,0 @@ } |
@@ -6,2 +6,3 @@ import Asset from './asset.js'; | ||
import STAC from './stac.js'; | ||
import Band from './band.js'; | ||
@@ -138,11 +139,10 @@ /** | ||
* @todo Merge bands from assets | ||
* @returns {Array.<Object>} | ||
* @returns {Array.<Band>} | ||
*/ | ||
getBands() { | ||
let eo = this.getMetadata('eo:bands'); | ||
if (Array.isArray(eo)) { | ||
return eo; | ||
const bands = this.getMetadata('bands'); | ||
if (Array.isArray(bands)) { | ||
return Band.fromBands(bands, this); | ||
} | ||
else { | ||
// todo: merge bands from assets? | ||
return []; | ||
@@ -149,0 +149,0 @@ } |
@@ -110,3 +110,3 @@ import { centerOfBoundingBox } from './geo.js'; | ||
/** | ||
* Check whether this given object is a STAC LInk. | ||
* Check whether this given object is a STAC Link. | ||
* | ||
@@ -120,2 +120,11 @@ * @returns {boolean} `true` if the object is a STAC Link, `false` otherwise. | ||
/** | ||
* Check whether this given object is a STAC Band. | ||
* | ||
* @returns {boolean} `true` if the object is a STAC Band, `false` otherwise. | ||
*/ | ||
isBand() { | ||
return false; | ||
} | ||
/** | ||
* Returns the type of the STAC object. | ||
@@ -131,2 +140,3 @@ * | ||
* - Link | ||
* - Band | ||
* @abstract | ||
@@ -133,0 +143,0 @@ * @returns {string} |
@@ -18,3 +18,3 @@ import { browserProtocols, toAbsolute } from './http.js'; | ||
* @param {Object} data The STAC API Collection object | ||
* @param {STAC|null} context The object that contains the link | ||
* @param {STAC|null} context The object that contains the reference | ||
* @param {Object.<string, function>} keyMap Keys and functions that convert the values to stac-js objects. | ||
@@ -21,0 +21,0 @@ * @param {Array.<string>} privateKeys Keys that are private members of the stac-js objects (for cloning and export). |
@@ -177,5 +177,5 @@ import { geotiffMediaTypes, isMediaType } from './mediatypes.js'; | ||
* @typedef {Object} VisualAssets | ||
* @property {BandWithIndex} red The red band with its index | ||
* @property {BandWithIndex} green The green band with its index | ||
* @property {BandWithIndex} blue The blue band with its index | ||
* @property {Band} red The red band with its index | ||
* @property {Band} green The green band with its index | ||
* @property {Band} blue The blue band with its index | ||
*/ | ||
@@ -197,9 +197,5 @@ | ||
for(let asset of assets) { | ||
let bands = asset.getBands(); | ||
if (bands.length !== 1) { | ||
continue; | ||
} | ||
let result = asset.findBand(names, 'common_name', bands); | ||
let result = asset.findBand(names, 'eo:common_name'); | ||
if (result) { | ||
rgb[result.band.common_name] = asset; | ||
rgb[result["eo:common_name"]] = asset; | ||
} | ||
@@ -206,0 +202,0 @@ } |
@@ -14,10 +14,2 @@ /** | ||
/** | ||
* A band with the corresponding index. | ||
* | ||
* @typedef {Object} BandWithIndex | ||
* @property {number} index The index in the bands array. | ||
* @property {Object} band The band object | ||
*/ | ||
/** | ||
* A data provider. | ||
@@ -24,0 +16,0 @@ * |
131
src/utils.js
@@ -98,1 +98,132 @@ /** | ||
} | ||
/** | ||
* Gets the reported minimum and maximum values for a STAC object. | ||
* | ||
* Searches through different extension fields in raster, claasification, and file. | ||
* | ||
* @param {StacObject} object | ||
* @returns {Statistics} | ||
*/ | ||
export function getMinMaxValues(object) { | ||
/** | ||
* Statistics | ||
* | ||
* @typedef {Object} Statistics | ||
* @property {number|null} minimum Minimum value | ||
* @property {number|null} maximum Maximum value | ||
*/ | ||
const stats = { | ||
minimum: null, | ||
maximum: null | ||
}; | ||
// Checks whether the stats object is completely filled | ||
const isComplete = obj => obj.minimum !== null && obj.maximum !== null; | ||
// data sources: raster (statistics, histogram, data_type), classification, file (values, data_type) | ||
const statistics = object.getMetadata("statistics"); | ||
if (isObject(statistics)) { | ||
if (typeof statistics.minimum === 'number') { | ||
stats.minimum = statistics.minimum; | ||
} | ||
if (typeof statistics.maximum === 'number') { | ||
stats.maximum = statistics.maximum; | ||
} | ||
if (isComplete(stats)) { | ||
return stats; | ||
} | ||
} | ||
const histogram = object.getMetadata("raster:histogram"); | ||
if (isObject(histogram)) { | ||
if (typeof histogram.min === 'number') { | ||
stats.minimum = histogram.min; | ||
} | ||
if (typeof histogram.max === 'number') { | ||
stats.maximum = histogram.max; | ||
} | ||
if (isComplete(stats)) { | ||
return stats; | ||
} | ||
} | ||
const classification = object.getMetadata("classification:classes"); | ||
if (Array.isArray(classification)) { | ||
classification.reduce((obj, cls) => { | ||
obj.minimum = Math.min(obj.minimum, cls.value); | ||
obj.maximum = Math.max(obj.maximum, cls.value); | ||
return obj; | ||
}, stats); | ||
if (isComplete(stats)) { | ||
return stats; | ||
} | ||
} | ||
const values = object.getMetadata("file:values"); | ||
if (Array.isArray(values)) { | ||
values.reduce((obj, map) => { | ||
obj.minimum = Math.min(obj.minimum, ...map.values); | ||
obj.maximum = Math.max(obj.maximum, ...map.values); | ||
return obj; | ||
}, stats); | ||
if (isComplete(stats)) { | ||
return stats; | ||
} | ||
} | ||
const data_type = object.getMetadata("data_type"); | ||
if (data_type) { | ||
stats.minimum = getMinForDataType(data_type); | ||
stats.maximum = getMaxForDataType(data_type); | ||
} | ||
return stats; | ||
} | ||
/** | ||
* Gets the reported no-data values for a STAC Object. | ||
* | ||
* Searches through different extension fields in nodata, classification, and file. | ||
* | ||
* @param {StacObject} object | ||
* @returns {Array.<*>} | ||
*/ | ||
export function getNoDataValues(object) { | ||
// data sources: raster (nodata), classification (nodata flag), file (nodata) | ||
let nodata = []; | ||
const common = object.getMetadata("nodata"); | ||
if (typeof common !== 'undefined') { | ||
nodata.push(common); | ||
} | ||
else { | ||
const file = object.getMetadata("file:nodata"); | ||
if (typeof file !== 'undefined') { | ||
nodata = file; | ||
} | ||
else { | ||
const classification = object.getMetadata("classification:classes"); | ||
if (Array.isArray(classification)) { | ||
nodata = classification | ||
.filter(cls => Boolean(cls.nodata)) | ||
.map(cls => cls.value); | ||
} | ||
} | ||
} | ||
return nodata.map(value => { | ||
if (value === "nan") { | ||
return NaN; | ||
} | ||
else if (value === "+inf") { | ||
return +Infinity; | ||
} | ||
else if (value === "-inf") { | ||
return -Infinity; | ||
} | ||
else { | ||
return value; | ||
} | ||
}); | ||
} |
82495
24
2410
74
7
+ Added@radiantearth/stac-migrate@2.0.1(transitive)
+ Addedcall-bind-apply-helpers@1.0.2(transitive)
+ Addedfor-each@0.3.5(transitive)
- Removed@radiantearth/stac-migrate@1.6.0(transitive)
- Removedcall-bind-apply-helpers@1.0.1(transitive)
- Removedfor-each@0.3.4(transitive)