Metadata-Version: 2.4
Name: drb-addon-geolocation
Version: 1.0a2
Summary: DRB geolocation add-on: RDF-declared and intrinsic geolocation of DRB nodes
Author-email: GAEL Systems <drb-python@gael.fr>
License: LGPLv3
Project-URL: Documentation, http://drb-python.gitlab.io/add-ons/geolocalization/core
Project-URL: Repository, https://git2.gael.fr/gael10/drb/drb-python/add-ons/geolocalization/core
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: drb<3,>=2.11
Requires-Dist: drb-extractor>=1.3
Requires-Dist: rdflib>=7
Requires-Dist: pyproj>=3.6
Requires-Dist: shapely>=2.0.1
Requires-Dist: pygml>=0.2.2
Requires-Dist: rasterio>=1.4
Dynamic: license-file

# DRB Geolocation Addon

The DRB geolocation add-on extracts the geolocation (named geometries + CRS)
of a DRB node. Geolocation comes from two composed mechanisms:

- **declarative descriptors**: `geoloc:` RDF blocks attached to topic class
  URIs, loaded from installed TTL packages and Fuseki (the same double
  source as the core topic DAO);
- **intrinsic providers**: georeferencing carried by the data itself (raster
  affine transform or ground control points), read at access time, never
  declared.

The legacy `cortex.yml` YAML model is gone. There is no `drb-metadata`
dependency: this add-on is the only geolocation source.

## Model overview

| Unit | Responsibility |
|---|---|
| `drb.addons.geolocation.model` | Output types `GeoLocation` / `GeoLocationSet` and their conversions |
| `drb.addons.geolocation.formats` | Registry of extracted-value formats (geometry and CRS parsers), entry-point extensible |
| `drb.addons.geolocation.providers` | Intrinsic geolocation plugins keyed by format-topic URIs |
| `drb.addons.geolocation.core` | `geoloc:` descriptor decoding + `GeolocationAddon` (resolution, inheritance, binding) |
| `drb.addons.geolocation.validator` | Static TTL validation, used by the `geoloc-validator` script |

## The `geoloc:` descriptor vocabulary

Namespace `http://knowledge-base.gael.fr/drb/addons/geolocation/`, prefix
`geoloc:`. This is deliberately **not** GeoSPARQL: GeoSPARQL predicates
carry geometry *data* literals, while `geoloc:` carries *extraction rules*
(XQuery programs run against a node). GeoSPARQL **datatype URIs are reused
as format terms**, since they already type the extracted string exactly:
`geo:gmlLiteral`, `geo:wktLiteral`, `geo:geoJSONLiteral`. CRS formats have
no GeoSPARQL equivalent and live in the `geoloc:` namespace: `geoloc:epsg`,
`geoloc:wktCrs`.

One building block: a `geoloc:geometry` block attached to a topic class
URI. One block = one named geometry, exactly one source, and an optional
CRS.

### Block rules

- **Exactly one source** per block: `geoloc:xquery` (extraction),
  `geoloc:value` (constant), or `geoloc:children` (aggregate). A block
  declaring zero or several of these is **skipped with a warning** at load
  time — a malformed block never aborts loading globally.
- **`geoloc:format`** is mandatory for `xquery`/`value` sources and must be
  a registered geometry format term. An unknown or missing format also
  skips the block with a warning (the `geoloc-validator` catches this
  statically).
- **CRS precedence**: an explicit `geoloc:crs` sub-block wins over a CRS
  embedded in the format (GML `srsName`, GeoJSON implicit EPSG:4326), which
  wins over the default EPSG:4326.
- **`geoloc:children` aggregate semantics**: the pattern is a `/`-separated
  glob matched with `fnmatch` against each descendant's structural child
  names (the pattern's segment count bounds the walk depth); every matching
  child is geolocated recursively (its own intrinsic provider or its own
  descriptors); `geoloc:select` picks one named geometry from each child's
  set (default: every geometry of each child); every collected geometry is
  reprojected to EPSG:4326 before aggregation, and the aggregated result is
  always EPSG:4326. `geoloc:aggregate` selects the mode: `geoloc:union`
  (shapely unary union), `geoloc:envelope` (bounding box of the union,
  default), `geoloc:collection` (GeometryCollection).
- **`subClassOf` inheritance**: declarations are collected by walking the
  topic's `subClassOf` chain; on equal names, **the most specific topic
  wins**. Intrinsic providers enter the set with the lowest precedence, so
  a declarative block can override a provider-published name.
- **Reserved name**: the shipped raster provider publishes under `bounds`;
  family descriptors reference it with `geoloc:select "bounds"` in a
  `geoloc:children` block.
- **Skip-and-warn**: every rule above that a block violates causes that one
  block to be skipped (logged at `WARNING`), never a global load failure.

### Example: XQuery source and explicit CRS

```ttl
@prefix geoloc: <http://knowledge-base.gael.fr/drb/addons/geolocation/> .
@prefix geo:    <http://www.opengis.net/ont/geosparql#> .
@prefix sentinel-1: <http://knowledge-base.gael.fr/drb/sentinel-1/> .

# XQuery source: product footprint from the manifest
sentinel-1:product geoloc:geometry [
    geoloc:name    "footprint" ;
    geoloc:format  geo:gmlLiteral ;
    geoloc:xquery  """<XQuery returning the frameSet GML polygon>""" ;
] .

# Explicit CRS when the geometry value does not carry one
sentinel-1:product geoloc:geometry [
    geoloc:name    "scene-center" ;
    geoloc:format  geo:wktLiteral ;
    geoloc:xquery  """<XQuery returning a WKT POINT>""" ;
    geoloc:crs     [ geoloc:format geoloc:epsg ;
                     geoloc:xquery """<XQuery returning the code>""" ] ;
] .
```

### Example: children aggregate composing the raster provider

```ttl
# Children aggregate: composes the intrinsic provider, zero duplication
sentinel-1:product geoloc:geometry [
    geoloc:name      "measurements" ;
    geoloc:children  "measurement/s1*-grd-*.tiff" ;  # relative glob pattern
    geoloc:select    "bounds" ;      # named geometry taken from each child
    geoloc:aggregate geoloc:union ;  # union | envelope | collection
] .
```

Family descriptor packages (`drbx-kb-geolocation-sentinel-1`, `-sentinel-2`)
are out of scope of this repository; naming follows the KB convention of
hyphenated family identifiers (`sentinel-1`, `sentinel-2`, never
`sentinel1`).

## Python usage

```python
from drb.topics import resolver
from drb.addons.geolocation import GeolocationAddon, GeoLocationSet

topic, node = resolver.resolve("<data_url>")

# using the addon directly
geoset = GeolocationAddon().apply(node, topic=topic)

# using the addon implicitly via node implementations
geoset = node.get_impl(GeoLocationSet)
# or, disambiguated by addon identifier:
geoset = node.get_impl(GeoLocationSet, 'geolocation')

geoset.names()               # declared/published names, no evaluation
footprint = geoset["footprint"]     # evaluated on first access, then memoized
footprint.transform("EPSG:2154")    # a new, reprojected GeoLocation
footprint.to_wkt()
footprint.to_geojson()               # a GeoJSON Feature (RFC 7946, WGS84)

geoset.transform("EPSG:2154").to_geojson()  # a GeoJSON FeatureCollection
```

`GeoLocationSet` is a lazy `Mapping[str, GeoLocation]`: a geometry is only
extracted the first time its name is accessed, then cached. This matters
because a `geoloc:children` aggregate may open many rasters through
zip/S3 — `apply()` itself never triggers extraction, and one name failing
to evaluate (`GeolocationExtractionError`, raised at access) does not
prevent the other names in the same set from being read.

If no declaration and no provider apply to the node's topic chain,
`apply()` raises `GeolocationNotAvailable`.

## Provider extension point

Intrinsic providers publish geolocation without any RDF declaration, keyed
by the format-topic class URIs they serve. Register a provider class
through the `drb.addon.geolocation.provider` entry point group; the
built-in `RasterProvider` (declared for `drb:image` and `drb:jp2`) is
loaded the same way.

```python
from drb.addons.geolocation import GeolocationProvider, GeoLocation

class MyProvider(GeolocationProvider):
    def topics(self) -> list[str]:
        return ["http://www.gael.fr/drb#my-format"]

    def names(self) -> set[str]:
        return {"bounds"}          # static, no node access

    def extract(self, node) -> dict[str, GeoLocation]:
        ...                        # called lazily, on first access only
        return {"bounds": GeoLocation("bounds", crs, geometry)}
```

```toml
[project.entry-points."drb.addon.geolocation.provider"]
my-provider = "my_package.geolocation:MyProvider"
```

The shipped `RasterProvider` reads `node.get_impl(rasterio.DatasetReader)`
only — it is transport-agnostic and never opens files itself. It derives
`bounds` from the affine transform and CRS when present (e.g. Sentinel-2
JP2 bands), falling back to the convex hull of the ground control points
when only GCPs are available (e.g. Sentinel-1 GeoTIFF measurements), and
raises `GeolocationExtractionError` when the raster carries neither.
**Intrinsic raster geolocation requires `drb-driver-image` at runtime**
(not a hard dependency of this package) to provide the `rasterio` node
implementation.

## Format extension point

Geometry and CRS value formats are registered functions, keyed by URI
(GeoSPARQL datatypes for geometry, `geoloc:` terms for CRS). Register
additional formats through the `drb.addon.geolocation.format` entry point
group: each entry point resolves to a zero-argument callable that performs
the registration.

```python
from drb.addons.geolocation.formats import register_geometry_format

def register():
    register_geometry_format(
        "http://example.org/my-format", parse_my_format)
    # parse_my_format(value: str) -> (shapely.Geometry, pyproj.CRS | None)
```

```toml
[project.entry-points."drb.addon.geolocation.format"]
my-format = "my_package.geolocation:register"
```

## Static validation: `geoloc-validator`

The `geoloc-validator` console script statically checks `geoloc:geometry`
blocks in one or more Turtle files: unparsable Turtle, more than one or zero
sources per block, and unknown/missing formats or aggregate modes are all
reported without evaluating any XQuery.

```bash
geoloc-validator path/to/cortex.ttl [more.ttl ...]
```

Exit code `0` when every file is clean, `1` as soon as one problem is
reported (one line per problem, on stderr).
