Vector data — decoded surface observations (TimescaleDB / PostGIS)¶
Goal: publish continuously decoded surface observations (SYNOP, METAR…) as WMS layers with a TIME dimension and as WFS features, readable by every replica the moment they are inserted, with the canonical unit carried through (ADR-0003).
flowchart LR
SRC[SYNOP · METAR · …] --> DEC[Decoding service<br/>simulator in this prototype]
DEC -- "INSERT (canonical unit,<br/>source_unit kept)" --> DB[(schema obs<br/>station · observation hypertable<br/>publication views)]
GS[GeoServer ×N] -- "jdbc/meteo (read-only,<br/>datastore obs-db, schema=obs)" --> DB
PLUG[geoserver-init plugin<br/>VECTOR_LAYERS ConfigMap] -- "feature types + TIME dimension<br/>title/unit from reference data" --> GS
C[WMS TIME · WFS · CQL] --> GS
Decision: ADR-0011.
Artifacts: poc/06-vector/ (schema, Jobs, tests) and the geoserver-init
plugin (ensureVectorLayers).
Prerequisites¶
Sub-projects 1 to 5 (database, image, cluster, S3, reference data in
mosaic.parameter). Secrets: decoder-db (decoding-service account),
meteo-reader.
Data model (schema obs, owned by the decoding service)¶
| Object | Role |
|---|---|
station |
One row per station: identifier (ICAO/WMO), name, geom Point 4326, elevation |
observation |
TimescaleDB hypertable, PK (station_id, time, param_key) + surrogate fid ; value in the canonical unit, source_unit kept (METAR temperatures arrive in Celsius), message_type |
obs_air_temperature (view) |
Full series joined with stations — backs the TIME-enabled layer |
obs_latest_air_temperature (view) |
Latest value per station (DISTINCT ON … ORDER BY time DESC) — backs the map-friendly layer |
obs.gt_pk_metadata (view) |
Exposes the virtual key fid of the views to GeoTools |
The schema is created by a Job run with the operator account
(k8s/job-init-obs-schema.yaml); GeoServer's reader gets SELECT through the
grant-reader Job (DATA_SCHEMA=obs). The two init Jobs may race: the grant
Job simply retries until the schema exists (backoff).
Decoder simulator¶
ingest.py obs --from 2026-09-02T06:00Z --hours 3 --step-min 10 (same
container as the raster ingestion) upserts 10 demo stations across Europe and
Asia and generates one observation per station and per 10-minute step:
synthetic 2 m temperature in Celsius, converted to kelvin on insert
(source_unit=Cel), alternating METAR/SYNOP message types. Real
SYNOP/BUFR/TAC decoding is out of scope of the prototype. Run it as a Job
(k8s/job-ingest-obs.yaml, via ../05-ingestion/ingest.sh); a CronJob or the
real decoding service takes over in production.
Publication by the plugin (declarative)¶
VECTOR_LAYERS="name|view|timeAttribute(- for none)|param_key|title;…" in the
geoserver-mosaics ConfigMap. On each pod start the plugin ensures:
- a second datastore
obs-dbon the same JNDI resourcejdbc/meteowithschema=obs(one datastore per schema; the stores always name a JNDI resource, never a URL); - one feature type + layer per view, with
TIMEenabled on the given attribute (continuous interval presentation — listing every observation instant would be meaningless), default = most recent; - title and abstract carrying the canonical unit read from
mosaic.parameter(Surface observations — air temperature (K)).
No GWC tile layer is created for observation layers: requests are time-qualified and served straight by WMS.
Verification (tests poc/06-vector/tests/)¶
Schema, grant (retry visible), 180 observations ingested:
Startup: datastore and layers created/verified:
01 Capabilities: titles with the unit, continuous TIME dimension
(start/end/PT1S, default = latest):
02 The TIME dimension really filters: an in-range instant renders the
stations; an out-of-range instant is rejected with an explicit
ServiceException (fail loud — consistent with the project rule); WFS
counts per instant: 10, 10, 0. Two valid instants render identical images by
design (the default point style does not encode the value — thematic styling
comes with sub-project 8):
03 One more hour ingested → TIME extent and feature counts grow on both
pods, no restart, no notification (the views read the live tables):
04 The reader stays read-only (permission denied on INSERT) and the layers
survive a pod replacement:
Deviations and pitfalls observed¶
- A WMS request outside the TIME domain returns a
ServiceException(Could not find a match for 'time' value) rather than an empty map — document it to client teams. - Identical images for two valid instants are expected with the default style; the real proof of filtering is WFS/CQL counts (test 02).
gt_pk_metadatamust be visible inside the datastore's schema: theobsschema exposes a filtered view over the shared table.








