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.
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.
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.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.catalog.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.stale=true (dimmed, doesn't load, row persists; "Repair source…" re-activates).-- 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
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.
/api/project/tiles/{z}/{x}/{y}.mvt iterates the catalog's visible layers and concatenates per-layer MVT blobs — multiple named layers per tile.
/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.
MVT tiles carry OGC_FID + optional name. On click: hitTest → fid + source-layer → FeatureServer query → full-attribute popup.
A per-layer try/catch in the tile route means one bad layer doesn't poison the whole tile — the others still render.
ResolveMvtColumns(layerId) detects the geometry + id column per layer (ogc_fid / fid / objectid / gid / id). Never hardcoded.
Bookmarks use a real GEOMETRY POINT column; standard GPKG envelope (application_id='GPKG').
| Scope | Lives in | Examples |
|---|---|---|
| Per-project | .tmgproj · project folder | vector 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}.
One portable .tmgproj, no data duplication, idempotent reloads — the foundation every other capability builds on.