Skip to content

Repository files navigation

GeoLens Examples

Live gallery Verify examples GeoLens v1.13+ License: MIT

GeoLens is a self-hosted spatial data hub: catalog, search, maps, analysis, and open APIs over data that stays on your own infrastructure. This repo holds copy-paste integrations for the tools your stack already uses. Every browser, Python, and DuckDB example reads the public demo anonymously, so you clone, open, and see it run; one constant at the top points it at your own instance. The exception is cli/, which publishes to a catalog and so runs against your own instance with a credential.

Semantic catalog search: the phrase 'every space rock ever recovered on this planet' matches the meteorite landings dataset, drawn on the map beside the result card QGIS showing the demo's subway stations and lines over OGC API Features, and the Matterhorn DEM as XYZ tiles A saved GeoLens map, Restless Earth, embedded in an iframe on a plain page with its legend and styling intact

Search the catalog by meaning, connect from desktop GIS, or embed a map, with the same self-hosted catalog and open APIs underneath.

  • Live gallery: every live browser example running, arranged by what you are trying to do. Open one before you clone anything.
  • Try GeoLens: the public demo these examples read. Its catalog and its saved maps open without an account.
  • Main repository: GeoLens itself, with the install script, the docs, and the issue tracker.

If GeoLens is useful to you, star it on GitHub. That is how most people find it. If these examples save you an afternoon, star or watch this repo too.

GeoLens serves OGC API Features and Records, STAC 1.0, XYZ vector tiles (MVT), and raster tiles (API reference). These examples show those surfaces from the consumer's side.

GeoLens in 10 minutes

The table below is arranged by tool; the numbered steps here trace the platform from an empty instance to a map someone else can use.

  1. Install. curl -fsSL https://getgeolens.com/install.sh | sh starts the stack with Docker Compose; open http://localhost:8080 about a minute later (install guide). The public demo covers the read steps below without an install.
  2. Publish two sources. Declare them in a manifest and run geolens apply. cli/ holds a geolens.yaml with two Natural Earth layers and the GitHub Actions workflow that applies it (CLI guide).
  3. Find them by meaning. search/catalog.html asks the catalog for a phrase rather than a title. On your own instance semantic search is off until an admin adds an embedding provider, turns it on under Admin > Settings > AI, and runs the embedding backfill (search guide).
  4. Build a map. Add layers from the catalog, style each one, set the viewport, save (map builder guide). The demo's showcase maps came in through the same maps API, by script rather than by hand.
  5. Analyze on the server. The builder's Analysis panel runs buffer, intersect, dissolve and five more operations in PostGIS and writes the result back to the catalog as a new dataset; all but dissolve preview on the map first (analysis guide). The demo answers anonymous analysis calls with 401, so there is no live example here; python/analyze.py does a comparable spatial join client-side.
  6. Publish or embed it. A share link gives the map a stable /m/<token> URL, and ?embed=true puts it in an iframe on a page that is not GeoLens: embed/iframe.html.
  7. Read it back from anywhere. The same catalog answers QGIS over OGC API Features (qgis/), the Python SDK (python/sdk-catalog.py), the TypeScript SDK (typescript/catalog-map.html), leafmap and GeoPandas in a notebook (leafmap/quickstart.ipynb), and plain SQL in DuckDB (duckdb/query.py).

Examples

Example Tool Demonstrates Run it
maplibre/vector-tiles.html MapLibre GL JS 6.x Vector tiles (MVT) cut per request from PostGIS Live
maplibre/features.html MapLibre GL JS 6.x GeoJSON features, click identify with no round-trip Live
maplibre/features-viewport.html MapLibre GL JS 6.x Features by viewport: bbox, rel="next" paging, cancellation, and the cap where vector tiles take over Live
maplibre/imagery.html MapLibre GL JS 6.x Raster tiles through a WebGL texture (needs CORS) Live
maplibre/pmtiles.html MapLibre GL JS 6.x + pmtiles 4.5 A PMTiles export as one committed static file: range reads where the host answers 206, no tile server either way Live
maplibre/pmtiles-live.html MapLibre GL JS 6.x + pmtiles 4.5 The same protocol pointed straight at a live export URL, no file committed: cross-origin range reads, served with CORS from GeoLens 1.16.1 on Live
arcgis-js/features.html ArcGIS Maps SDK for JavaScript 5.1 OGCFeatureLayer against the OGC API landing page Live
arcgis-js/imagery.html ArcGIS Maps SDK for JavaScript 5.1 WebTileLayer with Esri's {level}/{col}/{row} names Live
openlayers/features.html OpenLayers 10 OGC API Features, CRS84 reprojected on read Live
openlayers/imagery.html OpenLayers 10 XYZ raster, and what crossOrigin costs you Live
leaflet/features.html Leaflet 1.9 GeoJSON features straight into L.geoJSON Live
leaflet/imagery.html Leaflet 1.9 Raster tiles as plain <img>, so no CORS needed Live
typescript/catalog-map.html @geolens/sdk 1.20.0 + MapLibre Catalog search, schema and freshness, then the tile link the collection advertises (TypeScript SDK guide) Live
search/catalog.html MapLibre GL JS 6.x + fetch Semantic catalog search, narrowed to the map view, then drawn Live
embed/iframe.html No library A saved GeoLens map in an iframe, styling and legend intact Live
stac/browse.html MapLibre GL JS 6.x STAC item search over the map view, then the tile asset each item advertises Live
python/analyze.py Python (single-file uv run script) Features API → GeoPandas spatial join, metric-CRS analysis, styled plot uv run python/analyze.py
python/sdk-catalog.py geolens 1.20.0 (single-file uv run script) SDK catalog search, schema semantics, a server-side CQL2 filter count, export into GeoPandas (Python SDK guide) uv run python/sdk-catalog.py
leafmap/quickstart.ipynb leafmap + GeoPandas (Jupyter notebook) Catalog search, OGC API Features into GeoPandas, CQL2 filters on the catalog and on a dataset's rows, raster tiles rendered by TiTiler uv run --with jupyterlab --with ipykernel --with pip jupyter lab leafmap/quickstart.ipynb
leafmap/samgeo.ipynb leafmap + samgeo (Jupyter notebook) STAC item search by footprint, the two assets a by-reference import preserves, raster tiles, optional Segment Anything segmentation uv run --with jupyterlab --with ipykernel --with pip jupyter lab leafmap/samgeo.ipynb
mcp/ Any MCP client via the GeoLens MCP server Catalog search, schema, spatial queries, and tool chaining from an AI assistant (MCP server guide) claude mcp add geolens -e GEOLENS_INSTANCE=https://demo.getgeolens.com -- uvx geolens-mcp@1.20.0
qgis/ QGIS 4.2 OGC API Features + Records with CQL2, XYZ raster, tile-token auth https://demo.getgeolens.com/api/
duckdb/query.py DuckDB 1.5 + spatial (single-file uv run script) One SQL join across the GeoParquet export and the Features API, with column pruning over HTTP ranges uv run duckdb/query.py
cli/ geolens-cli 1.20.0 Catalog-as-code: offline validate, then apply --dry-run and apply against your instance uvx --from geolens-cli==1.20.0 geolens validate cli/geolens.yaml

Every browser row above with a Live link is checked against the live demo by ci/verify-examples.mjs, which asserts the documented data loaded and the map painted; a 200 response alone does not pass. Where an example draws two layers of its own in fixed colours, it also asserts each one painted, by colour. The three uv run scripts run green with one command each, and each asserts its own answers rather than just finishing. leafmap/quickstart.ipynb and leafmap/samgeo.ipynb are checked the same way, one level up: leafmap/verify.py and leafmap/verify_samgeo.py each execute their own notebook headlessly and fail on the first cell that raises.

MapLibre examples also work with Mapbox GL JS with minimal changes (both consume the same MVT and raster sources).

Running the browser examples

Each browser example is one static HTML file with no build step: the library loads from a pinned CDN. Serve the folder and open the page from there. Opened from disk, Chrome and Edge can't start MapLibre 6's worker, so vector and GeoJSON layers never draw and the status line says "Worker failed to load". That comes from the file:// URL, not from CORS. Firefox and Safari draw most pages from disk, but maplibre/pmtiles.html reads a file next to itself and needs the server:

python3 -m http.server 8000
# then visit http://localhost:8000/maplibre/vector-tiles.html

Using your own GeoLens instance

Each example declares its target at the top:

const GEOLENS = "https://demo.getgeolens.com";

Change it to your instance URL.

These examples need GeoLens v1.13.0 or newer. Raster tiles only started sending Access-Control-Allow-Origin in that release (geolens#1464), and without it the MapLibre and ArcGIS imagery examples draw an empty map while the server returns perfectly valid PNGs. The features and vector-tile examples reach further back: the anonymous CORS wildcard they depend on has been there since v1.4.7.

Dataset IDs and table names in these examples belong to the demo catalog. Against your own instance, list what's available at /api/collections and substitute your collection IDs. The demo gets reset from time to time, and a reset can change its dataset UUIDs, so treat the IDs hardcoded here as demo-specific rather than as part of any API.

CI replays every live browser example against the demo on every pull request, on each push to main, and every morning on a schedule, so an ID or share link that stops resolving turns the build red instead of quietly leaving you with a blank map. Those fixtures are named in ci/fixtures.json and probed before the browser sweep runs, so a reset shows up as a red preflight naming what moved rather than as every example failing at once.

Anonymous cross-origin reads work with no setup: GeoLens answers the standards paths (/api/collections, /api/stac, conformance), and from GeoLens 1.14.1 the native catalog search at /api/search/datasets/, with Access-Control-Allow-Origin: * as long as the request carries no credential. Send a credential and that wildcard is gone, so your page's origin has to be listed in the instance's CORS_ALLOWED_ORIGINS. A literal * there is rejected, since credentialed CORS requires explicit origins.

Authenticating against your own instance

The demo is public, so none of these examples send a credential. On your own instance, pick the method your client can actually use:

Client Use
fetch, an SDK, Python, GDAL, ArcGIS request interceptors X-Api-Key: <key> header, or Authorization: Bearer <jwt>
A static XYZ/MVT URL template that cannot set headers a signed tile token, scoped to one dataset and short-lived
Public data, including everything in the demo nothing

The ?api_key= query parameter carries the same key as the header, but from GeoLens 1.18.1 it only counts on GET, HEAD and OPTIONS requests. On anything else, a POST read such as /api/stac/search included, the key is treated as absent (_supplied_api_key in backend/app/modules/auth/dependencies.py, geolens#1845). The order GeoLens checks credentials in is documented in the authentication guide and implemented by _resolve_api_key and get_optional_user in the same file.

Prefer the header. A key in a URL ends up in browser history, server access logs, every proxy log along the way, analytics, screenshots, and anything anyone copy-pastes, which is why GeoLens deprecated the query lane in geolens#821 and kept it only for clients that genuinely cannot set a header. Desktop GIS consuming an XYZ template is the case it exists for.

Do not put a long-lived API key in a static HTML file, and do not commit one. Anyone who reads the page source has your key with all of your access until someone notices and revokes it. For a client that only reads, such as a dashboard, a CI check or a notebook, mint the key read_only: it authenticates reads and nothing else, so a leak cannot turn into a write (API keys).

Signed tile tokens

GET /api/tiles/token/<dataset_id>/ mints a token for a single dataset (the tile endpoints reference covers what a tile token is and why it is not an API key). A vector dataset returns sig, exp, and scope to append to the tile template; a raster dataset returns the whole tile_url with those already in the query string.

curl https://demo.getgeolens.com/api/tiles/token/d8fd56a9-d12f-4dbb-af8b-81a7289fc600/
# {"kind":"raster",
#  "tile_url":"/raster-tiles/d8fd.../tiles/{z}/{x}/{y}.png?sig=7031...&exp=1790127900&scope=d8fd...%3Ap0&v=1&pv=0",
#  "expires_in":160, ...}

exp is always a 15-minute boundary, usually the next one. When that boundary is under a minute away the mint skips to the following one instead, so a fresh token carries anywhere from 60 seconds to just under 16 minutes. Read expires_in off the response rather than assuming a fixed TTL. POST /api/tiles/tokens/ mints up to 50 in one call for a multi-layer map.

Two properties decide whether this fits your page.

Minting is itself authorized. A public, published dataset hands a token to anyone, which is why the curl above works signed out. A private one answers an anonymous mint with 401, so a page holding no credential cannot mint its own token and something server-side has to hold the key and pass tokens down. A scoped token does not remove the need for a credential. It keeps the credential out of the browser.

Tokens expire and clients do not renew them on their own. MapLibre keeps requesting whatever template you handed it, so a page that stays open has to re-mint and reset the source URL before exp passes. From GeoLens 1.19.0 a token also stops working within a minute of its dataset being unpublished or made private, whatever time it had left (tile endpoints).

X-Embed-Token is a different mechanism and not a substitute here. Embed tokens are minted per map by an authenticated owner, and the tile routes read them from the header only, so one cannot ride along in a URL template.

OGC API Features or vector tiles

The three features.html examples fetch every feature once with ?limit=2000 and hold the whole result in browser memory. That works for the demo's subway layers (496 stations, 29 lines). It is the wrong shape for a parcel, road, or building layer, where the same code silently renders one truncated page.

Use OGC API Features when the result is small or bounded, when you need attributes on the client, when you load by viewport or filter instead of all at once, or when the page interacts with individual features.

Use vector tiles when the dataset is large, when users pan and zoom across all of it, when not every feature needs to reach the browser, and when rendering performance matters more than holding the full attribute table. maplibre/vector-tiles.html shows that path: the server cuts MVT per tile and the client holds only what is on screen.

Every items response says which case you are in:

curl "https://demo.getgeolens.com/api/collections/4e7cba4c-4caa-4609-b5c4-3c6cd252697c/items?limit=2" \
  | jq '{numberMatched, numberReturned}'
# { "numberMatched": 496, "numberReturned": 2 }

numberMatched is what the query found; numberReturned is what this page contains. When they differ you are holding a partial result, which is the signal that a one-shot fetch has truncated your data.

It is not the signal that another page exists. Walk the stations collection at limit=400 and the last page returns 96 of 496 matched, counts differing, with no next link on it. The rel="next" link is the authority: follow it until it stops appearing, and read the counts as a diagnostic rather than a loop condition. Paging is keyset-based (after_gid=), so rows do not shift under a reader mid-scan. python/analyze.py does exactly that in a few lines. maplibre/features-viewport.html is the same guidance in a browser: it requests bbox=<view> on every settled view, follows next, cancels the walk a pan made stale, and stops at a per-view cap that says on screen when to switch to vector tiles. From GeoLens 1.18.0 the counts can also be approximate: past 20,000 matches on a filtered request (bbox, a property filter or CQL2), numberMatched is the database's estimate and the response carries X-GeoLens-Number-Matched: estimated (paging).

License

MIT. The examples are intentionally small; copy them into your project freely.

About

Runnable GeoLens integrations for MapLibre, QGIS, ArcGIS, Python, TypeScript, DuckDB, STAC, OGC APIs, CLI and MCP. Every live example runs against the public demo.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages