webgis-framework
v1.0.3
Published
Manifest-driven WebGIS framework: import createWebgis() to serve Mapnik raster/vector tiles + PostGIS queries from any dataset
Maintainers
Readme
webgis-framework
An importable geospatial tiling library (PostGIS + node-mapnik). You give it a
dataset manifest.json and a database; it hands back an Express Router that
serves raster (PNG) and vector (MVT) tiles, a features/click-identify data API,
and a public config endpoint — all generated from the manifest. No hand-written
Mapnik XML, no per-dataset code beyond wiring.
framework/backend is the library. framework/frontend is a config-driven
OpenLayers UI that consumes its /api/config. A dataset lives outside the
framework, as an app that imports it — see projects/webgis/ (the London demo).
Install & use
The library is consumed as an npm package. Locally it is linked with file::
// projects/mydata/package.json
{
"dependencies": {
"webgis-framework": "file:../../framework/backend",
"express": "^4.18.2",
"cors": "^2.8.5"
}
}// projects/mydata/server.ts — the whole backend for one dataset
import express from 'express';
import { createWebgis, createPool } from 'webgis-framework';
const app = express();
app.use(express.json());
app.use(createWebgis({
manifest: './manifest.json', // object, file path, or project-dir path
db: createPool(), // DB_* env vars, or pass a pg PoolConfig / your own pool
}));
app.listen(3000);That mounts, under wherever you app.use it:
GET /tiles/raster/:z/:x/:y.pngandGET /tiles/vector/:z/:x/:y.mvtGET /api/data/layers,/api/data/layers/:id/features,/api/data/query/pointGET /api/configandGET /api/projects
cors() / express.json() are the host app's to add — the library only supplies
the router, so you stay in control of the surrounding Express app.
Public API
| Export | Signature | Purpose |
|---|---|---|
| createWebgis | (options) => Router | the factory — build a router for one dataset |
| createPool | (config?) => WebgisPool | pg pool from a PoolConfig (or DB_* env) |
| generateRasterTemplate / generateVectorXml | (manifest, dbParams?) => string | the Mapnik XML generators |
| validateManifest / resolveManifest / toPublicConfig / resolveDbParams | — | manifest helpers |
| loadProject / loadManifestFile | (path) => ResolvedManifest | read + validate a manifest from disk |
Plus the TypeScript types: Manifest, ResolvedManifest, LayerManifest,
ResolvedLayer, PublicConfig, PublicLayer, GeometryType, BasemapConfig,
DbParams, WebgisContext, DbClient.
createWebgis(options)
interface CreateWebgisOptions {
manifest: Manifest | ResolvedManifest | string; // object, .json file, or project dir
db?: DbClient | PoolConfig; // your pool, a pg config, or omit → DB_* env
dbParams?: DbParams; // override the creds baked into the Mapnik XML
projectId?: string; // ?project= key / config.projectId (default: slug of project name)
}The manifest is validated and resolved once; the two Mapnik XML documents are
generated once at build time and cached — per tile only the per-request colour/
opacity/simplify substitution runs. There are no process globals: each
createWebgis(...) owns its own registry + DB, so several can run side by side.
Note — two DB paths. The data API uses the pg pool (
db). Mapnik opens its own PostGIS connection with literal creds baked into the generated XML (dbParams). Whendbis aPoolConfig/omitted, both come from the same place; passdbParamsexplicitly if you hand in a barequery()-only client.
Manifest schema (manifest.json)
The single source of truth for a dataset: map defaults + the layer list. Each layer points at a PostGIS table (or subquery) and declares its geometry + colours.
{
"project": "myproject",
"map": {
"center": [lon, lat], // initial view (also used by Reset View)
"zoom": 14,
"basemap": { "type": "osm" } // "osm" | "none" | { "type":"custom","url":"...{z}/{x}/{y}...","attribution":"" }
},
"tiles": { "tileSize": 256, "simplifyDefault": 0 }, // optional
"api": { "baseUrl": "http://localhost:3000" }, // where the browser reaches the backend ('' = same origin)
"defaultQueryLayer": "buildings", // "Load Features" target (default: first layer)
"db": { "hostEnv": "DB_HOST" }, // optional: which env vars hold the creds (defaults DB_*)
"layers": [
{
"id": "buildings", // ^[a-z][a-z0-9_]*$ — used in URLs, toggles, Mapnik layer/style names
"name": "Buildings", // UI label (default: id)
"source": "buildings", // table name, OR a raw "SELECT ..." with sourceIsSubquery:true
"sourceIsSubquery": false,
"geometryColumn": "geom", // default "geom"
"srid": 4326, // default 4326
"geometryType": "polygon", // "point" | "line" | "polygon" — picks the symbolizer
"labelColumn": "name", // column drawn as label (omit to disable labels)
"attributes": ["id","name","type"], // columns returned by click-identify
"vectorColor": "#0000ff", // client-side MVT colour
"rasterColor": "#cccccc", // server-side PNG fill/stroke
"rasterOpacity": 1, // 0..1
"strokeColor": "#999999", // optional polygon outline
"strokeWidth": 0.5,
"minZoom": 0, "maxZoom": 22 // optional
}
],
"ingest": [ // optional — used by `webgis-ingest`
{ "driver": "ogr2ogr", "path": "data/roads.shp", "targetTable": "roads", "srid": 4326 },
{ "driver": "raster2pgsql", "path": "data/dem.tif", "targetTable": "dem", "srid": 3857 },
{ "driver": "osm2pgsql", "path": "data/city.osm.pbf", "targetTable": "planet_osm" }
]
}Notes:
- Layer order = raster draw order (first layer drawn at the bottom).
sourcemay be a JOIN/subquery (sourceIsSubquery: true) — Mapnik wraps it as(<your SQL>) AS data.- Vector styling is client-side;
vectorColoris the default a user can override in the settings page (per browser, localStorage). - Raster styling is tweakable per-request via tile-URL query params
(
?<layerId>=%23rrggbb&<layerId>_op=0.5&simplify=2); the settings page writes these. Invalid values fall back to the manifest defaults (an XML-injection guard).
Getting data into PostGIS
Each layer's source table must exist with the declared geometryColumn
(default geom) and srid (default 4326). Two ways to populate them:
a hand-written
data/init.sql(postgres runs it once on a fresh volume), orthe ingest CLI — reads the manifest's
ingestsection and shells out toogr2ogr/raster2pgsql/osm2pgsql:# from a project dir, with webgis-ingest on PATH (it's a bin of webgis-framework) webgis-ingest --project .
Endpoints
| Endpoint | Purpose |
|---|---|
| GET /api/config | resolved manifest (no secrets) — drives the frontend |
| GET /api/projects | project summary — feeds the map picker page |
| GET /tiles/raster/:z/:x/:y.png | Mapnik PNG; optional ?<layer>=%23hex&<layer>_op=&simplify= |
| GET /tiles/vector/:z/:x/:y.mvt | Mapnik vector tile (MVT) |
| GET /api/data/layers | layer id/name/geometryType list |
| GET /api/data/layers/:id/features?bbox=minX,minY,maxX,maxY | GeoJSON (id allowlisted against the manifest) |
| GET /api/data/query/point?lon=&lat= | click-identify across all layers (20 m) |
An optional ?project=<id> selects among datasets when a single process hosts
more than one (the registry is keyed by projectId); a single-dataset app can
ignore it, and an unknown id returns 404.
Not covered yet
Non-geographic / CAD data (SRID 0, model-space units needing a georeference transform) is out of scope — the manifest supports custom geometry columns, arbitrary SRID and subquery sources, but a georeferencing step would be needed for that class of dataset.
