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
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 errorThe 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 errorWhere 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 errorA tile's edges in degrees: { west, south, east, north, center: [lon, lat] }.
map_tile_from_quadkey(quadkey: str) → anyor an errorThe 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 errorThe 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 errorA 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 errorA 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/map_tiles.qwer$ curl -O https://quillolang.org/plugins/files/map_tiles.qweThen, in your program, all of it or only what you call:
use map_tiles.qweruse map_tile_at, map_tile_pixel from map_tiles // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// 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
//! 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)}Related
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.7 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.7 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.7 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.7 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.7 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.8 views · 9 downloadsThread 0 posts
On Map tiles: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.