★ New · v1.5 · FOUNDATION

A project is a folder you can copy.

A project is a folder on disk containing your real data files plus a single .tmgproj GeoPackage metadata file. Switching projects re-points the app at a different folder — only one is open at a time. Backed by an ephemeral in-memory DuckDB hub that registers your files as VIEWs, not copies: no data duplication, idempotent reloads, and a layer you add today just works tomorrow. The .tmgproj is a valid GeoPackage, so DuckDB spatial reads it back transparently for cross-querying.

.tmgproj GeoPackageIn-memory DuckDB hubVIEW-default (no copy) Composite MVTPortable pathsReconcile-on-openPer-project vs global
How it works

The GPKG is the truth; the hub is ephemeral

The .tmgproj file — written via NetTopologySuite + Microsoft.Data.Sqlite — holds all per-project metadata in standard GPKG tables: project, catalog, layer_style, bookmarks, annotations, chart_configs, folder_connections. The DuckDB hub is re-created in-memory on each open and is throwaway; it holds VIEW registrations + opt-in materialized tables.

  • VIEW-default storage — adding a vector file registers CREATE OR REPLACE VIEW, not a table. DuckDB ST_Read streams features via GDAL on every query — no copy on disk. This dissolves the "table already exists" bug; reloading a file just re-creates the view.
  • Opt-in materialize — "Index this layer" runs CREATE TABLE with make-valid + reprojection + an RTree index into the hub, replacing the VIEW. "Unindex" drops the table + recreates the VIEW. The catalog.materialized flag records the state.
  • Portable pathscatalog.source_path is relative to the project folder when the file is inside it (copy the folder and links resolve); absolute + path_kind='absolute' for files outside.
  • Reconcile on open — for each catalog row the hub checks the source file exists; missing → stale=true (dimmed, doesn't load, row persists; "Repair source…" re-activates).
CREATE OR REPLACE VIEW
-- projected source → VIEW (no copy)
CREATE OR REPLACE VIEW "parcels" AS
  SELECT * EXCLUDE ("geom"),
    ST_Transform(ST_MakeValid("geom"),
                 'EPSG:2240', 'EPSG:4326',
                 always_xy := true) AS "geom"
  FROM ST_Read('parcels.gpkg')
  WHERE "geom" IS NOT NULL
    AND NOT ST_IsEmpty("geom");

→ idempotent · re-run on every reload
→ no copy on disk · GDAL streams on query
MVT display architecture

One VectorTileLayer serves every project layer

Loaded project vector layers render as composite Mapbox Vector Tiles via a single VectorTileLayer — not as a FeatureLayer over FeatureServer JSON. ST_AsMVT generates binary tiles on demand, clipped to the tile envelope, scaling to county-level data (500k+ features). The FeatureServer is analysis only; MVT is the performance path.

🧩

Composite tile endpoint

/api/project/tiles/{z}/{x}/{y}.mvt iterates the catalog's visible layers and concatenates per-layer MVT blobs — multiple named layers per tile.

🎨

Dynamic style.json

/api/project/style.json generates a Mapbox-GL style on the fly — one vector source, one layer per catalog entry (fill/line/circle), default color = hashed-HSL.

🔍

Click-identify

MVT tiles carry OGC_FID + optional name. On click: hitTestfid + source-layer → FeatureServer query → full-attribute popup.

🛡️

Per-layer resilience

A per-layer try/catch in the tile route means one bad layer doesn't poison the whole tile — the others still render.

📐

Column resolution

ResolveMvtColumns(layerId) detects the geometry + id column per layer (ogc_fid / fid / objectid / gid / id). Never hardcoded.

📦

NetTopologySuite + NTS WKB

Bookmarks use a real GEOMETRY POINT column; standard GPKG envelope (application_id='GPKG').

The split

Per-project vs. global

ScopeLives inExamples
Per-project.tmgproj · project foldervector catalog · layer styling · bookmarks · drawings/annotations · chart configs · classification sets · mission collaboration · folder connections · raster/3D asset registrations
Global (machine)%AppData%/3DMapExplorer/credential profiles (ERP/TAK/STAC/connectors/cloud) · basemap URLs · file-download cache · spatial extension · general settings · recent_projects.json

Migration: on first launch after upgrade, ProjectMigrator moves existing vector/*.duckdb + bookmarks/missions into a <dataDir>/Default/ project + creates Default.tmgproj (idempotent; original data moved, recoverable, not deleted). REST: GET /api/project/{current,recent,catalog}, POST /api/project/{create,open}.

Copy the folder. The whole project travels.

One portable .tmgproj, no data duplication, idempotent reloads — the foundation every other capability builds on.