Skip to main content

watermask

Water masks for any area on Earth: the sea, lakes, rivers, canals and docks as a coverage grid, shoreline polylines and a distance-to-shore field. The water comes from OpenStreetMap vector tiles fetched on demand, so there is nothing to download in advance: only the tiles covering the area, at the zoom that matches the output resolution.

One Rust core, published for Rust, Python, Node.js and the browser (WebAssembly).

Cape Cod, Martha's Vineyard and Nantucket: the shoreline, and lines following it out to sea

Cape Cod and the islands. The shoreline is mask.outlines(); the lines at sea are outlines of mask.distance() at growing distances, fading out.

Amsterdam: the canal ring and the IJ Venice and its lagoon Stockholm: islands between lake Mälaren and the Baltic

Amsterdam, Venice, Stockholm: mask.coverage in grey, the shoreline in black, waterway centre lines in grey. cargo run --release --example readme redraws every image here from live tiles.

import watermask

vineyard = (-70.85, 41.3, -70.45, 41.55)          # west, south, east, north
water = watermask.fetch(vineyard, width=1200)     # tiles are cached after the first run
mask = water.mask(vineyard, 1200)

mask.coverage      # (1000, 1200) float32, fraction of each pixel that is water
mask.outlines()    # shoreline: [(n, 2) float32] in pixels, water on the left
mask.distance()    # pixels to the shore, positive in water, negative on land
water.geojson()    # every piece of water as GeoJSON in lon/lat

Why

Deriving water from elevation (everything at or below 0 m) misses lakes and rivers, which sit above sea level, and floods land that lies below it: the Netherlands, deltas, reclaimed coasts. Complete coastline and water datasets exist (the OSM water polygons), but they are gigabytes and have to be tiled and stored before use. Vector tile services already serve that data cut into tiles, for free, one small tile at a time.

Where the water comes from

By default OpenFreeMap: the whole planet in the OpenMapTiles schema, rebuilt weekly from OpenStreetMap, free with no key and no request limits. Any source in that schema works (MapTiler, a self-hosted copy of OpenFreeMap's planet file): pass its TileJSON URL or a {z}/{x}/{y} URL template.

Two layers are read:

layer kind classes (default in bold)
water areas ocean, lake, river, dock, pond, swimming_pool
waterway lines river, canal, stream, ditch, drain

river areas are the water surface of wide rivers and canals; river lines are centre lines, of every river, however narrow. Seasonal water (intermittent) and water underground (tunnels) are left out unless asked for.

At zoom 14, the deepest, a tile is about 2.4 km across at the equator and its coordinates are quantised to about 0.6 m; OpenMapTiles simplifies geometry there by less than that, so this is full OpenStreetMap detail. At lower zooms tiles are simplified and small features dropped, in step with the pixel size.

Attribution. Maps made with this data must credit "© OpenMapTiles © OpenStreetMap contributors".

How it works

  1. Zoom. The shallowest zoom whose tiles (drawn 256 px wide) have pixels no larger than the output's, capped at the source's deepest zoom, then lowered while the area needs more than max_tiles tiles (default 256).
  2. Tiles. Fetched in parallel and cached on disk, only their water layers (a fifth of the bytes or less). The cache is $WATERMASK_CACHE, or else ~/Library/Caches/watermask on macOS, %LOCALAPPDATA%\watermask on Windows, ~/.cache/watermask on Linux. A cached tile is used for 30 days, across OpenFreeMap's weekly builds, then downloaded again; when offline, older tiles are used anyway. Tiles past 30 days that nobody asks for are deleted, checked once a day. The TileJSON is read each run, since OpenFreeMap's tile URLs change with every build, and the last good copy is used when offline.
  3. Decoding. A small protobuf reader pulls the two layers out of each tile and cuts every feature to its own tile, so the buffer tiles share with their neighbours is not counted twice.
  4. Mask. Polygons are filled with the nonzero winding rule, so the many pieces of one sea join without seams and islands stay holes. Each pixel gets its exact covered fraction along the row and four sub-rows down it.
  5. Shoreline. Marching squares at coverage ½, joined into polylines with water on the left.
  6. Distance. An exact Euclidean distance transform, signed.

Masks are on a north-up Web Mercator grid. In Rust, Water::mask_with takes any projection instead.

Rust

[dependencies]
watermask = "0.1"          # default-features = false drops the HTTP client
use watermask::{Fetcher, Filter, Grid, MaskOptions, ZoomLimits};

let bounds = [-70.85, 41.3, -70.45, 41.55];
let grid = Grid::with_width(bounds, 1200);
let water = Fetcher::new().water_for(&grid, &Filter::default(), &ZoomLimits::default(), |done, total| {
    eprintln!("{done}/{total} tiles");
})?;
let mask = water.mask(&grid, &MaskOptions::default());
let shore = mask.outlines();
let dist = mask.distance();

// Any projection: lon, lat → pixel.
let tm = water.mask_with(800, 600, &MaskOptions::default(), |lon, lat| my_projection(lon, lat));

Without the fetch feature, bring tiles from anywhere:

let mut water = watermask::Water::new();
for id in watermask::tiles_for(&bounds, 13) {
    water.add_tile(id, &bytes_of(id), &Filter::default())?;
}

cargo run --release --example render -- -70.85,41.3,-70.45,41.55 1200 out.png draws the mask, shoreline and waterways of an area.

Python

pip install watermask

The example at the top covers most uses. Also: watermask.fetch(bounds, width, areas=["ocean"], lines=[], source=..., log=print), water.lines(bounds, width) for waterway centre lines as (class, (n, 2) array), water.mask(..., line_width=1.5) to burn them into the mask, Water().add_tile(z, x, y, data) for your own tiles, zoom_for(bounds, width) and tiles_for(bounds, zoom).

To join the pieces into shapely geometry:

from shapely.geometry import shape
from shapely.ops import unary_union

sea = unary_union([shape(f["geometry"]) for f in water.geojson()["features"]
                   if f["properties"]["class"] == "ocean"])

Node.js

npm install watermask
const wm = require('watermask')

const vineyard = [-70.85, 41.3, -70.45, 41.55]
const water = wm.fetch(vineyard, { width: 1200 })   // blocks until the tiles are in
const mask = water.mask(vineyard, 1200)
mask.coverage()                 // Float32Array, row 0 at the top
const [pts, ends] = mask.outlines()   // x,y pairs; ends[i] = end of line i, in points
mask.distance()                 // Float32Array

Browser (WebAssembly)

npm install watermask-wasm

The page fetches the tiles; watermask reads them.

import init, { Water, zoomFor, tilesFor, tileUrl } from 'watermask-wasm'

await init()
const [w, s, e, n] = [-70.85, 41.3, -70.45, 41.55]
const tilejson = await (await fetch('https://tiles.openfreemap.org/planet')).json()
const z = zoomFor(w, s, e, n, 1200, 256, tilejson.maxzoom)
const ids = tilesFor(w, s, e, n, z)                 // z, x, y, z, x, y, …
const water = new Water()
await Promise.all(Array.from({ length: ids.length / 3 }, async (_, i) => {
  const [tz, tx, ty] = ids.slice(3 * i, 3 * i + 3)
  const r = await fetch(tileUrl(tilejson.tiles[0], tz, tx, ty))
  if (r.ok) water.addTile(tz, tx, ty, new Uint8Array(await r.arrayBuffer()))
}))
const mask = water.mask(w, s, e, n, 1200)

demo/index.html does this on a canvas: build the package into demo/pkg (below), then serve the folder (python3 -m http.server -d demo).

Building

cargo test                                   # core, offline (real tiles in tests/fixtures)
maturin develop --release && pytest          # Python, in a virtualenv
cd node && npm install && npm run build && npm test
wasm-pack build crates/watermask-wasm --release --target web --out-dir ../../demo/pkg

Limits

  • Masks are Web Mercator unless you project yourself (mask_with, Rust only).
  • Areas come back as tile pieces; the mask, outlines and distance are merged, the GeoJSON is not.
  • Water covered by something else (covered=yes) is not in the tiles.
  • Across the 180° meridian, tiles are fetched correctly; GeoJSON longitudes on the far side run past 180.

License

MIT.

Metadata

Release files for watermask 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for watermask 0.1.0
File Size Uploaded
watermask-0.1.0.tar.gz 87.3 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for watermask 0.1.0
File
watermask-0.1.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
watermask-0.1.0-cp310-abi3-musllinux_1_2_x86_64.whl CPython 3.10 abi3 Linux musl 1.2+ x86-64 Details
watermask-0.1.0-cp310-abi3-musllinux_1_2_aarch64.whl CPython 3.10 abi3 Linux musl 1.2+ ARM64 Details
watermask-0.1.0-cp310-abi3-manylinux_2_28_aarch64.whl CPython 3.10 abi3 Linux glibc 2.28+ ARM64 Details
watermask-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
watermask-0.1.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details
watermask-0.1.0-cp310-abi3-macosx_10_13_x86_64.whl CPython 3.10 abi3 macOS 10.13+ x86-64 Details

Total release size: 9.3 MB

Release files / watermask-0.1.0.tar.gz

Download URL watermask-0.1.0.tar.gz
Size 87.3 kB
Tags Source
SHA-256 checksum
How to use checksums
142abc624a110202c668fa683425013ec62f2e56e35c4a5c27752d060dfdc5e9
BLAKE2b-256 checksum
How to use checksums
0e7d8c9df8858a5967d1507b18fcac88f7f7dea16568c66a15ed6f8c256cd08f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / watermask-0.1.0-cp310-abi3-win_amd64.whl

Download URL watermask-0.1.0-cp310-abi3-win_amd64.whl
Size 1.2 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
947ba6565c49cf41b09b0635d07bb480676d6423e696413b75c711f06bfa22b5
BLAKE2b-256 checksum
How to use checksums
9f03fe509ce2be39a0a239ff0fffaf9b38b1e69020c9ef9c55b71a318fbe9a46
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / watermask-0.1.0-cp310-abi3-musllinux_1_2_x86_64.whl

Download URL watermask-0.1.0-cp310-abi3-musllinux_1_2_x86_64.whl
Size 1.5 MB
Tags CPython 3.10 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
1145012a3316816c0a0c371f1c6b751ab89788e1218081b7ea7e3a7386478b18
BLAKE2b-256 checksum
How to use checksums
71382423765c1c2c70ba6de93f286d6ff6fbd83741d5bbae21958634b2121149
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / watermask-0.1.0-cp310-abi3-musllinux_1_2_aarch64.whl

Download URL watermask-0.1.0-cp310-abi3-musllinux_1_2_aarch64.whl
Size 1.5 MB
Tags CPython 3.10 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
75f53cc1a80958c7cfdcb831e712eb34d9c0c2dfb0a2e9cc02753c82d41aaa08
BLAKE2b-256 checksum
How to use checksums
c3a5718b518edea6fb605c58c0278853ccb473d4179c783fe38a8922aa968ec0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / watermask-0.1.0-cp310-abi3-manylinux_2_28_aarch64.whl

Download URL watermask-0.1.0-cp310-abi3-manylinux_2_28_aarch64.whl
Size 1.3 MB
Tags CPython 3.10 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
598cd36a94361634855d29097f1fb6bb53cf04cf04f16c6c26ddb630cb9ba44a
BLAKE2b-256 checksum
How to use checksums
6481ac813da73eb08f3ac1dde983267b1ab57aa8a25baba8106eb7c28f770599
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / watermask-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL watermask-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 1.3 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
d34da65d2135d848043f51c8b77713a2fff434987052539daa611b29b6c3e2bf
BLAKE2b-256 checksum
How to use checksums
e8e39c49d41dc44a29f0a25aab5333acd4a90d3b64c8a2a554d23f83fee51702
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / watermask-0.1.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL watermask-0.1.0-cp310-abi3-macosx_11_0_arm64.whl
Size 1.2 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
a1465d28b60224057f8ec4c2c6f5caf4d51f9be1d6befb709db4f23cce1ca58c
BLAKE2b-256 checksum
How to use checksums
519c9564e8aabdc02efbcd98015fb83b69dc2cb5c66ca9d9759f4125f58cb68f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / watermask-0.1.0-cp310-abi3-macosx_10_13_x86_64.whl

Download URL watermask-0.1.0-cp310-abi3-macosx_10_13_x86_64.whl
Size 1.3 MB
Tags CPython 3.10 abi3 macOS 10.13+ x86-64
SHA-256 checksum
How to use checksums
253226472b33041a36a0554e04b9be2ddc89fedd8cb3d038532494c5b3eaede3
BLAKE2b-256 checksum
How to use checksums
adf82cd6c98052f01c8c14bc8077ebda8172741da5bee7ffb945f9cd4dadcf37
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

8 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page