@@ -459,191 +459,3 @@ Curve labels are `station/variable` when multiple variables are selected, or jus
459459- When `normalize_ref()` cannot determine a `station` value, set it to a non-empty
460460 fallback (e.g. the `source` basename) so the auto-derived catalog name is stable.
461461
462- ---
463-
464- ## 14. CLI Launch Pattern — Manager CRS and the `dvue ui --plugin` Command
465-
466- ### Manager-declared CRS
467-
468- Every `DataUIManager` subclass has a `crs = None` class attribute. Override it as
469- an **instance attribute** in `__init__` to declare the map projection:
470-
471- ```python
472- import cartopy.crs as ccrs
473-
474- class MyUIManager(RegistryUIManager):
475- def __init__(self, **kwargs):
476- super().__init__(**kwargs)
477- self.crs = ccrs.epsg("26910") # NAD83 UTM 10N — California Delta
478- ```
479-
480- `serve_session_app` and `serve_desktop_app` probe the manager' s `crs` and
481- ` station_id_column` automatically when the **caller** does not supply them, so
482- the serve calls in CLI commands can be written without those arguments :
483-
484- ` ` ` python
485- def build_manager():
486- return MyUIManager()
487-
488- # ✅ CRS is probed automatically from MyUIManager().crs
489- _serve(build_manager, title="My App", port=port)
490-
491- # ❌ Repeating the same information — fragile if CRS ever changes
492- _serve(build_manager, title="My App", port=port, crs=ccrs.epsg("26910"))
493- ` ` `
494-
495- Caller-provided values always win — the probe only fills in `None` gaps. This lets
496- integration tests override CRS without modifying the manager class.
497-
498- # ## Thin CLI wrapper recipe
499-
500- ` ` ` python
501- import click
502- from dvue.session_persistence import serve_desktop_app, serve_session_app
503-
504- @click.command("mytool")
505- @click.argument("files", nargs=-1, required=False)
506- @click.option("--port", default=5006)
507- @click.option("--desktop", is_flag=True, default=False)
508- def show_my_ui(files, port, desktop):
509- import panel as pn; pn.extension()
510-
511- def build_manager():
512- mgr = MyUIManager()
513- if files:
514- mgr.add_source_files(list(files))
515- return mgr
516-
517- _serve = serve_desktop_app if desktop else serve_session_app
518- _serve(build_manager, title="My Tool", port=port)
519- ` ` `
520-
521- Register it in `dvue.cli` via `dvue ui --plugin mypackage.mymodule` :
522-
523- ` ` ` bash
524- dvue ui --plugin mypackage.mymodule path/to/data.ext --desktop
525- ` ` `
526-
527- The `dvue ui` command imports the plugin module (which calls `ReaderRegistry.register()`
528- at module level) then runs `serve_session_app` / `serve_desktop_app` with drag-and-drop
529- support.
530-
531- ---
532-
533- # # 15. Geo Loading — `load_geo_dataframe` and `add_geo_source`
534-
535- # ## `dvue.utils.load_geo_dataframe`
536-
537- ` ` ` python
538- from dvue.utils import load_geo_dataframe
539-
540- gdf = load_geo_dataframe(
541- path, # .geojson / .json / .shp / .gpkg / .csv
542- lat_col="lat", # CSV only: latitude column (WGS84)
543- lon_col="lon", # CSV only: longitude column (WGS84)
544- id_col=None, # informational; used by callers for join key tracking
545- crs=None, # override auto-detected CRS
546- )
547- ` ` `
548-
549- Auto-detection for CSV files (in priority order) :
550-
551- | Columns present | Auto-detected CRS |
552- |-----------------|-------------------|
553- | `lat` / `lon` (or user-specified) | EPSG:4326 (WGS84) |
554- | `utm_easting` / `utm_northing` | EPSG:26910 (NAD83 UTM 10N) |
555- | `easting` / `northing` | EPSG:26910 |
556-
557- GeoJSON/shapefile/GeoPackage formats are passed directly to `geopandas.read_file`.
558-
559- # ## `RegistryUIManager.add_geo_source`
560-
561- ` ` ` python
562- class MyUIManager(RegistryUIManager):
563- def on_file_added(self, path, refs):
564- # Auto-load geometry when the first data file is added
565- if not getattr(self, "_geo_loaded", False):
566- self.add_geo_source(
567- "/path/to/stations.geojson",
568- id_column="STATION_ID", # column in geo file
569- station_column="station", # column in catalog to join on
570- )
571- self._geo_loaded = True
572- ` ` `
573-
574- After `add_geo_source` :
575-
576- - ` get_data_catalog()` returns a `GeoDataFrame` with a `geometry` column.
577- - Map display in `DataUI` automatically picks up geometry when `station_id_column`
578- matches the join column.
579- - If `self.crs` is `None` and the loaded geo file has a CRS, `self.crs` is set
580- automatically from the file's EPSG code via `cartopy.crs.epsg()`.
581- - The geo merge is re-applied automatically whenever the catalog grows (e.g. when
582- additional files are dropped after the initial load).
583-
584- # ## Geo merge rules
585-
586- - The merge is a **left join** from catalog to geo data on `station_column`.
587- Catalog rows without a matching geo entry retain `NaN` geometry.
588- - Duplicate `id_column` values in the geo file are deduplicated before the merge
589- (first occurrence wins).
590- - If `geometry` already exists in the catalog DataFrame (from a previous `add_geo_source`
591- call), it is stripped before re-merging to avoid pandas column conflicts.
592-
593- ---
594-
595- # # 16. Multi-`ref_type` Scanning — One Scanner, Several Loaders
596-
597- A single scanner class (registered for an extension) can return refs of **mixed
598- ` ref_type` values** — each `ref_type` can resolve to a different loader.
599-
600- # ## Pattern
601-
602- ` ` ` python
603- class _BCRef(DataReference):
604- ref_type = "bc_flow" # → BCFlowLoader
605-
606- class _OutputRef(DataReference):
607- ref_type = "dss_output" # → DSM2DSSReader (already registered)
608-
609- class EchoScanner:
610- def __init__(self, source): self._source = source
611-
612- @classmethod
613- def scan(cls, path):
614- refs = []
615- ...
616- # Input boundary ref — uses BCFlowLoader
617- refs.append(_BCRef(source=path, FILE=dss_file, PATH=dss_path, SIGN=sign, ...))
618- # Output channel ref — reuses DSM2DSSReader (source = DSS file)
619- refs.append(_OutputRef(source=dss_file, NAME=name, VARIABLE=var, ...))
620- return refs
621-
622- # Scanner registered for .inp — loaders registered for each ref_type
623- ReaderRegistry.register(EchoScanner, ref_type="echo_inp", extensions=[".inp"])
624- ReaderRegistry.register(BCFlowLoader, ref_type="bc_flow")
625- # DSM2DSSReader is already registered for ref_type="dsm2_dss" elsewhere
626- ` ` `
627-
628- # ## Key points
629-
630- - ` ref.source` is the path passed to the **loader**, not necessarily the scan path.
631- When output refs point at a DSS file, set `source=dss_file` so the loader opens
632- the right file.
633- - The `ReaderRegistry` caches loaders by `(ref_type, source)`, so multiple refs from
634- the same DSS file share one reader instance.
635- - ` RegistryUIManager._dvue_catalog` accepts mixed ref types in the same catalog.
636- Use a `primary_key` that includes a `category` or `TABLE` column to keep input and
637- output refs uniquely keyed :
638-
639- ` ` ` python
640- self._dvue_catalog = DataCatalog(
641- primary_key=["category", "TABLE", "station", "variable"]
642- )
643- ` ` `
644-
645- - Conflict warning : registering two reader classes for the **same extension** triggers
646- a `WARNING` log message at register time (last-write-wins). To avoid the conflict,
647- do not register an extension entry for the "generic" reader — instead bypass the
648- extension map in `add_source_files()` and call `YourReader.scan(path)` directly.
649-
0 commit comments