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 and the islands. The shoreline is mask.outlines(); the lines at sea are
outlines of mask.distance() at growing distances, fading out.
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
- 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_tilestiles (default 256). - 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/watermaskon macOS,%LOCALAPPDATA%\watermaskon Windows,~/.cache/watermaskon 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. - 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.
- 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.
- Shoreline. Marching squares at coverage ½, joined into polylines with water on the left.
- 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)
| File | Size | Uploaded | |
|---|---|---|---|
| watermask-0.1.0.tar.gz | 87.3 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| 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
|