Skip to content

Commit da2ace4

Browse files
committed
Improve STAC generator: add JSON input format
1 parent 2d27194 commit da2ace4

File tree

3 files changed

+155
-31
lines changed

3 files changed

+155
-31
lines changed

scripts/README.md

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,71 @@
22

33
This directory contains a helper script to generate `layers-config.json` from STAC catalog entries.
44

5-
## Usage
5+
## Quick Start
6+
7+
**Recommended: Use a JSON input file** (clearer what you need to specify vs what comes from STAC):
68

79
```bash
8-
python scripts/stac-to-layers-config.py \
9-
--catalog https://s3-west.nrp-nautilus.io/public-data/stac/catalog.json \
10-
--output app/layers-config.json \
11-
--layer COLLECTION_ID:ASSET_ID:LAYER_KEY:DISPLAY_NAME
10+
python3 scripts/stac-to-layers-config.py \
11+
--input scripts/layers-input-example.json \
12+
--output app/layers-config.json
1213
```
1314

14-
### Examples
15+
## Input JSON Format
16+
17+
Create a JSON file specifying which layers to generate:
18+
19+
```json
20+
{
21+
"catalog": "https://s3-west.nrp-nautilus.io/public-data/stac/catalog.json",
22+
"titiler_url": "https://titiler.nrp-nautilus.io",
23+
"layers": [
24+
{
25+
"collection_id": "cpad-2025b",
26+
"asset_id": "cpad-units-pmtiles",
27+
"layer_key": "cpad",
28+
"display_name": "California Protected Areas (CPAD)",
29+
"comment": "PMTiles vector - filterable properties auto-extracted from STAC"
30+
},
31+
{
32+
"collection_id": "irrecoverable-carbon",
33+
"asset_id": "vulnerable-total-2018-cog",
34+
"layer_key": "carbon",
35+
"display_name": "Vulnerable Carbon",
36+
"options": {
37+
"colormap": "reds",
38+
"rescale": "0,100"
39+
},
40+
"comment": "COG raster - served via TiTiler"
41+
}
42+
]
43+
}
44+
```
45+
46+
### What You Specify vs What Comes from STAC
47+
48+
**User-specified (required):**
49+
- `collection_id`: STAC collection ID
50+
- `asset_id`: Asset ID from the collection
51+
- `layer_key`: Key to use in layers-config.json
52+
53+
**User-specified (optional):**
54+
- `display_name`: Layer display name (falls back to STAC asset title)
55+
- `options.colormap`: Colormap for raster layers (default: "reds")
56+
- `options.rescale`: Rescale range for rasters (e.g., "0,100")
57+
58+
**Auto-extracted from STAC:**
59+
- Attribution (from collection providers/links)
60+
- Filterable properties (from `table:columns` for vector layers)
61+
- Asset URLs and types
62+
- Layer metadata
63+
64+
## Legacy CLI Usage
65+
66+
The script also supports command-line arguments for backward compatibility:
1567

16-
**Generate CPAD layer config:**
1768
```bash
18-
python scripts/stac-to-layers-config.py \
69+
python3 scripts/stac-to-layers-config.py \
1970
--catalog https://s3-west.nrp-nautilus.io/public-data/stac/catalog.json \
2071
--output test-config.json \
2172
--layer cpad-2025b:cpad-units-pmtiles:cpad:"California Protected Areas (CPAD)"

scripts/layers-input-example.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"catalog": "https://s3-west.nrp-nautilus.io/public-data/stac/catalog.json",
3+
"titiler_url": "https://titiler.nrp-nautilus.io",
4+
"layers": [
5+
{
6+
"collection_id": "cpad-2025b",
7+
"asset_id": "cpad-units-pmtiles",
8+
"layer_key": "cpad",
9+
"display_name": "California Protected Areas (CPAD)",
10+
"comment": "PMTiles vector layer - filterable properties will be auto-extracted from STAC"
11+
},
12+
{
13+
"collection_id": "irrecoverable-carbon",
14+
"asset_id": "vulnerable-total-2018-cog",
15+
"layer_key": "carbon",
16+
"display_name": "Vulnerable Carbon",
17+
"options": {
18+
"colormap": "reds",
19+
"rescale": "0,100"
20+
},
21+
"comment": "COG raster layer - will be served via TiTiler"
22+
}
23+
]
24+
}

scripts/stac-to-layers-config.py

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,17 @@ def main():
202202
formatter_class=argparse.RawDescriptionHelpFormatter,
203203
epilog=__doc__
204204
)
205+
206+
# Input method 1: JSON file
207+
parser.add_argument(
208+
"--input",
209+
help="Path to input JSON file specifying layers (recommended)"
210+
)
211+
212+
# Input method 2: Command line arguments (legacy)
205213
parser.add_argument(
206214
"--catalog",
207-
required=True,
208-
help="URL to STAC catalog.json"
215+
help="URL to STAC catalog.json (required if not using --input)"
209216
)
210217
parser.add_argument(
211218
"--output",
@@ -215,7 +222,6 @@ def main():
215222
parser.add_argument(
216223
"--layer",
217224
action="append",
218-
required=True,
219225
metavar="COLLECTION:ASSET:KEY:NAME",
220226
help="Layer specification: collection_id:asset_id:layer_key:display_name (can be specified multiple times)"
221227
)
@@ -232,49 +238,92 @@ def main():
232238

233239
args = parser.parse_args()
234240

235-
# Parse layer specifications
241+
# Determine input method
242+
if args.input:
243+
# Load from JSON file
244+
input_path = Path(args.input)
245+
if not input_path.exists():
246+
print(f"Error: Input file '{args.input}' not found", file=sys.stderr)
247+
sys.exit(1)
248+
249+
with open(input_path) as f:
250+
input_config = json.load(f)
251+
252+
catalog_url = input_config.get("catalog")
253+
if not catalog_url:
254+
print("Error: 'catalog' field required in input JSON", file=sys.stderr)
255+
sys.exit(1)
256+
257+
titiler_url = input_config.get("titiler_url", args.titiler)
258+
layer_specs = input_config.get("layers", [])
259+
260+
if not layer_specs:
261+
print("Error: 'layers' array is empty in input JSON", file=sys.stderr)
262+
sys.exit(1)
263+
264+
elif args.catalog and args.layer:
265+
# Use command-line arguments (legacy mode)
266+
catalog_url = args.catalog
267+
titiler_url = args.titiler
268+
layer_specs = []
269+
270+
for layer_spec in args.layer:
271+
parts = layer_spec.split(":", 3)
272+
if len(parts) < 3:
273+
print(f"Error: Invalid layer specification '{layer_spec}'", file=sys.stderr)
274+
print("Format: collection_id:asset_id:layer_key[:display_name]", file=sys.stderr)
275+
sys.exit(1)
276+
277+
layer_specs.append({
278+
"collection_id": parts[0],
279+
"asset_id": parts[1],
280+
"layer_key": parts[2],
281+
"display_name": parts[3] if len(parts) > 3 else None,
282+
"options": {}
283+
})
284+
285+
else:
286+
print("Error: Must specify either --input or both --catalog and --layer", file=sys.stderr)
287+
parser.print_help()
288+
sys.exit(1)
289+
290+
# Generate layers configuration
236291
layers_config = {
237292
"version": "1.0",
238293
"description": "Map layer configuration for California Protected Lands application",
239294
"layers": {}
240295
}
241296

242-
for layer_spec in args.layer:
243-
parts = layer_spec.split(":", 3)
244-
if len(parts) < 3:
245-
print(f"Error: Invalid layer specification '{layer_spec}'", file=sys.stderr)
246-
print("Format: collection_id:asset_id:layer_key[:display_name]", file=sys.stderr)
247-
sys.exit(1)
248-
249-
collection_id = parts[0]
250-
asset_id = parts[1]
251-
layer_key = parts[2]
252-
display_name = parts[3] if len(parts) > 3 else None
297+
for spec in layer_specs:
298+
collection_id = spec.get("collection_id")
299+
asset_id = spec.get("asset_id")
300+
layer_key = spec.get("layer_key")
301+
display_name = spec.get("display_name")
302+
options = spec.get("options", {})
253303

254304
print(f"Processing {collection_id}:{asset_id} -> {layer_key}...", file=sys.stderr)
255305

256306
# Find collection URL
257-
collection_url = find_collection_url(args.catalog, collection_id)
307+
collection_url = find_collection_url(catalog_url, collection_id)
258308
if not collection_url:
259309
print(f"Error: Collection '{collection_id}' not found in catalog", file=sys.stderr)
260310
sys.exit(1)
261311

262312
# Fetch collection
263313
collection = fetch_json(collection_url)
264314

265-
# Generate layer config
266-
# For COG layers, try to infer rescale from asset title or use default
267-
rescale = None
268-
if "carbon" in asset_id.lower():
269-
rescale = "0,100" # Reasonable default for carbon data
315+
# Get options with defaults
316+
colormap = options.get("colormap", args.colormap)
317+
rescale = options.get("rescale")
270318

319+
# Generate layer config
271320
layer_config = generate_layer_config(
272321
collection,
273322
asset_id,
274323
layer_key,
275324
display_name,
276-
args.titiler,
277-
args.colormap,
325+
titiler_url,
326+
colormap,
278327
rescale
279328
)
280329

0 commit comments

Comments
 (0)