Skip to content

Reading a catalog ​

STAC.read is the one entry point. It takes a local path, an https:// URL, or any href the IO stack has a route for, reads the document's type key, and builds the struct that key names.

typeStructIs
"Catalog"Catalogan id, a description, and the links that form a tree
"Collection"Collectiona catalog that also states its extent, license, and providers
"Feature"Itemone scene: a footprint, an instant, and the files that hold it
"FeatureCollection"ItemCollectiona page of items

The examples on this page read the catalog the package ships under test/fixtures/static/, so they run with nothing fetched.

julia
julia> examples = joinpath(pkgdir(STAC), "test", "fixtures", "static", "self-contained");

julia> cat = STAC.read(joinpath(examples, "catalog.json"))
Catalog "examples" — Example Catalog
  links       root, child ×2, item
  metadata    1 key: "stac_version"

julia> cat.id
"examples"

Where the fields are ​

Every field the spec names is a field on the struct. ?STAC.Item in the REPL prints one line per field; the summary is:

On an ItemHolds
id, collection, stac_extensionsthe identifiers
geometrythe footprint, a Float64 GeoJSON.jl geometry in longitude/latitude
bboxthe same footprint as 4 or 6 numbers
propertiesProperties: datetime, platform, gsd, and the rest of common metadata
properties.otherthe property keys no field names — extension keys and the producer's own
linksLinks, hrefs kept exactly as the producer wrote them
assetsAssets, keyed as the producer keyed them
extensionsthe extensions parsed into fields; see Extensions
metadatathe top-level keys no field names, stac_version among them
hrefwhere the document was read from
julia
julia> item = STAC.read(joinpath(examples, "simple-collection", "extended-item.json"));

julia> item.id, item.collection
("extended-item", "simple-collection")

julia> item.properties.datetime
2020-12-14T18:02:31.437

julia> item.properties.platform, item.properties.gsd
("cool_sat2", 0.66)

julia> keys(item.assets)
KeySet for a OrderedCollections.OrderedDict{String, STAC.Asset} with 6 entries. Keys:
  "analytic"
  "thumbnail"
  "visual"
  "udm"
  "json-metadata"
  "ephemeris"

julia> item.assets["visual"].type
"image/tiff; application=geotiff; profile=cloud-optimized"

A Collection adds what it covers, under what terms, and who made it:

julia
julia> col = STAC.read(joinpath(examples, "simple-collection", "collection.json"))
Collection "simple-collection" — Simple Example Collection
  extent      bbox (172.9117, 1.3439, 172.9547, 1.369)  2020-12-11T22:38:32.125Z … 2020-12-14T18:02:31.437Z
  license     CC-BY-4.0
  links       root, parent, item ×3
  metadata    1 key: "stac_version"

julia> col.extent.spatial.bbox[1]
4-element Vector{Float64}:
 172.91173669923782
   1.3438851951615003
 172.95469614953714
   1.3690476620161975

julia> [p.name for p in col.providers]
1-element Vector{String}:
 "Remote Data, Inc"

The metadata tail ​

Keys the spec does not name are kept in document order rather than dropped, so a document reads in and writes back out with the same keys. Metadata is where they land, and it answers the AbstractDict interface:

julia
julia> collect(keys(item.metadata))
1-element Vector{String}:
 "stac_version"

julia> collect(keys(item.properties.other))
6-element Vector{String}:
 "statistics"
 "rd:type"
 "rd:anomalous_pixels"
 "rd:earth_sun_distance"
 "rd:sat_id"
 "rd:product_level"

julia> item.properties.other["rd:sat_id"]
"cool_sat2"

julia> get(item.properties.other, "not:a-key", nothing) === nothing
true

rd: is the Remote Data extension, which this package ships no struct for. Its keys are on the tail, they round-trip through STAC.json, and a struct of your own turns them into fields — see Extensions.

Walking the tree ​

children, items, parent, and STAC.root are lazy iterators over rel-filtered links. Nothing is fetched until an element is reached, so length costs no request and first costs exactly one.

julia
julia> length(STAC.children(cat))       # from the link count, before any request
2

julia> [c.id for c in STAC.children(cat)]
2-element Vector{String}:
 "simple-collection"
 "empty-collection"

julia> [i.id for i in STAC.items(cat)]  # the catalog's own `item` links
1-element Vector{String}:
 "collectionless-item"

julia> [i.id for i in STAC.items(cat; recursive = true)]   # and every descendant's
4-element Vector{String}:
 "collectionless-item"
 "simple-item"
 "core-item"
 "extended-item"

julia> parent(col).id, STAC.root(col).id
("examples", "examples")

parent is Base.parent with a method for STAC objects, so it takes no prefix. Every name this package owns takes one.

A relative href resolves against the origin of the object whose links it came from, per RFC 3986, which is what makes the spec's publishing layouts read the same way. Two of the three are local trees and read from a path:

julia
julia> layouts = joinpath(pkgdir(STAC), "test", "fixtures", "static");

julia> [length(collect(STAC.items(STAC.read(joinpath(layouts, l, "catalog.json"));
                                  recursive = true)))
        for l in ("self-contained", "relative-published")]
2-element Vector{Int64}:
 4
 4

The third, absolute-published, states every link as a URL on the host it will be served from, so reading it from a path reaches for the network. That is the layout's point, and Bulk formats is where the three are written.

Links keep the href exactly as written, so a catalog read from disk can be re-rooted or written back verbatim. STAC.resolve is the function that makes one absolute, and STAC.selfhref reads the absolute self link an object publishes.

What the parse produces ​

Three keywords fix the concrete types a read produces, and they are the three type parameters of Item. They travel together as ParseOptions, and they reach children, items, and search as well, so one walk produces one element type all the way down.

KeywordDefaultChoose another when
extensionsthe six shipped structsyou have your own extension, or want none parsed eagerly (Extensions)
geometryPolygon, MultiPolygon, or nothingthe catalog holds points or lines: geometry = STAC.ANY_GEOMETRY
metadatatruethe unnamed keys are not worth keeping: metadata = false
julia
julia> bare = STAC.read(joinpath(examples, "simple-collection", "core-item.json");
                        extensions = (), metadata = false);

julia> typeof(bare).parameters[1], typeof(bare).parameters[3]
(Any, STAC.NoMetadata)

julia> isempty(bare.metadata)
true

Item{Any} is the exploration form: nothing is parsed eagerly, and every prefixed key stays in properties.other where get still finds it.

Writing a document back ​

STAC.json is the inverse of the parse. type and stac_version are put back, extensions are written under their prefixes inside properties, and every tail key returns where the producer put it.

julia
julia> back = STAC.parse(STAC.json(item));

julia> back.id == item.id && back.properties.datetime == item.properties.datetime
true

julia> back.properties.other["rd:sat_id"]
"cool_sat2"

julia> STAC.json(item; pretty = 2)[1:38]
"{\n  \"type\": \"Feature\",\n  \"stac_version"

STAC.write writes one document to a path ending in .json and a whole tree to any other path; see Bulk formats.

When something is missing ​

Every failure raises a STAC.STACError, so one catch clause covers the package, and the exception carries the values its message names as fields rather than only in text.

julia
julia> STAC.asset(item, "B04")
ERROR: this item has no asset named "B04"; it has analytic, thumbnail, visual, udm, json-metadata, ephemeris
[...]

julia> STAC.parse("{\"type\": \"Banana\"}")
ERROR: not a STAC document: `type` is "Banana", expected "Feature", "FeatureCollection", "Catalog", or "Collection"
[...]

The four groups under STAC.STACError — STAC.DocumentError, STAC.LookupError, STAC.ArgumentShapeError, and STAC.EndpointError — say which kind of thing went wrong without naming the concrete type.