@radiantearth/stac-migrate 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +25 -10
  2. package/bin/cli.js +70 -0
  3. package/migrate.js +926 -816
  4. package/package.json +8 -3
package/migrate.js CHANGED
@@ -1,817 +1,927 @@
1
- // Migration rules partly based on PySTAC and @openeo/js-commons,
2
- // see https://github.com/stac-utils/pystac/tree/v0.5.4/pystac/serialization/migrate.py
3
- // and https://github.com/Open-EO/openeo-js-commons/tree/v1.2.0/src/migrate/collections.js
4
-
5
- var compareVersions = require('compare-versions');
6
-
7
- const LATEST_VERSION = '1.0.0';
8
- const DONE = true; // This is used to verify in code coverage whether something has been used or not
9
- const SCHEMAS = {
10
- 'datacube': 'https://stac-extensions.github.io/datacube/v1.0.0/schema.json',
11
- 'eo': 'https://stac-extensions.github.io/eo/v1.0.0/schema.json',
12
- 'file': 'https://stac-extensions.github.io/file/v1.0.0/schema.json',
13
- 'item-assets': 'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json',
14
- 'label': 'https://stac-extensions.github.io/label/v1.0.0/schema.json',
15
- 'pointcloud': 'https://stac-extensions.github.io/pointcloud/v1.0.0/schema.json',
16
- 'processing': 'https://stac-extensions.github.io/processing/v1.0.0/schema.json',
17
- 'projection': 'https://stac-extensions.github.io/projection/v1.0.0/schema.json',
18
- 'raster': 'https://stac-extensions.github.io/raster/v1.0.0/schema.json',
19
- 'sar': 'https://stac-extensions.github.io/sar/v1.0.0/schema.json',
20
- 'sat': 'https://stac-extensions.github.io/sat/v1.0.0/schema.json',
21
- 'scientific': 'https://stac-extensions.github.io/scientific/v1.0.0/schema.json',
22
- 'timestamps': 'https://stac-extensions.github.io/timestamps/v1.0.0/schema.json',
23
- 'version': 'https://stac-extensions.github.io/version/v1.0.0/schema.json',
24
- 'view': 'https://stac-extensions.github.io/view/v1.0.0/schema.json'
25
- };
26
- const EXTENSIONS = {
27
- // Add a : at the end to indicate it has a prefix, otherwise list all fields separately (see version extension for example).
28
- itemAndCollection: {
29
- // with prefix
30
- 'cube:': SCHEMAS.datacube,
31
- 'eo:': SCHEMAS.eo,
32
- 'file:': SCHEMAS.file,
33
- 'label:': SCHEMAS.label,
34
- 'pc:': SCHEMAS.pointcloud,
35
- 'processing:': SCHEMAS.processing,
36
- 'proj:': SCHEMAS.projection,
37
- 'raster:': SCHEMAS.raster,
38
- 'sar:': SCHEMAS.sar,
39
- 'sat:': SCHEMAS.sat,
40
- 'sci:': SCHEMAS.scientific,
41
- 'view:': SCHEMAS.view,
42
- // without prefix
43
- 'version': SCHEMAS.version,
44
- 'deprecated': SCHEMAS.version,
45
- 'published': SCHEMAS.timestamps,
46
- 'expires': SCHEMAS.timestamps,
47
- 'unpublished': SCHEMAS.timestamps
48
- },
49
- catalog: {
50
- // None yet
51
- },
52
- collection: {
53
- 'item_assets': SCHEMAS['item-assets']
54
- },
55
- item: {
56
- // None yet
57
- },
58
- };
59
- EXTENSIONS.collection = Object.assign(EXTENSIONS.collection, EXTENSIONS.itemAndCollection);
60
- EXTENSIONS.item = Object.assign(EXTENSIONS.item, EXTENSIONS.itemAndCollection);
61
-
62
- var V = {
63
- version: LATEST_VERSION,
64
-
65
- set(version) {
66
- if (!version) {
67
- version = '0.6.0'; // Assume the worst case, it doesn't seem there's a clear indicator for 0.7.0
68
- }
69
- V.version = version;
70
- },
71
-
72
- before(version) {
73
- return compareVersions.compare(V.version, version, '<');
74
- }
75
- };
76
-
77
- var _ = {
78
-
79
- type(val) {
80
- let type = typeof val;
81
- if (type === 'object') {
82
- if (val === null) {
83
- return 'null';
84
- }
85
- else if (Array.isArray(val)) {
86
- return 'array';
87
- }
88
- }
89
- return type;
90
- },
91
-
92
- is(val, type) {
93
- return (_.type(val) === type);
94
- },
95
-
96
- isDefined(val) {
97
- return (typeof val !== 'undefined');
98
- },
99
-
100
- isObject(obj) {
101
- return (typeof obj === 'object' && obj === Object(obj) && !Array.isArray(obj));
102
- },
103
-
104
- rename(obj, oldKey, newKey) {
105
- if (typeof obj[oldKey] !== 'undefined' && typeof obj[newKey] === 'undefined') {
106
- obj[newKey] = obj[oldKey];
107
- delete obj[oldKey];
108
- return true;
109
- }
110
- return false;
111
- },
112
-
113
- toArray(obj, key) {
114
- if (typeof obj[key] !== 'undefined' && !Array.isArray(obj[key])) {
115
- obj[key] = [obj[key]];
116
- return true;
117
- }
118
- return false;
119
- },
120
-
121
- flattenArray(obj, key, newKeys, summary = false) {
122
- if (Array.isArray(obj[key])) {
123
- for(let i in obj[key]) {
124
- if (typeof newKeys[i] === 'string') {
125
- let value = obj[key][i];
126
- obj[newKeys[i]] = summary ? [value] : value;
127
- }
128
- }
129
- delete obj[key];
130
- return true;
131
- }
132
- return false;
133
- },
134
-
135
- flattenOneElementArray(obj, key, summary = false) {
136
- if (!summary && Array.isArray(obj[key])) {
137
- if (obj[key].length === 1) {
138
- obj[key] = obj[key][0];
139
- return true;
140
- }
141
- else {
142
- return false; // It's still an array and we don't know which element to choose
143
- }
144
- }
145
- return true; // It's already a single element
146
- },
147
-
148
- removeFromArray(obj, key, valueToRemove) {
149
- if (Array.isArray(obj[key])) {
150
- let index = obj[key].indexOf(valueToRemove);
151
- if (index > -1) {
152
- obj[key].splice(index, 1);
153
- }
154
- return true;
155
- }
156
- return false;
157
- },
158
-
159
- addToArrayIfNotExists(obj, key, valueToAdd) {
160
- if (Array.isArray(obj[key])) {
161
- let index = obj[key].indexOf(valueToAdd);
162
- if (index === -1) {
163
- obj[key].push(valueToAdd);
164
- }
165
- obj[key].sort();
166
- return true;
167
- }
168
- return false;
169
- },
170
-
171
- ensure(obj, key, defaultValue) {
172
- if (_.type(defaultValue) !== _.type(obj[key])) {
173
- obj[key] = defaultValue;
174
- }
175
- return true;
176
- },
177
-
178
- addExtension(context, newExtension) {
179
- if (!_.isObject(context)) {
180
- return true; // We are likely in summaries and don't need to do anything
181
- }
182
-
183
- return _.addToArrayIfNotExists(context, 'stac_extensions', newExtension) && DONE;
184
- },
185
-
186
- removeExtension(context, oldExtension) {
187
- if (!_.isObject(context)) {
188
- return true; // We are likely in summaries and don't need to do anything
189
- }
190
-
191
- return _.removeFromArray(context, 'stac_extensions', oldExtension) && DONE;
192
- },
193
-
194
- migrateExtensionShortnames(context) {
195
- let oldShortnames = Object.keys(SCHEMAS);
196
- let newSchemas = Object.values(SCHEMAS);
197
- return _.mapValues(context, 'stac_extensions', oldShortnames, newSchemas);
198
- },
199
-
200
- populateExtensions(context, type) {
201
- let objectsToCheck = [];
202
- if (type == 'catalog' || type == 'collection') {
203
- objectsToCheck.push(context);
204
- }
205
- if ((type == 'item' || type == 'collection') && _.isObject(context.assets)) {
206
- objectsToCheck = objectsToCheck.concat(Object.values(context.assets));
207
- }
208
- if (type == 'collection' && _.isObject(context.item_assets)) {
209
- objectsToCheck = objectsToCheck.concat(Object.values(context.item_assets));
210
- }
211
- if (type == 'collection' && _.isObject(context.summaries)) {
212
- objectsToCheck = objectsToCheck.concat(Object.values(context.summaries));
213
- }
214
- if (type == 'item' && _.isObject(context.properties)) {
215
- objectsToCheck.push(context.properties);
216
- }
217
-
218
- for (let obj of objectsToCheck) {
219
- Object.keys(obj).forEach(key => {
220
- let prefix = key.match(/^(\w+:|[^:]+$)/i);
221
- if (Array.isArray(prefix)) {
222
- let ext = EXTENSIONS[type][prefix[0]];
223
- _.is(ext, 'string') && _.addExtension(context, ext) && DONE;
224
- }
225
- });
226
- }
227
- },
228
-
229
- mapValues(obj, key, oldValues, newValues) {
230
- let fn = value => {
231
- let index = oldValues.indexOf(value);
232
- if (index >= 0) {
233
- return newValues[index];
234
- }
235
- return value;
236
- };
237
- if (Array.isArray(obj[key])) {
238
- obj[key] = obj[key].map(fn);
239
- }
240
- else if (typeof obj[key] !== 'undefined') {
241
- obj[key] = fn(obj[key]);
242
- }
243
- return true;
244
- },
245
-
246
- mapObject(obj, fn) {
247
- for(let key in obj) {
248
- obj[key] = fn(obj[key], key);
249
- }
250
- },
251
-
252
- moveTo(obj, key, context, fromSummary = false, mergedSummary = false) {
253
- let condition;
254
- if (fromSummary) {
255
- if (mergedSummary) {
256
- condition = val => Array.isArray(val);
257
- }
258
- else {
259
- condition = val => Array.isArray(val) && val.length === 1;
260
- }
261
- }
262
- else {
263
- condition = _.isDefined
264
- }
265
- if (condition(obj[key])) {
266
- context[key] = fromSummary && !mergedSummary ? obj[key][0] : obj[key];
267
- delete obj[key];
268
- return true;
269
- }
270
- return false;
271
- },
272
-
273
- runAll(migrations, obj, context = null) {
274
- for(let fn in migrations) {
275
- if (!fn.startsWith('migrate')) {
276
- migrations[fn](obj, context);
277
- }
278
- }
279
- },
280
-
281
- toUTC(obj, key) {
282
- if (typeof obj[key] === 'string') {
283
- try {
284
- obj[key] = this.toISOString(obj[key]);
285
- return true;
286
- } catch(error) {}
287
- }
288
- delete obj[key];
289
- return false;
290
- },
291
-
292
- toISOString(date) {
293
- if (!(date instanceof Date)) {
294
- date = new Date(date);
295
- }
296
- return date.toISOString().replace('.000', ''); // Don't export milliseconds if not needed
297
- }
298
-
299
- };
300
-
301
- var Checksum = {
302
-
303
- multihash: null,
304
-
305
- hexToUint8(hexString) {
306
- if(hexString.length === 0 || hexString.length % 2 !== 0){
307
- throw new Error(`The string "${hexString}" is not valid hex.`)
308
- }
309
- return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
310
- },
311
-
312
- uint8ToHex(bytes) {
313
- return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
314
- },
315
-
316
- toMultihash(obj, key, algo) {
317
- if (!Checksum.multihash || !_.is(obj[key], 'string')) {
318
- return false;
319
- }
320
- try {
321
- const encoded = Checksum.multihash.encode(Checksum.hexToUint8(obj[key]), algo);
322
- obj[key] = Checksum.uint8ToHex(encoded);
323
- return true;
324
- } catch (error) {
325
- console.warn(error);
326
- return false;
327
- }
328
- }
329
-
330
- };
331
-
332
- var Catalog = {
333
-
334
- migrate(catalog, updateVersionNumber = true) {
335
- V.set(catalog.stac_version);
336
- if (updateVersionNumber) {
337
- catalog.stac_version = LATEST_VERSION;
338
- }
339
- catalog.type = 'Catalog';
340
- V.before('1.0.0-rc.1') && _.migrateExtensionShortnames(catalog) && DONE;
341
-
342
- _.ensure(catalog, 'id', '') && DONE;
343
- _.ensure(catalog, 'description', '') && DONE;
344
- _.ensure(catalog, 'links', []) && DONE;
345
-
346
- _.runAll(Catalog, catalog, catalog);
347
-
348
- _.ensure(catalog, 'stac_extensions', []) && DONE;
349
- V.before('0.8.0') && _.populateExtensions(catalog, 'catalog') && DONE;
350
- },
351
-
352
- openeo(obj) {
353
- _.rename(obj, 'api_version', 'openeo:api_version') && DONE;
354
- _.rename(obj, 'backend_version', 'openeo:backend_version') && DONE;
355
- _.rename(obj, 'production', 'openeo:production') && DONE;
356
- _.rename(obj, 'endpoints', 'openeo:endpoints') && DONE;
357
- _.rename(obj, 'billing', 'openeo:billing') && DONE;
358
- },
359
-
360
- };
361
-
362
- var Collection = {
363
-
364
- migrate(collection, updateVersionNumber = true) {
365
- Catalog.migrate(collection, updateVersionNumber); // Migrates stac_version, stac_extensions, id, title, description, links
366
- collection.type = 'Collection';
367
- V.before('1.0.0-rc.1') && _.migrateExtensionShortnames(collection) && DONE;
368
-
369
- _.ensure(collection, 'license', 'proprietary') && DONE;
370
- _.ensure(collection, 'extent', {
371
- spatial: {
372
- bbox: []
373
- },
374
- temporal: {
375
- interval: []
376
- }
377
- }) && DONE;
378
-
379
- _.runAll(Collection, collection, collection);
380
-
381
- // Migrate Commons extension - part 3
382
- if (_.isObject(collection.properties)) {
383
- _.removeFromArray(collection, 'stac_extensions', 'commons') && DONE;
384
- delete collection.properties;
385
- }
386
-
387
- V.before('0.8.0') && _.populateExtensions(collection, 'collection') && DONE;
388
- V.before('1.0.0-beta.1') && _.mapValues(collection, 'stac_extensions', ['assets'], ['item-assets']) && DONE;
389
- },
390
-
391
- extent(collection) {
392
- _.ensure(collection, "extent", {});
393
-
394
- if (V.before('0.8.0')) {
395
- // Restructure spatial extent
396
- if (Array.isArray(collection.extent.spatial)) {
397
- collection.extent.spatial = {
398
- bbox: [
399
- collection.extent.spatial
400
- ]
401
- };
402
- }
403
- // Restructure temporal extent
404
- if (Array.isArray(collection.extent.temporal)) {
405
- collection.extent.temporal = {
406
- interval: [
407
- collection.extent.temporal
408
- ]
409
- };
410
- }
411
- }
412
-
413
- _.ensure(collection.extent, "spatial", {});
414
- _.ensure(collection.extent.spatial, "bbox", []);
415
- _.ensure(collection.extent, "temporal", {});
416
- _.ensure(collection.extent.temporal, "interval", []);
417
-
418
- if (V.before('1.0.0-rc.3')) {
419
- // The first extent in a Collection is always the overall extent, followed by more specific extents.
420
- if (collection.extent.temporal.interval.length > 1) {
421
- let min, max;
422
- for(let interval of collection.extent.temporal.interval) {
423
- if (interval[0] === null) {
424
- min = null;
425
- }
426
- else if (typeof interval[0] === 'string' && min !== null) {
427
- try {
428
- let start = new Date(interval[0]);
429
- if (typeof min === 'undefined' || start < min) {
430
- min = start;
431
- }
432
- } catch (error) {}
433
- }
434
-
435
- if (interval[1] === null) {
436
- max = null;
437
- }
438
- else if (typeof interval[1] === 'string' && max !== null) {
439
- try {
440
- let end = new Date(interval[1]);
441
- if (typeof max === 'undefined' || end > max) {
442
- max = end;
443
- }
444
- } catch (error) {}
445
- }
446
- }
447
- collection.extent.temporal.interval.unshift([
448
- min ? _.toISOString(min) : null,
449
- max ? _.toISOString(max) : null
450
- ]);
451
- }
452
- if (collection.extent.spatial.bbox.length > 1) {
453
- let count = collection.extent.spatial.bbox.reduce((val, bbox) => Array.isArray(bbox) ? Math.max(bbox.length, val) : val, 4);
454
- if (count >= 4) {
455
- let union = new Array(count).fill(null);
456
- let middle = count / 2;
457
- for(let bbox of collection.extent.spatial.bbox) {
458
- if (!Array.isArray(bbox) || bbox.length < 4) {
459
- break;
460
- }
461
- for(let i in bbox) {
462
- let c = bbox[i];
463
- if (union[i] === null) {
464
- union[i] = c;
465
- }
466
- else if (i < middle) {
467
- union[i] = Math.min(c, union[i]);
468
- }
469
- else {
470
- union[i] = Math.max(c, union[i]);
471
- }
472
-
473
- }
474
- }
475
- if (union.findIndex(c => c === null) === -1) {
476
- collection.extent.spatial.bbox.unshift(union);
477
- }
478
- }
479
- }
480
- }
481
- },
482
-
483
- collectionAssets(collection) {
484
- V.before('1.0.0-rc.1') && _.removeExtension(collection, 'collection-assets') && DONE;
485
-
486
- Asset.migrateAll(collection);
487
- },
488
-
489
- itemAsset(collection) {
490
- V.before('1.0.0-beta.2') && _.rename(collection, 'item_assets', 'assets');
491
-
492
- Asset.migrateAll(collection, 'item_assets');
493
- },
494
-
495
- summaries(collection) {
496
- _.ensure(collection, 'summaries', {});
497
-
498
- // other_properties: An early version of the Collection summaries with a similar structure
499
- // Was mostly used in standalone collctions from openEO and GEE. Move other_properties to summaries.
500
- if (V.before('0.8.0') && _.isObject(collection.other_properties)) {
501
- for(let key in collection.other_properties) {
502
- let prop = collection.other_properties[key];
503
- if (Array.isArray(prop.extent) && prop.extent.length === 2) {
504
- collection.summaries[key] = {
505
- minimum: prop.extent[0],
506
- maximum: prop.extent[1],
507
- };
508
- }
509
- else if (Array.isArray(prop.values)) {
510
- if (prop.values.filter(v => Array.isArray(v)).length === prop.values.length) {
511
- // If it is an array of arrays, merge the arrays
512
- collection.summaries[key] = prop.values.reduce((a, b) => a.concat(b), []);
513
- }
514
- else {
515
- collection.summaries[key] = prop.values;
516
- }
517
- }
518
- }
519
- delete collection.other_properties;
520
- }
521
-
522
- // Migrate Commons extension - part 2
523
- // Move properties to (single element) summaries if the Collection is standalone
524
- // see also https://github.com/stac-utils/stac-migrate/issues/3
525
- if (V.before('1.0.0-beta.1') && _.isObject(collection.properties) && !collection.links.find(link => ['child', 'item'].includes(link.rel))) {
526
- for(let key in collection.properties) {
527
- let value = collection.properties[key];
528
- if (!Array.isArray(value)) {
529
- value = [value];
530
- }
531
- collection.summaries[key] = value;
532
- }
533
- }
534
-
535
- if (V.before('1.0.0-rc.1')) {
536
- _.mapObject(collection.summaries, val => {
537
- _.rename(val, 'min', 'minimum') && DONE;
538
- _.rename(val, 'max', 'maximum') && DONE;
539
- return val;
540
- });
541
- }
542
-
543
- // now we can work on all summaries and migrate them
544
- Fields.migrate(collection.summaries);
545
-
546
- // Some fields should usually be on root-level if there's only one element
547
- _.moveTo(collection.summaries, 'sci:doi', collection, true) && _.addExtension(collection, SCHEMAS.scientific) && DONE;
548
- _.moveTo(collection.summaries, 'sci:publications', collection, true, true) && _.addExtension(collection, SCHEMAS.scientific) && DONE;
549
- _.moveTo(collection.summaries, 'sci:citation', collection, true) && _.addExtension(collection, SCHEMAS.scientific) && DONE;
550
- _.moveTo(collection.summaries, 'cube:dimensions', collection, true) && _.addExtension(collection, SCHEMAS.datacube) && DONE;
551
-
552
- // Remove summary field if empty
553
- if (Object.keys(collection.summaries).length === 0) {
554
- delete collection.summaries;
555
- }
556
- }
557
-
558
- };
559
-
560
- var Item = {
561
-
562
- migrate(item, collection = null, updateVersionNumber = true) {
563
- V.set(item.stac_version);
564
- if (updateVersionNumber) {
565
- item.stac_version = LATEST_VERSION;
566
- }
567
- V.before('1.0.0-rc.1') && _.migrateExtensionShortnames(item) && DONE;
568
-
569
- _.ensure(item, 'id', '') && DONE;
570
- _.ensure(item, 'type', 'Feature') && DONE;
571
- if (!_.isObject(item.geometry)) {
572
- item.geometry = null;
573
- }
574
- if (item.geometry !== null) {
575
- _.ensure(item, 'bbox', []) && DONE;
576
- }
577
- _.ensure(item, 'properties', {}) && DONE;
578
- _.ensure(item, 'links', []) && DONE;
579
- _.ensure(item, 'assets', {}) && DONE;
580
-
581
- // Migrate Commons extension - part 1
582
- let commons = false;
583
- if (_.isObject(collection) && _.isObject(collection.properties)) {
584
- _.removeFromArray(item, 'stac_extensions', 'commons');
585
- item.properties = Object.assign({}, collection.properties, item.properties);
586
- commons = true;
587
- }
588
-
589
- _.runAll(Item, item, item);
590
-
591
- Fields.migrate(item.properties, item);
592
-
593
- Asset.migrateAll(item);
594
-
595
- _.ensure(item, 'stac_extensions', []) && DONE;
596
- // Also populate extensions if commons has been implemented
597
- (V.before('0.8.0') || commons) && _.populateExtensions(item, 'item') && DONE;
598
- }
599
-
600
- };
601
-
602
- var Asset = {
603
-
604
- migrateAll(context, field = 'assets') {
605
- for(let key in context[field]) {
606
- Asset.migrate(context[field][key], context);
607
- }
608
- },
609
-
610
- migrate(asset, context) {
611
- _.runAll(Asset, asset, context);
612
- Fields.migrate(asset, context);
613
- },
614
-
615
- mediaTypes(asset) {
616
- _.is(asset.type, 'string') && _.mapValues(
617
- asset, 'type',
618
- ['image/vnd.stac.geotiff', 'image/vnd.stac.geotiff; cloud-optimized=true'],
619
- ['image/tiff; application=geotiff', 'image/tiff; application=geotiff; profile=cloud-optimized']
620
- );
621
- },
622
-
623
- eo(asset, context) {
624
- let bands = _.isObject(context.properties) && Array.isArray(context.properties['eo:bands']) ? context.properties['eo:bands'] : [];
625
- if (Array.isArray(asset['eo:bands'])) {
626
- for(let i in asset['eo:bands']) {
627
- let band = asset['eo:bands'][i];
628
- if (_.is(band, 'number') && _.isObject(bands[band])) {
629
- band = bands[band];
630
- }
631
- else if (!_.isObject(band)) {
632
- band = {}; // "Fix" invalid band index
633
- }
634
- asset['eo:bands'][i] = band;
635
- }
636
- }
637
- }
638
-
639
- };
640
-
641
- var Fields = {
642
-
643
- // If no context is given, we are working in summaries
644
- migrate(obj, context) {
645
- _.runAll(Fields, obj, context);
646
- },
647
-
648
- _commonMetadata(obj) {
649
- // Timestamps must be always in UTC
650
- // datetime, start_datetime and end_datetime already required UTC before
651
- if (V.before('1.0.0-rc.3')) {
652
- _.toUTC(obj, 'created') && DONE;
653
- _.toUTC(obj, 'updated') && DONE;
654
- }
655
- },
656
-
657
- _timestamps(obj) {
658
- // Timestamps must be always in UTC
659
- _.toUTC(obj, 'published') && DONE;
660
- _.toUTC(obj, 'expires') && DONE;
661
- _.toUTC(obj, 'unpublished') && DONE;
662
- },
663
-
664
- _versioningIndicator(obj) {
665
- // Nothing to do
666
- },
667
-
668
- checksum(obj, context) {
669
- if (V.before('0.9.0') && Checksum.multihash) {
670
- _.rename(obj, 'checksum:md5', 'checksum:multihash') && Checksum.toMultihash(obj, 'checksum:multihash', 'md5') && DONE;
671
- _.rename(obj, 'checksum:sha1', 'checksum:multihash') && Checksum.toMultihash(obj, 'checksum:multihash', 'sha1') && DONE;
672
- // We assume sha2/3-256 although that may fail in some cases and other lengths are chosen
673
- // Never seen this implemtned in the wild, so let's try this until another use case comes up
674
- _.rename(obj, 'checksum:sha2', 'checksum:multihash') && Checksum.toMultihash(obj, 'checksum:multihash', 'sha2-256') && DONE;
675
- _.rename(obj, 'checksum:sha3', 'checksum:multihash') && Checksum.toMultihash(obj, 'checksum:multihash', 'sha3-256') && DONE;
676
- }
677
-
678
- V.before('1.0.0-rc.1') && _.rename(obj, 'checksum:multihash', 'file:checksum') && _.addExtension(context, SCHEMAS.file) && DONE;
679
- },
680
-
681
- cube() {
682
- // Nothing to do
683
- },
684
-
685
- dtr(obj, context) {
686
- if (V.before('0.9.0')) {
687
- _.rename(obj, 'dtr:start_datetime', 'start_datetime') && DONE;
688
- _.rename(obj, 'dtr:end_datetime', 'end_datetime') && DONE;
689
- _.removeExtension(context, 'datetime-range') && DONE;
690
- }
691
- },
692
-
693
- eo(obj, context) {
694
- if (V.before('0.9.0')) {
695
- _.rename(obj, 'eo:epsg', 'proj:epsg') && _.addExtension(context, SCHEMAS.projection) && DONE;
696
- _.rename(obj, 'eo:platform', 'platform') && DONE;
697
- _.rename(obj, 'eo:instrument', 'instruments') && _.toArray(obj, 'instruments') && DONE;
698
- _.rename(obj, 'eo:constellation', 'constellation') && DONE;
699
- _.rename(obj, 'eo:off_nadir', 'view:off_nadir') && _.addExtension(context, SCHEMAS.view) && DONE;
700
- _.rename(obj, 'eo:azimuth', 'view:azimuth') && _.addExtension(context, SCHEMAS.view) && DONE;
701
- _.rename(obj, 'eo:incidence_angle', 'view:incidence_angle') && _.addExtension(context, SCHEMAS.view) && DONE;
702
- _.rename(obj, 'eo:sun_azimuth', 'view:sun_azimuth') && _.addExtension(context, SCHEMAS.view) && DONE;
703
- _.rename(obj, 'eo:sun_elevation', 'view:sun_elevation') && _.addExtension(context, SCHEMAS.view) && DONE;
704
- }
705
-
706
- V.before('1.0.0-beta.1') && _.rename(obj, 'eo:gsd', 'gsd') && DONE;
707
- },
708
-
709
- label(obj) {
710
- // Migrate 0.8.0-rc1 non-pluralized forms
711
- if (V.before('0.8.0')) {
712
- _.rename(obj, 'label:property', 'label:properties') && DONE;
713
- _.rename(obj, 'label:task', 'label:tasks') && DONE;
714
- _.rename(obj, 'label:overview', 'label:overviews') && _.toArray(obj, 'label:overviews') && DONE;
715
- _.rename(obj, 'label:method', 'label:methods') && DONE;
716
- _.toArray(obj, 'label:classes') && DONE;
717
- }
718
- },
719
-
720
- pc(obj) {
721
- V.before('0.8.0') && _.rename(obj, 'pc:schema', 'pc:schemas') && DONE;
722
- },
723
-
724
- proj(obj) {
725
- // Nothing to do
726
- },
727
-
728
- sar(obj, context) {
729
- // If no context is given, it's in summaries
730
- let summary = !context;
731
-
732
- // Which version have they been (re)moved?
733
- _.rename(obj, 'sar:incidence_angle', 'view:incidence_angle') && _.addExtension(context, SCHEMAS.view) && DONE;
734
- _.rename(obj, 'sar:pass_direction', 'sat:orbit_state') && _.mapValues(obj, 'sat:orbit_state', [null], ['geostationary']) && _.addExtension(context, SCHEMAS.sat) && DONE;
735
-
736
- if (V.before('0.7.0')) {
737
- _.flattenArray(obj, 'sar:resolution', ['sar:resolution_range', 'sar:resolution_azimuth'], summary) && DONE;
738
- _.flattenArray(obj, 'sar:pixel_spacing', ['sar:pixel_spacing_range', 'sar:pixel_spacing_azimuth'], summary) && DONE;
739
- _.flattenArray(obj, 'sar:looks', ['sar:looks_range', 'sar:looks_azimuth', 'sar:looks_equivalent_number'], summary) && DONE;
740
- _.rename(obj, 'sar:off_nadir', 'view:off_nadir') && _.addExtension(context, SCHEMAS.view) && DONE;
741
- }
742
-
743
- if (V.before('0.9.0')) {
744
- _.rename(obj, 'sar:platform', 'platform') && DONE;
745
- _.rename(obj, 'sar:instrument', 'instruments') && _.toArray(obj, 'instruments') && DONE;
746
- _.rename(obj, 'sar:constellation', 'constellation') && DONE;
747
- _.rename(obj, 'sar:type', 'sar:product_type') && DONE;
748
- _.rename(obj, 'sar:polarization', 'sar:polarizations') && DONE;
749
- _.flattenOneElementArray(obj, 'sar:absolute_orbit', summary) && _.rename(obj, 'sar:absolute_orbit', 'sat:absolute_orbit') && _.addExtension(context, SCHEMAS.sat) && DONE;
750
- _.flattenOneElementArray(obj, 'sar:relative_orbit', summary) && _.rename(obj, 'sar:relative_orbit', 'sat:relative_orbit') && _.addExtension(context, SCHEMAS.sat) && DONE;
751
- }
752
- },
753
-
754
- sat(obj) {
755
- // Migrate 0.9.0-rc _angle suffixes
756
- if (V.before('0.9.0')) {
757
- _.rename(obj, 'sat:off_nadir_angle', 'sat:off_nadir') && DONE;
758
- _.rename(obj, 'sat:azimuth_angle', 'sat:azimuth') && DONE;
759
- _.rename(obj, 'sat:sun_azimuth_angle', 'sat:sun_azimuth') && DONE;
760
- _.rename(obj, 'sat:sun_elevation_angle', 'sat:sun_elevation') && DONE;
761
- }
762
- },
763
-
764
- sci(obj) {
765
- // Nothing to do
766
- },
767
-
768
- item(obj) { // Single Item
769
- if (V.before('0.8.0')) {
770
- _.rename(obj, 'item:license', 'license') && DONE;
771
- _.rename(obj, 'item:providers', 'providers') && DONE;
772
- // No need to remove the extension from stac_extensions as it was not available before 0.8.0
773
- }
774
- },
775
-
776
- view(obj) {
777
- // Nothing to do
778
- }
779
-
780
- };
781
-
782
- var Migrate = {
783
-
784
- item(item, collection = null, updateVersionNumber = true) {
785
- Item.migrate(item, collection, updateVersionNumber);
786
- return item;
787
- },
788
-
789
- catalog(catalog, updateVersionNumber = true) {
790
- Catalog.migrate(catalog, updateVersionNumber);
791
- return catalog;
792
- },
793
-
794
- collection(collection, updateVersionNumber = true) {
795
- Collection.migrate(collection, updateVersionNumber);
796
- return collection;
797
- },
798
-
799
- stac(object, updateVersionNumber = true) {
800
- if (object.type === 'Feature') {
801
- return Migrate.item(object, null, updateVersionNumber);
802
- }
803
- else if (object.type === 'Collection' || _.isDefined(object.extent) || _.isDefined(object.license)) {
804
- return Migrate.collection(object, updateVersionNumber);
805
- }
806
- else {
807
- return Migrate.catalog(object, updateVersionNumber);
808
- }
809
- },
810
-
811
- enableMultihash(multihash) {
812
- Checksum.multihash = multihash;
813
- }
814
-
815
- };
816
-
1
+ // Migration rules partly based on PySTAC and @openeo/js-commons,
2
+ // see https://github.com/stac-utils/pystac/tree/v0.5.4/pystac/serialization/migrate.py
3
+ // and https://github.com/Open-EO/openeo-js-commons/tree/v1.2.0/src/migrate/collections.js
4
+
5
+ var compareVersions = require('compare-versions');
6
+
7
+ const LATEST_VERSION = '1.0.0';
8
+ const DONE = true; // This is used to verify in code coverage whether something has been used or not
9
+ const SCHEMAS = {
10
+ 'classification': 'https://stac-extensions.github.io/classification/v1.1.0/schema.json',
11
+ 'datacube': 'https://stac-extensions.github.io/datacube/v2.1.0/schema.json',
12
+ 'eo': 'https://stac-extensions.github.io/eo/v1.0.0/schema.json',
13
+ 'file': 'https://stac-extensions.github.io/file/v1.0.0/schema.json',
14
+ 'item-assets': 'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json',
15
+ 'label': 'https://stac-extensions.github.io/label/v1.0.1/schema.json',
16
+ 'pointcloud': 'https://stac-extensions.github.io/pointcloud/v1.0.0/schema.json',
17
+ 'processing': 'https://stac-extensions.github.io/processing/v1.1.0/schema.json',
18
+ 'projection': 'https://stac-extensions.github.io/projection/v1.0.0/schema.json',
19
+ 'raster': 'https://stac-extensions.github.io/raster/v1.1.0/schema.json',
20
+ 'sar': 'https://stac-extensions.github.io/sar/v1.0.0/schema.json',
21
+ 'sat': 'https://stac-extensions.github.io/sat/v1.0.0/schema.json',
22
+ 'scientific': 'https://stac-extensions.github.io/scientific/v1.0.0/schema.json',
23
+ 'table': 'https://stac-extensions.github.io/table/v1.2.0/schema.json',
24
+ 'timestamps': 'https://stac-extensions.github.io/timestamps/v1.0.0/schema.json',
25
+ 'version': 'https://stac-extensions.github.io/version/v1.0.0/schema.json',
26
+ 'view': 'https://stac-extensions.github.io/view/v1.0.0/schema.json'
27
+ };
28
+ const EXTENSIONS = {
29
+ // Add a : at the end to indicate it has a prefix, otherwise list all fields separately (see version extension for example).
30
+ itemAndCollection: {
31
+ // with prefix
32
+ 'cube:': SCHEMAS.datacube,
33
+ 'eo:': SCHEMAS.eo,
34
+ 'file:': SCHEMAS.file,
35
+ 'label:': SCHEMAS.label,
36
+ 'pc:': SCHEMAS.pointcloud,
37
+ 'processing:': SCHEMAS.processing,
38
+ 'proj:': SCHEMAS.projection,
39
+ 'raster:': SCHEMAS.raster,
40
+ 'sar:': SCHEMAS.sar,
41
+ 'sat:': SCHEMAS.sat,
42
+ 'sci:': SCHEMAS.scientific,
43
+ 'view:': SCHEMAS.view,
44
+ // without prefix
45
+ 'version': SCHEMAS.version,
46
+ 'deprecated': SCHEMAS.version,
47
+ 'published': SCHEMAS.timestamps,
48
+ 'expires': SCHEMAS.timestamps,
49
+ 'unpublished': SCHEMAS.timestamps
50
+ },
51
+ catalog: {
52
+ // None yet
53
+ },
54
+ collection: {
55
+ 'item_assets': SCHEMAS['item-assets']
56
+ },
57
+ item: {
58
+ // None yet
59
+ },
60
+ };
61
+ EXTENSIONS.collection = Object.assign(EXTENSIONS.collection, EXTENSIONS.itemAndCollection);
62
+ EXTENSIONS.item = Object.assign(EXTENSIONS.item, EXTENSIONS.itemAndCollection);
63
+
64
+ var Ext = {
65
+ parseUrl(url) {
66
+ let match = url.match(/^https?:\/\/stac-extensions.github.io\/([^\/]+)\/v([^\/]+)\/[^.]+.json$/i);
67
+ if (match) {
68
+ return {
69
+ id: match[1],
70
+ version: match[2]
71
+ };
72
+ }
73
+ }
74
+ };
75
+
76
+ var V = {
77
+ version: LATEST_VERSION,
78
+ extensions: {},
79
+
80
+ set(stac) {
81
+ if (typeof stac.stac_version !== 'string') {
82
+ V.version = '0.6.0'; // Assume the worst case, it doesn't seem there's a clear indicator for 0.7.0
83
+ }
84
+ else {
85
+ V.version = stac.stac_version;
86
+ }
87
+
88
+ if (Array.isArray(stac.stac_extensions)) {
89
+ for(let ext of stac.stac_extensions) {
90
+ let e = Ext.parseUrl(ext);
91
+ if (e) {
92
+ V.extensions[e.id] = e.version;
93
+ }
94
+ }
95
+ }
96
+ },
97
+
98
+ before(version, ext = null) {
99
+ let compareTo = ext ? V.extensions[ext] : V.version;
100
+ if (typeof compareTo === 'undefined') {
101
+ return false;
102
+ }
103
+ else {
104
+ return compareVersions.compare(compareTo, version, '<');
105
+ }
106
+ }
107
+ };
108
+
109
+ var _ = {
110
+
111
+ type(val) {
112
+ let type = typeof val;
113
+ if (type === 'object') {
114
+ if (val === null) {
115
+ return 'null';
116
+ }
117
+ else if (Array.isArray(val)) {
118
+ return 'array';
119
+ }
120
+ }
121
+ return type;
122
+ },
123
+
124
+ is(val, type) {
125
+ return (_.type(val) === type);
126
+ },
127
+
128
+ isDefined(val) {
129
+ return (typeof val !== 'undefined');
130
+ },
131
+
132
+ isObject(obj) {
133
+ return (typeof obj === 'object' && obj === Object(obj) && !Array.isArray(obj));
134
+ },
135
+
136
+ rename(obj, oldKey, newKey) {
137
+ if (typeof obj[oldKey] !== 'undefined' && typeof obj[newKey] === 'undefined') {
138
+ obj[newKey] = obj[oldKey];
139
+ delete obj[oldKey];
140
+ return true;
141
+ }
142
+ return false;
143
+ },
144
+
145
+ forAll(obj, key, fn) {
146
+ if (obj[key] && typeof obj[key] === 'object') {
147
+ for(let i in obj[key]) {
148
+ fn(obj[key][i]);
149
+ }
150
+ }
151
+ },
152
+
153
+ toArray(obj, key) {
154
+ if (typeof obj[key] !== 'undefined' && !Array.isArray(obj[key])) {
155
+ obj[key] = [obj[key]];
156
+ return true;
157
+ }
158
+ return false;
159
+ },
160
+
161
+ flattenArray(obj, key, newKeys, summary = false) {
162
+ if (Array.isArray(obj[key])) {
163
+ for(let i in obj[key]) {
164
+ if (typeof newKeys[i] === 'string') {
165
+ let value = obj[key][i];
166
+ obj[newKeys[i]] = summary ? [value] : value;
167
+ }
168
+ }
169
+ delete obj[key];
170
+ return true;
171
+ }
172
+ return false;
173
+ },
174
+
175
+ flattenOneElementArray(obj, key, summary = false) {
176
+ if (!summary && Array.isArray(obj[key])) {
177
+ if (obj[key].length === 1) {
178
+ obj[key] = obj[key][0];
179
+ return true;
180
+ }
181
+ else {
182
+ return false; // It's still an array and we don't know which element to choose
183
+ }
184
+ }
185
+ return true; // It's already a single element
186
+ },
187
+
188
+ removeFromArray(obj, key, valueToRemove) {
189
+ if (Array.isArray(obj[key])) {
190
+ let index = obj[key].indexOf(valueToRemove);
191
+ if (index > -1) {
192
+ obj[key].splice(index, 1);
193
+ }
194
+ return true;
195
+ }
196
+ return false;
197
+ },
198
+
199
+ ensure(obj, key, defaultValue) {
200
+ if (_.type(defaultValue) !== _.type(obj[key])) {
201
+ obj[key] = defaultValue;
202
+ }
203
+ return true;
204
+ },
205
+
206
+ upgradeExtension(context, extension) {
207
+ let {id, version} = Ext.parseUrl(extension);
208
+ let index = context.stac_extensions.findIndex(url => {
209
+ let old = Ext.parseUrl(url);
210
+ return (old && old.id === id && compareVersions.compare(old.version, version, '<'));
211
+ });
212
+ if (index !== -1) {
213
+ context.stac_extensions[index] = extension;
214
+ return true;
215
+ }
216
+ else {
217
+ return false;
218
+ }
219
+ },
220
+
221
+ addExtension(context, newExtension) {
222
+ let {id, version} = Ext.parseUrl(newExtension);
223
+ let index = context.stac_extensions.findIndex(url => {
224
+ if (url === newExtension) {
225
+ return true;
226
+ }
227
+ let old = Ext.parseUrl(url);
228
+ if (old && old.id === id && compareVersions.compare(old.version, version, '<')) {
229
+ return true;
230
+ }
231
+ return false;
232
+ });
233
+ if (index === -1) {
234
+ context.stac_extensions.push(newExtension);
235
+ }
236
+ else {
237
+ context.stac_extensions[index] = newExtension;
238
+ }
239
+
240
+ context.stac_extensions.sort();
241
+ return true;
242
+ },
243
+
244
+ removeExtension(context, oldExtension) {
245
+ return _.removeFromArray(context, 'stac_extensions', oldExtension);
246
+ },
247
+
248
+ migrateExtensionShortnames(context) {
249
+ let oldShortnames = Object.keys(SCHEMAS);
250
+ let newSchemas = Object.values(SCHEMAS);
251
+ return _.mapValues(context, 'stac_extensions', oldShortnames, newSchemas);
252
+ },
253
+
254
+ populateExtensions(context, type) {
255
+ let objectsToCheck = [];
256
+ if (type == 'catalog' || type == 'collection') {
257
+ objectsToCheck.push(context);
258
+ }
259
+ if ((type == 'item' || type == 'collection') && _.isObject(context.assets)) {
260
+ objectsToCheck = objectsToCheck.concat(Object.values(context.assets));
261
+ }
262
+ if (type == 'collection' && _.isObject(context.item_assets)) {
263
+ objectsToCheck = objectsToCheck.concat(Object.values(context.item_assets));
264
+ }
265
+ if (type == 'collection' && _.isObject(context.summaries)) {
266
+ objectsToCheck.push(context.summaries);
267
+ }
268
+ if (type == 'item' && _.isObject(context.properties)) {
269
+ objectsToCheck.push(context.properties);
270
+ }
271
+
272
+ for (let obj of objectsToCheck) {
273
+ Object.keys(obj).forEach(key => {
274
+ let prefix = key.match(/^(\w+:|[^:]+$)/i);
275
+ if (Array.isArray(prefix)) {
276
+ let ext = EXTENSIONS[type][prefix[0]];
277
+ _.is(ext, 'string') && _.addExtension(context, ext) && DONE;
278
+ }
279
+ });
280
+ }
281
+ },
282
+
283
+ mapValues(obj, key, oldValues, newValues) {
284
+ let fn = value => {
285
+ let index = oldValues.indexOf(value);
286
+ if (index >= 0) {
287
+ return newValues[index];
288
+ }
289
+ return value;
290
+ };
291
+ if (Array.isArray(obj[key])) {
292
+ obj[key] = obj[key].map(fn);
293
+ }
294
+ else if (typeof obj[key] !== 'undefined') {
295
+ obj[key] = fn(obj[key]);
296
+ }
297
+ return true;
298
+ },
299
+
300
+ mapObject(obj, fn) {
301
+ for(let key in obj) {
302
+ obj[key] = fn(obj[key], key);
303
+ }
304
+ },
305
+
306
+ moveTo(obj, key, context, fromSummary = false, mergedSummary = false) {
307
+ let condition;
308
+ if (fromSummary) {
309
+ if (mergedSummary) {
310
+ condition = val => Array.isArray(val);
311
+ }
312
+ else {
313
+ condition = val => Array.isArray(val) && val.length === 1;
314
+ }
315
+ }
316
+ else {
317
+ condition = _.isDefined
318
+ }
319
+ if (condition(obj[key])) {
320
+ context[key] = fromSummary && !mergedSummary ? obj[key][0] : obj[key];
321
+ delete obj[key];
322
+ return true;
323
+ }
324
+ return false;
325
+ },
326
+
327
+ runAll(migrations, obj, context, summaries) {
328
+ for(let fn in migrations) {
329
+ if (!fn.startsWith('migrate')) {
330
+ migrations[fn](obj, context, summaries);
331
+ }
332
+ }
333
+ },
334
+
335
+ toUTC(obj, key) {
336
+ if (typeof obj[key] === 'string') {
337
+ try {
338
+ obj[key] = this.toISOString(obj[key]);
339
+ return true;
340
+ } catch(error) {}
341
+ }
342
+ delete obj[key];
343
+ return false;
344
+ },
345
+
346
+ toISOString(date) {
347
+ if (!(date instanceof Date)) {
348
+ date = new Date(date);
349
+ }
350
+ return date.toISOString().replace('.000', ''); // Don't export milliseconds if not needed
351
+ }
352
+
353
+ };
354
+
355
+ var Checksum = {
356
+
357
+ multihash: null,
358
+
359
+ hexToUint8(hexString) {
360
+ if(hexString.length === 0 || hexString.length % 2 !== 0){
361
+ throw new Error(`The string "${hexString}" is not valid hex.`)
362
+ }
363
+ return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
364
+ },
365
+
366
+ uint8ToHex(bytes) {
367
+ return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
368
+ },
369
+
370
+ toMultihash(obj, key, algo) {
371
+ if (!Checksum.multihash || !_.is(obj[key], 'string')) {
372
+ return false;
373
+ }
374
+ try {
375
+ const encoded = Checksum.multihash.encode(Checksum.hexToUint8(obj[key]), algo);
376
+ obj[key] = Checksum.uint8ToHex(encoded);
377
+ return true;
378
+ } catch (error) {
379
+ console.warn(error);
380
+ return false;
381
+ }
382
+ }
383
+
384
+ };
385
+
386
+ var Catalog = {
387
+
388
+ migrate(catalog, updateVersionNumber = true) {
389
+ V.set(catalog);
390
+ if (updateVersionNumber) {
391
+ catalog.stac_version = LATEST_VERSION;
392
+ }
393
+ catalog.type = 'Catalog';
394
+
395
+ _.ensure(catalog, 'stac_extensions', []) && DONE;
396
+ V.before('1.0.0-rc.1') && _.migrateExtensionShortnames(catalog) && DONE;
397
+
398
+ _.ensure(catalog, 'id', '') && DONE;
399
+ _.ensure(catalog, 'description', '') && DONE;
400
+ _.ensure(catalog, 'links', []) && DONE;
401
+
402
+ _.runAll(Catalog, catalog, catalog);
403
+
404
+ V.before('0.8.0') && _.populateExtensions(catalog, 'catalog') && DONE;
405
+ },
406
+
407
+ openeo(obj) {
408
+ _.rename(obj, 'api_version', 'openeo:api_version') && DONE;
409
+ _.rename(obj, 'backend_version', 'openeo:backend_version') && DONE;
410
+ _.rename(obj, 'production', 'openeo:production') && DONE;
411
+ _.rename(obj, 'endpoints', 'openeo:endpoints') && DONE;
412
+ _.rename(obj, 'billing', 'openeo:billing') && DONE;
413
+ },
414
+
415
+ };
416
+
417
+ var Collection = {
418
+
419
+ migrate(collection, updateVersionNumber = true) {
420
+ Catalog.migrate(collection, updateVersionNumber); // Migrates stac_version, stac_extensions, id, title, description, links
421
+ collection.type = 'Collection';
422
+
423
+ V.before('1.0.0-rc.1') && _.migrateExtensionShortnames(collection) && DONE;
424
+
425
+ _.ensure(collection, 'license', 'proprietary') && DONE;
426
+ _.ensure(collection, 'extent', {
427
+ spatial: {
428
+ bbox: []
429
+ },
430
+ temporal: {
431
+ interval: []
432
+ }
433
+ }) && DONE;
434
+
435
+ _.runAll(Collection, collection, collection);
436
+
437
+ // Migrate Commons extension - part 3
438
+ if (_.isObject(collection.properties)) {
439
+ _.removeFromArray(collection, 'stac_extensions', 'commons') && DONE;
440
+ delete collection.properties;
441
+ }
442
+
443
+ V.before('0.8.0') && _.populateExtensions(collection, 'collection') && DONE;
444
+ V.before('1.0.0-beta.1') && _.mapValues(collection, 'stac_extensions', ['assets'], ['item-assets']) && DONE;
445
+ },
446
+
447
+ extent(collection) {
448
+ _.ensure(collection, "extent", {});
449
+
450
+ if (V.before('0.8.0')) {
451
+ // Restructure spatial extent
452
+ if (Array.isArray(collection.extent.spatial)) {
453
+ collection.extent.spatial = {
454
+ bbox: [
455
+ collection.extent.spatial
456
+ ]
457
+ };
458
+ }
459
+ // Restructure temporal extent
460
+ if (Array.isArray(collection.extent.temporal)) {
461
+ collection.extent.temporal = {
462
+ interval: [
463
+ collection.extent.temporal
464
+ ]
465
+ };
466
+ }
467
+ }
468
+
469
+ _.ensure(collection.extent, "spatial", {});
470
+ _.ensure(collection.extent.spatial, "bbox", []);
471
+ _.ensure(collection.extent, "temporal", {});
472
+ _.ensure(collection.extent.temporal, "interval", []);
473
+
474
+ if (V.before('1.0.0-rc.3')) {
475
+ // The first extent in a Collection is always the overall extent, followed by more specific extents.
476
+ if (collection.extent.temporal.interval.length > 1) {
477
+ let min, max;
478
+ for(let interval of collection.extent.temporal.interval) {
479
+ if (interval[0] === null) {
480
+ min = null;
481
+ }
482
+ else if (typeof interval[0] === 'string' && min !== null) {
483
+ try {
484
+ let start = new Date(interval[0]);
485
+ if (typeof min === 'undefined' || start < min) {
486
+ min = start;
487
+ }
488
+ } catch (error) {}
489
+ }
490
+
491
+ if (interval[1] === null) {
492
+ max = null;
493
+ }
494
+ else if (typeof interval[1] === 'string' && max !== null) {
495
+ try {
496
+ let end = new Date(interval[1]);
497
+ if (typeof max === 'undefined' || end > max) {
498
+ max = end;
499
+ }
500
+ } catch (error) {}
501
+ }
502
+ }
503
+ collection.extent.temporal.interval.unshift([
504
+ min ? _.toISOString(min) : null,
505
+ max ? _.toISOString(max) : null
506
+ ]);
507
+ }
508
+ if (collection.extent.spatial.bbox.length > 1) {
509
+ let count = collection.extent.spatial.bbox.reduce((val, bbox) => Array.isArray(bbox) ? Math.max(bbox.length, val) : val, 4);
510
+ if (count >= 4) {
511
+ let union = new Array(count).fill(null);
512
+ let middle = count / 2;
513
+ for(let bbox of collection.extent.spatial.bbox) {
514
+ if (!Array.isArray(bbox) || bbox.length < 4) {
515
+ break;
516
+ }
517
+ for(let i in bbox) {
518
+ let c = bbox[i];
519
+ if (union[i] === null) {
520
+ union[i] = c;
521
+ }
522
+ else if (i < middle) {
523
+ union[i] = Math.min(c, union[i]);
524
+ }
525
+ else {
526
+ union[i] = Math.max(c, union[i]);
527
+ }
528
+
529
+ }
530
+ }
531
+ if (union.findIndex(c => c === null) === -1) {
532
+ collection.extent.spatial.bbox.unshift(union);
533
+ }
534
+ }
535
+ }
536
+ }
537
+ },
538
+
539
+ collectionAssets(collection) {
540
+ V.before('1.0.0-rc.1') && _.removeExtension(collection, 'collection-assets') && DONE;
541
+
542
+ Asset.migrateAll(collection);
543
+ },
544
+
545
+ itemAsset(collection) {
546
+ V.before('1.0.0-beta.2') && _.rename(collection, 'item_assets', 'assets');
547
+
548
+ Asset.migrateAll(collection, 'item_assets');
549
+ },
550
+
551
+ summaries(collection) {
552
+ _.ensure(collection, 'summaries', {});
553
+
554
+ // other_properties: An early version of the Collection summaries with a similar structure
555
+ // Was mostly used in standalone collctions from openEO and GEE. Move other_properties to summaries.
556
+ if (V.before('0.8.0') && _.isObject(collection.other_properties)) {
557
+ for(let key in collection.other_properties) {
558
+ let prop = collection.other_properties[key];
559
+ if (Array.isArray(prop.extent) && prop.extent.length === 2) {
560
+ collection.summaries[key] = {
561
+ minimum: prop.extent[0],
562
+ maximum: prop.extent[1],
563
+ };
564
+ }
565
+ else if (Array.isArray(prop.values)) {
566
+ if (prop.values.filter(v => Array.isArray(v)).length === prop.values.length) {
567
+ // If it is an array of arrays, merge the arrays
568
+ collection.summaries[key] = prop.values.reduce((a, b) => a.concat(b), []);
569
+ }
570
+ else {
571
+ collection.summaries[key] = prop.values;
572
+ }
573
+ }
574
+ }
575
+ delete collection.other_properties;
576
+ }
577
+
578
+ // Migrate Commons extension - part 2
579
+ // Move properties to (single element) summaries if the Collection is standalone
580
+ // see also https://github.com/stac-utils/stac-migrate/issues/3
581
+ if (V.before('1.0.0-beta.1') && _.isObject(collection.properties) && !collection.links.find(link => ['child', 'item'].includes(link.rel))) {
582
+ for(let key in collection.properties) {
583
+ let value = collection.properties[key];
584
+ if (!Array.isArray(value)) {
585
+ value = [value];
586
+ }
587
+ collection.summaries[key] = value;
588
+ }
589
+ }
590
+
591
+ if (V.before('1.0.0-rc.1')) {
592
+ _.mapObject(collection.summaries, val => {
593
+ _.rename(val, 'min', 'minimum') && DONE;
594
+ _.rename(val, 'max', 'maximum') && DONE;
595
+ return val;
596
+ });
597
+ }
598
+
599
+ // now we can work on all summaries and migrate them
600
+ Fields.migrate(collection.summaries, collection, true);
601
+
602
+ // Some fields should usually be on root-level if there's only one element
603
+ _.moveTo(collection.summaries, 'sci:doi', collection, true) && _.addExtension(collection, SCHEMAS.scientific) && DONE;
604
+ _.moveTo(collection.summaries, 'sci:publications', collection, true, true) && _.addExtension(collection, SCHEMAS.scientific) && DONE;
605
+ _.moveTo(collection.summaries, 'sci:citation', collection, true) && _.addExtension(collection, SCHEMAS.scientific) && DONE;
606
+ _.moveTo(collection.summaries, 'cube:dimensions', collection, true) && _.addExtension(collection, SCHEMAS.datacube) && DONE;
607
+
608
+ // Remove summary field if empty
609
+ if (Object.keys(collection.summaries).length === 0) {
610
+ delete collection.summaries;
611
+ }
612
+ }
613
+
614
+ };
615
+
616
+ var Item = {
617
+
618
+ migrate(item, collection = null, updateVersionNumber = true) {
619
+ V.set(item);
620
+ if (updateVersionNumber) {
621
+ item.stac_version = LATEST_VERSION;
622
+ }
623
+
624
+ _.ensure(item, 'stac_extensions', []) && DONE;
625
+ V.before('1.0.0-rc.1') && _.migrateExtensionShortnames(item) && DONE;
626
+
627
+ _.ensure(item, 'id', '') && DONE;
628
+ _.ensure(item, 'type', 'Feature') && DONE;
629
+ if (!_.isObject(item.geometry)) {
630
+ item.geometry = null;
631
+ }
632
+ if (item.geometry !== null) {
633
+ _.ensure(item, 'bbox', []) && DONE;
634
+ }
635
+ _.ensure(item, 'properties', {}) && DONE;
636
+ _.ensure(item, 'links', []) && DONE;
637
+ _.ensure(item, 'assets', {}) && DONE;
638
+
639
+ // Migrate Commons extension - part 1
640
+ let commons = false;
641
+ if (_.isObject(collection) && _.isObject(collection.properties)) {
642
+ _.removeFromArray(item, 'stac_extensions', 'commons');
643
+ item.properties = Object.assign({}, collection.properties, item.properties);
644
+ commons = true;
645
+ }
646
+
647
+ _.runAll(Item, item, item);
648
+
649
+ Fields.migrate(item.properties, item);
650
+
651
+ Asset.migrateAll(item);
652
+
653
+ // Also populate extensions if commons has been implemented
654
+ (V.before('0.8.0') || commons) && _.populateExtensions(item, 'item') && DONE;
655
+ }
656
+
657
+ };
658
+
659
+ var Asset = {
660
+
661
+ migrateAll(context, field = 'assets') {
662
+ for(let key in context[field]) {
663
+ Asset.migrate(context[field][key], context);
664
+ }
665
+ },
666
+
667
+ migrate(asset, context) {
668
+ _.runAll(Asset, asset, context);
669
+ Fields.migrate(asset, context);
670
+ },
671
+
672
+ mediaTypes(asset) {
673
+ _.is(asset.type, 'string') && _.mapValues(
674
+ asset, 'type',
675
+ ['image/vnd.stac.geotiff', 'image/vnd.stac.geotiff; cloud-optimized=true'],
676
+ ['image/tiff; application=geotiff', 'image/tiff; application=geotiff; profile=cloud-optimized']
677
+ );
678
+ },
679
+
680
+ eo(asset, context) {
681
+ let bands = _.isObject(context.properties) && Array.isArray(context.properties['eo:bands']) ? context.properties['eo:bands'] : [];
682
+ if (Array.isArray(asset['eo:bands'])) {
683
+ for(let i in asset['eo:bands']) {
684
+ let band = asset['eo:bands'][i];
685
+ if (_.is(band, 'number') && _.isObject(bands[band])) {
686
+ band = bands[band];
687
+ }
688
+ else if (!_.isObject(band)) {
689
+ band = {}; // "Fix" invalid band index
690
+ }
691
+ asset['eo:bands'][i] = band;
692
+ }
693
+ }
694
+ }
695
+
696
+ };
697
+
698
+ var Fields = {
699
+
700
+ migrate(obj, context, summaries = false) {
701
+ _.runAll(Fields, obj, context, summaries);
702
+ },
703
+
704
+ _commonMetadata(obj) {
705
+ // Timestamps must be always in UTC
706
+ // datetime, start_datetime and end_datetime already required UTC before
707
+ if (V.before('1.0.0-rc.3')) {
708
+ _.toUTC(obj, 'created') && DONE;
709
+ _.toUTC(obj, 'updated') && DONE;
710
+ }
711
+ },
712
+
713
+ _timestamps(obj, context) {
714
+ // Timestamps must be always in UTC
715
+ _.toUTC(obj, 'published') && DONE;
716
+ _.toUTC(obj, 'expires') && DONE;
717
+ _.toUTC(obj, 'unpublished') && DONE;
718
+
719
+ _.upgradeExtension(context, SCHEMAS.timestamps);
720
+ },
721
+
722
+ _versioningIndicator(obj, context) {
723
+ // Nothing to do
724
+
725
+ _.upgradeExtension(context, SCHEMAS.version);
726
+ },
727
+
728
+ checksum(obj, context) {
729
+ if (V.before('0.9.0') && Checksum.multihash) {
730
+ _.rename(obj, 'checksum:md5', 'checksum:multihash') && Checksum.toMultihash(obj, 'checksum:multihash', 'md5') && DONE;
731
+ _.rename(obj, 'checksum:sha1', 'checksum:multihash') && Checksum.toMultihash(obj, 'checksum:multihash', 'sha1') && DONE;
732
+ // We assume sha2/3-256 although that may fail in some cases and other lengths are chosen
733
+ // Never seen this implemtned in the wild, so let's try this until another use case comes up
734
+ _.rename(obj, 'checksum:sha2', 'checksum:multihash') && Checksum.toMultihash(obj, 'checksum:multihash', 'sha2-256') && DONE;
735
+ _.rename(obj, 'checksum:sha3', 'checksum:multihash') && Checksum.toMultihash(obj, 'checksum:multihash', 'sha3-256') && DONE;
736
+ }
737
+
738
+ V.before('1.0.0-rc.1') && _.rename(obj, 'checksum:multihash', 'file:checksum') && _.addExtension(context, SCHEMAS.file) && DONE;
739
+
740
+ _.removeExtension(context, 'checksum');
741
+ },
742
+
743
+ classification(obj, context) {
744
+ if (V.before('1.1.0', 'classification')) {
745
+ _.forAll(obj, 'classification:classes', o => _.rename(o, 'color-hint', 'color_hint')) && DONE;
746
+ }
747
+
748
+ _.upgradeExtension(context, SCHEMAS.classification);
749
+ },
750
+
751
+ cube(obj, context) {
752
+ // We'd need to convert proj strings to something else for v1.0 -> v2.0, but that's unfeasible here.
753
+ // Nothing else to do here.
754
+
755
+ _.upgradeExtension(context, SCHEMAS.datacube);
756
+ },
757
+
758
+ dtr(obj, context) {
759
+ if (V.before('0.9.0')) {
760
+ _.rename(obj, 'dtr:start_datetime', 'start_datetime') && DONE;
761
+ _.rename(obj, 'dtr:end_datetime', 'end_datetime') && DONE;
762
+ _.removeExtension(context, 'datetime-range') && DONE;
763
+ }
764
+ },
765
+
766
+ eo(obj, context) {
767
+ if (V.before('0.9.0')) {
768
+ _.rename(obj, 'eo:epsg', 'proj:epsg') && _.addExtension(context, SCHEMAS.projection) && DONE;
769
+ _.rename(obj, 'eo:platform', 'platform') && DONE;
770
+ _.rename(obj, 'eo:instrument', 'instruments') && _.toArray(obj, 'instruments') && DONE;
771
+ _.rename(obj, 'eo:constellation', 'constellation') && DONE;
772
+ _.rename(obj, 'eo:off_nadir', 'view:off_nadir') && _.addExtension(context, SCHEMAS.view) && DONE;
773
+ _.rename(obj, 'eo:azimuth', 'view:azimuth') && _.addExtension(context, SCHEMAS.view) && DONE;
774
+ _.rename(obj, 'eo:incidence_angle', 'view:incidence_angle') && _.addExtension(context, SCHEMAS.view) && DONE;
775
+ _.rename(obj, 'eo:sun_azimuth', 'view:sun_azimuth') && _.addExtension(context, SCHEMAS.view) && DONE;
776
+ _.rename(obj, 'eo:sun_elevation', 'view:sun_elevation') && _.addExtension(context, SCHEMAS.view) && DONE;
777
+ }
778
+
779
+ V.before('1.0.0-beta.1') && _.rename(obj, 'eo:gsd', 'gsd') && DONE;
780
+
781
+ _.upgradeExtension(context, SCHEMAS.eo);
782
+ },
783
+
784
+ file(obj, context) {
785
+ // TODO: Migrate to v2.1.0 - https://github.com/stac-utils/stac-migrate/issues/7
786
+
787
+ _.upgradeExtension(context, SCHEMAS.file);
788
+ },
789
+
790
+ label(obj, context) {
791
+ // Migrate 0.8.0-rc1 non-pluralized forms
792
+ if (V.before('0.8.0')) {
793
+ _.rename(obj, 'label:property', 'label:properties') && DONE;
794
+ _.rename(obj, 'label:task', 'label:tasks') && DONE;
795
+ _.rename(obj, 'label:overview', 'label:overviews') && _.toArray(obj, 'label:overviews') && DONE;
796
+ _.rename(obj, 'label:method', 'label:methods') && DONE;
797
+ _.toArray(obj, 'label:classes') && DONE;
798
+ }
799
+
800
+ _.upgradeExtension(context, SCHEMAS.label);
801
+ },
802
+
803
+ pc(obj, context) {
804
+ V.before('0.8.0') && _.rename(obj, 'pc:schema', 'pc:schemas') && DONE;
805
+
806
+ _.upgradeExtension(context, SCHEMAS.pointcloud);
807
+ },
808
+
809
+ processing(obj, context) {
810
+ // Nothing to do
811
+
812
+ _.upgradeExtension(context, SCHEMAS.processing);
813
+ },
814
+
815
+ proj(obj, context) {
816
+ // Nothing to do
817
+
818
+ _.upgradeExtension(context, SCHEMAS.projection);
819
+ },
820
+
821
+ raster(obj, context) {
822
+ // Nothing to do
823
+
824
+ _.upgradeExtension(context, SCHEMAS.raster);
825
+ },
826
+
827
+ sar(obj, context, summary) {
828
+ // Which version have they been (re)moved?
829
+ _.rename(obj, 'sar:incidence_angle', 'view:incidence_angle') && _.addExtension(context, SCHEMAS.view) && DONE;
830
+ _.rename(obj, 'sar:pass_direction', 'sat:orbit_state') && _.mapValues(obj, 'sat:orbit_state', [null], ['geostationary']) && _.addExtension(context, SCHEMAS.sat) && DONE;
831
+
832
+ if (V.before('0.7.0')) {
833
+ _.flattenArray(obj, 'sar:resolution', ['sar:resolution_range', 'sar:resolution_azimuth'], summary) && DONE;
834
+ _.flattenArray(obj, 'sar:pixel_spacing', ['sar:pixel_spacing_range', 'sar:pixel_spacing_azimuth'], summary) && DONE;
835
+ _.flattenArray(obj, 'sar:looks', ['sar:looks_range', 'sar:looks_azimuth', 'sar:looks_equivalent_number'], summary) && DONE;
836
+ _.rename(obj, 'sar:off_nadir', 'view:off_nadir') && _.addExtension(context, SCHEMAS.view) && DONE;
837
+ }
838
+
839
+ if (V.before('0.9.0')) {
840
+ _.rename(obj, 'sar:platform', 'platform') && DONE;
841
+ _.rename(obj, 'sar:instrument', 'instruments') && _.toArray(obj, 'instruments') && DONE;
842
+ _.rename(obj, 'sar:constellation', 'constellation') && DONE;
843
+ _.rename(obj, 'sar:type', 'sar:product_type') && DONE;
844
+ _.rename(obj, 'sar:polarization', 'sar:polarizations') && DONE;
845
+ _.flattenOneElementArray(obj, 'sar:absolute_orbit', summary) && _.rename(obj, 'sar:absolute_orbit', 'sat:absolute_orbit') && _.addExtension(context, SCHEMAS.sat) && DONE;
846
+ _.flattenOneElementArray(obj, 'sar:relative_orbit', summary) && _.rename(obj, 'sar:relative_orbit', 'sat:relative_orbit') && _.addExtension(context, SCHEMAS.sat) && DONE;
847
+ }
848
+
849
+ _.upgradeExtension(context, SCHEMAS.sar);
850
+ },
851
+
852
+ sat(obj, context) {
853
+ // Migrate 0.9.0-rc _angle suffixes
854
+ if (V.before('0.9.0')) {
855
+ _.rename(obj, 'sat:off_nadir_angle', 'sat:off_nadir') && DONE;
856
+ _.rename(obj, 'sat:azimuth_angle', 'sat:azimuth') && DONE;
857
+ _.rename(obj, 'sat:sun_azimuth_angle', 'sat:sun_azimuth') && DONE;
858
+ _.rename(obj, 'sat:sun_elevation_angle', 'sat:sun_elevation') && DONE;
859
+ }
860
+
861
+ _.upgradeExtension(context, SCHEMAS.sat);
862
+ },
863
+
864
+ sci(obj, context) {
865
+ // Nothing to do
866
+
867
+ _.upgradeExtension(context, SCHEMAS.scientific);
868
+ },
869
+
870
+ item(obj) { // Single Item
871
+ if (V.before('0.8.0')) {
872
+ _.rename(obj, 'item:license', 'license') && DONE;
873
+ _.rename(obj, 'item:providers', 'providers') && DONE;
874
+ // No need to remove the extension from stac_extensions as it was not available before 0.8.0
875
+ }
876
+ },
877
+
878
+ table(obj, context) {
879
+ // Nothing to do
880
+
881
+ _.upgradeExtension(context, SCHEMAS.table);
882
+ },
883
+
884
+ view(obj, context) {
885
+ // Nothing to do
886
+
887
+ _.upgradeExtension(context, SCHEMAS.view);
888
+ }
889
+
890
+ };
891
+
892
+ var Migrate = {
893
+
894
+ item(item, collection = null, updateVersionNumber = true) {
895
+ Item.migrate(item, collection, updateVersionNumber);
896
+ return item;
897
+ },
898
+
899
+ catalog(catalog, updateVersionNumber = true) {
900
+ Catalog.migrate(catalog, updateVersionNumber);
901
+ return catalog;
902
+ },
903
+
904
+ collection(collection, updateVersionNumber = true) {
905
+ Collection.migrate(collection, updateVersionNumber);
906
+ return collection;
907
+ },
908
+
909
+ stac(object, updateVersionNumber = true) {
910
+ if (object.type === 'Feature') {
911
+ return Migrate.item(object, null, updateVersionNumber);
912
+ }
913
+ else if (object.type === 'Collection' || _.isDefined(object.extent) || _.isDefined(object.license)) {
914
+ return Migrate.collection(object, updateVersionNumber);
915
+ }
916
+ else {
917
+ return Migrate.catalog(object, updateVersionNumber);
918
+ }
919
+ },
920
+
921
+ enableMultihash(multihash) {
922
+ Checksum.multihash = multihash;
923
+ }
924
+
925
+ };
926
+
817
927
  module.exports = Migrate;