Places

Map tiles

Slippy map tiles as OpenStreetMap and Google number them: the tile of a point, a tile's bounds and pixel offsets, Bing quadkeys, the tiles covering an area for offline caching, and tile URLs from a template.

Version 1.0.0 · 7 functions · 0 crates

#maps#geo#caching#urls

Functions

What use map_tiles.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use map_tile_at, map_tile_pixel from map_tiles (Quillo 0.6).

map_tile_at(lat: float, lon: float, z: int) → anyor an error

The tile holding a point at zoom z: { z, x, y, quadkey }. Latitudes past ±85.0511° (the edge of Web Mercator) fall in the edge tiles.

map_tile_pixel(lat: float, lon: float, z: int, tile_size: int) → anyor an error

Where a point falls in its tile, in pixels from the tile's top left (tile_size 256 or 512): { z, x, y, px, py, world_x, world_y }, for markers drawn on a static map.

map_tile_bounds(z: int, x: int, y: int) → anyor an error

A tile's edges in degrees: { west, south, east, north, center: [lon, lat] }.

map_tile_from_quadkey(quadkey: str) → anyor an error

The tile a Bing Maps quadkey names ("120210"): { z, x, y, quadkey }.

map_tiles_cover(bbox: list, min_z: int, max_z: int, limit: int) → listor an error

The tiles covering an area, [west, south, east, north] in degrees, at each zoom from min_z to max_z: a list of { z, x, y, quadkey }, for caching a region for offline use. An error past limit tiles. An area across the 180° line has west > east.

map_tile_family(z: int, x: int, y: int) → anyor an error

A tile's parent one zoom out and its four children one zoom in: { parent, children }.

map_tile_url(template: str, z: int, x: int, y: int, subdomains: list) → stror an error

A tile's URL from a template: {z}, {x}, {y}, {-y} (TMS, counted from the bottom), {q} (quadkey) and {s} (one of subdomains, the same for a tile each time).

Get it

$ quillo install map_tiles               # map_tiles.qwer into this folder$ quillo install map_tiles --example     # and its example, map_tiles.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/map_tiles.qwer$ curl -O https://quillolang.org/plugins/files/map_tiles.qwe

Then, in your program, all of it or only what you call:

main.qwe
use map_tiles.qweruse map_tile_at, map_tile_pixel from map_tiles      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]

Example

map_tiles.qwe
// A map tile cache: which tiles hold a place, which to fetch to keep a city for offline use, and where to fetch them.////   quillo run map_tiles.qwe      then open http://localhost:8080/tilesuse "map_tiles.qwer"// The Brandenburg Gate, as in OpenStreetMap's own example of tile numbers.let GATE = { lat: 52.51628011262304, lon: 13.37771496361961 }route GET /tiles {    let tile = map_tile_at(GATE.lat, GATE.lon, 17)?    {        gate: tile,        bounds: map_tile_bounds(17, tile.x, tile.y)?,        pixel: map_tile_pixel(GATE.lat, GATE.lon, 17, 256)?,        bing: map_tile_from_quadkey("213")?,        osm: map_tile_url("https://tile.openstreetmap.org/{z}/{x}/{y}.png", 17, tile.x, tile.y, [])?,        tms: map_tile_url("https://{s}.tiles.example/{z}/{x}/{-y}.png", 3, 4, 2, ["a", "b", "c"])?,        family: map_tile_family(1, 1, 0)?,    }}// The tiles to keep central Berlin offline, zooms 10 to 13.route GET /offline {    let tiles = map_tiles_cover([13.30, 52.48, 13.45, 52.55], 10, 13, 500)?    { count: len(tiles), first: first(tiles), too_many: map_tiles_cover([-10.0, 35.0, 30.0, 60.0], 0, 12, 1000).error }}
$ quillo run map_tiles.qwe$ curl 'localhost:8080/tiles'$ curl 'localhost:8080/offline'

Source

map_tiles.qwer
//! Slippy map tiles as OpenStreetMap, Google and Mapbox number them: the tile of a point at a zoom, a tile's bounds, Bing quadkeys, the tiles covering an area (for caching a region offline), and tile URLs from a template.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};use std::f64::consts::PI;const MAX_LAT: f64 = 85.051_128_779_806_59;fn check_z(z: i64) -> Result<u32, String> {    if !(0..=30).contains(&z) {        return Err(format!("zoom is 0 to 30, not {}", z));    }    Ok(z as u32)}fn check_xy(x: i64, y: i64, z: u32) -> Result<(), String> {    let n = 1i64 << z;    if x < 0 || y < 0 || x >= n || y >= n {        return Err(format!("tile {}/{}/{} does not exist: x and y are 0 to {} at zoom {}", z, x, y, n - 1, z));    }    Ok(())}fn frac(lat: f64, lon: f64, z: u32) -> (f64, f64) {    let n = (1u64 << z) as f64;    let lat = lat.clamp(-MAX_LAT, MAX_LAT).to_radians();    let x = (lon + 180.0) / 360.0 * n;    let y = (1.0 - (lat.tan() + 1.0 / lat.cos()).ln() / PI) / 2.0 * n;    (x, y)}fn tile_of(lat: f64, lon: f64, z: u32) -> (i64, i64) {    let n = 1i64 << z;    let (x, y) = frac(lat, lon, z);    ((x.floor() as i64).clamp(0, n - 1), (y.floor() as i64).clamp(0, n - 1))}fn lon_of(x: f64, z: u32) -> f64 {    x / (1u64 << z) as f64 * 360.0 - 180.0}fn lat_of(y: f64, z: u32) -> f64 {    (PI * (1.0 - 2.0 * y / (1u64 << z) as f64)).sinh().atan().to_degrees()}fn quadkey(x: i64, y: i64, z: u32) -> String {    (1..=z).rev().map(|i| {        let mask = 1i64 << (i - 1);        let mut d = 0u8;        if x & mask != 0 { d += 1 }        if y & mask != 0 { d += 2 }        (b'0' + d) as char    }).collect()}fn round7(v: f64) -> f64 {    (v * 1e7).round() / 1e7}fn tile_json(x: i64, y: i64, z: u32) -> Value {    json!({ "z": z, "x": x, "y": y, "quadkey": quadkey(x, y, z) })}/// The tile holding a point at zoom `z`: `{ z, x, y, quadkey }`. Latitudes past ±85.0511° (the edge of Web Mercator) fall in the edge tiles.pub fn map_tile_at(lat: f64, lon: f64, z: i64) -> Result<Value, String> {    let z = check_z(z)?;    let (x, y) = tile_of(lat, lon, z);    Ok(tile_json(x, y, z))}/// Where a point falls in its tile, in pixels from the tile's top left (`tile_size` 256 or 512): `{ z, x, y, px, py, world_x, world_y }`, for markers drawn on a static map.pub fn map_tile_pixel(lat: f64, lon: f64, z: i64, tile_size: i64) -> Result<Value, String> {    let z = check_z(z)?;    let (fx, fy) = frac(lat, lon, z);    let (x, y) = tile_of(lat, lon, z);    let s = tile_size as f64;    Ok(json!({        "z": z, "x": x, "y": y,        "px": ((fx - x as f64) * s).floor() as i64, "py": ((fy - y as f64) * s).floor() as i64,        "world_x": (fx * s).floor() as i64, "world_y": (fy * s).floor() as i64,    }))}/// A tile's edges in degrees: `{ west, south, east, north, center: [lon, lat] }`.pub fn map_tile_bounds(z: i64, x: i64, y: i64) -> Result<Value, String> {    let z = check_z(z)?;    check_xy(x, y, z)?;    let (w, e) = (lon_of(x as f64, z), lon_of(x as f64 + 1.0, z));    let (n, s) = (lat_of(y as f64, z), lat_of(y as f64 + 1.0, z));    Ok(json!({        "west": round7(w), "south": round7(s), "east": round7(e), "north": round7(n),        "center": [round7(lon_of(x as f64 + 0.5, z)), round7(lat_of(y as f64 + 0.5, z))],    }))}/// The tile a Bing Maps quadkey names (`"120210"`): `{ z, x, y, quadkey }`.pub fn map_tile_from_quadkey(quadkey: &str) -> Result<Value, String> {    if quadkey.len() > 30 {        return Err("a quadkey has at most 30 digits".into());    }    let z = quadkey.len() as u32;    let (mut x, mut y) = (0i64, 0i64);    for (i, c) in quadkey.chars().enumerate() {        let mask = 1i64 << (z - 1 - i as u32);        match c {            '0' => {}            '1' => x |= mask,            '2' => y |= mask,            '3' => { x |= mask; y |= mask }            _ => return Err(format!("`{}` is not a quadkey: its digits are 0 to 3", quadkey)),        }    }    Ok(tile_json(x, y, z))}/// The tiles covering an area, `[west, south, east, north]` in degrees, at each zoom from `min_z` to `max_z`: a list of `{ z, x, y, quadkey }`, for caching a region for offline use. An error past `limit` tiles. An area across the 180° line has west > east.pub fn map_tiles_cover(bbox: Vec<f64>, min_z: i64, max_z: i64, limit: i64) -> Result<Vec<Value>, String> {    if bbox.len() != 4 {        return Err("the area is [west, south, east, north]".into());    }    let (w, s, e, n) = (bbox[0], bbox[1], bbox[2], bbox[3]);    if s > n {        return Err("the area's south is above its north".into());    }    let (lo, hi) = (check_z(min_z)?, check_z(max_z)?);    let mut out = Vec::new();    for z in lo..=hi {        let (x0, y0) = tile_of(n, w, z);        let (x1, y1) = tile_of(s, e, z);        let size = 1i64 << z;        let xs: Vec<i64> = if w <= e { (x0..=x1).collect() } else { (x0..size).chain(0..=x1).collect() };        let count = xs.len() as i64 * (y1 - y0 + 1);        if out.len() as i64 + count > limit {            return Err(format!("the area takes more than {} tiles by zoom {}", limit, z));        }        for y in y0..=y1 {            for &x in &xs {                out.push(tile_json(x, y, z));            }        }    }    Ok(out)}/// A tile's parent one zoom out and its four children one zoom in: `{ parent, children }`.pub fn map_tile_family(z: i64, x: i64, y: i64) -> Result<Value, String> {    let zu = check_z(z)?;    check_xy(x, y, zu)?;    let parent = if zu == 0 { Value::Null } else { tile_json(x / 2, y / 2, zu - 1) };    let children: Vec<Value> = if zu == 30 { vec![] } else {        vec![tile_json(2 * x, 2 * y, zu + 1), tile_json(2 * x + 1, 2 * y, zu + 1), tile_json(2 * x, 2 * y + 1, zu + 1), tile_json(2 * x + 1, 2 * y + 1, zu + 1)]    };    Ok(json!({ "parent": parent, "children": children }))}/// A tile's URL from a template: `{z}`, `{x}`, `{y}`, `{-y}` (TMS, counted from the bottom), `{q}` (quadkey) and `{s}` (one of `subdomains`, the same for a tile each time).pub fn map_tile_url(template: &str, z: i64, x: i64, y: i64, subdomains: Vec<String>) -> Result<String, String> {    let zu = check_z(z)?;    check_xy(x, y, zu)?;    let mut url = template        .replace("{z}", &z.to_string())        .replace("{x}", &x.to_string())        .replace("{-y}", &((1i64 << zu) - 1 - y).to_string())        .replace("{y}", &y.to_string())        .replace("{q}", &quadkey(x, y, zu));    if url.contains("{s}") {        let s = if subdomains.is_empty() { String::new() } else { subdomains[((x + y) as usize) % subdomains.len()].clone() };        url = url.replace("{s}", &s);    }    Ok(url)}
Coordinates coordinates.qwer · 1.0.0Coordinates as people write them, such as 52°22'N 4°53'E or N 52° 22.2', read as one, written as degrees-minutes-seconds, and converted to and from UTM.geomapsformsgps7 views · 8 downloadsH3 hexagons h3.qwer · 1.0.0H3 hexagon cells: the cell of a point at a resolution, its neighbours within k rings, its outline for a map, and distances between cells.geomapshexagonssearchdelivery7 views · 8 downloadsKML and KMZ kml.qwer · 1.0.0KML and KMZ files from Google Earth and My Maps read to GeoJSON, placemarks with their names, descriptions, folders and extended data, and GeoJSON written back as KML or KMZ.geogeojsonmapsimport7 views · 8 downloadsMaidenhead locators maidenhead.qwer · 1.0.0Maidenhead grid locators as radio amateurs use them (FN31pr, JN58td): from a latitude and longitude at 2 to 10 characters, back to the square's centre and corners, and the distance and bearing between two.geomapsgpslocation7 views · 8 downloadsMap clusters map_clusters.qwer · 1.0.0Map markers clustered by zoom as Supercluster does: thousands of points loaded once, then for each view and zoom the clusters with their counts and the single points, a cluster's points, and the zoom where it opens.mapsgeogeojsonperformance7 views · 7 downloadsMap projections projections.qwer · 1.0.0Coordinates moved between map projections offline, by EPSG code or PROJ string: GPS degrees to Web Mercator, UTM, the British, Dutch and Swiss grids and some 6,000 others, for points and whole GeoJSON.geomapsgeojsongps8 views · 9 downloads
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On Map tiles: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.