stac 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module STAC
4
+ # Represents \STAC extent object, which describes the spatio-temporal extents of a Collection.
5
+ #
6
+ # Specification: https://github.com/radiantearth/stac-spec/blob/master/collection-spec/collection-spec.md#extent-object
7
+ class Extent
8
+ # Describes the spatial extents of a Collection
9
+ class Spatial
10
+ # Deserializes a Spatial from a Hash.
11
+ def self.from_hash(hash)
12
+ new(**hash.transform_keys(&:to_sym))
13
+ end
14
+
15
+ attr_accessor :bbox, :extra
16
+
17
+ def initialize(bbox:, **extra)
18
+ @bbox = bbox
19
+ @extra = extra.transform_keys(&:to_s)
20
+ end
21
+
22
+ # Serializes self to a Hash.
23
+ def to_h
24
+ {
25
+ 'bbox' => bbox,
26
+ }.merge(extra)
27
+ end
28
+ end
29
+
30
+ # Describes the temporal extents of a Collection.
31
+ class Temporal
32
+ # Deserializes a Temporal from a Hash.
33
+ def self.from_hash(hash)
34
+ new(**hash.transform_keys(&:to_sym))
35
+ end
36
+
37
+ attr_accessor :interval, :extra
38
+
39
+ def initialize(interval:, **extra)
40
+ @interval = interval
41
+ @extra = extra.transform_keys(&:to_s)
42
+ end
43
+
44
+ # Serializes self to a Hash.
45
+ def to_h
46
+ {
47
+ 'interval' => interval,
48
+ }.merge(extra)
49
+ end
50
+ end
51
+
52
+ class << self
53
+ # Deserializes an Extent from a Hash.
54
+ def from_hash(hash)
55
+ transformed = hash.transform_keys(&:to_sym)
56
+ transformed[:spatial] = Spatial.from_hash(transformed.fetch(:spatial))
57
+ transformed[:temporal] = Temporal.from_hash(transformed.fetch(:temporal))
58
+ new(**transformed)
59
+ end
60
+ end
61
+
62
+ attr_accessor :spatial, :temporal, :extra
63
+
64
+ def initialize(spatial:, temporal:, **extra)
65
+ @spatial = spatial
66
+ @temporal = temporal
67
+ @extra = extra.transform_keys(&:to_s)
68
+ end
69
+
70
+ # Serializes self to a Hash.
71
+ def to_h
72
+ {
73
+ 'spatial' => spatial.to_h,
74
+ 'temporal' => temporal.to_h,
75
+ }.merge(extra)
76
+ end
77
+ end
78
+ end
data/lib/stac/item.rb ADDED
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'asset'
4
+ require_relative 'errors'
5
+ require_relative 'properties'
6
+ require_relative 'stac_object'
7
+
8
+ module STAC
9
+ # Represents \STAC item.
10
+ #
11
+ # \STAC \Item Specification: https://github.com/radiantearth/stac-spec/tree/master/item-spec
12
+ class Item < STAC::STACObject
13
+ self.type = 'Feature'
14
+
15
+ class << self
16
+ def from_hash(hash)
17
+ h = hash.dup
18
+ h['properties'] = Properties.from_hash(h.fetch('properties'))
19
+ h['assets'] = h.fetch('assets').transform_values { |v| Asset.from_hash(v) }
20
+ super(h)
21
+ rescue KeyError => e
22
+ raise ArgumentError, "required field not found: #{e.key}"
23
+ end
24
+ end
25
+
26
+ attr_accessor :geometry, :bbox, :properties, :assets, :collection_id
27
+
28
+ def initialize(
29
+ id:, geometry:, properties:, links:, assets:, bbox: nil, collection: nil, stac_extensions: nil, **extra
30
+ )
31
+ super(id: id, links: links, stac_extensions: stac_extensions, **extra)
32
+ @geometry = geometry
33
+ @properties = properties
34
+ @assets = assets
35
+ @bbox = bbox
36
+ case collection
37
+ when Collection
38
+ self.collection = collection
39
+ else
40
+ @collection_id = collection
41
+ end
42
+ end
43
+
44
+ def to_h
45
+ super.merge(
46
+ {
47
+ 'geometry' => geometry, # required but nullable
48
+ },
49
+ ).merge(
50
+ {
51
+ 'bbox' => bbox,
52
+ 'properties' => properties.to_h,
53
+ 'assets' => assets.transform_values(&:to_h),
54
+ 'collection' => collection_id,
55
+ }.compact,
56
+ )
57
+ end
58
+
59
+ # Returns datetime from #properties.
60
+ def datetime
61
+ properties.datetime
62
+ end
63
+
64
+ # Returns a rel="collection" link as a collection object if it exists.
65
+ def collection
66
+ link = find_link(rel: 'collection')
67
+ link&.target
68
+ end
69
+
70
+ # Overwrites rel="collection" link and #collection_id attribute.
71
+ def collection=(collection)
72
+ raise ArgumentError, 'collection must have a rel="self" link' unless (collection_href = collection.self_href)
73
+
74
+ @collection_id = collection.id
75
+ collection_link = Link.new(
76
+ rel: 'collection',
77
+ href: collection_href,
78
+ type: 'application/json',
79
+ title: collection.title,
80
+ )
81
+ remove_link(rel: 'collection')
82
+ add_link(collection_link)
83
+ end
84
+ end
85
+ end
data/lib/stac/link.rb ADDED
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'uri'
5
+ require_relative 'errors'
6
+
7
+ module STAC
8
+ # Represents \STAC link object, which describes a relationship with another entity.
9
+ class Link
10
+ class << self
11
+ # Deserializes a Link from a Hash.
12
+ def from_hash(hash)
13
+ new(**hash.transform_keys(&:to_sym))
14
+ end
15
+ end
16
+
17
+ attr_accessor :rel, :href, :type, :title, :extra
18
+
19
+ # Owner object of this link.
20
+ attr_accessor :owner
21
+
22
+ attr_writer :resolver # :nodoc:
23
+
24
+ def initialize(rel:, href:, type: nil, title: nil, **extra)
25
+ @rel = rel
26
+ @href = href
27
+ @type = type
28
+ @title = title
29
+ @extra = extra.transform_keys(&:to_s)
30
+ end
31
+
32
+ # Serializes self to a Hash.
33
+ def to_h
34
+ {
35
+ 'rel' => rel,
36
+ 'href' => href,
37
+ 'type' => type,
38
+ 'title' => title,
39
+ }.merge(extra).compact
40
+ end
41
+
42
+ # Returns the absolute HREF for this link.
43
+ #
44
+ # When it could not assemble the absolute HREF, it returns nil.
45
+ def absolute_href
46
+ if URI(href).absolute?
47
+ href
48
+ elsif (base_href = owner&.self_href)
49
+ Pathname(base_href).dirname.join(href).to_s
50
+ end
51
+ end
52
+
53
+ # Returns a \STAC object resolved from HREF.
54
+ #
55
+ # When it could not assemble the absolute HREF, it returns nil.
56
+ def target
57
+ @target ||= if (url = absolute_href)
58
+ object = resolver.resolve(url)
59
+ object.self_href = url
60
+ object
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def resolver
67
+ @resolver ||= ObjectResolver.new
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'catalog'
5
+ require_relative 'collection'
6
+ require_relative 'default_http_client'
7
+ require_relative 'errors'
8
+ require_relative 'item'
9
+
10
+ module STAC
11
+ # Resolves a \STAC object from a URL.
12
+ class ObjectResolver
13
+ RESOLVABLES = [Catalog, Collection, Item].freeze # :nodoc:
14
+
15
+ class << self
16
+ # Sets the default HTTP client.
17
+ #
18
+ # HTTP client must implement method `get: (URI uri) -> String,` which fetches the URI resource through HTTP.
19
+ attr_writer :default_http_client
20
+
21
+ # Returns the default HTTP client.
22
+ def default_http_client
23
+ @default_http_client ||= DefaultHTTPClient.new
24
+ end
25
+ end
26
+
27
+ attr_reader :http_client
28
+
29
+ def initialize(http_client: self.class.default_http_client)
30
+ @http_client = http_client
31
+ end
32
+
33
+ # Reads a JSON from the given URL and returns a \STAC object resolved from it.
34
+ #
35
+ # Supports the following URL scheme:
36
+ # - http
37
+ # - https
38
+ # - file
39
+ #
40
+ # Raises:
41
+ # - STAC::UnknownURISchemeError when a URL with unsupported scheme was given
42
+ # - STAC::TypeError when it could not resolve any \STAC objects
43
+ def resolve(url)
44
+ str = read(url)
45
+ hash = JSON.parse(str)
46
+ klass = RESOLVABLES.find { |c| c.type == hash['type'] }
47
+ raise TypeError, "unknown STAC object type: #{hash['type']}" unless klass
48
+
49
+ klass.from_hash(hash)
50
+ end
51
+
52
+ private
53
+
54
+ def read(url)
55
+ uri = URI.parse(url)
56
+ case uri
57
+ when URI::HTTP
58
+ http_client.get(uri)
59
+ when URI::File
60
+ File.read(uri.path.to_s)
61
+ else
62
+ raise UnknownURISchemeError, "unknown URI scheme: #{url}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module STAC
6
+ # Represents \STAC properties object, which is additional metadata for Item.
7
+ #
8
+ # Specification: https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#properties-object
9
+ class Properties
10
+ class << self
11
+ # Deserializes a Properties from a Hash.
12
+ def from_hash(hash)
13
+ h = hash.transform_keys(&:to_sym)
14
+ h[:datetime] = h[:datetime] ? Time.iso8601(h[:datetime]) : nil
15
+ new(**h)
16
+ end
17
+ end
18
+
19
+ attr_accessor :datetime, :extra
20
+
21
+ def initialize(datetime:, **extra)
22
+ @datetime = datetime
23
+ @extra = extra.transform_keys(&:to_s)
24
+ end
25
+
26
+ # Serializes self to a Hash.
27
+ def to_h
28
+ {
29
+ 'datetime' => datetime&.iso8601,
30
+ }.merge(extra)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module STAC
4
+ # Represents \STAC provider object, which provides information about a provider.
5
+ #
6
+ # Specicication: https://github.com/radiantearth/stac-spec/blob/master/collection-spec/collection-spec.md#provider-object
7
+ class Provider
8
+ class << self
9
+ # Deserializes a Provider from a Hash.
10
+ def from_hash(hash)
11
+ new(**hash.transform_keys(&:to_sym))
12
+ end
13
+ end
14
+
15
+ attr_accessor :name, :description, :roles, :url, :extra
16
+
17
+ def initialize(name:, description: nil, roles: nil, url: nil, **extra)
18
+ @name = name
19
+ @description = description
20
+ @roles = roles
21
+ @url = url
22
+ @extra = extra.transform_keys(&:to_s)
23
+ end
24
+
25
+ # Serializes self to a Hash.
26
+ def to_h
27
+ {
28
+ 'name' => name,
29
+ 'description' => description,
30
+ 'roles' => roles,
31
+ 'url' => url,
32
+ }.merge(extra).compact
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module STAC
4
+ SPEC_VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'errors'
5
+ require_relative 'link'
6
+ require_relative 'spec_version'
7
+
8
+ module STAC
9
+ # Base class for \STAC objects (i.e. Catalog, Collection and Item).
10
+ class STACObject
11
+ class << self
12
+ attr_accessor :type # :nodoc:
13
+
14
+ # Base method to deserialize shared fields from a Hash.
15
+ #
16
+ # Raises ArgumentError when any required fields are missing.
17
+ def from_hash(hash)
18
+ raise TypeError, "type field is not 'Catalog': #{hash['type']}" if hash.fetch('type') != type
19
+
20
+ transformed = hash.transform_keys(&:to_sym).except(:type, :stac_version)
21
+ transformed[:links] = transformed.fetch(:links).map { |link| Link.from_hash(link) }
22
+ new(**transformed)
23
+ rescue KeyError => e
24
+ raise ArgumentError, "required field not found: #{e.key}"
25
+ end
26
+ end
27
+
28
+ attr_accessor :id, :stac_extensions, :extra
29
+
30
+ attr_reader :links
31
+
32
+ def initialize(id:, links:, stac_extensions: nil, **extra)
33
+ @id = id
34
+ @links = []
35
+ links.each do |link|
36
+ add_link(link) # to set `owner`
37
+ end
38
+ @stac_extensions = stac_extensions
39
+ @extra = extra.transform_keys(&:to_s)
40
+ end
41
+
42
+ def type
43
+ self.class.type
44
+ end
45
+
46
+ # Serializes self to a Hash.
47
+ def to_h
48
+ {
49
+ 'type' => type,
50
+ 'stac_version' => SPEC_VERSION,
51
+ 'stac_extensions' => stac_extensions,
52
+ 'id' => id,
53
+ 'links' => links.map(&:to_h),
54
+ }.merge(extra).compact
55
+ end
56
+
57
+ # Serializes self to a JSON string.
58
+ def to_json(...)
59
+ to_h.to_json(...)
60
+ end
61
+
62
+ # Adds a link with setting Link#owner as self.
63
+ def add_link(link)
64
+ link.owner = self
65
+ links << link
66
+ end
67
+
68
+ # Reterns HREF of the rel="self" link.
69
+ def self_href
70
+ find_link(rel: 'self')&.href
71
+ end
72
+
73
+ # Adds a link with the give HREF as rel="self".
74
+ #
75
+ # When any ref="self" links already exist, removes them.
76
+ def self_href=(absolute_href)
77
+ self_link = Link.new(rel: 'self', href: absolute_href, type: 'application/json')
78
+ remove_link(rel: 'self')
79
+ add_link(self_link)
80
+ end
81
+
82
+ private
83
+
84
+ def find_link(rel:)
85
+ links.find { |link| link.rel == rel }
86
+ end
87
+
88
+ def remove_link(rel:)
89
+ links.reject! { |link| link.rel == rel }
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module STAC
4
+ VERSION = '0.1.0'
5
+ end
data/lib/stac.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'stac/object_resolver'
4
+ require_relative 'stac/version'
5
+
6
+ # Gem namespace.
7
+ #
8
+ # Provides some utility methods.
9
+ module STAC
10
+ class << self
11
+ # Returns a \STAC object resolved from the given file path.
12
+ def from_file(path)
13
+ from_url("file://#{File.expand_path(path)}")
14
+ end
15
+
16
+ # Returns a \STAC object resolved from the given URL.
17
+ #
18
+ # When the resolved object does not have rel="self" link, adds a rel="self" link with the give url.
19
+ def from_url(url, resolver: ObjectResolver.new)
20
+ object = resolver.resolve(url)
21
+ object.self_href = url unless object.self_href
22
+ object
23
+ end
24
+ end
25
+ end
data/sig/open-uri.rbs ADDED
@@ -0,0 +1,8 @@
1
+ module OpenURI
2
+ class HTTPError < StandardError
3
+ end
4
+ end
5
+
6
+ module URI
7
+ def read: (?Hash[String, String] options) -> String
8
+ end
@@ -0,0 +1,18 @@
1
+ module STAC
2
+ class Asset
3
+ def self.from_hash: (Hash[String, untyped] hash) -> Asset
4
+
5
+ attr_accessor href: String
6
+ attr_accessor title: String?
7
+ attr_accessor description: String?
8
+ attr_accessor type: String?
9
+ attr_accessor roles: Array[String]?
10
+ attr_accessor extra: Hash[String, untyped]
11
+
12
+ def initialize: (
13
+ href: String, ?title: String?, ?description: String?, ?type: String?, ?roles: Array[String]?, **untyped
14
+ ) -> void
15
+
16
+ def to_h: -> Hash[String, untyped]
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module STAC
2
+ class Catalog < STACObject
3
+ def self.from_hash: (Hash[String, untyped] hash) -> Catalog
4
+
5
+ attr_accessor description: String
6
+ attr_accessor title: String?
7
+
8
+ def initialize: (
9
+ id: String, description: String, links: Array[Link], ?title: String?, ?stac_extensions: Array[String]?, **untyped
10
+ ) -> void
11
+
12
+ def to_h: -> Hash[String, untyped]
13
+ def children: -> Enumerator::Lazy[Catalog | Collection, void]
14
+ def collections: -> Enumerator::Lazy[Collection, void]
15
+ def find_child: (String id, ?recursive: bool) -> (Catalog | Collection | nil)
16
+ def items: -> Enumerator::Lazy[Item, void]
17
+ def all_items: -> Enumerator::Lazy[Item, void]
18
+ def find_item: (String id, ?recursive: bool) -> Item?
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ module STAC
2
+ class Collection < Catalog
3
+ def self.from_hash: (Hash[String, untyped] hash) -> Collection
4
+
5
+ attr_accessor license: String
6
+ attr_accessor extent: Extent
7
+ attr_accessor keywords: Array[String]?
8
+ attr_accessor providers: Array[Provider]?
9
+ attr_accessor summaries: Hash[String, untyped]?
10
+ attr_accessor assets: Hash[String, Asset]?
11
+
12
+ def initialize: (
13
+ id: String,
14
+ description: String,
15
+ links: Array[Link],
16
+ license: String,
17
+ extent: Extent,
18
+ ?title: String?,
19
+ ?keywords: Array[String]?,
20
+ ?providers: Array[Provider]?,
21
+ ?summaries: Hash[String, untyped]?,
22
+ ?assets: Hash[String, Asset]?,
23
+ ?stac_extensions: Array[String]?,
24
+ **untyped
25
+ ) -> void
26
+
27
+ def to_h: -> Hash[String, untyped]
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ module STAC
2
+ class DefaultHTTPClient
3
+ attr_reader options: Hash[String, String]
4
+
5
+ def initialize: (?Hash[String, String] options) -> void
6
+
7
+ def get: (URI uri) -> String
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module STAC
2
+ class Error < StandardError
3
+ end
4
+
5
+ class TypeError < Error
6
+ end
7
+
8
+ class UnknownURISchemeError < Error
9
+ end
10
+
11
+ class HTTPError < Error
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ module STAC
2
+ class Extent
3
+ class Spatial
4
+ def self.from_hash: (Hash[String, untyped] hash) -> Spatial
5
+
6
+ attr_accessor bbox: Array[Array[Numeric]]
7
+ attr_accessor extra: Hash[String, untyped]
8
+
9
+ def initialize: (bbox: Array[Array[Numeric]], **untyped) -> void
10
+
11
+ def to_h: -> Hash[String, untyped]
12
+ end
13
+
14
+ class Temporal
15
+ def self.from_hash: (Hash[String, untyped] hash) -> Temporal
16
+
17
+ attr_accessor interval: Array[Array[String?]]
18
+ attr_accessor extra: Hash[String, untyped]
19
+
20
+ def initialize: (interval: Array[Array[String?]], **untyped) -> void
21
+
22
+ def to_h: -> Hash[String, untyped]
23
+ end
24
+
25
+ def self.from_hash: (Hash[String, untyped] hash) -> Extent
26
+
27
+ attr_accessor spatial: Spatial
28
+ attr_accessor temporal: Temporal
29
+ attr_accessor extra: Hash[String, untyped]
30
+
31
+ def initialize: (spatial: Spatial, temporal: Temporal, **untyped) -> void
32
+
33
+ def to_h: -> Hash[String, untyped]
34
+ end
35
+ end
data/sig/stac/item.rbs ADDED
@@ -0,0 +1,27 @@
1
+ module STAC
2
+ class Item < STACObject
3
+ def self.from_hash: (Hash[String, untyped] hash) -> Item
4
+
5
+ attr_accessor geometry: Hash[String, untyped]?
6
+ attr_accessor properties: Properties
7
+ attr_accessor assets: Hash[String, Asset]
8
+ attr_accessor bbox: Array[Numeric]?
9
+ attr_accessor collection_id: String?
10
+
11
+ def initialize: (
12
+ id: String,
13
+ geometry: Hash[String, untyped]?,
14
+ properties: Properties,
15
+ links: Array[Link],
16
+ assets: Hash[String, Asset],
17
+ ?bbox: Array[Numeric]?,
18
+ ?collection: String | Collection | nil,
19
+ ?stac_extensions: Array[String]?,
20
+ **untyped
21
+ ) -> void
22
+
23
+ def datetime: -> Time?
24
+ def collection: -> Collection?
25
+ def collection=: (Collection collection) -> void
26
+ end
27
+ end
data/sig/stac/link.rbs ADDED
@@ -0,0 +1,24 @@
1
+ module STAC
2
+ class Link
3
+ def self.from_hash: (Hash[String, untyped]) -> Link
4
+
5
+ attr_accessor rel: String
6
+ attr_accessor href: String
7
+ attr_accessor type: String?
8
+ attr_accessor title: String?
9
+ attr_accessor extra: Hash[String, untyped]
10
+ attr_accessor owner: STACObject | nil
11
+ @target: Catalog | Collection | Item | nil
12
+ @resolver: ObjectResolver
13
+
14
+ def initialize: (rel: String, href: String, ?type: String?, ?title: String?, **untyped) -> void
15
+
16
+ def to_h: -> Hash[String, untyped]
17
+ def absolute_href: -> String?
18
+ def target: -> (Catalog | Collection | Item | nil)
19
+
20
+ private
21
+
22
+ def resolver: -> ObjectResolver
23
+ end
24
+ end