npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

webgis-framework

v1.0.3

Published

Manifest-driven WebGIS framework: import createWebgis() to serve Mapnik raster/vector tiles + PostGIS queries from any dataset

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.png and GET /tiles/vector/:z/:x/:y.mvt
  • GET /api/data/layers, /api/data/layers/:id/features, /api/data/query/point
  • GET /api/config and GET /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). When db is a PoolConfig/omitted, both come from the same place; pass dbParams explicitly if you hand in a bare query()-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).
  • source may be a JOIN/subquery (sourceIsSubquery: true) — Mapnik wraps it as (<your SQL>) AS data.
  • Vector styling is client-side; vectorColor is 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), or

  • the ingest CLI — reads the manifest's ingest section and shells out to ogr2ogr / 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.