Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/dev_container.dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Base image for the development container
ARG BASE_URL=python:3.8-slim
ARG BASE_URL=python:3.11-slim
FROM ${BASE_URL}

USER root
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/github-python-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.8"
python-version: "3.11"

- name: Install dependencies
run: |
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.8"
python-version: "3.11"

- name: Install dependencies
run: |
Expand Down
29 changes: 28 additions & 1 deletion example/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import podpac
import numpy as np

# Setup new dimension
podpac.core.coordinates.utils.add_valid_dimension("forecastOffsetHr")

# create some podpac nodes
data = np.random.default_rng(1).random((11, 21))
lat = np.linspace(90, -90, 11)
Expand All @@ -23,12 +26,19 @@
data2 = np.random.default_rng(1).random((11, 21))
node2 = podpac.data.Array(source=data2, coordinates=coords)

time = np.array(["2025-10-24T12:00:00"], dtype="datetime64")
offsets = [np.timedelta64(0, "h")]
coords = podpac.Coordinates([lat, lon, time, offsets], dims=["lat", "lon", "time", "forecastOffsetHr"])
data3 = np.random.default_rng(1).random((11, 21, 1, 1))
node3 = podpac.data.Array(source=data3, coordinates=coords)

# use podpac nodes to create some OGC layers
layer1 = pogc.Layer(
node=node1,
identifier="layer1",
title="OGC/POPAC layer containing random data",
abstract="This layer contains some random data",
group="Layers",
)

layer2 = pogc.Layer(
Expand All @@ -37,9 +47,19 @@
title="FOUO: Another OGC/POPAC layer containing random data",
abstract="Marked as FOUO. This layer contains some random data. Same coordinates as layer1, but different values.",
is_fouo=True,
group="Layers",
)

layer3 = pogc.Layer(
node=node3,
identifier="layer3",
title="OGC/POPAC layer containing random data with time instances available.",
abstract="This layer contains some random data with time instances available.",
group="Layers",
valid_times=[dt.astype(datetime) for dt in time],
)

all_layers = [layer1, layer2]
all_layers = [layer1, layer2, layer3]
non_fouo_layers = [layer for layer in all_layers if not layer.is_fouo]

# create a couple of different ogc endpoints
Expand Down Expand Up @@ -69,6 +89,13 @@ def api_home(endpoint):
<li><a href="?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetLegendGraphic&LAYER={test_layer}&STYLE=default&FORMAT=image/png">WMS GetLegend Example (PNG)</a> <i>(v1.3.0)</i></li>
</ul>
</li>
<li> EDR: Open Geospatial Consortium (OGC) Environmental Data Retrieval (EDR) <i>(v1.0.1)</i>
<ul>
<li><a href="{endpoint}/edr?f=html">EDR Landing Page (HTML)</a> <i>(v1.0.1)</i></li>
<li><a href="{endpoint}/edr/conformance?f=json">EDR Conformance (JSON)</a> <i>(v1.0.1)</i></li>
<li><a href="{endpoint}/edr/collections?f=json">EDR Collections (JSON)</a> <i>(v1.0.1)</i></li>
</ul>
</li>
</ul>
"""

Expand Down
11 changes: 4 additions & 7 deletions ogc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
OGC WMS/WCS (v1.3.0/v1.0.0) server
OGC WMS/WCS (v1.3.0/v1.0.0) server
"""

import traitlets as tl
Expand Down Expand Up @@ -70,10 +70,9 @@ class Layer(tl.HasTraits):
identifier = tl.Unicode()
title = tl.Unicode(default_value="An OGC Layer")
abstract = tl.Unicode(default_value="This is an example OGC Layer")
group = tl.Unicode(default_value="Default")
is_fouo = tl.Bool(default_value=False)
grid_coordinates = tl.Instance(
klass=GridCoordinates, default_value=GridCoordinates()
)
grid_coordinates = tl.Instance(klass=GridCoordinates, default_value=GridCoordinates())
valid_times = tl.List(
trait=tl.Instance(datetime.datetime),
default_value=tl.Undefined,
Expand Down Expand Up @@ -101,9 +100,7 @@ def __init__(self, *args, **kwargs):
elif "title" in kwargs:
string_repr = kwargs["title"]
if "is_enumerated" in kwargs:
self._style = Style(
string_repr=string_repr, is_enumerated=kwargs["is_enumerated"]
)
self._style = Style(string_repr=string_repr, is_enumerated=kwargs["is_enumerated"])
else:
self._style = Style(string_repr=string_repr)
if self.valid_times is not tl.Undefined:
Expand Down
2 changes: 2 additions & 0 deletions ogc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from . import wcs_response_1_0_0
from . import wms_response_1_3_0
from . import ogc_common
from .edr import EdrRoutes

from ogc.ogc_common import WCSException

Expand Down Expand Up @@ -62,6 +63,7 @@ def __init__(self, layers=[], **kwargs):
service_abstract=self.service_abstract,
service_group_title=self.service_group_title,
)
self.edr_routes = EdrRoutes(base_url=f"{self.server_address}{self.endpoint}/edr", layers=layers)

def get_coverage_from_id(self, identifier):
for coverage in self.wcs_capabilities.coverages:
Expand Down
3 changes: 3 additions & 0 deletions ogc/edr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .edr_routes import EdrRoutes

__all__ = ["EdrRoutes"]
50 changes: 50 additions & 0 deletions ogc/edr/config/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"server": {
"mimetype": "application/json; charset=UTF-8",
"encoding": "utf-8",
"language": "en-US",
"cors": true,
"pretty_print": true,
"limits": {
"default_items": 50,
"max_items": 1000,
"max_distance_x": 999999999999,
"max_distance_y": 999999999999,
"max_distance_units": "km",
"on_exceed": "error"
},
"admin": false,
"map": {
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "'&copy; <a href='https://openstreetmap.org/copyright'>OpenStreetMap contributors</a>'"
}
},
"logging": {
"level": "ERROR"
},
"metadata": {
"identification": {
"title": "OGC Server",
"description": "An example OGC Server",
"keywords": [
"geospatial",
"podpac"
],
"keywords_type": "theme",
"terms_of_service": "http://www.apache.org/licenses/LICENSE-2.0",
"url": "https://github.com/creare-com/ogc"
},
"license": {
"name": "Apache 2.0 license",
"url": "http://www.apache.org/licenses/LICENSE-2.0"
},
"provider": {
"name": "Creare LLC",
"url": "https://github.com/creare-com"
},
"contact": {
"name": "Creare LLC",
"url": "https://github.com/creare-com"
}
}
}
202 changes: 202 additions & 0 deletions ogc/edr/edr_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import os
import json
import numpy as np
import traitlets as tl
from datetime import datetime
from collections import defaultdict
from typing import List, Dict, Tuple, Any
from ogc import podpac as pogc
from .. import settings


class EdrConfig:
"""Defines the configuration for the pygeoapi based server.

This configuration is used to replace the typical YAML based configurations in order to provide dynamic properties.
"""

@staticmethod
def get_configuration(base_url: str, layers: List[pogc.Layer]) -> Dict[str, Any]:
"""Generate the configuration for the API.

Parameters
----------
base_url : str
The base URL for the EDR endpoints.
layers : List[pogc.Layer]
The layers which define the data sources for the EDR server.

Returns
-------
Dict[str, Any]
The configuration for the API as a dictionary.
"""
configuration_path = settings.EDR_CONFIGURATION_PATH
if configuration_path is None:
configuration_path = os.path.abspath(os.path.join(os.path.dirname(__file__) + "/config/default.json"))

configuration = {}
with open(configuration_path) as f:
configuration = json.load(f)

# Add default static files with an absolute path
server = configuration.get("server", {})
configuration["server"] = server | {
"templates": {
"path": os.path.abspath(os.path.join(os.path.dirname(__file__) + "/templates/")),
"static": os.path.abspath(os.path.join(os.path.dirname(__file__) + "/static/")),
}
}
configuration["server"]["url"] = base_url

# Add the data resources and provider information
resources = configuration.get("resources", {})
configuration["resources"] = resources | EdrConfig._resources_definition(layers)

return configuration

@staticmethod
def _resources_definition(layers: List[pogc.Layer]) -> Dict[str, Any]:
"""Define resource related data for the configuration.

The resources dictionary holds the information needed to generate the collections.
Each group is mapped to a collection with the layers in the group forming the collection parameters.
The custom provider is specified with a data value of the group name.
This allows for the provider to generate the collection data for each group.

Parameters
----------
layers : List[pogc.Layer]
The layers which define the data sources for the EDR server.

Returns
-------
Dict[str, Any]
The resources configuration for the API as a dictionary.
"""

resources = {}
groups = defaultdict(list)

# Organize the data into groups
for layer in layers:
groups[layer.group].append(layer)

# Generate collection resources based on groups
for group_name, group_layers in groups.items():
resource = {
group_name: {
"type": "collection",
"visibility": "default",
"title": group_name,
"description": f"Collection of data related to {group_name}",
"keywords": ["podpac"],
"extents": EdrConfig._generate_extents(group_layers),
"providers": [
{
"type": "edr",
"default": True,
"name": "ogc.edr.edr_provider.EdrProvider",
"data": group_name,
"layers": group_layers,
"crs": [
"https://www.opengis.net/def/crs/OGC/1.3/CRS84",
"https://www.opengis.net/def/crs/EPSG/0/4326",
],
"format": {
"name": "geotiff",
"mimetype": "image/tiff",
},
}
],
}
}
resources.update(resource)

return resources

@staticmethod
def _generate_extents(layers: List[pogc.Layer]) -> Dict[str, Any]:
"""Generate the extents dictionary for provided layers.

Parameters
----------
layers : List[pogc.Layer]
The layers to create the temporal and spatial extents for.

Returns
-------
Dict[str, Any]
The extents dictionary for the layers.
"""
llc_lon, llc_lat, urc_lon, urc_lat = None, None, None, None
min_time, max_time = None, None
time_range = None
# Determine bounding box which holds all layers
for layer in layers:
llc_lon_tmp, llc_lat_tmp, urc_lon_tmp, urc_lat_tmp = EdrConfig._wgs84_bounding_box(layer)
if any(coord is None for coord in [llc_lon, llc_lat, urc_lon, urc_lat]):
llc_lon, llc_lat, urc_lon, urc_lat = llc_lon_tmp, llc_lat_tmp, urc_lon_tmp, urc_lat_tmp
else:
llc_lon = min(llc_lon, llc_lon_tmp)
llc_lat = min(llc_lat, llc_lat_tmp)
urc_lon = max(urc_lon, urc_lon_tmp)
urc_lat = max(urc_lat, urc_lat_tmp)

if hasattr(layer, "valid_times") and layer.valid_times is not tl.Undefined and len(layer.valid_times) > 0:
layer_min_time = np.min(layer.valid_times)
layer_max_time = np.max(layer.valid_times)
if any(time is None for time in [min_time, max_time]):
min_time = layer_min_time
max_time = layer_max_time
else:
min_time = min(min_time, layer_min_time)
max_time = max(max_time, layer_max_time)

time_range = [
min_time.isoformat(),
max_time.isoformat(),
]

return {
"spatial": {
"bbox": [llc_lon, llc_lat, urc_lon, urc_lat], # minx, miny, maxx, maxy
"crs": "https://www.opengis.net/def/crs/OGC/1.3/CRS84",
},
**(
{
"temporal": {
"begin": datetime.fromisoformat(time_range[0]), # start datetime in RFC3339
"end": datetime.fromisoformat(time_range[-1]), # end datetime in RFC3339
"trs": "https://www.opengis.net/def/uom/ISO-8601/0/Gregorian",
}
}
if time_range is not None
else {}
),
}

@staticmethod
def _wgs84_bounding_box(layer: pogc.Layer) -> Tuple[float, float, float, float]:
"""Retrieve the bounding box for the layer with a default fallback.

Parameters
----------
layer : pogc.Layer
The layer from which to get the bounding box coordinates.

Returns
-------
Tuple[float, float, float, float]
Lower-left longitude, lower-left latitude, upper-right longitude, upper-right latitude.
"""
try:
return (
layer.grid_coordinates.LLC.lon,
layer.grid_coordinates.LLC.lat,
layer.grid_coordinates.URC.lon,
layer.grid_coordinates.URC.lat,
)
except Exception:
crs_extents = settings.EDR_CRS["crs:84"]
return (crs_extents["minx"], crs_extents["miny"], crs_extents["maxx"], crs_extents["maxy"])
Loading