feat(layers): editable layer metadata with STAC Item export (#2904)

* feat(layers): editable layer metadata with STAC Item export

Add an optional `descriptiveMetadata` block to GeoLibreLayer (title,
abstract, keywords, license, attribution, contact, lineage, temporal
extent, links). It is normalized on load and save, so old projects load
unchanged and an empty block is never written. The field is named
apart from the existing internal `metadata` record the renderers and
plugins key off.

The layer Metadata dialog becomes an editable form above the read-only
source details, with email, URL and date validation. Save commits the
draft through setLayerDescriptiveMetadata, one undoable store update.
"Export as STAC Item" writes a STAC 1.0 Item (extent, datetime or
start/end, providers, processing:lineage, links, a data asset for a
remote source URL). GeoParquet exports carry the block in the Parquet
key-value metadata under `geolibre:metadata`.

The layer library captures and restores the block, and the Python
package and MCP server gain set_layer_metadata / clear_layer_metadata,
pinned by the cross-language round-trip fixture.

Part of #2858

* Address review feedback

- MCP set_layer_metadata: merge temporal_start and temporal_end one at a
  time, so passing only one bound keeps the stored other bound.
- isRealDate: use setUTCFullYear instead of Date.UTC, which maps years
  0-99 to 1900-1999 and rejected valid dates such as 0099-01-01.
- metadataDateToRfc3339: normalize a compact +HHMM offset to +HH:MM
  before parsing; the compact form is not an ECMAScript date-time.
- Python link validation error names every accepted URL scheme.

* Address Claude review feedback

- Metadata dialog status line shows "Unsaved changes" alongside a save
  or STAC export note, so an export note cannot hide draft edits that
  the export (built from the stored record) did not include.
- STAC footprint of a box degenerate on one axis is a LineString, not a
  zero-area Polygon.

* Address Claude review feedback

- metadataDateToRfc3339 upper-cases the date-time before parsing: the
  validator is case-insensitive, but ECMAScript only promises to read an
  upper-case T and Z, so a lower-case z could export the export time.
- The Python mirror upper-cases too, covering a lower-case t separator.

* Address Claude review feedback

- A {z}/{x}/{y} tile template is exported as a `tiles` asset (role
  `tiles`, no guessed media type) instead of a `data` asset, since it
  cannot be fetched as-is. Documented in project-format.md.
This commit is contained in:
Qiusheng Wu authored and GitHub committed 2026-10-04 09:55:27 -04:00
1 parent 5ae9762510
commit 81fb86faaf
33 files changed
+2878 -26

No files matched your search

+1
View File
@@ -330,6 +330,7 @@ m.on_layer_change(lambda e: print("layers", e["layerIds"]))
| `remove_layer(layer_id)` / `clear_layers()` | Remove one layer by id, name, or handle, or remove all layers. |
| `set_popup(layer, fields=None, click=, hover=, title=, title_expression=, body_expression=, show_feature_id=, tooltip=, merge=False)` | Choose what a click popup shows for a layer, and how each value is formatted. |
| `set_tooltip(layer, fields=True)` / `clear_popup(layer)` | Turn a hover tooltip on (or off), or drop the popup config and restore the default popup. |
| `set_layer_metadata(layer, merge=False, title=, abstract=, keywords=, license=, attribution=, contact=, lineage=, temporal_extent=, links=)` / `clear_layer_metadata(layer)` | Describe a layer for catalogs (the app's Metadata dialog, its STAC Item export and GeoParquet export metadata). Emails, dates (ISO 8601) and link URLs are validated. `Layer.descriptive_metadata` reads it back. |
| `set_layer_filter(layer, expression)` | Hide a layer's features that do not match a boolean MapLibre expression, e.g. `[">=", ["get", "pop"], 100000]` (the filter Select by Expression → Filter layer saves). `None` clears it. |
| `set_labels(layer, field=None, expression=None, enabled=None, **options)` | Label features from a property or a text expression. Options: `placement`, `size`, `color`, `halo_color`, `halo_width`, `min_zoom`, `max_zoom`, `allow_overlap`, `anchor`, `offset_x`, `offset_y`, `rotation`, `max_width`, `transform`, `number_format`, `number_decimals`, `number_locale`, `dedupe`, and the data-defined `size_expression`, `color_expression`, `opacity_expression`, `visibility_expression`, `priority_expression`. Settings left out keep their values. |
| `set_plugin_state(plugin_id, state=None, position=, activate=True, allow_unknown=False, clear=False)` | Store a plugin's saved project state (the blob it restores on open) for a built-in id in `geolibre.project.PLUGIN_STATE_IDS`, or an external plugin's id with `allow_unknown=True`. `state` must be plain JSON; omit it to change only `position`/`activate`. `clear=True` removes the stored state and leaves activation alone. |