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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **`type = "dem"` source**: elevation, or slope in degrees or percent, from SRTM, Copernicus GLO-30
or TOPODATA (or your own `tiles`), read over the grid plus a `margin` and registered as a raster
source with checksum and provenance. The slope is computed on a metric (UTM) grid by central
differences; asking for a Copernicus slope logs a warning, since that model includes the forest
canopy. TOPODATA sheets are downloaded once into a cache. Replaces the DEM script of the Lab15
reconstruction.
- **`type = "osm"` source**: ways from OpenStreetMap through the Overpass API, clipped to the grid
plus a `margin`, registered as a vector source with checksum and provenance (request, server,
time retrieved, `timestamp_osm_base`, ODbL). The answer is cached by request, so a pipeline reads
the same data (and gets the same `spec_hash`) on every run; `date` asks for an older snapshot
(`date = "{year}-07-01"` with `years`); several public servers are tried with retries; HTTP 406
points to `OSM_CONTACT`. Replaces the OpenStreetMap download script of the Lab15 reconstruction.

## [0.4.0] - 2026-09-30

### Changed
- **DisSModel is now optional.** DisSCube no longer depends on it: install
`disscube[dissmodel]` only to hand a cube to a model. `CubeClient.to_lucc_data()`
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ disscube/
│ ├── _categorical.py legends (.qml/.json/.csv) and reclassification
│ ├── mapbiomas.py MapBiomas annual land-cover maps (Collection 11, 10 m series)
│ ├── prodes.py PRODES deforestation (download + cache, legend from the .qml)
│ ├── osm.py OpenStreetMap ways via Overpass (cached, with date snapshots)
│ ├── dem.py Elevation and slope from SRTM, Copernicus GLO-30 or TOPODATA
│ └── classified.py any classified map with its legend, e.g. from SITS
└── utils.py Checksums (sha256_file) and BDC tile importer (import_bdc_grids)
```
Expand Down
29 changes: 27 additions & 2 deletions disscube/pipeline/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
from disscube.pipeline.schema import (
BdcSource,
ClassifiedSource,
DemSource,
DeriveConfig,
ExportConfig,
FileSource,
GridConfig,
MapbiomasSource,
OsmSource,
PipelineConfig,
ProdesSource,
UnionSource,
Expand Down Expand Up @@ -56,7 +58,7 @@ def base_dir(self) -> Path:
@dataclass
class PlannedSource:
id: str
config: FileSource | BdcSource | MapbiomasSource | ProdesSource | ClassifiedSource | UnionSource
config: FileSource | BdcSource | MapbiomasSource | ProdesSource | ClassifiedSource | OsmSource | DemSource | UnionSource
year: int | None = None


Expand Down Expand Up @@ -412,7 +414,7 @@ def _register_source(
):
c = s.config
# ── Validação defensiva: apenas fontes dinâmicas em janela na nuvem precisam de bbox_geo ──
if isinstance(c, (BdcSource, MapbiomasSource, ProdesSource, ClassifiedSource)):
if isinstance(c, (BdcSource, MapbiomasSource, ProdesSource, ClassifiedSource, OsmSource, DemSource)):
if bbox_geo is None:
raise PipelineError(
f"source {c.id!r} ({c.type}): windowed cloud sources require an"
Expand Down Expand Up @@ -448,6 +450,29 @@ def _register_source(
files=files,
name=c.name,
)
if isinstance(c, OsmSource):
from disscube.sources import osm

try:
return osm.register_osm_source(
cube, c.id, c.query, bbox_geo, raw, margin=c.margin, date=c.date,
endpoints=c.endpoints, cache=(base / c.cache) if c.cache else None,
time=c.time, name=c.name,
)
except (osm.OsmError, ValueError) as exc:
raise PipelineError(f"source {c.id!r}: {exc}") from None
if isinstance(c, DemSource):
from disscube.sources import dem

try:
return dem.register_dem_source(
cube, c.id, bbox_geo, raw, dem=c.dem,
tiles=[_resolve(base, t) for t in c.tiles] if c.tiles else None,
product=c.product, margin=c.margin, resolution=c.resolution,
cache=(base / c.cache) if c.cache else None, name=c.name,
)
except (dem.DemError, ValueError) as exc:
raise PipelineError(f"source {c.id!r}: {exc}") from None
if isinstance(c, ClassifiedSource):
from disscube.sources.classified import register_classified_map

Expand Down
51 changes: 47 additions & 4 deletions disscube/pipeline/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,49 @@ class ClassifiedSource(_SourceBase):
producer: str | None = None


class OsmSource(_SourceBase):
"""Ways from OpenStreetMap (Overpass), clipped to the grid plus ``margin``.

``query`` holds Overpass statements without the bounding box (``way["highway"]["ref"~"BR-163"]``),
separated by ``;`` or given as a list. ``margin`` is in degrees: a ``distance`` needs
features beyond the grid, a ``count`` does not. ``date`` asks for the map as it was on
that day; with ``years``, ``date = "{year}-07-01"`` gives one snapshot per year. The answer
is cached, so a pipeline reads the same data until the cache is cleared. Set ``OSM_CONTACT``.
"""

type: Literal["osm"]
query: str | list[str]
margin: float = Field(default=0.0, ge=0)
date: str | None = None
endpoints: list[str] | None = None
cache: str | None = None
time: int | None = None


class DemSource(_SourceBase):
"""Elevation or slope from a DEM (SRTM, Copernicus GLO-30, TOPODATA), over the grid plus ``margin``.

Give ``dem`` to download one, or ``tiles`` (GeoTIFFs, paths relative to the pipeline file or URLs
GDAL can read) to use your own. ``product`` is ``elevation`` (m), ``slope_deg`` or ``slope_pct``;
a slope is computed on a metric (UTM) grid of ``resolution`` metres. Prefer SRTM or TOPODATA to
Copernicus for slope: Copernicus includes the forest canopy.
"""

type: Literal["dem"]
dem: Literal["srtm", "copernicus", "topodata"] | None = None
tiles: list[str] | None = None
product: Literal["elevation", "slope_deg", "slope_pct"] = "elevation"
margin: float = Field(default=0.02, ge=0)
resolution: float = Field(default=30.0, gt=0)
cache: str | None = None

@model_validator(mode="after")
def _one_input(self):
if (self.dem is None) == (self.tiles is None):
raise ValueError(f"source {self.id!r}: give exactly one of 'dem' or 'tiles'")
return self


class UnionSource(_SourceBase):
"""The features of several vector sources of this file as one source."""

Expand All @@ -139,7 +182,7 @@ class UnionSource(_SourceBase):


Source = Annotated[
FileSource | BdcSource | MapbiomasSource | ProdesSource | ClassifiedSource | UnionSource,
FileSource | BdcSource | MapbiomasSource | ProdesSource | ClassifiedSource | OsmSource | DemSource | UnionSource,
Field(discriminator="type"),
]

Expand Down Expand Up @@ -174,7 +217,7 @@ class PipelineConfig(_Strict):
grid: GridConfig | None = None
extent: list[float] | None = Field(default=None, min_length=4, max_length=4)
"""``[min_lon, min_lat, max_lon, max_lat]`` (WGS84) a sources-only file reads
windowed sources over (classified maps, BDC, MapBiomas, PRODES)."""
windowed sources over (classified maps, BDC, MapBiomas, PRODES, OpenStreetMap, DEM)."""
sources_from_catalog: bool = False
"""Let [[derive]] blocks use sources this file does not declare, registered
in the workspace's catalog by another pipeline file. Off by default, so a
Expand All @@ -190,14 +233,14 @@ def _grid_or_extent(self):

# Apenas fontes dinâmicas em janela na nuvem precisam de extent.
# Fontes 'file' e 'union' já têm extensão definida por seus arquivos locais.
windowed_types = {"bdc", "mapbiomas", "prodes", "classified"}
windowed_types = {"bdc", "mapbiomas", "prodes", "classified", "osm", "dem"}
needs_extent = any(
getattr(s, "type", None) in windowed_types for s in self.source
)

if self.grid is None and self.extent is None and needs_extent:
raise ValueError(
"a sources-only file with windowed sources (BDC, MapBiomas, PRODES)"
"a sources-only file with windowed sources (BDC, MapBiomas, PRODES, OpenStreetMap, DEM)"
" needs `extent` (or a [grid])"
)
return self
Expand Down
Loading
Loading