diff --git a/examples/world.ts b/examples/world.ts new file mode 100644 index 0000000..9c23511 --- /dev/null +++ b/examples/world.ts @@ -0,0 +1,77 @@ +/** + * A clickable world map: `bun examples/world.ts` + * + * Click a country to select it, hover to see what is under the cursor, and + * press z to zoom to the selection or r to go back to the whole globe. + * + * The map is the canvas drawing polylines in degrees, which is all a map is + * once the canvas works in the caller's own coordinates. The clicking is the + * other direction: the cell under the cursor is turned back into a longitude + * and latitude and tested against the outlines, so the answer is the country + * actually there rather than whichever bounding box happened to be first. + */ +import { createApp, countryBounds, findCountry, WORLD_X, WORLD_Y } from "@profullstack/hqtui"; +import type { CountryOutline, Bounds } from "@profullstack/hqtui"; + +let selected: CountryOutline | undefined; +let hovered: CountryOutline | undefined; +let window: { x: Bounds; y: Bounds } = { x: WORLD_X, y: WORLD_Y }; + +const app = await createApp({ title: "hqtui · world" }); + +app.render(({ ui, theme }) => { + const shown = hovered ?? selected; + const zoomed = window.x !== WORLD_X; + + ui.panel( + { + title: "World", + subtitle: shown ? shown.name : "click a country", + footer: zoomed ? "z zoom · r reset · q quit" : "z zoom to selection · q quit", + }, + (p) => { + p.worldMap({ + ...window, + color: theme.border, + highlight: [selected?.iso || selected?.name || "", hovered?.iso || hovered?.name || ""], + highlightColor: theme.accent, + onSelect: (country) => { + selected = country; + app.invalidate(); + }, + onHover: (country) => { + if (country?.name !== hovered?.name) { + hovered = country; + app.invalidate(); + } + }, + }); + + p.row({ size: 1, gap: 2 }, (row) => { + row.keyValues( + [ + { label: "Selected", value: selected?.name ?? "—" }, + { label: "ISO", value: selected?.iso || "—" }, + ], + { spread: false }, + ); + }); + }, + ); +}); + +app.on("key", (key) => { + if (key.name === "z" && selected) { + window = countryBounds(selected); + app.invalidate(); + } + if (key.name === "r") { + window = { x: WORLD_X, y: WORLD_Y }; + app.invalidate(); + } +}); + +// Somewhere to start from, so the first frame is not an empty selection. +selected = findCountry("Japan"); + +await app.start(); diff --git a/packages/hqtui/scripts/generate-world.ts b/packages/hqtui/scripts/generate-world.ts new file mode 100644 index 0000000..cbceef2 --- /dev/null +++ b/packages/hqtui/scripts/generate-world.ts @@ -0,0 +1,280 @@ +/** + * Generates `src/graphics/world-data.ts` from Natural Earth's country polygons. + * + * bun packages/hqtui/scripts/generate-world.ts + * + * The source is Natural Earth 1:110m Admin 0 countries, which is public domain + * . It is fetched rather + * than committed: the raw file is 800KB of GeoJSON and this only needs running + * when the borders change, which is roughly never. + * + * The output is committed. Regenerating it should be a no-op unless the + * tolerance below changes. + */ +import { writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const SOURCE = + "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson"; + +/** + * Douglas-Peucker tolerance, in degrees. + * + * A world map across an eighty-column terminal is about 160 Braille pixels + * wide, so one pixel is a bit over two degrees. At one degree the simplified + * outline is already finer than anything a terminal can show, and it keeps 171 + * of the 177 countries; the six it drops are island states smaller than a pixel, + * which could be neither seen nor clicked. + */ +const TOLERANCE = 1.0; + +/** A country whose bounding box is smaller than this cannot be drawn or hit. */ +const MIN_SPAN = 1.5; + +type Point = [number, number]; + +function perpendicular(p: Point, a: Point, b: Point): number { + const [px, py] = p; + const [ax, ay] = a; + const [bx, by] = b; + const dx = bx - ax; + const dy = by - ay; + if (dx === 0 && dy === 0) return Math.hypot(px - ax, py - ay); + const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy))); + return Math.hypot(px - (ax + t * dx), py - (ay + t * dy)); +} + +/** Douglas-Peucker, iterative so a long coastline cannot blow the stack. */ +function simplify(points: Point[], tolerance: number): Point[] { + if (points.length < 3) return points; + const keep = new Array(points.length).fill(false); + keep[0] = true; + keep[points.length - 1] = true; + const stack: [number, number][] = [[0, points.length - 1]]; + while (stack.length > 0) { + const [lo, hi] = stack.pop() as [number, number]; + let worst = 0; + let at = -1; + for (let i = lo + 1; i < hi; i++) { + const d = perpendicular(points[i], points[lo], points[hi]); + if (d > worst) { + worst = d; + at = i; + } + } + if (worst > tolerance && at !== -1) { + keep[at] = true; + stack.push([lo, at], [at, hi]); + } + } + return points.filter((_, i) => keep[i]); +} + +interface Feature { + properties: Record; + geometry: { type: string; coordinates: number[][][] | number[][][][] }; +} + +const response = await fetch(SOURCE); +if (!response.ok) throw new Error(`${SOURCE} responded ${response.status}`); +const collection = (await response.json()) as { features: Feature[] }; + +const countries: { name: string; iso: string; rings: Point[][] }[] = []; +for (const feature of collection.features) { + const props = feature.properties; + const name = String(props.NAME ?? props.NAME_LONG ?? ""); + const iso = String(props.ISO_A2_EH ?? props.ISO_A2 ?? ""); + if (!name) continue; + + const geometry = feature.geometry; + const polygons = (geometry.type === "MultiPolygon" + ? geometry.coordinates + : [geometry.coordinates]) as number[][][][]; + + const rings: Point[][] = []; + for (const polygon of polygons) { + // Only the outer ring. A lake inside a country is not a border at this + // resolution, and carrying the holes doubles the data for nothing. + const ring = polygon[0].map(([x, y]) => [x, y] as Point); + const xs = ring.map((p) => p[0]); + const ys = ring.map((p) => p[1]); + if (Math.max(...xs) - Math.min(...xs) < MIN_SPAN && + Math.max(...ys) - Math.min(...ys) < MIN_SPAN) continue; + const cut = simplify(ring, TOLERANCE); + if (cut.length >= 3) { + rings.push(cut.map(([x, y]) => [Math.round(x * 10) / 10, Math.round(y * 10) / 10] as Point)); + } + } + if (rings.length > 0) countries.push({ name, iso, rings }); +} + +countries.sort((a, b) => a.name.localeCompare(b.name)); + +const points = countries.reduce((n, c) => n + c.rings.reduce((m, r) => m + r.length, 0), 0); +const here = dirname(fileURLToPath(import.meta.url)); +const repo = join(here, "..", "..", ".."); + +/** The same provenance note at the top of every generated file. */ +function banner(comment: string): string { + const line = (text: string) => (text ? `${comment} ${text}` : comment.trimEnd()); + return [ + line("Country outlines, flattened for a terminal."), + line(""), + line("Generated by packages/hqtui/scripts/generate-world.ts from Natural Earth's"), + line("1:110m Admin 0 countries, which is public domain. Do not edit by hand."), + line(""), + line("Each ring is longitude and latitude interleaved -- lon, lat, lon, lat --"), + line(`rather than a list of pairs, because at ${points} points the nested form`), + line("costs a container per coordinate for no gain. A country has more than one"), + line("ring when it is more than one landmass."), + line(""), + line(`${countries.length} countries, ${points} points, simplified at ${TOLERANCE} degrees.`), + ].join("\n"); +} + +const flat = (ring: Point[]) => ring.flatMap(([x, y]) => [x, y]); +/** A float literal every language reads the same way, including whole numbers. */ +const real = (v: number) => (Number.isInteger(v) ? `${v}.0` : `${v}`); +const quoted = (v: string) => JSON.stringify(v); + +const write = (relative: string, text: string) => { + const target = join(repo, relative); + writeFileSync(target, text); + console.log(` ${relative}`); +}; + +// ------------------------------------------------------------------ TypeScript +write( + "packages/hqtui/src/graphics/world-data.ts", + `/**\n${banner(" *")}\n */\n\n` + + `export interface CountryOutline {\n name: string;\n` + + ` /** ISO 3166-1 alpha-2, where Natural Earth has one. */\n iso: string;\n` + + ` /** Longitude and latitude, interleaved. */\n rings: number[][];\n}\n\n` + + `export const WORLD_COUNTRIES: readonly CountryOutline[] = [\n` + + countries + .map((c) => + ` {\n name: ${quoted(c.name)},\n iso: ${quoted(c.iso)},\n rings: [\n` + + c.rings.map((r) => ` [${flat(r).join(",")}],`).join("\n") + + `\n ],\n },`) + .join("\n") + + `\n];\n`, +); + +// ------------------------------------------------------------------------ Rust +write( + "ports/rust/src/graphics/world_data.rs", + `${banner("//!")}\n\npub struct CountryOutline {\n pub name: &'static str,\n` + + ` /// ISO 3166-1 alpha-2, where Natural Earth has one.\n pub iso: &'static str,\n` + + ` /// Longitude and latitude, interleaved.\n pub rings: &'static [&'static [f64]],\n}\n\n` + + `pub static WORLD_COUNTRIES: &[CountryOutline] = &[\n` + + countries + .map((c) => + ` CountryOutline {\n name: ${quoted(c.name)},\n iso: ${quoted(c.iso)},\n` + + ` rings: &[\n` + + c.rings.map((r) => ` &[${flat(r).map(real).join(",")}],`).join("\n") + + `\n ],\n },`) + .join("\n") + + `\n];\n`, +); + +// -------------------------------------------------------------------------- Go +write( + "ports/go/world_data.go", + `package hqtui\n\n${banner("//")}\n\ntype CountryOutline struct {\n\tName string\n` + + `\t// ISO 3166-1 alpha-2, where Natural Earth has one.\n\tISO string\n` + + `\t// Rings are longitude and latitude, interleaved.\n\tRings [][]float64\n}\n\n` + + `var WorldCountries = []CountryOutline{\n` + + countries + .map((c) => + `\t{\n\t\tName: ${quoted(c.name)},\n\t\tISO: ${quoted(c.iso)},\n\t\tRings: [][]float64{\n` + + c.rings.map((r) => `\t\t\t{${flat(r).join(",")}},`).join("\n") + + `\n\t\t},\n\t},`) + .join("\n") + + `\n}\n`, +); + +// ---------------------------------------------------------------------- Python +write( + "ports/python/hqtui/graphics/world_data.py", + `"""\n${banner("")}\n"""\n\nfrom __future__ import annotations\n\n` + + `from dataclasses import dataclass\n\n\n` + + `@dataclass(frozen=True, slots=True)\nclass CountryOutline:\n name: str\n` + + ` #: ISO 3166-1 alpha-2, where Natural Earth has one.\n iso: str\n` + + ` #: Longitude and latitude, interleaved.\n rings: tuple[tuple[float, ...], ...]\n\n\n` + + `WORLD_COUNTRIES: tuple[CountryOutline, ...] = (\n` + + countries + .map((c) => + ` CountryOutline(\n ${quoted(c.name)},\n ${quoted(c.iso)},\n (\n` + + c.rings.map((r) => ` (${flat(r).join(",")}),`).join("\n") + + `\n ),\n ),`) + .join("\n") + + `\n)\n`, +); + +// ------------------------------------------------------------------------- Zig +write( + "ports/zig/src/graphics/world_data.zig", + `${banner("//!")}\n\npub const CountryOutline = struct {\n name: []const u8,\n` + + ` /// ISO 3166-1 alpha-2, where Natural Earth has one.\n iso: []const u8,\n` + + ` /// Longitude and latitude, interleaved.\n rings: []const []const f64,\n};\n\n` + + `pub const WORLD_COUNTRIES = [_]CountryOutline{\n` + + countries + .map((c) => + ` .{\n .name = ${quoted(c.name)},\n .iso = ${quoted(c.iso)},\n` + + ` .rings = &.{\n` + + c.rings.map((r) => ` &.{${flat(r).map(real).join(",")}},`).join("\n") + + `\n },\n },`) + .join("\n") + + `\n};\n`, +); + +// ------------------------------------------------------------------------- C++ +// A flat coordinate pool with index tables, rather than nested initialiser +// lists: the nested form is the same data but minutes of compile time, and this +// is the one language where that bites. +const pool: number[] = []; +const ringSpans: [number, number][] = []; +const countrySpans: [number, number][] = []; +for (const c of countries) { + const first = ringSpans.length; + for (const ring of c.rings) { + const at = pool.length; + pool.push(...flat(ring)); + ringSpans.push([at, pool.length - at]); + } + countrySpans.push([first, ringSpans.length - first]); +} +const chunk = (values: string[], per: number) => { + const lines: string[] = []; + for (let i = 0; i < values.length; i += per) lines.push(` ${values.slice(i, i + per).join(",")},`); + return lines.join("\n"); +}; +write( + "ports/cpp/src/world_data.cpp", + `${banner("///")}\n#include \n\nnamespace hqtui {\nnamespace {\n\n` + + `const double POOL[] = {\n${chunk(pool.map(real), 12)}\n};\n\n` + + `struct Span {\n int at, length;\n};\n\n` + + `const Span RINGS[] = {\n${chunk(ringSpans.map(([a, n]) => `{${a},${n}}`), 8)}\n};\n\n` + + `const Span COUNTRIES[] = {\n${chunk(countrySpans.map(([a, n]) => `{${a},${n}}`), 8)}\n};\n\n` + + `const char *const NAMES[] = {\n${chunk(countries.map((c) => quoted(c.name)), 4)}\n};\n\n` + + `const char *const ISO[] = {\n${chunk(countries.map((c) => quoted(c.iso)), 12)}\n};\n\n` + + `} // namespace\n\n` + + `/// Built once, on first use: the pool above is plain static data, and this\n` + + `/// turns it into the shape the rest of the code wants without a static\n` + + `/// initialiser that has to run before main.\n` + + `const std::vector &world_countries() {\n` + + ` static const std::vector countries = [] {\n` + + ` std::vector out;\n` + + ` out.reserve(${countries.length});\n` + + ` for (std::size_t i = 0; i < ${countries.length}; i++) {\n` + + ` CountryOutline c;\n c.name = NAMES[i];\n c.iso = ISO[i];\n` + + ` const Span &cs = COUNTRIES[i];\n` + + ` for (int r = 0; r < cs.length; r++) {\n` + + ` const Span &rs = RINGS[cs.at + r];\n` + + ` c.rings.emplace_back(POOL + rs.at, POOL + rs.at + rs.length);\n` + + ` }\n out.push_back(std::move(c));\n }\n return out;\n }();\n` + + ` return countries;\n}\n\n} // namespace hqtui\n`, +); + +console.log(`${countries.length} countries, ${points} points`); diff --git a/packages/hqtui/scripts/world-probe.ts b/packages/hqtui/scripts/world-probe.ts new file mode 100644 index 0000000..3cb0d4d --- /dev/null +++ b/packages/hqtui/scripts/world-probe.ts @@ -0,0 +1,42 @@ +/** + * Prints what the country lookup answers for a fixed set of points. + * + * The same probe exists for every port, so "the ports agree about the world" is + * a diff rather than a hope. The reference output is this one, because the + * TypeScript implementation is the reference for everything else too. + * + * bun packages/hqtui/scripts/world-probe.ts + */ +import { WORLD_X, WORLD_Y, countryAt, degreesAt } from "../src/graphics/world.ts"; +import { countryAtCell } from "../src/widgets/world.ts"; + +const places: [string, number, number][] = [ + ["Paris", 2.35, 48.86], + ["Tokyo", 139.7, 35.7], + ["Cairo", 31.2, 30.0], + ["Brasilia", -47.9, -15.8], + ["Canberra", 149.1, -35.3], + ["Denver", -105.0, 39.7], + ["Moscow", 37.6, 55.75], + ["Delhi", 77.2, 28.6], + ["Nairobi", 36.8, -1.3], + ["Pacific", -140.0, 0.0], + ["Atlantic", -30.0, 0.0], + ["SouthernOcean", 80.0, -40.0], + ["NorthPacific", -150.0, 40.0], +]; +for (const [name, lon, lat] of places) { + console.log(`${name} ${countryAt(lon, lat)?.name ?? "-"}`); +} + +// The cell path, which has to agree with what the canvas drew. +for (const [column, row] of [[173, 28], [74, 2], [20, 25], [88, 7]]) { + console.log(`cell:${column},${row} ${countryAtCell(column, row, 200, 50)?.name ?? "-"}`); +} + +// And the projection itself, so a drift shows up as a number rather than as a +// country that happens to still be right. +for (const [column, row] of [[0, 0], [99, 25], [50, 13]]) { + const at = degreesAt(column, row, 100, 26, WORLD_X, WORLD_Y); + console.log(`degrees:${column},${row} ${at!.lon.toFixed(4)} ${at!.lat.toFixed(4)}`); +} diff --git a/packages/hqtui/src/graphics/index.ts b/packages/hqtui/src/graphics/index.ts index 3feeadb..c0a8c29 100644 --- a/packages/hqtui/src/graphics/index.ts +++ b/packages/hqtui/src/graphics/index.ts @@ -2,4 +2,5 @@ export { BrailleCanvas } from "./braille.ts"; export * from "./blocks.ts"; export * from "./plot.ts"; export * from "./canvas.ts"; +export * from "./world.ts"; export * from "./chart.ts"; diff --git a/packages/hqtui/src/graphics/world-data.ts b/packages/hqtui/src/graphics/world-data.ts new file mode 100644 index 0000000..d794667 --- /dev/null +++ b/packages/hqtui/src/graphics/world-data.ts @@ -0,0 +1,1308 @@ +/** + * Country outlines, flattened for a terminal. + * + * Generated by packages/hqtui/scripts/generate-world.ts from Natural Earth's + * 1:110m Admin 0 countries, which is public domain. Do not edit by hand. + * + * Each ring is longitude and latitude interleaved -- lon, lat, lon, lat -- + * rather than a list of pairs, because at 1982 points the nested form + * costs a container per coordinate for no gain. A country has more than one + * ring when it is more than one landmass. + * + * 171 countries, 1982 points, simplified at 1 degrees. + */ + +export interface CountryOutline { + name: string; + /** ISO 3166-1 alpha-2, where Natural Earth has one. */ + iso: string; + /** Longitude and latitude, interleaved. */ + rings: number[][]; +} + +export const WORLD_COUNTRIES: readonly CountryOutline[] = [ + { + name: "Afghanistan", + iso: "AF", + rings: [ + [66.5,37.4,70.8,38.5,71.8,36.7,75.2,37.1,71.3,36.1,69.3,31.9,66.3,29.9,60.9,29.8,61.2,35.7,66.5,37.4], + ], + }, + { + name: "Albania", + iso: "AL", + rings: [ + [21,40.8,19.4,40.3,19.7,42.7,21,40.8], + ], + }, + { + name: "Algeria", + iso: "DZ", + rings: [ + [-8.7,27.4,-8.7,28.8,-1.3,32.3,-1.2,35.7,8.4,36.9,7.5,34.1,9.8,29.4,9.3,26.1,12,23.5,3.2,19.1,-8.7,27.4], + ], + }, + { + name: "Angola", + iso: "AO", + rings: [ + [12.3,-6.1,16.3,-5.9,17.5,-8.1,21.7,-7.3,22.2,-11.1,24,-11.2,24,-12.9,21.9,-12.9,23.2,-17.5,11.7,-17.3,13.7,-11.3,12.3,-6.1], + ], + }, + { + name: "Antarctica", + iso: "AQ", + rings: [ + [-48.7,-78,-43.9,-78.5,-43.3,-80,-54.2,-80.6,-48.7,-78], + [-66.3,-80.3,-59.6,-80,-66.3,-80.3], + [-73.9,-71.3,-70.3,-68.9,-68.3,-71.4,-75,-72.1,-73.9,-71.3], + [-102.3,-71.9,-96.2,-72.5,-102.3,-71.9], + [-122.6,-73.7,-118.7,-73.5,-122.6,-73.7], + [-127.3,-73.5,-124,-73.9,-127.3,-73.5], + [-163.7,-78.6,-159.2,-79.5,-163.7,-78.6], + [180,-84.7,180,-90,-180,-90,-179.1,-84.1,-143.1,-85,-153.6,-83.7,-152.9,-82,-156.8,-81.1,-146.4,-80.3,-155.3,-79.1,-158.4,-76.9,-151.3,-77.4,-146.1,-76.5,-146.2,-75.4,-135.2,-74.3,-100.1,-74.9,-103.7,-72.6,-74.9,-73.9,-67.4,-72.5,-67.7,-67.3,-57.8,-63.3,-65.7,-68,-61.8,-70.7,-60.8,-73.7,-70.6,-76.6,-77.2,-76.7,-73.7,-77.9,-78,-79.2,-58.2,-83.2,-28.5,-80.3,-35.6,-79.5,-35.8,-78.3,-17.5,-75.1,-15.4,-73.1,-6.9,-70.9,27.1,-70.5,33.9,-68.5,38.6,-69.8,54.5,-65.8,61.4,-68,68.9,-67.9,69.7,-69.2,67.9,-71.9,69.9,-72.3,73.9,-69.9,88,-66.2,95.8,-67.4,102.8,-65.6,106.2,-66.9,113.6,-65.9,119.8,-67.3,135.1,-65.3,137.5,-67,145.5,-66.9,171.2,-71.7,163.6,-76.2,167,-78.8,161.8,-79.2,159.8,-80.9,169.4,-83.8,180,-84.7], + ], + }, + { + name: "Argentina", + iso: "AR", + rings: [ + [-68.6,-52.6,-65,-54.7,-68.6,-54.9,-68.6,-52.6], + [-57.6,-30.2,-58.5,-34.4,-56.8,-36.9,-62.3,-38.8,-62.7,-41,-65.1,-41.1,-63.5,-42.6,-67.3,-45.6,-65.6,-47.2,-69.1,-50.7,-68.1,-52.3,-71.9,-52,-73.4,-49.3,-71.2,-44.8,-72.1,-42.3,-68.4,-24.5,-66.3,-21.8,-62.8,-22,-57.8,-25.2,-58.6,-27.1,-55.7,-27.4,-54.1,-25.5,-53.6,-26.9,-57.6,-30.2], + ], + }, + { + name: "Armenia", + iso: "AM", + rings: [ + [46.5,38.8,43.6,41.1,45.6,40.8,46.5,38.8], + ], + }, + { + name: "Australia", + iso: "AU", + rings: [ + [147.7,-40.8,147.9,-43.2,146,-43.5,144.7,-40.7,147.7,-40.8], + [126.1,-32.2,118,-35.1,115,-34.2,113.7,-22.5,120.9,-19.7,125.7,-14.2,129.6,-15,132.4,-11.1,136.5,-11.9,135.5,-15,140.2,-17.7,142.5,-10.7,146.4,-19,150.7,-22.4,153.6,-28.1,150,-37.4,146.3,-39,140.6,-38,138.2,-34.4,136.8,-35.3,137.8,-32.9,136,-34.9,131.3,-31.5,126.1,-32.2], + ], + }, + { + name: "Austria", + iso: "AT", + rings: [ + [17,48.1,14.6,46.4,9.5,47.1,12.9,47.5,13.6,48.9,17,48.1], + ], + }, + { + name: "Azerbaijan", + iso: "AZ", + rings: [ + [46.4,41.9,50.4,40.3,48.9,38.3,45.6,39.9,45,41.2,46.4,41.9], + ], + }, + { + name: "Bahamas", + iso: "BS", + rings: [ + [-78.2,25.2,-77.5,23.8,-78.2,25.2], + ], + }, + { + name: "Bangladesh", + iso: "BD", + rings: [ + [92.7,22,92.4,20.7,91.4,22.8,89,22.1,88.6,26.4,92.4,25,91.2,23.5,92.7,22], + ], + }, + { + name: "Belarus", + iso: "BY", + rings: [ + [28.2,56.2,30.9,55.6,32.7,53.4,31.8,52.1,23.5,51.6,23.5,53.9,28.2,56.2], + ], + }, + { + name: "Belgium", + iso: "BE", + rings: [ + [6.2,50.8,5.7,49.5,2.5,51.1,6.2,50.8], + ], + }, + { + name: "Belize", + iso: "BZ", + rings: [ + [-89.1,17.8,-88.1,18.3,-88.9,15.9,-89.1,17.8], + ], + }, + { + name: "Benin", + iso: "BJ", + rings: [ + [2.7,6.3,0.8,10.5,2.8,12.2,2.7,6.3], + ], + }, + { + name: "Bhutan", + iso: "BT", + rings: [ + [91.7,27.8,88.8,27.1,91.7,27.8], + ], + }, + { + name: "Bolivia", + iso: "BO", + rings: [ + [-69.5,-11,-65.3,-9.8,-65.4,-11.6,-60.5,-13.8,-60.2,-16.3,-58.2,-16.3,-57.9,-20,-61.8,-19.6,-62.7,-22.2,-67.8,-22.9,-69.5,-11], + ], + }, + { + name: "Bosnia and Herz.", + iso: "BA", + rings: [ + [18.6,42.7,16,45.2,19.4,44.9,18.6,42.7], + ], + }, + { + name: "Botswana", + iso: "BW", + rings: [ + [29.4,-22.1,25.7,-25.5,21.6,-26.7,19.9,-24.8,20.9,-18.3,25.3,-17.7,29.4,-22.1], + ], + }, + { + name: "Brazil", + iso: "BR", + rings: [ + [-53.4,-33.8,-53.8,-32,-57.6,-30.2,-53.6,-26.1,-55.8,-22.4,-57.9,-22.1,-58.2,-16.3,-60.2,-16.3,-60.5,-13.8,-65.4,-11.6,-65.3,-9.8,-70.5,-11,-70.5,-9.5,-72.2,-10.1,-74,-7.5,-72.9,-5.3,-69.9,-4.3,-69.8,1.7,-65.5,0.8,-63.4,2.2,-64.8,4.1,-60.7,5.2,-59,1.3,-52.9,2.1,-51.3,4.2,-50.4,-0.1,-44.6,-2.7,-40,-2.9,-35.6,-5.1,-34.7,-7.3,-38.7,-13.1,-40.9,-21.9,-47.6,-24.9,-48.9,-28.7,-53.4,-33.8], + ], + }, + { + name: "Bulgaria", + iso: "BG", + rings: [ + [22.7,44.2,28.6,43.7,28,42,23,41.3,22.7,44.2], + ], + }, + { + name: "Burkina Faso", + iso: "BF", + rings: [ + [-5.4,10.4,-4.3,13.2,-1.1,15,2.2,12.6,0.9,11,-2.9,11,-2.8,9.6,-5.4,10.4], + ], + }, + { + name: "Burundi", + iso: "BI", + rings: [ + [30.5,-2.4,29.3,-4.5,29,-2.8,30.5,-2.4], + ], + }, + { + name: "Cambodia", + iso: "KH", + rings: [ + [102.6,12.2,103,14.2,107.6,13.5,106.2,11,103.5,10.6,102.6,12.2], + ], + }, + { + name: "Cameroon", + iso: "CM", + rings: [ + [14.5,12.9,14.5,4.7,15.9,1.7,9.6,2.3,8.8,5.5,11.7,7,14.5,12.9], + ], + }, + { + name: "Canada", + iso: "CA", + rings: [ + [-122.8,49,-127.4,50.8,-130.5,54.3,-130,55.9,-135.5,59.8,-137.5,58.9,-141,60.3,-141,69.7,-136.5,68.9,-128.1,70.5,-113.5,67.7,-106.1,68.8,-101.5,67.6,-97.7,68.6,-96.1,67.3,-94.2,69.1,-96.5,70.1,-95.2,71.9,-87.4,67.2,-85.5,69.9,-82.6,69.7,-81.4,67.1,-85.8,66.6,-90.7,63.6,-94.7,58.9,-92.3,57.1,-82.3,55.1,-79.9,51.2,-78.6,52.6,-79.8,54.7,-76.5,56.5,-78.5,58.8,-77.3,59.9,-78.1,62.3,-73.8,62.4,-69.6,61.1,-67.6,58.2,-64.6,60.3,-61.8,56.3,-57.3,54.6,-55.7,52.1,-60,50.2,-66.4,50.2,-71.1,46.8,-65.1,49.2,-64.5,46.2,-60.5,47,-59.8,45.9,-65.4,43.5,-66.2,44.5,-64.4,45.3,-67.1,45.1,-69.2,47.4,-71.5,45,-82.4,41.7,-82.6,45.3,-88.4,48.3,-122.8,49], + [-84,62.5,-81.9,62.9,-84,62.5], + [-79.8,72.8,-80.8,73.7,-76.3,72.8,-79.8,72.8], + [-93.6,75,-96.8,74.9,-93.6,75], + [-93.8,77.5,-96.4,77.8,-93.8,77.5], + [-96.8,78.8,-95.6,78.4,-98.6,78.9,-96.8,78.8], + [-88.2,74.4,-97.1,76.8,-79.8,74.9,-88.2,74.4], + [-111.3,78.2,-109.9,78,-113.5,77.7,-111.3,78.2], + [-111,78.8,-109.7,78.6,-112.5,78.4,-111,78.8], + [-55.6,51.3,-56.8,49.8,-53.5,49.2,-53.1,46.7,-59.3,47.6,-55.6,51.3], + [-83.9,65.1,-80.1,63.7,-87.2,63.5,-85.9,65.7,-83.9,65.1], + [-78.8,72.4,-68.8,70.5,-67,69.2,-68.8,68.7,-61.9,66.9,-63.9,65,-68,66.3,-64.7,63.4,-68.8,63.7,-66.2,61.9,-68.9,62.3,-78.6,64.6,-74,65.5,-73.3,68.1,-79,70.2,-88.7,70.4,-90.2,72.2,-85.8,73.8,-85.8,72.5,-82.3,73.8,-78.8,72.4], + [-94.5,74.1,-90.5,73.9,-95.4,72.1,-96,73.4,-94.5,74.1], + [-122.9,76.1,-116.2,77.6,-122.9,76.1], + [-132.7,54,-131.2,52.2,-132.7,54], + [-105.5,79.3,-99.7,77.9,-105.5,79.3], + [-123.5,48.5,-128.4,50.8,-123.5,48.5], + [-121.5,74.4,-115.5,73.5,-123.1,70.9,-125.9,71.9,-123.9,73.7,-124.9,74.3,-121.5,74.4], + [-107.8,75.8,-105.7,75.5,-117.7,75.2,-115.4,76.5,-107.8,75.8], + [-106.5,73.1,-101.1,69.6,-113.3,68.5,-117.3,70,-112.4,70.4,-119.4,71.6,-115.2,73.3,-108.2,71.7,-108.4,73.1,-106.5,73.1], + [-100.4,72.7,-101.5,73.4,-97.4,73.8,-96.5,72.6,-98.4,71.3,-102.5,72.5,-100.4,72.7], + [-106.6,73.6,-104.5,73.4,-106.6,73.6], + [-98.5,76.7,-98.2,75,-102.5,75.6,-98.5,76.7], + [-96,80.6,-92.4,81.3,-85.8,79.3,-92.9,78.3,-96,80.6], + [-91.6,81.9,-61.8,82.6,-76.9,79.3,-75.4,78.5,-80.6,76.2,-89.5,76.5,-88.3,77.9,-85,77.5,-88,78.4,-85.1,79.3,-86.9,80.3,-81.8,80.5,-91.6,81.9], + [-75.2,67.4,-77.2,67.6,-75.2,67.4], + [-96.3,69.5,-99.8,69.4,-96.3,69.5], + [-64.5,49.9,-61.8,49.1,-64.5,49.9], + [-64,47,-62,46.4,-64,47], + ], + }, + { + name: "Central African Rep.", + iso: "CF", + rings: [ + [27.4,5.2,22.4,4,19.5,5,16,2.3,14.5,5.5,15.3,7.4,22.9,11.1,27.4,5.2], + ], + }, + { + name: "Chad", + iso: "TD", + rings: [ + [23.8,19.6,23.9,15.6,21.9,12.6,22.9,11.1,15.3,7.4,13.5,14.4,15.9,20.4,14.9,22.9,23.8,19.6], + ], + }, + { + name: "Chile", + iso: "CL", + rings: [ + [-68.6,-52.6,-68.6,-54.9,-67,-54.9,-68.1,-55.6,-74.7,-52.8,-71.1,-54.1,-68.6,-52.6], + [-69.6,-17.6,-67,-23,-70.5,-31.4,-69.8,-34.2,-72.1,-42.3,-71.2,-44.8,-73.4,-49.3,-71.9,-52,-68.6,-52.3,-71.4,-53.9,-74.9,-52.3,-75.6,-48.7,-74.1,-46.9,-75.6,-46.6,-72.7,-42.4,-74.3,-43.2,-69.6,-17.6], + ], + }, + { + name: "China", + iso: "CN", + rings: [ + [109.5,18.2,108.6,19.4,110.8,20.1,109.5,18.2], + [80.3,42.3,80,44.9,87.8,49.3,91,46.9,90.9,45.3,96.3,42.7,109.2,42.5,111.9,45.1,119.7,46.7,115.5,48.1,122.2,53.4,125.9,52.8,131,47.8,135,48.5,133.1,45.1,131,45,130.6,42.4,121.1,38.9,121.6,40.9,117.5,38.7,122.4,37.5,119.2,34.9,121.9,31.7,121.7,28.2,118.7,24.5,110.4,20.3,105.3,23.4,101.7,22.3,101.8,21.2,99.2,22.1,97.6,23.9,98.7,27.5,96.1,29.5,88.8,27.3,78.7,31.5,78.9,34.3,73.7,39.4,80.3,42.3], + ], + }, + { + name: "Colombia", + iso: "CO", + rings: [ + [-66.9,1.3,-69.8,1.7,-69.9,-4.3,-70,-2.7,-77.4,0.4,-79,1.7,-77.1,3.8,-77.5,8.5,-71.4,12.4,-73.3,9.2,-72,7,-67.3,6.1,-66.9,1.3], + ], + }, + { + name: "Congo", + iso: "CG", + rings: [ + [18.5,3.5,16,-3.5,11.9,-5,11.5,-2.8,14.4,-1.3,13.1,2.3,15.9,1.7,18.5,3.5], + ], + }, + { + name: "Costa Rica", + iso: "CR", + rings: [ + [-82.5,9.6,-83,8.2,-85.9,10.9,-82.5,9.6], + ], + }, + { + name: "Côte d'Ivoire", + iso: "CI", + rings: [ + [-8,10.2,-2.8,9.6,-2.9,5,-7.7,4.4,-8,10.2], + ], + }, + { + name: "Croatia", + iso: "HR", + rings: [ + [16.6,46.5,19.4,45.2,15.8,44.8,18.5,42.5,13.7,45.1,16.6,46.5], + ], + }, + { + name: "Cuba", + iso: "CU", + rings: [ + [-82.3,23.2,-74.2,20.3,-77.8,19.9,-81.8,22.6,-85,21.9,-82.3,23.2], + ], + }, + { + name: "Cyprus", + iso: "CY", + rings: [ + [32.7,35.1,34,35,32.7,35.1], + ], + }, + { + name: "Czechia", + iso: "CZ", + rings: [ + [15,51.1,18.9,49.5,12.5,49.5,15,51.1], + ], + }, + { + name: "Dem. Rep. Congo", + iso: "CD", + rings: [ + [29.3,-4.5,30.7,-8.3,28.7,-8.5,28.4,-11.8,29.7,-13.3,22.2,-11.1,21.7,-7.3,17.5,-8.1,16.3,-5.9,12.2,-5.8,16,-3.5,19.5,5,29.7,4.6,31.2,2.2,29.3,-4.5], + ], + }, + { + name: "Denmark", + iso: "DK", + rings: [ + [9.9,55,8.1,56.5,10.6,57.7,9.9,55], + [12.4,56.1,12.1,54.8,11,55.4,12.4,56.1], + ], + }, + { + name: "Djibouti", + iso: "DJ", + rings: [ + [42.4,12.5,42.8,10.9,42.4,12.5], + ], + }, + { + name: "Dominican Rep.", + iso: "DO", + rings: [ + [-71.7,18,-71.6,19.9,-68.3,18.6,-71.7,18], + ], + }, + { + name: "Ecuador", + iso: "EC", + rings: [ + [-75.4,-0.2,-78.6,-4.5,-80.4,-4.4,-80.1,0.8,-75.4,-0.2], + ], + }, + { + name: "Egypt", + iso: "EG", + rings: [ + [36.9,22,25,22,25.2,31.6,34.3,31.2,34.2,27.8,32.3,29.8,36.9,22], + ], + }, + { + name: "El Salvador", + iso: "SV", + rings: [ + [-89.4,14.4,-87.9,13.1,-90.1,13.7,-89.4,14.4], + ], + }, + { + name: "Eq. Guinea", + iso: "GQ", + rings: [ + [9.6,2.3,11.3,1.1,9.5,1,9.6,2.3], + ], + }, + { + name: "Eritrea", + iso: "ER", + rings: [ + [36.4,14.4,38.4,18,43.1,12.7,36.4,14.4], + ], + }, + { + name: "Estonia", + iso: "EE", + rings: [ + [28,59.5,27.3,57.5,23.3,59.2,28,59.5], + ], + }, + { + name: "eSwatini", + iso: "SZ", + rings: [ + [32.1,-26.7,31,-25.7,32.1,-26.7], + ], + }, + { + name: "Ethiopia", + iso: "ET", + rings: [ + [47.8,8,45,5,39.6,3.4,36.2,4.4,33,7.8,37.9,15,41.6,13.5,43.7,9.2,47.8,8], + ], + }, + { + name: "Falkland Is.", + iso: "FK", + rings: [ + [-61.2,-51.8,-57.7,-51.5,-61.2,-51.8], + ], + }, + { + name: "Finland", + iso: "FI", + rings: [ + [28.6,69.1,31.1,62.4,28.1,60.5,21.3,60.7,21.5,63.2,25.4,65.1,20.6,69.1,24.7,68.6,27.7,70.2,28.6,69.1], + ], + }, + { + name: "Fr. S. Antarctic Lands", + iso: "TF", + rings: [ + [68.9,-48.6,70.6,-49.3,68.7,-49.8,68.9,-48.6], + ], + }, + { + name: "France", + iso: "FR", + rings: [ + [-51.7,4.2,-52.9,2.1,-54.5,2.3,-54,5.8,-51.7,4.2], + [6.2,49.5,8.1,49,6,46.7,7.4,43.7,1.8,42.3,-1.9,43.4,-1.2,46,-4.6,48.7,-1.6,48.6,-1.9,49.8,2.5,51.1,6.2,49.5], + [8.7,42.6,9.2,41.4,8.7,42.6], + ], + }, + { + name: "Gabon", + iso: "GA", + rings: [ + [11.3,2.3,14.3,1.2,14.4,-1.3,11.1,-4,8.8,-1.1,11.3,2.3], + ], + }, + { + name: "Gambia", + iso: "GM", + rings: [ + [-16.7,13.6,-13.8,13.5,-16.7,13.6], + ], + }, + { + name: "Georgia", + iso: "GE", + rings: [ + [40,43.4,46.6,41.2,41.6,41.5,40,43.4], + ], + }, + { + name: "Germany", + iso: "DE", + rings: [ + [14.1,53.8,15,51.1,12.2,50.3,12.9,47.5,7.5,47.6,8.1,49,6,50.1,7.1,53.7,9.9,55,14.1,53.8], + ], + }, + { + name: "Ghana", + iso: "GH", + rings: [ + [0,11,1.1,5.9,-2.9,5,-2.9,11,0,11], + ], + }, + { + name: "Greece", + iso: "GR", + rings: [ + [26.3,35.3,23.5,35.3,26.3,35.3], + [23,41.3,26.6,41.6,22.6,40.3,24,37.7,22.5,36.4,20.2,39.6,23,41.3], + ], + }, + { + name: "Greenland", + iso: "GL", + rings: [ + [-46.8,82.6,-27.1,83.5,-20.8,82.7,-31.9,82.2,-12.2,81.3,-20,80.2,-17.7,80.1,-19.7,78.8,-18.5,77,-21.7,76.6,-19.4,74.3,-24.8,72.3,-21.8,70.7,-25.5,71.4,-26.4,70.2,-22.3,70.1,-39.8,65.5,-43.4,60.1,-48.3,60.9,-51.6,63.6,-54,67.2,-50.9,69.9,-54.7,69.6,-54.4,70.8,-51.4,70.6,-55.8,71.7,-54.7,72.6,-58.6,75.5,-68.5,76.1,-71.4,77,-66.8,77.4,-73.3,78,-65.7,79.4,-68,80.1,-62.7,81.8,-44.5,81.7,-46.8,82.6], + ], + }, + { + name: "Guatemala", + iso: "GT", + rings: [ + [-92.2,14.5,-90.5,16.1,-91,17.8,-89.1,17.8,-88.2,15.7,-89.4,14.4,-92.2,14.5], + ], + }, + { + name: "Guinea", + iso: "GN", + rings: [ + [-13.7,12.6,-9.1,12.3,-8.3,7.7,-11.1,10,-13.2,8.9,-15.1,11,-13.7,12.6], + ], + }, + { + name: "Guinea-Bissau", + iso: "GW", + rings: [ + [-16.7,12.4,-13.7,11.8,-15.1,11,-16.7,12.4], + ], + }, + { + name: "Guyana", + iso: "GY", + rings: [ + [-56.5,1.9,-59.6,1.8,-61.4,6,-59.8,8.4,-57.1,6,-58,4.1,-56.5,1.9], + ], + }, + { + name: "Haiti", + iso: "HT", + rings: [ + [-71.7,19.7,-71.7,18,-74.5,18.3,-71.7,19.7], + ], + }, + { + name: "Honduras", + iso: "HN", + rings: [ + [-83.1,15,-87.3,13,-89.4,14.4,-87.9,15.9,-83.1,15], + ], + }, + { + name: "Hungary", + iso: "HU", + rings: [ + [22.1,48.4,21,46.3,16.2,46.9,17,48.1,22.1,48.4], + ], + }, + { + name: "Iceland", + iso: "IS", + rings: [ + [-14.5,66.5,-13.6,65.1,-18.7,63.5,-22.8,64,-21.8,64.4,-24,64.9,-22.2,65.4,-24.3,65.6,-14.5,66.5], + ], + }, + { + name: "India", + iso: "IN", + rings: [ + [97.3,28.3,92.7,22,91.2,23.5,92.4,25,88.6,26.4,88.9,21.7,80.3,15.9,79.9,10.4,77.5,8,72.6,21.4,70.5,20.9,68.2,23.7,71,24.4,69.5,26.9,75.3,32.3,73.7,34.3,77.8,35.5,78.7,31.5,81.1,30.2,80.1,28.8,83.3,27.4,88.1,26.4,88.7,28.1,92,26.8,96.1,29.5,97.3,28.3], + ], + }, + { + name: "Indonesia", + iso: "ID", + rings: [ + [141,-2.6,141,-9.1,137.6,-8.4,137.9,-5.4,133,-4.1,132,-2.8,133.7,-2.2,130.5,-0.9,134,-0.8,135.5,-3.4,137.4,-1.7,141,-2.6], + [125,-8.9,123.5,-10.2,125,-8.9], + [117.9,4.1,119,0.9,116.1,-4,110.2,-2.9,109.7,2,110.5,0.8,113.8,1.2,115.9,4.3,117.9,4.1], + [129.4,-2.8,130.8,-3.9,127.9,-3.4,129.4,-2.8], + [127.9,2.2,128.1,-0.9,127.9,2.2], + [122.9,0.9,125.2,1.4,120,-0.5,123.3,-0.6,121.5,-1.9,123.2,-5.3,121,-2.6,119.8,-5.7,118.8,-2.8,119.8,0.2,122.9,0.9], + [120.3,-10.3,119,-9.6,120.3,-10.3], + [121.3,-8.5,122.9,-8.1,119.9,-8.8,121.3,-8.5], + [118.3,-8.4,116.7,-9,118.3,-8.4], + [108.5,-6.4,115.7,-8.4,105.4,-6.9,108.5,-6.4], + [104.4,-1.1,106.1,-3.1,105.8,-5.9,102.6,-4.2,95.3,5.5,97.5,5.2,104.4,-1.1], + ], + }, + { + name: "Iran", + iso: "IR", + rings: [ + [48.6,29.9,45.4,34,46.1,35.7,44.1,39.4,48.1,39.6,50.8,36.9,56.6,38.1,61.1,36.5,60.9,29.8,63.3,26.8,61.5,25.1,57.4,25.7,48.6,29.9], + ], + }, + { + name: "Iraq", + iso: "IQ", + rings: [ + [39.2,32.2,41.3,36.4,44.8,37.2,48.6,29.9,44.7,29.2,39.2,32.2], + ], + }, + { + name: "Ireland", + iso: "IE", + rings: [ + [-6.2,53.9,-6.8,52.3,-10,51.8,-7.6,55.1,-6.2,53.9], + ], + }, + { + name: "Israel", + iso: "IL", + rings: [ + [35.7,32.7,34.9,29.5,34.3,31.2,35.7,32.7], + ], + }, + { + name: "Italy", + iso: "IT", + rings: [ + [10.4,46.9,13.8,46.5,12.6,44.1,18.3,39.8,16.9,40.4,15.7,37.9,15.4,40,10.2,43.9,7.4,43.7,6.8,46,10.4,46.9], + [14.8,38.1,15.1,36.6,12.4,37.6,14.8,38.1], + [8.7,40.9,9.8,40.5,8.8,38.9,8.7,40.9], + ], + }, + { + name: "Jamaica", + iso: "JM", + rings: [ + [-77.6,18.5,-76.2,17.9,-77.6,18.5], + ], + }, + { + name: "Japan", + iso: "JP", + rings: [ + [141.9,39.2,140.3,35.1,135.8,33.5,135.1,34.6,131,33.9,132,33.1,130.2,31.4,129.4,33.3,139.4,38.2,140.3,41.2,141.9,39.2], + [144.6,44,145.5,43.3,140,41.6,142,45.6,144.6,44], + [132.4,33.5,134.8,33.8,132.4,33.5], + ], + }, + { + name: "Jordan", + iso: "JO", + rings: [ + [35.5,32.4,38.8,33.4,39.2,32.2,37,31.5,38,30.5,36.1,29.2,34.9,29.5,35.5,32.4], + ], + }, + { + name: "Kazakhstan", + iso: "KZ", + rings: [ + [87.4,49.2,80,44.9,80.3,42.3,74.2,43.3,68.6,40.7,64.9,43.7,62,43.5,58.5,45.6,55.9,45,56,41.3,52.5,41.8,50.3,44.6,53,45.3,53,46.9,49.1,46.4,46.5,48.4,50.8,51.7,61.3,50.8,60,52,61.4,54,69.1,55.4,73.4,53.5,76.9,54.5,80,50.9,87.4,49.2], + ], + }, + { + name: "Kenya", + iso: "KE", + rings: [ + [39.2,-4.7,33.9,-0.9,35.3,5.5,38.1,3.6,41.9,3.9,41.6,-1.7,39.2,-4.7], + ], + }, + { + name: "Kosovo", + iso: "XK", + rings: [ + [20.6,41.9,20.6,43.2,21.8,42.7,20.6,41.9], + ], + }, + { + name: "Kuwait", + iso: "KW", + rings: [ + [48,30,48.4,28.6,46.6,29.1,48,30], + ], + }, + { + name: "Kyrgyzstan", + iso: "KG", + rings: [ + [71,42.3,74.2,43.3,80.3,42.3,73.7,39.4,69.5,39.5,73.1,40.9,71,42.3], + ], + }, + { + name: "Laos", + iso: "LA", + rings: [ + [107.4,14.2,105.2,14.3,104,18.2,101.1,17.5,100.1,20.4,101.7,22.3,104.4,20.8,103.9,19.3,107.4,14.2], + ], + }, + { + name: "Latvia", + iso: "LV", + rings: [ + [27.3,57.5,28.2,56.2,26.5,55.6,21.1,56,22.5,57.8,27.3,57.5], + ], + }, + { + name: "Lebanon", + iso: "LB", + rings: [ + [35.8,33.3,36.4,34.6,35.8,33.3], + ], + }, + { + name: "Lesotho", + iso: "LS", + rings: [ + [29,-29,28.1,-30.5,27,-29.9,29,-29], + ], + }, + { + name: "Liberia", + iso: "LR", + rings: [ + [-8.4,7.7,-7.7,4.4,-11.4,6.8,-10.2,8.4,-8.4,7.7], + ], + }, + { + name: "Libya", + iso: "LY", + rings: [ + [25,22,23.8,19.6,10.3,24.4,10,31.4,11.5,33.1,19.1,30.3,20.9,32.7,24.9,31.9,25,22], + ], + }, + { + name: "Lithuania", + iso: "LT", + rings: [ + [26.5,55.6,23.5,53.9,21.1,56,26.5,55.6], + ], + }, + { + name: "Madagascar", + iso: "MG", + rings: [ + [49.5,-12.5,50.4,-15.7,47.1,-24.9,45.4,-25.6,43.3,-22.8,44,-17.4,49.5,-12.5], + ], + }, + { + name: "Malawi", + iso: "MW", + rings: [ + [32.8,-9.2,35.7,-14.6,35,-16.8,32.7,-13.7,32.8,-9.2], + ], + }, + { + name: "Malaysia", + iso: "MY", + rings: [ + [100.1,6.5,103,5.5,104.2,1.3,101.4,2.8,100.1,6.5], + [117.9,4.1,115.9,4.3,114.6,1.4,109.8,1.3,115.3,4.3,116.7,6.9,119.2,5.4,117.9,4.1], + ], + }, + { + name: "Mali", + iso: "ML", + rings: [ + [-11.5,12.4,-11.7,15.4,-5.5,15.5,-6.5,25,4.3,19.2,3.6,15.6,-4,13.5,-5.4,10.4,-11.5,12.4], + ], + }, + { + name: "Mauritania", + iso: "MR", + rings: [ + [-17.1,21,-12.9,21.3,-12,25.9,-8.7,25.9,-8.7,27.4,-4.9,25,-6.5,25,-5.5,15.5,-12.2,14.6,-14.6,16.6,-16.5,16.1,-17.1,21], + ], + }, + { + name: "Mexico", + iso: "MX", + rings: [ + [-117.1,32.5,-106.5,31.8,-103.9,29.3,-101.7,29.8,-97.1,25.9,-97.9,22.4,-95.9,18.8,-91.4,18.9,-90.3,21,-87.1,21.5,-87.8,18.3,-91,17.8,-90.5,16.1,-92.2,14.5,-103.5,18.3,-113.1,31.2,-114.9,31.4,-109.9,22.8,-115.1,27.7,-114.2,28.6,-117.1,32.5], + ], + }, + { + name: "Moldova", + iso: "MD", + rings: [ + [26.6,48.2,30,46.4,28.2,45.5,26.6,48.2], + ], + }, + { + name: "Mongolia", + iso: "MN", + rings: [ + [87.8,49.3,92.2,50.8,97.3,49.7,98.9,52,108.5,49.3,116.7,49.9,115.7,47.7,119.8,47,105,41.6,96.3,42.7,90.9,45.3,91,46.9,87.8,49.3], + ], + }, + { + name: "Montenegro", + iso: "ME", + rings: [ + [20.1,42.6,18.5,42.5,20.1,42.6], + ], + }, + { + name: "Morocco", + iso: "MA", + rings: [ + [-2.2,35.2,-1.3,32.3,-8.7,28.8,-8.8,27.1,-11.4,26.9,-14.8,21.5,-17,21.4,-14.4,26.3,-9.6,29.9,-8.7,33.2,-5.9,35.8,-2.2,35.2], + ], + }, + { + name: "Mozambique", + iso: "MZ", + rings: [ + [34.6,-11.5,40.3,-10.3,40.8,-14.7,34.8,-19.8,35.5,-24.1,32.1,-26.7,31.2,-22.3,32.8,-16.7,30.2,-14.8,33.2,-14,35,-16.8,34.6,-11.5], + ], + }, + { + name: "Myanmar", + iso: "MM", + rings: [ + [100.1,20.4,97.4,18.4,99.6,11.9,98.6,9.9,97.2,16.9,94.2,16,92.3,21.5,97.3,28.3,98.7,27.5,97.6,23.9,101.2,21.8,100.1,20.4], + ], + }, + { + name: "N. Cyprus", + iso: "-99", + rings: [ + [32.7,35.1,34.6,35.7,32.7,35.1], + ], + }, + { + name: "Namibia", + iso: "NA", + rings: [ + [19.9,-24.8,19.9,-28.5,16.3,-28.6,11.7,-17.3,25.1,-17.6,20.9,-18.3,19.9,-24.8], + ], + }, + { + name: "Nepal", + iso: "NP", + rings: [ + [88.1,27.9,87.2,26.4,80.1,28.8,81.5,30.4,88.1,27.9], + ], + }, + { + name: "Netherlands", + iso: "NL", + rings: [ + [6.9,53.5,6.2,50.8,3.3,51.3,6.9,53.5], + ], + }, + { + name: "New Caledonia", + iso: "NC", + rings: [ + [165.8,-21.1,167.1,-22.2,164,-20.1,165.8,-21.1], + ], + }, + { + name: "New Zealand", + iso: "NZ", + rings: [ + [176.9,-40.1,174.7,-41.3,174.7,-37.4,172.6,-34.5,176,-37.6,178.5,-37.7,176.9,-40.1], + [169.7,-43.6,172.8,-40.5,174.2,-41.3,170.6,-45.9,166.7,-46.2,169.7,-43.6], + ], + }, + { + name: "Nicaragua", + iso: "NI", + rings: [ + [-83.7,10.9,-87.7,12.9,-83.1,15,-83.7,10.9], + ], + }, + { + name: "Niger", + iso: "NE", + rings: [ + [14.9,22.9,15.9,20.4,13.5,14.4,14.2,12.5,5.4,13.9,3.6,11.7,1,12.9,0.4,14.9,3.6,15.6,4.3,19.2,12,23.5,14.9,22.9], + ], + }, + { + name: "Nigeria", + iso: "NG", + rings: [ + [2.7,6.3,4.4,13.7,13.1,13.6,14.6,12.1,11.7,7,8.5,4.8,5.9,4.3,2.7,6.3], + ], + }, + { + name: "North Korea", + iso: "KP", + rings: [ + [130.6,42.4,127.5,39.8,128.2,38.4,124.7,38.1,125.1,40.6,130.6,42.4], + ], + }, + { + name: "North Macedonia", + iso: "MK", + rings: [ + [22.4,42.3,23,41.3,20.6,41.1,22.4,42.3], + ], + }, + { + name: "Norway", + iso: "NO", + rings: [ + [15.1,79.7,21.5,79,15.9,76.8,10.4,79.7,15.1,79.7], + [31.1,69.6,18,68.6,12.6,64.1,11,58.9,5.7,58.6,5,62,19.2,69.8,28.2,71.2,31.1,69.6], + [27.4,80.1,17.4,80.3,27.4,80.1], + [24.7,77.9,20.7,77.7,24.7,77.9], + ], + }, + { + name: "Oman", + iso: "OM", + rings: [ + [55.2,22.7,56.4,24.9,59.8,22.3,57.7,18.9,53.1,16.7,52,19,55,20,55.2,22.7], + ], + }, + { + name: "Pakistan", + iso: "PK", + rings: [ + [77.8,35.5,73.7,34.3,75.3,32.3,69.5,26.9,71,24.4,61.5,25.1,63.3,26.8,60.9,29.8,66.3,29.9,71.8,36.5,75.2,37.1,77.8,35.5], + ], + }, + { + name: "Panama", + iso: "PA", + rings: [ + [-77.4,8.7,-77.9,7.2,-79.1,9,-80.9,7.2,-82.9,9.5,-77.4,8.7], + ], + }, + { + name: "Papua New Guinea", + iso: "PG", + rings: [ + [141,-2.6,147.6,-6.1,147.2,-7.4,150.7,-10.6,144.7,-7.6,141,-9.1,141,-2.6], + [152.6,-3.7,152.8,-4.8,150.7,-2.7,152.6,-3.7], + [151.3,-5.8,148.3,-5.7,152.1,-4.1,151.3,-5.8], + [154.8,-5.3,155.9,-6.8,154.8,-5.3], + ], + }, + { + name: "Paraguay", + iso: "PY", + rings: [ + [-58.2,-20.2,-57.9,-22.1,-54.3,-24,-55.7,-27.4,-58.6,-27.1,-57.8,-25.2,-62.7,-22.2,-61.8,-19.6,-58.2,-20.2], + ], + }, + { + name: "Peru", + iso: "PE", + rings: [ + [-69.9,-4.3,-72.9,-5.3,-74,-7.5,-68.7,-12.6,-70.4,-18.3,-76,-14.6,-81.4,-4.7,-80.3,-3.4,-78.6,-4.5,-75.1,-0.1,-73.1,-2.3,-70,-2.7,-69.9,-4.3], + ], + }, + { + name: "Philippines", + iso: "PH", + rings: [ + [122.6,10,124.1,11.2,123,9,122.6,10], + [126.4,8.4,125.4,5.6,123.6,7.8,121.9,7.2,125.4,9.8,126.4,8.4], + [118.5,9.3,117.2,8.4,119.5,11.4,118.5,9.3], + [122.3,18.2,121.7,14.3,124.1,12.5,119.9,15.4,120.7,18.5,122.3,18.2], + [125.5,12.2,124.8,10.1,124.3,12.6,125.5,12.2], + ], + }, + { + name: "Poland", + iso: "PL", + rings: [ + [23.5,53.9,24,50.7,22.8,49,16.2,50.4,14.1,53,17.6,54.9,23.5,53.9], + ], + }, + { + name: "Portugal", + iso: "PT", + rings: [ + [-9,41.9,-6.4,41.4,-7.9,36.8,-9.5,38.7,-9,41.9], + ], + }, + { + name: "Puerto Rico", + iso: "PR", + rings: [ + [-66.3,18.5,-67.2,17.9,-66.3,18.5], + ], + }, + { + name: "Qatar", + iso: "QA", + rings: [ + [50.8,24.8,51.3,26.1,50.8,24.8], + ], + }, + { + name: "Romania", + iso: "RO", + rings: [ + [28.2,45.5,29.6,45.3,28.6,43.7,22.9,43.8,20.2,46.1,26.6,48.2,28.2,45.5], + ], + }, + { + name: "Russia", + iso: "RU", + rings: [ + [49.1,46.4,46.7,44.6,47.8,41.2,36.7,45.2,40.1,49.6,31.8,52.1,32.7,53.4,30.9,55.6,27.3,57.5,29.1,60,28.1,60.5,31.5,62.9,30,63.6,28.6,69.1,32.1,69.9,41.1,67.5,38.4,66,33.2,66.6,37,63.8,37.2,65.1,43.9,66.1,43.5,68.6,46.3,68.3,46.3,66.7,53.7,68.9,59.9,68.3,60.6,69.9,68.5,68.1,66.7,71,69.9,73,72.8,72.2,71.8,71.4,73.7,68.4,71.3,66.3,72.4,66.2,75.1,67.8,73.1,71.4,74.7,72.8,76.4,71.2,81.5,71.8,80.5,73.6,104.4,77.7,114.1,75.8,109.4,74.2,127,73.6,131.3,70.8,139.9,71.5,139.1,72.4,140.5,72.8,159,70.9,160.9,69.4,180,69,180,65,177.4,64.6,179.2,62.3,170.3,59.9,163.5,59.9,162,58.2,163.2,57.6,162.1,54.9,156.8,51,155.9,56.8,164.5,62.6,160.1,60.5,156.7,61.4,154.2,59.8,155,59.1,142.2,59,135.1,54.7,141.3,53.1,140.1,48.4,134.9,43.4,130.8,42.2,131,45,133.1,45.1,135,48.5,131,47.8,123.6,53.5,120.2,52.8,117.9,49.5,108.5,49.3,98.9,52,97.3,49.7,92.2,50.8,87.4,49.2,80,50.9,76.9,54.5,73.4,53.5,69.1,55.4,61.4,54,60,52,61.3,50.8,50.8,51.7,47.5,50.5,46.5,48.4,49.1,46.4], + [93.8,81,100.2,79.8,97.8,78.8,91.2,80.3,93.8,81], + [102.8,79.3,105.4,78.7,99.4,77.9,102.8,79.3], + [138.8,76.1,145.1,75.6,137,75.3,138.8,76.1], + [148.2,75.3,150.7,75.1,146.1,75.2,148.2,75.3], + [139.9,73.4,143.6,73.2,139.9,73.4], + [44.8,80.6,51.5,80.7,44.8,80.6], + [22.7,54.3,19.7,54.4,22.7,54.3], + [53.5,73.7,61.2,76.3,68.9,76.5,58.5,74.3,55.4,72.4,57.5,70.7,51.6,71.5,53.5,73.7], + [142.9,53.7,144.7,49,143.2,49.3,143.5,46.1,142.1,46,141.7,53.3,142.9,53.7], + [-174.9,67.2,-169.9,66,-173,64.3,-178.7,66.1,-180,65,-180,69,-174.9,67.2], + [-178.7,70.9,-180,71.5,-177.6,71.3,-178.7,70.9], + [33.4,46,36.5,45.5,33.9,44.4,32.5,45.3,33.4,46], + ], + }, + { + name: "Rwanda", + iso: "RW", + rings: [ + [30.4,-1.1,29,-2.8,30.4,-1.1], + ], + }, + { + name: "S. Sudan", + iso: "SS", + rings: [ + [30.8,3.5,23.9,8.6,25.8,10.4,31.4,9.8,33.2,12.2,33,7.8,35.3,5.5,30.8,3.5], + ], + }, + { + name: "Saudi Arabia", + iso: "SA", + rings: [ + [35,29.4,39.2,32.2,47.5,29,52,23,55.2,22.7,55,20,47,16.9,43.4,17.6,42.8,16.3,35,29.4], + ], + }, + { + name: "Senegal", + iso: "SN", + rings: [ + [-16.7,13.6,-17.6,14.7,-14.6,16.6,-11.5,12.4,-16.7,12.4,-13.8,13.5,-16.7,13.6], + ], + }, + { + name: "Serbia", + iso: "RS", + rings: [ + [18.8,45.9,22.7,44.6,22.5,42.5,19.2,43.5,18.8,45.9], + ], + }, + { + name: "Sierra Leone", + iso: "SL", + rings: [ + [-13.2,8.9,-11.1,10,-10.2,8.4,-11.4,6.8,-13.2,8.9], + ], + }, + { + name: "Slovakia", + iso: "SK", + rings: [ + [22.6,49.1,16.9,48.5,22.6,49.1], + ], + }, + { + name: "Slovenia", + iso: "SI", + rings: [ + [13.8,46.5,16.6,46.5,15.3,45.5,13.8,46.5], + ], + }, + { + name: "Solomon Is.", + iso: "SB", + rings: [ + [159.6,-8,158.2,-7.4,159.6,-8], + ], + }, + { + name: "Somalia", + iso: "SO", + rings: [ + [41.6,-1.7,41,2.8,45,5,48.9,9.5,48.9,11.4,51.1,12,48.6,5.3,41.6,-1.7], + ], + }, + { + name: "Somaliland", + iso: "-99", + rings: [ + [48.9,11.4,47.8,8,42.6,10.6,48.9,11.4], + ], + }, + { + name: "South Africa", + iso: "ZA", + rings: [ + [16.3,-28.6,19.9,-28.5,19.9,-24.8,21.6,-26.7,25.7,-25.5,29.4,-22.1,31.2,-22.3,31.9,-24.4,30.7,-26.7,32.8,-26.7,28.2,-32.8,20.1,-34.8,18.4,-34.1,16.3,-28.6], + ], + }, + { + name: "South Korea", + iso: "KR", + rings: [ + [126.2,37.7,128.3,38.6,129.1,35.1,126.5,34.4,126.2,37.7], + ], + }, + { + name: "Spain", + iso: "ES", + rings: [ + [-7.5,37.1,-6.4,41.4,-9.4,43,3,42.5,-2.1,36.7,-7.5,37.1], + ], + }, + { + name: "Sri Lanka", + iso: "LK", + rings: [ + [81.8,7.5,80.3,6,80.1,9.8,81.8,7.5], + ], + }, + { + name: "Sudan", + iso: "SD", + rings: [ + [24.6,8.2,21.9,12.6,25,22,36.9,22,38.4,18,34,8.7,32.7,12.2,31.4,9.8,25.1,10.3,24.6,8.2], + ], + }, + { + name: "Suriname", + iso: "SR", + rings: [ + [-54.5,2.3,-56.5,1.9,-57.6,3.3,-57.1,6,-54,5.8,-54.5,2.3], + ], + }, + { + name: "Sweden", + iso: "SE", + rings: [ + [11,58.9,12.6,61.3,11.9,63.1,16.8,68,20.6,69.1,23.5,67.9,23.9,66,17.8,62.7,17.1,61.3,18.8,60.1,15.9,56.1,12.9,55.4,11,58.9], + ], + }, + { + name: "Switzerland", + iso: "CH", + rings: [ + [9.6,47.5,10.4,46.5,6,46.3,9.6,47.5], + ], + }, + { + name: "Syria", + iso: "SY", + rings: [ + [35.7,32.7,36.7,36.8,42.3,37.2,41,34.4,35.7,32.7], + ], + }, + { + name: "Taiwan", + iso: "TW", + rings: [ + [121.8,24.4,120.7,22,120.1,23.6,121.8,24.4], + ], + }, + { + name: "Tajikistan", + iso: "TJ", + rings: [ + [67.8,37.1,67.7,39.6,70.7,41,69.5,39.5,73.7,39.4,75,37.4,71.8,36.7,70.8,38.5,67.8,37.1], + ], + }, + { + name: "Tanzania", + iso: "TZ", + rings: [ + [33.9,-0.9,39.2,-4.7,39.5,-10.9,34.6,-11.5,29.6,-6.5,30.4,-1.1,33.9,-0.9], + ], + }, + { + name: "Thailand", + iso: "TH", + rings: [ + [105.2,14.3,103,14.2,102.6,12.2,100.1,13.4,99.2,9.2,102.1,6.2,101.2,5.7,98.2,8.4,99.6,11.9,97.4,18.4,100.1,20.4,101.1,17.5,104.7,17.4,105.2,14.3], + ], + }, + { + name: "Timor-Leste", + iso: "TL", + rings: [ + [125,-8.9,127.3,-8.4,125,-8.9], + ], + }, + { + name: "Togo", + iso: "TG", + rings: [ + [0.9,11,1.1,5.9,0.9,11], + ], + }, + { + name: "Tunisia", + iso: "TN", + rings: [ + [9.5,30.3,7.5,34.1,9.5,37.3,11,37.1,10.1,34.3,11.5,33.1,9.5,30.3], + ], + }, + { + name: "Turkey", + iso: "TR", + rings: [ + [44.8,37.2,29.7,36.1,27.6,36.7,26.2,39.5,33.5,42,42.6,41.6,44.8,39.7,44.8,37.2], + [26.1,41.8,29,41.3,26.4,40.2,26.1,41.8], + ], + }, + { + name: "Turkmenistan", + iso: "TM", + rings: [ + [52.5,41.8,57.1,41.3,58.6,42.8,66.5,37.4,62.2,35.3,57.3,38,53.9,37.2,52.7,40,54.7,41,52.5,41.8], + ], + }, + { + name: "Uganda", + iso: "UG", + rings: [ + [33.9,-0.9,29.6,-1.3,31.2,3.8,34.5,3.6,33.9,-0.9], + ], + }, + { + name: "Ukraine", + iso: "UA", + rings: [ + [31.8,52.1,40.1,49.6,39.7,47.9,35,45.7,31.7,46.7,28.7,45.3,30,46.4,28.7,48.1,22.1,48.4,23.5,51.6,31.8,52.1], + ], + }, + { + name: "United Arab Emirates", + iso: "AE", + rings: [ + [51.6,24.2,56.3,25.7,55,22.5,51.6,24.2], + ], + }, + { + name: "United Kingdom", + iso: "GB", + rings: [ + [-6.2,53.9,-7.6,55.1,-6.2,53.9], + [-3.1,53.4,-6.1,56.8,-5,58.6,-2,57.7,-3.1,56,1.7,52.7,1.4,51.3,-5.8,50.2,-3.4,51.4,-5.3,52,-4.6,53.5,-3.1,53.4], + ], + }, + { + name: "United States of America", + iso: "US", + rings: [ + [-122.8,49,-88.4,48.3,-82.6,45.3,-82.7,41.7,-71.5,45,-69.2,47.4,-67,44.8,-70.1,43.7,-70,41.6,-75.5,39.5,-75.9,37.2,-76.3,39.2,-77,38.2,-75.7,35.6,-81.3,31.4,-80.4,25.2,-83.7,29.9,-86.4,30.4,-94.7,29.5,-97.5,25.8,-101,29.4,-103.9,29.3,-106.5,31.8,-117.1,32.5,-120.6,34.6,-124.4,40.3,-124.7,48.2,-122.6,47.1,-122.8,49], + [-166.5,60.4,-165.6,59.9,-167.5,60.2,-166.5,60.4], + [-153.2,58,-152.1,57.6,-154.5,57,-153.2,58], + [-141,69.7,-141,60.3,-137.5,58.9,-135.5,59.8,-130,55.9,-130.5,54.8,-134.1,58.1,-139.9,59.5,-147.1,60.9,-151.7,59.2,-150.6,61.3,-158.4,56,-164.9,54.6,-157,58.9,-162,58.7,-165.3,60.5,-165.7,62.1,-160.8,64.8,-168.1,65.7,-161.7,66.1,-166.2,68.9,-156.6,71.4,-141,69.7], + [-171.7,63.8,-168.7,63.3,-171.7,63.8], + ], + }, + { + name: "Uruguay", + iso: "UY", + rings: [ + [-57.6,-30.2,-53.8,-32,-53.8,-34.4,-58.4,-33.9,-57.6,-30.2], + ], + }, + { + name: "Uzbekistan", + iso: "UZ", + rings: [ + [56,41.3,55.9,45,58.5,45.6,62,43.5,64.9,43.7,68.3,40.7,71,42.3,73.1,40.9,67.7,39.6,67.8,37.1,58.6,42.8,56,41.3], + ], + }, + { + name: "Venezuela", + iso: "VE", + rings: [ + [-60.7,5.2,-64.8,4.1,-63.4,2.2,-66.3,0.7,-67.8,2.8,-67.3,6.1,-72,7,-72.9,10.5,-71.3,11.8,-71.3,9.1,-69.9,12.2,-68.2,10.6,-61.9,10.7,-59.8,8.4,-60.7,5.2], + ], + }, + { + name: "Vietnam", + iso: "VN", + rings: [ + [104.3,10.5,107.5,12.3,107.6,15.2,102.2,22.5,105.3,23.4,108.1,21.6,105.7,19.1,108.9,15.3,109.2,11.7,105.2,8.6,104.3,10.5], + ], + }, + { + name: "W. Sahara", + iso: "EH", + rings: [ + [-8.7,27.7,-8.7,25.9,-12,25.9,-12.9,21.3,-17.1,21,-14.8,21.5,-11.4,26.9,-8.7,27.7], + ], + }, + { + name: "Yemen", + iso: "YE", + rings: [ + [52,19,52.2,15.6,43.5,12.6,43.4,17.6,47,16.9,52,19], + ], + }, + { + name: "Zambia", + iso: "ZM", + rings: [ + [30.7,-8.3,33.2,-9.7,33.2,-14,27,-17.9,23.2,-17.5,21.9,-12.9,24,-12.9,23.9,-10.9,29.7,-13.3,28.4,-9.2,30.7,-8.3], + ], + }, + { + name: "Zimbabwe", + iso: "ZW", + rings: [ + [31.2,-22.3,28,-21.5,25.3,-17.7,30.3,-15.5,32.8,-16.7,31.2,-22.3], + ], + }, +]; diff --git a/packages/hqtui/src/graphics/world.ts b/packages/hqtui/src/graphics/world.ts new file mode 100644 index 0000000..ec1faca --- /dev/null +++ b/packages/hqtui/src/graphics/world.ts @@ -0,0 +1,171 @@ +/** + * The world, as shapes for the canvas, and the lookup that makes it clickable. + * + * The canvas already draws in the caller's own coordinates, and longitude and + * latitude are just another pair of axes -- so a map is a list of polylines in + * degrees, and nothing here needs a projection of its own beyond deciding which + * window on the globe to show. + * + * The interesting half is the other direction. A click arrives as a terminal + * cell, and a country is a polygon, so answering "what did they click" means + * turning the cell back into degrees and testing it against the outlines. Doing + * it that way rather than with bounding boxes is what makes the answer right: + * Russia's bounding box covers most of the northern hemisphere, and Chile's + * covers Argentina. + */ +import type { Color } from "../color.ts"; +import type { Bounds, Shape } from "./canvas.ts"; +import { type CountryOutline, WORLD_COUNTRIES } from "./world-data.ts"; + +export { type CountryOutline, WORLD_COUNTRIES }; + +/** The whole globe, which is what a map shows unless told otherwise. */ +export const WORLD_X: Bounds = { min: -180, max: 180 }; +export const WORLD_Y: Bounds = { min: -90, max: 90 }; + +export interface WorldShapeOptions { + /** Colour for countries with nothing special about them. */ + color?: Color; + /** Countries to pick out, by name or ISO code. */ + highlight?: readonly string[]; + highlightColor?: Color; +} + +/** Match on either the name or the ISO code, case-insensitively. */ +function matches(country: CountryOutline, keys: readonly string[]): boolean { + for (const key of keys) { + if (key.length === 0) continue; + if (country.name.toLowerCase() === key.toLowerCase()) return true; + if (country.iso.length > 0 && country.iso.toLowerCase() === key.toLowerCase()) return true; + } + return false; +} + +/** + * The world as canvas shapes, one polyline per landmass. + * + * Polylines rather than scattered points: the outlines are closed rings, so + * joining them draws a coastline instead of a dotted suggestion of one, and it + * reads at a fraction of the resolution dots would need. + */ +export function worldShapes(options: WorldShapeOptions = {}): Shape[] { + const shapes: Shape[] = []; + const highlight = options.highlight ?? []; + for (const country of WORLD_COUNTRIES) { + const picked = highlight.length > 0 && matches(country, highlight); + const color = picked ? options.highlightColor ?? options.color : options.color; + for (const ring of country.rings) { + const points: [number, number][] = []; + for (let i = 0; i + 1 < ring.length; i += 2) points.push([ring[i], ring[i + 1]]); + // Closed: the last point joins the first, or every country has a gap in + // its coastline where the ring started. + if (points.length > 0) points.push(points[0]); + shapes.push({ type: "polyline", points, color }); + } + } + return shapes; +} + +/** + * Whether a point is inside a ring, by ray casting. + * + * The ring is a flat list of interleaved coordinates, so this walks it two at a + * time rather than allocating a pair per vertex -- it runs once per country per + * click, and there are a couple of thousand vertices. + */ +function insideRing(ring: number[], lon: number, lat: number): boolean { + let inside = false; + const n = ring.length / 2; + for (let i = 0, j = n - 1; i < n; j = i++) { + const xi = ring[i * 2]; + const yi = ring[i * 2 + 1]; + const xj = ring[j * 2]; + const yj = ring[j * 2 + 1]; + if ((yi > lat) !== (yj > lat) && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) { + inside = !inside; + } + } + return inside; +} + +/** + * The country containing a point, or undefined for open water. + * + * Where outlines overlap -- and at this resolution simplified borders do + * overlap -- the first match wins, which is stable because the data is sorted + * by name. + */ +export function countryAt(lon: number, lat: number): CountryOutline | undefined { + if (!Number.isFinite(lon) || !Number.isFinite(lat)) return undefined; + for (const country of WORLD_COUNTRIES) { + for (const ring of country.rings) { + if (insideRing(ring, lon, lat)) return country; + } + } + return undefined; +} + +/** Look a country up by name or ISO code. */ +export function findCountry(key: string): CountryOutline | undefined { + return WORLD_COUNTRIES.find((c) => matches(c, [key])); +} + +/** + * The window a country fills, with a little room around it. + * + * For zooming a map to a country: the bounding box alone puts the coastline + * flat against the edge of the panel, which reads as though the country has + * been cut off rather than framed. + */ +export function countryBounds( + country: CountryOutline, + margin = 0.08, +): { x: Bounds; y: Bounds } { + let minLon = Infinity; + let maxLon = -Infinity; + let minLat = Infinity; + let maxLat = -Infinity; + for (const ring of country.rings) { + for (let i = 0; i + 1 < ring.length; i += 2) { + minLon = Math.min(minLon, ring[i]); + maxLon = Math.max(maxLon, ring[i]); + minLat = Math.min(minLat, ring[i + 1]); + maxLat = Math.max(maxLat, ring[i + 1]); + } + } + if (!Number.isFinite(minLon)) return { x: WORLD_X, y: WORLD_Y }; + // A single-point country would give a zero-width window, which cannot be + // mapped onto anything. + const padX = Math.max((maxLon - minLon) * margin, 1); + const padY = Math.max((maxLat - minLat) * margin, 1); + return { + x: { min: minLon - padX, max: maxLon + padX }, + y: { min: minLat - padY, max: maxLat + padY }, + }; +} + +/** + * The degrees under a terminal cell, given the window the map was drawn with. + * + * The inverse of what the canvas does on the way in, taken at the centre of the + * cell: a click lands on a whole cell, and the centre is the only point in it + * that is not arbitrarily nearer one neighbour than the other. + */ +export function degreesAt( + column: number, + row: number, + width: number, + height: number, + x: Bounds = WORLD_X, + y: Bounds = WORLD_Y, +): { lon: number; lat: number } | undefined { + if (width <= 0 || height <= 0) return undefined; + // The canvas is 2x4 Braille pixels per cell, and it spans its bounds across + // `pixels - 1`, so the inverse has to use the same denominators or a click + // drifts from what was drawn. + const px = Math.max(1, width * 2 - 1); + const py = Math.max(1, height * 4 - 1); + const lon = x.min + ((column * 2 + 1) / px) * (x.max - x.min); + const lat = y.min + (1 - (row * 4 + 2) / py) * (y.max - y.min); + return { lon, lat }; +} diff --git a/packages/hqtui/src/index.ts b/packages/hqtui/src/index.ts index 7de353c..bb8c556 100644 --- a/packages/hqtui/src/index.ts +++ b/packages/hqtui/src/index.ts @@ -71,9 +71,12 @@ export { plot, blit, sparkline, bar, gauge, donut, histogram, verticalGlyph, horizontalGlyph, shadeGlyph, bestMode, plotPoints, domainOf, drawCanvas, projection, + worldShapes, countryAt, findCountry, countryBounds, degreesAt, + WORLD_COUNTRIES, WORLD_X, WORLD_Y, type Series, type PlotOptions, type FillMode, type Point, type MarkType, type ChartSeries, type AxisOptions, type ChartPlotOptions, type Domain, type Shape, type Bounds, type CanvasOptions, type Projection, + type CountryOutline, type WorldShapeOptions, } from "./graphics/index.ts"; // Widgets (for drawing straight onto a Surface) diff --git a/packages/hqtui/src/ui.ts b/packages/hqtui/src/ui.ts index f35d4eb..3565a18 100644 --- a/packages/hqtui/src/ui.ts +++ b/packages/hqtui/src/ui.ts @@ -10,6 +10,7 @@ import { stringWidth, wrap } from "./unicode.ts"; import { isRich, toSpanLines, wrapRich, type RichText } from "./richtext.ts"; import { BrailleCanvas } from "./graphics/braille.ts"; import { drawCanvas, type CanvasOptions } from "./graphics/canvas.ts"; +import type { CountryOutline } from "./graphics/world.ts"; import * as W from "./widgets/index.ts"; export interface HitRegion { @@ -631,6 +632,33 @@ export class Container { return this.add((s) => drawCanvas(s, options), this.sizeOf(options, "fill")); } + /** + * A world map, and the country under whatever gets clicked. + * + * The click is answered by turning the cell back into degrees and testing it + * against the outlines, so the answer is the country actually under the + * cursor. Bounding boxes would be cheaper and wrong: Russia's covers most of + * the northern hemisphere and Chile's covers Argentina. + */ + worldMap( + options: W.WorldMapOptions & ContainerOptions & { + onSelect?: (country: CountryOutline | undefined) => void; + onHover?: (country: CountryOutline | undefined) => void; + } = {}, + ): this { + return this.add((s) => { + W.drawWorldMap(s, options); + if (!options.onSelect && !options.onHover) return; + const at = (x: number, y: number) => + W.countryAtCell(x, y, s.width, s.height, options); + this.ctx.hit({ + rect: s.hitRect(), + onClick: options.onSelect ? (x, y) => options.onSelect?.(at(x, y)) : undefined, + onHover: options.onHover ? (x, y) => options.onHover?.(at(x, y)) : undefined, + }); + }, this.sizeOf(options, "fill")); + } + /** A Braille pixel canvas sized to the region, blitted when you are done. */ canvas( fn: (canvas: BrailleCanvas, surface: Surface) => void, diff --git a/packages/hqtui/src/widgets/index.ts b/packages/hqtui/src/widgets/index.ts index b0f075f..49a88f2 100644 --- a/packages/hqtui/src/widgets/index.ts +++ b/packages/hqtui/src/widgets/index.ts @@ -3,6 +3,7 @@ export * from "./calendar.ts"; export * from "./chart.ts"; export * from "./shadow.ts"; export * from "./surface.ts"; +export * from "./world.ts"; export * from "./scrollbar.ts"; export * from "./table.ts"; export * from "./meters.ts"; diff --git a/packages/hqtui/src/widgets/world.ts b/packages/hqtui/src/widgets/world.ts new file mode 100644 index 0000000..e951265 --- /dev/null +++ b/packages/hqtui/src/widgets/world.ts @@ -0,0 +1,61 @@ +/** + * A world map you can click. + * + * The drawing is the canvas doing what it already does -- polylines in the + * caller's own coordinates, which for a map are degrees. What this adds is the + * other direction: turning a click back into a country. + */ +import type { Surface } from "../surface.ts"; +import type { Color } from "../color.ts"; +import type { Bounds } from "../graphics/canvas.ts"; +import { drawCanvas } from "../graphics/canvas.ts"; +import { + type CountryOutline, WORLD_X, WORLD_Y, countryAt, degreesAt, worldShapes, +} from "../graphics/world.ts"; + +export interface WorldMapOptions { + /** The window on the globe. Defaults to all of it. */ + x?: Bounds; + y?: Bounds; + /** Coastline colour. */ + color?: Color; + /** Countries to pick out, by name or ISO code. */ + highlight?: readonly string[]; + highlightColor?: Color; + background?: Color; + grid?: boolean; +} + +export function drawWorldMap(surface: Surface, options: WorldMapOptions = {}): void { + if (surface.empty) return; + const theme = surface.theme; + drawCanvas(surface, { + shapes: worldShapes({ + color: options.color ?? theme.border, + highlight: options.highlight, + highlightColor: options.highlightColor ?? theme.accent, + }), + x: options.x ?? WORLD_X, + y: options.y ?? WORLD_Y, + background: options.background, + grid: options.grid, + }); +} + +/** + * The country under a cell of a map drawn with these bounds. + * + * Exposed so a caller can answer a hover as well as a click, and so the + * arithmetic that has to agree with the drawing lives in one place. + */ +export function countryAtCell( + column: number, + row: number, + width: number, + height: number, + options: WorldMapOptions = {}, +): CountryOutline | undefined { + const at = degreesAt(column, row, width, height, options.x ?? WORLD_X, options.y ?? WORLD_Y); + if (!at) return undefined; + return countryAt(at.lon, at.lat); +} diff --git a/packages/hqtui/test/world.test.ts b/packages/hqtui/test/world.test.ts new file mode 100644 index 0000000..022eae8 --- /dev/null +++ b/packages/hqtui/test/world.test.ts @@ -0,0 +1,182 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { renderToScreen } from "../src/index.ts"; +import { + WORLD_COUNTRIES, WORLD_X, WORLD_Y, countryAt, countryBounds, degreesAt, findCountry, + worldShapes, +} from "../src/graphics/world.ts"; +import { countryAtCell } from "../src/widgets/world.ts"; + +test("world: the data covers the countries a map is expected to have", () => { + assert.ok(WORLD_COUNTRIES.length > 160, `only ${WORLD_COUNTRIES.length} countries`); + // Sorted by name, which is what makes the overlap tie-break below stable. + const names = WORLD_COUNTRIES.map((c) => c.name); + assert.deepEqual(names, [...names].sort((a, b) => a.localeCompare(b))); + for (const key of ["France", "Japan", "Brazil", "Kenya", "Australia"]) { + assert.ok(findCountry(key), `${key} is missing`); + } + // ISO codes work as keys too, which is what an app is likely to have. + assert.equal(findCountry("JP")?.name, "Japan"); + assert.equal(findCountry("jp")?.name, "Japan", "the lookup is case-insensitive"); +}); + +test("world: every ring is a closed list of finite coordinate pairs", () => { + for (const country of WORLD_COUNTRIES) { + for (const ring of country.rings) { + assert.equal(ring.length % 2, 0, `${country.name}: odd coordinate count`); + assert.ok(ring.length >= 6, `${country.name}: a ring needs three points`); + for (let i = 0; i + 1 < ring.length; i += 2) { + assert.ok(Number.isFinite(ring[i]) && Number.isFinite(ring[i + 1])); + assert.ok(ring[i] >= -180.5 && ring[i] <= 180.5, `${country.name}: longitude ${ring[i]}`); + assert.ok(ring[i + 1] >= -90.5 && ring[i + 1] <= 90.5, `${country.name}: latitude ${ring[i + 1]}`); + } + } + } +}); + +test("world: a point lands in the country that is actually there", () => { + // Capitals, which is the least arguable way to check a polygon lookup. + const cities: [string, number, number, string][] = [ + ["Paris", 2.35, 48.86, "France"], + ["Tokyo", 139.7, 35.7, "Japan"], + ["Cairo", 31.2, 30.0, "Egypt"], + ["Brasilia", -47.9, -15.8, "Brazil"], + ["Canberra", 149.1, -35.3, "Australia"], + ["Denver", -105.0, 39.7, "United States of America"], + ["Moscow", 37.6, 55.75, "Russia"], + ["Delhi", 77.2, 28.6, "India"], + ]; + for (const [city, lon, lat, expected] of cities) { + assert.equal(countryAt(lon, lat)?.name, expected, `${city} (${lon}, ${lat})`); + } +}); + +test("world: the open ocean is not a country", () => { + // A bounding-box lookup would answer Russia or Chile for half of these. + for (const [lon, lat] of [[-140, 0], [-30, 0], [-25, -40], [80, -40], [-150, 40]]) { + assert.equal(countryAt(lon, lat), undefined, `(${lon}, ${lat}) claimed land`); + } +}); + +test("world: nonsense coordinates are water, not a crash", () => { + assert.equal(countryAt(Number.NaN, 0), undefined); + assert.equal(countryAt(0, Number.POSITIVE_INFINITY), undefined); +}); + +test("world: a cell maps back to the degrees it was drawn from", () => { + // If this is off by even half a cell, every click lands somewhere the user + // did not point at, and nothing about the map would look wrong. + const w = 100; + const h = 26; + for (let row = 0; row < h; row++) { + for (let column = 0; column < w; column++) { + const at = degreesAt(column, row, w, h); + assert.ok(at, `no degrees for (${column}, ${row})`); + const px = ((at.lon - WORLD_X.min) / 360) * (w * 2 - 1); + const py = (1 - (at.lat - WORLD_Y.min) / 180) * (h * 4 - 1); + assert.equal(Math.floor(px / 2), column, `column ${column} came back as ${px / 2}`); + assert.equal(Math.floor(py / 4), row, `row ${row} came back as ${py / 4}`); + } + } +}); + +test("world: an empty map has no cells to click", () => { + assert.equal(degreesAt(0, 0, 0, 10), undefined); + assert.equal(degreesAt(0, 0, 10, 0), undefined); +}); + +test("world: clicking a cell over a country reports that country", () => { + // Big enough that a country is more than one cell wide, which is the case + // where the answer is unambiguous. + const w = 200; + const h = 50; + const cellOf = (lon: number, lat: number): [number, number] => [ + Math.round((((lon + 180) / 360) * (w * 2 - 1) - 1) / 2), + Math.round(((1 - (lat + 90) / 180) * (h * 4 - 1) - 2) / 4), + ]; + const cases: [string, number, number][] = [ + ["Japan", 138.0, 36.5], + ["Brazil", -50.0, -10.0], + ["Australia", 134.0, -25.0], + ["Egypt", 29.0, 26.0], + ["India", 78.0, 22.0], + ]; + for (const [expected, lon, lat] of cases) { + const [column, row] = cellOf(lon, lat); + assert.equal(countryAtCell(column, row, w, h)?.name, expected, `${expected} at (${lon}, ${lat})`); + } +}); + +test("world: zooming changes which country a given cell holds", () => { + // The same cell over two different windows is two different places, which is + // the whole reason the lookup takes the bounds. + const japan = countryBounds(findCountry("Japan")!); + // A cell that is Honshu once the map is zoomed to Japan, and Russia when the + // same map shows the whole globe. + const cell = { column: 74, row: 2 }; + const whole = countryAtCell(cell.column, cell.row, 100, 26); + const zoomed = countryAtCell(cell.column, cell.row, 100, 26, japan); + assert.equal(zoomed?.name, "Japan"); + assert.equal(whole?.name, "Russia"); +}); + +test("world: a country's window contains it, with room around it", () => { + const japan = findCountry("Japan")!; + const bounds = countryBounds(japan); + let minLon = Infinity; + let maxLon = -Infinity; + for (const ring of japan.rings) { + for (let i = 0; i + 1 < ring.length; i += 2) { + minLon = Math.min(minLon, ring[i]); + maxLon = Math.max(maxLon, ring[i]); + } + } + assert.ok(bounds.x.min < minLon, "no margin on the left"); + assert.ok(bounds.x.max > maxLon, "no margin on the right"); +}); + +test("world: shapes are closed rings, so a coastline has no seam", () => { + const shapes = worldShapes(); + assert.ok(shapes.length > 200, `only ${shapes.length} shapes`); + for (const shape of shapes) { + assert.equal(shape.type, "polyline"); + if (shape.type !== "polyline") continue; + const first = shape.points[0]; + const last = shape.points[shape.points.length - 1]; + assert.deepEqual(first, last, "a ring that does not close leaves a gap"); + } +}); + +test("world: highlighted countries are drawn in their own colour", () => { + const shapes = worldShapes({ color: 0x111111, highlight: ["JP"], highlightColor: 0x222222 }); + const colours = new Set(shapes.map((s) => s.color)); + assert.deepEqual([...colours].sort(), [0x111111, 0x222222]); +}); + +test("world: the map draws, and draws differently when zoomed", () => { + const whole = renderToScreen(({ ui }) => ui.worldMap({}), { width: 60, height: 16 }).text(); + assert.ok(whole.trim().length > 0, "nothing was drawn"); + const japan = countryBounds(findCountry("Japan")!); + const zoomed = renderToScreen(({ ui }) => ui.worldMap(japan), { width: 60, height: 16 }).text(); + assert.notEqual(whole, zoomed); +}); + +test("world: a map with no handlers registers nothing to click", () => { + const plain = renderToScreen(({ ui }) => ui.worldMap({}), { width: 40, height: 12 }); + assert.equal(plain.regions.length, 0); + + let picked: string | undefined; + const live = renderToScreen( + ({ ui }) => ui.worldMap({ onSelect: (c) => { picked = c?.name; } }), + { width: 200, height: 50 }, + ); + assert.equal(live.regions.length, 1); + // A click over Australia comes back as Australia. + live.regions[0].onClick?.(173, 28, "left"); + assert.equal(picked, "Australia"); + + // And a click on open water comes back as nothing, rather than the nearest + // bounding box. + live.regions[0].onClick?.(20, 25, "left"); + assert.equal(picked, undefined); +}); diff --git a/ports/conformance/fixtures/widgets.json b/ports/conformance/fixtures/widgets.json index e0b7877..be6dbd2 100644 --- a/ports/conformance/fixtures/widgets.json +++ b/ports/conformance/fixtures/widgets.json @@ -5264,6 +5264,5706 @@ ] } }, + { + "name": "world", + "width": 60, + "height": 16, + "result": { + "width": 60, + "height": 16, + "chars": [ + [ + 14, + 32 + ], + [ + 1, + 10368 + ], + [ + 3, + 10432 + ], + [ + 1, + 10368 + ], + [ + 5, + 10432 + ], + [ + 1, + 10404 + ], + [ + 1, + 10468 + ], + [ + 2, + 10432 + ], + [ + 4, + 32 + ], + [ + 1, + 10368 + ], + [ + 2, + 10432 + ], + [ + 2, + 32 + ], + [ + 2, + 10432 + ], + [ + 6, + 32 + ], + [ + 1, + 10432 + ], + [ + 14, + 32 + ], + [ + 1, + 10436 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 6, + 10432 + ], + [ + 1, + 10422 + ], + [ + 1, + 10404 + ], + [ + 1, + 10340 + ], + [ + 1, + 10276 + ], + [ + 1, + 10368 + ], + [ + 1, + 10304 + ], + [ + 1, + 10267 + ], + [ + 1, + 10258 + ], + [ + 1, + 10248 + ], + [ + 1, + 10267 + ], + [ + 1, + 10259 + ], + [ + 1, + 10402 + ], + [ + 1, + 10436 + ], + [ + 3, + 32 + ], + [ + 1, + 10400 + ], + [ + 1, + 10334 + ], + [ + 4, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10368 + ], + [ + 1, + 10304 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 32 + ], + [ + 1, + 10304 + ], + [ + 1, + 10272 + ], + [ + 1, + 10388 + ], + [ + 1, + 10258 + ], + [ + 1, + 10242 + ], + [ + 1, + 10436 + ], + [ + 1, + 10276 + ], + [ + 2, + 10258 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 1, + 10294 + ], + [ + 2, + 10276 + ], + [ + 1, + 10432 + ], + [ + 1, + 10256 + ], + [ + 1, + 10276 + ], + [ + 1, + 10258 + ], + [ + 5, + 10432 + ], + [ + 1, + 10251 + ], + [ + 1, + 10256 + ], + [ + 1, + 10242 + ], + [ + 1, + 10435 + ], + [ + 1, + 10432 + ], + [ + 1, + 10340 + ], + [ + 1, + 10428 + ], + [ + 1, + 10432 + ], + [ + 1, + 10304 + ], + [ + 1, + 32 + ], + [ + 2, + 10249 + ], + [ + 2, + 10241 + ], + [ + 1, + 10337 + ], + [ + 1, + 10256 + ], + [ + 1, + 10258 + ], + [ + 1, + 10249 + ], + [ + 1, + 10300 + ], + [ + 1, + 10255 + ], + [ + 1, + 32 + ], + [ + 1, + 10259 + ], + [ + 1, + 10276 + ], + [ + 1, + 10258 + ], + [ + 1, + 10249 + ], + [ + 1, + 10256 + ], + [ + 1, + 10266 + ], + [ + 1, + 10267 + ], + [ + 1, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 10400 + ], + [ + 1, + 10384 + ], + [ + 1, + 10413 + ], + [ + 1, + 10254 + ], + [ + 1, + 10424 + ], + [ + 1, + 10267 + ], + [ + 1, + 10251 + ], + [ + 1, + 10249 + ], + [ + 1, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10248 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 10, + 32 + ], + [ + 2, + 10432 + ], + [ + 1, + 10464 + ], + [ + 1, + 10468 + ], + [ + 1, + 10436 + ], + [ + 1, + 10432 + ], + [ + 1, + 10302 + ], + [ + 2, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 3, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10464 + ], + [ + 2, + 10276 + ], + [ + 1, + 10372 + ], + [ + 2, + 10432 + ], + [ + 1, + 10304 + ], + [ + 1, + 10297 + ], + [ + 1, + 10243 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 10464 + ], + [ + 1, + 10308 + ], + [ + 6, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10295 + ], + [ + 1, + 10246 + ], + [ + 1, + 10432 + ], + [ + 1, + 10249 + ], + [ + 1, + 10416 + ], + [ + 1, + 10450 + ], + [ + 1, + 10322 + ], + [ + 1, + 10404 + ], + [ + 1, + 10400 + ], + [ + 1, + 10322 + ], + [ + 1, + 10290 + ], + [ + 1, + 10262 + ], + [ + 1, + 10249 + ], + [ + 1, + 10266 + ], + [ + 3, + 10276 + ], + [ + 1, + 10262 + ], + [ + 2, + 10276 + ], + [ + 1, + 10260 + ], + [ + 1, + 10274 + ], + [ + 1, + 10372 + ], + [ + 1, + 10329 + ], + [ + 1, + 10486 + ], + [ + 1, + 32 + ], + [ + 1, + 10296 + ], + [ + 1, + 10250 + ], + [ + 12, + 32 + ], + [ + 1, + 10275 + ], + [ + 1, + 10304 + ], + [ + 4, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10259 + ], + [ + 1, + 10349 + ], + [ + 1, + 10251 + ], + [ + 1, + 10249 + ], + [ + 8, + 32 + ], + [ + 1, + 10425 + ], + [ + 1, + 10482 + ], + [ + 1, + 10242 + ], + [ + 1, + 10400 + ], + [ + 1, + 10248 + ], + [ + 1, + 10265 + ], + [ + 2, + 10248 + ], + [ + 1, + 10241 + ], + [ + 1, + 10246 + ], + [ + 1, + 10288 + ], + [ + 1, + 10267 + ], + [ + 1, + 10285 + ], + [ + 1, + 10358 + ], + [ + 2, + 10242 + ], + [ + 1, + 32 + ], + [ + 1, + 10257 + ], + [ + 2, + 10258 + ], + [ + 1, + 10249 + ], + [ + 1, + 10404 + ], + [ + 1, + 10400 + ], + [ + 1, + 10308 + ], + [ + 1, + 10249 + ], + [ + 1, + 10267 + ], + [ + 1, + 10241 + ], + [ + 15, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10249 + ], + [ + 1, + 10257 + ], + [ + 1, + 10274 + ], + [ + 2, + 10258 + ], + [ + 1, + 10430 + ], + [ + 1, + 10304 + ], + [ + 9, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10340 + ], + [ + 1, + 10250 + ], + [ + 1, + 32 + ], + [ + 1, + 10265 + ], + [ + 1, + 10304 + ], + [ + 1, + 10251 + ], + [ + 1, + 10311 + ], + [ + 1, + 10248 + ], + [ + 1, + 10241 + ], + [ + 1, + 10258 + ], + [ + 1, + 10368 + ], + [ + 1, + 10304 + ], + [ + 1, + 10454 + ], + [ + 1, + 10453 + ], + [ + 1, + 10241 + ], + [ + 1, + 10258 + ], + [ + 1, + 10276 + ], + [ + 1, + 10368 + ], + [ + 1, + 10308 + ], + [ + 1, + 10304 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10304 + ], + [ + 1, + 10248 + ], + [ + 1, + 10241 + ], + [ + 18, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10257 + ], + [ + 1, + 10266 + ], + [ + 1, + 10294 + ], + [ + 1, + 10368 + ], + [ + 1, + 10308 + ], + [ + 1, + 10242 + ], + [ + 1, + 10304 + ], + [ + 1, + 10242 + ], + [ + 7, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10304 + ], + [ + 1, + 10246 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 10304 + ], + [ + 1, + 10400 + ], + [ + 1, + 10251 + ], + [ + 1, + 10441 + ], + [ + 1, + 10247 + ], + [ + 1, + 10470 + ], + [ + 1, + 10294 + ], + [ + 1, + 10241 + ], + [ + 1, + 32 + ], + [ + 1, + 10264 + ], + [ + 1, + 10308 + ], + [ + 1, + 10324 + ], + [ + 1, + 10250 + ], + [ + 1, + 10275 + ], + [ + 1, + 10248 + ], + [ + 1, + 10410 + ], + [ + 1, + 10304 + ], + [ + 1, + 10248 + ], + [ + 1, + 10368 + ], + [ + 24, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10265 + ], + [ + 1, + 10248 + ], + [ + 1, + 10427 + ], + [ + 1, + 10477 + ], + [ + 2, + 10242 + ], + [ + 5, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10243 + ], + [ + 1, + 10241 + ], + [ + 1, + 10243 + ], + [ + 1, + 10268 + ], + [ + 1, + 10304 + ], + [ + 1, + 10248 + ], + [ + 1, + 10251 + ], + [ + 1, + 10468 + ], + [ + 1, + 10368 + ], + [ + 1, + 10265 + ], + [ + 1, + 10241 + ], + [ + 3, + 32 + ], + [ + 1, + 10264 + ], + [ + 1, + 10259 + ], + [ + 1, + 32 + ], + [ + 1, + 10272 + ], + [ + 1, + 10291 + ], + [ + 1, + 10249 + ], + [ + 1, + 10464 + ], + [ + 1, + 10250 + ], + [ + 1, + 10248 + ], + [ + 1, + 10304 + ], + [ + 24, + 32 + ], + [ + 1, + 10416 + ], + [ + 1, + 10298 + ], + [ + 1, + 10307 + ], + [ + 1, + 10304 + ], + [ + 1, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10249 + ], + [ + 1, + 10386 + ], + [ + 1, + 10246 + ], + [ + 6, + 32 + ], + [ + 1, + 10257 + ], + [ + 1, + 10241 + ], + [ + 2, + 10368 + ], + [ + 1, + 10308 + ], + [ + 1, + 10402 + ], + [ + 9, + 32 + ], + [ + 1, + 10257 + ], + [ + 1, + 10261 + ], + [ + 1, + 10276 + ], + [ + 1, + 10244 + ], + [ + 1, + 10368 + ], + [ + 1, + 10244 + ], + [ + 1, + 10299 + ], + [ + 1, + 10359 + ], + [ + 1, + 10256 + ], + [ + 1, + 10288 + ], + [ + 1, + 10244 + ], + [ + 20, + 32 + ], + [ + 1, + 10259 + ], + [ + 1, + 10254 + ], + [ + 1, + 10400 + ], + [ + 1, + 10304 + ], + [ + 2, + 32 + ], + [ + 1, + 10332 + ], + [ + 7, + 32 + ], + [ + 1, + 10400 + ], + [ + 1, + 10276 + ], + [ + 1, + 10296 + ], + [ + 1, + 10388 + ], + [ + 1, + 10310 + ], + [ + 1, + 10241 + ], + [ + 1, + 10452 + ], + [ + 1, + 10247 + ], + [ + 9, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10276 + ], + [ + 2, + 10250 + ], + [ + 1, + 10283 + ], + [ + 1, + 10254 + ], + [ + 1, + 10374 + ], + [ + 2, + 32 + ], + [ + 1, + 10432 + ], + [ + 19, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10319 + ], + [ + 1, + 10241 + ], + [ + 1, + 10436 + ], + [ + 1, + 10304 + ], + [ + 1, + 10249 + ], + [ + 9, + 32 + ], + [ + 1, + 10272 + ], + [ + 1, + 10323 + ], + [ + 1, + 10465 + ], + [ + 1, + 10243 + ], + [ + 1, + 32 + ], + [ + 1, + 10248 + ], + [ + 10, + 32 + ], + [ + 1, + 10264 + ], + [ + 1, + 10308 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 10368 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10247 + ], + [ + 21, + 32 + ], + [ + 1, + 10424 + ], + [ + 1, + 10311 + ], + [ + 1, + 10356 + ], + [ + 1, + 10251 + ], + [ + 1, + 10241 + ], + [ + 11, + 32 + ], + [ + 1, + 10249 + ], + [ + 15, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10241 + ], + [ + 1, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10258 + ], + [ + 1, + 10266 + ], + [ + 3, + 32 + ], + [ + 1, + 10336 + ], + [ + 1, + 10262 + ], + [ + 17, + 32 + ], + [ + 1, + 10296 + ], + [ + 1, + 10276 + ], + [ + 1, + 10272 + ], + [ + 1, + 10244 + ], + [ + 20, + 32 + ], + [ + 1, + 10258 + ], + [ + 15, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10241 + ], + [ + 19, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10464 + ], + [ + 1, + 10244 + ], + [ + 14, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 3, + 10432 + ], + [ + 1, + 10304 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 2, + 10432 + ], + [ + 1, + 10276 + ], + [ + 4, + 10432 + ], + [ + 1, + 10276 + ], + [ + 1, + 10372 + ], + [ + 2, + 10432 + ], + [ + 7, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10272 + ], + [ + 1, + 10432 + ], + [ + 7, + 10276 + ], + [ + 1, + 10292 + ], + [ + 1, + 10262 + ], + [ + 3, + 10258 + ], + [ + 1, + 10486 + ], + [ + 1, + 10302 + ], + [ + 1, + 10266 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 10336 + ], + [ + 1, + 10276 + ], + [ + 1, + 10262 + ], + [ + 7, + 10249 + ], + [ + 1, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 3, + 32 + ], + [ + 1, + 10266 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 11, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10377 + ], + [ + 1, + 10486 + ], + [ + 1, + 10242 + ], + [ + 1, + 32 + ], + [ + 1, + 10454 + ], + [ + 3, + 10450 + ], + [ + 2, + 10459 + ], + [ + 1, + 10434 + ], + [ + 11, + 10432 + ], + [ + 1, + 10441 + ], + [ + 1, + 10449 + ], + [ + 2, + 10450 + ], + [ + 1, + 10442 + ], + [ + 2, + 10441 + ], + [ + 1, + 10433 + ], + [ + 30, + 10432 + ], + [ + 1, + 10441 + ], + [ + 2, + 10450 + ], + [ + 1, + 10482 + ] + ], + "fg": [ + [ + 14, + 29806811 + ], + [ + 14, + 19148864 + ], + [ + 4, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 6, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 14, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 20, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 4, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 32, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 10, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 9, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 10, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 14, + 19148864 + ], + [ + 6, + 29806811 + ], + [ + 26, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 12, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 4, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 8, + 29806811 + ], + [ + 16, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 10, + 19148864 + ], + [ + 15, + 29806811 + ], + [ + 8, + 19148864 + ], + [ + 9, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 17, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 18, + 29806811 + ], + [ + 9, + 19148864 + ], + [ + 7, + 29806811 + ], + [ + 13, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 10, + 19148864 + ], + [ + 24, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 5, + 29806811 + ], + [ + 12, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 24, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 6, + 29806811 + ], + [ + 6, + 19148864 + ], + [ + 9, + 29806811 + ], + [ + 11, + 19148864 + ], + [ + 20, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 7, + 29806811 + ], + [ + 8, + 19148864 + ], + [ + 9, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 19, + 29806811 + ], + [ + 6, + 19148864 + ], + [ + 9, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 10, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 21, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 11, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 15, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 17, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 20, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 15, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 19, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 14, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 12, + 19148864 + ], + [ + 7, + 29806811 + ], + [ + 18, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 11, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 11, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 60, + 19148864 + ] + ], + "bg": [ + [ + 960, + 17106698 + ] + ], + "attrs": [ + [ + 960, + 0 + ] + ], + "clusters": [], + "text": [ + " ⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀⢤⣤⣀⣀ ⢀⣀⣀ ⣀⣀ ⣀ ", + "⣄ ⢀⣀⣀⣀⣀⣀⣀⢶⢤⡤⠤⢀⡀⠛⠒⠈⠛⠓⢢⣄ ⢠⡞ ⠈⢀⡀⢀⣀ ⡀⠠⢔⠒⠂⣄⠤⠒⠒⠉⠁⠶⠤⠤⣀⠐⠤⠒⣀⣀⣀⣀⣀", + "⠋⠐⠂⣃⣀⡤⢼⣀⡀ ⠉⠉⠁⠁⡡⠐⠒⠉⠼⠏ ⠓⠤⠒⠉⠐⠚⠛ ⣀⢠⢐⢭⠎⢸⠛⠋⠉ ⠉⠈⠉⠁ ⣀⣀⣠⣤⣄⣀⠾", + " ⠈⠉⠁ ⠉⣠⠤⠤⢄⣀⣀⡀⠹⠃⢀⣀⣠⡄ ⠈⠷⠆⣀⠉⢰⣒⡒⢤⢠⡒⠲⠖⠉⠚⠤⠤⠤⠖⠤⠤⠔⠢⢄⡙⣶ ⠸⠊ ", + " ⠣⡀ ⠈⠓⡭⠋⠉ ⢹⣲⠂⢠⠈⠙⠈⠈⠁⠆⠰⠛⠭⡶⠂⠂ ⠑⠒⠒⠉⢤⢠⡄⠉⠛⠁ ", + " ⠈⠉⠑⠢⠒⠒⢾⡀ ⢀⡤⠊ ⠙⡀⠋⡇⠈⠁⠒⢀⡀⣖⣕⠁⠒⠤⢀⡄⡀ ⢀⡀⠈⠁ ", + " ⠈⠑⠚⠶⢀⡄⠂⡀⠂ ⠉⡀⠆⢀⣀⡀⢠⠋⣉⠇⣦⠶⠁ ⠘⡄⡔⠊⠣⠈⢪⡀⠈⢀ ", + " ⠈⠙⠈⢻⣭⠂⠂ ⠈⠃⠁⠃⠜⡀⠈⠋⣤⢀⠙⠁ ⠘⠓ ⠠⠳⠉⣠⠊⠈⡀ ", + " ⢰⠺⡃⡀ ⠈⠉⢒⠆ ⠑⠁⢀⢀⡄⢢ ⠑⠕⠤⠄⢀⠄⠻⡷⠐⠰⠄ ", + " ⠓⠎⢠⡀ ⡜ ⢠⠤⠸⢔⡆⠁⣔⠇ ⢀⠤⠊⠊⠫⠎⢆ ⣀ ", + " ⢀⡏⠁⣄⡀⠉ ⠠⡓⣡⠃ ⠈ ⠘⡄⢀⣀⢀ ⢀⠇ ", + " ⢸⡇⡴⠋⠁ ⠉ ⠈⠁ ⠉⠒⠚ ⡠⠖", + " ⠸⠤⠠⠄ ⠒ ⠈⠁ ", + " ⢀⣠⠄ ⣀ ⢀⣀⣀⣀⡀ ⢀⣀⣀⠤⣀⣀⣀⣀⠤⢄⣀⣀ ", + " ⢀⠠⣀⠤⠤⠤⠤⠤⠤⠤⠴⠖⠒⠒⠒⣶⠾⠚ ⢀⣀ ⣀⡠⠤⠖⠉⠉⠉⠉⠉⠉⠉ ⠉⠁ ⠚⠉⠁ ⠉⢉⣶⠂ ", + "⣖⣒⣒⣒⣛⣛⣂⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣉⣑⣒⣒⣊⣉⣉⣁⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣉⣒⣒⣲" + ] + } + }, + { + "name": "world-zoom", + "width": 40, + "height": 12, + "result": { + "width": 40, + "height": 12, + "chars": [ + [ + 6, + 10258 + ], + [ + 1, + 10241 + ], + [ + 7, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10274 + ], + [ + 1, + 10372 + ], + [ + 2, + 32 + ], + [ + 2, + 10432 + ], + [ + 4, + 32 + ], + [ + 1, + 10332 + ], + [ + 1, + 10248 + ], + [ + 1, + 10310 + ], + [ + 1, + 10326 + ], + [ + 1, + 10293 + ], + [ + 13, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10441 + ], + [ + 1, + 10293 + ], + [ + 1, + 10262 + ], + [ + 1, + 10242 + ], + [ + 9, + 32 + ], + [ + 2, + 10249 + ], + [ + 1, + 10368 + ], + [ + 1, + 10254 + ], + [ + 2, + 32 + ], + [ + 1, + 10336 + ], + [ + 1, + 10250 + ], + [ + 2, + 32 + ], + [ + 1, + 10439 + ], + [ + 1, + 10488 + ], + [ + 11, + 32 + ], + [ + 1, + 10276 + ], + [ + 1, + 10258 + ], + [ + 1, + 10250 + ], + [ + 1, + 10249 + ], + [ + 13, + 32 + ], + [ + 1, + 10326 + ], + [ + 1, + 10258 + ], + [ + 1, + 10250 + ], + [ + 1, + 32 + ], + [ + 1, + 10336 + ], + [ + 1, + 10250 + ], + [ + 3, + 32 + ], + [ + 1, + 10352 + ], + [ + 1, + 10257 + ], + [ + 1, + 10274 + ], + [ + 1, + 10372 + ], + [ + 24, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 10340 + ], + [ + 1, + 10279 + ], + [ + 2, + 10258 + ], + [ + 1, + 10249 + ], + [ + 4, + 32 + ], + [ + 1, + 10480 + ], + [ + 1, + 10277 + ], + [ + 1, + 10276 + ], + [ + 1, + 10258 + ], + [ + 1, + 10250 + ], + [ + 1, + 10241 + ], + [ + 14, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10336 + ], + [ + 1, + 10260 + ], + [ + 1, + 10362 + ], + [ + 1, + 10432 + ], + [ + 1, + 10336 + ], + [ + 1, + 10276 + ], + [ + 1, + 10326 + ], + [ + 1, + 10249 + ], + [ + 1, + 10325 + ], + [ + 1, + 10249 + ], + [ + 9, + 32 + ], + [ + 1, + 10332 + ], + [ + 1, + 10402 + ], + [ + 18, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10258 + ], + [ + 1, + 10274 + ], + [ + 1, + 10285 + ], + [ + 1, + 10432 + ], + [ + 1, + 32 + ], + [ + 1, + 10296 + ], + [ + 1, + 10400 + ], + [ + 1, + 10260 + ], + [ + 1, + 10290 + ], + [ + 1, + 10304 + ], + [ + 6, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 10276 + ], + [ + 1, + 10266 + ], + [ + 1, + 32 + ], + [ + 1, + 10400 + ], + [ + 1, + 10243 + ], + [ + 18, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10276 + ], + [ + 1, + 10250 + ], + [ + 1, + 10241 + ], + [ + 2, + 32 + ], + [ + 1, + 10264 + ], + [ + 1, + 10308 + ], + [ + 1, + 10368 + ], + [ + 1, + 10467 + ], + [ + 2, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 10276 + ], + [ + 1, + 10258 + ], + [ + 1, + 10249 + ], + [ + 3, + 32 + ], + [ + 1, + 10464 + ], + [ + 1, + 10243 + ], + [ + 20, + 32 + ], + [ + 1, + 10257 + ], + [ + 1, + 10308 + ], + [ + 4, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 1, + 10400 + ], + [ + 1, + 10258 + ], + [ + 1, + 10299 + ], + [ + 1, + 10272 + ], + [ + 1, + 10292 + ], + [ + 1, + 10258 + ], + [ + 1, + 10274 + ], + [ + 1, + 10260 + ], + [ + 1, + 10258 + ], + [ + 1, + 10249 + ], + [ + 23, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10310 + ], + [ + 6, + 32 + ], + [ + 1, + 10275 + ], + [ + 1, + 10250 + ], + [ + 30, + 32 + ], + [ + 1, + 10424 + ], + [ + 38, + 32 + ], + [ + 1, + 10336 + ], + [ + 1, + 10243 + ], + [ + 36, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 10268 + ], + [ + 1, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 10308 + ], + [ + 30, + 32 + ] + ], + "fg": [ + [ + 7, + 19148864 + ], + [ + 7, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 4, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 13, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 9, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 11, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 13, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 24, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 4, + 29806811 + ], + [ + 6, + 19148864 + ], + [ + 14, + 29806811 + ], + [ + 11, + 19148864 + ], + [ + 9, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 18, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 6, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 18, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 20, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 4, + 29806811 + ], + [ + 12, + 19148864 + ], + [ + 23, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 6, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 30, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 38, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 36, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 30, + 29806811 + ] + ], + "bg": [ + [ + 480, + 17106698 + ] + ], + "attrs": [ + [ + 480, + 0 + ] + ], + "clusters": [], + "text": [ + "⠒⠒⠒⠒⠒⠒⠁ ⠈⠢⢄ ⣀⣀ ⡜⠈⡆⡖⠵ ", + " ⠈⣉⠵⠖⠂ ⠉⠉⢀⠎ ⡠⠊ ⣇⣸ ", + "⠤⠒⠊⠉ ⡖⠒⠊ ⡠⠊ ⡰⠑⠢⢄ ", + " ⢀⣀⡤⠧⠒⠒⠉ ⣰⠥⠤⠒⠊⠁ ", + " ⢀⡠⠔⡺⣀⡠⠤⡖⠉⡕⠉ ⡜⢢ ", + " ⠉⠒⠢⠭⣀ ⠸⢠⠔⠲⡀ ⣀⠤⠚ ⢠⠃ ", + " ⢀⠤⠊⠁ ⠘⡄⢀⣣ ⣀⠤⠒⠉ ⣠⠃ ", + " ⠑⡄ ⠉⠁⢠⠒⠻⠠⠴⠒⠢⠔⠒⠉ ", + " ⠈⡆ ⠣⠊ ", + " ⢸ ", + " ⡠⠃ ", + " ⣀⠜ ⣀⡄ " + ] + } + }, + { + "name": "world-highlight", + "width": 60, + "height": 16, + "result": { + "width": 60, + "height": 16, + "chars": [ + [ + 14, + 32 + ], + [ + 1, + 10368 + ], + [ + 3, + 10432 + ], + [ + 1, + 10368 + ], + [ + 5, + 10432 + ], + [ + 1, + 10404 + ], + [ + 1, + 10468 + ], + [ + 2, + 10432 + ], + [ + 4, + 32 + ], + [ + 1, + 10368 + ], + [ + 2, + 10432 + ], + [ + 2, + 32 + ], + [ + 2, + 10432 + ], + [ + 6, + 32 + ], + [ + 1, + 10432 + ], + [ + 14, + 32 + ], + [ + 1, + 10436 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 6, + 10432 + ], + [ + 1, + 10422 + ], + [ + 1, + 10404 + ], + [ + 1, + 10340 + ], + [ + 1, + 10276 + ], + [ + 1, + 10368 + ], + [ + 1, + 10304 + ], + [ + 1, + 10267 + ], + [ + 1, + 10258 + ], + [ + 1, + 10248 + ], + [ + 1, + 10267 + ], + [ + 1, + 10259 + ], + [ + 1, + 10402 + ], + [ + 1, + 10436 + ], + [ + 3, + 32 + ], + [ + 1, + 10400 + ], + [ + 1, + 10334 + ], + [ + 4, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10368 + ], + [ + 1, + 10304 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 32 + ], + [ + 1, + 10304 + ], + [ + 1, + 10272 + ], + [ + 1, + 10388 + ], + [ + 1, + 10258 + ], + [ + 1, + 10242 + ], + [ + 1, + 10436 + ], + [ + 1, + 10276 + ], + [ + 2, + 10258 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 1, + 10294 + ], + [ + 2, + 10276 + ], + [ + 1, + 10432 + ], + [ + 1, + 10256 + ], + [ + 1, + 10276 + ], + [ + 1, + 10258 + ], + [ + 5, + 10432 + ], + [ + 1, + 10251 + ], + [ + 1, + 10256 + ], + [ + 1, + 10242 + ], + [ + 1, + 10435 + ], + [ + 1, + 10432 + ], + [ + 1, + 10340 + ], + [ + 1, + 10428 + ], + [ + 1, + 10432 + ], + [ + 1, + 10304 + ], + [ + 1, + 32 + ], + [ + 2, + 10249 + ], + [ + 2, + 10241 + ], + [ + 1, + 10337 + ], + [ + 1, + 10256 + ], + [ + 1, + 10258 + ], + [ + 1, + 10249 + ], + [ + 1, + 10300 + ], + [ + 1, + 10255 + ], + [ + 1, + 32 + ], + [ + 1, + 10259 + ], + [ + 1, + 10276 + ], + [ + 1, + 10258 + ], + [ + 1, + 10249 + ], + [ + 1, + 10256 + ], + [ + 1, + 10266 + ], + [ + 1, + 10267 + ], + [ + 1, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 10400 + ], + [ + 1, + 10384 + ], + [ + 1, + 10413 + ], + [ + 1, + 10254 + ], + [ + 1, + 10424 + ], + [ + 1, + 10267 + ], + [ + 1, + 10251 + ], + [ + 1, + 10249 + ], + [ + 1, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10248 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 10, + 32 + ], + [ + 2, + 10432 + ], + [ + 1, + 10464 + ], + [ + 1, + 10468 + ], + [ + 1, + 10436 + ], + [ + 1, + 10432 + ], + [ + 1, + 10302 + ], + [ + 2, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 3, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10464 + ], + [ + 2, + 10276 + ], + [ + 1, + 10372 + ], + [ + 2, + 10432 + ], + [ + 1, + 10304 + ], + [ + 1, + 10297 + ], + [ + 1, + 10243 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 10464 + ], + [ + 1, + 10308 + ], + [ + 6, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10295 + ], + [ + 1, + 10246 + ], + [ + 1, + 10432 + ], + [ + 1, + 10249 + ], + [ + 1, + 10416 + ], + [ + 1, + 10450 + ], + [ + 1, + 10322 + ], + [ + 1, + 10404 + ], + [ + 1, + 10400 + ], + [ + 1, + 10322 + ], + [ + 1, + 10290 + ], + [ + 1, + 10262 + ], + [ + 1, + 10249 + ], + [ + 1, + 10266 + ], + [ + 3, + 10276 + ], + [ + 1, + 10262 + ], + [ + 2, + 10276 + ], + [ + 1, + 10260 + ], + [ + 1, + 10274 + ], + [ + 1, + 10372 + ], + [ + 1, + 10329 + ], + [ + 1, + 10486 + ], + [ + 1, + 32 + ], + [ + 1, + 10296 + ], + [ + 1, + 10250 + ], + [ + 12, + 32 + ], + [ + 1, + 10275 + ], + [ + 1, + 10304 + ], + [ + 4, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10259 + ], + [ + 1, + 10349 + ], + [ + 1, + 10251 + ], + [ + 1, + 10249 + ], + [ + 8, + 32 + ], + [ + 1, + 10425 + ], + [ + 1, + 10482 + ], + [ + 1, + 10242 + ], + [ + 1, + 10400 + ], + [ + 1, + 10248 + ], + [ + 1, + 10265 + ], + [ + 2, + 10248 + ], + [ + 1, + 10241 + ], + [ + 1, + 10246 + ], + [ + 1, + 10288 + ], + [ + 1, + 10267 + ], + [ + 1, + 10285 + ], + [ + 1, + 10358 + ], + [ + 2, + 10242 + ], + [ + 1, + 32 + ], + [ + 1, + 10257 + ], + [ + 2, + 10258 + ], + [ + 1, + 10249 + ], + [ + 1, + 10404 + ], + [ + 1, + 10400 + ], + [ + 1, + 10308 + ], + [ + 1, + 10249 + ], + [ + 1, + 10267 + ], + [ + 1, + 10241 + ], + [ + 15, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10249 + ], + [ + 1, + 10257 + ], + [ + 1, + 10274 + ], + [ + 2, + 10258 + ], + [ + 1, + 10430 + ], + [ + 1, + 10304 + ], + [ + 9, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10340 + ], + [ + 1, + 10250 + ], + [ + 1, + 32 + ], + [ + 1, + 10265 + ], + [ + 1, + 10304 + ], + [ + 1, + 10251 + ], + [ + 1, + 10311 + ], + [ + 1, + 10248 + ], + [ + 1, + 10241 + ], + [ + 1, + 10258 + ], + [ + 1, + 10368 + ], + [ + 1, + 10304 + ], + [ + 1, + 10454 + ], + [ + 1, + 10453 + ], + [ + 1, + 10241 + ], + [ + 1, + 10258 + ], + [ + 1, + 10276 + ], + [ + 1, + 10368 + ], + [ + 1, + 10308 + ], + [ + 1, + 10304 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10304 + ], + [ + 1, + 10248 + ], + [ + 1, + 10241 + ], + [ + 18, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10257 + ], + [ + 1, + 10266 + ], + [ + 1, + 10294 + ], + [ + 1, + 10368 + ], + [ + 1, + 10308 + ], + [ + 1, + 10242 + ], + [ + 1, + 10304 + ], + [ + 1, + 10242 + ], + [ + 7, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10304 + ], + [ + 1, + 10246 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 10304 + ], + [ + 1, + 10400 + ], + [ + 1, + 10251 + ], + [ + 1, + 10441 + ], + [ + 1, + 10247 + ], + [ + 1, + 10470 + ], + [ + 1, + 10294 + ], + [ + 1, + 10241 + ], + [ + 1, + 32 + ], + [ + 1, + 10264 + ], + [ + 1, + 10308 + ], + [ + 1, + 10324 + ], + [ + 1, + 10250 + ], + [ + 1, + 10275 + ], + [ + 1, + 10248 + ], + [ + 1, + 10410 + ], + [ + 1, + 10304 + ], + [ + 1, + 10248 + ], + [ + 1, + 10368 + ], + [ + 24, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10265 + ], + [ + 1, + 10248 + ], + [ + 1, + 10427 + ], + [ + 1, + 10477 + ], + [ + 2, + 10242 + ], + [ + 5, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10243 + ], + [ + 1, + 10241 + ], + [ + 1, + 10243 + ], + [ + 1, + 10268 + ], + [ + 1, + 10304 + ], + [ + 1, + 10248 + ], + [ + 1, + 10251 + ], + [ + 1, + 10468 + ], + [ + 1, + 10368 + ], + [ + 1, + 10265 + ], + [ + 1, + 10241 + ], + [ + 3, + 32 + ], + [ + 1, + 10264 + ], + [ + 1, + 10259 + ], + [ + 1, + 32 + ], + [ + 1, + 10272 + ], + [ + 1, + 10291 + ], + [ + 1, + 10249 + ], + [ + 1, + 10464 + ], + [ + 1, + 10250 + ], + [ + 1, + 10248 + ], + [ + 1, + 10304 + ], + [ + 24, + 32 + ], + [ + 1, + 10416 + ], + [ + 1, + 10298 + ], + [ + 1, + 10307 + ], + [ + 1, + 10304 + ], + [ + 1, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10249 + ], + [ + 1, + 10386 + ], + [ + 1, + 10246 + ], + [ + 6, + 32 + ], + [ + 1, + 10257 + ], + [ + 1, + 10241 + ], + [ + 2, + 10368 + ], + [ + 1, + 10308 + ], + [ + 1, + 10402 + ], + [ + 9, + 32 + ], + [ + 1, + 10257 + ], + [ + 1, + 10261 + ], + [ + 1, + 10276 + ], + [ + 1, + 10244 + ], + [ + 1, + 10368 + ], + [ + 1, + 10244 + ], + [ + 1, + 10299 + ], + [ + 1, + 10359 + ], + [ + 1, + 10256 + ], + [ + 1, + 10288 + ], + [ + 1, + 10244 + ], + [ + 20, + 32 + ], + [ + 1, + 10259 + ], + [ + 1, + 10254 + ], + [ + 1, + 10400 + ], + [ + 1, + 10304 + ], + [ + 2, + 32 + ], + [ + 1, + 10332 + ], + [ + 7, + 32 + ], + [ + 1, + 10400 + ], + [ + 1, + 10276 + ], + [ + 1, + 10296 + ], + [ + 1, + 10388 + ], + [ + 1, + 10310 + ], + [ + 1, + 10241 + ], + [ + 1, + 10452 + ], + [ + 1, + 10247 + ], + [ + 9, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10276 + ], + [ + 2, + 10250 + ], + [ + 1, + 10283 + ], + [ + 1, + 10254 + ], + [ + 1, + 10374 + ], + [ + 2, + 32 + ], + [ + 1, + 10432 + ], + [ + 19, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10319 + ], + [ + 1, + 10241 + ], + [ + 1, + 10436 + ], + [ + 1, + 10304 + ], + [ + 1, + 10249 + ], + [ + 9, + 32 + ], + [ + 1, + 10272 + ], + [ + 1, + 10323 + ], + [ + 1, + 10465 + ], + [ + 1, + 10243 + ], + [ + 1, + 32 + ], + [ + 1, + 10248 + ], + [ + 10, + 32 + ], + [ + 1, + 10264 + ], + [ + 1, + 10308 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 10368 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10247 + ], + [ + 21, + 32 + ], + [ + 1, + 10424 + ], + [ + 1, + 10311 + ], + [ + 1, + 10356 + ], + [ + 1, + 10251 + ], + [ + 1, + 10241 + ], + [ + 11, + 32 + ], + [ + 1, + 10249 + ], + [ + 15, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10241 + ], + [ + 1, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10258 + ], + [ + 1, + 10266 + ], + [ + 3, + 32 + ], + [ + 1, + 10336 + ], + [ + 1, + 10262 + ], + [ + 17, + 32 + ], + [ + 1, + 10296 + ], + [ + 1, + 10276 + ], + [ + 1, + 10272 + ], + [ + 1, + 10244 + ], + [ + 20, + 32 + ], + [ + 1, + 10258 + ], + [ + 15, + 32 + ], + [ + 1, + 10248 + ], + [ + 1, + 10241 + ], + [ + 19, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10464 + ], + [ + 1, + 10244 + ], + [ + 14, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 3, + 10432 + ], + [ + 1, + 10304 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 2, + 10432 + ], + [ + 1, + 10276 + ], + [ + 4, + 10432 + ], + [ + 1, + 10276 + ], + [ + 1, + 10372 + ], + [ + 2, + 10432 + ], + [ + 7, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10272 + ], + [ + 1, + 10432 + ], + [ + 7, + 10276 + ], + [ + 1, + 10292 + ], + [ + 1, + 10262 + ], + [ + 3, + 10258 + ], + [ + 1, + 10486 + ], + [ + 1, + 10302 + ], + [ + 1, + 10266 + ], + [ + 1, + 32 + ], + [ + 1, + 10368 + ], + [ + 1, + 10432 + ], + [ + 1, + 32 + ], + [ + 1, + 10432 + ], + [ + 1, + 10336 + ], + [ + 1, + 10276 + ], + [ + 1, + 10262 + ], + [ + 7, + 10249 + ], + [ + 1, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 3, + 32 + ], + [ + 1, + 10266 + ], + [ + 1, + 10249 + ], + [ + 1, + 10241 + ], + [ + 11, + 32 + ], + [ + 1, + 10249 + ], + [ + 1, + 10377 + ], + [ + 1, + 10486 + ], + [ + 1, + 10242 + ], + [ + 1, + 32 + ], + [ + 1, + 10454 + ], + [ + 3, + 10450 + ], + [ + 2, + 10459 + ], + [ + 1, + 10434 + ], + [ + 11, + 10432 + ], + [ + 1, + 10441 + ], + [ + 1, + 10449 + ], + [ + 2, + 10450 + ], + [ + 1, + 10442 + ], + [ + 2, + 10441 + ], + [ + 1, + 10433 + ], + [ + 30, + 10432 + ], + [ + 1, + 10441 + ], + [ + 2, + 10450 + ], + [ + 1, + 10482 + ] + ], + "fg": [ + [ + 14, + 29806811 + ], + [ + 14, + 19148864 + ], + [ + 4, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 6, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 14, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 20, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 4, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 32, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 10, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 9, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 10, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 14, + 19148864 + ], + [ + 6, + 29806811 + ], + [ + 26, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 12, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 4, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 8, + 29806811 + ], + [ + 16, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 8, + 19148864 + ], + [ + 2, + 22467805 + ], + [ + 15, + 29806811 + ], + [ + 8, + 19148864 + ], + [ + 9, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 17, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 2, + 22467805 + ], + [ + 18, + 29806811 + ], + [ + 9, + 19148864 + ], + [ + 7, + 29806811 + ], + [ + 13, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 10, + 19148864 + ], + [ + 24, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 5, + 29806811 + ], + [ + 12, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 24, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 1, + 22467805 + ], + [ + 1, + 29806811 + ], + [ + 4, + 22467805 + ], + [ + 6, + 29806811 + ], + [ + 6, + 19148864 + ], + [ + 9, + 29806811 + ], + [ + 11, + 19148864 + ], + [ + 20, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 1, + 22467805 + ], + [ + 7, + 29806811 + ], + [ + 8, + 19148864 + ], + [ + 9, + 29806811 + ], + [ + 7, + 19148864 + ], + [ + 2, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 19, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 1, + 22467805 + ], + [ + 9, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 10, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 21, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 11, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 15, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 17, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 20, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 15, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 19, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 14, + 29806811 + ], + [ + 1, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 5, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 12, + 19148864 + ], + [ + 7, + 29806811 + ], + [ + 18, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 11, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 2, + 19148864 + ], + [ + 3, + 29806811 + ], + [ + 3, + 19148864 + ], + [ + 11, + 29806811 + ], + [ + 4, + 19148864 + ], + [ + 1, + 29806811 + ], + [ + 60, + 19148864 + ] + ], + "bg": [ + [ + 960, + 17106698 + ] + ], + "attrs": [ + [ + 960, + 0 + ] + ], + "clusters": [], + "text": [ + " ⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀⢤⣤⣀⣀ ⢀⣀⣀ ⣀⣀ ⣀ ", + "⣄ ⢀⣀⣀⣀⣀⣀⣀⢶⢤⡤⠤⢀⡀⠛⠒⠈⠛⠓⢢⣄ ⢠⡞ ⠈⢀⡀⢀⣀ ⡀⠠⢔⠒⠂⣄⠤⠒⠒⠉⠁⠶⠤⠤⣀⠐⠤⠒⣀⣀⣀⣀⣀", + "⠋⠐⠂⣃⣀⡤⢼⣀⡀ ⠉⠉⠁⠁⡡⠐⠒⠉⠼⠏ ⠓⠤⠒⠉⠐⠚⠛ ⣀⢠⢐⢭⠎⢸⠛⠋⠉ ⠉⠈⠉⠁ ⣀⣀⣠⣤⣄⣀⠾", + " ⠈⠉⠁ ⠉⣠⠤⠤⢄⣀⣀⡀⠹⠃⢀⣀⣠⡄ ⠈⠷⠆⣀⠉⢰⣒⡒⢤⢠⡒⠲⠖⠉⠚⠤⠤⠤⠖⠤⠤⠔⠢⢄⡙⣶ ⠸⠊ ", + " ⠣⡀ ⠈⠓⡭⠋⠉ ⢹⣲⠂⢠⠈⠙⠈⠈⠁⠆⠰⠛⠭⡶⠂⠂ ⠑⠒⠒⠉⢤⢠⡄⠉⠛⠁ ", + " ⠈⠉⠑⠢⠒⠒⢾⡀ ⢀⡤⠊ ⠙⡀⠋⡇⠈⠁⠒⢀⡀⣖⣕⠁⠒⠤⢀⡄⡀ ⢀⡀⠈⠁ ", + " ⠈⠑⠚⠶⢀⡄⠂⡀⠂ ⠉⡀⠆⢀⣀⡀⢠⠋⣉⠇⣦⠶⠁ ⠘⡄⡔⠊⠣⠈⢪⡀⠈⢀ ", + " ⠈⠙⠈⢻⣭⠂⠂ ⠈⠃⠁⠃⠜⡀⠈⠋⣤⢀⠙⠁ ⠘⠓ ⠠⠳⠉⣠⠊⠈⡀ ", + " ⢰⠺⡃⡀ ⠈⠉⢒⠆ ⠑⠁⢀⢀⡄⢢ ⠑⠕⠤⠄⢀⠄⠻⡷⠐⠰⠄ ", + " ⠓⠎⢠⡀ ⡜ ⢠⠤⠸⢔⡆⠁⣔⠇ ⢀⠤⠊⠊⠫⠎⢆ ⣀ ", + " ⢀⡏⠁⣄⡀⠉ ⠠⡓⣡⠃ ⠈ ⠘⡄⢀⣀⢀ ⢀⠇ ", + " ⢸⡇⡴⠋⠁ ⠉ ⠈⠁ ⠉⠒⠚ ⡠⠖", + " ⠸⠤⠠⠄ ⠒ ⠈⠁ ", + " ⢀⣠⠄ ⣀ ⢀⣀⣀⣀⡀ ⢀⣀⣀⠤⣀⣀⣀⣀⠤⢄⣀⣀ ", + " ⢀⠠⣀⠤⠤⠤⠤⠤⠤⠤⠴⠖⠒⠒⠒⣶⠾⠚ ⢀⣀ ⣀⡠⠤⠖⠉⠉⠉⠉⠉⠉⠉ ⠉⠁ ⠚⠉⠁ ⠉⢉⣶⠂ ", + "⣖⣒⣒⣒⣛⣛⣂⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣉⣑⣒⣒⣊⣉⣉⣁⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣉⣒⣒⣲" + ] + } + }, { "name": "badge", "width": 20, diff --git a/ports/conformance/generate.ts b/ports/conformance/generate.ts index 34222fb..5ec0f68 100644 Binary files a/ports/conformance/generate.ts and b/ports/conformance/generate.ts differ diff --git a/ports/cpp/CMakeLists.txt b/ports/cpp/CMakeLists.txt index bf46e4e..507b29c 100644 --- a/ports/cpp/CMakeLists.txt +++ b/ports/cpp/CMakeLists.txt @@ -14,7 +14,7 @@ if(HQTUI_LTO) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) endif() add_library(hqtui_cpp INTERFACE) -add_library(hqtui_cpp_widgets src/widgets.cpp src/scrollbar.cpp src/chart.cpp src/calendar.cpp src/canvas.cpp src/shadow.cpp) +add_library(hqtui_cpp_widgets src/widgets.cpp src/scrollbar.cpp src/chart.cpp src/calendar.cpp src/canvas.cpp src/shadow.cpp src/world.cpp src/world_data.cpp) set_target_properties(hqtui_cpp_widgets PROPERTIES POSITION_INDEPENDENT_CODE ON) target_compile_features(hqtui_cpp_widgets PUBLIC cxx_std_17) if(MSVC) @@ -71,6 +71,8 @@ if(UNIX) # keeps the published C++ snippets true. add_executable(hqtui-widgets examples/widgets.cpp) target_link_libraries(hqtui-widgets PRIVATE hqtui::cpp) + add_executable(hqtui-world-probe examples/world-probe.cpp) + target_link_libraries(hqtui-world-probe PRIVATE hqtui::cpp) endif() if(BUILD_TESTING) find_package(Python3 COMPONENTS Interpreter) diff --git a/ports/cpp/examples/world-probe.cpp b/ports/cpp/examples/world-probe.cpp new file mode 100644 index 0000000..22b14cd --- /dev/null +++ b/ports/cpp/examples/world-probe.cpp @@ -0,0 +1,45 @@ +/// Prints what the country lookup answers for a fixed set of points. +/// +/// The same probe exists for every port, so "the ports agree about the world" is +/// a diff rather than a hope. +#include +#include + +using namespace hqtui; + +int main() { + struct Place { + const char *name; + double lon, lat; + }; + const Place places[] = { + {"Paris", 2.35, 48.86}, {"Tokyo", 139.7, 35.7}, + {"Cairo", 31.2, 30.0}, {"Brasilia", -47.9, -15.8}, + {"Canberra", 149.1, -35.3}, {"Denver", -105.0, 39.7}, + {"Moscow", 37.6, 55.75}, {"Delhi", 77.2, 28.6}, + {"Nairobi", 36.8, -1.3}, {"Pacific", -140.0, 0.0}, + {"Atlantic", -30.0, 0.0}, {"SouthernOcean", 80.0, -40.0}, + {"NorthPacific", -150.0, 40.0}, + }; + for (auto &place : places) { + const CountryOutline *found = country_at(place.lon, place.lat); + std::printf("%s %s\n", place.name, found ? found->name.c_str() : "-"); + } + + // The cell path, which has to agree with what the canvas drew. + const int cells[][2] = {{173, 28}, {74, 2}, {20, 25}, {88, 7}}; + for (auto &cell : cells) { + const CountryOutline *found = country_at_cell(cell[0], cell[1], 200, 50); + std::printf("cell:%d,%d %s\n", cell[0], cell[1], found ? found->name.c_str() : "-"); + } + + // And the projection itself, so a drift shows up as a number rather than as a + // country that happens to still be right. + const int probes[][2] = {{0, 0}, {99, 25}, {50, 13}}; + for (auto &cell : probes) { + double lon = 0, lat = 0; + degrees_at(cell[0], cell[1], 100, 26, Bounds{-180, 180}, Bounds{-90, 90}, &lon, &lat); + std::printf("degrees:%d,%d %.4f %.4f\n", cell[0], cell[1], lon, lat); + } + return 0; +} diff --git a/ports/cpp/include/hqtui/widgets.hpp b/ports/cpp/include/hqtui/widgets.hpp index a5edc9c..e9f18e7 100644 --- a/ports/cpp/include/hqtui/widgets.hpp +++ b/ports/cpp/include/hqtui/widgets.hpp @@ -592,6 +592,51 @@ Projection canvas_projection(const Braille &, Bounds x, Bounds y); /// A canvas drawn in the caller's own coordinates rather than in pixels. void draw_canvas(Surface, const Canvas &); +struct CountryOutline { + std::string name; + /// ISO 3166-1 alpha-2, where Natural Earth has one. + std::string iso; + /// Longitude and latitude, interleaved. More than one ring means more than + /// one landmass. + std::vector> rings; +}; +/// The country outlines, built once on first use. +const std::vector &world_countries(); +struct WorldShapeOptions { + /// Colour for countries with nothing special about them. + Color color = 0; + /// Countries to pick out, by name or ISO code. + std::vector highlight; + Color highlight_color = 0; +}; +/// The world as canvas shapes, one closed polyline per landmass. +std::vector world_shapes(const WorldShapeOptions & = {}); +/// The country containing a point, or null for open water. +const CountryOutline *country_at(double lon, double lat); +/// Look a country up by name or ISO code. +const CountryOutline *find_country(std::string_view key); +/// The window a country fills, with a little room around it. +void country_bounds(const CountryOutline &, double margin, Bounds *x, Bounds *y); +/// The degrees under a terminal cell, given the window the map was drawn with. +bool degrees_at(int column, int row, int width, int height, Bounds x, Bounds y, double *lon, + double *lat); +struct WorldMap { + /// The window on the globe. Unset means all of it. + std::optional x, y; + /// Coastline colour. + Color color = 0; + /// Countries to pick out, by name or ISO code. + std::vector highlight; + Color highlight_color = 0; + std::optional background; + bool grid = false; +}; +/// A world map drawn in degrees. +void draw_world_map(Surface, const WorldMap & = {}); +/// The country under a cell of a map drawn with these bounds. +const CountryOutline *country_at_cell(int column, int row, int width, int height, + const WorldMap & = {}); + struct Shadow { /// How far the shadow falls. int offset_x = 1, offset_y = 1; @@ -1075,6 +1120,10 @@ class UI { void shapes(Canvas o, Constraint size = fr()) { draw([=](Surface s) { draw_canvas(s, o); }, size); } + /// A world map, and the country under whatever gets clicked. + void world_map(WorldMap o, Constraint size = fr()) { + draw([=](Surface s) { draw_world_map(s, o); }, size); + } void sparkline(Sparkline o) { draw([=](Surface s) { draw_sparkline(s, o); }, cells(1)); } diff --git a/ports/cpp/src/world.cpp b/ports/cpp/src/world.cpp new file mode 100644 index 0000000..f2b1f36 --- /dev/null +++ b/ports/cpp/src/world.cpp @@ -0,0 +1,169 @@ +/// The world, as shapes for the canvas, and the lookup that makes it clickable. +/// +/// The canvas already draws in the caller's own coordinates, and longitude and +/// latitude are just another pair of axes -- so a map is a list of polylines in +/// degrees, and nothing here needs a projection of its own beyond deciding +/// which window on the globe to show. +/// +/// The interesting half is the other direction. A click arrives as a terminal +/// cell, and a country is a polygon, so answering "what did they click" means +/// turning the cell back into degrees and testing it against the outlines. +/// Doing it that way rather than with bounding boxes is what makes the answer +/// right: Russia's bounding box covers most of the northern hemisphere, and +/// Chile's covers Argentina. +#include + +namespace hqtui { +namespace { + +bool equals_ignoring_case(std::string_view a, std::string_view b) { + if (a.size() != b.size()) + return false; + for (std::size_t i = 0; i < a.size(); i++) + if (std::tolower((unsigned char)a[i]) != std::tolower((unsigned char)b[i])) + return false; + return true; +} + +/// Match on either the name or the ISO code, case-insensitively. +bool matches(const CountryOutline &country, const std::vector &keys) { + for (auto &key : keys) { + if (key.empty()) + continue; + if (equals_ignoring_case(country.name, key)) + return true; + if (!country.iso.empty() && equals_ignoring_case(country.iso, key)) + return true; + } + return false; +} + +/// Whether a point is inside a ring, by ray casting. +/// +/// The ring is a flat list of interleaved coordinates, so this walks it two at a +/// time rather than allocating a pair per vertex -- it runs once per country per +/// click, and there are a couple of thousand vertices. +bool inside_ring(const std::vector &ring, double lon, double lat) { + bool inside = false; + std::size_t n = ring.size() / 2; + if (n == 0) + return false; + std::size_t j = n - 1; + for (std::size_t i = 0; i < n; i++) { + double xi = ring[i * 2], yi = ring[i * 2 + 1]; + double xj = ring[j * 2], yj = ring[j * 2 + 1]; + if ((yi > lat) != (yj > lat) && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) + inside = !inside; + j = i; + } + return inside; +} + +} // namespace + +std::vector world_shapes(const WorldShapeOptions &o) { + std::vector shapes; + for (auto &country : world_countries()) { + bool picked = !o.highlight.empty() && matches(country, o.highlight); + Color color = picked && o.highlight_color ? o.highlight_color : o.color; + for (auto &ring : country.rings) { + Shape shape; + shape.kind = HQ_SHAPE_POLYLINE; + shape.color = color; + std::size_t n = ring.size() / 2; + shape.points.reserve(n + 1); + for (std::size_t i = 0; i < n; i++) + shape.points.push_back({ring[i * 2], ring[i * 2 + 1]}); + // Closed: the last point joins the first, or every country has a gap in + // its coastline where the ring started. + if (n > 0) + shape.points.push_back(shape.points.front()); + shapes.push_back(std::move(shape)); + } + } + return shapes; +} + +const CountryOutline *country_at(double lon, double lat) { + if (!std::isfinite(lon) || !std::isfinite(lat)) + return nullptr; + for (auto &country : world_countries()) + for (auto &ring : country.rings) + if (inside_ring(ring, lon, lat)) + return &country; + return nullptr; +} + +const CountryOutline *find_country(std::string_view key) { + std::vector keys{std::string(key)}; + for (auto &country : world_countries()) + if (matches(country, keys)) + return &country; + return nullptr; +} + +void country_bounds(const CountryOutline &country, double margin, Bounds *x, Bounds *y) { + double min_lon = INFINITY, max_lon = -INFINITY; + double min_lat = INFINITY, max_lat = -INFINITY; + for (auto &ring : country.rings) + for (std::size_t i = 0; i + 1 < ring.size(); i += 2) { + min_lon = std::min(min_lon, ring[i]); + max_lon = std::max(max_lon, ring[i]); + min_lat = std::min(min_lat, ring[i + 1]); + max_lat = std::max(max_lat, ring[i + 1]); + } + if (!std::isfinite(min_lon)) { + *x = Bounds{-180, 180}; + *y = Bounds{-90, 90}; + return; + } + // A single-point country would give a zero-width window, which cannot be + // mapped onto anything. + double pad_x = std::max((max_lon - min_lon) * margin, 1.0); + double pad_y = std::max((max_lat - min_lat) * margin, 1.0); + *x = Bounds{min_lon - pad_x, max_lon + pad_x}; + *y = Bounds{min_lat - pad_y, max_lat + pad_y}; +} + +bool degrees_at(int column, int row, int width, int height, Bounds x, Bounds y, double *lon, + double *lat) { + if (width <= 0 || height <= 0) + return false; + // The canvas is 2x4 Braille pixels per cell, and it spans its bounds across + // `pixels - 1`, so the inverse has to use the same denominators or a click + // drifts from what was drawn. + double px = std::max(1, width * 2 - 1); + double py = std::max(1, height * 4 - 1); + *lon = x.min + ((column * 2 + 1) / px) * (x.max - x.min); + *lat = y.min + (1 - (row * 4 + 2) / py) * (y.max - y.min); + return true; +} + +void draw_world_map(Surface s, const WorldMap &o) { + if (s.rect().width <= 0 || s.rect().height <= 0) + return; + auto &t = theme(s); + WorldShapeOptions shapes; + shapes.color = o.color ? o.color : t.border; + shapes.highlight = o.highlight; + shapes.highlight_color = o.highlight_color ? o.highlight_color : t.accent; + + Canvas canvas; + canvas.shapes = world_shapes(shapes); + canvas.x = o.x.value_or(Bounds{-180, 180}); + canvas.y = o.y.value_or(Bounds{-90, 90}); + canvas.background = o.background; + canvas.grid = o.grid; + draw_canvas(s, canvas); +} + +const CountryOutline *country_at_cell(int column, int row, int width, int height, + const WorldMap &o) { + double lon = 0, lat = 0; + if (!degrees_at(column, row, width, height, o.x.value_or(Bounds{-180, 180}), + o.y.value_or(Bounds{-90, 90}), &lon, &lat)) + return nullptr; + return country_at(lon, lat); +} + +} // namespace hqtui diff --git a/ports/cpp/src/world_data.cpp b/ports/cpp/src/world_data.cpp new file mode 100644 index 0000000..ec897a7 --- /dev/null +++ b/ports/cpp/src/world_data.cpp @@ -0,0 +1,505 @@ +/// Country outlines, flattened for a terminal. +/// +/// Generated by packages/hqtui/scripts/generate-world.ts from Natural Earth's +/// 1:110m Admin 0 countries, which is public domain. Do not edit by hand. +/// +/// Each ring is longitude and latitude interleaved -- lon, lat, lon, lat -- +/// rather than a list of pairs, because at 1982 points the nested form +/// costs a container per coordinate for no gain. A country has more than one +/// ring when it is more than one landmass. +/// +/// 171 countries, 1982 points, simplified at 1 degrees. +#include + +namespace hqtui { +namespace { + +const double POOL[] = { + 66.5,37.4,70.8,38.5,71.8,36.7,75.2,37.1,71.3,36.1,69.3,31.9, + 66.3,29.9,60.9,29.8,61.2,35.7,66.5,37.4,21.0,40.8,19.4,40.3, + 19.7,42.7,21.0,40.8,-8.7,27.4,-8.7,28.8,-1.3,32.3,-1.2,35.7, + 8.4,36.9,7.5,34.1,9.8,29.4,9.3,26.1,12.0,23.5,3.2,19.1, + -8.7,27.4,12.3,-6.1,16.3,-5.9,17.5,-8.1,21.7,-7.3,22.2,-11.1, + 24.0,-11.2,24.0,-12.9,21.9,-12.9,23.2,-17.5,11.7,-17.3,13.7,-11.3, + 12.3,-6.1,-48.7,-78.0,-43.9,-78.5,-43.3,-80.0,-54.2,-80.6,-48.7,-78.0, + -66.3,-80.3,-59.6,-80.0,-66.3,-80.3,-73.9,-71.3,-70.3,-68.9,-68.3,-71.4, + -75.0,-72.1,-73.9,-71.3,-102.3,-71.9,-96.2,-72.5,-102.3,-71.9,-122.6,-73.7, + -118.7,-73.5,-122.6,-73.7,-127.3,-73.5,-124.0,-73.9,-127.3,-73.5,-163.7,-78.6, + -159.2,-79.5,-163.7,-78.6,180.0,-84.7,180.0,-90.0,-180.0,-90.0,-179.1,-84.1, + -143.1,-85.0,-153.6,-83.7,-152.9,-82.0,-156.8,-81.1,-146.4,-80.3,-155.3,-79.1, + -158.4,-76.9,-151.3,-77.4,-146.1,-76.5,-146.2,-75.4,-135.2,-74.3,-100.1,-74.9, + -103.7,-72.6,-74.9,-73.9,-67.4,-72.5,-67.7,-67.3,-57.8,-63.3,-65.7,-68.0, + -61.8,-70.7,-60.8,-73.7,-70.6,-76.6,-77.2,-76.7,-73.7,-77.9,-78.0,-79.2, + -58.2,-83.2,-28.5,-80.3,-35.6,-79.5,-35.8,-78.3,-17.5,-75.1,-15.4,-73.1, + -6.9,-70.9,27.1,-70.5,33.9,-68.5,38.6,-69.8,54.5,-65.8,61.4,-68.0, + 68.9,-67.9,69.7,-69.2,67.9,-71.9,69.9,-72.3,73.9,-69.9,88.0,-66.2, + 95.8,-67.4,102.8,-65.6,106.2,-66.9,113.6,-65.9,119.8,-67.3,135.1,-65.3, + 137.5,-67.0,145.5,-66.9,171.2,-71.7,163.6,-76.2,167.0,-78.8,161.8,-79.2, + 159.8,-80.9,169.4,-83.8,180.0,-84.7,-68.6,-52.6,-65.0,-54.7,-68.6,-54.9, + -68.6,-52.6,-57.6,-30.2,-58.5,-34.4,-56.8,-36.9,-62.3,-38.8,-62.7,-41.0, + -65.1,-41.1,-63.5,-42.6,-67.3,-45.6,-65.6,-47.2,-69.1,-50.7,-68.1,-52.3, + -71.9,-52.0,-73.4,-49.3,-71.2,-44.8,-72.1,-42.3,-68.4,-24.5,-66.3,-21.8, + -62.8,-22.0,-57.8,-25.2,-58.6,-27.1,-55.7,-27.4,-54.1,-25.5,-53.6,-26.9, + -57.6,-30.2,46.5,38.8,43.6,41.1,45.6,40.8,46.5,38.8,147.7,-40.8, + 147.9,-43.2,146.0,-43.5,144.7,-40.7,147.7,-40.8,126.1,-32.2,118.0,-35.1, + 115.0,-34.2,113.7,-22.5,120.9,-19.7,125.7,-14.2,129.6,-15.0,132.4,-11.1, + 136.5,-11.9,135.5,-15.0,140.2,-17.7,142.5,-10.7,146.4,-19.0,150.7,-22.4, + 153.6,-28.1,150.0,-37.4,146.3,-39.0,140.6,-38.0,138.2,-34.4,136.8,-35.3, + 137.8,-32.9,136.0,-34.9,131.3,-31.5,126.1,-32.2,17.0,48.1,14.6,46.4, + 9.5,47.1,12.9,47.5,13.6,48.9,17.0,48.1,46.4,41.9,50.4,40.3, + 48.9,38.3,45.6,39.9,45.0,41.2,46.4,41.9,-78.2,25.2,-77.5,23.8, + -78.2,25.2,92.7,22.0,92.4,20.7,91.4,22.8,89.0,22.1,88.6,26.4, + 92.4,25.0,91.2,23.5,92.7,22.0,28.2,56.2,30.9,55.6,32.7,53.4, + 31.8,52.1,23.5,51.6,23.5,53.9,28.2,56.2,6.2,50.8,5.7,49.5, + 2.5,51.1,6.2,50.8,-89.1,17.8,-88.1,18.3,-88.9,15.9,-89.1,17.8, + 2.7,6.3,0.8,10.5,2.8,12.2,2.7,6.3,91.7,27.8,88.8,27.1, + 91.7,27.8,-69.5,-11.0,-65.3,-9.8,-65.4,-11.6,-60.5,-13.8,-60.2,-16.3, + -58.2,-16.3,-57.9,-20.0,-61.8,-19.6,-62.7,-22.2,-67.8,-22.9,-69.5,-11.0, + 18.6,42.7,16.0,45.2,19.4,44.9,18.6,42.7,29.4,-22.1,25.7,-25.5, + 21.6,-26.7,19.9,-24.8,20.9,-18.3,25.3,-17.7,29.4,-22.1,-53.4,-33.8, + -53.8,-32.0,-57.6,-30.2,-53.6,-26.1,-55.8,-22.4,-57.9,-22.1,-58.2,-16.3, + -60.2,-16.3,-60.5,-13.8,-65.4,-11.6,-65.3,-9.8,-70.5,-11.0,-70.5,-9.5, + -72.2,-10.1,-74.0,-7.5,-72.9,-5.3,-69.9,-4.3,-69.8,1.7,-65.5,0.8, + -63.4,2.2,-64.8,4.1,-60.7,5.2,-59.0,1.3,-52.9,2.1,-51.3,4.2, + -50.4,-0.1,-44.6,-2.7,-40.0,-2.9,-35.6,-5.1,-34.7,-7.3,-38.7,-13.1, + -40.9,-21.9,-47.6,-24.9,-48.9,-28.7,-53.4,-33.8,22.7,44.2,28.6,43.7, + 28.0,42.0,23.0,41.3,22.7,44.2,-5.4,10.4,-4.3,13.2,-1.1,15.0, + 2.2,12.6,0.9,11.0,-2.9,11.0,-2.8,9.6,-5.4,10.4,30.5,-2.4, + 29.3,-4.5,29.0,-2.8,30.5,-2.4,102.6,12.2,103.0,14.2,107.6,13.5, + 106.2,11.0,103.5,10.6,102.6,12.2,14.5,12.9,14.5,4.7,15.9,1.7, + 9.6,2.3,8.8,5.5,11.7,7.0,14.5,12.9,-122.8,49.0,-127.4,50.8, + -130.5,54.3,-130.0,55.9,-135.5,59.8,-137.5,58.9,-141.0,60.3,-141.0,69.7, + -136.5,68.9,-128.1,70.5,-113.5,67.7,-106.1,68.8,-101.5,67.6,-97.7,68.6, + -96.1,67.3,-94.2,69.1,-96.5,70.1,-95.2,71.9,-87.4,67.2,-85.5,69.9, + -82.6,69.7,-81.4,67.1,-85.8,66.6,-90.7,63.6,-94.7,58.9,-92.3,57.1, + -82.3,55.1,-79.9,51.2,-78.6,52.6,-79.8,54.7,-76.5,56.5,-78.5,58.8, + -77.3,59.9,-78.1,62.3,-73.8,62.4,-69.6,61.1,-67.6,58.2,-64.6,60.3, + -61.8,56.3,-57.3,54.6,-55.7,52.1,-60.0,50.2,-66.4,50.2,-71.1,46.8, + -65.1,49.2,-64.5,46.2,-60.5,47.0,-59.8,45.9,-65.4,43.5,-66.2,44.5, + -64.4,45.3,-67.1,45.1,-69.2,47.4,-71.5,45.0,-82.4,41.7,-82.6,45.3, + -88.4,48.3,-122.8,49.0,-84.0,62.5,-81.9,62.9,-84.0,62.5,-79.8,72.8, + -80.8,73.7,-76.3,72.8,-79.8,72.8,-93.6,75.0,-96.8,74.9,-93.6,75.0, + -93.8,77.5,-96.4,77.8,-93.8,77.5,-96.8,78.8,-95.6,78.4,-98.6,78.9, + -96.8,78.8,-88.2,74.4,-97.1,76.8,-79.8,74.9,-88.2,74.4,-111.3,78.2, + -109.9,78.0,-113.5,77.7,-111.3,78.2,-111.0,78.8,-109.7,78.6,-112.5,78.4, + -111.0,78.8,-55.6,51.3,-56.8,49.8,-53.5,49.2,-53.1,46.7,-59.3,47.6, + -55.6,51.3,-83.9,65.1,-80.1,63.7,-87.2,63.5,-85.9,65.7,-83.9,65.1, + -78.8,72.4,-68.8,70.5,-67.0,69.2,-68.8,68.7,-61.9,66.9,-63.9,65.0, + -68.0,66.3,-64.7,63.4,-68.8,63.7,-66.2,61.9,-68.9,62.3,-78.6,64.6, + -74.0,65.5,-73.3,68.1,-79.0,70.2,-88.7,70.4,-90.2,72.2,-85.8,73.8, + -85.8,72.5,-82.3,73.8,-78.8,72.4,-94.5,74.1,-90.5,73.9,-95.4,72.1, + -96.0,73.4,-94.5,74.1,-122.9,76.1,-116.2,77.6,-122.9,76.1,-132.7,54.0, + -131.2,52.2,-132.7,54.0,-105.5,79.3,-99.7,77.9,-105.5,79.3,-123.5,48.5, + -128.4,50.8,-123.5,48.5,-121.5,74.4,-115.5,73.5,-123.1,70.9,-125.9,71.9, + -123.9,73.7,-124.9,74.3,-121.5,74.4,-107.8,75.8,-105.7,75.5,-117.7,75.2, + -115.4,76.5,-107.8,75.8,-106.5,73.1,-101.1,69.6,-113.3,68.5,-117.3,70.0, + -112.4,70.4,-119.4,71.6,-115.2,73.3,-108.2,71.7,-108.4,73.1,-106.5,73.1, + -100.4,72.7,-101.5,73.4,-97.4,73.8,-96.5,72.6,-98.4,71.3,-102.5,72.5, + -100.4,72.7,-106.6,73.6,-104.5,73.4,-106.6,73.6,-98.5,76.7,-98.2,75.0, + -102.5,75.6,-98.5,76.7,-96.0,80.6,-92.4,81.3,-85.8,79.3,-92.9,78.3, + -96.0,80.6,-91.6,81.9,-61.8,82.6,-76.9,79.3,-75.4,78.5,-80.6,76.2, + -89.5,76.5,-88.3,77.9,-85.0,77.5,-88.0,78.4,-85.1,79.3,-86.9,80.3, + -81.8,80.5,-91.6,81.9,-75.2,67.4,-77.2,67.6,-75.2,67.4,-96.3,69.5, + -99.8,69.4,-96.3,69.5,-64.5,49.9,-61.8,49.1,-64.5,49.9,-64.0,47.0, + -62.0,46.4,-64.0,47.0,27.4,5.2,22.4,4.0,19.5,5.0,16.0,2.3, + 14.5,5.5,15.3,7.4,22.9,11.1,27.4,5.2,23.8,19.6,23.9,15.6, + 21.9,12.6,22.9,11.1,15.3,7.4,13.5,14.4,15.9,20.4,14.9,22.9, + 23.8,19.6,-68.6,-52.6,-68.6,-54.9,-67.0,-54.9,-68.1,-55.6,-74.7,-52.8, + -71.1,-54.1,-68.6,-52.6,-69.6,-17.6,-67.0,-23.0,-70.5,-31.4,-69.8,-34.2, + -72.1,-42.3,-71.2,-44.8,-73.4,-49.3,-71.9,-52.0,-68.6,-52.3,-71.4,-53.9, + -74.9,-52.3,-75.6,-48.7,-74.1,-46.9,-75.6,-46.6,-72.7,-42.4,-74.3,-43.2, + -69.6,-17.6,109.5,18.2,108.6,19.4,110.8,20.1,109.5,18.2,80.3,42.3, + 80.0,44.9,87.8,49.3,91.0,46.9,90.9,45.3,96.3,42.7,109.2,42.5, + 111.9,45.1,119.7,46.7,115.5,48.1,122.2,53.4,125.9,52.8,131.0,47.8, + 135.0,48.5,133.1,45.1,131.0,45.0,130.6,42.4,121.1,38.9,121.6,40.9, + 117.5,38.7,122.4,37.5,119.2,34.9,121.9,31.7,121.7,28.2,118.7,24.5, + 110.4,20.3,105.3,23.4,101.7,22.3,101.8,21.2,99.2,22.1,97.6,23.9, + 98.7,27.5,96.1,29.5,88.8,27.3,78.7,31.5,78.9,34.3,73.7,39.4, + 80.3,42.3,-66.9,1.3,-69.8,1.7,-69.9,-4.3,-70.0,-2.7,-77.4,0.4, + -79.0,1.7,-77.1,3.8,-77.5,8.5,-71.4,12.4,-73.3,9.2,-72.0,7.0, + -67.3,6.1,-66.9,1.3,18.5,3.5,16.0,-3.5,11.9,-5.0,11.5,-2.8, + 14.4,-1.3,13.1,2.3,15.9,1.7,18.5,3.5,-82.5,9.6,-83.0,8.2, + -85.9,10.9,-82.5,9.6,-8.0,10.2,-2.8,9.6,-2.9,5.0,-7.7,4.4, + -8.0,10.2,16.6,46.5,19.4,45.2,15.8,44.8,18.5,42.5,13.7,45.1, + 16.6,46.5,-82.3,23.2,-74.2,20.3,-77.8,19.9,-81.8,22.6,-85.0,21.9, + -82.3,23.2,32.7,35.1,34.0,35.0,32.7,35.1,15.0,51.1,18.9,49.5, + 12.5,49.5,15.0,51.1,29.3,-4.5,30.7,-8.3,28.7,-8.5,28.4,-11.8, + 29.7,-13.3,22.2,-11.1,21.7,-7.3,17.5,-8.1,16.3,-5.9,12.2,-5.8, + 16.0,-3.5,19.5,5.0,29.7,4.6,31.2,2.2,29.3,-4.5,9.9,55.0, + 8.1,56.5,10.6,57.7,9.9,55.0,12.4,56.1,12.1,54.8,11.0,55.4, + 12.4,56.1,42.4,12.5,42.8,10.9,42.4,12.5,-71.7,18.0,-71.6,19.9, + -68.3,18.6,-71.7,18.0,-75.4,-0.2,-78.6,-4.5,-80.4,-4.4,-80.1,0.8, + -75.4,-0.2,36.9,22.0,25.0,22.0,25.2,31.6,34.3,31.2,34.2,27.8, + 32.3,29.8,36.9,22.0,-89.4,14.4,-87.9,13.1,-90.1,13.7,-89.4,14.4, + 9.6,2.3,11.3,1.1,9.5,1.0,9.6,2.3,36.4,14.4,38.4,18.0, + 43.1,12.7,36.4,14.4,28.0,59.5,27.3,57.5,23.3,59.2,28.0,59.5, + 32.1,-26.7,31.0,-25.7,32.1,-26.7,47.8,8.0,45.0,5.0,39.6,3.4, + 36.2,4.4,33.0,7.8,37.9,15.0,41.6,13.5,43.7,9.2,47.8,8.0, + -61.2,-51.8,-57.7,-51.5,-61.2,-51.8,28.6,69.1,31.1,62.4,28.1,60.5, + 21.3,60.7,21.5,63.2,25.4,65.1,20.6,69.1,24.7,68.6,27.7,70.2, + 28.6,69.1,68.9,-48.6,70.6,-49.3,68.7,-49.8,68.9,-48.6,-51.7,4.2, + -52.9,2.1,-54.5,2.3,-54.0,5.8,-51.7,4.2,6.2,49.5,8.1,49.0, + 6.0,46.7,7.4,43.7,1.8,42.3,-1.9,43.4,-1.2,46.0,-4.6,48.7, + -1.6,48.6,-1.9,49.8,2.5,51.1,6.2,49.5,8.7,42.6,9.2,41.4, + 8.7,42.6,11.3,2.3,14.3,1.2,14.4,-1.3,11.1,-4.0,8.8,-1.1, + 11.3,2.3,-16.7,13.6,-13.8,13.5,-16.7,13.6,40.0,43.4,46.6,41.2, + 41.6,41.5,40.0,43.4,14.1,53.8,15.0,51.1,12.2,50.3,12.9,47.5, + 7.5,47.6,8.1,49.0,6.0,50.1,7.1,53.7,9.9,55.0,14.1,53.8, + 0.0,11.0,1.1,5.9,-2.9,5.0,-2.9,11.0,0.0,11.0,26.3,35.3, + 23.5,35.3,26.3,35.3,23.0,41.3,26.6,41.6,22.6,40.3,24.0,37.7, + 22.5,36.4,20.2,39.6,23.0,41.3,-46.8,82.6,-27.1,83.5,-20.8,82.7, + -31.9,82.2,-12.2,81.3,-20.0,80.2,-17.7,80.1,-19.7,78.8,-18.5,77.0, + -21.7,76.6,-19.4,74.3,-24.8,72.3,-21.8,70.7,-25.5,71.4,-26.4,70.2, + -22.3,70.1,-39.8,65.5,-43.4,60.1,-48.3,60.9,-51.6,63.6,-54.0,67.2, + -50.9,69.9,-54.7,69.6,-54.4,70.8,-51.4,70.6,-55.8,71.7,-54.7,72.6, + -58.6,75.5,-68.5,76.1,-71.4,77.0,-66.8,77.4,-73.3,78.0,-65.7,79.4, + -68.0,80.1,-62.7,81.8,-44.5,81.7,-46.8,82.6,-92.2,14.5,-90.5,16.1, + -91.0,17.8,-89.1,17.8,-88.2,15.7,-89.4,14.4,-92.2,14.5,-13.7,12.6, + -9.1,12.3,-8.3,7.7,-11.1,10.0,-13.2,8.9,-15.1,11.0,-13.7,12.6, + -16.7,12.4,-13.7,11.8,-15.1,11.0,-16.7,12.4,-56.5,1.9,-59.6,1.8, + -61.4,6.0,-59.8,8.4,-57.1,6.0,-58.0,4.1,-56.5,1.9,-71.7,19.7, + -71.7,18.0,-74.5,18.3,-71.7,19.7,-83.1,15.0,-87.3,13.0,-89.4,14.4, + -87.9,15.9,-83.1,15.0,22.1,48.4,21.0,46.3,16.2,46.9,17.0,48.1, + 22.1,48.4,-14.5,66.5,-13.6,65.1,-18.7,63.5,-22.8,64.0,-21.8,64.4, + -24.0,64.9,-22.2,65.4,-24.3,65.6,-14.5,66.5,97.3,28.3,92.7,22.0, + 91.2,23.5,92.4,25.0,88.6,26.4,88.9,21.7,80.3,15.9,79.9,10.4, + 77.5,8.0,72.6,21.4,70.5,20.9,68.2,23.7,71.0,24.4,69.5,26.9, + 75.3,32.3,73.7,34.3,77.8,35.5,78.7,31.5,81.1,30.2,80.1,28.8, + 83.3,27.4,88.1,26.4,88.7,28.1,92.0,26.8,96.1,29.5,97.3,28.3, + 141.0,-2.6,141.0,-9.1,137.6,-8.4,137.9,-5.4,133.0,-4.1,132.0,-2.8, + 133.7,-2.2,130.5,-0.9,134.0,-0.8,135.5,-3.4,137.4,-1.7,141.0,-2.6, + 125.0,-8.9,123.5,-10.2,125.0,-8.9,117.9,4.1,119.0,0.9,116.1,-4.0, + 110.2,-2.9,109.7,2.0,110.5,0.8,113.8,1.2,115.9,4.3,117.9,4.1, + 129.4,-2.8,130.8,-3.9,127.9,-3.4,129.4,-2.8,127.9,2.2,128.1,-0.9, + 127.9,2.2,122.9,0.9,125.2,1.4,120.0,-0.5,123.3,-0.6,121.5,-1.9, + 123.2,-5.3,121.0,-2.6,119.8,-5.7,118.8,-2.8,119.8,0.2,122.9,0.9, + 120.3,-10.3,119.0,-9.6,120.3,-10.3,121.3,-8.5,122.9,-8.1,119.9,-8.8, + 121.3,-8.5,118.3,-8.4,116.7,-9.0,118.3,-8.4,108.5,-6.4,115.7,-8.4, + 105.4,-6.9,108.5,-6.4,104.4,-1.1,106.1,-3.1,105.8,-5.9,102.6,-4.2, + 95.3,5.5,97.5,5.2,104.4,-1.1,48.6,29.9,45.4,34.0,46.1,35.7, + 44.1,39.4,48.1,39.6,50.8,36.9,56.6,38.1,61.1,36.5,60.9,29.8, + 63.3,26.8,61.5,25.1,57.4,25.7,48.6,29.9,39.2,32.2,41.3,36.4, + 44.8,37.2,48.6,29.9,44.7,29.2,39.2,32.2,-6.2,53.9,-6.8,52.3, + -10.0,51.8,-7.6,55.1,-6.2,53.9,35.7,32.7,34.9,29.5,34.3,31.2, + 35.7,32.7,10.4,46.9,13.8,46.5,12.6,44.1,18.3,39.8,16.9,40.4, + 15.7,37.9,15.4,40.0,10.2,43.9,7.4,43.7,6.8,46.0,10.4,46.9, + 14.8,38.1,15.1,36.6,12.4,37.6,14.8,38.1,8.7,40.9,9.8,40.5, + 8.8,38.9,8.7,40.9,-77.6,18.5,-76.2,17.9,-77.6,18.5,141.9,39.2, + 140.3,35.1,135.8,33.5,135.1,34.6,131.0,33.9,132.0,33.1,130.2,31.4, + 129.4,33.3,139.4,38.2,140.3,41.2,141.9,39.2,144.6,44.0,145.5,43.3, + 140.0,41.6,142.0,45.6,144.6,44.0,132.4,33.5,134.8,33.8,132.4,33.5, + 35.5,32.4,38.8,33.4,39.2,32.2,37.0,31.5,38.0,30.5,36.1,29.2, + 34.9,29.5,35.5,32.4,87.4,49.2,80.0,44.9,80.3,42.3,74.2,43.3, + 68.6,40.7,64.9,43.7,62.0,43.5,58.5,45.6,55.9,45.0,56.0,41.3, + 52.5,41.8,50.3,44.6,53.0,45.3,53.0,46.9,49.1,46.4,46.5,48.4, + 50.8,51.7,61.3,50.8,60.0,52.0,61.4,54.0,69.1,55.4,73.4,53.5, + 76.9,54.5,80.0,50.9,87.4,49.2,39.2,-4.7,33.9,-0.9,35.3,5.5, + 38.1,3.6,41.9,3.9,41.6,-1.7,39.2,-4.7,20.6,41.9,20.6,43.2, + 21.8,42.7,20.6,41.9,48.0,30.0,48.4,28.6,46.6,29.1,48.0,30.0, + 71.0,42.3,74.2,43.3,80.3,42.3,73.7,39.4,69.5,39.5,73.1,40.9, + 71.0,42.3,107.4,14.2,105.2,14.3,104.0,18.2,101.1,17.5,100.1,20.4, + 101.7,22.3,104.4,20.8,103.9,19.3,107.4,14.2,27.3,57.5,28.2,56.2, + 26.5,55.6,21.1,56.0,22.5,57.8,27.3,57.5,35.8,33.3,36.4,34.6, + 35.8,33.3,29.0,-29.0,28.1,-30.5,27.0,-29.9,29.0,-29.0,-8.4,7.7, + -7.7,4.4,-11.4,6.8,-10.2,8.4,-8.4,7.7,25.0,22.0,23.8,19.6, + 10.3,24.4,10.0,31.4,11.5,33.1,19.1,30.3,20.9,32.7,24.9,31.9, + 25.0,22.0,26.5,55.6,23.5,53.9,21.1,56.0,26.5,55.6,49.5,-12.5, + 50.4,-15.7,47.1,-24.9,45.4,-25.6,43.3,-22.8,44.0,-17.4,49.5,-12.5, + 32.8,-9.2,35.7,-14.6,35.0,-16.8,32.7,-13.7,32.8,-9.2,100.1,6.5, + 103.0,5.5,104.2,1.3,101.4,2.8,100.1,6.5,117.9,4.1,115.9,4.3, + 114.6,1.4,109.8,1.3,115.3,4.3,116.7,6.9,119.2,5.4,117.9,4.1, + -11.5,12.4,-11.7,15.4,-5.5,15.5,-6.5,25.0,4.3,19.2,3.6,15.6, + -4.0,13.5,-5.4,10.4,-11.5,12.4,-17.1,21.0,-12.9,21.3,-12.0,25.9, + -8.7,25.9,-8.7,27.4,-4.9,25.0,-6.5,25.0,-5.5,15.5,-12.2,14.6, + -14.6,16.6,-16.5,16.1,-17.1,21.0,-117.1,32.5,-106.5,31.8,-103.9,29.3, + -101.7,29.8,-97.1,25.9,-97.9,22.4,-95.9,18.8,-91.4,18.9,-90.3,21.0, + -87.1,21.5,-87.8,18.3,-91.0,17.8,-90.5,16.1,-92.2,14.5,-103.5,18.3, + -113.1,31.2,-114.9,31.4,-109.9,22.8,-115.1,27.7,-114.2,28.6,-117.1,32.5, + 26.6,48.2,30.0,46.4,28.2,45.5,26.6,48.2,87.8,49.3,92.2,50.8, + 97.3,49.7,98.9,52.0,108.5,49.3,116.7,49.9,115.7,47.7,119.8,47.0, + 105.0,41.6,96.3,42.7,90.9,45.3,91.0,46.9,87.8,49.3,20.1,42.6, + 18.5,42.5,20.1,42.6,-2.2,35.2,-1.3,32.3,-8.7,28.8,-8.8,27.1, + -11.4,26.9,-14.8,21.5,-17.0,21.4,-14.4,26.3,-9.6,29.9,-8.7,33.2, + -5.9,35.8,-2.2,35.2,34.6,-11.5,40.3,-10.3,40.8,-14.7,34.8,-19.8, + 35.5,-24.1,32.1,-26.7,31.2,-22.3,32.8,-16.7,30.2,-14.8,33.2,-14.0, + 35.0,-16.8,34.6,-11.5,100.1,20.4,97.4,18.4,99.6,11.9,98.6,9.9, + 97.2,16.9,94.2,16.0,92.3,21.5,97.3,28.3,98.7,27.5,97.6,23.9, + 101.2,21.8,100.1,20.4,32.7,35.1,34.6,35.7,32.7,35.1,19.9,-24.8, + 19.9,-28.5,16.3,-28.6,11.7,-17.3,25.1,-17.6,20.9,-18.3,19.9,-24.8, + 88.1,27.9,87.2,26.4,80.1,28.8,81.5,30.4,88.1,27.9,6.9,53.5, + 6.2,50.8,3.3,51.3,6.9,53.5,165.8,-21.1,167.1,-22.2,164.0,-20.1, + 165.8,-21.1,176.9,-40.1,174.7,-41.3,174.7,-37.4,172.6,-34.5,176.0,-37.6, + 178.5,-37.7,176.9,-40.1,169.7,-43.6,172.8,-40.5,174.2,-41.3,170.6,-45.9, + 166.7,-46.2,169.7,-43.6,-83.7,10.9,-87.7,12.9,-83.1,15.0,-83.7,10.9, + 14.9,22.9,15.9,20.4,13.5,14.4,14.2,12.5,5.4,13.9,3.6,11.7, + 1.0,12.9,0.4,14.9,3.6,15.6,4.3,19.2,12.0,23.5,14.9,22.9, + 2.7,6.3,4.4,13.7,13.1,13.6,14.6,12.1,11.7,7.0,8.5,4.8, + 5.9,4.3,2.7,6.3,130.6,42.4,127.5,39.8,128.2,38.4,124.7,38.1, + 125.1,40.6,130.6,42.4,22.4,42.3,23.0,41.3,20.6,41.1,22.4,42.3, + 15.1,79.7,21.5,79.0,15.9,76.8,10.4,79.7,15.1,79.7,31.1,69.6, + 18.0,68.6,12.6,64.1,11.0,58.9,5.7,58.6,5.0,62.0,19.2,69.8, + 28.2,71.2,31.1,69.6,27.4,80.1,17.4,80.3,27.4,80.1,24.7,77.9, + 20.7,77.7,24.7,77.9,55.2,22.7,56.4,24.9,59.8,22.3,57.7,18.9, + 53.1,16.7,52.0,19.0,55.0,20.0,55.2,22.7,77.8,35.5,73.7,34.3, + 75.3,32.3,69.5,26.9,71.0,24.4,61.5,25.1,63.3,26.8,60.9,29.8, + 66.3,29.9,71.8,36.5,75.2,37.1,77.8,35.5,-77.4,8.7,-77.9,7.2, + -79.1,9.0,-80.9,7.2,-82.9,9.5,-77.4,8.7,141.0,-2.6,147.6,-6.1, + 147.2,-7.4,150.7,-10.6,144.7,-7.6,141.0,-9.1,141.0,-2.6,152.6,-3.7, + 152.8,-4.8,150.7,-2.7,152.6,-3.7,151.3,-5.8,148.3,-5.7,152.1,-4.1, + 151.3,-5.8,154.8,-5.3,155.9,-6.8,154.8,-5.3,-58.2,-20.2,-57.9,-22.1, + -54.3,-24.0,-55.7,-27.4,-58.6,-27.1,-57.8,-25.2,-62.7,-22.2,-61.8,-19.6, + -58.2,-20.2,-69.9,-4.3,-72.9,-5.3,-74.0,-7.5,-68.7,-12.6,-70.4,-18.3, + -76.0,-14.6,-81.4,-4.7,-80.3,-3.4,-78.6,-4.5,-75.1,-0.1,-73.1,-2.3, + -70.0,-2.7,-69.9,-4.3,122.6,10.0,124.1,11.2,123.0,9.0,122.6,10.0, + 126.4,8.4,125.4,5.6,123.6,7.8,121.9,7.2,125.4,9.8,126.4,8.4, + 118.5,9.3,117.2,8.4,119.5,11.4,118.5,9.3,122.3,18.2,121.7,14.3, + 124.1,12.5,119.9,15.4,120.7,18.5,122.3,18.2,125.5,12.2,124.8,10.1, + 124.3,12.6,125.5,12.2,23.5,53.9,24.0,50.7,22.8,49.0,16.2,50.4, + 14.1,53.0,17.6,54.9,23.5,53.9,-9.0,41.9,-6.4,41.4,-7.9,36.8, + -9.5,38.7,-9.0,41.9,-66.3,18.5,-67.2,17.9,-66.3,18.5,50.8,24.8, + 51.3,26.1,50.8,24.8,28.2,45.5,29.6,45.3,28.6,43.7,22.9,43.8, + 20.2,46.1,26.6,48.2,28.2,45.5,49.1,46.4,46.7,44.6,47.8,41.2, + 36.7,45.2,40.1,49.6,31.8,52.1,32.7,53.4,30.9,55.6,27.3,57.5, + 29.1,60.0,28.1,60.5,31.5,62.9,30.0,63.6,28.6,69.1,32.1,69.9, + 41.1,67.5,38.4,66.0,33.2,66.6,37.0,63.8,37.2,65.1,43.9,66.1, + 43.5,68.6,46.3,68.3,46.3,66.7,53.7,68.9,59.9,68.3,60.6,69.9, + 68.5,68.1,66.7,71.0,69.9,73.0,72.8,72.2,71.8,71.4,73.7,68.4, + 71.3,66.3,72.4,66.2,75.1,67.8,73.1,71.4,74.7,72.8,76.4,71.2, + 81.5,71.8,80.5,73.6,104.4,77.7,114.1,75.8,109.4,74.2,127.0,73.6, + 131.3,70.8,139.9,71.5,139.1,72.4,140.5,72.8,159.0,70.9,160.9,69.4, + 180.0,69.0,180.0,65.0,177.4,64.6,179.2,62.3,170.3,59.9,163.5,59.9, + 162.0,58.2,163.2,57.6,162.1,54.9,156.8,51.0,155.9,56.8,164.5,62.6, + 160.1,60.5,156.7,61.4,154.2,59.8,155.0,59.1,142.2,59.0,135.1,54.7, + 141.3,53.1,140.1,48.4,134.9,43.4,130.8,42.2,131.0,45.0,133.1,45.1, + 135.0,48.5,131.0,47.8,123.6,53.5,120.2,52.8,117.9,49.5,108.5,49.3, + 98.9,52.0,97.3,49.7,92.2,50.8,87.4,49.2,80.0,50.9,76.9,54.5, + 73.4,53.5,69.1,55.4,61.4,54.0,60.0,52.0,61.3,50.8,50.8,51.7, + 47.5,50.5,46.5,48.4,49.1,46.4,93.8,81.0,100.2,79.8,97.8,78.8, + 91.2,80.3,93.8,81.0,102.8,79.3,105.4,78.7,99.4,77.9,102.8,79.3, + 138.8,76.1,145.1,75.6,137.0,75.3,138.8,76.1,148.2,75.3,150.7,75.1, + 146.1,75.2,148.2,75.3,139.9,73.4,143.6,73.2,139.9,73.4,44.8,80.6, + 51.5,80.7,44.8,80.6,22.7,54.3,19.7,54.4,22.7,54.3,53.5,73.7, + 61.2,76.3,68.9,76.5,58.5,74.3,55.4,72.4,57.5,70.7,51.6,71.5, + 53.5,73.7,142.9,53.7,144.7,49.0,143.2,49.3,143.5,46.1,142.1,46.0, + 141.7,53.3,142.9,53.7,-174.9,67.2,-169.9,66.0,-173.0,64.3,-178.7,66.1, + -180.0,65.0,-180.0,69.0,-174.9,67.2,-178.7,70.9,-180.0,71.5,-177.6,71.3, + -178.7,70.9,33.4,46.0,36.5,45.5,33.9,44.4,32.5,45.3,33.4,46.0, + 30.4,-1.1,29.0,-2.8,30.4,-1.1,30.8,3.5,23.9,8.6,25.8,10.4, + 31.4,9.8,33.2,12.2,33.0,7.8,35.3,5.5,30.8,3.5,35.0,29.4, + 39.2,32.2,47.5,29.0,52.0,23.0,55.2,22.7,55.0,20.0,47.0,16.9, + 43.4,17.6,42.8,16.3,35.0,29.4,-16.7,13.6,-17.6,14.7,-14.6,16.6, + -11.5,12.4,-16.7,12.4,-13.8,13.5,-16.7,13.6,18.8,45.9,22.7,44.6, + 22.5,42.5,19.2,43.5,18.8,45.9,-13.2,8.9,-11.1,10.0,-10.2,8.4, + -11.4,6.8,-13.2,8.9,22.6,49.1,16.9,48.5,22.6,49.1,13.8,46.5, + 16.6,46.5,15.3,45.5,13.8,46.5,159.6,-8.0,158.2,-7.4,159.6,-8.0, + 41.6,-1.7,41.0,2.8,45.0,5.0,48.9,9.5,48.9,11.4,51.1,12.0, + 48.6,5.3,41.6,-1.7,48.9,11.4,47.8,8.0,42.6,10.6,48.9,11.4, + 16.3,-28.6,19.9,-28.5,19.9,-24.8,21.6,-26.7,25.7,-25.5,29.4,-22.1, + 31.2,-22.3,31.9,-24.4,30.7,-26.7,32.8,-26.7,28.2,-32.8,20.1,-34.8, + 18.4,-34.1,16.3,-28.6,126.2,37.7,128.3,38.6,129.1,35.1,126.5,34.4, + 126.2,37.7,-7.5,37.1,-6.4,41.4,-9.4,43.0,3.0,42.5,-2.1,36.7, + -7.5,37.1,81.8,7.5,80.3,6.0,80.1,9.8,81.8,7.5,24.6,8.2, + 21.9,12.6,25.0,22.0,36.9,22.0,38.4,18.0,34.0,8.7,32.7,12.2, + 31.4,9.8,25.1,10.3,24.6,8.2,-54.5,2.3,-56.5,1.9,-57.6,3.3, + -57.1,6.0,-54.0,5.8,-54.5,2.3,11.0,58.9,12.6,61.3,11.9,63.1, + 16.8,68.0,20.6,69.1,23.5,67.9,23.9,66.0,17.8,62.7,17.1,61.3, + 18.8,60.1,15.9,56.1,12.9,55.4,11.0,58.9,9.6,47.5,10.4,46.5, + 6.0,46.3,9.6,47.5,35.7,32.7,36.7,36.8,42.3,37.2,41.0,34.4, + 35.7,32.7,121.8,24.4,120.7,22.0,120.1,23.6,121.8,24.4,67.8,37.1, + 67.7,39.6,70.7,41.0,69.5,39.5,73.7,39.4,75.0,37.4,71.8,36.7, + 70.8,38.5,67.8,37.1,33.9,-0.9,39.2,-4.7,39.5,-10.9,34.6,-11.5, + 29.6,-6.5,30.4,-1.1,33.9,-0.9,105.2,14.3,103.0,14.2,102.6,12.2, + 100.1,13.4,99.2,9.2,102.1,6.2,101.2,5.7,98.2,8.4,99.6,11.9, + 97.4,18.4,100.1,20.4,101.1,17.5,104.7,17.4,105.2,14.3,125.0,-8.9, + 127.3,-8.4,125.0,-8.9,0.9,11.0,1.1,5.9,0.9,11.0,9.5,30.3, + 7.5,34.1,9.5,37.3,11.0,37.1,10.1,34.3,11.5,33.1,9.5,30.3, + 44.8,37.2,29.7,36.1,27.6,36.7,26.2,39.5,33.5,42.0,42.6,41.6, + 44.8,39.7,44.8,37.2,26.1,41.8,29.0,41.3,26.4,40.2,26.1,41.8, + 52.5,41.8,57.1,41.3,58.6,42.8,66.5,37.4,62.2,35.3,57.3,38.0, + 53.9,37.2,52.7,40.0,54.7,41.0,52.5,41.8,33.9,-0.9,29.6,-1.3, + 31.2,3.8,34.5,3.6,33.9,-0.9,31.8,52.1,40.1,49.6,39.7,47.9, + 35.0,45.7,31.7,46.7,28.7,45.3,30.0,46.4,28.7,48.1,22.1,48.4, + 23.5,51.6,31.8,52.1,51.6,24.2,56.3,25.7,55.0,22.5,51.6,24.2, + -6.2,53.9,-7.6,55.1,-6.2,53.9,-3.1,53.4,-6.1,56.8,-5.0,58.6, + -2.0,57.7,-3.1,56.0,1.7,52.7,1.4,51.3,-5.8,50.2,-3.4,51.4, + -5.3,52.0,-4.6,53.5,-3.1,53.4,-122.8,49.0,-88.4,48.3,-82.6,45.3, + -82.7,41.7,-71.5,45.0,-69.2,47.4,-67.0,44.8,-70.1,43.7,-70.0,41.6, + -75.5,39.5,-75.9,37.2,-76.3,39.2,-77.0,38.2,-75.7,35.6,-81.3,31.4, + -80.4,25.2,-83.7,29.9,-86.4,30.4,-94.7,29.5,-97.5,25.8,-101.0,29.4, + -103.9,29.3,-106.5,31.8,-117.1,32.5,-120.6,34.6,-124.4,40.3,-124.7,48.2, + -122.6,47.1,-122.8,49.0,-166.5,60.4,-165.6,59.9,-167.5,60.2,-166.5,60.4, + -153.2,58.0,-152.1,57.6,-154.5,57.0,-153.2,58.0,-141.0,69.7,-141.0,60.3, + -137.5,58.9,-135.5,59.8,-130.0,55.9,-130.5,54.8,-134.1,58.1,-139.9,59.5, + -147.1,60.9,-151.7,59.2,-150.6,61.3,-158.4,56.0,-164.9,54.6,-157.0,58.9, + -162.0,58.7,-165.3,60.5,-165.7,62.1,-160.8,64.8,-168.1,65.7,-161.7,66.1, + -166.2,68.9,-156.6,71.4,-141.0,69.7,-171.7,63.8,-168.7,63.3,-171.7,63.8, + -57.6,-30.2,-53.8,-32.0,-53.8,-34.4,-58.4,-33.9,-57.6,-30.2,56.0,41.3, + 55.9,45.0,58.5,45.6,62.0,43.5,64.9,43.7,68.3,40.7,71.0,42.3, + 73.1,40.9,67.7,39.6,67.8,37.1,58.6,42.8,56.0,41.3,-60.7,5.2, + -64.8,4.1,-63.4,2.2,-66.3,0.7,-67.8,2.8,-67.3,6.1,-72.0,7.0, + -72.9,10.5,-71.3,11.8,-71.3,9.1,-69.9,12.2,-68.2,10.6,-61.9,10.7, + -59.8,8.4,-60.7,5.2,104.3,10.5,107.5,12.3,107.6,15.2,102.2,22.5, + 105.3,23.4,108.1,21.6,105.7,19.1,108.9,15.3,109.2,11.7,105.2,8.6, + 104.3,10.5,-8.7,27.7,-8.7,25.9,-12.0,25.9,-12.9,21.3,-17.1,21.0, + -14.8,21.5,-11.4,26.9,-8.7,27.7,52.0,19.0,52.2,15.6,43.5,12.6, + 43.4,17.6,47.0,16.9,52.0,19.0,30.7,-8.3,33.2,-9.7,33.2,-14.0, + 27.0,-17.9,23.2,-17.5,21.9,-12.9,24.0,-12.9,23.9,-10.9,29.7,-13.3, + 28.4,-9.2,30.7,-8.3,31.2,-22.3,28.0,-21.5,25.3,-17.7,30.3,-15.5, + 32.8,-16.7,31.2,-22.3, +}; + +struct Span { + int at, length; +}; + +const Span RINGS[] = { + {0,20},{20,8},{28,22},{50,24},{74,10},{84,6},{90,10},{100,6}, + {106,6},{112,6},{118,6},{124,122},{246,8},{254,48},{302,8},{310,10}, + {320,48},{368,12},{380,12},{392,6},{398,16},{414,14},{428,8},{436,8}, + {444,8},{452,6},{458,22},{480,8},{488,14},{502,70},{572,10},{582,16}, + {598,8},{606,12},{618,14},{632,116},{748,6},{754,8},{762,6},{768,6}, + {774,8},{782,8},{790,8},{798,8},{806,12},{818,10},{828,42},{870,10}, + {880,6},{886,6},{892,6},{898,6},{904,14},{918,10},{928,20},{948,14}, + {962,6},{968,8},{976,10},{986,26},{1012,6},{1018,6},{1024,6},{1030,6}, + {1036,16},{1052,18},{1070,14},{1084,34},{1118,8},{1126,76},{1202,26},{1228,16}, + {1244,8},{1252,10},{1262,12},{1274,12},{1286,6},{1292,8},{1300,30},{1330,8}, + {1338,8},{1346,6},{1352,8},{1360,10},{1370,14},{1384,8},{1392,8},{1400,8}, + {1408,8},{1416,6},{1422,18},{1440,6},{1446,20},{1466,8},{1474,10},{1484,24}, + {1508,6},{1514,12},{1526,6},{1532,8},{1540,20},{1560,10},{1570,6},{1576,14}, + {1590,74},{1664,14},{1678,14},{1692,8},{1700,14},{1714,8},{1722,10},{1732,10}, + {1742,18},{1760,52},{1812,24},{1836,6},{1842,18},{1860,8},{1868,6},{1874,22}, + {1896,6},{1902,8},{1910,6},{1916,8},{1924,14},{1938,26},{1964,12},{1976,10}, + {1986,8},{1994,22},{2016,8},{2024,8},{2032,6},{2038,22},{2060,10},{2070,6}, + {2076,16},{2092,50},{2142,14},{2156,8},{2164,8},{2172,14},{2186,18},{2204,12}, + {2216,6},{2222,8},{2230,10},{2240,18},{2258,8},{2266,14},{2280,10},{2290,10}, + {2300,16},{2316,18},{2334,24},{2358,42},{2400,8},{2408,26},{2434,6},{2440,24}, + {2464,24},{2488,24},{2512,6},{2518,14},{2532,10},{2542,8},{2550,8},{2558,14}, + {2572,12},{2584,8},{2592,24},{2616,16},{2632,12},{2644,8},{2652,10},{2662,18}, + {2680,6},{2686,6},{2692,16},{2708,24},{2732,12},{2744,14},{2758,8},{2766,8}, + {2774,6},{2780,18},{2798,26},{2824,8},{2832,12},{2844,8},{2852,12},{2864,8}, + {2872,14},{2886,10},{2896,6},{2902,6},{2908,14},{2922,192},{3114,10},{3124,8}, + {3132,8},{3140,8},{3148,6},{3154,6},{3160,6},{3166,16},{3182,14},{3196,14}, + {3210,8},{3218,10},{3228,6},{3234,16},{3250,20},{3270,14},{3284,10},{3294,10}, + {3304,6},{3310,8},{3318,6},{3324,16},{3340,8},{3348,28},{3376,10},{3386,12}, + {3398,8},{3406,20},{3426,12},{3438,26},{3464,8},{3472,10},{3482,8},{3490,18}, + {3508,14},{3522,28},{3550,6},{3556,6},{3562,14},{3576,16},{3592,8},{3600,20}, + {3620,10},{3630,22},{3652,8},{3660,6},{3666,24},{3690,58},{3748,8},{3756,8}, + {3764,46},{3810,6},{3816,10},{3826,24},{3850,30},{3880,22},{3902,16},{3918,12}, + {3930,22},{3952,12}, +}; + +const Span COUNTRIES[] = { + {0,1},{1,1},{2,1},{3,1},{4,8},{12,2},{14,1},{15,2}, + {17,1},{18,1},{19,1},{20,1},{21,1},{22,1},{23,1},{24,1}, + {25,1},{26,1},{27,1},{28,1},{29,1},{30,1},{31,1},{32,1}, + {33,1},{34,1},{35,29},{64,1},{65,1},{66,2},{68,2},{70,1}, + {71,1},{72,1},{73,1},{74,1},{75,1},{76,1},{77,1},{78,1}, + {79,2},{81,1},{82,1},{83,1},{84,1},{85,1},{86,1},{87,1}, + {88,1},{89,1},{90,1},{91,1},{92,1},{93,1},{94,3},{97,1}, + {98,1},{99,1},{100,1},{101,1},{102,2},{104,1},{105,1},{106,1}, + {107,1},{108,1},{109,1},{110,1},{111,1},{112,1},{113,1},{114,11}, + {125,1},{126,1},{127,1},{128,1},{129,3},{132,1},{133,3},{136,1}, + {137,1},{138,1},{139,1},{140,1},{141,1},{142,1},{143,1},{144,1}, + {145,1},{146,1},{147,1},{148,1},{149,1},{150,1},{151,2},{153,1}, + {154,1},{155,1},{156,1},{157,1},{158,1},{159,1},{160,1},{161,1}, + {162,1},{163,1},{164,1},{165,1},{166,1},{167,2},{169,1},{170,1}, + {171,1},{172,1},{173,1},{174,4},{178,1},{179,1},{180,1},{181,4}, + {185,1},{186,1},{187,5},{192,1},{193,1},{194,1},{195,1},{196,1}, + {197,13},{210,1},{211,1},{212,1},{213,1},{214,1},{215,1},{216,1}, + {217,1},{218,1},{219,1},{220,1},{221,1},{222,1},{223,1},{224,1}, + {225,1},{226,1},{227,1},{228,1},{229,1},{230,1},{231,1},{232,1}, + {233,1},{234,1},{235,1},{236,1},{237,2},{239,1},{240,1},{241,1}, + {242,1},{243,2},{245,5},{250,1},{251,1},{252,1},{253,1},{254,1}, + {255,1},{256,1},{257,1}, +}; + +const char *const NAMES[] = { + "Afghanistan","Albania","Algeria","Angola", + "Antarctica","Argentina","Armenia","Australia", + "Austria","Azerbaijan","Bahamas","Bangladesh", + "Belarus","Belgium","Belize","Benin", + "Bhutan","Bolivia","Bosnia and Herz.","Botswana", + "Brazil","Bulgaria","Burkina Faso","Burundi", + "Cambodia","Cameroon","Canada","Central African Rep.", + "Chad","Chile","China","Colombia", + "Congo","Costa Rica","Côte d'Ivoire","Croatia", + "Cuba","Cyprus","Czechia","Dem. Rep. Congo", + "Denmark","Djibouti","Dominican Rep.","Ecuador", + "Egypt","El Salvador","Eq. Guinea","Eritrea", + "Estonia","eSwatini","Ethiopia","Falkland Is.", + "Finland","Fr. S. Antarctic Lands","France","Gabon", + "Gambia","Georgia","Germany","Ghana", + "Greece","Greenland","Guatemala","Guinea", + "Guinea-Bissau","Guyana","Haiti","Honduras", + "Hungary","Iceland","India","Indonesia", + "Iran","Iraq","Ireland","Israel", + "Italy","Jamaica","Japan","Jordan", + "Kazakhstan","Kenya","Kosovo","Kuwait", + "Kyrgyzstan","Laos","Latvia","Lebanon", + "Lesotho","Liberia","Libya","Lithuania", + "Madagascar","Malawi","Malaysia","Mali", + "Mauritania","Mexico","Moldova","Mongolia", + "Montenegro","Morocco","Mozambique","Myanmar", + "N. Cyprus","Namibia","Nepal","Netherlands", + "New Caledonia","New Zealand","Nicaragua","Niger", + "Nigeria","North Korea","North Macedonia","Norway", + "Oman","Pakistan","Panama","Papua New Guinea", + "Paraguay","Peru","Philippines","Poland", + "Portugal","Puerto Rico","Qatar","Romania", + "Russia","Rwanda","S. Sudan","Saudi Arabia", + "Senegal","Serbia","Sierra Leone","Slovakia", + "Slovenia","Solomon Is.","Somalia","Somaliland", + "South Africa","South Korea","Spain","Sri Lanka", + "Sudan","Suriname","Sweden","Switzerland", + "Syria","Taiwan","Tajikistan","Tanzania", + "Thailand","Timor-Leste","Togo","Tunisia", + "Turkey","Turkmenistan","Uganda","Ukraine", + "United Arab Emirates","United Kingdom","United States of America","Uruguay", + "Uzbekistan","Venezuela","Vietnam","W. Sahara", + "Yemen","Zambia","Zimbabwe", +}; + +const char *const ISO[] = { + "AF","AL","DZ","AO","AQ","AR","AM","AU","AT","AZ","BS","BD", + "BY","BE","BZ","BJ","BT","BO","BA","BW","BR","BG","BF","BI", + "KH","CM","CA","CF","TD","CL","CN","CO","CG","CR","CI","HR", + "CU","CY","CZ","CD","DK","DJ","DO","EC","EG","SV","GQ","ER", + "EE","SZ","ET","FK","FI","TF","FR","GA","GM","GE","DE","GH", + "GR","GL","GT","GN","GW","GY","HT","HN","HU","IS","IN","ID", + "IR","IQ","IE","IL","IT","JM","JP","JO","KZ","KE","XK","KW", + "KG","LA","LV","LB","LS","LR","LY","LT","MG","MW","MY","ML", + "MR","MX","MD","MN","ME","MA","MZ","MM","-99","NA","NP","NL", + "NC","NZ","NI","NE","NG","KP","MK","NO","OM","PK","PA","PG", + "PY","PE","PH","PL","PT","PR","QA","RO","RU","RW","SS","SA", + "SN","RS","SL","SK","SI","SB","SO","-99","ZA","KR","ES","LK", + "SD","SR","SE","CH","SY","TW","TJ","TZ","TH","TL","TG","TN", + "TR","TM","UG","UA","AE","GB","US","UY","UZ","VE","VN","EH", + "YE","ZM","ZW", +}; + +} // namespace + +/// Built once, on first use: the pool above is plain static data, and this +/// turns it into the shape the rest of the code wants without a static +/// initialiser that has to run before main. +const std::vector &world_countries() { + static const std::vector countries = [] { + std::vector out; + out.reserve(171); + for (std::size_t i = 0; i < 171; i++) { + CountryOutline c; + c.name = NAMES[i]; + c.iso = ISO[i]; + const Span &cs = COUNTRIES[i]; + for (int r = 0; r < cs.length; r++) { + const Span &rs = RINGS[cs.at + r]; + c.rings.emplace_back(POOL + rs.at, POOL + rs.at + rs.length); + } + out.push_back(std::move(c)); + } + return out; + }(); + return countries; +} + +} // namespace hqtui diff --git a/ports/cpp/tests/conformance_widgets.cpp b/ports/cpp/tests/conformance_widgets.cpp index d76bbc9..6557701 100644 --- a/ports/cpp/tests/conformance_widgets.cpp +++ b/ports/cpp/tests/conformance_widgets.cpp @@ -251,6 +251,23 @@ bool draw_scene(const std::string &name, Surface s) { draw_shadow(s, Rect{2, 1, 6, 2}, sh); return true; } + if (name == "world") { + draw_world_map(s, {}); + return true; + } + if (name == "world-zoom") { + WorldMap m; + m.x = Bounds{112, 156}; + m.y = Bounds{24, 50}; + draw_world_map(s, m); + return true; + } + if (name == "world-highlight") { + WorldMap m; + m.highlight = {"Brazil", "JP"}; + draw_world_map(s, m); + return true; + } if (name == "badge") { { Badge badge; diff --git a/ports/go/conformance_widgets_test.go b/ports/go/conformance_widgets_test.go index ff59e13..cc9c083 100644 --- a/ports/go/conformance_widgets_test.go +++ b/ports/go/conformance_widgets_test.go @@ -129,6 +129,13 @@ func drawWidgetScene(t *testing.T, name string, s Surface) { solid := RGB(0x10, 0x14, 0x18) DrawShadow(s, Rect{X: 2, Y: 1, Width: 6, Height: 2}, ShadowOptions{OffsetX: 1, OffsetY: 1, Color: &solid}) + case "world": + DrawWorldMap(s, WorldMapOptions{}) + case "world-zoom": + zx, zy := Bounds{112, 156}, Bounds{24, 50} + DrawWorldMap(s, WorldMapOptions{X: &zx, Y: &zy}) + case "world-highlight": + DrawWorldMap(s, WorldMapOptions{Highlight: []string{"Brazil", "JP"}}) case "badge": DrawBadge(s, BadgeOptions{Text: "LIVE"}) case "badge-outline": diff --git a/ports/go/examples/world-probe/main.go b/ports/go/examples/world-probe/main.go new file mode 100644 index 0000000..784ec61 --- /dev/null +++ b/ports/go/examples/world-probe/main.go @@ -0,0 +1,56 @@ +// Prints what the country lookup answers for a fixed set of points. +// +// The same probe exists for every port, so "the ports agree about the world" is +// a diff rather than a hope. +package main + +import ( + "fmt" + + hqtui "github.com/profullstack/hqtui/ports/go" +) + +func main() { + places := []struct { + name string + lon, lat float64 + }{ + {"Paris", 2.35, 48.86}, + {"Tokyo", 139.7, 35.7}, + {"Cairo", 31.2, 30.0}, + {"Brasilia", -47.9, -15.8}, + {"Canberra", 149.1, -35.3}, + {"Denver", -105.0, 39.7}, + {"Moscow", 37.6, 55.75}, + {"Delhi", 77.2, 28.6}, + {"Nairobi", 36.8, -1.3}, + {"Pacific", -140.0, 0.0}, + {"Atlantic", -30.0, 0.0}, + {"SouthernOcean", 80.0, -40.0}, + {"NorthPacific", -150.0, 40.0}, + } + for _, p := range places { + name := "-" + if c := hqtui.CountryAt(p.lon, p.lat); c != nil { + name = c.Name + } + fmt.Printf("%s %s\n", p.name, name) + } + + // The cell path, which has to agree with what the canvas drew. + cells := [][2]int{{173, 28}, {74, 2}, {20, 25}, {88, 7}} + for _, cell := range cells { + name := "-" + if c := hqtui.CountryAtCell(cell[0], cell[1], 200, 50, hqtui.WorldMapOptions{}); c != nil { + name = c.Name + } + fmt.Printf("cell:%d,%d %s\n", cell[0], cell[1], name) + } + + // And the projection itself, so a drift shows up as a number rather than as + // a country that happens to still be right. + for _, cell := range [][2]int{{0, 0}, {99, 25}, {50, 13}} { + lon, lat, _ := hqtui.DegreesAt(cell[0], cell[1], 100, 26, hqtui.WorldX, hqtui.WorldY) + fmt.Printf("degrees:%d,%d %.4f %.4f\n", cell[0], cell[1], lon, lat) + } +} diff --git a/ports/go/ui.go b/ports/go/ui.go index da9fc56..d59d074 100644 --- a/ports/go/ui.go +++ b/ports/go/ui.go @@ -523,6 +523,19 @@ func (c *Container) Shapes(o CanvasOptions, layout ...Layout) *Container { return c.add(c.filling(firstLayout(layout)), func(s Surface) { DrawCanvas(s, o) }) } +// WorldMap draws a world map and reports the country under whatever is clicked. +// +// The click is answered by turning the cell back into degrees and testing it +// against the outlines, so the answer is the country actually under the cursor. +// Bounding boxes would be cheaper and wrong: Russia's covers most of the +// northern hemisphere and Chile's covers Argentina. +func (c *Container) WorldMap(o WorldMapOptions, h ScrollHandlers, layout ...Layout) *Container { + return c.add(c.filling(firstLayout(layout)), func(s Surface) { + DrawWorldMap(s, o) + c.attachScroll(s, h, 0) + }) +} + func (c *Container) Sparkline(o SparklineWidgetOptions, layout ...Layout) *Container { return c.add(c.leaf(firstLayout(layout), 1), func(s Surface) { DrawSparkline(s, o) }) } diff --git a/ports/go/widgets_world.go b/ports/go/widgets_world.go new file mode 100644 index 0000000..c367f53 --- /dev/null +++ b/ports/go/widgets_world.go @@ -0,0 +1,71 @@ +package hqtui + +// A world map you can click. +// +// The drawing is the canvas doing what it already does — polylines in the +// caller's own coordinates, which for a map are degrees. What this adds is the +// other direction: turning a click back into a country. + +type WorldMapOptions struct { + // X and Y are the window on the globe. Nil means all of it. + X *Bounds + Y *Bounds + // Color is the coastline colour. + Color *Color + // Highlight names countries to pick out, by name or ISO code. + Highlight []string + HighlightColor *Color + Background *Color + Grid bool +} + +func (o WorldMapOptions) window() (Bounds, Bounds) { + x, y := WorldX, WorldY + if o.X != nil { + x = *o.X + } + if o.Y != nil { + y = *o.Y + } + return x, y +} + +func DrawWorldMap(s Surface, o WorldMapOptions) { + if s.IsEmpty() { + return + } + theme := s.Theme + color := theme.Border + if o.Color != nil { + color = *o.Color + } + highlight := theme.Accent + if o.HighlightColor != nil { + highlight = *o.HighlightColor + } + x, y := o.window() + DrawCanvas(s, CanvasOptions{ + Shapes: WorldShapes(WorldShapeOptions{ + Color: &color, + Highlight: o.Highlight, + HighlightColor: &highlight, + }), + X: &x, + Y: &y, + Background: o.Background, + Grid: o.Grid, + }) +} + +// CountryAtCell is the country under a cell of a map drawn with these bounds. +// +// Exposed so a caller can answer a hover as well as a click, and so the +// arithmetic that has to agree with the drawing lives in one place. +func CountryAtCell(column, row, width, height int, o WorldMapOptions) *CountryOutline { + x, y := o.window() + lon, lat, ok := DegreesAt(column, row, width, height, x, y) + if !ok { + return nil + } + return CountryAt(lon, lat) +} diff --git a/ports/go/world.go b/ports/go/world.go new file mode 100644 index 0000000..a3e5d38 --- /dev/null +++ b/ports/go/world.go @@ -0,0 +1,178 @@ +package hqtui + +import ( + "math" + "strings" +) + +// The world, as shapes for the canvas, and the lookup that makes it clickable. +// +// The canvas already draws in the caller's own coordinates, and longitude and +// latitude are just another pair of axes — so a map is a list of polylines in +// degrees, and nothing here needs a projection of its own beyond deciding which +// window on the globe to show. +// +// The interesting half is the other direction. A click arrives as a terminal +// cell, and a country is a polygon, so answering "what did they click" means +// turning the cell back into degrees and testing it against the outlines. Doing +// it that way rather than with bounding boxes is what makes the answer right: +// Russia's bounding box covers most of the northern hemisphere, and Chile's +// covers Argentina. + +// WorldX and WorldY are the whole globe, which is what a map shows unless told +// otherwise. +var ( + WorldX = Bounds{Min: -180, Max: 180} + WorldY = Bounds{Min: -90, Max: 90} +) + +type WorldShapeOptions struct { + // Color is for countries with nothing special about them. + Color *Color + // Highlight names countries to pick out, by name or ISO code. + Highlight []string + HighlightColor *Color +} + +// countryMatches matches on either the name or the ISO code, case-insensitively. +func countryMatches(c CountryOutline, keys []string) bool { + for _, key := range keys { + if key == "" { + continue + } + if strings.EqualFold(c.Name, key) { + return true + } + if c.ISO != "" && strings.EqualFold(c.ISO, key) { + return true + } + } + return false +} + +// WorldShapes is the world as canvas shapes, one polyline per landmass. +// +// Polylines rather than scattered points: the outlines are closed rings, so +// joining them draws a coastline instead of a dotted suggestion of one, and it +// reads at a fraction of the resolution dots would need. +func WorldShapes(o WorldShapeOptions) []Shape { + shapes := make([]Shape, 0, 300) + for _, country := range WorldCountries { + color := o.Color + if len(o.Highlight) > 0 && countryMatches(country, o.Highlight) && o.HighlightColor != nil { + color = o.HighlightColor + } + for _, ring := range country.Rings { + points := make([]Point, 0, len(ring)/2+1) + for i := 0; i+1 < len(ring); i += 2 { + points = append(points, Point{X: ring[i], Y: ring[i+1]}) + } + // Closed: the last point joins the first, or every country has a gap + // in its coastline where the ring started. + if len(points) > 0 { + points = append(points, points[0]) + } + shapes = append(shapes, Shape{Kind: ShapePolyline, Points: points, Color: color}) + } + } + return shapes +} + +// insideRing reports whether a point is inside a ring, by ray casting. +// +// The ring is a flat list of interleaved coordinates, so this walks it two at a +// time rather than allocating a pair per vertex — it runs once per country per +// click, and there are a couple of thousand vertices. +func insideRing(ring []float64, lon, lat float64) bool { + inside := false + n := len(ring) / 2 + if n == 0 { + return false + } + j := n - 1 + for i := 0; i < n; i++ { + xi, yi := ring[i*2], ring[i*2+1] + xj, yj := ring[j*2], ring[j*2+1] + if (yi > lat) != (yj > lat) && lon < (xj-xi)*(lat-yi)/(yj-yi)+xi { + inside = !inside + } + j = i + } + return inside +} + +// CountryAt is the country containing a point, or nil for open water. +// +// Where outlines overlap — and at this resolution simplified borders do overlap +// — the first match wins, which is stable because the data is sorted by name. +func CountryAt(lon, lat float64) *CountryOutline { + if math.IsNaN(lon) || math.IsNaN(lat) || math.IsInf(lon, 0) || math.IsInf(lat, 0) { + return nil + } + for i := range WorldCountries { + for _, ring := range WorldCountries[i].Rings { + if insideRing(ring, lon, lat) { + return &WorldCountries[i] + } + } + } + return nil +} + +// FindCountry looks a country up by name or ISO code. +func FindCountry(key string) *CountryOutline { + keys := []string{key} + for i := range WorldCountries { + if countryMatches(WorldCountries[i], keys) { + return &WorldCountries[i] + } + } + return nil +} + +// CountryBounds is the window a country fills, with a little room around it. +// +// For zooming a map to a country: the bounding box alone puts the coastline +// flat against the edge of the panel, which reads as though the country has +// been cut off rather than framed. +func CountryBounds(country *CountryOutline, margin float64) (Bounds, Bounds) { + minLon, maxLon := math.Inf(1), math.Inf(-1) + minLat, maxLat := math.Inf(1), math.Inf(-1) + for _, ring := range country.Rings { + for i := 0; i+1 < len(ring); i += 2 { + minLon = math.Min(minLon, ring[i]) + maxLon = math.Max(maxLon, ring[i]) + minLat = math.Min(minLat, ring[i+1]) + maxLat = math.Max(maxLat, ring[i+1]) + } + } + if math.IsInf(minLon, 0) { + return WorldX, WorldY + } + // A single-point country would give a zero-width window, which cannot be + // mapped onto anything. + padX := math.Max((maxLon-minLon)*margin, 1) + padY := math.Max((maxLat-minLat)*margin, 1) + return Bounds{Min: minLon - padX, Max: maxLon + padX}, + Bounds{Min: minLat - padY, Max: maxLat + padY} +} + +// DegreesAt is the degrees under a terminal cell, given the window the map was +// drawn with. +// +// The inverse of what the canvas does on the way in, taken at the centre of the +// cell: a click lands on a whole cell, and the centre is the only point in it +// that is not arbitrarily nearer one neighbour than the other. +func DegreesAt(column, row, width, height int, x, y Bounds) (lon, lat float64, ok bool) { + if width <= 0 || height <= 0 { + return 0, 0, false + } + // The canvas is 2x4 Braille pixels per cell, and it spans its bounds across + // `pixels - 1`, so the inverse has to use the same denominators or a click + // drifts from what was drawn. + px := math.Max(1, float64(width*2-1)) + py := math.Max(1, float64(height*4-1)) + lon = x.Min + (float64(column*2+1)/px)*(x.Max-x.Min) + lat = y.Min + (1-float64(row*4+2)/py)*(y.Max-y.Min) + return lon, lat, true +} diff --git a/ports/go/world_data.go b/ports/go/world_data.go new file mode 100644 index 0000000..853223b --- /dev/null +++ b/ports/go/world_data.go @@ -0,0 +1,1308 @@ +package hqtui + +// Country outlines, flattened for a terminal. +// +// Generated by packages/hqtui/scripts/generate-world.ts from Natural Earth's +// 1:110m Admin 0 countries, which is public domain. Do not edit by hand. +// +// Each ring is longitude and latitude interleaved -- lon, lat, lon, lat -- +// rather than a list of pairs, because at 1982 points the nested form +// costs a container per coordinate for no gain. A country has more than one +// ring when it is more than one landmass. +// +// 171 countries, 1982 points, simplified at 1 degrees. + +type CountryOutline struct { + Name string + // ISO 3166-1 alpha-2, where Natural Earth has one. + ISO string + // Rings are longitude and latitude, interleaved. + Rings [][]float64 +} + +var WorldCountries = []CountryOutline{ + { + Name: "Afghanistan", + ISO: "AF", + Rings: [][]float64{ + {66.5,37.4,70.8,38.5,71.8,36.7,75.2,37.1,71.3,36.1,69.3,31.9,66.3,29.9,60.9,29.8,61.2,35.7,66.5,37.4}, + }, + }, + { + Name: "Albania", + ISO: "AL", + Rings: [][]float64{ + {21,40.8,19.4,40.3,19.7,42.7,21,40.8}, + }, + }, + { + Name: "Algeria", + ISO: "DZ", + Rings: [][]float64{ + {-8.7,27.4,-8.7,28.8,-1.3,32.3,-1.2,35.7,8.4,36.9,7.5,34.1,9.8,29.4,9.3,26.1,12,23.5,3.2,19.1,-8.7,27.4}, + }, + }, + { + Name: "Angola", + ISO: "AO", + Rings: [][]float64{ + {12.3,-6.1,16.3,-5.9,17.5,-8.1,21.7,-7.3,22.2,-11.1,24,-11.2,24,-12.9,21.9,-12.9,23.2,-17.5,11.7,-17.3,13.7,-11.3,12.3,-6.1}, + }, + }, + { + Name: "Antarctica", + ISO: "AQ", + Rings: [][]float64{ + {-48.7,-78,-43.9,-78.5,-43.3,-80,-54.2,-80.6,-48.7,-78}, + {-66.3,-80.3,-59.6,-80,-66.3,-80.3}, + {-73.9,-71.3,-70.3,-68.9,-68.3,-71.4,-75,-72.1,-73.9,-71.3}, + {-102.3,-71.9,-96.2,-72.5,-102.3,-71.9}, + {-122.6,-73.7,-118.7,-73.5,-122.6,-73.7}, + {-127.3,-73.5,-124,-73.9,-127.3,-73.5}, + {-163.7,-78.6,-159.2,-79.5,-163.7,-78.6}, + {180,-84.7,180,-90,-180,-90,-179.1,-84.1,-143.1,-85,-153.6,-83.7,-152.9,-82,-156.8,-81.1,-146.4,-80.3,-155.3,-79.1,-158.4,-76.9,-151.3,-77.4,-146.1,-76.5,-146.2,-75.4,-135.2,-74.3,-100.1,-74.9,-103.7,-72.6,-74.9,-73.9,-67.4,-72.5,-67.7,-67.3,-57.8,-63.3,-65.7,-68,-61.8,-70.7,-60.8,-73.7,-70.6,-76.6,-77.2,-76.7,-73.7,-77.9,-78,-79.2,-58.2,-83.2,-28.5,-80.3,-35.6,-79.5,-35.8,-78.3,-17.5,-75.1,-15.4,-73.1,-6.9,-70.9,27.1,-70.5,33.9,-68.5,38.6,-69.8,54.5,-65.8,61.4,-68,68.9,-67.9,69.7,-69.2,67.9,-71.9,69.9,-72.3,73.9,-69.9,88,-66.2,95.8,-67.4,102.8,-65.6,106.2,-66.9,113.6,-65.9,119.8,-67.3,135.1,-65.3,137.5,-67,145.5,-66.9,171.2,-71.7,163.6,-76.2,167,-78.8,161.8,-79.2,159.8,-80.9,169.4,-83.8,180,-84.7}, + }, + }, + { + Name: "Argentina", + ISO: "AR", + Rings: [][]float64{ + {-68.6,-52.6,-65,-54.7,-68.6,-54.9,-68.6,-52.6}, + {-57.6,-30.2,-58.5,-34.4,-56.8,-36.9,-62.3,-38.8,-62.7,-41,-65.1,-41.1,-63.5,-42.6,-67.3,-45.6,-65.6,-47.2,-69.1,-50.7,-68.1,-52.3,-71.9,-52,-73.4,-49.3,-71.2,-44.8,-72.1,-42.3,-68.4,-24.5,-66.3,-21.8,-62.8,-22,-57.8,-25.2,-58.6,-27.1,-55.7,-27.4,-54.1,-25.5,-53.6,-26.9,-57.6,-30.2}, + }, + }, + { + Name: "Armenia", + ISO: "AM", + Rings: [][]float64{ + {46.5,38.8,43.6,41.1,45.6,40.8,46.5,38.8}, + }, + }, + { + Name: "Australia", + ISO: "AU", + Rings: [][]float64{ + {147.7,-40.8,147.9,-43.2,146,-43.5,144.7,-40.7,147.7,-40.8}, + {126.1,-32.2,118,-35.1,115,-34.2,113.7,-22.5,120.9,-19.7,125.7,-14.2,129.6,-15,132.4,-11.1,136.5,-11.9,135.5,-15,140.2,-17.7,142.5,-10.7,146.4,-19,150.7,-22.4,153.6,-28.1,150,-37.4,146.3,-39,140.6,-38,138.2,-34.4,136.8,-35.3,137.8,-32.9,136,-34.9,131.3,-31.5,126.1,-32.2}, + }, + }, + { + Name: "Austria", + ISO: "AT", + Rings: [][]float64{ + {17,48.1,14.6,46.4,9.5,47.1,12.9,47.5,13.6,48.9,17,48.1}, + }, + }, + { + Name: "Azerbaijan", + ISO: "AZ", + Rings: [][]float64{ + {46.4,41.9,50.4,40.3,48.9,38.3,45.6,39.9,45,41.2,46.4,41.9}, + }, + }, + { + Name: "Bahamas", + ISO: "BS", + Rings: [][]float64{ + {-78.2,25.2,-77.5,23.8,-78.2,25.2}, + }, + }, + { + Name: "Bangladesh", + ISO: "BD", + Rings: [][]float64{ + {92.7,22,92.4,20.7,91.4,22.8,89,22.1,88.6,26.4,92.4,25,91.2,23.5,92.7,22}, + }, + }, + { + Name: "Belarus", + ISO: "BY", + Rings: [][]float64{ + {28.2,56.2,30.9,55.6,32.7,53.4,31.8,52.1,23.5,51.6,23.5,53.9,28.2,56.2}, + }, + }, + { + Name: "Belgium", + ISO: "BE", + Rings: [][]float64{ + {6.2,50.8,5.7,49.5,2.5,51.1,6.2,50.8}, + }, + }, + { + Name: "Belize", + ISO: "BZ", + Rings: [][]float64{ + {-89.1,17.8,-88.1,18.3,-88.9,15.9,-89.1,17.8}, + }, + }, + { + Name: "Benin", + ISO: "BJ", + Rings: [][]float64{ + {2.7,6.3,0.8,10.5,2.8,12.2,2.7,6.3}, + }, + }, + { + Name: "Bhutan", + ISO: "BT", + Rings: [][]float64{ + {91.7,27.8,88.8,27.1,91.7,27.8}, + }, + }, + { + Name: "Bolivia", + ISO: "BO", + Rings: [][]float64{ + {-69.5,-11,-65.3,-9.8,-65.4,-11.6,-60.5,-13.8,-60.2,-16.3,-58.2,-16.3,-57.9,-20,-61.8,-19.6,-62.7,-22.2,-67.8,-22.9,-69.5,-11}, + }, + }, + { + Name: "Bosnia and Herz.", + ISO: "BA", + Rings: [][]float64{ + {18.6,42.7,16,45.2,19.4,44.9,18.6,42.7}, + }, + }, + { + Name: "Botswana", + ISO: "BW", + Rings: [][]float64{ + {29.4,-22.1,25.7,-25.5,21.6,-26.7,19.9,-24.8,20.9,-18.3,25.3,-17.7,29.4,-22.1}, + }, + }, + { + Name: "Brazil", + ISO: "BR", + Rings: [][]float64{ + {-53.4,-33.8,-53.8,-32,-57.6,-30.2,-53.6,-26.1,-55.8,-22.4,-57.9,-22.1,-58.2,-16.3,-60.2,-16.3,-60.5,-13.8,-65.4,-11.6,-65.3,-9.8,-70.5,-11,-70.5,-9.5,-72.2,-10.1,-74,-7.5,-72.9,-5.3,-69.9,-4.3,-69.8,1.7,-65.5,0.8,-63.4,2.2,-64.8,4.1,-60.7,5.2,-59,1.3,-52.9,2.1,-51.3,4.2,-50.4,-0.1,-44.6,-2.7,-40,-2.9,-35.6,-5.1,-34.7,-7.3,-38.7,-13.1,-40.9,-21.9,-47.6,-24.9,-48.9,-28.7,-53.4,-33.8}, + }, + }, + { + Name: "Bulgaria", + ISO: "BG", + Rings: [][]float64{ + {22.7,44.2,28.6,43.7,28,42,23,41.3,22.7,44.2}, + }, + }, + { + Name: "Burkina Faso", + ISO: "BF", + Rings: [][]float64{ + {-5.4,10.4,-4.3,13.2,-1.1,15,2.2,12.6,0.9,11,-2.9,11,-2.8,9.6,-5.4,10.4}, + }, + }, + { + Name: "Burundi", + ISO: "BI", + Rings: [][]float64{ + {30.5,-2.4,29.3,-4.5,29,-2.8,30.5,-2.4}, + }, + }, + { + Name: "Cambodia", + ISO: "KH", + Rings: [][]float64{ + {102.6,12.2,103,14.2,107.6,13.5,106.2,11,103.5,10.6,102.6,12.2}, + }, + }, + { + Name: "Cameroon", + ISO: "CM", + Rings: [][]float64{ + {14.5,12.9,14.5,4.7,15.9,1.7,9.6,2.3,8.8,5.5,11.7,7,14.5,12.9}, + }, + }, + { + Name: "Canada", + ISO: "CA", + Rings: [][]float64{ + {-122.8,49,-127.4,50.8,-130.5,54.3,-130,55.9,-135.5,59.8,-137.5,58.9,-141,60.3,-141,69.7,-136.5,68.9,-128.1,70.5,-113.5,67.7,-106.1,68.8,-101.5,67.6,-97.7,68.6,-96.1,67.3,-94.2,69.1,-96.5,70.1,-95.2,71.9,-87.4,67.2,-85.5,69.9,-82.6,69.7,-81.4,67.1,-85.8,66.6,-90.7,63.6,-94.7,58.9,-92.3,57.1,-82.3,55.1,-79.9,51.2,-78.6,52.6,-79.8,54.7,-76.5,56.5,-78.5,58.8,-77.3,59.9,-78.1,62.3,-73.8,62.4,-69.6,61.1,-67.6,58.2,-64.6,60.3,-61.8,56.3,-57.3,54.6,-55.7,52.1,-60,50.2,-66.4,50.2,-71.1,46.8,-65.1,49.2,-64.5,46.2,-60.5,47,-59.8,45.9,-65.4,43.5,-66.2,44.5,-64.4,45.3,-67.1,45.1,-69.2,47.4,-71.5,45,-82.4,41.7,-82.6,45.3,-88.4,48.3,-122.8,49}, + {-84,62.5,-81.9,62.9,-84,62.5}, + {-79.8,72.8,-80.8,73.7,-76.3,72.8,-79.8,72.8}, + {-93.6,75,-96.8,74.9,-93.6,75}, + {-93.8,77.5,-96.4,77.8,-93.8,77.5}, + {-96.8,78.8,-95.6,78.4,-98.6,78.9,-96.8,78.8}, + {-88.2,74.4,-97.1,76.8,-79.8,74.9,-88.2,74.4}, + {-111.3,78.2,-109.9,78,-113.5,77.7,-111.3,78.2}, + {-111,78.8,-109.7,78.6,-112.5,78.4,-111,78.8}, + {-55.6,51.3,-56.8,49.8,-53.5,49.2,-53.1,46.7,-59.3,47.6,-55.6,51.3}, + {-83.9,65.1,-80.1,63.7,-87.2,63.5,-85.9,65.7,-83.9,65.1}, + {-78.8,72.4,-68.8,70.5,-67,69.2,-68.8,68.7,-61.9,66.9,-63.9,65,-68,66.3,-64.7,63.4,-68.8,63.7,-66.2,61.9,-68.9,62.3,-78.6,64.6,-74,65.5,-73.3,68.1,-79,70.2,-88.7,70.4,-90.2,72.2,-85.8,73.8,-85.8,72.5,-82.3,73.8,-78.8,72.4}, + {-94.5,74.1,-90.5,73.9,-95.4,72.1,-96,73.4,-94.5,74.1}, + {-122.9,76.1,-116.2,77.6,-122.9,76.1}, + {-132.7,54,-131.2,52.2,-132.7,54}, + {-105.5,79.3,-99.7,77.9,-105.5,79.3}, + {-123.5,48.5,-128.4,50.8,-123.5,48.5}, + {-121.5,74.4,-115.5,73.5,-123.1,70.9,-125.9,71.9,-123.9,73.7,-124.9,74.3,-121.5,74.4}, + {-107.8,75.8,-105.7,75.5,-117.7,75.2,-115.4,76.5,-107.8,75.8}, + {-106.5,73.1,-101.1,69.6,-113.3,68.5,-117.3,70,-112.4,70.4,-119.4,71.6,-115.2,73.3,-108.2,71.7,-108.4,73.1,-106.5,73.1}, + {-100.4,72.7,-101.5,73.4,-97.4,73.8,-96.5,72.6,-98.4,71.3,-102.5,72.5,-100.4,72.7}, + {-106.6,73.6,-104.5,73.4,-106.6,73.6}, + {-98.5,76.7,-98.2,75,-102.5,75.6,-98.5,76.7}, + {-96,80.6,-92.4,81.3,-85.8,79.3,-92.9,78.3,-96,80.6}, + {-91.6,81.9,-61.8,82.6,-76.9,79.3,-75.4,78.5,-80.6,76.2,-89.5,76.5,-88.3,77.9,-85,77.5,-88,78.4,-85.1,79.3,-86.9,80.3,-81.8,80.5,-91.6,81.9}, + {-75.2,67.4,-77.2,67.6,-75.2,67.4}, + {-96.3,69.5,-99.8,69.4,-96.3,69.5}, + {-64.5,49.9,-61.8,49.1,-64.5,49.9}, + {-64,47,-62,46.4,-64,47}, + }, + }, + { + Name: "Central African Rep.", + ISO: "CF", + Rings: [][]float64{ + {27.4,5.2,22.4,4,19.5,5,16,2.3,14.5,5.5,15.3,7.4,22.9,11.1,27.4,5.2}, + }, + }, + { + Name: "Chad", + ISO: "TD", + Rings: [][]float64{ + {23.8,19.6,23.9,15.6,21.9,12.6,22.9,11.1,15.3,7.4,13.5,14.4,15.9,20.4,14.9,22.9,23.8,19.6}, + }, + }, + { + Name: "Chile", + ISO: "CL", + Rings: [][]float64{ + {-68.6,-52.6,-68.6,-54.9,-67,-54.9,-68.1,-55.6,-74.7,-52.8,-71.1,-54.1,-68.6,-52.6}, + {-69.6,-17.6,-67,-23,-70.5,-31.4,-69.8,-34.2,-72.1,-42.3,-71.2,-44.8,-73.4,-49.3,-71.9,-52,-68.6,-52.3,-71.4,-53.9,-74.9,-52.3,-75.6,-48.7,-74.1,-46.9,-75.6,-46.6,-72.7,-42.4,-74.3,-43.2,-69.6,-17.6}, + }, + }, + { + Name: "China", + ISO: "CN", + Rings: [][]float64{ + {109.5,18.2,108.6,19.4,110.8,20.1,109.5,18.2}, + {80.3,42.3,80,44.9,87.8,49.3,91,46.9,90.9,45.3,96.3,42.7,109.2,42.5,111.9,45.1,119.7,46.7,115.5,48.1,122.2,53.4,125.9,52.8,131,47.8,135,48.5,133.1,45.1,131,45,130.6,42.4,121.1,38.9,121.6,40.9,117.5,38.7,122.4,37.5,119.2,34.9,121.9,31.7,121.7,28.2,118.7,24.5,110.4,20.3,105.3,23.4,101.7,22.3,101.8,21.2,99.2,22.1,97.6,23.9,98.7,27.5,96.1,29.5,88.8,27.3,78.7,31.5,78.9,34.3,73.7,39.4,80.3,42.3}, + }, + }, + { + Name: "Colombia", + ISO: "CO", + Rings: [][]float64{ + {-66.9,1.3,-69.8,1.7,-69.9,-4.3,-70,-2.7,-77.4,0.4,-79,1.7,-77.1,3.8,-77.5,8.5,-71.4,12.4,-73.3,9.2,-72,7,-67.3,6.1,-66.9,1.3}, + }, + }, + { + Name: "Congo", + ISO: "CG", + Rings: [][]float64{ + {18.5,3.5,16,-3.5,11.9,-5,11.5,-2.8,14.4,-1.3,13.1,2.3,15.9,1.7,18.5,3.5}, + }, + }, + { + Name: "Costa Rica", + ISO: "CR", + Rings: [][]float64{ + {-82.5,9.6,-83,8.2,-85.9,10.9,-82.5,9.6}, + }, + }, + { + Name: "Côte d'Ivoire", + ISO: "CI", + Rings: [][]float64{ + {-8,10.2,-2.8,9.6,-2.9,5,-7.7,4.4,-8,10.2}, + }, + }, + { + Name: "Croatia", + ISO: "HR", + Rings: [][]float64{ + {16.6,46.5,19.4,45.2,15.8,44.8,18.5,42.5,13.7,45.1,16.6,46.5}, + }, + }, + { + Name: "Cuba", + ISO: "CU", + Rings: [][]float64{ + {-82.3,23.2,-74.2,20.3,-77.8,19.9,-81.8,22.6,-85,21.9,-82.3,23.2}, + }, + }, + { + Name: "Cyprus", + ISO: "CY", + Rings: [][]float64{ + {32.7,35.1,34,35,32.7,35.1}, + }, + }, + { + Name: "Czechia", + ISO: "CZ", + Rings: [][]float64{ + {15,51.1,18.9,49.5,12.5,49.5,15,51.1}, + }, + }, + { + Name: "Dem. Rep. Congo", + ISO: "CD", + Rings: [][]float64{ + {29.3,-4.5,30.7,-8.3,28.7,-8.5,28.4,-11.8,29.7,-13.3,22.2,-11.1,21.7,-7.3,17.5,-8.1,16.3,-5.9,12.2,-5.8,16,-3.5,19.5,5,29.7,4.6,31.2,2.2,29.3,-4.5}, + }, + }, + { + Name: "Denmark", + ISO: "DK", + Rings: [][]float64{ + {9.9,55,8.1,56.5,10.6,57.7,9.9,55}, + {12.4,56.1,12.1,54.8,11,55.4,12.4,56.1}, + }, + }, + { + Name: "Djibouti", + ISO: "DJ", + Rings: [][]float64{ + {42.4,12.5,42.8,10.9,42.4,12.5}, + }, + }, + { + Name: "Dominican Rep.", + ISO: "DO", + Rings: [][]float64{ + {-71.7,18,-71.6,19.9,-68.3,18.6,-71.7,18}, + }, + }, + { + Name: "Ecuador", + ISO: "EC", + Rings: [][]float64{ + {-75.4,-0.2,-78.6,-4.5,-80.4,-4.4,-80.1,0.8,-75.4,-0.2}, + }, + }, + { + Name: "Egypt", + ISO: "EG", + Rings: [][]float64{ + {36.9,22,25,22,25.2,31.6,34.3,31.2,34.2,27.8,32.3,29.8,36.9,22}, + }, + }, + { + Name: "El Salvador", + ISO: "SV", + Rings: [][]float64{ + {-89.4,14.4,-87.9,13.1,-90.1,13.7,-89.4,14.4}, + }, + }, + { + Name: "Eq. Guinea", + ISO: "GQ", + Rings: [][]float64{ + {9.6,2.3,11.3,1.1,9.5,1,9.6,2.3}, + }, + }, + { + Name: "Eritrea", + ISO: "ER", + Rings: [][]float64{ + {36.4,14.4,38.4,18,43.1,12.7,36.4,14.4}, + }, + }, + { + Name: "Estonia", + ISO: "EE", + Rings: [][]float64{ + {28,59.5,27.3,57.5,23.3,59.2,28,59.5}, + }, + }, + { + Name: "eSwatini", + ISO: "SZ", + Rings: [][]float64{ + {32.1,-26.7,31,-25.7,32.1,-26.7}, + }, + }, + { + Name: "Ethiopia", + ISO: "ET", + Rings: [][]float64{ + {47.8,8,45,5,39.6,3.4,36.2,4.4,33,7.8,37.9,15,41.6,13.5,43.7,9.2,47.8,8}, + }, + }, + { + Name: "Falkland Is.", + ISO: "FK", + Rings: [][]float64{ + {-61.2,-51.8,-57.7,-51.5,-61.2,-51.8}, + }, + }, + { + Name: "Finland", + ISO: "FI", + Rings: [][]float64{ + {28.6,69.1,31.1,62.4,28.1,60.5,21.3,60.7,21.5,63.2,25.4,65.1,20.6,69.1,24.7,68.6,27.7,70.2,28.6,69.1}, + }, + }, + { + Name: "Fr. S. Antarctic Lands", + ISO: "TF", + Rings: [][]float64{ + {68.9,-48.6,70.6,-49.3,68.7,-49.8,68.9,-48.6}, + }, + }, + { + Name: "France", + ISO: "FR", + Rings: [][]float64{ + {-51.7,4.2,-52.9,2.1,-54.5,2.3,-54,5.8,-51.7,4.2}, + {6.2,49.5,8.1,49,6,46.7,7.4,43.7,1.8,42.3,-1.9,43.4,-1.2,46,-4.6,48.7,-1.6,48.6,-1.9,49.8,2.5,51.1,6.2,49.5}, + {8.7,42.6,9.2,41.4,8.7,42.6}, + }, + }, + { + Name: "Gabon", + ISO: "GA", + Rings: [][]float64{ + {11.3,2.3,14.3,1.2,14.4,-1.3,11.1,-4,8.8,-1.1,11.3,2.3}, + }, + }, + { + Name: "Gambia", + ISO: "GM", + Rings: [][]float64{ + {-16.7,13.6,-13.8,13.5,-16.7,13.6}, + }, + }, + { + Name: "Georgia", + ISO: "GE", + Rings: [][]float64{ + {40,43.4,46.6,41.2,41.6,41.5,40,43.4}, + }, + }, + { + Name: "Germany", + ISO: "DE", + Rings: [][]float64{ + {14.1,53.8,15,51.1,12.2,50.3,12.9,47.5,7.5,47.6,8.1,49,6,50.1,7.1,53.7,9.9,55,14.1,53.8}, + }, + }, + { + Name: "Ghana", + ISO: "GH", + Rings: [][]float64{ + {0,11,1.1,5.9,-2.9,5,-2.9,11,0,11}, + }, + }, + { + Name: "Greece", + ISO: "GR", + Rings: [][]float64{ + {26.3,35.3,23.5,35.3,26.3,35.3}, + {23,41.3,26.6,41.6,22.6,40.3,24,37.7,22.5,36.4,20.2,39.6,23,41.3}, + }, + }, + { + Name: "Greenland", + ISO: "GL", + Rings: [][]float64{ + {-46.8,82.6,-27.1,83.5,-20.8,82.7,-31.9,82.2,-12.2,81.3,-20,80.2,-17.7,80.1,-19.7,78.8,-18.5,77,-21.7,76.6,-19.4,74.3,-24.8,72.3,-21.8,70.7,-25.5,71.4,-26.4,70.2,-22.3,70.1,-39.8,65.5,-43.4,60.1,-48.3,60.9,-51.6,63.6,-54,67.2,-50.9,69.9,-54.7,69.6,-54.4,70.8,-51.4,70.6,-55.8,71.7,-54.7,72.6,-58.6,75.5,-68.5,76.1,-71.4,77,-66.8,77.4,-73.3,78,-65.7,79.4,-68,80.1,-62.7,81.8,-44.5,81.7,-46.8,82.6}, + }, + }, + { + Name: "Guatemala", + ISO: "GT", + Rings: [][]float64{ + {-92.2,14.5,-90.5,16.1,-91,17.8,-89.1,17.8,-88.2,15.7,-89.4,14.4,-92.2,14.5}, + }, + }, + { + Name: "Guinea", + ISO: "GN", + Rings: [][]float64{ + {-13.7,12.6,-9.1,12.3,-8.3,7.7,-11.1,10,-13.2,8.9,-15.1,11,-13.7,12.6}, + }, + }, + { + Name: "Guinea-Bissau", + ISO: "GW", + Rings: [][]float64{ + {-16.7,12.4,-13.7,11.8,-15.1,11,-16.7,12.4}, + }, + }, + { + Name: "Guyana", + ISO: "GY", + Rings: [][]float64{ + {-56.5,1.9,-59.6,1.8,-61.4,6,-59.8,8.4,-57.1,6,-58,4.1,-56.5,1.9}, + }, + }, + { + Name: "Haiti", + ISO: "HT", + Rings: [][]float64{ + {-71.7,19.7,-71.7,18,-74.5,18.3,-71.7,19.7}, + }, + }, + { + Name: "Honduras", + ISO: "HN", + Rings: [][]float64{ + {-83.1,15,-87.3,13,-89.4,14.4,-87.9,15.9,-83.1,15}, + }, + }, + { + Name: "Hungary", + ISO: "HU", + Rings: [][]float64{ + {22.1,48.4,21,46.3,16.2,46.9,17,48.1,22.1,48.4}, + }, + }, + { + Name: "Iceland", + ISO: "IS", + Rings: [][]float64{ + {-14.5,66.5,-13.6,65.1,-18.7,63.5,-22.8,64,-21.8,64.4,-24,64.9,-22.2,65.4,-24.3,65.6,-14.5,66.5}, + }, + }, + { + Name: "India", + ISO: "IN", + Rings: [][]float64{ + {97.3,28.3,92.7,22,91.2,23.5,92.4,25,88.6,26.4,88.9,21.7,80.3,15.9,79.9,10.4,77.5,8,72.6,21.4,70.5,20.9,68.2,23.7,71,24.4,69.5,26.9,75.3,32.3,73.7,34.3,77.8,35.5,78.7,31.5,81.1,30.2,80.1,28.8,83.3,27.4,88.1,26.4,88.7,28.1,92,26.8,96.1,29.5,97.3,28.3}, + }, + }, + { + Name: "Indonesia", + ISO: "ID", + Rings: [][]float64{ + {141,-2.6,141,-9.1,137.6,-8.4,137.9,-5.4,133,-4.1,132,-2.8,133.7,-2.2,130.5,-0.9,134,-0.8,135.5,-3.4,137.4,-1.7,141,-2.6}, + {125,-8.9,123.5,-10.2,125,-8.9}, + {117.9,4.1,119,0.9,116.1,-4,110.2,-2.9,109.7,2,110.5,0.8,113.8,1.2,115.9,4.3,117.9,4.1}, + {129.4,-2.8,130.8,-3.9,127.9,-3.4,129.4,-2.8}, + {127.9,2.2,128.1,-0.9,127.9,2.2}, + {122.9,0.9,125.2,1.4,120,-0.5,123.3,-0.6,121.5,-1.9,123.2,-5.3,121,-2.6,119.8,-5.7,118.8,-2.8,119.8,0.2,122.9,0.9}, + {120.3,-10.3,119,-9.6,120.3,-10.3}, + {121.3,-8.5,122.9,-8.1,119.9,-8.8,121.3,-8.5}, + {118.3,-8.4,116.7,-9,118.3,-8.4}, + {108.5,-6.4,115.7,-8.4,105.4,-6.9,108.5,-6.4}, + {104.4,-1.1,106.1,-3.1,105.8,-5.9,102.6,-4.2,95.3,5.5,97.5,5.2,104.4,-1.1}, + }, + }, + { + Name: "Iran", + ISO: "IR", + Rings: [][]float64{ + {48.6,29.9,45.4,34,46.1,35.7,44.1,39.4,48.1,39.6,50.8,36.9,56.6,38.1,61.1,36.5,60.9,29.8,63.3,26.8,61.5,25.1,57.4,25.7,48.6,29.9}, + }, + }, + { + Name: "Iraq", + ISO: "IQ", + Rings: [][]float64{ + {39.2,32.2,41.3,36.4,44.8,37.2,48.6,29.9,44.7,29.2,39.2,32.2}, + }, + }, + { + Name: "Ireland", + ISO: "IE", + Rings: [][]float64{ + {-6.2,53.9,-6.8,52.3,-10,51.8,-7.6,55.1,-6.2,53.9}, + }, + }, + { + Name: "Israel", + ISO: "IL", + Rings: [][]float64{ + {35.7,32.7,34.9,29.5,34.3,31.2,35.7,32.7}, + }, + }, + { + Name: "Italy", + ISO: "IT", + Rings: [][]float64{ + {10.4,46.9,13.8,46.5,12.6,44.1,18.3,39.8,16.9,40.4,15.7,37.9,15.4,40,10.2,43.9,7.4,43.7,6.8,46,10.4,46.9}, + {14.8,38.1,15.1,36.6,12.4,37.6,14.8,38.1}, + {8.7,40.9,9.8,40.5,8.8,38.9,8.7,40.9}, + }, + }, + { + Name: "Jamaica", + ISO: "JM", + Rings: [][]float64{ + {-77.6,18.5,-76.2,17.9,-77.6,18.5}, + }, + }, + { + Name: "Japan", + ISO: "JP", + Rings: [][]float64{ + {141.9,39.2,140.3,35.1,135.8,33.5,135.1,34.6,131,33.9,132,33.1,130.2,31.4,129.4,33.3,139.4,38.2,140.3,41.2,141.9,39.2}, + {144.6,44,145.5,43.3,140,41.6,142,45.6,144.6,44}, + {132.4,33.5,134.8,33.8,132.4,33.5}, + }, + }, + { + Name: "Jordan", + ISO: "JO", + Rings: [][]float64{ + {35.5,32.4,38.8,33.4,39.2,32.2,37,31.5,38,30.5,36.1,29.2,34.9,29.5,35.5,32.4}, + }, + }, + { + Name: "Kazakhstan", + ISO: "KZ", + Rings: [][]float64{ + {87.4,49.2,80,44.9,80.3,42.3,74.2,43.3,68.6,40.7,64.9,43.7,62,43.5,58.5,45.6,55.9,45,56,41.3,52.5,41.8,50.3,44.6,53,45.3,53,46.9,49.1,46.4,46.5,48.4,50.8,51.7,61.3,50.8,60,52,61.4,54,69.1,55.4,73.4,53.5,76.9,54.5,80,50.9,87.4,49.2}, + }, + }, + { + Name: "Kenya", + ISO: "KE", + Rings: [][]float64{ + {39.2,-4.7,33.9,-0.9,35.3,5.5,38.1,3.6,41.9,3.9,41.6,-1.7,39.2,-4.7}, + }, + }, + { + Name: "Kosovo", + ISO: "XK", + Rings: [][]float64{ + {20.6,41.9,20.6,43.2,21.8,42.7,20.6,41.9}, + }, + }, + { + Name: "Kuwait", + ISO: "KW", + Rings: [][]float64{ + {48,30,48.4,28.6,46.6,29.1,48,30}, + }, + }, + { + Name: "Kyrgyzstan", + ISO: "KG", + Rings: [][]float64{ + {71,42.3,74.2,43.3,80.3,42.3,73.7,39.4,69.5,39.5,73.1,40.9,71,42.3}, + }, + }, + { + Name: "Laos", + ISO: "LA", + Rings: [][]float64{ + {107.4,14.2,105.2,14.3,104,18.2,101.1,17.5,100.1,20.4,101.7,22.3,104.4,20.8,103.9,19.3,107.4,14.2}, + }, + }, + { + Name: "Latvia", + ISO: "LV", + Rings: [][]float64{ + {27.3,57.5,28.2,56.2,26.5,55.6,21.1,56,22.5,57.8,27.3,57.5}, + }, + }, + { + Name: "Lebanon", + ISO: "LB", + Rings: [][]float64{ + {35.8,33.3,36.4,34.6,35.8,33.3}, + }, + }, + { + Name: "Lesotho", + ISO: "LS", + Rings: [][]float64{ + {29,-29,28.1,-30.5,27,-29.9,29,-29}, + }, + }, + { + Name: "Liberia", + ISO: "LR", + Rings: [][]float64{ + {-8.4,7.7,-7.7,4.4,-11.4,6.8,-10.2,8.4,-8.4,7.7}, + }, + }, + { + Name: "Libya", + ISO: "LY", + Rings: [][]float64{ + {25,22,23.8,19.6,10.3,24.4,10,31.4,11.5,33.1,19.1,30.3,20.9,32.7,24.9,31.9,25,22}, + }, + }, + { + Name: "Lithuania", + ISO: "LT", + Rings: [][]float64{ + {26.5,55.6,23.5,53.9,21.1,56,26.5,55.6}, + }, + }, + { + Name: "Madagascar", + ISO: "MG", + Rings: [][]float64{ + {49.5,-12.5,50.4,-15.7,47.1,-24.9,45.4,-25.6,43.3,-22.8,44,-17.4,49.5,-12.5}, + }, + }, + { + Name: "Malawi", + ISO: "MW", + Rings: [][]float64{ + {32.8,-9.2,35.7,-14.6,35,-16.8,32.7,-13.7,32.8,-9.2}, + }, + }, + { + Name: "Malaysia", + ISO: "MY", + Rings: [][]float64{ + {100.1,6.5,103,5.5,104.2,1.3,101.4,2.8,100.1,6.5}, + {117.9,4.1,115.9,4.3,114.6,1.4,109.8,1.3,115.3,4.3,116.7,6.9,119.2,5.4,117.9,4.1}, + }, + }, + { + Name: "Mali", + ISO: "ML", + Rings: [][]float64{ + {-11.5,12.4,-11.7,15.4,-5.5,15.5,-6.5,25,4.3,19.2,3.6,15.6,-4,13.5,-5.4,10.4,-11.5,12.4}, + }, + }, + { + Name: "Mauritania", + ISO: "MR", + Rings: [][]float64{ + {-17.1,21,-12.9,21.3,-12,25.9,-8.7,25.9,-8.7,27.4,-4.9,25,-6.5,25,-5.5,15.5,-12.2,14.6,-14.6,16.6,-16.5,16.1,-17.1,21}, + }, + }, + { + Name: "Mexico", + ISO: "MX", + Rings: [][]float64{ + {-117.1,32.5,-106.5,31.8,-103.9,29.3,-101.7,29.8,-97.1,25.9,-97.9,22.4,-95.9,18.8,-91.4,18.9,-90.3,21,-87.1,21.5,-87.8,18.3,-91,17.8,-90.5,16.1,-92.2,14.5,-103.5,18.3,-113.1,31.2,-114.9,31.4,-109.9,22.8,-115.1,27.7,-114.2,28.6,-117.1,32.5}, + }, + }, + { + Name: "Moldova", + ISO: "MD", + Rings: [][]float64{ + {26.6,48.2,30,46.4,28.2,45.5,26.6,48.2}, + }, + }, + { + Name: "Mongolia", + ISO: "MN", + Rings: [][]float64{ + {87.8,49.3,92.2,50.8,97.3,49.7,98.9,52,108.5,49.3,116.7,49.9,115.7,47.7,119.8,47,105,41.6,96.3,42.7,90.9,45.3,91,46.9,87.8,49.3}, + }, + }, + { + Name: "Montenegro", + ISO: "ME", + Rings: [][]float64{ + {20.1,42.6,18.5,42.5,20.1,42.6}, + }, + }, + { + Name: "Morocco", + ISO: "MA", + Rings: [][]float64{ + {-2.2,35.2,-1.3,32.3,-8.7,28.8,-8.8,27.1,-11.4,26.9,-14.8,21.5,-17,21.4,-14.4,26.3,-9.6,29.9,-8.7,33.2,-5.9,35.8,-2.2,35.2}, + }, + }, + { + Name: "Mozambique", + ISO: "MZ", + Rings: [][]float64{ + {34.6,-11.5,40.3,-10.3,40.8,-14.7,34.8,-19.8,35.5,-24.1,32.1,-26.7,31.2,-22.3,32.8,-16.7,30.2,-14.8,33.2,-14,35,-16.8,34.6,-11.5}, + }, + }, + { + Name: "Myanmar", + ISO: "MM", + Rings: [][]float64{ + {100.1,20.4,97.4,18.4,99.6,11.9,98.6,9.9,97.2,16.9,94.2,16,92.3,21.5,97.3,28.3,98.7,27.5,97.6,23.9,101.2,21.8,100.1,20.4}, + }, + }, + { + Name: "N. Cyprus", + ISO: "-99", + Rings: [][]float64{ + {32.7,35.1,34.6,35.7,32.7,35.1}, + }, + }, + { + Name: "Namibia", + ISO: "NA", + Rings: [][]float64{ + {19.9,-24.8,19.9,-28.5,16.3,-28.6,11.7,-17.3,25.1,-17.6,20.9,-18.3,19.9,-24.8}, + }, + }, + { + Name: "Nepal", + ISO: "NP", + Rings: [][]float64{ + {88.1,27.9,87.2,26.4,80.1,28.8,81.5,30.4,88.1,27.9}, + }, + }, + { + Name: "Netherlands", + ISO: "NL", + Rings: [][]float64{ + {6.9,53.5,6.2,50.8,3.3,51.3,6.9,53.5}, + }, + }, + { + Name: "New Caledonia", + ISO: "NC", + Rings: [][]float64{ + {165.8,-21.1,167.1,-22.2,164,-20.1,165.8,-21.1}, + }, + }, + { + Name: "New Zealand", + ISO: "NZ", + Rings: [][]float64{ + {176.9,-40.1,174.7,-41.3,174.7,-37.4,172.6,-34.5,176,-37.6,178.5,-37.7,176.9,-40.1}, + {169.7,-43.6,172.8,-40.5,174.2,-41.3,170.6,-45.9,166.7,-46.2,169.7,-43.6}, + }, + }, + { + Name: "Nicaragua", + ISO: "NI", + Rings: [][]float64{ + {-83.7,10.9,-87.7,12.9,-83.1,15,-83.7,10.9}, + }, + }, + { + Name: "Niger", + ISO: "NE", + Rings: [][]float64{ + {14.9,22.9,15.9,20.4,13.5,14.4,14.2,12.5,5.4,13.9,3.6,11.7,1,12.9,0.4,14.9,3.6,15.6,4.3,19.2,12,23.5,14.9,22.9}, + }, + }, + { + Name: "Nigeria", + ISO: "NG", + Rings: [][]float64{ + {2.7,6.3,4.4,13.7,13.1,13.6,14.6,12.1,11.7,7,8.5,4.8,5.9,4.3,2.7,6.3}, + }, + }, + { + Name: "North Korea", + ISO: "KP", + Rings: [][]float64{ + {130.6,42.4,127.5,39.8,128.2,38.4,124.7,38.1,125.1,40.6,130.6,42.4}, + }, + }, + { + Name: "North Macedonia", + ISO: "MK", + Rings: [][]float64{ + {22.4,42.3,23,41.3,20.6,41.1,22.4,42.3}, + }, + }, + { + Name: "Norway", + ISO: "NO", + Rings: [][]float64{ + {15.1,79.7,21.5,79,15.9,76.8,10.4,79.7,15.1,79.7}, + {31.1,69.6,18,68.6,12.6,64.1,11,58.9,5.7,58.6,5,62,19.2,69.8,28.2,71.2,31.1,69.6}, + {27.4,80.1,17.4,80.3,27.4,80.1}, + {24.7,77.9,20.7,77.7,24.7,77.9}, + }, + }, + { + Name: "Oman", + ISO: "OM", + Rings: [][]float64{ + {55.2,22.7,56.4,24.9,59.8,22.3,57.7,18.9,53.1,16.7,52,19,55,20,55.2,22.7}, + }, + }, + { + Name: "Pakistan", + ISO: "PK", + Rings: [][]float64{ + {77.8,35.5,73.7,34.3,75.3,32.3,69.5,26.9,71,24.4,61.5,25.1,63.3,26.8,60.9,29.8,66.3,29.9,71.8,36.5,75.2,37.1,77.8,35.5}, + }, + }, + { + Name: "Panama", + ISO: "PA", + Rings: [][]float64{ + {-77.4,8.7,-77.9,7.2,-79.1,9,-80.9,7.2,-82.9,9.5,-77.4,8.7}, + }, + }, + { + Name: "Papua New Guinea", + ISO: "PG", + Rings: [][]float64{ + {141,-2.6,147.6,-6.1,147.2,-7.4,150.7,-10.6,144.7,-7.6,141,-9.1,141,-2.6}, + {152.6,-3.7,152.8,-4.8,150.7,-2.7,152.6,-3.7}, + {151.3,-5.8,148.3,-5.7,152.1,-4.1,151.3,-5.8}, + {154.8,-5.3,155.9,-6.8,154.8,-5.3}, + }, + }, + { + Name: "Paraguay", + ISO: "PY", + Rings: [][]float64{ + {-58.2,-20.2,-57.9,-22.1,-54.3,-24,-55.7,-27.4,-58.6,-27.1,-57.8,-25.2,-62.7,-22.2,-61.8,-19.6,-58.2,-20.2}, + }, + }, + { + Name: "Peru", + ISO: "PE", + Rings: [][]float64{ + {-69.9,-4.3,-72.9,-5.3,-74,-7.5,-68.7,-12.6,-70.4,-18.3,-76,-14.6,-81.4,-4.7,-80.3,-3.4,-78.6,-4.5,-75.1,-0.1,-73.1,-2.3,-70,-2.7,-69.9,-4.3}, + }, + }, + { + Name: "Philippines", + ISO: "PH", + Rings: [][]float64{ + {122.6,10,124.1,11.2,123,9,122.6,10}, + {126.4,8.4,125.4,5.6,123.6,7.8,121.9,7.2,125.4,9.8,126.4,8.4}, + {118.5,9.3,117.2,8.4,119.5,11.4,118.5,9.3}, + {122.3,18.2,121.7,14.3,124.1,12.5,119.9,15.4,120.7,18.5,122.3,18.2}, + {125.5,12.2,124.8,10.1,124.3,12.6,125.5,12.2}, + }, + }, + { + Name: "Poland", + ISO: "PL", + Rings: [][]float64{ + {23.5,53.9,24,50.7,22.8,49,16.2,50.4,14.1,53,17.6,54.9,23.5,53.9}, + }, + }, + { + Name: "Portugal", + ISO: "PT", + Rings: [][]float64{ + {-9,41.9,-6.4,41.4,-7.9,36.8,-9.5,38.7,-9,41.9}, + }, + }, + { + Name: "Puerto Rico", + ISO: "PR", + Rings: [][]float64{ + {-66.3,18.5,-67.2,17.9,-66.3,18.5}, + }, + }, + { + Name: "Qatar", + ISO: "QA", + Rings: [][]float64{ + {50.8,24.8,51.3,26.1,50.8,24.8}, + }, + }, + { + Name: "Romania", + ISO: "RO", + Rings: [][]float64{ + {28.2,45.5,29.6,45.3,28.6,43.7,22.9,43.8,20.2,46.1,26.6,48.2,28.2,45.5}, + }, + }, + { + Name: "Russia", + ISO: "RU", + Rings: [][]float64{ + {49.1,46.4,46.7,44.6,47.8,41.2,36.7,45.2,40.1,49.6,31.8,52.1,32.7,53.4,30.9,55.6,27.3,57.5,29.1,60,28.1,60.5,31.5,62.9,30,63.6,28.6,69.1,32.1,69.9,41.1,67.5,38.4,66,33.2,66.6,37,63.8,37.2,65.1,43.9,66.1,43.5,68.6,46.3,68.3,46.3,66.7,53.7,68.9,59.9,68.3,60.6,69.9,68.5,68.1,66.7,71,69.9,73,72.8,72.2,71.8,71.4,73.7,68.4,71.3,66.3,72.4,66.2,75.1,67.8,73.1,71.4,74.7,72.8,76.4,71.2,81.5,71.8,80.5,73.6,104.4,77.7,114.1,75.8,109.4,74.2,127,73.6,131.3,70.8,139.9,71.5,139.1,72.4,140.5,72.8,159,70.9,160.9,69.4,180,69,180,65,177.4,64.6,179.2,62.3,170.3,59.9,163.5,59.9,162,58.2,163.2,57.6,162.1,54.9,156.8,51,155.9,56.8,164.5,62.6,160.1,60.5,156.7,61.4,154.2,59.8,155,59.1,142.2,59,135.1,54.7,141.3,53.1,140.1,48.4,134.9,43.4,130.8,42.2,131,45,133.1,45.1,135,48.5,131,47.8,123.6,53.5,120.2,52.8,117.9,49.5,108.5,49.3,98.9,52,97.3,49.7,92.2,50.8,87.4,49.2,80,50.9,76.9,54.5,73.4,53.5,69.1,55.4,61.4,54,60,52,61.3,50.8,50.8,51.7,47.5,50.5,46.5,48.4,49.1,46.4}, + {93.8,81,100.2,79.8,97.8,78.8,91.2,80.3,93.8,81}, + {102.8,79.3,105.4,78.7,99.4,77.9,102.8,79.3}, + {138.8,76.1,145.1,75.6,137,75.3,138.8,76.1}, + {148.2,75.3,150.7,75.1,146.1,75.2,148.2,75.3}, + {139.9,73.4,143.6,73.2,139.9,73.4}, + {44.8,80.6,51.5,80.7,44.8,80.6}, + {22.7,54.3,19.7,54.4,22.7,54.3}, + {53.5,73.7,61.2,76.3,68.9,76.5,58.5,74.3,55.4,72.4,57.5,70.7,51.6,71.5,53.5,73.7}, + {142.9,53.7,144.7,49,143.2,49.3,143.5,46.1,142.1,46,141.7,53.3,142.9,53.7}, + {-174.9,67.2,-169.9,66,-173,64.3,-178.7,66.1,-180,65,-180,69,-174.9,67.2}, + {-178.7,70.9,-180,71.5,-177.6,71.3,-178.7,70.9}, + {33.4,46,36.5,45.5,33.9,44.4,32.5,45.3,33.4,46}, + }, + }, + { + Name: "Rwanda", + ISO: "RW", + Rings: [][]float64{ + {30.4,-1.1,29,-2.8,30.4,-1.1}, + }, + }, + { + Name: "S. Sudan", + ISO: "SS", + Rings: [][]float64{ + {30.8,3.5,23.9,8.6,25.8,10.4,31.4,9.8,33.2,12.2,33,7.8,35.3,5.5,30.8,3.5}, + }, + }, + { + Name: "Saudi Arabia", + ISO: "SA", + Rings: [][]float64{ + {35,29.4,39.2,32.2,47.5,29,52,23,55.2,22.7,55,20,47,16.9,43.4,17.6,42.8,16.3,35,29.4}, + }, + }, + { + Name: "Senegal", + ISO: "SN", + Rings: [][]float64{ + {-16.7,13.6,-17.6,14.7,-14.6,16.6,-11.5,12.4,-16.7,12.4,-13.8,13.5,-16.7,13.6}, + }, + }, + { + Name: "Serbia", + ISO: "RS", + Rings: [][]float64{ + {18.8,45.9,22.7,44.6,22.5,42.5,19.2,43.5,18.8,45.9}, + }, + }, + { + Name: "Sierra Leone", + ISO: "SL", + Rings: [][]float64{ + {-13.2,8.9,-11.1,10,-10.2,8.4,-11.4,6.8,-13.2,8.9}, + }, + }, + { + Name: "Slovakia", + ISO: "SK", + Rings: [][]float64{ + {22.6,49.1,16.9,48.5,22.6,49.1}, + }, + }, + { + Name: "Slovenia", + ISO: "SI", + Rings: [][]float64{ + {13.8,46.5,16.6,46.5,15.3,45.5,13.8,46.5}, + }, + }, + { + Name: "Solomon Is.", + ISO: "SB", + Rings: [][]float64{ + {159.6,-8,158.2,-7.4,159.6,-8}, + }, + }, + { + Name: "Somalia", + ISO: "SO", + Rings: [][]float64{ + {41.6,-1.7,41,2.8,45,5,48.9,9.5,48.9,11.4,51.1,12,48.6,5.3,41.6,-1.7}, + }, + }, + { + Name: "Somaliland", + ISO: "-99", + Rings: [][]float64{ + {48.9,11.4,47.8,8,42.6,10.6,48.9,11.4}, + }, + }, + { + Name: "South Africa", + ISO: "ZA", + Rings: [][]float64{ + {16.3,-28.6,19.9,-28.5,19.9,-24.8,21.6,-26.7,25.7,-25.5,29.4,-22.1,31.2,-22.3,31.9,-24.4,30.7,-26.7,32.8,-26.7,28.2,-32.8,20.1,-34.8,18.4,-34.1,16.3,-28.6}, + }, + }, + { + Name: "South Korea", + ISO: "KR", + Rings: [][]float64{ + {126.2,37.7,128.3,38.6,129.1,35.1,126.5,34.4,126.2,37.7}, + }, + }, + { + Name: "Spain", + ISO: "ES", + Rings: [][]float64{ + {-7.5,37.1,-6.4,41.4,-9.4,43,3,42.5,-2.1,36.7,-7.5,37.1}, + }, + }, + { + Name: "Sri Lanka", + ISO: "LK", + Rings: [][]float64{ + {81.8,7.5,80.3,6,80.1,9.8,81.8,7.5}, + }, + }, + { + Name: "Sudan", + ISO: "SD", + Rings: [][]float64{ + {24.6,8.2,21.9,12.6,25,22,36.9,22,38.4,18,34,8.7,32.7,12.2,31.4,9.8,25.1,10.3,24.6,8.2}, + }, + }, + { + Name: "Suriname", + ISO: "SR", + Rings: [][]float64{ + {-54.5,2.3,-56.5,1.9,-57.6,3.3,-57.1,6,-54,5.8,-54.5,2.3}, + }, + }, + { + Name: "Sweden", + ISO: "SE", + Rings: [][]float64{ + {11,58.9,12.6,61.3,11.9,63.1,16.8,68,20.6,69.1,23.5,67.9,23.9,66,17.8,62.7,17.1,61.3,18.8,60.1,15.9,56.1,12.9,55.4,11,58.9}, + }, + }, + { + Name: "Switzerland", + ISO: "CH", + Rings: [][]float64{ + {9.6,47.5,10.4,46.5,6,46.3,9.6,47.5}, + }, + }, + { + Name: "Syria", + ISO: "SY", + Rings: [][]float64{ + {35.7,32.7,36.7,36.8,42.3,37.2,41,34.4,35.7,32.7}, + }, + }, + { + Name: "Taiwan", + ISO: "TW", + Rings: [][]float64{ + {121.8,24.4,120.7,22,120.1,23.6,121.8,24.4}, + }, + }, + { + Name: "Tajikistan", + ISO: "TJ", + Rings: [][]float64{ + {67.8,37.1,67.7,39.6,70.7,41,69.5,39.5,73.7,39.4,75,37.4,71.8,36.7,70.8,38.5,67.8,37.1}, + }, + }, + { + Name: "Tanzania", + ISO: "TZ", + Rings: [][]float64{ + {33.9,-0.9,39.2,-4.7,39.5,-10.9,34.6,-11.5,29.6,-6.5,30.4,-1.1,33.9,-0.9}, + }, + }, + { + Name: "Thailand", + ISO: "TH", + Rings: [][]float64{ + {105.2,14.3,103,14.2,102.6,12.2,100.1,13.4,99.2,9.2,102.1,6.2,101.2,5.7,98.2,8.4,99.6,11.9,97.4,18.4,100.1,20.4,101.1,17.5,104.7,17.4,105.2,14.3}, + }, + }, + { + Name: "Timor-Leste", + ISO: "TL", + Rings: [][]float64{ + {125,-8.9,127.3,-8.4,125,-8.9}, + }, + }, + { + Name: "Togo", + ISO: "TG", + Rings: [][]float64{ + {0.9,11,1.1,5.9,0.9,11}, + }, + }, + { + Name: "Tunisia", + ISO: "TN", + Rings: [][]float64{ + {9.5,30.3,7.5,34.1,9.5,37.3,11,37.1,10.1,34.3,11.5,33.1,9.5,30.3}, + }, + }, + { + Name: "Turkey", + ISO: "TR", + Rings: [][]float64{ + {44.8,37.2,29.7,36.1,27.6,36.7,26.2,39.5,33.5,42,42.6,41.6,44.8,39.7,44.8,37.2}, + {26.1,41.8,29,41.3,26.4,40.2,26.1,41.8}, + }, + }, + { + Name: "Turkmenistan", + ISO: "TM", + Rings: [][]float64{ + {52.5,41.8,57.1,41.3,58.6,42.8,66.5,37.4,62.2,35.3,57.3,38,53.9,37.2,52.7,40,54.7,41,52.5,41.8}, + }, + }, + { + Name: "Uganda", + ISO: "UG", + Rings: [][]float64{ + {33.9,-0.9,29.6,-1.3,31.2,3.8,34.5,3.6,33.9,-0.9}, + }, + }, + { + Name: "Ukraine", + ISO: "UA", + Rings: [][]float64{ + {31.8,52.1,40.1,49.6,39.7,47.9,35,45.7,31.7,46.7,28.7,45.3,30,46.4,28.7,48.1,22.1,48.4,23.5,51.6,31.8,52.1}, + }, + }, + { + Name: "United Arab Emirates", + ISO: "AE", + Rings: [][]float64{ + {51.6,24.2,56.3,25.7,55,22.5,51.6,24.2}, + }, + }, + { + Name: "United Kingdom", + ISO: "GB", + Rings: [][]float64{ + {-6.2,53.9,-7.6,55.1,-6.2,53.9}, + {-3.1,53.4,-6.1,56.8,-5,58.6,-2,57.7,-3.1,56,1.7,52.7,1.4,51.3,-5.8,50.2,-3.4,51.4,-5.3,52,-4.6,53.5,-3.1,53.4}, + }, + }, + { + Name: "United States of America", + ISO: "US", + Rings: [][]float64{ + {-122.8,49,-88.4,48.3,-82.6,45.3,-82.7,41.7,-71.5,45,-69.2,47.4,-67,44.8,-70.1,43.7,-70,41.6,-75.5,39.5,-75.9,37.2,-76.3,39.2,-77,38.2,-75.7,35.6,-81.3,31.4,-80.4,25.2,-83.7,29.9,-86.4,30.4,-94.7,29.5,-97.5,25.8,-101,29.4,-103.9,29.3,-106.5,31.8,-117.1,32.5,-120.6,34.6,-124.4,40.3,-124.7,48.2,-122.6,47.1,-122.8,49}, + {-166.5,60.4,-165.6,59.9,-167.5,60.2,-166.5,60.4}, + {-153.2,58,-152.1,57.6,-154.5,57,-153.2,58}, + {-141,69.7,-141,60.3,-137.5,58.9,-135.5,59.8,-130,55.9,-130.5,54.8,-134.1,58.1,-139.9,59.5,-147.1,60.9,-151.7,59.2,-150.6,61.3,-158.4,56,-164.9,54.6,-157,58.9,-162,58.7,-165.3,60.5,-165.7,62.1,-160.8,64.8,-168.1,65.7,-161.7,66.1,-166.2,68.9,-156.6,71.4,-141,69.7}, + {-171.7,63.8,-168.7,63.3,-171.7,63.8}, + }, + }, + { + Name: "Uruguay", + ISO: "UY", + Rings: [][]float64{ + {-57.6,-30.2,-53.8,-32,-53.8,-34.4,-58.4,-33.9,-57.6,-30.2}, + }, + }, + { + Name: "Uzbekistan", + ISO: "UZ", + Rings: [][]float64{ + {56,41.3,55.9,45,58.5,45.6,62,43.5,64.9,43.7,68.3,40.7,71,42.3,73.1,40.9,67.7,39.6,67.8,37.1,58.6,42.8,56,41.3}, + }, + }, + { + Name: "Venezuela", + ISO: "VE", + Rings: [][]float64{ + {-60.7,5.2,-64.8,4.1,-63.4,2.2,-66.3,0.7,-67.8,2.8,-67.3,6.1,-72,7,-72.9,10.5,-71.3,11.8,-71.3,9.1,-69.9,12.2,-68.2,10.6,-61.9,10.7,-59.8,8.4,-60.7,5.2}, + }, + }, + { + Name: "Vietnam", + ISO: "VN", + Rings: [][]float64{ + {104.3,10.5,107.5,12.3,107.6,15.2,102.2,22.5,105.3,23.4,108.1,21.6,105.7,19.1,108.9,15.3,109.2,11.7,105.2,8.6,104.3,10.5}, + }, + }, + { + Name: "W. Sahara", + ISO: "EH", + Rings: [][]float64{ + {-8.7,27.7,-8.7,25.9,-12,25.9,-12.9,21.3,-17.1,21,-14.8,21.5,-11.4,26.9,-8.7,27.7}, + }, + }, + { + Name: "Yemen", + ISO: "YE", + Rings: [][]float64{ + {52,19,52.2,15.6,43.5,12.6,43.4,17.6,47,16.9,52,19}, + }, + }, + { + Name: "Zambia", + ISO: "ZM", + Rings: [][]float64{ + {30.7,-8.3,33.2,-9.7,33.2,-14,27,-17.9,23.2,-17.5,21.9,-12.9,24,-12.9,23.9,-10.9,29.7,-13.3,28.4,-9.2,30.7,-8.3}, + }, + }, + { + Name: "Zimbabwe", + ISO: "ZW", + Rings: [][]float64{ + {31.2,-22.3,28,-21.5,25.3,-17.7,30.3,-15.5,32.8,-16.7,31.2,-22.3}, + }, + }, +} diff --git a/ports/python/examples/world_probe.py b/ports/python/examples/world_probe.py new file mode 100644 index 0000000..f5b3d88 --- /dev/null +++ b/ports/python/examples/world_probe.py @@ -0,0 +1,41 @@ +"""Prints what the country lookup answers for a fixed set of points. + +The same probe exists for every port, so "the ports agree about the world" is a +diff rather than a hope. +""" + +from __future__ import annotations + +from hqtui.graphics.world import WORLD_X, WORLD_Y, country_at, degrees_at +from hqtui.widgets.world import WorldMapOptions, country_at_cell + +PLACES = [ + ("Paris", 2.35, 48.86), + ("Tokyo", 139.7, 35.7), + ("Cairo", 31.2, 30.0), + ("Brasilia", -47.9, -15.8), + ("Canberra", 149.1, -35.3), + ("Denver", -105.0, 39.7), + ("Moscow", 37.6, 55.75), + ("Delhi", 77.2, 28.6), + ("Nairobi", 36.8, -1.3), + ("Pacific", -140.0, 0.0), + ("Atlantic", -30.0, 0.0), + ("SouthernOcean", 80.0, -40.0), + ("NorthPacific", -150.0, 40.0), +] + +for name, lon, lat in PLACES: + found = country_at(lon, lat) + print(f"{name} {found.name if found else '-'}") + +# The cell path, which has to agree with what the canvas drew. +for column, row in [(173, 28), (74, 2), (20, 25), (88, 7)]: + found = country_at_cell(column, row, 200, 50, WorldMapOptions()) + print(f"cell:{column},{row} {found.name if found else '-'}") + +# And the projection itself, so a drift shows up as a number rather than as a +# country that happens to still be right. +for column, row in [(0, 0), (99, 25), (50, 13)]: + lon, lat = degrees_at(column, row, 100, 26, WORLD_X, WORLD_Y) + print(f"degrees:{column},{row} {lon:.4f} {lat:.4f}") diff --git a/ports/python/hqtui/graphics/__init__.py b/ports/python/hqtui/graphics/__init__.py index 9bf5a80..36801d7 100644 --- a/ports/python/hqtui/graphics/__init__.py +++ b/ports/python/hqtui/graphics/__init__.py @@ -15,6 +15,18 @@ vertical_glyph, ) from .braille import BrailleCanvas +from .world import ( + WORLD_COUNTRIES, + WORLD_X, + WORLD_Y, + CountryOutline, + WorldShapeOptions, + country_at, + country_bounds, + degrees_at, + find_country, + world_shapes, +) from .canvas import ( Bounds, CanvasOptions, @@ -54,6 +66,16 @@ ) __all__ = [ + "WORLD_COUNTRIES", + "WORLD_X", + "WORLD_Y", + "CountryOutline", + "WorldShapeOptions", + "country_at", + "country_bounds", + "degrees_at", + "find_country", + "world_shapes", "Bounds", "CanvasOptions", "Projection", diff --git a/ports/python/hqtui/graphics/world.py b/ports/python/hqtui/graphics/world.py new file mode 100644 index 0000000..d4f3de0 --- /dev/null +++ b/ports/python/hqtui/graphics/world.py @@ -0,0 +1,183 @@ +"""The world, as shapes for the canvas, and the lookup that makes it clickable. + +The canvas already draws in the caller's own coordinates, and longitude and +latitude are just another pair of axes — so a map is a list of polylines in +degrees, and nothing here needs a projection of its own beyond deciding which +window on the globe to show. + +The interesting half is the other direction. A click arrives as a terminal cell, +and a country is a polygon, so answering "what did they click" means turning the +cell back into degrees and testing it against the outlines. Doing it that way +rather than with bounding boxes is what makes the answer right: Russia's +bounding box covers most of the northern hemisphere, and Chile's covers +Argentina. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field +from typing import Sequence + +from ..color import Color +from .canvas import Bounds, Shape +from .world_data import CountryOutline, WORLD_COUNTRIES + +__all__ = [ + "WORLD_COUNTRIES", + "WORLD_X", + "WORLD_Y", + "CountryOutline", + "WorldShapeOptions", + "country_at", + "country_bounds", + "degrees_at", + "find_country", + "world_shapes", +] + +#: The whole globe, which is what a map shows unless told otherwise. +WORLD_X = Bounds(-180.0, 180.0) +WORLD_Y = Bounds(-90.0, 90.0) + + +@dataclass(frozen=True, slots=True) +class WorldShapeOptions: + #: Colour for countries with nothing special about them. + color: Color | None = None + #: Countries to pick out, by name or ISO code. + highlight: Sequence[str] = () + highlight_color: Color | None = None + + +def _matches(country: CountryOutline, keys: Sequence[str]) -> bool: + """Match on either the name or the ISO code, case-insensitively.""" + for key in keys: + if not key: + continue + if country.name.lower() == key.lower(): + return True + if country.iso and country.iso.lower() == key.lower(): + return True + return False + + +def world_shapes(options: WorldShapeOptions = WorldShapeOptions()) -> list[Shape]: + """The world as canvas shapes, one polyline per landmass. + + Polylines rather than scattered points: the outlines are closed rings, so + joining them draws a coastline instead of a dotted suggestion of one, and it + reads at a fraction of the resolution dots would need. + """ + shapes: list[Shape] = [] + for country in WORLD_COUNTRIES: + picked = bool(options.highlight) and _matches(country, options.highlight) + color = options.highlight_color if picked else options.color + if picked and color is None: + color = options.color + for ring in country.rings: + points = [(ring[i], ring[i + 1]) for i in range(0, len(ring) - 1, 2)] + # Closed: the last point joins the first, or every country has a gap + # in its coastline where the ring started. + if points: + points.append(points[0]) + shapes.append(Shape(kind="polyline", points=tuple(points), color=color)) + return shapes + + +def _inside_ring(ring: Sequence[float], lon: float, lat: float) -> bool: + """Whether a point is inside a ring, by ray casting. + + The ring is a flat list of interleaved coordinates, so this walks it two at + a time rather than allocating a pair per vertex — it runs once per country + per click, and there are a couple of thousand vertices. + """ + inside = False + n = len(ring) // 2 + if n == 0: + return False + j = n - 1 + for i in range(n): + xi, yi = ring[i * 2], ring[i * 2 + 1] + xj, yj = ring[j * 2], ring[j * 2 + 1] + if (yi > lat) != (yj > lat) and lon < (xj - xi) * (lat - yi) / (yj - yi) + xi: + inside = not inside + j = i + return inside + + +def country_at(lon: float, lat: float) -> CountryOutline | None: + """The country containing a point, or None for open water. + + Where outlines overlap — and at this resolution simplified borders do + overlap — the first match wins, which is stable because the data is sorted + by name. + """ + if not math.isfinite(lon) or not math.isfinite(lat): + return None + for country in WORLD_COUNTRIES: + for ring in country.rings: + if _inside_ring(ring, lon, lat): + return country + return None + + +def find_country(key: str) -> CountryOutline | None: + """Look a country up by name or ISO code.""" + for country in WORLD_COUNTRIES: + if _matches(country, (key,)): + return country + return None + + +def country_bounds(country: CountryOutline, margin: float = 0.08) -> tuple[Bounds, Bounds]: + """The window a country fills, with a little room around it. + + For zooming a map to a country: the bounding box alone puts the coastline + flat against the edge of the panel, which reads as though the country has + been cut off rather than framed. + """ + min_lon = min_lat = math.inf + max_lon = max_lat = -math.inf + for ring in country.rings: + for i in range(0, len(ring) - 1, 2): + min_lon = min(min_lon, ring[i]) + max_lon = max(max_lon, ring[i]) + min_lat = min(min_lat, ring[i + 1]) + max_lat = max(max_lat, ring[i + 1]) + if not math.isfinite(min_lon): + return WORLD_X, WORLD_Y + # A single-point country would give a zero-width window, which cannot be + # mapped onto anything. + pad_x = max((max_lon - min_lon) * margin, 1.0) + pad_y = max((max_lat - min_lat) * margin, 1.0) + return ( + Bounds(min_lon - pad_x, max_lon + pad_x), + Bounds(min_lat - pad_y, max_lat + pad_y), + ) + + +def degrees_at( + column: int, + row: int, + width: int, + height: int, + x: Bounds = WORLD_X, + y: Bounds = WORLD_Y, +) -> tuple[float, float] | None: + """The degrees under a terminal cell, given the window the map was drawn with. + + The inverse of what the canvas does on the way in, taken at the centre of + the cell: a click lands on a whole cell, and the centre is the only point in + it that is not arbitrarily nearer one neighbour than the other. + """ + if width <= 0 or height <= 0: + return None + # The canvas is 2x4 Braille pixels per cell, and it spans its bounds across + # ``pixels - 1``, so the inverse has to use the same denominators or a click + # drifts from what was drawn. + px = max(1, width * 2 - 1) + py = max(1, height * 4 - 1) + lon = x.min + ((column * 2 + 1) / px) * (x.max - x.min) + lat = y.min + (1 - (row * 4 + 2) / py) * (y.max - y.min) + return lon, lat diff --git a/ports/python/hqtui/graphics/world_data.py b/ports/python/hqtui/graphics/world_data.py new file mode 100644 index 0000000..21d2fca --- /dev/null +++ b/ports/python/hqtui/graphics/world_data.py @@ -0,0 +1,1314 @@ +""" + Country outlines, flattened for a terminal. + + Generated by packages/hqtui/scripts/generate-world.ts from Natural Earth's + 1:110m Admin 0 countries, which is public domain. Do not edit by hand. + + Each ring is longitude and latitude interleaved -- lon, lat, lon, lat -- + rather than a list of pairs, because at 1982 points the nested form + costs a container per coordinate for no gain. A country has more than one + ring when it is more than one landmass. + + 171 countries, 1982 points, simplified at 1 degrees. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class CountryOutline: + name: str + #: ISO 3166-1 alpha-2, where Natural Earth has one. + iso: str + #: Longitude and latitude, interleaved. + rings: tuple[tuple[float, ...], ...] + + +WORLD_COUNTRIES: tuple[CountryOutline, ...] = ( + CountryOutline( + "Afghanistan", + "AF", + ( + (66.5,37.4,70.8,38.5,71.8,36.7,75.2,37.1,71.3,36.1,69.3,31.9,66.3,29.9,60.9,29.8,61.2,35.7,66.5,37.4), + ), + ), + CountryOutline( + "Albania", + "AL", + ( + (21,40.8,19.4,40.3,19.7,42.7,21,40.8), + ), + ), + CountryOutline( + "Algeria", + "DZ", + ( + (-8.7,27.4,-8.7,28.8,-1.3,32.3,-1.2,35.7,8.4,36.9,7.5,34.1,9.8,29.4,9.3,26.1,12,23.5,3.2,19.1,-8.7,27.4), + ), + ), + CountryOutline( + "Angola", + "AO", + ( + (12.3,-6.1,16.3,-5.9,17.5,-8.1,21.7,-7.3,22.2,-11.1,24,-11.2,24,-12.9,21.9,-12.9,23.2,-17.5,11.7,-17.3,13.7,-11.3,12.3,-6.1), + ), + ), + CountryOutline( + "Antarctica", + "AQ", + ( + (-48.7,-78,-43.9,-78.5,-43.3,-80,-54.2,-80.6,-48.7,-78), + (-66.3,-80.3,-59.6,-80,-66.3,-80.3), + (-73.9,-71.3,-70.3,-68.9,-68.3,-71.4,-75,-72.1,-73.9,-71.3), + (-102.3,-71.9,-96.2,-72.5,-102.3,-71.9), + (-122.6,-73.7,-118.7,-73.5,-122.6,-73.7), + (-127.3,-73.5,-124,-73.9,-127.3,-73.5), + (-163.7,-78.6,-159.2,-79.5,-163.7,-78.6), + (180,-84.7,180,-90,-180,-90,-179.1,-84.1,-143.1,-85,-153.6,-83.7,-152.9,-82,-156.8,-81.1,-146.4,-80.3,-155.3,-79.1,-158.4,-76.9,-151.3,-77.4,-146.1,-76.5,-146.2,-75.4,-135.2,-74.3,-100.1,-74.9,-103.7,-72.6,-74.9,-73.9,-67.4,-72.5,-67.7,-67.3,-57.8,-63.3,-65.7,-68,-61.8,-70.7,-60.8,-73.7,-70.6,-76.6,-77.2,-76.7,-73.7,-77.9,-78,-79.2,-58.2,-83.2,-28.5,-80.3,-35.6,-79.5,-35.8,-78.3,-17.5,-75.1,-15.4,-73.1,-6.9,-70.9,27.1,-70.5,33.9,-68.5,38.6,-69.8,54.5,-65.8,61.4,-68,68.9,-67.9,69.7,-69.2,67.9,-71.9,69.9,-72.3,73.9,-69.9,88,-66.2,95.8,-67.4,102.8,-65.6,106.2,-66.9,113.6,-65.9,119.8,-67.3,135.1,-65.3,137.5,-67,145.5,-66.9,171.2,-71.7,163.6,-76.2,167,-78.8,161.8,-79.2,159.8,-80.9,169.4,-83.8,180,-84.7), + ), + ), + CountryOutline( + "Argentina", + "AR", + ( + (-68.6,-52.6,-65,-54.7,-68.6,-54.9,-68.6,-52.6), + (-57.6,-30.2,-58.5,-34.4,-56.8,-36.9,-62.3,-38.8,-62.7,-41,-65.1,-41.1,-63.5,-42.6,-67.3,-45.6,-65.6,-47.2,-69.1,-50.7,-68.1,-52.3,-71.9,-52,-73.4,-49.3,-71.2,-44.8,-72.1,-42.3,-68.4,-24.5,-66.3,-21.8,-62.8,-22,-57.8,-25.2,-58.6,-27.1,-55.7,-27.4,-54.1,-25.5,-53.6,-26.9,-57.6,-30.2), + ), + ), + CountryOutline( + "Armenia", + "AM", + ( + (46.5,38.8,43.6,41.1,45.6,40.8,46.5,38.8), + ), + ), + CountryOutline( + "Australia", + "AU", + ( + (147.7,-40.8,147.9,-43.2,146,-43.5,144.7,-40.7,147.7,-40.8), + (126.1,-32.2,118,-35.1,115,-34.2,113.7,-22.5,120.9,-19.7,125.7,-14.2,129.6,-15,132.4,-11.1,136.5,-11.9,135.5,-15,140.2,-17.7,142.5,-10.7,146.4,-19,150.7,-22.4,153.6,-28.1,150,-37.4,146.3,-39,140.6,-38,138.2,-34.4,136.8,-35.3,137.8,-32.9,136,-34.9,131.3,-31.5,126.1,-32.2), + ), + ), + CountryOutline( + "Austria", + "AT", + ( + (17,48.1,14.6,46.4,9.5,47.1,12.9,47.5,13.6,48.9,17,48.1), + ), + ), + CountryOutline( + "Azerbaijan", + "AZ", + ( + (46.4,41.9,50.4,40.3,48.9,38.3,45.6,39.9,45,41.2,46.4,41.9), + ), + ), + CountryOutline( + "Bahamas", + "BS", + ( + (-78.2,25.2,-77.5,23.8,-78.2,25.2), + ), + ), + CountryOutline( + "Bangladesh", + "BD", + ( + (92.7,22,92.4,20.7,91.4,22.8,89,22.1,88.6,26.4,92.4,25,91.2,23.5,92.7,22), + ), + ), + CountryOutline( + "Belarus", + "BY", + ( + (28.2,56.2,30.9,55.6,32.7,53.4,31.8,52.1,23.5,51.6,23.5,53.9,28.2,56.2), + ), + ), + CountryOutline( + "Belgium", + "BE", + ( + (6.2,50.8,5.7,49.5,2.5,51.1,6.2,50.8), + ), + ), + CountryOutline( + "Belize", + "BZ", + ( + (-89.1,17.8,-88.1,18.3,-88.9,15.9,-89.1,17.8), + ), + ), + CountryOutline( + "Benin", + "BJ", + ( + (2.7,6.3,0.8,10.5,2.8,12.2,2.7,6.3), + ), + ), + CountryOutline( + "Bhutan", + "BT", + ( + (91.7,27.8,88.8,27.1,91.7,27.8), + ), + ), + CountryOutline( + "Bolivia", + "BO", + ( + (-69.5,-11,-65.3,-9.8,-65.4,-11.6,-60.5,-13.8,-60.2,-16.3,-58.2,-16.3,-57.9,-20,-61.8,-19.6,-62.7,-22.2,-67.8,-22.9,-69.5,-11), + ), + ), + CountryOutline( + "Bosnia and Herz.", + "BA", + ( + (18.6,42.7,16,45.2,19.4,44.9,18.6,42.7), + ), + ), + CountryOutline( + "Botswana", + "BW", + ( + (29.4,-22.1,25.7,-25.5,21.6,-26.7,19.9,-24.8,20.9,-18.3,25.3,-17.7,29.4,-22.1), + ), + ), + CountryOutline( + "Brazil", + "BR", + ( + (-53.4,-33.8,-53.8,-32,-57.6,-30.2,-53.6,-26.1,-55.8,-22.4,-57.9,-22.1,-58.2,-16.3,-60.2,-16.3,-60.5,-13.8,-65.4,-11.6,-65.3,-9.8,-70.5,-11,-70.5,-9.5,-72.2,-10.1,-74,-7.5,-72.9,-5.3,-69.9,-4.3,-69.8,1.7,-65.5,0.8,-63.4,2.2,-64.8,4.1,-60.7,5.2,-59,1.3,-52.9,2.1,-51.3,4.2,-50.4,-0.1,-44.6,-2.7,-40,-2.9,-35.6,-5.1,-34.7,-7.3,-38.7,-13.1,-40.9,-21.9,-47.6,-24.9,-48.9,-28.7,-53.4,-33.8), + ), + ), + CountryOutline( + "Bulgaria", + "BG", + ( + (22.7,44.2,28.6,43.7,28,42,23,41.3,22.7,44.2), + ), + ), + CountryOutline( + "Burkina Faso", + "BF", + ( + (-5.4,10.4,-4.3,13.2,-1.1,15,2.2,12.6,0.9,11,-2.9,11,-2.8,9.6,-5.4,10.4), + ), + ), + CountryOutline( + "Burundi", + "BI", + ( + (30.5,-2.4,29.3,-4.5,29,-2.8,30.5,-2.4), + ), + ), + CountryOutline( + "Cambodia", + "KH", + ( + (102.6,12.2,103,14.2,107.6,13.5,106.2,11,103.5,10.6,102.6,12.2), + ), + ), + CountryOutline( + "Cameroon", + "CM", + ( + (14.5,12.9,14.5,4.7,15.9,1.7,9.6,2.3,8.8,5.5,11.7,7,14.5,12.9), + ), + ), + CountryOutline( + "Canada", + "CA", + ( + (-122.8,49,-127.4,50.8,-130.5,54.3,-130,55.9,-135.5,59.8,-137.5,58.9,-141,60.3,-141,69.7,-136.5,68.9,-128.1,70.5,-113.5,67.7,-106.1,68.8,-101.5,67.6,-97.7,68.6,-96.1,67.3,-94.2,69.1,-96.5,70.1,-95.2,71.9,-87.4,67.2,-85.5,69.9,-82.6,69.7,-81.4,67.1,-85.8,66.6,-90.7,63.6,-94.7,58.9,-92.3,57.1,-82.3,55.1,-79.9,51.2,-78.6,52.6,-79.8,54.7,-76.5,56.5,-78.5,58.8,-77.3,59.9,-78.1,62.3,-73.8,62.4,-69.6,61.1,-67.6,58.2,-64.6,60.3,-61.8,56.3,-57.3,54.6,-55.7,52.1,-60,50.2,-66.4,50.2,-71.1,46.8,-65.1,49.2,-64.5,46.2,-60.5,47,-59.8,45.9,-65.4,43.5,-66.2,44.5,-64.4,45.3,-67.1,45.1,-69.2,47.4,-71.5,45,-82.4,41.7,-82.6,45.3,-88.4,48.3,-122.8,49), + (-84,62.5,-81.9,62.9,-84,62.5), + (-79.8,72.8,-80.8,73.7,-76.3,72.8,-79.8,72.8), + (-93.6,75,-96.8,74.9,-93.6,75), + (-93.8,77.5,-96.4,77.8,-93.8,77.5), + (-96.8,78.8,-95.6,78.4,-98.6,78.9,-96.8,78.8), + (-88.2,74.4,-97.1,76.8,-79.8,74.9,-88.2,74.4), + (-111.3,78.2,-109.9,78,-113.5,77.7,-111.3,78.2), + (-111,78.8,-109.7,78.6,-112.5,78.4,-111,78.8), + (-55.6,51.3,-56.8,49.8,-53.5,49.2,-53.1,46.7,-59.3,47.6,-55.6,51.3), + (-83.9,65.1,-80.1,63.7,-87.2,63.5,-85.9,65.7,-83.9,65.1), + (-78.8,72.4,-68.8,70.5,-67,69.2,-68.8,68.7,-61.9,66.9,-63.9,65,-68,66.3,-64.7,63.4,-68.8,63.7,-66.2,61.9,-68.9,62.3,-78.6,64.6,-74,65.5,-73.3,68.1,-79,70.2,-88.7,70.4,-90.2,72.2,-85.8,73.8,-85.8,72.5,-82.3,73.8,-78.8,72.4), + (-94.5,74.1,-90.5,73.9,-95.4,72.1,-96,73.4,-94.5,74.1), + (-122.9,76.1,-116.2,77.6,-122.9,76.1), + (-132.7,54,-131.2,52.2,-132.7,54), + (-105.5,79.3,-99.7,77.9,-105.5,79.3), + (-123.5,48.5,-128.4,50.8,-123.5,48.5), + (-121.5,74.4,-115.5,73.5,-123.1,70.9,-125.9,71.9,-123.9,73.7,-124.9,74.3,-121.5,74.4), + (-107.8,75.8,-105.7,75.5,-117.7,75.2,-115.4,76.5,-107.8,75.8), + (-106.5,73.1,-101.1,69.6,-113.3,68.5,-117.3,70,-112.4,70.4,-119.4,71.6,-115.2,73.3,-108.2,71.7,-108.4,73.1,-106.5,73.1), + (-100.4,72.7,-101.5,73.4,-97.4,73.8,-96.5,72.6,-98.4,71.3,-102.5,72.5,-100.4,72.7), + (-106.6,73.6,-104.5,73.4,-106.6,73.6), + (-98.5,76.7,-98.2,75,-102.5,75.6,-98.5,76.7), + (-96,80.6,-92.4,81.3,-85.8,79.3,-92.9,78.3,-96,80.6), + (-91.6,81.9,-61.8,82.6,-76.9,79.3,-75.4,78.5,-80.6,76.2,-89.5,76.5,-88.3,77.9,-85,77.5,-88,78.4,-85.1,79.3,-86.9,80.3,-81.8,80.5,-91.6,81.9), + (-75.2,67.4,-77.2,67.6,-75.2,67.4), + (-96.3,69.5,-99.8,69.4,-96.3,69.5), + (-64.5,49.9,-61.8,49.1,-64.5,49.9), + (-64,47,-62,46.4,-64,47), + ), + ), + CountryOutline( + "Central African Rep.", + "CF", + ( + (27.4,5.2,22.4,4,19.5,5,16,2.3,14.5,5.5,15.3,7.4,22.9,11.1,27.4,5.2), + ), + ), + CountryOutline( + "Chad", + "TD", + ( + (23.8,19.6,23.9,15.6,21.9,12.6,22.9,11.1,15.3,7.4,13.5,14.4,15.9,20.4,14.9,22.9,23.8,19.6), + ), + ), + CountryOutline( + "Chile", + "CL", + ( + (-68.6,-52.6,-68.6,-54.9,-67,-54.9,-68.1,-55.6,-74.7,-52.8,-71.1,-54.1,-68.6,-52.6), + (-69.6,-17.6,-67,-23,-70.5,-31.4,-69.8,-34.2,-72.1,-42.3,-71.2,-44.8,-73.4,-49.3,-71.9,-52,-68.6,-52.3,-71.4,-53.9,-74.9,-52.3,-75.6,-48.7,-74.1,-46.9,-75.6,-46.6,-72.7,-42.4,-74.3,-43.2,-69.6,-17.6), + ), + ), + CountryOutline( + "China", + "CN", + ( + (109.5,18.2,108.6,19.4,110.8,20.1,109.5,18.2), + (80.3,42.3,80,44.9,87.8,49.3,91,46.9,90.9,45.3,96.3,42.7,109.2,42.5,111.9,45.1,119.7,46.7,115.5,48.1,122.2,53.4,125.9,52.8,131,47.8,135,48.5,133.1,45.1,131,45,130.6,42.4,121.1,38.9,121.6,40.9,117.5,38.7,122.4,37.5,119.2,34.9,121.9,31.7,121.7,28.2,118.7,24.5,110.4,20.3,105.3,23.4,101.7,22.3,101.8,21.2,99.2,22.1,97.6,23.9,98.7,27.5,96.1,29.5,88.8,27.3,78.7,31.5,78.9,34.3,73.7,39.4,80.3,42.3), + ), + ), + CountryOutline( + "Colombia", + "CO", + ( + (-66.9,1.3,-69.8,1.7,-69.9,-4.3,-70,-2.7,-77.4,0.4,-79,1.7,-77.1,3.8,-77.5,8.5,-71.4,12.4,-73.3,9.2,-72,7,-67.3,6.1,-66.9,1.3), + ), + ), + CountryOutline( + "Congo", + "CG", + ( + (18.5,3.5,16,-3.5,11.9,-5,11.5,-2.8,14.4,-1.3,13.1,2.3,15.9,1.7,18.5,3.5), + ), + ), + CountryOutline( + "Costa Rica", + "CR", + ( + (-82.5,9.6,-83,8.2,-85.9,10.9,-82.5,9.6), + ), + ), + CountryOutline( + "Côte d'Ivoire", + "CI", + ( + (-8,10.2,-2.8,9.6,-2.9,5,-7.7,4.4,-8,10.2), + ), + ), + CountryOutline( + "Croatia", + "HR", + ( + (16.6,46.5,19.4,45.2,15.8,44.8,18.5,42.5,13.7,45.1,16.6,46.5), + ), + ), + CountryOutline( + "Cuba", + "CU", + ( + (-82.3,23.2,-74.2,20.3,-77.8,19.9,-81.8,22.6,-85,21.9,-82.3,23.2), + ), + ), + CountryOutline( + "Cyprus", + "CY", + ( + (32.7,35.1,34,35,32.7,35.1), + ), + ), + CountryOutline( + "Czechia", + "CZ", + ( + (15,51.1,18.9,49.5,12.5,49.5,15,51.1), + ), + ), + CountryOutline( + "Dem. Rep. Congo", + "CD", + ( + (29.3,-4.5,30.7,-8.3,28.7,-8.5,28.4,-11.8,29.7,-13.3,22.2,-11.1,21.7,-7.3,17.5,-8.1,16.3,-5.9,12.2,-5.8,16,-3.5,19.5,5,29.7,4.6,31.2,2.2,29.3,-4.5), + ), + ), + CountryOutline( + "Denmark", + "DK", + ( + (9.9,55,8.1,56.5,10.6,57.7,9.9,55), + (12.4,56.1,12.1,54.8,11,55.4,12.4,56.1), + ), + ), + CountryOutline( + "Djibouti", + "DJ", + ( + (42.4,12.5,42.8,10.9,42.4,12.5), + ), + ), + CountryOutline( + "Dominican Rep.", + "DO", + ( + (-71.7,18,-71.6,19.9,-68.3,18.6,-71.7,18), + ), + ), + CountryOutline( + "Ecuador", + "EC", + ( + (-75.4,-0.2,-78.6,-4.5,-80.4,-4.4,-80.1,0.8,-75.4,-0.2), + ), + ), + CountryOutline( + "Egypt", + "EG", + ( + (36.9,22,25,22,25.2,31.6,34.3,31.2,34.2,27.8,32.3,29.8,36.9,22), + ), + ), + CountryOutline( + "El Salvador", + "SV", + ( + (-89.4,14.4,-87.9,13.1,-90.1,13.7,-89.4,14.4), + ), + ), + CountryOutline( + "Eq. Guinea", + "GQ", + ( + (9.6,2.3,11.3,1.1,9.5,1,9.6,2.3), + ), + ), + CountryOutline( + "Eritrea", + "ER", + ( + (36.4,14.4,38.4,18,43.1,12.7,36.4,14.4), + ), + ), + CountryOutline( + "Estonia", + "EE", + ( + (28,59.5,27.3,57.5,23.3,59.2,28,59.5), + ), + ), + CountryOutline( + "eSwatini", + "SZ", + ( + (32.1,-26.7,31,-25.7,32.1,-26.7), + ), + ), + CountryOutline( + "Ethiopia", + "ET", + ( + (47.8,8,45,5,39.6,3.4,36.2,4.4,33,7.8,37.9,15,41.6,13.5,43.7,9.2,47.8,8), + ), + ), + CountryOutline( + "Falkland Is.", + "FK", + ( + (-61.2,-51.8,-57.7,-51.5,-61.2,-51.8), + ), + ), + CountryOutline( + "Finland", + "FI", + ( + (28.6,69.1,31.1,62.4,28.1,60.5,21.3,60.7,21.5,63.2,25.4,65.1,20.6,69.1,24.7,68.6,27.7,70.2,28.6,69.1), + ), + ), + CountryOutline( + "Fr. S. Antarctic Lands", + "TF", + ( + (68.9,-48.6,70.6,-49.3,68.7,-49.8,68.9,-48.6), + ), + ), + CountryOutline( + "France", + "FR", + ( + (-51.7,4.2,-52.9,2.1,-54.5,2.3,-54,5.8,-51.7,4.2), + (6.2,49.5,8.1,49,6,46.7,7.4,43.7,1.8,42.3,-1.9,43.4,-1.2,46,-4.6,48.7,-1.6,48.6,-1.9,49.8,2.5,51.1,6.2,49.5), + (8.7,42.6,9.2,41.4,8.7,42.6), + ), + ), + CountryOutline( + "Gabon", + "GA", + ( + (11.3,2.3,14.3,1.2,14.4,-1.3,11.1,-4,8.8,-1.1,11.3,2.3), + ), + ), + CountryOutline( + "Gambia", + "GM", + ( + (-16.7,13.6,-13.8,13.5,-16.7,13.6), + ), + ), + CountryOutline( + "Georgia", + "GE", + ( + (40,43.4,46.6,41.2,41.6,41.5,40,43.4), + ), + ), + CountryOutline( + "Germany", + "DE", + ( + (14.1,53.8,15,51.1,12.2,50.3,12.9,47.5,7.5,47.6,8.1,49,6,50.1,7.1,53.7,9.9,55,14.1,53.8), + ), + ), + CountryOutline( + "Ghana", + "GH", + ( + (0,11,1.1,5.9,-2.9,5,-2.9,11,0,11), + ), + ), + CountryOutline( + "Greece", + "GR", + ( + (26.3,35.3,23.5,35.3,26.3,35.3), + (23,41.3,26.6,41.6,22.6,40.3,24,37.7,22.5,36.4,20.2,39.6,23,41.3), + ), + ), + CountryOutline( + "Greenland", + "GL", + ( + (-46.8,82.6,-27.1,83.5,-20.8,82.7,-31.9,82.2,-12.2,81.3,-20,80.2,-17.7,80.1,-19.7,78.8,-18.5,77,-21.7,76.6,-19.4,74.3,-24.8,72.3,-21.8,70.7,-25.5,71.4,-26.4,70.2,-22.3,70.1,-39.8,65.5,-43.4,60.1,-48.3,60.9,-51.6,63.6,-54,67.2,-50.9,69.9,-54.7,69.6,-54.4,70.8,-51.4,70.6,-55.8,71.7,-54.7,72.6,-58.6,75.5,-68.5,76.1,-71.4,77,-66.8,77.4,-73.3,78,-65.7,79.4,-68,80.1,-62.7,81.8,-44.5,81.7,-46.8,82.6), + ), + ), + CountryOutline( + "Guatemala", + "GT", + ( + (-92.2,14.5,-90.5,16.1,-91,17.8,-89.1,17.8,-88.2,15.7,-89.4,14.4,-92.2,14.5), + ), + ), + CountryOutline( + "Guinea", + "GN", + ( + (-13.7,12.6,-9.1,12.3,-8.3,7.7,-11.1,10,-13.2,8.9,-15.1,11,-13.7,12.6), + ), + ), + CountryOutline( + "Guinea-Bissau", + "GW", + ( + (-16.7,12.4,-13.7,11.8,-15.1,11,-16.7,12.4), + ), + ), + CountryOutline( + "Guyana", + "GY", + ( + (-56.5,1.9,-59.6,1.8,-61.4,6,-59.8,8.4,-57.1,6,-58,4.1,-56.5,1.9), + ), + ), + CountryOutline( + "Haiti", + "HT", + ( + (-71.7,19.7,-71.7,18,-74.5,18.3,-71.7,19.7), + ), + ), + CountryOutline( + "Honduras", + "HN", + ( + (-83.1,15,-87.3,13,-89.4,14.4,-87.9,15.9,-83.1,15), + ), + ), + CountryOutline( + "Hungary", + "HU", + ( + (22.1,48.4,21,46.3,16.2,46.9,17,48.1,22.1,48.4), + ), + ), + CountryOutline( + "Iceland", + "IS", + ( + (-14.5,66.5,-13.6,65.1,-18.7,63.5,-22.8,64,-21.8,64.4,-24,64.9,-22.2,65.4,-24.3,65.6,-14.5,66.5), + ), + ), + CountryOutline( + "India", + "IN", + ( + (97.3,28.3,92.7,22,91.2,23.5,92.4,25,88.6,26.4,88.9,21.7,80.3,15.9,79.9,10.4,77.5,8,72.6,21.4,70.5,20.9,68.2,23.7,71,24.4,69.5,26.9,75.3,32.3,73.7,34.3,77.8,35.5,78.7,31.5,81.1,30.2,80.1,28.8,83.3,27.4,88.1,26.4,88.7,28.1,92,26.8,96.1,29.5,97.3,28.3), + ), + ), + CountryOutline( + "Indonesia", + "ID", + ( + (141,-2.6,141,-9.1,137.6,-8.4,137.9,-5.4,133,-4.1,132,-2.8,133.7,-2.2,130.5,-0.9,134,-0.8,135.5,-3.4,137.4,-1.7,141,-2.6), + (125,-8.9,123.5,-10.2,125,-8.9), + (117.9,4.1,119,0.9,116.1,-4,110.2,-2.9,109.7,2,110.5,0.8,113.8,1.2,115.9,4.3,117.9,4.1), + (129.4,-2.8,130.8,-3.9,127.9,-3.4,129.4,-2.8), + (127.9,2.2,128.1,-0.9,127.9,2.2), + (122.9,0.9,125.2,1.4,120,-0.5,123.3,-0.6,121.5,-1.9,123.2,-5.3,121,-2.6,119.8,-5.7,118.8,-2.8,119.8,0.2,122.9,0.9), + (120.3,-10.3,119,-9.6,120.3,-10.3), + (121.3,-8.5,122.9,-8.1,119.9,-8.8,121.3,-8.5), + (118.3,-8.4,116.7,-9,118.3,-8.4), + (108.5,-6.4,115.7,-8.4,105.4,-6.9,108.5,-6.4), + (104.4,-1.1,106.1,-3.1,105.8,-5.9,102.6,-4.2,95.3,5.5,97.5,5.2,104.4,-1.1), + ), + ), + CountryOutline( + "Iran", + "IR", + ( + (48.6,29.9,45.4,34,46.1,35.7,44.1,39.4,48.1,39.6,50.8,36.9,56.6,38.1,61.1,36.5,60.9,29.8,63.3,26.8,61.5,25.1,57.4,25.7,48.6,29.9), + ), + ), + CountryOutline( + "Iraq", + "IQ", + ( + (39.2,32.2,41.3,36.4,44.8,37.2,48.6,29.9,44.7,29.2,39.2,32.2), + ), + ), + CountryOutline( + "Ireland", + "IE", + ( + (-6.2,53.9,-6.8,52.3,-10,51.8,-7.6,55.1,-6.2,53.9), + ), + ), + CountryOutline( + "Israel", + "IL", + ( + (35.7,32.7,34.9,29.5,34.3,31.2,35.7,32.7), + ), + ), + CountryOutline( + "Italy", + "IT", + ( + (10.4,46.9,13.8,46.5,12.6,44.1,18.3,39.8,16.9,40.4,15.7,37.9,15.4,40,10.2,43.9,7.4,43.7,6.8,46,10.4,46.9), + (14.8,38.1,15.1,36.6,12.4,37.6,14.8,38.1), + (8.7,40.9,9.8,40.5,8.8,38.9,8.7,40.9), + ), + ), + CountryOutline( + "Jamaica", + "JM", + ( + (-77.6,18.5,-76.2,17.9,-77.6,18.5), + ), + ), + CountryOutline( + "Japan", + "JP", + ( + (141.9,39.2,140.3,35.1,135.8,33.5,135.1,34.6,131,33.9,132,33.1,130.2,31.4,129.4,33.3,139.4,38.2,140.3,41.2,141.9,39.2), + (144.6,44,145.5,43.3,140,41.6,142,45.6,144.6,44), + (132.4,33.5,134.8,33.8,132.4,33.5), + ), + ), + CountryOutline( + "Jordan", + "JO", + ( + (35.5,32.4,38.8,33.4,39.2,32.2,37,31.5,38,30.5,36.1,29.2,34.9,29.5,35.5,32.4), + ), + ), + CountryOutline( + "Kazakhstan", + "KZ", + ( + (87.4,49.2,80,44.9,80.3,42.3,74.2,43.3,68.6,40.7,64.9,43.7,62,43.5,58.5,45.6,55.9,45,56,41.3,52.5,41.8,50.3,44.6,53,45.3,53,46.9,49.1,46.4,46.5,48.4,50.8,51.7,61.3,50.8,60,52,61.4,54,69.1,55.4,73.4,53.5,76.9,54.5,80,50.9,87.4,49.2), + ), + ), + CountryOutline( + "Kenya", + "KE", + ( + (39.2,-4.7,33.9,-0.9,35.3,5.5,38.1,3.6,41.9,3.9,41.6,-1.7,39.2,-4.7), + ), + ), + CountryOutline( + "Kosovo", + "XK", + ( + (20.6,41.9,20.6,43.2,21.8,42.7,20.6,41.9), + ), + ), + CountryOutline( + "Kuwait", + "KW", + ( + (48,30,48.4,28.6,46.6,29.1,48,30), + ), + ), + CountryOutline( + "Kyrgyzstan", + "KG", + ( + (71,42.3,74.2,43.3,80.3,42.3,73.7,39.4,69.5,39.5,73.1,40.9,71,42.3), + ), + ), + CountryOutline( + "Laos", + "LA", + ( + (107.4,14.2,105.2,14.3,104,18.2,101.1,17.5,100.1,20.4,101.7,22.3,104.4,20.8,103.9,19.3,107.4,14.2), + ), + ), + CountryOutline( + "Latvia", + "LV", + ( + (27.3,57.5,28.2,56.2,26.5,55.6,21.1,56,22.5,57.8,27.3,57.5), + ), + ), + CountryOutline( + "Lebanon", + "LB", + ( + (35.8,33.3,36.4,34.6,35.8,33.3), + ), + ), + CountryOutline( + "Lesotho", + "LS", + ( + (29,-29,28.1,-30.5,27,-29.9,29,-29), + ), + ), + CountryOutline( + "Liberia", + "LR", + ( + (-8.4,7.7,-7.7,4.4,-11.4,6.8,-10.2,8.4,-8.4,7.7), + ), + ), + CountryOutline( + "Libya", + "LY", + ( + (25,22,23.8,19.6,10.3,24.4,10,31.4,11.5,33.1,19.1,30.3,20.9,32.7,24.9,31.9,25,22), + ), + ), + CountryOutline( + "Lithuania", + "LT", + ( + (26.5,55.6,23.5,53.9,21.1,56,26.5,55.6), + ), + ), + CountryOutline( + "Madagascar", + "MG", + ( + (49.5,-12.5,50.4,-15.7,47.1,-24.9,45.4,-25.6,43.3,-22.8,44,-17.4,49.5,-12.5), + ), + ), + CountryOutline( + "Malawi", + "MW", + ( + (32.8,-9.2,35.7,-14.6,35,-16.8,32.7,-13.7,32.8,-9.2), + ), + ), + CountryOutline( + "Malaysia", + "MY", + ( + (100.1,6.5,103,5.5,104.2,1.3,101.4,2.8,100.1,6.5), + (117.9,4.1,115.9,4.3,114.6,1.4,109.8,1.3,115.3,4.3,116.7,6.9,119.2,5.4,117.9,4.1), + ), + ), + CountryOutline( + "Mali", + "ML", + ( + (-11.5,12.4,-11.7,15.4,-5.5,15.5,-6.5,25,4.3,19.2,3.6,15.6,-4,13.5,-5.4,10.4,-11.5,12.4), + ), + ), + CountryOutline( + "Mauritania", + "MR", + ( + (-17.1,21,-12.9,21.3,-12,25.9,-8.7,25.9,-8.7,27.4,-4.9,25,-6.5,25,-5.5,15.5,-12.2,14.6,-14.6,16.6,-16.5,16.1,-17.1,21), + ), + ), + CountryOutline( + "Mexico", + "MX", + ( + (-117.1,32.5,-106.5,31.8,-103.9,29.3,-101.7,29.8,-97.1,25.9,-97.9,22.4,-95.9,18.8,-91.4,18.9,-90.3,21,-87.1,21.5,-87.8,18.3,-91,17.8,-90.5,16.1,-92.2,14.5,-103.5,18.3,-113.1,31.2,-114.9,31.4,-109.9,22.8,-115.1,27.7,-114.2,28.6,-117.1,32.5), + ), + ), + CountryOutline( + "Moldova", + "MD", + ( + (26.6,48.2,30,46.4,28.2,45.5,26.6,48.2), + ), + ), + CountryOutline( + "Mongolia", + "MN", + ( + (87.8,49.3,92.2,50.8,97.3,49.7,98.9,52,108.5,49.3,116.7,49.9,115.7,47.7,119.8,47,105,41.6,96.3,42.7,90.9,45.3,91,46.9,87.8,49.3), + ), + ), + CountryOutline( + "Montenegro", + "ME", + ( + (20.1,42.6,18.5,42.5,20.1,42.6), + ), + ), + CountryOutline( + "Morocco", + "MA", + ( + (-2.2,35.2,-1.3,32.3,-8.7,28.8,-8.8,27.1,-11.4,26.9,-14.8,21.5,-17,21.4,-14.4,26.3,-9.6,29.9,-8.7,33.2,-5.9,35.8,-2.2,35.2), + ), + ), + CountryOutline( + "Mozambique", + "MZ", + ( + (34.6,-11.5,40.3,-10.3,40.8,-14.7,34.8,-19.8,35.5,-24.1,32.1,-26.7,31.2,-22.3,32.8,-16.7,30.2,-14.8,33.2,-14,35,-16.8,34.6,-11.5), + ), + ), + CountryOutline( + "Myanmar", + "MM", + ( + (100.1,20.4,97.4,18.4,99.6,11.9,98.6,9.9,97.2,16.9,94.2,16,92.3,21.5,97.3,28.3,98.7,27.5,97.6,23.9,101.2,21.8,100.1,20.4), + ), + ), + CountryOutline( + "N. Cyprus", + "-99", + ( + (32.7,35.1,34.6,35.7,32.7,35.1), + ), + ), + CountryOutline( + "Namibia", + "NA", + ( + (19.9,-24.8,19.9,-28.5,16.3,-28.6,11.7,-17.3,25.1,-17.6,20.9,-18.3,19.9,-24.8), + ), + ), + CountryOutline( + "Nepal", + "NP", + ( + (88.1,27.9,87.2,26.4,80.1,28.8,81.5,30.4,88.1,27.9), + ), + ), + CountryOutline( + "Netherlands", + "NL", + ( + (6.9,53.5,6.2,50.8,3.3,51.3,6.9,53.5), + ), + ), + CountryOutline( + "New Caledonia", + "NC", + ( + (165.8,-21.1,167.1,-22.2,164,-20.1,165.8,-21.1), + ), + ), + CountryOutline( + "New Zealand", + "NZ", + ( + (176.9,-40.1,174.7,-41.3,174.7,-37.4,172.6,-34.5,176,-37.6,178.5,-37.7,176.9,-40.1), + (169.7,-43.6,172.8,-40.5,174.2,-41.3,170.6,-45.9,166.7,-46.2,169.7,-43.6), + ), + ), + CountryOutline( + "Nicaragua", + "NI", + ( + (-83.7,10.9,-87.7,12.9,-83.1,15,-83.7,10.9), + ), + ), + CountryOutline( + "Niger", + "NE", + ( + (14.9,22.9,15.9,20.4,13.5,14.4,14.2,12.5,5.4,13.9,3.6,11.7,1,12.9,0.4,14.9,3.6,15.6,4.3,19.2,12,23.5,14.9,22.9), + ), + ), + CountryOutline( + "Nigeria", + "NG", + ( + (2.7,6.3,4.4,13.7,13.1,13.6,14.6,12.1,11.7,7,8.5,4.8,5.9,4.3,2.7,6.3), + ), + ), + CountryOutline( + "North Korea", + "KP", + ( + (130.6,42.4,127.5,39.8,128.2,38.4,124.7,38.1,125.1,40.6,130.6,42.4), + ), + ), + CountryOutline( + "North Macedonia", + "MK", + ( + (22.4,42.3,23,41.3,20.6,41.1,22.4,42.3), + ), + ), + CountryOutline( + "Norway", + "NO", + ( + (15.1,79.7,21.5,79,15.9,76.8,10.4,79.7,15.1,79.7), + (31.1,69.6,18,68.6,12.6,64.1,11,58.9,5.7,58.6,5,62,19.2,69.8,28.2,71.2,31.1,69.6), + (27.4,80.1,17.4,80.3,27.4,80.1), + (24.7,77.9,20.7,77.7,24.7,77.9), + ), + ), + CountryOutline( + "Oman", + "OM", + ( + (55.2,22.7,56.4,24.9,59.8,22.3,57.7,18.9,53.1,16.7,52,19,55,20,55.2,22.7), + ), + ), + CountryOutline( + "Pakistan", + "PK", + ( + (77.8,35.5,73.7,34.3,75.3,32.3,69.5,26.9,71,24.4,61.5,25.1,63.3,26.8,60.9,29.8,66.3,29.9,71.8,36.5,75.2,37.1,77.8,35.5), + ), + ), + CountryOutline( + "Panama", + "PA", + ( + (-77.4,8.7,-77.9,7.2,-79.1,9,-80.9,7.2,-82.9,9.5,-77.4,8.7), + ), + ), + CountryOutline( + "Papua New Guinea", + "PG", + ( + (141,-2.6,147.6,-6.1,147.2,-7.4,150.7,-10.6,144.7,-7.6,141,-9.1,141,-2.6), + (152.6,-3.7,152.8,-4.8,150.7,-2.7,152.6,-3.7), + (151.3,-5.8,148.3,-5.7,152.1,-4.1,151.3,-5.8), + (154.8,-5.3,155.9,-6.8,154.8,-5.3), + ), + ), + CountryOutline( + "Paraguay", + "PY", + ( + (-58.2,-20.2,-57.9,-22.1,-54.3,-24,-55.7,-27.4,-58.6,-27.1,-57.8,-25.2,-62.7,-22.2,-61.8,-19.6,-58.2,-20.2), + ), + ), + CountryOutline( + "Peru", + "PE", + ( + (-69.9,-4.3,-72.9,-5.3,-74,-7.5,-68.7,-12.6,-70.4,-18.3,-76,-14.6,-81.4,-4.7,-80.3,-3.4,-78.6,-4.5,-75.1,-0.1,-73.1,-2.3,-70,-2.7,-69.9,-4.3), + ), + ), + CountryOutline( + "Philippines", + "PH", + ( + (122.6,10,124.1,11.2,123,9,122.6,10), + (126.4,8.4,125.4,5.6,123.6,7.8,121.9,7.2,125.4,9.8,126.4,8.4), + (118.5,9.3,117.2,8.4,119.5,11.4,118.5,9.3), + (122.3,18.2,121.7,14.3,124.1,12.5,119.9,15.4,120.7,18.5,122.3,18.2), + (125.5,12.2,124.8,10.1,124.3,12.6,125.5,12.2), + ), + ), + CountryOutline( + "Poland", + "PL", + ( + (23.5,53.9,24,50.7,22.8,49,16.2,50.4,14.1,53,17.6,54.9,23.5,53.9), + ), + ), + CountryOutline( + "Portugal", + "PT", + ( + (-9,41.9,-6.4,41.4,-7.9,36.8,-9.5,38.7,-9,41.9), + ), + ), + CountryOutline( + "Puerto Rico", + "PR", + ( + (-66.3,18.5,-67.2,17.9,-66.3,18.5), + ), + ), + CountryOutline( + "Qatar", + "QA", + ( + (50.8,24.8,51.3,26.1,50.8,24.8), + ), + ), + CountryOutline( + "Romania", + "RO", + ( + (28.2,45.5,29.6,45.3,28.6,43.7,22.9,43.8,20.2,46.1,26.6,48.2,28.2,45.5), + ), + ), + CountryOutline( + "Russia", + "RU", + ( + (49.1,46.4,46.7,44.6,47.8,41.2,36.7,45.2,40.1,49.6,31.8,52.1,32.7,53.4,30.9,55.6,27.3,57.5,29.1,60,28.1,60.5,31.5,62.9,30,63.6,28.6,69.1,32.1,69.9,41.1,67.5,38.4,66,33.2,66.6,37,63.8,37.2,65.1,43.9,66.1,43.5,68.6,46.3,68.3,46.3,66.7,53.7,68.9,59.9,68.3,60.6,69.9,68.5,68.1,66.7,71,69.9,73,72.8,72.2,71.8,71.4,73.7,68.4,71.3,66.3,72.4,66.2,75.1,67.8,73.1,71.4,74.7,72.8,76.4,71.2,81.5,71.8,80.5,73.6,104.4,77.7,114.1,75.8,109.4,74.2,127,73.6,131.3,70.8,139.9,71.5,139.1,72.4,140.5,72.8,159,70.9,160.9,69.4,180,69,180,65,177.4,64.6,179.2,62.3,170.3,59.9,163.5,59.9,162,58.2,163.2,57.6,162.1,54.9,156.8,51,155.9,56.8,164.5,62.6,160.1,60.5,156.7,61.4,154.2,59.8,155,59.1,142.2,59,135.1,54.7,141.3,53.1,140.1,48.4,134.9,43.4,130.8,42.2,131,45,133.1,45.1,135,48.5,131,47.8,123.6,53.5,120.2,52.8,117.9,49.5,108.5,49.3,98.9,52,97.3,49.7,92.2,50.8,87.4,49.2,80,50.9,76.9,54.5,73.4,53.5,69.1,55.4,61.4,54,60,52,61.3,50.8,50.8,51.7,47.5,50.5,46.5,48.4,49.1,46.4), + (93.8,81,100.2,79.8,97.8,78.8,91.2,80.3,93.8,81), + (102.8,79.3,105.4,78.7,99.4,77.9,102.8,79.3), + (138.8,76.1,145.1,75.6,137,75.3,138.8,76.1), + (148.2,75.3,150.7,75.1,146.1,75.2,148.2,75.3), + (139.9,73.4,143.6,73.2,139.9,73.4), + (44.8,80.6,51.5,80.7,44.8,80.6), + (22.7,54.3,19.7,54.4,22.7,54.3), + (53.5,73.7,61.2,76.3,68.9,76.5,58.5,74.3,55.4,72.4,57.5,70.7,51.6,71.5,53.5,73.7), + (142.9,53.7,144.7,49,143.2,49.3,143.5,46.1,142.1,46,141.7,53.3,142.9,53.7), + (-174.9,67.2,-169.9,66,-173,64.3,-178.7,66.1,-180,65,-180,69,-174.9,67.2), + (-178.7,70.9,-180,71.5,-177.6,71.3,-178.7,70.9), + (33.4,46,36.5,45.5,33.9,44.4,32.5,45.3,33.4,46), + ), + ), + CountryOutline( + "Rwanda", + "RW", + ( + (30.4,-1.1,29,-2.8,30.4,-1.1), + ), + ), + CountryOutline( + "S. Sudan", + "SS", + ( + (30.8,3.5,23.9,8.6,25.8,10.4,31.4,9.8,33.2,12.2,33,7.8,35.3,5.5,30.8,3.5), + ), + ), + CountryOutline( + "Saudi Arabia", + "SA", + ( + (35,29.4,39.2,32.2,47.5,29,52,23,55.2,22.7,55,20,47,16.9,43.4,17.6,42.8,16.3,35,29.4), + ), + ), + CountryOutline( + "Senegal", + "SN", + ( + (-16.7,13.6,-17.6,14.7,-14.6,16.6,-11.5,12.4,-16.7,12.4,-13.8,13.5,-16.7,13.6), + ), + ), + CountryOutline( + "Serbia", + "RS", + ( + (18.8,45.9,22.7,44.6,22.5,42.5,19.2,43.5,18.8,45.9), + ), + ), + CountryOutline( + "Sierra Leone", + "SL", + ( + (-13.2,8.9,-11.1,10,-10.2,8.4,-11.4,6.8,-13.2,8.9), + ), + ), + CountryOutline( + "Slovakia", + "SK", + ( + (22.6,49.1,16.9,48.5,22.6,49.1), + ), + ), + CountryOutline( + "Slovenia", + "SI", + ( + (13.8,46.5,16.6,46.5,15.3,45.5,13.8,46.5), + ), + ), + CountryOutline( + "Solomon Is.", + "SB", + ( + (159.6,-8,158.2,-7.4,159.6,-8), + ), + ), + CountryOutline( + "Somalia", + "SO", + ( + (41.6,-1.7,41,2.8,45,5,48.9,9.5,48.9,11.4,51.1,12,48.6,5.3,41.6,-1.7), + ), + ), + CountryOutline( + "Somaliland", + "-99", + ( + (48.9,11.4,47.8,8,42.6,10.6,48.9,11.4), + ), + ), + CountryOutline( + "South Africa", + "ZA", + ( + (16.3,-28.6,19.9,-28.5,19.9,-24.8,21.6,-26.7,25.7,-25.5,29.4,-22.1,31.2,-22.3,31.9,-24.4,30.7,-26.7,32.8,-26.7,28.2,-32.8,20.1,-34.8,18.4,-34.1,16.3,-28.6), + ), + ), + CountryOutline( + "South Korea", + "KR", + ( + (126.2,37.7,128.3,38.6,129.1,35.1,126.5,34.4,126.2,37.7), + ), + ), + CountryOutline( + "Spain", + "ES", + ( + (-7.5,37.1,-6.4,41.4,-9.4,43,3,42.5,-2.1,36.7,-7.5,37.1), + ), + ), + CountryOutline( + "Sri Lanka", + "LK", + ( + (81.8,7.5,80.3,6,80.1,9.8,81.8,7.5), + ), + ), + CountryOutline( + "Sudan", + "SD", + ( + (24.6,8.2,21.9,12.6,25,22,36.9,22,38.4,18,34,8.7,32.7,12.2,31.4,9.8,25.1,10.3,24.6,8.2), + ), + ), + CountryOutline( + "Suriname", + "SR", + ( + (-54.5,2.3,-56.5,1.9,-57.6,3.3,-57.1,6,-54,5.8,-54.5,2.3), + ), + ), + CountryOutline( + "Sweden", + "SE", + ( + (11,58.9,12.6,61.3,11.9,63.1,16.8,68,20.6,69.1,23.5,67.9,23.9,66,17.8,62.7,17.1,61.3,18.8,60.1,15.9,56.1,12.9,55.4,11,58.9), + ), + ), + CountryOutline( + "Switzerland", + "CH", + ( + (9.6,47.5,10.4,46.5,6,46.3,9.6,47.5), + ), + ), + CountryOutline( + "Syria", + "SY", + ( + (35.7,32.7,36.7,36.8,42.3,37.2,41,34.4,35.7,32.7), + ), + ), + CountryOutline( + "Taiwan", + "TW", + ( + (121.8,24.4,120.7,22,120.1,23.6,121.8,24.4), + ), + ), + CountryOutline( + "Tajikistan", + "TJ", + ( + (67.8,37.1,67.7,39.6,70.7,41,69.5,39.5,73.7,39.4,75,37.4,71.8,36.7,70.8,38.5,67.8,37.1), + ), + ), + CountryOutline( + "Tanzania", + "TZ", + ( + (33.9,-0.9,39.2,-4.7,39.5,-10.9,34.6,-11.5,29.6,-6.5,30.4,-1.1,33.9,-0.9), + ), + ), + CountryOutline( + "Thailand", + "TH", + ( + (105.2,14.3,103,14.2,102.6,12.2,100.1,13.4,99.2,9.2,102.1,6.2,101.2,5.7,98.2,8.4,99.6,11.9,97.4,18.4,100.1,20.4,101.1,17.5,104.7,17.4,105.2,14.3), + ), + ), + CountryOutline( + "Timor-Leste", + "TL", + ( + (125,-8.9,127.3,-8.4,125,-8.9), + ), + ), + CountryOutline( + "Togo", + "TG", + ( + (0.9,11,1.1,5.9,0.9,11), + ), + ), + CountryOutline( + "Tunisia", + "TN", + ( + (9.5,30.3,7.5,34.1,9.5,37.3,11,37.1,10.1,34.3,11.5,33.1,9.5,30.3), + ), + ), + CountryOutline( + "Turkey", + "TR", + ( + (44.8,37.2,29.7,36.1,27.6,36.7,26.2,39.5,33.5,42,42.6,41.6,44.8,39.7,44.8,37.2), + (26.1,41.8,29,41.3,26.4,40.2,26.1,41.8), + ), + ), + CountryOutline( + "Turkmenistan", + "TM", + ( + (52.5,41.8,57.1,41.3,58.6,42.8,66.5,37.4,62.2,35.3,57.3,38,53.9,37.2,52.7,40,54.7,41,52.5,41.8), + ), + ), + CountryOutline( + "Uganda", + "UG", + ( + (33.9,-0.9,29.6,-1.3,31.2,3.8,34.5,3.6,33.9,-0.9), + ), + ), + CountryOutline( + "Ukraine", + "UA", + ( + (31.8,52.1,40.1,49.6,39.7,47.9,35,45.7,31.7,46.7,28.7,45.3,30,46.4,28.7,48.1,22.1,48.4,23.5,51.6,31.8,52.1), + ), + ), + CountryOutline( + "United Arab Emirates", + "AE", + ( + (51.6,24.2,56.3,25.7,55,22.5,51.6,24.2), + ), + ), + CountryOutline( + "United Kingdom", + "GB", + ( + (-6.2,53.9,-7.6,55.1,-6.2,53.9), + (-3.1,53.4,-6.1,56.8,-5,58.6,-2,57.7,-3.1,56,1.7,52.7,1.4,51.3,-5.8,50.2,-3.4,51.4,-5.3,52,-4.6,53.5,-3.1,53.4), + ), + ), + CountryOutline( + "United States of America", + "US", + ( + (-122.8,49,-88.4,48.3,-82.6,45.3,-82.7,41.7,-71.5,45,-69.2,47.4,-67,44.8,-70.1,43.7,-70,41.6,-75.5,39.5,-75.9,37.2,-76.3,39.2,-77,38.2,-75.7,35.6,-81.3,31.4,-80.4,25.2,-83.7,29.9,-86.4,30.4,-94.7,29.5,-97.5,25.8,-101,29.4,-103.9,29.3,-106.5,31.8,-117.1,32.5,-120.6,34.6,-124.4,40.3,-124.7,48.2,-122.6,47.1,-122.8,49), + (-166.5,60.4,-165.6,59.9,-167.5,60.2,-166.5,60.4), + (-153.2,58,-152.1,57.6,-154.5,57,-153.2,58), + (-141,69.7,-141,60.3,-137.5,58.9,-135.5,59.8,-130,55.9,-130.5,54.8,-134.1,58.1,-139.9,59.5,-147.1,60.9,-151.7,59.2,-150.6,61.3,-158.4,56,-164.9,54.6,-157,58.9,-162,58.7,-165.3,60.5,-165.7,62.1,-160.8,64.8,-168.1,65.7,-161.7,66.1,-166.2,68.9,-156.6,71.4,-141,69.7), + (-171.7,63.8,-168.7,63.3,-171.7,63.8), + ), + ), + CountryOutline( + "Uruguay", + "UY", + ( + (-57.6,-30.2,-53.8,-32,-53.8,-34.4,-58.4,-33.9,-57.6,-30.2), + ), + ), + CountryOutline( + "Uzbekistan", + "UZ", + ( + (56,41.3,55.9,45,58.5,45.6,62,43.5,64.9,43.7,68.3,40.7,71,42.3,73.1,40.9,67.7,39.6,67.8,37.1,58.6,42.8,56,41.3), + ), + ), + CountryOutline( + "Venezuela", + "VE", + ( + (-60.7,5.2,-64.8,4.1,-63.4,2.2,-66.3,0.7,-67.8,2.8,-67.3,6.1,-72,7,-72.9,10.5,-71.3,11.8,-71.3,9.1,-69.9,12.2,-68.2,10.6,-61.9,10.7,-59.8,8.4,-60.7,5.2), + ), + ), + CountryOutline( + "Vietnam", + "VN", + ( + (104.3,10.5,107.5,12.3,107.6,15.2,102.2,22.5,105.3,23.4,108.1,21.6,105.7,19.1,108.9,15.3,109.2,11.7,105.2,8.6,104.3,10.5), + ), + ), + CountryOutline( + "W. Sahara", + "EH", + ( + (-8.7,27.7,-8.7,25.9,-12,25.9,-12.9,21.3,-17.1,21,-14.8,21.5,-11.4,26.9,-8.7,27.7), + ), + ), + CountryOutline( + "Yemen", + "YE", + ( + (52,19,52.2,15.6,43.5,12.6,43.4,17.6,47,16.9,52,19), + ), + ), + CountryOutline( + "Zambia", + "ZM", + ( + (30.7,-8.3,33.2,-9.7,33.2,-14,27,-17.9,23.2,-17.5,21.9,-12.9,24,-12.9,23.9,-10.9,29.7,-13.3,28.4,-9.2,30.7,-8.3), + ), + ), + CountryOutline( + "Zimbabwe", + "ZW", + ( + (31.2,-22.3,28,-21.5,25.3,-17.7,30.3,-15.5,32.8,-16.7,31.2,-22.3), + ), + ), +) diff --git a/ports/python/hqtui/ui.py b/ports/python/hqtui/ui.py index c4782f1..7bda9a5 100644 --- a/ports/python/hqtui/ui.py +++ b/ports/python/hqtui/ui.py @@ -529,6 +529,19 @@ def shapes(self, options: g.CanvasOptions, layout: Layout | None = None): lambda s: g.draw_canvas(s, options), ) + def world_map(self, options: w.WorldMapOptions, layout: Layout | None = None): + """A world map, and the country under whatever gets clicked. + + The click is answered by turning the cell back into degrees and testing + it against the outlines, so the answer is the country actually under the + cursor. Bounding boxes would be cheaper and wrong: Russia's covers most + of the northern hemisphere and Chile's covers Argentina. + """ + return self._add( + self._constraint(layout or Layout(), "fill"), + lambda s: w.draw_world_map(s, options), + ) + def sparkline(self, options: w.SparklineWidgetOptions, layout: Layout | None = None): return self._add(self._leaf(layout or Layout(), 1), lambda s: w.draw_sparkline(s, options)) diff --git a/ports/python/hqtui/widgets/__init__.py b/ports/python/hqtui/widgets/__init__.py index 1ea8e1f..a82dd1d 100644 --- a/ports/python/hqtui/widgets/__init__.py +++ b/ports/python/hqtui/widgets/__init__.py @@ -77,6 +77,7 @@ ) from .chart import ChartOptions, draw_chart from .shadow import ShadowOptions, dim_rect, draw_shadow +from .world import WorldMapOptions, country_at_cell, draw_world_map from .surface import ClearOptions, FillOptions, draw_clear, draw_fill from .scrollbar import ( ScrollbarOptions, @@ -122,6 +123,7 @@ "draw_calendar", "is_leap_year", "CALENDAR_WIDTH", "ChartOptions", "draw_chart", "ClearOptions", "FillOptions", "draw_clear", "draw_fill", "ShadowOptions", "dim_rect", "draw_shadow", + "WorldMapOptions", "country_at_cell", "draw_world_map", "ScrollbarOptions", "ScrollbarOrientation", "is_vertical", "offset_for_position", "thumb", "draw_scrollbar", "draw_scrollbar_widget", "draw_select", "draw_sparkline", "draw_status_bar", "draw_table", "draw_tabs", "draw_text", "draw_text_input", "draw_tooltip", diff --git a/ports/python/hqtui/widgets/world.py b/ports/python/hqtui/widgets/world.py new file mode 100644 index 0000000..cff21d6 --- /dev/null +++ b/ports/python/hqtui/widgets/world.py @@ -0,0 +1,90 @@ +"""A world map you can click. + +The drawing is the canvas doing what it already does — polylines in the caller's +own coordinates, which for a map are degrees. What this adds is the other +direction: turning a click back into a country. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Sequence + +from ..color import Color +from ..graphics.canvas import Bounds, CanvasOptions, draw_canvas +from ..graphics.world import ( + WORLD_X, + WORLD_Y, + CountryOutline, + WorldShapeOptions, + country_at, + degrees_at, + world_shapes, +) +from ..surface import Surface + +__all__ = ["WorldMapOptions", "country_at_cell", "draw_world_map"] + + +@dataclass(frozen=True, slots=True) +class WorldMapOptions: + #: The window on the globe. None means all of it. + x: Bounds | None = None + y: Bounds | None = None + #: Coastline colour. + color: Color | None = None + #: Countries to pick out, by name or ISO code. + highlight: Sequence[str] = () + highlight_color: Color | None = None + background: Color | None = None + grid: bool = False + + @property + def window(self) -> tuple[Bounds, Bounds]: + return (self.x or WORLD_X, self.y or WORLD_Y) + + +def draw_world_map(surface: Surface, options: WorldMapOptions = WorldMapOptions()) -> None: + if surface.empty: + return + theme = surface.theme + x, y = options.window + draw_canvas( + surface, + CanvasOptions( + shapes=world_shapes( + WorldShapeOptions( + color=options.color if options.color is not None else theme.border, + highlight=options.highlight, + highlight_color=( + options.highlight_color + if options.highlight_color is not None + else theme.accent + ), + ) + ), + x=x, + y=y, + background=options.background, + grid=options.grid, + ), + ) + + +def country_at_cell( + column: int, + row: int, + width: int, + height: int, + options: WorldMapOptions = WorldMapOptions(), +) -> CountryOutline | None: + """The country under a cell of a map drawn with these bounds. + + Exposed so a caller can answer a hover as well as a click, and so the + arithmetic that has to agree with the drawing lives in one place. + """ + x, y = options.window + at = degrees_at(column, row, width, height, x, y) + if at is None: + return None + return country_at(at[0], at[1]) diff --git a/ports/python/tests/test_conformance_widgets.py b/ports/python/tests/test_conformance_widgets.py index acebbe5..69ae2e4 100644 --- a/ports/python/tests/test_conformance_widgets.py +++ b/ports/python/tests/test_conformance_widgets.py @@ -138,6 +138,14 @@ def draw_scene(case, name: str, s: Surface) -> None: elif name == "shadow-solid": w.draw_fill(s, w.FillOptions(symbol="x")) w.draw_shadow(s, Rect(2, 1, 6, 2), w.ShadowOptions(color=rgb(0x10, 0x14, 0x18))) + elif name == "world": + w.draw_world_map(s, w.WorldMapOptions()) + elif name == "world-zoom": + w.draw_world_map(s, w.WorldMapOptions( + x=gc.Bounds(112, 156), y=gc.Bounds(24, 50) + )) + elif name == "world-highlight": + w.draw_world_map(s, w.WorldMapOptions(highlight=("Brazil", "JP"))) elif name == "badge": w.draw_badge(s, w.BadgeOptions(text="LIVE")) elif name == "badge-outline": diff --git a/ports/rust/examples/world-probe.rs b/ports/rust/examples/world-probe.rs new file mode 100644 index 0000000..5befef9 --- /dev/null +++ b/ports/rust/examples/world-probe.rs @@ -0,0 +1,42 @@ +//! Prints what the country lookup answers for a fixed set of points. +//! +//! The same probe exists for every port, so "the ports agree about the world" +//! is a diff rather than a hope. +use hqtui::graphics::{country_at, degrees_at, WORLD_X, WORLD_Y}; +use hqtui::widgets::{country_at_cell, WorldMapOptions}; + +fn main() { + let places: [(&str, f64, f64); 13] = [ + ("Paris", 2.35, 48.86), + ("Tokyo", 139.7, 35.7), + ("Cairo", 31.2, 30.0), + ("Brasilia", -47.9, -15.8), + ("Canberra", 149.1, -35.3), + ("Denver", -105.0, 39.7), + ("Moscow", 37.6, 55.75), + ("Delhi", 77.2, 28.6), + ("Nairobi", 36.8, -1.3), + ("Pacific", -140.0, 0.0), + ("Atlantic", -30.0, 0.0), + ("SouthernOcean", 80.0, -40.0), + ("NorthPacific", -150.0, 40.0), + ]; + for (name, lon, lat) in places { + let found = country_at(lon, lat).map(|c| c.name).unwrap_or("-"); + println!("{name} {found}"); + } + + // The cell path, which has to agree with what the canvas drew. + let options = WorldMapOptions::default(); + for (column, row) in [(173usize, 28usize), (74, 2), (20, 25), (88, 7)] { + let found = country_at_cell(column, row, 200, 50, &options).map(|c| c.name).unwrap_or("-"); + println!("cell:{column},{row} {found}"); + } + + // And the projection itself, so a drift shows up as a number rather than as + // a country that happens to still be right. + for (column, row) in [(0usize, 0usize), (99, 25), (50, 13)] { + let (lon, lat) = degrees_at(column, row, 100, 26, WORLD_X, WORLD_Y).unwrap(); + println!("degrees:{column},{row} {lon:.4} {lat:.4}"); + } +} diff --git a/ports/rust/src/graphics/mod.rs b/ports/rust/src/graphics/mod.rs index cce0075..26ef2f7 100644 --- a/ports/rust/src/graphics/mod.rs +++ b/ports/rust/src/graphics/mod.rs @@ -3,6 +3,8 @@ pub mod blocks; pub mod canvas; +pub mod world; +pub mod world_data; pub mod chart; pub mod braille; pub mod plot; @@ -12,6 +14,11 @@ pub use blocks::{ HORIZONTAL_EIGHTHS, QUADRANTS, SHADES, VERTICAL_EIGHTHS, }; pub use braille::BrailleCanvas; +pub use world::{ + country_at, country_bounds, degrees_at, find_country, world_shapes, WorldShapeOptions, + WORLD_X, WORLD_Y, +}; +pub use world_data::{CountryOutline, WORLD_COUNTRIES}; pub use canvas::{ draw_canvas, projection, Bounds, CanvasOptions, Projection, Shape, }; diff --git a/ports/rust/src/graphics/world.rs b/ports/rust/src/graphics/world.rs new file mode 100644 index 0000000..7ed1897 --- /dev/null +++ b/ports/rust/src/graphics/world.rs @@ -0,0 +1,169 @@ +//! The world, as shapes for the canvas, and the lookup that makes it clickable. +//! +//! The canvas already draws in the caller's own coordinates, and longitude and +//! latitude are just another pair of axes -- so a map is a list of polylines in +//! degrees, and nothing here needs a projection of its own beyond deciding +//! which window on the globe to show. +//! +//! The interesting half is the other direction. A click arrives as a terminal +//! cell, and a country is a polygon, so answering "what did they click" means +//! turning the cell back into degrees and testing it against the outlines. +//! Doing it that way rather than with bounding boxes is what makes the answer +//! right: Russia's bounding box covers most of the northern hemisphere, and +//! Chile's covers Argentina. + +use crate::color::Color; +use crate::graphics::canvas::{Bounds, Shape}; +use crate::graphics::world_data::{CountryOutline, WORLD_COUNTRIES}; + +/// The whole globe, which is what a map shows unless told otherwise. +pub const WORLD_X: Bounds = Bounds { min: -180.0, max: 180.0 }; +pub const WORLD_Y: Bounds = Bounds { min: -90.0, max: 90.0 }; + +#[derive(Clone, Debug, Default)] +pub struct WorldShapeOptions { + /// Colour for countries with nothing special about them. + pub color: Option, + /// Countries to pick out, by name or ISO code. + pub highlight: Vec, + pub highlight_color: Option, +} + +/// Match on either the name or the ISO code, case-insensitively. +fn matches(country: &CountryOutline, keys: &[String]) -> bool { + keys.iter().any(|key| { + !key.is_empty() + && (country.name.eq_ignore_ascii_case(key) + || (!country.iso.is_empty() && country.iso.eq_ignore_ascii_case(key))) + }) +} + +/// The world as canvas shapes, one polyline per landmass. +/// +/// Polylines rather than scattered points: the outlines are closed rings, so +/// joining them draws a coastline instead of a dotted suggestion of one, and it +/// reads at a fraction of the resolution dots would need. +pub fn world_shapes(options: &WorldShapeOptions) -> Vec { + let mut shapes = Vec::new(); + for country in WORLD_COUNTRIES { + let picked = !options.highlight.is_empty() && matches(country, &options.highlight); + let color = if picked { options.highlight_color.or(options.color) } else { options.color }; + for ring in country.rings { + let mut points: Vec<(f64, f64)> = Vec::with_capacity(ring.len() / 2 + 1); + let mut i = 0; + while i + 1 < ring.len() { + points.push((ring[i], ring[i + 1])); + i += 2; + } + // Closed: the last point joins the first, or every country has a + // gap in its coastline where the ring started. + if let Some(first) = points.first().copied() { + points.push(first); + } + shapes.push(Shape::Polyline { points, color }); + } + } + shapes +} + +/// Whether a point is inside a ring, by ray casting. +/// +/// The ring is a flat list of interleaved coordinates, so this walks it two at +/// a time rather than allocating a pair per vertex -- it runs once per country +/// per click, and there are a couple of thousand vertices. +fn inside_ring(ring: &[f64], lon: f64, lat: f64) -> bool { + let mut inside = false; + let n = ring.len() / 2; + if n == 0 { + return false; + } + let mut j = n - 1; + for i in 0..n { + let (xi, yi) = (ring[i * 2], ring[i * 2 + 1]); + let (xj, yj) = (ring[j * 2], ring[j * 2 + 1]); + if (yi > lat) != (yj > lat) && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi { + inside = !inside; + } + j = i; + } + inside +} + +/// The country containing a point, or `None` for open water. +/// +/// Where outlines overlap -- and at this resolution simplified borders do +/// overlap -- the first match wins, which is stable because the data is sorted +/// by name. +pub fn country_at(lon: f64, lat: f64) -> Option<&'static CountryOutline> { + if !lon.is_finite() || !lat.is_finite() { + return None; + } + WORLD_COUNTRIES + .iter() + .find(|country| country.rings.iter().any(|ring| inside_ring(ring, lon, lat))) +} + +/// Look a country up by name or ISO code. +pub fn find_country(key: &str) -> Option<&'static CountryOutline> { + let keys = [key.to_string()]; + WORLD_COUNTRIES.iter().find(|country| matches(country, &keys)) +} + +/// The window a country fills, with a little room around it. +/// +/// For zooming a map to a country: the bounding box alone puts the coastline +/// flat against the edge of the panel, which reads as though the country has +/// been cut off rather than framed. +pub fn country_bounds(country: &CountryOutline, margin: f64) -> (Bounds, Bounds) { + let mut min_lon = f64::INFINITY; + let mut max_lon = f64::NEG_INFINITY; + let mut min_lat = f64::INFINITY; + let mut max_lat = f64::NEG_INFINITY; + for ring in country.rings { + let mut i = 0; + while i + 1 < ring.len() { + min_lon = min_lon.min(ring[i]); + max_lon = max_lon.max(ring[i]); + min_lat = min_lat.min(ring[i + 1]); + max_lat = max_lat.max(ring[i + 1]); + i += 2; + } + } + if !min_lon.is_finite() { + return (WORLD_X, WORLD_Y); + } + // A single-point country would give a zero-width window, which cannot be + // mapped onto anything. + let pad_x = ((max_lon - min_lon) * margin).max(1.0); + let pad_y = ((max_lat - min_lat) * margin).max(1.0); + ( + Bounds { min: min_lon - pad_x, max: max_lon + pad_x }, + Bounds { min: min_lat - pad_y, max: max_lat + pad_y }, + ) +} + +/// The degrees under a terminal cell, given the window the map was drawn with. +/// +/// The inverse of what the canvas does on the way in, taken at the centre of +/// the cell: a click lands on a whole cell, and the centre is the only point in +/// it that is not arbitrarily nearer one neighbour than the other. +pub fn degrees_at( + column: usize, + row: usize, + width: usize, + height: usize, + x: Bounds, + y: Bounds, +) -> Option<(f64, f64)> { + if width == 0 || height == 0 { + return None; + } + // The canvas is 2x4 Braille pixels per cell, and it spans its bounds across + // `pixels - 1`, so the inverse has to use the same denominators or a click + // drifts from what was drawn. + let px = (width * 2).saturating_sub(1).max(1) as f64; + let py = (height * 4).saturating_sub(1).max(1) as f64; + let lon = x.min + ((column * 2 + 1) as f64 / px) * (x.max - x.min); + let lat = y.min + (1.0 - (row * 4 + 2) as f64 / py) * (y.max - y.min); + Some((lon, lat)) +} diff --git a/ports/rust/src/graphics/world_data.rs b/ports/rust/src/graphics/world_data.rs new file mode 100644 index 0000000..7d25d36 --- /dev/null +++ b/ports/rust/src/graphics/world_data.rs @@ -0,0 +1,1306 @@ +//! Country outlines, flattened for a terminal. +//! +//! Generated by packages/hqtui/scripts/generate-world.ts from Natural Earth's +//! 1:110m Admin 0 countries, which is public domain. Do not edit by hand. +//! +//! Each ring is longitude and latitude interleaved -- lon, lat, lon, lat -- +//! rather than a list of pairs, because at 1982 points the nested form +//! costs a container per coordinate for no gain. A country has more than one +//! ring when it is more than one landmass. +//! +//! 171 countries, 1982 points, simplified at 1 degrees. + +pub struct CountryOutline { + pub name: &'static str, + /// ISO 3166-1 alpha-2, where Natural Earth has one. + pub iso: &'static str, + /// Longitude and latitude, interleaved. + pub rings: &'static [&'static [f64]], +} + +pub static WORLD_COUNTRIES: &[CountryOutline] = &[ + CountryOutline { + name: "Afghanistan", + iso: "AF", + rings: &[ + &[66.5,37.4,70.8,38.5,71.8,36.7,75.2,37.1,71.3,36.1,69.3,31.9,66.3,29.9,60.9,29.8,61.2,35.7,66.5,37.4], + ], + }, + CountryOutline { + name: "Albania", + iso: "AL", + rings: &[ + &[21.0,40.8,19.4,40.3,19.7,42.7,21.0,40.8], + ], + }, + CountryOutline { + name: "Algeria", + iso: "DZ", + rings: &[ + &[-8.7,27.4,-8.7,28.8,-1.3,32.3,-1.2,35.7,8.4,36.9,7.5,34.1,9.8,29.4,9.3,26.1,12.0,23.5,3.2,19.1,-8.7,27.4], + ], + }, + CountryOutline { + name: "Angola", + iso: "AO", + rings: &[ + &[12.3,-6.1,16.3,-5.9,17.5,-8.1,21.7,-7.3,22.2,-11.1,24.0,-11.2,24.0,-12.9,21.9,-12.9,23.2,-17.5,11.7,-17.3,13.7,-11.3,12.3,-6.1], + ], + }, + CountryOutline { + name: "Antarctica", + iso: "AQ", + rings: &[ + &[-48.7,-78.0,-43.9,-78.5,-43.3,-80.0,-54.2,-80.6,-48.7,-78.0], + &[-66.3,-80.3,-59.6,-80.0,-66.3,-80.3], + &[-73.9,-71.3,-70.3,-68.9,-68.3,-71.4,-75.0,-72.1,-73.9,-71.3], + &[-102.3,-71.9,-96.2,-72.5,-102.3,-71.9], + &[-122.6,-73.7,-118.7,-73.5,-122.6,-73.7], + &[-127.3,-73.5,-124.0,-73.9,-127.3,-73.5], + &[-163.7,-78.6,-159.2,-79.5,-163.7,-78.6], + &[180.0,-84.7,180.0,-90.0,-180.0,-90.0,-179.1,-84.1,-143.1,-85.0,-153.6,-83.7,-152.9,-82.0,-156.8,-81.1,-146.4,-80.3,-155.3,-79.1,-158.4,-76.9,-151.3,-77.4,-146.1,-76.5,-146.2,-75.4,-135.2,-74.3,-100.1,-74.9,-103.7,-72.6,-74.9,-73.9,-67.4,-72.5,-67.7,-67.3,-57.8,-63.3,-65.7,-68.0,-61.8,-70.7,-60.8,-73.7,-70.6,-76.6,-77.2,-76.7,-73.7,-77.9,-78.0,-79.2,-58.2,-83.2,-28.5,-80.3,-35.6,-79.5,-35.8,-78.3,-17.5,-75.1,-15.4,-73.1,-6.9,-70.9,27.1,-70.5,33.9,-68.5,38.6,-69.8,54.5,-65.8,61.4,-68.0,68.9,-67.9,69.7,-69.2,67.9,-71.9,69.9,-72.3,73.9,-69.9,88.0,-66.2,95.8,-67.4,102.8,-65.6,106.2,-66.9,113.6,-65.9,119.8,-67.3,135.1,-65.3,137.5,-67.0,145.5,-66.9,171.2,-71.7,163.6,-76.2,167.0,-78.8,161.8,-79.2,159.8,-80.9,169.4,-83.8,180.0,-84.7], + ], + }, + CountryOutline { + name: "Argentina", + iso: "AR", + rings: &[ + &[-68.6,-52.6,-65.0,-54.7,-68.6,-54.9,-68.6,-52.6], + &[-57.6,-30.2,-58.5,-34.4,-56.8,-36.9,-62.3,-38.8,-62.7,-41.0,-65.1,-41.1,-63.5,-42.6,-67.3,-45.6,-65.6,-47.2,-69.1,-50.7,-68.1,-52.3,-71.9,-52.0,-73.4,-49.3,-71.2,-44.8,-72.1,-42.3,-68.4,-24.5,-66.3,-21.8,-62.8,-22.0,-57.8,-25.2,-58.6,-27.1,-55.7,-27.4,-54.1,-25.5,-53.6,-26.9,-57.6,-30.2], + ], + }, + CountryOutline { + name: "Armenia", + iso: "AM", + rings: &[ + &[46.5,38.8,43.6,41.1,45.6,40.8,46.5,38.8], + ], + }, + CountryOutline { + name: "Australia", + iso: "AU", + rings: &[ + &[147.7,-40.8,147.9,-43.2,146.0,-43.5,144.7,-40.7,147.7,-40.8], + &[126.1,-32.2,118.0,-35.1,115.0,-34.2,113.7,-22.5,120.9,-19.7,125.7,-14.2,129.6,-15.0,132.4,-11.1,136.5,-11.9,135.5,-15.0,140.2,-17.7,142.5,-10.7,146.4,-19.0,150.7,-22.4,153.6,-28.1,150.0,-37.4,146.3,-39.0,140.6,-38.0,138.2,-34.4,136.8,-35.3,137.8,-32.9,136.0,-34.9,131.3,-31.5,126.1,-32.2], + ], + }, + CountryOutline { + name: "Austria", + iso: "AT", + rings: &[ + &[17.0,48.1,14.6,46.4,9.5,47.1,12.9,47.5,13.6,48.9,17.0,48.1], + ], + }, + CountryOutline { + name: "Azerbaijan", + iso: "AZ", + rings: &[ + &[46.4,41.9,50.4,40.3,48.9,38.3,45.6,39.9,45.0,41.2,46.4,41.9], + ], + }, + CountryOutline { + name: "Bahamas", + iso: "BS", + rings: &[ + &[-78.2,25.2,-77.5,23.8,-78.2,25.2], + ], + }, + CountryOutline { + name: "Bangladesh", + iso: "BD", + rings: &[ + &[92.7,22.0,92.4,20.7,91.4,22.8,89.0,22.1,88.6,26.4,92.4,25.0,91.2,23.5,92.7,22.0], + ], + }, + CountryOutline { + name: "Belarus", + iso: "BY", + rings: &[ + &[28.2,56.2,30.9,55.6,32.7,53.4,31.8,52.1,23.5,51.6,23.5,53.9,28.2,56.2], + ], + }, + CountryOutline { + name: "Belgium", + iso: "BE", + rings: &[ + &[6.2,50.8,5.7,49.5,2.5,51.1,6.2,50.8], + ], + }, + CountryOutline { + name: "Belize", + iso: "BZ", + rings: &[ + &[-89.1,17.8,-88.1,18.3,-88.9,15.9,-89.1,17.8], + ], + }, + CountryOutline { + name: "Benin", + iso: "BJ", + rings: &[ + &[2.7,6.3,0.8,10.5,2.8,12.2,2.7,6.3], + ], + }, + CountryOutline { + name: "Bhutan", + iso: "BT", + rings: &[ + &[91.7,27.8,88.8,27.1,91.7,27.8], + ], + }, + CountryOutline { + name: "Bolivia", + iso: "BO", + rings: &[ + &[-69.5,-11.0,-65.3,-9.8,-65.4,-11.6,-60.5,-13.8,-60.2,-16.3,-58.2,-16.3,-57.9,-20.0,-61.8,-19.6,-62.7,-22.2,-67.8,-22.9,-69.5,-11.0], + ], + }, + CountryOutline { + name: "Bosnia and Herz.", + iso: "BA", + rings: &[ + &[18.6,42.7,16.0,45.2,19.4,44.9,18.6,42.7], + ], + }, + CountryOutline { + name: "Botswana", + iso: "BW", + rings: &[ + &[29.4,-22.1,25.7,-25.5,21.6,-26.7,19.9,-24.8,20.9,-18.3,25.3,-17.7,29.4,-22.1], + ], + }, + CountryOutline { + name: "Brazil", + iso: "BR", + rings: &[ + &[-53.4,-33.8,-53.8,-32.0,-57.6,-30.2,-53.6,-26.1,-55.8,-22.4,-57.9,-22.1,-58.2,-16.3,-60.2,-16.3,-60.5,-13.8,-65.4,-11.6,-65.3,-9.8,-70.5,-11.0,-70.5,-9.5,-72.2,-10.1,-74.0,-7.5,-72.9,-5.3,-69.9,-4.3,-69.8,1.7,-65.5,0.8,-63.4,2.2,-64.8,4.1,-60.7,5.2,-59.0,1.3,-52.9,2.1,-51.3,4.2,-50.4,-0.1,-44.6,-2.7,-40.0,-2.9,-35.6,-5.1,-34.7,-7.3,-38.7,-13.1,-40.9,-21.9,-47.6,-24.9,-48.9,-28.7,-53.4,-33.8], + ], + }, + CountryOutline { + name: "Bulgaria", + iso: "BG", + rings: &[ + &[22.7,44.2,28.6,43.7,28.0,42.0,23.0,41.3,22.7,44.2], + ], + }, + CountryOutline { + name: "Burkina Faso", + iso: "BF", + rings: &[ + &[-5.4,10.4,-4.3,13.2,-1.1,15.0,2.2,12.6,0.9,11.0,-2.9,11.0,-2.8,9.6,-5.4,10.4], + ], + }, + CountryOutline { + name: "Burundi", + iso: "BI", + rings: &[ + &[30.5,-2.4,29.3,-4.5,29.0,-2.8,30.5,-2.4], + ], + }, + CountryOutline { + name: "Cambodia", + iso: "KH", + rings: &[ + &[102.6,12.2,103.0,14.2,107.6,13.5,106.2,11.0,103.5,10.6,102.6,12.2], + ], + }, + CountryOutline { + name: "Cameroon", + iso: "CM", + rings: &[ + &[14.5,12.9,14.5,4.7,15.9,1.7,9.6,2.3,8.8,5.5,11.7,7.0,14.5,12.9], + ], + }, + CountryOutline { + name: "Canada", + iso: "CA", + rings: &[ + &[-122.8,49.0,-127.4,50.8,-130.5,54.3,-130.0,55.9,-135.5,59.8,-137.5,58.9,-141.0,60.3,-141.0,69.7,-136.5,68.9,-128.1,70.5,-113.5,67.7,-106.1,68.8,-101.5,67.6,-97.7,68.6,-96.1,67.3,-94.2,69.1,-96.5,70.1,-95.2,71.9,-87.4,67.2,-85.5,69.9,-82.6,69.7,-81.4,67.1,-85.8,66.6,-90.7,63.6,-94.7,58.9,-92.3,57.1,-82.3,55.1,-79.9,51.2,-78.6,52.6,-79.8,54.7,-76.5,56.5,-78.5,58.8,-77.3,59.9,-78.1,62.3,-73.8,62.4,-69.6,61.1,-67.6,58.2,-64.6,60.3,-61.8,56.3,-57.3,54.6,-55.7,52.1,-60.0,50.2,-66.4,50.2,-71.1,46.8,-65.1,49.2,-64.5,46.2,-60.5,47.0,-59.8,45.9,-65.4,43.5,-66.2,44.5,-64.4,45.3,-67.1,45.1,-69.2,47.4,-71.5,45.0,-82.4,41.7,-82.6,45.3,-88.4,48.3,-122.8,49.0], + &[-84.0,62.5,-81.9,62.9,-84.0,62.5], + &[-79.8,72.8,-80.8,73.7,-76.3,72.8,-79.8,72.8], + &[-93.6,75.0,-96.8,74.9,-93.6,75.0], + &[-93.8,77.5,-96.4,77.8,-93.8,77.5], + &[-96.8,78.8,-95.6,78.4,-98.6,78.9,-96.8,78.8], + &[-88.2,74.4,-97.1,76.8,-79.8,74.9,-88.2,74.4], + &[-111.3,78.2,-109.9,78.0,-113.5,77.7,-111.3,78.2], + &[-111.0,78.8,-109.7,78.6,-112.5,78.4,-111.0,78.8], + &[-55.6,51.3,-56.8,49.8,-53.5,49.2,-53.1,46.7,-59.3,47.6,-55.6,51.3], + &[-83.9,65.1,-80.1,63.7,-87.2,63.5,-85.9,65.7,-83.9,65.1], + &[-78.8,72.4,-68.8,70.5,-67.0,69.2,-68.8,68.7,-61.9,66.9,-63.9,65.0,-68.0,66.3,-64.7,63.4,-68.8,63.7,-66.2,61.9,-68.9,62.3,-78.6,64.6,-74.0,65.5,-73.3,68.1,-79.0,70.2,-88.7,70.4,-90.2,72.2,-85.8,73.8,-85.8,72.5,-82.3,73.8,-78.8,72.4], + &[-94.5,74.1,-90.5,73.9,-95.4,72.1,-96.0,73.4,-94.5,74.1], + &[-122.9,76.1,-116.2,77.6,-122.9,76.1], + &[-132.7,54.0,-131.2,52.2,-132.7,54.0], + &[-105.5,79.3,-99.7,77.9,-105.5,79.3], + &[-123.5,48.5,-128.4,50.8,-123.5,48.5], + &[-121.5,74.4,-115.5,73.5,-123.1,70.9,-125.9,71.9,-123.9,73.7,-124.9,74.3,-121.5,74.4], + &[-107.8,75.8,-105.7,75.5,-117.7,75.2,-115.4,76.5,-107.8,75.8], + &[-106.5,73.1,-101.1,69.6,-113.3,68.5,-117.3,70.0,-112.4,70.4,-119.4,71.6,-115.2,73.3,-108.2,71.7,-108.4,73.1,-106.5,73.1], + &[-100.4,72.7,-101.5,73.4,-97.4,73.8,-96.5,72.6,-98.4,71.3,-102.5,72.5,-100.4,72.7], + &[-106.6,73.6,-104.5,73.4,-106.6,73.6], + &[-98.5,76.7,-98.2,75.0,-102.5,75.6,-98.5,76.7], + &[-96.0,80.6,-92.4,81.3,-85.8,79.3,-92.9,78.3,-96.0,80.6], + &[-91.6,81.9,-61.8,82.6,-76.9,79.3,-75.4,78.5,-80.6,76.2,-89.5,76.5,-88.3,77.9,-85.0,77.5,-88.0,78.4,-85.1,79.3,-86.9,80.3,-81.8,80.5,-91.6,81.9], + &[-75.2,67.4,-77.2,67.6,-75.2,67.4], + &[-96.3,69.5,-99.8,69.4,-96.3,69.5], + &[-64.5,49.9,-61.8,49.1,-64.5,49.9], + &[-64.0,47.0,-62.0,46.4,-64.0,47.0], + ], + }, + CountryOutline { + name: "Central African Rep.", + iso: "CF", + rings: &[ + &[27.4,5.2,22.4,4.0,19.5,5.0,16.0,2.3,14.5,5.5,15.3,7.4,22.9,11.1,27.4,5.2], + ], + }, + CountryOutline { + name: "Chad", + iso: "TD", + rings: &[ + &[23.8,19.6,23.9,15.6,21.9,12.6,22.9,11.1,15.3,7.4,13.5,14.4,15.9,20.4,14.9,22.9,23.8,19.6], + ], + }, + CountryOutline { + name: "Chile", + iso: "CL", + rings: &[ + &[-68.6,-52.6,-68.6,-54.9,-67.0,-54.9,-68.1,-55.6,-74.7,-52.8,-71.1,-54.1,-68.6,-52.6], + &[-69.6,-17.6,-67.0,-23.0,-70.5,-31.4,-69.8,-34.2,-72.1,-42.3,-71.2,-44.8,-73.4,-49.3,-71.9,-52.0,-68.6,-52.3,-71.4,-53.9,-74.9,-52.3,-75.6,-48.7,-74.1,-46.9,-75.6,-46.6,-72.7,-42.4,-74.3,-43.2,-69.6,-17.6], + ], + }, + CountryOutline { + name: "China", + iso: "CN", + rings: &[ + &[109.5,18.2,108.6,19.4,110.8,20.1,109.5,18.2], + &[80.3,42.3,80.0,44.9,87.8,49.3,91.0,46.9,90.9,45.3,96.3,42.7,109.2,42.5,111.9,45.1,119.7,46.7,115.5,48.1,122.2,53.4,125.9,52.8,131.0,47.8,135.0,48.5,133.1,45.1,131.0,45.0,130.6,42.4,121.1,38.9,121.6,40.9,117.5,38.7,122.4,37.5,119.2,34.9,121.9,31.7,121.7,28.2,118.7,24.5,110.4,20.3,105.3,23.4,101.7,22.3,101.8,21.2,99.2,22.1,97.6,23.9,98.7,27.5,96.1,29.5,88.8,27.3,78.7,31.5,78.9,34.3,73.7,39.4,80.3,42.3], + ], + }, + CountryOutline { + name: "Colombia", + iso: "CO", + rings: &[ + &[-66.9,1.3,-69.8,1.7,-69.9,-4.3,-70.0,-2.7,-77.4,0.4,-79.0,1.7,-77.1,3.8,-77.5,8.5,-71.4,12.4,-73.3,9.2,-72.0,7.0,-67.3,6.1,-66.9,1.3], + ], + }, + CountryOutline { + name: "Congo", + iso: "CG", + rings: &[ + &[18.5,3.5,16.0,-3.5,11.9,-5.0,11.5,-2.8,14.4,-1.3,13.1,2.3,15.9,1.7,18.5,3.5], + ], + }, + CountryOutline { + name: "Costa Rica", + iso: "CR", + rings: &[ + &[-82.5,9.6,-83.0,8.2,-85.9,10.9,-82.5,9.6], + ], + }, + CountryOutline { + name: "Côte d'Ivoire", + iso: "CI", + rings: &[ + &[-8.0,10.2,-2.8,9.6,-2.9,5.0,-7.7,4.4,-8.0,10.2], + ], + }, + CountryOutline { + name: "Croatia", + iso: "HR", + rings: &[ + &[16.6,46.5,19.4,45.2,15.8,44.8,18.5,42.5,13.7,45.1,16.6,46.5], + ], + }, + CountryOutline { + name: "Cuba", + iso: "CU", + rings: &[ + &[-82.3,23.2,-74.2,20.3,-77.8,19.9,-81.8,22.6,-85.0,21.9,-82.3,23.2], + ], + }, + CountryOutline { + name: "Cyprus", + iso: "CY", + rings: &[ + &[32.7,35.1,34.0,35.0,32.7,35.1], + ], + }, + CountryOutline { + name: "Czechia", + iso: "CZ", + rings: &[ + &[15.0,51.1,18.9,49.5,12.5,49.5,15.0,51.1], + ], + }, + CountryOutline { + name: "Dem. Rep. Congo", + iso: "CD", + rings: &[ + &[29.3,-4.5,30.7,-8.3,28.7,-8.5,28.4,-11.8,29.7,-13.3,22.2,-11.1,21.7,-7.3,17.5,-8.1,16.3,-5.9,12.2,-5.8,16.0,-3.5,19.5,5.0,29.7,4.6,31.2,2.2,29.3,-4.5], + ], + }, + CountryOutline { + name: "Denmark", + iso: "DK", + rings: &[ + &[9.9,55.0,8.1,56.5,10.6,57.7,9.9,55.0], + &[12.4,56.1,12.1,54.8,11.0,55.4,12.4,56.1], + ], + }, + CountryOutline { + name: "Djibouti", + iso: "DJ", + rings: &[ + &[42.4,12.5,42.8,10.9,42.4,12.5], + ], + }, + CountryOutline { + name: "Dominican Rep.", + iso: "DO", + rings: &[ + &[-71.7,18.0,-71.6,19.9,-68.3,18.6,-71.7,18.0], + ], + }, + CountryOutline { + name: "Ecuador", + iso: "EC", + rings: &[ + &[-75.4,-0.2,-78.6,-4.5,-80.4,-4.4,-80.1,0.8,-75.4,-0.2], + ], + }, + CountryOutline { + name: "Egypt", + iso: "EG", + rings: &[ + &[36.9,22.0,25.0,22.0,25.2,31.6,34.3,31.2,34.2,27.8,32.3,29.8,36.9,22.0], + ], + }, + CountryOutline { + name: "El Salvador", + iso: "SV", + rings: &[ + &[-89.4,14.4,-87.9,13.1,-90.1,13.7,-89.4,14.4], + ], + }, + CountryOutline { + name: "Eq. Guinea", + iso: "GQ", + rings: &[ + &[9.6,2.3,11.3,1.1,9.5,1.0,9.6,2.3], + ], + }, + CountryOutline { + name: "Eritrea", + iso: "ER", + rings: &[ + &[36.4,14.4,38.4,18.0,43.1,12.7,36.4,14.4], + ], + }, + CountryOutline { + name: "Estonia", + iso: "EE", + rings: &[ + &[28.0,59.5,27.3,57.5,23.3,59.2,28.0,59.5], + ], + }, + CountryOutline { + name: "eSwatini", + iso: "SZ", + rings: &[ + &[32.1,-26.7,31.0,-25.7,32.1,-26.7], + ], + }, + CountryOutline { + name: "Ethiopia", + iso: "ET", + rings: &[ + &[47.8,8.0,45.0,5.0,39.6,3.4,36.2,4.4,33.0,7.8,37.9,15.0,41.6,13.5,43.7,9.2,47.8,8.0], + ], + }, + CountryOutline { + name: "Falkland Is.", + iso: "FK", + rings: &[ + &[-61.2,-51.8,-57.7,-51.5,-61.2,-51.8], + ], + }, + CountryOutline { + name: "Finland", + iso: "FI", + rings: &[ + &[28.6,69.1,31.1,62.4,28.1,60.5,21.3,60.7,21.5,63.2,25.4,65.1,20.6,69.1,24.7,68.6,27.7,70.2,28.6,69.1], + ], + }, + CountryOutline { + name: "Fr. S. Antarctic Lands", + iso: "TF", + rings: &[ + &[68.9,-48.6,70.6,-49.3,68.7,-49.8,68.9,-48.6], + ], + }, + CountryOutline { + name: "France", + iso: "FR", + rings: &[ + &[-51.7,4.2,-52.9,2.1,-54.5,2.3,-54.0,5.8,-51.7,4.2], + &[6.2,49.5,8.1,49.0,6.0,46.7,7.4,43.7,1.8,42.3,-1.9,43.4,-1.2,46.0,-4.6,48.7,-1.6,48.6,-1.9,49.8,2.5,51.1,6.2,49.5], + &[8.7,42.6,9.2,41.4,8.7,42.6], + ], + }, + CountryOutline { + name: "Gabon", + iso: "GA", + rings: &[ + &[11.3,2.3,14.3,1.2,14.4,-1.3,11.1,-4.0,8.8,-1.1,11.3,2.3], + ], + }, + CountryOutline { + name: "Gambia", + iso: "GM", + rings: &[ + &[-16.7,13.6,-13.8,13.5,-16.7,13.6], + ], + }, + CountryOutline { + name: "Georgia", + iso: "GE", + rings: &[ + &[40.0,43.4,46.6,41.2,41.6,41.5,40.0,43.4], + ], + }, + CountryOutline { + name: "Germany", + iso: "DE", + rings: &[ + &[14.1,53.8,15.0,51.1,12.2,50.3,12.9,47.5,7.5,47.6,8.1,49.0,6.0,50.1,7.1,53.7,9.9,55.0,14.1,53.8], + ], + }, + CountryOutline { + name: "Ghana", + iso: "GH", + rings: &[ + &[0.0,11.0,1.1,5.9,-2.9,5.0,-2.9,11.0,0.0,11.0], + ], + }, + CountryOutline { + name: "Greece", + iso: "GR", + rings: &[ + &[26.3,35.3,23.5,35.3,26.3,35.3], + &[23.0,41.3,26.6,41.6,22.6,40.3,24.0,37.7,22.5,36.4,20.2,39.6,23.0,41.3], + ], + }, + CountryOutline { + name: "Greenland", + iso: "GL", + rings: &[ + &[-46.8,82.6,-27.1,83.5,-20.8,82.7,-31.9,82.2,-12.2,81.3,-20.0,80.2,-17.7,80.1,-19.7,78.8,-18.5,77.0,-21.7,76.6,-19.4,74.3,-24.8,72.3,-21.8,70.7,-25.5,71.4,-26.4,70.2,-22.3,70.1,-39.8,65.5,-43.4,60.1,-48.3,60.9,-51.6,63.6,-54.0,67.2,-50.9,69.9,-54.7,69.6,-54.4,70.8,-51.4,70.6,-55.8,71.7,-54.7,72.6,-58.6,75.5,-68.5,76.1,-71.4,77.0,-66.8,77.4,-73.3,78.0,-65.7,79.4,-68.0,80.1,-62.7,81.8,-44.5,81.7,-46.8,82.6], + ], + }, + CountryOutline { + name: "Guatemala", + iso: "GT", + rings: &[ + &[-92.2,14.5,-90.5,16.1,-91.0,17.8,-89.1,17.8,-88.2,15.7,-89.4,14.4,-92.2,14.5], + ], + }, + CountryOutline { + name: "Guinea", + iso: "GN", + rings: &[ + &[-13.7,12.6,-9.1,12.3,-8.3,7.7,-11.1,10.0,-13.2,8.9,-15.1,11.0,-13.7,12.6], + ], + }, + CountryOutline { + name: "Guinea-Bissau", + iso: "GW", + rings: &[ + &[-16.7,12.4,-13.7,11.8,-15.1,11.0,-16.7,12.4], + ], + }, + CountryOutline { + name: "Guyana", + iso: "GY", + rings: &[ + &[-56.5,1.9,-59.6,1.8,-61.4,6.0,-59.8,8.4,-57.1,6.0,-58.0,4.1,-56.5,1.9], + ], + }, + CountryOutline { + name: "Haiti", + iso: "HT", + rings: &[ + &[-71.7,19.7,-71.7,18.0,-74.5,18.3,-71.7,19.7], + ], + }, + CountryOutline { + name: "Honduras", + iso: "HN", + rings: &[ + &[-83.1,15.0,-87.3,13.0,-89.4,14.4,-87.9,15.9,-83.1,15.0], + ], + }, + CountryOutline { + name: "Hungary", + iso: "HU", + rings: &[ + &[22.1,48.4,21.0,46.3,16.2,46.9,17.0,48.1,22.1,48.4], + ], + }, + CountryOutline { + name: "Iceland", + iso: "IS", + rings: &[ + &[-14.5,66.5,-13.6,65.1,-18.7,63.5,-22.8,64.0,-21.8,64.4,-24.0,64.9,-22.2,65.4,-24.3,65.6,-14.5,66.5], + ], + }, + CountryOutline { + name: "India", + iso: "IN", + rings: &[ + &[97.3,28.3,92.7,22.0,91.2,23.5,92.4,25.0,88.6,26.4,88.9,21.7,80.3,15.9,79.9,10.4,77.5,8.0,72.6,21.4,70.5,20.9,68.2,23.7,71.0,24.4,69.5,26.9,75.3,32.3,73.7,34.3,77.8,35.5,78.7,31.5,81.1,30.2,80.1,28.8,83.3,27.4,88.1,26.4,88.7,28.1,92.0,26.8,96.1,29.5,97.3,28.3], + ], + }, + CountryOutline { + name: "Indonesia", + iso: "ID", + rings: &[ + &[141.0,-2.6,141.0,-9.1,137.6,-8.4,137.9,-5.4,133.0,-4.1,132.0,-2.8,133.7,-2.2,130.5,-0.9,134.0,-0.8,135.5,-3.4,137.4,-1.7,141.0,-2.6], + &[125.0,-8.9,123.5,-10.2,125.0,-8.9], + &[117.9,4.1,119.0,0.9,116.1,-4.0,110.2,-2.9,109.7,2.0,110.5,0.8,113.8,1.2,115.9,4.3,117.9,4.1], + &[129.4,-2.8,130.8,-3.9,127.9,-3.4,129.4,-2.8], + &[127.9,2.2,128.1,-0.9,127.9,2.2], + &[122.9,0.9,125.2,1.4,120.0,-0.5,123.3,-0.6,121.5,-1.9,123.2,-5.3,121.0,-2.6,119.8,-5.7,118.8,-2.8,119.8,0.2,122.9,0.9], + &[120.3,-10.3,119.0,-9.6,120.3,-10.3], + &[121.3,-8.5,122.9,-8.1,119.9,-8.8,121.3,-8.5], + &[118.3,-8.4,116.7,-9.0,118.3,-8.4], + &[108.5,-6.4,115.7,-8.4,105.4,-6.9,108.5,-6.4], + &[104.4,-1.1,106.1,-3.1,105.8,-5.9,102.6,-4.2,95.3,5.5,97.5,5.2,104.4,-1.1], + ], + }, + CountryOutline { + name: "Iran", + iso: "IR", + rings: &[ + &[48.6,29.9,45.4,34.0,46.1,35.7,44.1,39.4,48.1,39.6,50.8,36.9,56.6,38.1,61.1,36.5,60.9,29.8,63.3,26.8,61.5,25.1,57.4,25.7,48.6,29.9], + ], + }, + CountryOutline { + name: "Iraq", + iso: "IQ", + rings: &[ + &[39.2,32.2,41.3,36.4,44.8,37.2,48.6,29.9,44.7,29.2,39.2,32.2], + ], + }, + CountryOutline { + name: "Ireland", + iso: "IE", + rings: &[ + &[-6.2,53.9,-6.8,52.3,-10.0,51.8,-7.6,55.1,-6.2,53.9], + ], + }, + CountryOutline { + name: "Israel", + iso: "IL", + rings: &[ + &[35.7,32.7,34.9,29.5,34.3,31.2,35.7,32.7], + ], + }, + CountryOutline { + name: "Italy", + iso: "IT", + rings: &[ + &[10.4,46.9,13.8,46.5,12.6,44.1,18.3,39.8,16.9,40.4,15.7,37.9,15.4,40.0,10.2,43.9,7.4,43.7,6.8,46.0,10.4,46.9], + &[14.8,38.1,15.1,36.6,12.4,37.6,14.8,38.1], + &[8.7,40.9,9.8,40.5,8.8,38.9,8.7,40.9], + ], + }, + CountryOutline { + name: "Jamaica", + iso: "JM", + rings: &[ + &[-77.6,18.5,-76.2,17.9,-77.6,18.5], + ], + }, + CountryOutline { + name: "Japan", + iso: "JP", + rings: &[ + &[141.9,39.2,140.3,35.1,135.8,33.5,135.1,34.6,131.0,33.9,132.0,33.1,130.2,31.4,129.4,33.3,139.4,38.2,140.3,41.2,141.9,39.2], + &[144.6,44.0,145.5,43.3,140.0,41.6,142.0,45.6,144.6,44.0], + &[132.4,33.5,134.8,33.8,132.4,33.5], + ], + }, + CountryOutline { + name: "Jordan", + iso: "JO", + rings: &[ + &[35.5,32.4,38.8,33.4,39.2,32.2,37.0,31.5,38.0,30.5,36.1,29.2,34.9,29.5,35.5,32.4], + ], + }, + CountryOutline { + name: "Kazakhstan", + iso: "KZ", + rings: &[ + &[87.4,49.2,80.0,44.9,80.3,42.3,74.2,43.3,68.6,40.7,64.9,43.7,62.0,43.5,58.5,45.6,55.9,45.0,56.0,41.3,52.5,41.8,50.3,44.6,53.0,45.3,53.0,46.9,49.1,46.4,46.5,48.4,50.8,51.7,61.3,50.8,60.0,52.0,61.4,54.0,69.1,55.4,73.4,53.5,76.9,54.5,80.0,50.9,87.4,49.2], + ], + }, + CountryOutline { + name: "Kenya", + iso: "KE", + rings: &[ + &[39.2,-4.7,33.9,-0.9,35.3,5.5,38.1,3.6,41.9,3.9,41.6,-1.7,39.2,-4.7], + ], + }, + CountryOutline { + name: "Kosovo", + iso: "XK", + rings: &[ + &[20.6,41.9,20.6,43.2,21.8,42.7,20.6,41.9], + ], + }, + CountryOutline { + name: "Kuwait", + iso: "KW", + rings: &[ + &[48.0,30.0,48.4,28.6,46.6,29.1,48.0,30.0], + ], + }, + CountryOutline { + name: "Kyrgyzstan", + iso: "KG", + rings: &[ + &[71.0,42.3,74.2,43.3,80.3,42.3,73.7,39.4,69.5,39.5,73.1,40.9,71.0,42.3], + ], + }, + CountryOutline { + name: "Laos", + iso: "LA", + rings: &[ + &[107.4,14.2,105.2,14.3,104.0,18.2,101.1,17.5,100.1,20.4,101.7,22.3,104.4,20.8,103.9,19.3,107.4,14.2], + ], + }, + CountryOutline { + name: "Latvia", + iso: "LV", + rings: &[ + &[27.3,57.5,28.2,56.2,26.5,55.6,21.1,56.0,22.5,57.8,27.3,57.5], + ], + }, + CountryOutline { + name: "Lebanon", + iso: "LB", + rings: &[ + &[35.8,33.3,36.4,34.6,35.8,33.3], + ], + }, + CountryOutline { + name: "Lesotho", + iso: "LS", + rings: &[ + &[29.0,-29.0,28.1,-30.5,27.0,-29.9,29.0,-29.0], + ], + }, + CountryOutline { + name: "Liberia", + iso: "LR", + rings: &[ + &[-8.4,7.7,-7.7,4.4,-11.4,6.8,-10.2,8.4,-8.4,7.7], + ], + }, + CountryOutline { + name: "Libya", + iso: "LY", + rings: &[ + &[25.0,22.0,23.8,19.6,10.3,24.4,10.0,31.4,11.5,33.1,19.1,30.3,20.9,32.7,24.9,31.9,25.0,22.0], + ], + }, + CountryOutline { + name: "Lithuania", + iso: "LT", + rings: &[ + &[26.5,55.6,23.5,53.9,21.1,56.0,26.5,55.6], + ], + }, + CountryOutline { + name: "Madagascar", + iso: "MG", + rings: &[ + &[49.5,-12.5,50.4,-15.7,47.1,-24.9,45.4,-25.6,43.3,-22.8,44.0,-17.4,49.5,-12.5], + ], + }, + CountryOutline { + name: "Malawi", + iso: "MW", + rings: &[ + &[32.8,-9.2,35.7,-14.6,35.0,-16.8,32.7,-13.7,32.8,-9.2], + ], + }, + CountryOutline { + name: "Malaysia", + iso: "MY", + rings: &[ + &[100.1,6.5,103.0,5.5,104.2,1.3,101.4,2.8,100.1,6.5], + &[117.9,4.1,115.9,4.3,114.6,1.4,109.8,1.3,115.3,4.3,116.7,6.9,119.2,5.4,117.9,4.1], + ], + }, + CountryOutline { + name: "Mali", + iso: "ML", + rings: &[ + &[-11.5,12.4,-11.7,15.4,-5.5,15.5,-6.5,25.0,4.3,19.2,3.6,15.6,-4.0,13.5,-5.4,10.4,-11.5,12.4], + ], + }, + CountryOutline { + name: "Mauritania", + iso: "MR", + rings: &[ + &[-17.1,21.0,-12.9,21.3,-12.0,25.9,-8.7,25.9,-8.7,27.4,-4.9,25.0,-6.5,25.0,-5.5,15.5,-12.2,14.6,-14.6,16.6,-16.5,16.1,-17.1,21.0], + ], + }, + CountryOutline { + name: "Mexico", + iso: "MX", + rings: &[ + &[-117.1,32.5,-106.5,31.8,-103.9,29.3,-101.7,29.8,-97.1,25.9,-97.9,22.4,-95.9,18.8,-91.4,18.9,-90.3,21.0,-87.1,21.5,-87.8,18.3,-91.0,17.8,-90.5,16.1,-92.2,14.5,-103.5,18.3,-113.1,31.2,-114.9,31.4,-109.9,22.8,-115.1,27.7,-114.2,28.6,-117.1,32.5], + ], + }, + CountryOutline { + name: "Moldova", + iso: "MD", + rings: &[ + &[26.6,48.2,30.0,46.4,28.2,45.5,26.6,48.2], + ], + }, + CountryOutline { + name: "Mongolia", + iso: "MN", + rings: &[ + &[87.8,49.3,92.2,50.8,97.3,49.7,98.9,52.0,108.5,49.3,116.7,49.9,115.7,47.7,119.8,47.0,105.0,41.6,96.3,42.7,90.9,45.3,91.0,46.9,87.8,49.3], + ], + }, + CountryOutline { + name: "Montenegro", + iso: "ME", + rings: &[ + &[20.1,42.6,18.5,42.5,20.1,42.6], + ], + }, + CountryOutline { + name: "Morocco", + iso: "MA", + rings: &[ + &[-2.2,35.2,-1.3,32.3,-8.7,28.8,-8.8,27.1,-11.4,26.9,-14.8,21.5,-17.0,21.4,-14.4,26.3,-9.6,29.9,-8.7,33.2,-5.9,35.8,-2.2,35.2], + ], + }, + CountryOutline { + name: "Mozambique", + iso: "MZ", + rings: &[ + &[34.6,-11.5,40.3,-10.3,40.8,-14.7,34.8,-19.8,35.5,-24.1,32.1,-26.7,31.2,-22.3,32.8,-16.7,30.2,-14.8,33.2,-14.0,35.0,-16.8,34.6,-11.5], + ], + }, + CountryOutline { + name: "Myanmar", + iso: "MM", + rings: &[ + &[100.1,20.4,97.4,18.4,99.6,11.9,98.6,9.9,97.2,16.9,94.2,16.0,92.3,21.5,97.3,28.3,98.7,27.5,97.6,23.9,101.2,21.8,100.1,20.4], + ], + }, + CountryOutline { + name: "N. Cyprus", + iso: "-99", + rings: &[ + &[32.7,35.1,34.6,35.7,32.7,35.1], + ], + }, + CountryOutline { + name: "Namibia", + iso: "NA", + rings: &[ + &[19.9,-24.8,19.9,-28.5,16.3,-28.6,11.7,-17.3,25.1,-17.6,20.9,-18.3,19.9,-24.8], + ], + }, + CountryOutline { + name: "Nepal", + iso: "NP", + rings: &[ + &[88.1,27.9,87.2,26.4,80.1,28.8,81.5,30.4,88.1,27.9], + ], + }, + CountryOutline { + name: "Netherlands", + iso: "NL", + rings: &[ + &[6.9,53.5,6.2,50.8,3.3,51.3,6.9,53.5], + ], + }, + CountryOutline { + name: "New Caledonia", + iso: "NC", + rings: &[ + &[165.8,-21.1,167.1,-22.2,164.0,-20.1,165.8,-21.1], + ], + }, + CountryOutline { + name: "New Zealand", + iso: "NZ", + rings: &[ + &[176.9,-40.1,174.7,-41.3,174.7,-37.4,172.6,-34.5,176.0,-37.6,178.5,-37.7,176.9,-40.1], + &[169.7,-43.6,172.8,-40.5,174.2,-41.3,170.6,-45.9,166.7,-46.2,169.7,-43.6], + ], + }, + CountryOutline { + name: "Nicaragua", + iso: "NI", + rings: &[ + &[-83.7,10.9,-87.7,12.9,-83.1,15.0,-83.7,10.9], + ], + }, + CountryOutline { + name: "Niger", + iso: "NE", + rings: &[ + &[14.9,22.9,15.9,20.4,13.5,14.4,14.2,12.5,5.4,13.9,3.6,11.7,1.0,12.9,0.4,14.9,3.6,15.6,4.3,19.2,12.0,23.5,14.9,22.9], + ], + }, + CountryOutline { + name: "Nigeria", + iso: "NG", + rings: &[ + &[2.7,6.3,4.4,13.7,13.1,13.6,14.6,12.1,11.7,7.0,8.5,4.8,5.9,4.3,2.7,6.3], + ], + }, + CountryOutline { + name: "North Korea", + iso: "KP", + rings: &[ + &[130.6,42.4,127.5,39.8,128.2,38.4,124.7,38.1,125.1,40.6,130.6,42.4], + ], + }, + CountryOutline { + name: "North Macedonia", + iso: "MK", + rings: &[ + &[22.4,42.3,23.0,41.3,20.6,41.1,22.4,42.3], + ], + }, + CountryOutline { + name: "Norway", + iso: "NO", + rings: &[ + &[15.1,79.7,21.5,79.0,15.9,76.8,10.4,79.7,15.1,79.7], + &[31.1,69.6,18.0,68.6,12.6,64.1,11.0,58.9,5.7,58.6,5.0,62.0,19.2,69.8,28.2,71.2,31.1,69.6], + &[27.4,80.1,17.4,80.3,27.4,80.1], + &[24.7,77.9,20.7,77.7,24.7,77.9], + ], + }, + CountryOutline { + name: "Oman", + iso: "OM", + rings: &[ + &[55.2,22.7,56.4,24.9,59.8,22.3,57.7,18.9,53.1,16.7,52.0,19.0,55.0,20.0,55.2,22.7], + ], + }, + CountryOutline { + name: "Pakistan", + iso: "PK", + rings: &[ + &[77.8,35.5,73.7,34.3,75.3,32.3,69.5,26.9,71.0,24.4,61.5,25.1,63.3,26.8,60.9,29.8,66.3,29.9,71.8,36.5,75.2,37.1,77.8,35.5], + ], + }, + CountryOutline { + name: "Panama", + iso: "PA", + rings: &[ + &[-77.4,8.7,-77.9,7.2,-79.1,9.0,-80.9,7.2,-82.9,9.5,-77.4,8.7], + ], + }, + CountryOutline { + name: "Papua New Guinea", + iso: "PG", + rings: &[ + &[141.0,-2.6,147.6,-6.1,147.2,-7.4,150.7,-10.6,144.7,-7.6,141.0,-9.1,141.0,-2.6], + &[152.6,-3.7,152.8,-4.8,150.7,-2.7,152.6,-3.7], + &[151.3,-5.8,148.3,-5.7,152.1,-4.1,151.3,-5.8], + &[154.8,-5.3,155.9,-6.8,154.8,-5.3], + ], + }, + CountryOutline { + name: "Paraguay", + iso: "PY", + rings: &[ + &[-58.2,-20.2,-57.9,-22.1,-54.3,-24.0,-55.7,-27.4,-58.6,-27.1,-57.8,-25.2,-62.7,-22.2,-61.8,-19.6,-58.2,-20.2], + ], + }, + CountryOutline { + name: "Peru", + iso: "PE", + rings: &[ + &[-69.9,-4.3,-72.9,-5.3,-74.0,-7.5,-68.7,-12.6,-70.4,-18.3,-76.0,-14.6,-81.4,-4.7,-80.3,-3.4,-78.6,-4.5,-75.1,-0.1,-73.1,-2.3,-70.0,-2.7,-69.9,-4.3], + ], + }, + CountryOutline { + name: "Philippines", + iso: "PH", + rings: &[ + &[122.6,10.0,124.1,11.2,123.0,9.0,122.6,10.0], + &[126.4,8.4,125.4,5.6,123.6,7.8,121.9,7.2,125.4,9.8,126.4,8.4], + &[118.5,9.3,117.2,8.4,119.5,11.4,118.5,9.3], + &[122.3,18.2,121.7,14.3,124.1,12.5,119.9,15.4,120.7,18.5,122.3,18.2], + &[125.5,12.2,124.8,10.1,124.3,12.6,125.5,12.2], + ], + }, + CountryOutline { + name: "Poland", + iso: "PL", + rings: &[ + &[23.5,53.9,24.0,50.7,22.8,49.0,16.2,50.4,14.1,53.0,17.6,54.9,23.5,53.9], + ], + }, + CountryOutline { + name: "Portugal", + iso: "PT", + rings: &[ + &[-9.0,41.9,-6.4,41.4,-7.9,36.8,-9.5,38.7,-9.0,41.9], + ], + }, + CountryOutline { + name: "Puerto Rico", + iso: "PR", + rings: &[ + &[-66.3,18.5,-67.2,17.9,-66.3,18.5], + ], + }, + CountryOutline { + name: "Qatar", + iso: "QA", + rings: &[ + &[50.8,24.8,51.3,26.1,50.8,24.8], + ], + }, + CountryOutline { + name: "Romania", + iso: "RO", + rings: &[ + &[28.2,45.5,29.6,45.3,28.6,43.7,22.9,43.8,20.2,46.1,26.6,48.2,28.2,45.5], + ], + }, + CountryOutline { + name: "Russia", + iso: "RU", + rings: &[ + &[49.1,46.4,46.7,44.6,47.8,41.2,36.7,45.2,40.1,49.6,31.8,52.1,32.7,53.4,30.9,55.6,27.3,57.5,29.1,60.0,28.1,60.5,31.5,62.9,30.0,63.6,28.6,69.1,32.1,69.9,41.1,67.5,38.4,66.0,33.2,66.6,37.0,63.8,37.2,65.1,43.9,66.1,43.5,68.6,46.3,68.3,46.3,66.7,53.7,68.9,59.9,68.3,60.6,69.9,68.5,68.1,66.7,71.0,69.9,73.0,72.8,72.2,71.8,71.4,73.7,68.4,71.3,66.3,72.4,66.2,75.1,67.8,73.1,71.4,74.7,72.8,76.4,71.2,81.5,71.8,80.5,73.6,104.4,77.7,114.1,75.8,109.4,74.2,127.0,73.6,131.3,70.8,139.9,71.5,139.1,72.4,140.5,72.8,159.0,70.9,160.9,69.4,180.0,69.0,180.0,65.0,177.4,64.6,179.2,62.3,170.3,59.9,163.5,59.9,162.0,58.2,163.2,57.6,162.1,54.9,156.8,51.0,155.9,56.8,164.5,62.6,160.1,60.5,156.7,61.4,154.2,59.8,155.0,59.1,142.2,59.0,135.1,54.7,141.3,53.1,140.1,48.4,134.9,43.4,130.8,42.2,131.0,45.0,133.1,45.1,135.0,48.5,131.0,47.8,123.6,53.5,120.2,52.8,117.9,49.5,108.5,49.3,98.9,52.0,97.3,49.7,92.2,50.8,87.4,49.2,80.0,50.9,76.9,54.5,73.4,53.5,69.1,55.4,61.4,54.0,60.0,52.0,61.3,50.8,50.8,51.7,47.5,50.5,46.5,48.4,49.1,46.4], + &[93.8,81.0,100.2,79.8,97.8,78.8,91.2,80.3,93.8,81.0], + &[102.8,79.3,105.4,78.7,99.4,77.9,102.8,79.3], + &[138.8,76.1,145.1,75.6,137.0,75.3,138.8,76.1], + &[148.2,75.3,150.7,75.1,146.1,75.2,148.2,75.3], + &[139.9,73.4,143.6,73.2,139.9,73.4], + &[44.8,80.6,51.5,80.7,44.8,80.6], + &[22.7,54.3,19.7,54.4,22.7,54.3], + &[53.5,73.7,61.2,76.3,68.9,76.5,58.5,74.3,55.4,72.4,57.5,70.7,51.6,71.5,53.5,73.7], + &[142.9,53.7,144.7,49.0,143.2,49.3,143.5,46.1,142.1,46.0,141.7,53.3,142.9,53.7], + &[-174.9,67.2,-169.9,66.0,-173.0,64.3,-178.7,66.1,-180.0,65.0,-180.0,69.0,-174.9,67.2], + &[-178.7,70.9,-180.0,71.5,-177.6,71.3,-178.7,70.9], + &[33.4,46.0,36.5,45.5,33.9,44.4,32.5,45.3,33.4,46.0], + ], + }, + CountryOutline { + name: "Rwanda", + iso: "RW", + rings: &[ + &[30.4,-1.1,29.0,-2.8,30.4,-1.1], + ], + }, + CountryOutline { + name: "S. Sudan", + iso: "SS", + rings: &[ + &[30.8,3.5,23.9,8.6,25.8,10.4,31.4,9.8,33.2,12.2,33.0,7.8,35.3,5.5,30.8,3.5], + ], + }, + CountryOutline { + name: "Saudi Arabia", + iso: "SA", + rings: &[ + &[35.0,29.4,39.2,32.2,47.5,29.0,52.0,23.0,55.2,22.7,55.0,20.0,47.0,16.9,43.4,17.6,42.8,16.3,35.0,29.4], + ], + }, + CountryOutline { + name: "Senegal", + iso: "SN", + rings: &[ + &[-16.7,13.6,-17.6,14.7,-14.6,16.6,-11.5,12.4,-16.7,12.4,-13.8,13.5,-16.7,13.6], + ], + }, + CountryOutline { + name: "Serbia", + iso: "RS", + rings: &[ + &[18.8,45.9,22.7,44.6,22.5,42.5,19.2,43.5,18.8,45.9], + ], + }, + CountryOutline { + name: "Sierra Leone", + iso: "SL", + rings: &[ + &[-13.2,8.9,-11.1,10.0,-10.2,8.4,-11.4,6.8,-13.2,8.9], + ], + }, + CountryOutline { + name: "Slovakia", + iso: "SK", + rings: &[ + &[22.6,49.1,16.9,48.5,22.6,49.1], + ], + }, + CountryOutline { + name: "Slovenia", + iso: "SI", + rings: &[ + &[13.8,46.5,16.6,46.5,15.3,45.5,13.8,46.5], + ], + }, + CountryOutline { + name: "Solomon Is.", + iso: "SB", + rings: &[ + &[159.6,-8.0,158.2,-7.4,159.6,-8.0], + ], + }, + CountryOutline { + name: "Somalia", + iso: "SO", + rings: &[ + &[41.6,-1.7,41.0,2.8,45.0,5.0,48.9,9.5,48.9,11.4,51.1,12.0,48.6,5.3,41.6,-1.7], + ], + }, + CountryOutline { + name: "Somaliland", + iso: "-99", + rings: &[ + &[48.9,11.4,47.8,8.0,42.6,10.6,48.9,11.4], + ], + }, + CountryOutline { + name: "South Africa", + iso: "ZA", + rings: &[ + &[16.3,-28.6,19.9,-28.5,19.9,-24.8,21.6,-26.7,25.7,-25.5,29.4,-22.1,31.2,-22.3,31.9,-24.4,30.7,-26.7,32.8,-26.7,28.2,-32.8,20.1,-34.8,18.4,-34.1,16.3,-28.6], + ], + }, + CountryOutline { + name: "South Korea", + iso: "KR", + rings: &[ + &[126.2,37.7,128.3,38.6,129.1,35.1,126.5,34.4,126.2,37.7], + ], + }, + CountryOutline { + name: "Spain", + iso: "ES", + rings: &[ + &[-7.5,37.1,-6.4,41.4,-9.4,43.0,3.0,42.5,-2.1,36.7,-7.5,37.1], + ], + }, + CountryOutline { + name: "Sri Lanka", + iso: "LK", + rings: &[ + &[81.8,7.5,80.3,6.0,80.1,9.8,81.8,7.5], + ], + }, + CountryOutline { + name: "Sudan", + iso: "SD", + rings: &[ + &[24.6,8.2,21.9,12.6,25.0,22.0,36.9,22.0,38.4,18.0,34.0,8.7,32.7,12.2,31.4,9.8,25.1,10.3,24.6,8.2], + ], + }, + CountryOutline { + name: "Suriname", + iso: "SR", + rings: &[ + &[-54.5,2.3,-56.5,1.9,-57.6,3.3,-57.1,6.0,-54.0,5.8,-54.5,2.3], + ], + }, + CountryOutline { + name: "Sweden", + iso: "SE", + rings: &[ + &[11.0,58.9,12.6,61.3,11.9,63.1,16.8,68.0,20.6,69.1,23.5,67.9,23.9,66.0,17.8,62.7,17.1,61.3,18.8,60.1,15.9,56.1,12.9,55.4,11.0,58.9], + ], + }, + CountryOutline { + name: "Switzerland", + iso: "CH", + rings: &[ + &[9.6,47.5,10.4,46.5,6.0,46.3,9.6,47.5], + ], + }, + CountryOutline { + name: "Syria", + iso: "SY", + rings: &[ + &[35.7,32.7,36.7,36.8,42.3,37.2,41.0,34.4,35.7,32.7], + ], + }, + CountryOutline { + name: "Taiwan", + iso: "TW", + rings: &[ + &[121.8,24.4,120.7,22.0,120.1,23.6,121.8,24.4], + ], + }, + CountryOutline { + name: "Tajikistan", + iso: "TJ", + rings: &[ + &[67.8,37.1,67.7,39.6,70.7,41.0,69.5,39.5,73.7,39.4,75.0,37.4,71.8,36.7,70.8,38.5,67.8,37.1], + ], + }, + CountryOutline { + name: "Tanzania", + iso: "TZ", + rings: &[ + &[33.9,-0.9,39.2,-4.7,39.5,-10.9,34.6,-11.5,29.6,-6.5,30.4,-1.1,33.9,-0.9], + ], + }, + CountryOutline { + name: "Thailand", + iso: "TH", + rings: &[ + &[105.2,14.3,103.0,14.2,102.6,12.2,100.1,13.4,99.2,9.2,102.1,6.2,101.2,5.7,98.2,8.4,99.6,11.9,97.4,18.4,100.1,20.4,101.1,17.5,104.7,17.4,105.2,14.3], + ], + }, + CountryOutline { + name: "Timor-Leste", + iso: "TL", + rings: &[ + &[125.0,-8.9,127.3,-8.4,125.0,-8.9], + ], + }, + CountryOutline { + name: "Togo", + iso: "TG", + rings: &[ + &[0.9,11.0,1.1,5.9,0.9,11.0], + ], + }, + CountryOutline { + name: "Tunisia", + iso: "TN", + rings: &[ + &[9.5,30.3,7.5,34.1,9.5,37.3,11.0,37.1,10.1,34.3,11.5,33.1,9.5,30.3], + ], + }, + CountryOutline { + name: "Turkey", + iso: "TR", + rings: &[ + &[44.8,37.2,29.7,36.1,27.6,36.7,26.2,39.5,33.5,42.0,42.6,41.6,44.8,39.7,44.8,37.2], + &[26.1,41.8,29.0,41.3,26.4,40.2,26.1,41.8], + ], + }, + CountryOutline { + name: "Turkmenistan", + iso: "TM", + rings: &[ + &[52.5,41.8,57.1,41.3,58.6,42.8,66.5,37.4,62.2,35.3,57.3,38.0,53.9,37.2,52.7,40.0,54.7,41.0,52.5,41.8], + ], + }, + CountryOutline { + name: "Uganda", + iso: "UG", + rings: &[ + &[33.9,-0.9,29.6,-1.3,31.2,3.8,34.5,3.6,33.9,-0.9], + ], + }, + CountryOutline { + name: "Ukraine", + iso: "UA", + rings: &[ + &[31.8,52.1,40.1,49.6,39.7,47.9,35.0,45.7,31.7,46.7,28.7,45.3,30.0,46.4,28.7,48.1,22.1,48.4,23.5,51.6,31.8,52.1], + ], + }, + CountryOutline { + name: "United Arab Emirates", + iso: "AE", + rings: &[ + &[51.6,24.2,56.3,25.7,55.0,22.5,51.6,24.2], + ], + }, + CountryOutline { + name: "United Kingdom", + iso: "GB", + rings: &[ + &[-6.2,53.9,-7.6,55.1,-6.2,53.9], + &[-3.1,53.4,-6.1,56.8,-5.0,58.6,-2.0,57.7,-3.1,56.0,1.7,52.7,1.4,51.3,-5.8,50.2,-3.4,51.4,-5.3,52.0,-4.6,53.5,-3.1,53.4], + ], + }, + CountryOutline { + name: "United States of America", + iso: "US", + rings: &[ + &[-122.8,49.0,-88.4,48.3,-82.6,45.3,-82.7,41.7,-71.5,45.0,-69.2,47.4,-67.0,44.8,-70.1,43.7,-70.0,41.6,-75.5,39.5,-75.9,37.2,-76.3,39.2,-77.0,38.2,-75.7,35.6,-81.3,31.4,-80.4,25.2,-83.7,29.9,-86.4,30.4,-94.7,29.5,-97.5,25.8,-101.0,29.4,-103.9,29.3,-106.5,31.8,-117.1,32.5,-120.6,34.6,-124.4,40.3,-124.7,48.2,-122.6,47.1,-122.8,49.0], + &[-166.5,60.4,-165.6,59.9,-167.5,60.2,-166.5,60.4], + &[-153.2,58.0,-152.1,57.6,-154.5,57.0,-153.2,58.0], + &[-141.0,69.7,-141.0,60.3,-137.5,58.9,-135.5,59.8,-130.0,55.9,-130.5,54.8,-134.1,58.1,-139.9,59.5,-147.1,60.9,-151.7,59.2,-150.6,61.3,-158.4,56.0,-164.9,54.6,-157.0,58.9,-162.0,58.7,-165.3,60.5,-165.7,62.1,-160.8,64.8,-168.1,65.7,-161.7,66.1,-166.2,68.9,-156.6,71.4,-141.0,69.7], + &[-171.7,63.8,-168.7,63.3,-171.7,63.8], + ], + }, + CountryOutline { + name: "Uruguay", + iso: "UY", + rings: &[ + &[-57.6,-30.2,-53.8,-32.0,-53.8,-34.4,-58.4,-33.9,-57.6,-30.2], + ], + }, + CountryOutline { + name: "Uzbekistan", + iso: "UZ", + rings: &[ + &[56.0,41.3,55.9,45.0,58.5,45.6,62.0,43.5,64.9,43.7,68.3,40.7,71.0,42.3,73.1,40.9,67.7,39.6,67.8,37.1,58.6,42.8,56.0,41.3], + ], + }, + CountryOutline { + name: "Venezuela", + iso: "VE", + rings: &[ + &[-60.7,5.2,-64.8,4.1,-63.4,2.2,-66.3,0.7,-67.8,2.8,-67.3,6.1,-72.0,7.0,-72.9,10.5,-71.3,11.8,-71.3,9.1,-69.9,12.2,-68.2,10.6,-61.9,10.7,-59.8,8.4,-60.7,5.2], + ], + }, + CountryOutline { + name: "Vietnam", + iso: "VN", + rings: &[ + &[104.3,10.5,107.5,12.3,107.6,15.2,102.2,22.5,105.3,23.4,108.1,21.6,105.7,19.1,108.9,15.3,109.2,11.7,105.2,8.6,104.3,10.5], + ], + }, + CountryOutline { + name: "W. Sahara", + iso: "EH", + rings: &[ + &[-8.7,27.7,-8.7,25.9,-12.0,25.9,-12.9,21.3,-17.1,21.0,-14.8,21.5,-11.4,26.9,-8.7,27.7], + ], + }, + CountryOutline { + name: "Yemen", + iso: "YE", + rings: &[ + &[52.0,19.0,52.2,15.6,43.5,12.6,43.4,17.6,47.0,16.9,52.0,19.0], + ], + }, + CountryOutline { + name: "Zambia", + iso: "ZM", + rings: &[ + &[30.7,-8.3,33.2,-9.7,33.2,-14.0,27.0,-17.9,23.2,-17.5,21.9,-12.9,24.0,-12.9,23.9,-10.9,29.7,-13.3,28.4,-9.2,30.7,-8.3], + ], + }, + CountryOutline { + name: "Zimbabwe", + iso: "ZW", + rings: &[ + &[31.2,-22.3,28.0,-21.5,25.3,-17.7,30.3,-15.5,32.8,-16.7,31.2,-22.3], + ], + }, +]; diff --git a/ports/rust/src/ui.rs b/ports/rust/src/ui.rs index 2d834ef..1374095 100644 --- a/ports/rust/src/ui.rs +++ b/ports/rust/src/ui.rs @@ -795,6 +795,24 @@ impl<'a> Container<'a> { self.add(constraint, move |s| crate::graphics::draw_canvas(&s, &options)) } + /// A world map, and the country under whatever gets clicked. + /// + /// The click is answered by turning the cell back into degrees and testing + /// it against the outlines, so the answer is the country actually under the + /// cursor. Bounding boxes would be cheaper and wrong: Russia's covers most + /// of the northern hemisphere and Chile's covers Argentina. + pub fn world_map(&mut self, options: w::WorldMapOptions, id: &str) -> &mut Self { + let constraint = self.filling(); + let ctx = self.ctx.clone(); + let id = id.to_string(); + self.add(constraint, move |s| { + w::draw_world_map(&s, &options); + if !id.is_empty() { + ctx.hit(&id, s.hit_rect(), 0); + } + }) + } + pub fn sparkline(&mut self, options: w::SparklineWidgetOptions) -> &mut Self { let constraint = self.leaf(1); self.add(constraint, move |s| w::draw_sparkline(&s, &options)) diff --git a/ports/rust/src/widgets/mod.rs b/ports/rust/src/widgets/mod.rs index 0ae7f78..013c4ef 100644 --- a/ports/rust/src/widgets/mod.rs +++ b/ports/rust/src/widgets/mod.rs @@ -9,6 +9,7 @@ pub mod meters; pub mod scrollbar; pub mod shadow; pub mod surface; +pub mod world; pub mod table; pub mod text; @@ -35,6 +36,7 @@ pub use scrollbar::{ }; pub use shadow::{dim_rect, draw_shadow, ShadowOptions}; pub use surface::{draw_clear, draw_fill, ClearOptions, FillOptions}; +pub use world::{country_at_cell, draw_world_map, WorldMapOptions}; pub use table::{ draw_list, draw_log, draw_table, draw_tree, resolve_offset, TableColumn, ListItem, ListOptions, LogEntry, LogOptions, TableOptions, TableRow, TreeNode, TreeOptions, diff --git a/ports/rust/src/widgets/world.rs b/ports/rust/src/widgets/world.rs new file mode 100644 index 0000000..b9b4469 --- /dev/null +++ b/ports/rust/src/widgets/world.rs @@ -0,0 +1,71 @@ +//! A world map you can click. +//! +//! The drawing is the canvas doing what it already does -- polylines in the +//! caller's own coordinates, which for a map are degrees. What this adds is the +//! other direction: turning a click back into a country. + +use crate::color::Color; +use crate::graphics::canvas::{draw_canvas, Bounds, CanvasOptions}; +use crate::graphics::world::{ + country_at, degrees_at, world_shapes, WorldShapeOptions, WORLD_X, WORLD_Y, +}; +use crate::graphics::world_data::CountryOutline; +use crate::surface::Surface; + +#[derive(Clone, Debug, Default)] +pub struct WorldMapOptions { + /// The window on the globe. Defaults to all of it. + pub x: Option, + pub y: Option, + /// Coastline colour. + pub color: Option, + /// Countries to pick out, by name or ISO code. + pub highlight: Vec, + pub highlight_color: Option, + pub background: Option, + pub grid: bool, +} + +pub fn draw_world_map(surface: &Surface, options: &WorldMapOptions) { + if surface.is_empty() { + return; + } + let theme = surface.theme.clone(); + draw_canvas( + surface, + &CanvasOptions { + shapes: world_shapes(&WorldShapeOptions { + color: Some(options.color.unwrap_or(theme.border)), + highlight: options.highlight.clone(), + highlight_color: Some(options.highlight_color.unwrap_or(theme.accent)), + }), + x: Some(options.x.unwrap_or(WORLD_X)), + y: Some(options.y.unwrap_or(WORLD_Y)), + background: options.background, + grid: options.grid, + ..Default::default() + }, + ); +} + +/// The country under a cell of a map drawn with these bounds. +/// +/// Exposed so a caller can answer a hover as well as a click, and so the +/// arithmetic that has to agree with the drawing lives in one place. +pub fn country_at_cell( + column: usize, + row: usize, + width: usize, + height: usize, + options: &WorldMapOptions, +) -> Option<&'static CountryOutline> { + let (lon, lat) = degrees_at( + column, + row, + width, + height, + options.x.unwrap_or(WORLD_X), + options.y.unwrap_or(WORLD_Y), + )?; + country_at(lon, lat) +} diff --git a/ports/rust/tests/conformance_widgets.rs b/ports/rust/tests/conformance_widgets.rs index 6a28c92..40b9f9d 100644 --- a/ports/rust/tests/conformance_widgets.rs +++ b/ports/rust/tests/conformance_widgets.rs @@ -176,6 +176,22 @@ fn draw_scene(name: &str, s: &Surface) { &ShadowOptions { color: Some(Color::rgb(0x10, 0x14, 0x18)), ..Default::default() }, ); } + "world" => draw_world_map(s, &WorldMapOptions::default()), + "world-zoom" => draw_world_map( + s, + &WorldMapOptions { + x: Some(Bounds { min: 112.0, max: 156.0 }), + y: Some(Bounds { min: 24.0, max: 50.0 }), + ..Default::default() + }, + ), + "world-highlight" => draw_world_map( + s, + &WorldMapOptions { + highlight: vec!["Brazil".into(), "JP".into()], + ..Default::default() + }, + ), "badge" => { draw_badge(s, &BadgeOptions::new("LIVE")); } diff --git a/ports/zig/build.zig b/ports/zig/build.zig index 687e773..6785e8a 100644 --- a/ports/zig/build.zig +++ b/ports/zig/build.zig @@ -46,7 +46,7 @@ pub fn build(b: *std.Build) void { // Examples are separate executables, so `zig build run-screenshot` works // without a TTY while `run-dashboard` takes one over. - for ([_][]const u8{ "hello", "dashboard", "screenshot", "widgets", "collapse", "justify_parity" }) |name| { + for ([_][]const u8{ "hello", "dashboard", "screenshot", "widgets", "collapse", "justify_parity", "world-probe" }) |name| { const command_name = if (std.mem.eql(u8, name, "dashboard")) "dashboard-mini" else name; const exe = b.addExecutable(.{ .name = command_name, diff --git a/ports/zig/examples/world-probe.zig b/ports/zig/examples/world-probe.zig new file mode 100644 index 0000000..26043e3 --- /dev/null +++ b/ports/zig/examples/world-probe.zig @@ -0,0 +1,66 @@ +//! Prints what the country lookup answers for a fixed set of points. +//! +//! The same probe exists for every port, so "the ports agree about the world" is +//! a diff rather than a hope. +const std = @import("std"); +const hqtui = @import("hqtui"); + +const world = hqtui.graphics.world; +const widgets = hqtui.widgets; + +pub fn main(init: std.process.Init) !void { + const stdout = std.Io.File.stdout(); + // A line at a time straight out: this prints a few dozen lines once, so a + // buffer of its own would be ceremony. + var line: [256]u8 = undefined; + + const places = [_]struct { name: []const u8, lon: f64, lat: f64 }{ + .{ .name = "Paris", .lon = 2.35, .lat = 48.86 }, + .{ .name = "Tokyo", .lon = 139.7, .lat = 35.7 }, + .{ .name = "Cairo", .lon = 31.2, .lat = 30.0 }, + .{ .name = "Brasilia", .lon = -47.9, .lat = -15.8 }, + .{ .name = "Canberra", .lon = 149.1, .lat = -35.3 }, + .{ .name = "Denver", .lon = -105.0, .lat = 39.7 }, + .{ .name = "Moscow", .lon = 37.6, .lat = 55.75 }, + .{ .name = "Delhi", .lon = 77.2, .lat = 28.6 }, + .{ .name = "Nairobi", .lon = 36.8, .lat = -1.3 }, + .{ .name = "Pacific", .lon = -140.0, .lat = 0.0 }, + .{ .name = "Atlantic", .lon = -30.0, .lat = 0.0 }, + .{ .name = "SouthernOcean", .lon = 80.0, .lat = -40.0 }, + .{ .name = "NorthPacific", .lon = -150.0, .lat = 40.0 }, + }; + for (places) |place| { + const found = world.countryAt(place.lon, place.lat); + const text = try std.fmt.bufPrint(&line, "{s} {s}\n", .{ + place.name, + if (found) |c| c.name else "-", + }); + try stdout.writeStreamingAll(init.io, text); + } + + // The cell path, which has to agree with what the canvas drew. + const cells = [_][2]usize{ .{ 173, 28 }, .{ 74, 2 }, .{ 20, 25 }, .{ 88, 7 } }; + for (cells) |cell| { + const found = widgets.countryAtCell(cell[0], cell[1], 200, 50, .{}); + const text = try std.fmt.bufPrint(&line, "cell:{d},{d} {s}\n", .{ + cell[0], + cell[1], + if (found) |c| c.name else "-", + }); + try stdout.writeStreamingAll(init.io, text); + } + + // And the projection itself, so a drift shows up as a number rather than as + // a country that happens to still be right. + const probes = [_][2]usize{ .{ 0, 0 }, .{ 99, 25 }, .{ 50, 13 } }; + for (probes) |cell| { + const at = world.degreesAt(cell[0], cell[1], 100, 26, world.WORLD_X, world.WORLD_Y).?; + const text = try std.fmt.bufPrint(&line, "degrees:{d},{d} {d:.4} {d:.4}\n", .{ + cell[0], + cell[1], + at.lon, + at.lat, + }); + try stdout.writeStreamingAll(init.io, text); + } +} diff --git a/ports/zig/src/conformance_widgets.zig b/ports/zig/src/conformance_widgets.zig index 35017c8..3d04a3e 100644 --- a/ports/zig/src/conformance_widgets.zig +++ b/ports/zig/src/conformance_widgets.zig @@ -137,6 +137,15 @@ fn drawScene(allocator: std.mem.Allocator, name: []const u8, s: Surface) !void { w.drawShadow(s, .{ .x = 2, .y = 1, .width = 6, .height = 2 }, .{ .color = hqtui_color.Color.rgb(0x10, 0x14, 0x18), }); + } else if (eq(u8, name, "world")) { + try w.drawWorldMap(allocator, s, .{}); + } else if (eq(u8, name, "world-zoom")) { + try w.drawWorldMap(allocator, s, .{ + .x = .{ .min = 112, .max = 156 }, + .y = .{ .min = 24, .max = 50 }, + }); + } else if (eq(u8, name, "world-highlight")) { + try w.drawWorldMap(allocator, s, .{ .highlight = &.{ "Brazil", "JP" } }); } else if (eq(u8, name, "badge")) { _ = w.drawBadge(s, .{ .text = "LIVE" }); } else if (eq(u8, name, "badge-outline")) { diff --git a/ports/zig/src/graphics.zig b/ports/zig/src/graphics.zig index 42904c8..83377c8 100644 --- a/ports/zig/src/graphics.zig +++ b/ports/zig/src/graphics.zig @@ -4,6 +4,8 @@ pub const blocks = @import("graphics/blocks.zig"); pub const braille = @import("graphics/braille.zig"); pub const canvas_mod = @import("graphics/canvas.zig"); +pub const world = @import("graphics/world.zig"); +pub const world_data = @import("graphics/world_data.zig"); pub const chart_mod = @import("graphics/chart.zig"); pub const plot_mod = @import("graphics/plot.zig"); @@ -35,6 +37,13 @@ pub const gauge = plot_mod.gauge; pub const histogram = plot_mod.histogram; pub const plot = plot_mod.plot; pub const Bounds = canvas_mod.Bounds; +pub const CountryOutline = world_data.CountryOutline; +pub const WORLD_COUNTRIES = world_data.WORLD_COUNTRIES; +pub const countryAt = world.countryAt; +pub const findCountry = world.findCountry; +pub const countryBounds = world.countryBounds; +pub const degreesAt = world.degreesAt; +pub const worldShapes = world.worldShapes; pub const CanvasOptions = canvas_mod.CanvasOptions; pub const Shape = canvas_mod.Shape; pub const ShapeKind = canvas_mod.ShapeKind; diff --git a/ports/zig/src/graphics/world.zig b/ports/zig/src/graphics/world.zig new file mode 100644 index 0000000..adc7a10 --- /dev/null +++ b/ports/zig/src/graphics/world.zig @@ -0,0 +1,202 @@ +//! The world, as shapes for the canvas, and the lookup that makes it clickable. +//! +//! The canvas already draws in the caller's own coordinates, and longitude and +//! latitude are just another pair of axes -- so a map is a list of polylines in +//! degrees, and nothing here needs a projection of its own beyond deciding +//! which window on the globe to show. +//! +//! The interesting half is the other direction. A click arrives as a terminal +//! cell, and a country is a polygon, so answering "what did they click" means +//! turning the cell back into degrees and testing it against the outlines. +//! Doing it that way rather than with bounding boxes is what makes the answer +//! right: Russia's bounding box covers most of the northern hemisphere, and +//! Chile's covers Argentina. + +const std = @import("std"); + +const braille_mod = @import("braille.zig"); +const canvas_mod = @import("canvas.zig"); +const color_mod = @import("../color.zig"); +const world_data = @import("world_data.zig"); + +const Bounds = canvas_mod.Bounds; +const Color = color_mod.Color; +const Point = braille_mod.Point; +const Shape = canvas_mod.Shape; + +pub const CountryOutline = world_data.CountryOutline; +pub const WORLD_COUNTRIES = world_data.WORLD_COUNTRIES; + +/// The whole globe, which is what a map shows unless told otherwise. +pub const WORLD_X = Bounds{ .min = -180, .max = 180 }; +pub const WORLD_Y = Bounds{ .min = -90, .max = 90 }; + +pub const WorldShapeOptions = struct { + /// Colour for countries with nothing special about them. + color: ?Color = null, + /// Countries to pick out, by name or ISO code. + highlight: []const []const u8 = &.{}, + highlight_color: ?Color = null, +}; + +/// Match on either the name or the ISO code, case-insensitively. +fn matches(country: CountryOutline, keys: []const []const u8) bool { + for (keys) |key| { + if (key.len == 0) continue; + if (std.ascii.eqlIgnoreCase(country.name, key)) return true; + if (country.iso.len > 0 and std.ascii.eqlIgnoreCase(country.iso, key)) return true; + } + return false; +} + +/// The world as canvas shapes, one polyline per landmass. +/// +/// Polylines rather than scattered points: the outlines are closed rings, so +/// joining them draws a coastline instead of a dotted suggestion of one, and it +/// reads at a fraction of the resolution dots would need. +/// +/// The caller owns the returned shapes and the points inside them, which is why +/// this takes an allocator: a ring becomes a list of pairs, and there is nowhere +/// else to put a couple of thousand of them. +pub fn worldShapes( + allocator: std.mem.Allocator, + options: WorldShapeOptions, +) ![]Shape { + var shapes = try allocator.alloc(Shape, countRings()); + var at: usize = 0; + errdefer freeShapes(allocator, shapes[0..at]); + + for (WORLD_COUNTRIES) |country| { + const picked = options.highlight.len > 0 and matches(country, options.highlight); + const color = if (picked) (options.highlight_color orelse options.color) else options.color; + for (country.rings) |ring| { + const count = ring.len / 2; + // Closed: the last point joins the first, or every country has a + // gap in its coastline where the ring started. + const points = try allocator.alloc(Point, if (count > 0) count + 1 else 0); + for (0..count) |i| { + points[i] = .{ .x = ring[i * 2], .y = ring[i * 2 + 1] }; + } + if (count > 0) points[count] = points[0]; + shapes[at] = .{ .kind = .polyline, .points = points, .color = color }; + at += 1; + } + } + return shapes; +} + +/// Release what `worldShapes` allocated. +pub fn freeShapes(allocator: std.mem.Allocator, shapes: []Shape) void { + for (shapes) |shape| allocator.free(shape.points); + allocator.free(shapes); +} + +fn countRings() usize { + var total: usize = 0; + for (WORLD_COUNTRIES) |country| total += country.rings.len; + return total; +} + +/// Whether a point is inside a ring, by ray casting. +/// +/// The ring is a flat list of interleaved coordinates, so this walks it two at +/// a time rather than allocating a pair per vertex -- it runs once per country +/// per click, and there are a couple of thousand vertices. +fn insideRing(ring: []const f64, lon: f64, lat: f64) bool { + var inside = false; + const n = ring.len / 2; + if (n == 0) return false; + var j = n - 1; + for (0..n) |i| { + const xi = ring[i * 2]; + const yi = ring[i * 2 + 1]; + const xj = ring[j * 2]; + const yj = ring[j * 2 + 1]; + if ((yi > lat) != (yj > lat) and lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) { + inside = !inside; + } + j = i; + } + return inside; +} + +/// The country containing a point, or null for open water. +/// +/// Where outlines overlap -- and at this resolution simplified borders do +/// overlap -- the first match wins, which is stable because the data is sorted +/// by name. +pub fn countryAt(lon: f64, lat: f64) ?CountryOutline { + if (!std.math.isFinite(lon) or !std.math.isFinite(lat)) return null; + for (WORLD_COUNTRIES) |country| { + for (country.rings) |ring| { + if (insideRing(ring, lon, lat)) return country; + } + } + return null; +} + +/// Look a country up by name or ISO code. +pub fn findCountry(key: []const u8) ?CountryOutline { + const keys = [_][]const u8{key}; + for (WORLD_COUNTRIES) |country| { + if (matches(country, &keys)) return country; + } + return null; +} + +/// The window a country fills, with a little room around it. +/// +/// For zooming a map to a country: the bounding box alone puts the coastline +/// flat against the edge of the panel, which reads as though the country has +/// been cut off rather than framed. +pub fn countryBounds(country: CountryOutline, margin: f64) struct { x: Bounds, y: Bounds } { + var min_lon: f64 = std.math.inf(f64); + var max_lon: f64 = -std.math.inf(f64); + var min_lat: f64 = std.math.inf(f64); + var max_lat: f64 = -std.math.inf(f64); + for (country.rings) |ring| { + var i: usize = 0; + while (i + 1 < ring.len) : (i += 2) { + min_lon = @min(min_lon, ring[i]); + max_lon = @max(max_lon, ring[i]); + min_lat = @min(min_lat, ring[i + 1]); + max_lat = @max(max_lat, ring[i + 1]); + } + } + if (!std.math.isFinite(min_lon)) return .{ .x = WORLD_X, .y = WORLD_Y }; + // A single-point country would give a zero-width window, which cannot be + // mapped onto anything. + const pad_x = @max((max_lon - min_lon) * margin, 1.0); + const pad_y = @max((max_lat - min_lat) * margin, 1.0); + return .{ + .x = .{ .min = min_lon - pad_x, .max = max_lon + pad_x }, + .y = .{ .min = min_lat - pad_y, .max = max_lat + pad_y }, + }; +} + +/// The degrees under a terminal cell, given the window the map was drawn with. +/// +/// The inverse of what the canvas does on the way in, taken at the centre of +/// the cell: a click lands on a whole cell, and the centre is the only point in +/// it that is not arbitrarily nearer one neighbour than the other. +pub fn degreesAt( + column: usize, + row: usize, + width: usize, + height: usize, + x: Bounds, + y: Bounds, +) ?struct { lon: f64, lat: f64 } { + if (width == 0 or height == 0) return null; + // The canvas is 2x4 Braille pixels per cell, and it spans its bounds across + // `pixels - 1`, so the inverse has to use the same denominators or a click + // drifts from what was drawn. + const px: f64 = @floatFromInt(@max(1, width * 2 -| 1)); + const py: f64 = @floatFromInt(@max(1, height * 4 -| 1)); + const cx: f64 = @floatFromInt(column * 2 + 1); + const cy: f64 = @floatFromInt(row * 4 + 2); + return .{ + .lon = x.min + (cx / px) * (x.max - x.min), + .lat = y.min + (1 - cy / py) * (y.max - y.min), + }; +} diff --git a/ports/zig/src/graphics/world_data.zig b/ports/zig/src/graphics/world_data.zig new file mode 100644 index 0000000..2b3b984 --- /dev/null +++ b/ports/zig/src/graphics/world_data.zig @@ -0,0 +1,1306 @@ +//! Country outlines, flattened for a terminal. +//! +//! Generated by packages/hqtui/scripts/generate-world.ts from Natural Earth's +//! 1:110m Admin 0 countries, which is public domain. Do not edit by hand. +//! +//! Each ring is longitude and latitude interleaved -- lon, lat, lon, lat -- +//! rather than a list of pairs, because at 1982 points the nested form +//! costs a container per coordinate for no gain. A country has more than one +//! ring when it is more than one landmass. +//! +//! 171 countries, 1982 points, simplified at 1 degrees. + +pub const CountryOutline = struct { + name: []const u8, + /// ISO 3166-1 alpha-2, where Natural Earth has one. + iso: []const u8, + /// Longitude and latitude, interleaved. + rings: []const []const f64, +}; + +pub const WORLD_COUNTRIES = [_]CountryOutline{ + .{ + .name = "Afghanistan", + .iso = "AF", + .rings = &.{ + &.{66.5,37.4,70.8,38.5,71.8,36.7,75.2,37.1,71.3,36.1,69.3,31.9,66.3,29.9,60.9,29.8,61.2,35.7,66.5,37.4}, + }, + }, + .{ + .name = "Albania", + .iso = "AL", + .rings = &.{ + &.{21.0,40.8,19.4,40.3,19.7,42.7,21.0,40.8}, + }, + }, + .{ + .name = "Algeria", + .iso = "DZ", + .rings = &.{ + &.{-8.7,27.4,-8.7,28.8,-1.3,32.3,-1.2,35.7,8.4,36.9,7.5,34.1,9.8,29.4,9.3,26.1,12.0,23.5,3.2,19.1,-8.7,27.4}, + }, + }, + .{ + .name = "Angola", + .iso = "AO", + .rings = &.{ + &.{12.3,-6.1,16.3,-5.9,17.5,-8.1,21.7,-7.3,22.2,-11.1,24.0,-11.2,24.0,-12.9,21.9,-12.9,23.2,-17.5,11.7,-17.3,13.7,-11.3,12.3,-6.1}, + }, + }, + .{ + .name = "Antarctica", + .iso = "AQ", + .rings = &.{ + &.{-48.7,-78.0,-43.9,-78.5,-43.3,-80.0,-54.2,-80.6,-48.7,-78.0}, + &.{-66.3,-80.3,-59.6,-80.0,-66.3,-80.3}, + &.{-73.9,-71.3,-70.3,-68.9,-68.3,-71.4,-75.0,-72.1,-73.9,-71.3}, + &.{-102.3,-71.9,-96.2,-72.5,-102.3,-71.9}, + &.{-122.6,-73.7,-118.7,-73.5,-122.6,-73.7}, + &.{-127.3,-73.5,-124.0,-73.9,-127.3,-73.5}, + &.{-163.7,-78.6,-159.2,-79.5,-163.7,-78.6}, + &.{180.0,-84.7,180.0,-90.0,-180.0,-90.0,-179.1,-84.1,-143.1,-85.0,-153.6,-83.7,-152.9,-82.0,-156.8,-81.1,-146.4,-80.3,-155.3,-79.1,-158.4,-76.9,-151.3,-77.4,-146.1,-76.5,-146.2,-75.4,-135.2,-74.3,-100.1,-74.9,-103.7,-72.6,-74.9,-73.9,-67.4,-72.5,-67.7,-67.3,-57.8,-63.3,-65.7,-68.0,-61.8,-70.7,-60.8,-73.7,-70.6,-76.6,-77.2,-76.7,-73.7,-77.9,-78.0,-79.2,-58.2,-83.2,-28.5,-80.3,-35.6,-79.5,-35.8,-78.3,-17.5,-75.1,-15.4,-73.1,-6.9,-70.9,27.1,-70.5,33.9,-68.5,38.6,-69.8,54.5,-65.8,61.4,-68.0,68.9,-67.9,69.7,-69.2,67.9,-71.9,69.9,-72.3,73.9,-69.9,88.0,-66.2,95.8,-67.4,102.8,-65.6,106.2,-66.9,113.6,-65.9,119.8,-67.3,135.1,-65.3,137.5,-67.0,145.5,-66.9,171.2,-71.7,163.6,-76.2,167.0,-78.8,161.8,-79.2,159.8,-80.9,169.4,-83.8,180.0,-84.7}, + }, + }, + .{ + .name = "Argentina", + .iso = "AR", + .rings = &.{ + &.{-68.6,-52.6,-65.0,-54.7,-68.6,-54.9,-68.6,-52.6}, + &.{-57.6,-30.2,-58.5,-34.4,-56.8,-36.9,-62.3,-38.8,-62.7,-41.0,-65.1,-41.1,-63.5,-42.6,-67.3,-45.6,-65.6,-47.2,-69.1,-50.7,-68.1,-52.3,-71.9,-52.0,-73.4,-49.3,-71.2,-44.8,-72.1,-42.3,-68.4,-24.5,-66.3,-21.8,-62.8,-22.0,-57.8,-25.2,-58.6,-27.1,-55.7,-27.4,-54.1,-25.5,-53.6,-26.9,-57.6,-30.2}, + }, + }, + .{ + .name = "Armenia", + .iso = "AM", + .rings = &.{ + &.{46.5,38.8,43.6,41.1,45.6,40.8,46.5,38.8}, + }, + }, + .{ + .name = "Australia", + .iso = "AU", + .rings = &.{ + &.{147.7,-40.8,147.9,-43.2,146.0,-43.5,144.7,-40.7,147.7,-40.8}, + &.{126.1,-32.2,118.0,-35.1,115.0,-34.2,113.7,-22.5,120.9,-19.7,125.7,-14.2,129.6,-15.0,132.4,-11.1,136.5,-11.9,135.5,-15.0,140.2,-17.7,142.5,-10.7,146.4,-19.0,150.7,-22.4,153.6,-28.1,150.0,-37.4,146.3,-39.0,140.6,-38.0,138.2,-34.4,136.8,-35.3,137.8,-32.9,136.0,-34.9,131.3,-31.5,126.1,-32.2}, + }, + }, + .{ + .name = "Austria", + .iso = "AT", + .rings = &.{ + &.{17.0,48.1,14.6,46.4,9.5,47.1,12.9,47.5,13.6,48.9,17.0,48.1}, + }, + }, + .{ + .name = "Azerbaijan", + .iso = "AZ", + .rings = &.{ + &.{46.4,41.9,50.4,40.3,48.9,38.3,45.6,39.9,45.0,41.2,46.4,41.9}, + }, + }, + .{ + .name = "Bahamas", + .iso = "BS", + .rings = &.{ + &.{-78.2,25.2,-77.5,23.8,-78.2,25.2}, + }, + }, + .{ + .name = "Bangladesh", + .iso = "BD", + .rings = &.{ + &.{92.7,22.0,92.4,20.7,91.4,22.8,89.0,22.1,88.6,26.4,92.4,25.0,91.2,23.5,92.7,22.0}, + }, + }, + .{ + .name = "Belarus", + .iso = "BY", + .rings = &.{ + &.{28.2,56.2,30.9,55.6,32.7,53.4,31.8,52.1,23.5,51.6,23.5,53.9,28.2,56.2}, + }, + }, + .{ + .name = "Belgium", + .iso = "BE", + .rings = &.{ + &.{6.2,50.8,5.7,49.5,2.5,51.1,6.2,50.8}, + }, + }, + .{ + .name = "Belize", + .iso = "BZ", + .rings = &.{ + &.{-89.1,17.8,-88.1,18.3,-88.9,15.9,-89.1,17.8}, + }, + }, + .{ + .name = "Benin", + .iso = "BJ", + .rings = &.{ + &.{2.7,6.3,0.8,10.5,2.8,12.2,2.7,6.3}, + }, + }, + .{ + .name = "Bhutan", + .iso = "BT", + .rings = &.{ + &.{91.7,27.8,88.8,27.1,91.7,27.8}, + }, + }, + .{ + .name = "Bolivia", + .iso = "BO", + .rings = &.{ + &.{-69.5,-11.0,-65.3,-9.8,-65.4,-11.6,-60.5,-13.8,-60.2,-16.3,-58.2,-16.3,-57.9,-20.0,-61.8,-19.6,-62.7,-22.2,-67.8,-22.9,-69.5,-11.0}, + }, + }, + .{ + .name = "Bosnia and Herz.", + .iso = "BA", + .rings = &.{ + &.{18.6,42.7,16.0,45.2,19.4,44.9,18.6,42.7}, + }, + }, + .{ + .name = "Botswana", + .iso = "BW", + .rings = &.{ + &.{29.4,-22.1,25.7,-25.5,21.6,-26.7,19.9,-24.8,20.9,-18.3,25.3,-17.7,29.4,-22.1}, + }, + }, + .{ + .name = "Brazil", + .iso = "BR", + .rings = &.{ + &.{-53.4,-33.8,-53.8,-32.0,-57.6,-30.2,-53.6,-26.1,-55.8,-22.4,-57.9,-22.1,-58.2,-16.3,-60.2,-16.3,-60.5,-13.8,-65.4,-11.6,-65.3,-9.8,-70.5,-11.0,-70.5,-9.5,-72.2,-10.1,-74.0,-7.5,-72.9,-5.3,-69.9,-4.3,-69.8,1.7,-65.5,0.8,-63.4,2.2,-64.8,4.1,-60.7,5.2,-59.0,1.3,-52.9,2.1,-51.3,4.2,-50.4,-0.1,-44.6,-2.7,-40.0,-2.9,-35.6,-5.1,-34.7,-7.3,-38.7,-13.1,-40.9,-21.9,-47.6,-24.9,-48.9,-28.7,-53.4,-33.8}, + }, + }, + .{ + .name = "Bulgaria", + .iso = "BG", + .rings = &.{ + &.{22.7,44.2,28.6,43.7,28.0,42.0,23.0,41.3,22.7,44.2}, + }, + }, + .{ + .name = "Burkina Faso", + .iso = "BF", + .rings = &.{ + &.{-5.4,10.4,-4.3,13.2,-1.1,15.0,2.2,12.6,0.9,11.0,-2.9,11.0,-2.8,9.6,-5.4,10.4}, + }, + }, + .{ + .name = "Burundi", + .iso = "BI", + .rings = &.{ + &.{30.5,-2.4,29.3,-4.5,29.0,-2.8,30.5,-2.4}, + }, + }, + .{ + .name = "Cambodia", + .iso = "KH", + .rings = &.{ + &.{102.6,12.2,103.0,14.2,107.6,13.5,106.2,11.0,103.5,10.6,102.6,12.2}, + }, + }, + .{ + .name = "Cameroon", + .iso = "CM", + .rings = &.{ + &.{14.5,12.9,14.5,4.7,15.9,1.7,9.6,2.3,8.8,5.5,11.7,7.0,14.5,12.9}, + }, + }, + .{ + .name = "Canada", + .iso = "CA", + .rings = &.{ + &.{-122.8,49.0,-127.4,50.8,-130.5,54.3,-130.0,55.9,-135.5,59.8,-137.5,58.9,-141.0,60.3,-141.0,69.7,-136.5,68.9,-128.1,70.5,-113.5,67.7,-106.1,68.8,-101.5,67.6,-97.7,68.6,-96.1,67.3,-94.2,69.1,-96.5,70.1,-95.2,71.9,-87.4,67.2,-85.5,69.9,-82.6,69.7,-81.4,67.1,-85.8,66.6,-90.7,63.6,-94.7,58.9,-92.3,57.1,-82.3,55.1,-79.9,51.2,-78.6,52.6,-79.8,54.7,-76.5,56.5,-78.5,58.8,-77.3,59.9,-78.1,62.3,-73.8,62.4,-69.6,61.1,-67.6,58.2,-64.6,60.3,-61.8,56.3,-57.3,54.6,-55.7,52.1,-60.0,50.2,-66.4,50.2,-71.1,46.8,-65.1,49.2,-64.5,46.2,-60.5,47.0,-59.8,45.9,-65.4,43.5,-66.2,44.5,-64.4,45.3,-67.1,45.1,-69.2,47.4,-71.5,45.0,-82.4,41.7,-82.6,45.3,-88.4,48.3,-122.8,49.0}, + &.{-84.0,62.5,-81.9,62.9,-84.0,62.5}, + &.{-79.8,72.8,-80.8,73.7,-76.3,72.8,-79.8,72.8}, + &.{-93.6,75.0,-96.8,74.9,-93.6,75.0}, + &.{-93.8,77.5,-96.4,77.8,-93.8,77.5}, + &.{-96.8,78.8,-95.6,78.4,-98.6,78.9,-96.8,78.8}, + &.{-88.2,74.4,-97.1,76.8,-79.8,74.9,-88.2,74.4}, + &.{-111.3,78.2,-109.9,78.0,-113.5,77.7,-111.3,78.2}, + &.{-111.0,78.8,-109.7,78.6,-112.5,78.4,-111.0,78.8}, + &.{-55.6,51.3,-56.8,49.8,-53.5,49.2,-53.1,46.7,-59.3,47.6,-55.6,51.3}, + &.{-83.9,65.1,-80.1,63.7,-87.2,63.5,-85.9,65.7,-83.9,65.1}, + &.{-78.8,72.4,-68.8,70.5,-67.0,69.2,-68.8,68.7,-61.9,66.9,-63.9,65.0,-68.0,66.3,-64.7,63.4,-68.8,63.7,-66.2,61.9,-68.9,62.3,-78.6,64.6,-74.0,65.5,-73.3,68.1,-79.0,70.2,-88.7,70.4,-90.2,72.2,-85.8,73.8,-85.8,72.5,-82.3,73.8,-78.8,72.4}, + &.{-94.5,74.1,-90.5,73.9,-95.4,72.1,-96.0,73.4,-94.5,74.1}, + &.{-122.9,76.1,-116.2,77.6,-122.9,76.1}, + &.{-132.7,54.0,-131.2,52.2,-132.7,54.0}, + &.{-105.5,79.3,-99.7,77.9,-105.5,79.3}, + &.{-123.5,48.5,-128.4,50.8,-123.5,48.5}, + &.{-121.5,74.4,-115.5,73.5,-123.1,70.9,-125.9,71.9,-123.9,73.7,-124.9,74.3,-121.5,74.4}, + &.{-107.8,75.8,-105.7,75.5,-117.7,75.2,-115.4,76.5,-107.8,75.8}, + &.{-106.5,73.1,-101.1,69.6,-113.3,68.5,-117.3,70.0,-112.4,70.4,-119.4,71.6,-115.2,73.3,-108.2,71.7,-108.4,73.1,-106.5,73.1}, + &.{-100.4,72.7,-101.5,73.4,-97.4,73.8,-96.5,72.6,-98.4,71.3,-102.5,72.5,-100.4,72.7}, + &.{-106.6,73.6,-104.5,73.4,-106.6,73.6}, + &.{-98.5,76.7,-98.2,75.0,-102.5,75.6,-98.5,76.7}, + &.{-96.0,80.6,-92.4,81.3,-85.8,79.3,-92.9,78.3,-96.0,80.6}, + &.{-91.6,81.9,-61.8,82.6,-76.9,79.3,-75.4,78.5,-80.6,76.2,-89.5,76.5,-88.3,77.9,-85.0,77.5,-88.0,78.4,-85.1,79.3,-86.9,80.3,-81.8,80.5,-91.6,81.9}, + &.{-75.2,67.4,-77.2,67.6,-75.2,67.4}, + &.{-96.3,69.5,-99.8,69.4,-96.3,69.5}, + &.{-64.5,49.9,-61.8,49.1,-64.5,49.9}, + &.{-64.0,47.0,-62.0,46.4,-64.0,47.0}, + }, + }, + .{ + .name = "Central African Rep.", + .iso = "CF", + .rings = &.{ + &.{27.4,5.2,22.4,4.0,19.5,5.0,16.0,2.3,14.5,5.5,15.3,7.4,22.9,11.1,27.4,5.2}, + }, + }, + .{ + .name = "Chad", + .iso = "TD", + .rings = &.{ + &.{23.8,19.6,23.9,15.6,21.9,12.6,22.9,11.1,15.3,7.4,13.5,14.4,15.9,20.4,14.9,22.9,23.8,19.6}, + }, + }, + .{ + .name = "Chile", + .iso = "CL", + .rings = &.{ + &.{-68.6,-52.6,-68.6,-54.9,-67.0,-54.9,-68.1,-55.6,-74.7,-52.8,-71.1,-54.1,-68.6,-52.6}, + &.{-69.6,-17.6,-67.0,-23.0,-70.5,-31.4,-69.8,-34.2,-72.1,-42.3,-71.2,-44.8,-73.4,-49.3,-71.9,-52.0,-68.6,-52.3,-71.4,-53.9,-74.9,-52.3,-75.6,-48.7,-74.1,-46.9,-75.6,-46.6,-72.7,-42.4,-74.3,-43.2,-69.6,-17.6}, + }, + }, + .{ + .name = "China", + .iso = "CN", + .rings = &.{ + &.{109.5,18.2,108.6,19.4,110.8,20.1,109.5,18.2}, + &.{80.3,42.3,80.0,44.9,87.8,49.3,91.0,46.9,90.9,45.3,96.3,42.7,109.2,42.5,111.9,45.1,119.7,46.7,115.5,48.1,122.2,53.4,125.9,52.8,131.0,47.8,135.0,48.5,133.1,45.1,131.0,45.0,130.6,42.4,121.1,38.9,121.6,40.9,117.5,38.7,122.4,37.5,119.2,34.9,121.9,31.7,121.7,28.2,118.7,24.5,110.4,20.3,105.3,23.4,101.7,22.3,101.8,21.2,99.2,22.1,97.6,23.9,98.7,27.5,96.1,29.5,88.8,27.3,78.7,31.5,78.9,34.3,73.7,39.4,80.3,42.3}, + }, + }, + .{ + .name = "Colombia", + .iso = "CO", + .rings = &.{ + &.{-66.9,1.3,-69.8,1.7,-69.9,-4.3,-70.0,-2.7,-77.4,0.4,-79.0,1.7,-77.1,3.8,-77.5,8.5,-71.4,12.4,-73.3,9.2,-72.0,7.0,-67.3,6.1,-66.9,1.3}, + }, + }, + .{ + .name = "Congo", + .iso = "CG", + .rings = &.{ + &.{18.5,3.5,16.0,-3.5,11.9,-5.0,11.5,-2.8,14.4,-1.3,13.1,2.3,15.9,1.7,18.5,3.5}, + }, + }, + .{ + .name = "Costa Rica", + .iso = "CR", + .rings = &.{ + &.{-82.5,9.6,-83.0,8.2,-85.9,10.9,-82.5,9.6}, + }, + }, + .{ + .name = "Côte d'Ivoire", + .iso = "CI", + .rings = &.{ + &.{-8.0,10.2,-2.8,9.6,-2.9,5.0,-7.7,4.4,-8.0,10.2}, + }, + }, + .{ + .name = "Croatia", + .iso = "HR", + .rings = &.{ + &.{16.6,46.5,19.4,45.2,15.8,44.8,18.5,42.5,13.7,45.1,16.6,46.5}, + }, + }, + .{ + .name = "Cuba", + .iso = "CU", + .rings = &.{ + &.{-82.3,23.2,-74.2,20.3,-77.8,19.9,-81.8,22.6,-85.0,21.9,-82.3,23.2}, + }, + }, + .{ + .name = "Cyprus", + .iso = "CY", + .rings = &.{ + &.{32.7,35.1,34.0,35.0,32.7,35.1}, + }, + }, + .{ + .name = "Czechia", + .iso = "CZ", + .rings = &.{ + &.{15.0,51.1,18.9,49.5,12.5,49.5,15.0,51.1}, + }, + }, + .{ + .name = "Dem. Rep. Congo", + .iso = "CD", + .rings = &.{ + &.{29.3,-4.5,30.7,-8.3,28.7,-8.5,28.4,-11.8,29.7,-13.3,22.2,-11.1,21.7,-7.3,17.5,-8.1,16.3,-5.9,12.2,-5.8,16.0,-3.5,19.5,5.0,29.7,4.6,31.2,2.2,29.3,-4.5}, + }, + }, + .{ + .name = "Denmark", + .iso = "DK", + .rings = &.{ + &.{9.9,55.0,8.1,56.5,10.6,57.7,9.9,55.0}, + &.{12.4,56.1,12.1,54.8,11.0,55.4,12.4,56.1}, + }, + }, + .{ + .name = "Djibouti", + .iso = "DJ", + .rings = &.{ + &.{42.4,12.5,42.8,10.9,42.4,12.5}, + }, + }, + .{ + .name = "Dominican Rep.", + .iso = "DO", + .rings = &.{ + &.{-71.7,18.0,-71.6,19.9,-68.3,18.6,-71.7,18.0}, + }, + }, + .{ + .name = "Ecuador", + .iso = "EC", + .rings = &.{ + &.{-75.4,-0.2,-78.6,-4.5,-80.4,-4.4,-80.1,0.8,-75.4,-0.2}, + }, + }, + .{ + .name = "Egypt", + .iso = "EG", + .rings = &.{ + &.{36.9,22.0,25.0,22.0,25.2,31.6,34.3,31.2,34.2,27.8,32.3,29.8,36.9,22.0}, + }, + }, + .{ + .name = "El Salvador", + .iso = "SV", + .rings = &.{ + &.{-89.4,14.4,-87.9,13.1,-90.1,13.7,-89.4,14.4}, + }, + }, + .{ + .name = "Eq. Guinea", + .iso = "GQ", + .rings = &.{ + &.{9.6,2.3,11.3,1.1,9.5,1.0,9.6,2.3}, + }, + }, + .{ + .name = "Eritrea", + .iso = "ER", + .rings = &.{ + &.{36.4,14.4,38.4,18.0,43.1,12.7,36.4,14.4}, + }, + }, + .{ + .name = "Estonia", + .iso = "EE", + .rings = &.{ + &.{28.0,59.5,27.3,57.5,23.3,59.2,28.0,59.5}, + }, + }, + .{ + .name = "eSwatini", + .iso = "SZ", + .rings = &.{ + &.{32.1,-26.7,31.0,-25.7,32.1,-26.7}, + }, + }, + .{ + .name = "Ethiopia", + .iso = "ET", + .rings = &.{ + &.{47.8,8.0,45.0,5.0,39.6,3.4,36.2,4.4,33.0,7.8,37.9,15.0,41.6,13.5,43.7,9.2,47.8,8.0}, + }, + }, + .{ + .name = "Falkland Is.", + .iso = "FK", + .rings = &.{ + &.{-61.2,-51.8,-57.7,-51.5,-61.2,-51.8}, + }, + }, + .{ + .name = "Finland", + .iso = "FI", + .rings = &.{ + &.{28.6,69.1,31.1,62.4,28.1,60.5,21.3,60.7,21.5,63.2,25.4,65.1,20.6,69.1,24.7,68.6,27.7,70.2,28.6,69.1}, + }, + }, + .{ + .name = "Fr. S. Antarctic Lands", + .iso = "TF", + .rings = &.{ + &.{68.9,-48.6,70.6,-49.3,68.7,-49.8,68.9,-48.6}, + }, + }, + .{ + .name = "France", + .iso = "FR", + .rings = &.{ + &.{-51.7,4.2,-52.9,2.1,-54.5,2.3,-54.0,5.8,-51.7,4.2}, + &.{6.2,49.5,8.1,49.0,6.0,46.7,7.4,43.7,1.8,42.3,-1.9,43.4,-1.2,46.0,-4.6,48.7,-1.6,48.6,-1.9,49.8,2.5,51.1,6.2,49.5}, + &.{8.7,42.6,9.2,41.4,8.7,42.6}, + }, + }, + .{ + .name = "Gabon", + .iso = "GA", + .rings = &.{ + &.{11.3,2.3,14.3,1.2,14.4,-1.3,11.1,-4.0,8.8,-1.1,11.3,2.3}, + }, + }, + .{ + .name = "Gambia", + .iso = "GM", + .rings = &.{ + &.{-16.7,13.6,-13.8,13.5,-16.7,13.6}, + }, + }, + .{ + .name = "Georgia", + .iso = "GE", + .rings = &.{ + &.{40.0,43.4,46.6,41.2,41.6,41.5,40.0,43.4}, + }, + }, + .{ + .name = "Germany", + .iso = "DE", + .rings = &.{ + &.{14.1,53.8,15.0,51.1,12.2,50.3,12.9,47.5,7.5,47.6,8.1,49.0,6.0,50.1,7.1,53.7,9.9,55.0,14.1,53.8}, + }, + }, + .{ + .name = "Ghana", + .iso = "GH", + .rings = &.{ + &.{0.0,11.0,1.1,5.9,-2.9,5.0,-2.9,11.0,0.0,11.0}, + }, + }, + .{ + .name = "Greece", + .iso = "GR", + .rings = &.{ + &.{26.3,35.3,23.5,35.3,26.3,35.3}, + &.{23.0,41.3,26.6,41.6,22.6,40.3,24.0,37.7,22.5,36.4,20.2,39.6,23.0,41.3}, + }, + }, + .{ + .name = "Greenland", + .iso = "GL", + .rings = &.{ + &.{-46.8,82.6,-27.1,83.5,-20.8,82.7,-31.9,82.2,-12.2,81.3,-20.0,80.2,-17.7,80.1,-19.7,78.8,-18.5,77.0,-21.7,76.6,-19.4,74.3,-24.8,72.3,-21.8,70.7,-25.5,71.4,-26.4,70.2,-22.3,70.1,-39.8,65.5,-43.4,60.1,-48.3,60.9,-51.6,63.6,-54.0,67.2,-50.9,69.9,-54.7,69.6,-54.4,70.8,-51.4,70.6,-55.8,71.7,-54.7,72.6,-58.6,75.5,-68.5,76.1,-71.4,77.0,-66.8,77.4,-73.3,78.0,-65.7,79.4,-68.0,80.1,-62.7,81.8,-44.5,81.7,-46.8,82.6}, + }, + }, + .{ + .name = "Guatemala", + .iso = "GT", + .rings = &.{ + &.{-92.2,14.5,-90.5,16.1,-91.0,17.8,-89.1,17.8,-88.2,15.7,-89.4,14.4,-92.2,14.5}, + }, + }, + .{ + .name = "Guinea", + .iso = "GN", + .rings = &.{ + &.{-13.7,12.6,-9.1,12.3,-8.3,7.7,-11.1,10.0,-13.2,8.9,-15.1,11.0,-13.7,12.6}, + }, + }, + .{ + .name = "Guinea-Bissau", + .iso = "GW", + .rings = &.{ + &.{-16.7,12.4,-13.7,11.8,-15.1,11.0,-16.7,12.4}, + }, + }, + .{ + .name = "Guyana", + .iso = "GY", + .rings = &.{ + &.{-56.5,1.9,-59.6,1.8,-61.4,6.0,-59.8,8.4,-57.1,6.0,-58.0,4.1,-56.5,1.9}, + }, + }, + .{ + .name = "Haiti", + .iso = "HT", + .rings = &.{ + &.{-71.7,19.7,-71.7,18.0,-74.5,18.3,-71.7,19.7}, + }, + }, + .{ + .name = "Honduras", + .iso = "HN", + .rings = &.{ + &.{-83.1,15.0,-87.3,13.0,-89.4,14.4,-87.9,15.9,-83.1,15.0}, + }, + }, + .{ + .name = "Hungary", + .iso = "HU", + .rings = &.{ + &.{22.1,48.4,21.0,46.3,16.2,46.9,17.0,48.1,22.1,48.4}, + }, + }, + .{ + .name = "Iceland", + .iso = "IS", + .rings = &.{ + &.{-14.5,66.5,-13.6,65.1,-18.7,63.5,-22.8,64.0,-21.8,64.4,-24.0,64.9,-22.2,65.4,-24.3,65.6,-14.5,66.5}, + }, + }, + .{ + .name = "India", + .iso = "IN", + .rings = &.{ + &.{97.3,28.3,92.7,22.0,91.2,23.5,92.4,25.0,88.6,26.4,88.9,21.7,80.3,15.9,79.9,10.4,77.5,8.0,72.6,21.4,70.5,20.9,68.2,23.7,71.0,24.4,69.5,26.9,75.3,32.3,73.7,34.3,77.8,35.5,78.7,31.5,81.1,30.2,80.1,28.8,83.3,27.4,88.1,26.4,88.7,28.1,92.0,26.8,96.1,29.5,97.3,28.3}, + }, + }, + .{ + .name = "Indonesia", + .iso = "ID", + .rings = &.{ + &.{141.0,-2.6,141.0,-9.1,137.6,-8.4,137.9,-5.4,133.0,-4.1,132.0,-2.8,133.7,-2.2,130.5,-0.9,134.0,-0.8,135.5,-3.4,137.4,-1.7,141.0,-2.6}, + &.{125.0,-8.9,123.5,-10.2,125.0,-8.9}, + &.{117.9,4.1,119.0,0.9,116.1,-4.0,110.2,-2.9,109.7,2.0,110.5,0.8,113.8,1.2,115.9,4.3,117.9,4.1}, + &.{129.4,-2.8,130.8,-3.9,127.9,-3.4,129.4,-2.8}, + &.{127.9,2.2,128.1,-0.9,127.9,2.2}, + &.{122.9,0.9,125.2,1.4,120.0,-0.5,123.3,-0.6,121.5,-1.9,123.2,-5.3,121.0,-2.6,119.8,-5.7,118.8,-2.8,119.8,0.2,122.9,0.9}, + &.{120.3,-10.3,119.0,-9.6,120.3,-10.3}, + &.{121.3,-8.5,122.9,-8.1,119.9,-8.8,121.3,-8.5}, + &.{118.3,-8.4,116.7,-9.0,118.3,-8.4}, + &.{108.5,-6.4,115.7,-8.4,105.4,-6.9,108.5,-6.4}, + &.{104.4,-1.1,106.1,-3.1,105.8,-5.9,102.6,-4.2,95.3,5.5,97.5,5.2,104.4,-1.1}, + }, + }, + .{ + .name = "Iran", + .iso = "IR", + .rings = &.{ + &.{48.6,29.9,45.4,34.0,46.1,35.7,44.1,39.4,48.1,39.6,50.8,36.9,56.6,38.1,61.1,36.5,60.9,29.8,63.3,26.8,61.5,25.1,57.4,25.7,48.6,29.9}, + }, + }, + .{ + .name = "Iraq", + .iso = "IQ", + .rings = &.{ + &.{39.2,32.2,41.3,36.4,44.8,37.2,48.6,29.9,44.7,29.2,39.2,32.2}, + }, + }, + .{ + .name = "Ireland", + .iso = "IE", + .rings = &.{ + &.{-6.2,53.9,-6.8,52.3,-10.0,51.8,-7.6,55.1,-6.2,53.9}, + }, + }, + .{ + .name = "Israel", + .iso = "IL", + .rings = &.{ + &.{35.7,32.7,34.9,29.5,34.3,31.2,35.7,32.7}, + }, + }, + .{ + .name = "Italy", + .iso = "IT", + .rings = &.{ + &.{10.4,46.9,13.8,46.5,12.6,44.1,18.3,39.8,16.9,40.4,15.7,37.9,15.4,40.0,10.2,43.9,7.4,43.7,6.8,46.0,10.4,46.9}, + &.{14.8,38.1,15.1,36.6,12.4,37.6,14.8,38.1}, + &.{8.7,40.9,9.8,40.5,8.8,38.9,8.7,40.9}, + }, + }, + .{ + .name = "Jamaica", + .iso = "JM", + .rings = &.{ + &.{-77.6,18.5,-76.2,17.9,-77.6,18.5}, + }, + }, + .{ + .name = "Japan", + .iso = "JP", + .rings = &.{ + &.{141.9,39.2,140.3,35.1,135.8,33.5,135.1,34.6,131.0,33.9,132.0,33.1,130.2,31.4,129.4,33.3,139.4,38.2,140.3,41.2,141.9,39.2}, + &.{144.6,44.0,145.5,43.3,140.0,41.6,142.0,45.6,144.6,44.0}, + &.{132.4,33.5,134.8,33.8,132.4,33.5}, + }, + }, + .{ + .name = "Jordan", + .iso = "JO", + .rings = &.{ + &.{35.5,32.4,38.8,33.4,39.2,32.2,37.0,31.5,38.0,30.5,36.1,29.2,34.9,29.5,35.5,32.4}, + }, + }, + .{ + .name = "Kazakhstan", + .iso = "KZ", + .rings = &.{ + &.{87.4,49.2,80.0,44.9,80.3,42.3,74.2,43.3,68.6,40.7,64.9,43.7,62.0,43.5,58.5,45.6,55.9,45.0,56.0,41.3,52.5,41.8,50.3,44.6,53.0,45.3,53.0,46.9,49.1,46.4,46.5,48.4,50.8,51.7,61.3,50.8,60.0,52.0,61.4,54.0,69.1,55.4,73.4,53.5,76.9,54.5,80.0,50.9,87.4,49.2}, + }, + }, + .{ + .name = "Kenya", + .iso = "KE", + .rings = &.{ + &.{39.2,-4.7,33.9,-0.9,35.3,5.5,38.1,3.6,41.9,3.9,41.6,-1.7,39.2,-4.7}, + }, + }, + .{ + .name = "Kosovo", + .iso = "XK", + .rings = &.{ + &.{20.6,41.9,20.6,43.2,21.8,42.7,20.6,41.9}, + }, + }, + .{ + .name = "Kuwait", + .iso = "KW", + .rings = &.{ + &.{48.0,30.0,48.4,28.6,46.6,29.1,48.0,30.0}, + }, + }, + .{ + .name = "Kyrgyzstan", + .iso = "KG", + .rings = &.{ + &.{71.0,42.3,74.2,43.3,80.3,42.3,73.7,39.4,69.5,39.5,73.1,40.9,71.0,42.3}, + }, + }, + .{ + .name = "Laos", + .iso = "LA", + .rings = &.{ + &.{107.4,14.2,105.2,14.3,104.0,18.2,101.1,17.5,100.1,20.4,101.7,22.3,104.4,20.8,103.9,19.3,107.4,14.2}, + }, + }, + .{ + .name = "Latvia", + .iso = "LV", + .rings = &.{ + &.{27.3,57.5,28.2,56.2,26.5,55.6,21.1,56.0,22.5,57.8,27.3,57.5}, + }, + }, + .{ + .name = "Lebanon", + .iso = "LB", + .rings = &.{ + &.{35.8,33.3,36.4,34.6,35.8,33.3}, + }, + }, + .{ + .name = "Lesotho", + .iso = "LS", + .rings = &.{ + &.{29.0,-29.0,28.1,-30.5,27.0,-29.9,29.0,-29.0}, + }, + }, + .{ + .name = "Liberia", + .iso = "LR", + .rings = &.{ + &.{-8.4,7.7,-7.7,4.4,-11.4,6.8,-10.2,8.4,-8.4,7.7}, + }, + }, + .{ + .name = "Libya", + .iso = "LY", + .rings = &.{ + &.{25.0,22.0,23.8,19.6,10.3,24.4,10.0,31.4,11.5,33.1,19.1,30.3,20.9,32.7,24.9,31.9,25.0,22.0}, + }, + }, + .{ + .name = "Lithuania", + .iso = "LT", + .rings = &.{ + &.{26.5,55.6,23.5,53.9,21.1,56.0,26.5,55.6}, + }, + }, + .{ + .name = "Madagascar", + .iso = "MG", + .rings = &.{ + &.{49.5,-12.5,50.4,-15.7,47.1,-24.9,45.4,-25.6,43.3,-22.8,44.0,-17.4,49.5,-12.5}, + }, + }, + .{ + .name = "Malawi", + .iso = "MW", + .rings = &.{ + &.{32.8,-9.2,35.7,-14.6,35.0,-16.8,32.7,-13.7,32.8,-9.2}, + }, + }, + .{ + .name = "Malaysia", + .iso = "MY", + .rings = &.{ + &.{100.1,6.5,103.0,5.5,104.2,1.3,101.4,2.8,100.1,6.5}, + &.{117.9,4.1,115.9,4.3,114.6,1.4,109.8,1.3,115.3,4.3,116.7,6.9,119.2,5.4,117.9,4.1}, + }, + }, + .{ + .name = "Mali", + .iso = "ML", + .rings = &.{ + &.{-11.5,12.4,-11.7,15.4,-5.5,15.5,-6.5,25.0,4.3,19.2,3.6,15.6,-4.0,13.5,-5.4,10.4,-11.5,12.4}, + }, + }, + .{ + .name = "Mauritania", + .iso = "MR", + .rings = &.{ + &.{-17.1,21.0,-12.9,21.3,-12.0,25.9,-8.7,25.9,-8.7,27.4,-4.9,25.0,-6.5,25.0,-5.5,15.5,-12.2,14.6,-14.6,16.6,-16.5,16.1,-17.1,21.0}, + }, + }, + .{ + .name = "Mexico", + .iso = "MX", + .rings = &.{ + &.{-117.1,32.5,-106.5,31.8,-103.9,29.3,-101.7,29.8,-97.1,25.9,-97.9,22.4,-95.9,18.8,-91.4,18.9,-90.3,21.0,-87.1,21.5,-87.8,18.3,-91.0,17.8,-90.5,16.1,-92.2,14.5,-103.5,18.3,-113.1,31.2,-114.9,31.4,-109.9,22.8,-115.1,27.7,-114.2,28.6,-117.1,32.5}, + }, + }, + .{ + .name = "Moldova", + .iso = "MD", + .rings = &.{ + &.{26.6,48.2,30.0,46.4,28.2,45.5,26.6,48.2}, + }, + }, + .{ + .name = "Mongolia", + .iso = "MN", + .rings = &.{ + &.{87.8,49.3,92.2,50.8,97.3,49.7,98.9,52.0,108.5,49.3,116.7,49.9,115.7,47.7,119.8,47.0,105.0,41.6,96.3,42.7,90.9,45.3,91.0,46.9,87.8,49.3}, + }, + }, + .{ + .name = "Montenegro", + .iso = "ME", + .rings = &.{ + &.{20.1,42.6,18.5,42.5,20.1,42.6}, + }, + }, + .{ + .name = "Morocco", + .iso = "MA", + .rings = &.{ + &.{-2.2,35.2,-1.3,32.3,-8.7,28.8,-8.8,27.1,-11.4,26.9,-14.8,21.5,-17.0,21.4,-14.4,26.3,-9.6,29.9,-8.7,33.2,-5.9,35.8,-2.2,35.2}, + }, + }, + .{ + .name = "Mozambique", + .iso = "MZ", + .rings = &.{ + &.{34.6,-11.5,40.3,-10.3,40.8,-14.7,34.8,-19.8,35.5,-24.1,32.1,-26.7,31.2,-22.3,32.8,-16.7,30.2,-14.8,33.2,-14.0,35.0,-16.8,34.6,-11.5}, + }, + }, + .{ + .name = "Myanmar", + .iso = "MM", + .rings = &.{ + &.{100.1,20.4,97.4,18.4,99.6,11.9,98.6,9.9,97.2,16.9,94.2,16.0,92.3,21.5,97.3,28.3,98.7,27.5,97.6,23.9,101.2,21.8,100.1,20.4}, + }, + }, + .{ + .name = "N. Cyprus", + .iso = "-99", + .rings = &.{ + &.{32.7,35.1,34.6,35.7,32.7,35.1}, + }, + }, + .{ + .name = "Namibia", + .iso = "NA", + .rings = &.{ + &.{19.9,-24.8,19.9,-28.5,16.3,-28.6,11.7,-17.3,25.1,-17.6,20.9,-18.3,19.9,-24.8}, + }, + }, + .{ + .name = "Nepal", + .iso = "NP", + .rings = &.{ + &.{88.1,27.9,87.2,26.4,80.1,28.8,81.5,30.4,88.1,27.9}, + }, + }, + .{ + .name = "Netherlands", + .iso = "NL", + .rings = &.{ + &.{6.9,53.5,6.2,50.8,3.3,51.3,6.9,53.5}, + }, + }, + .{ + .name = "New Caledonia", + .iso = "NC", + .rings = &.{ + &.{165.8,-21.1,167.1,-22.2,164.0,-20.1,165.8,-21.1}, + }, + }, + .{ + .name = "New Zealand", + .iso = "NZ", + .rings = &.{ + &.{176.9,-40.1,174.7,-41.3,174.7,-37.4,172.6,-34.5,176.0,-37.6,178.5,-37.7,176.9,-40.1}, + &.{169.7,-43.6,172.8,-40.5,174.2,-41.3,170.6,-45.9,166.7,-46.2,169.7,-43.6}, + }, + }, + .{ + .name = "Nicaragua", + .iso = "NI", + .rings = &.{ + &.{-83.7,10.9,-87.7,12.9,-83.1,15.0,-83.7,10.9}, + }, + }, + .{ + .name = "Niger", + .iso = "NE", + .rings = &.{ + &.{14.9,22.9,15.9,20.4,13.5,14.4,14.2,12.5,5.4,13.9,3.6,11.7,1.0,12.9,0.4,14.9,3.6,15.6,4.3,19.2,12.0,23.5,14.9,22.9}, + }, + }, + .{ + .name = "Nigeria", + .iso = "NG", + .rings = &.{ + &.{2.7,6.3,4.4,13.7,13.1,13.6,14.6,12.1,11.7,7.0,8.5,4.8,5.9,4.3,2.7,6.3}, + }, + }, + .{ + .name = "North Korea", + .iso = "KP", + .rings = &.{ + &.{130.6,42.4,127.5,39.8,128.2,38.4,124.7,38.1,125.1,40.6,130.6,42.4}, + }, + }, + .{ + .name = "North Macedonia", + .iso = "MK", + .rings = &.{ + &.{22.4,42.3,23.0,41.3,20.6,41.1,22.4,42.3}, + }, + }, + .{ + .name = "Norway", + .iso = "NO", + .rings = &.{ + &.{15.1,79.7,21.5,79.0,15.9,76.8,10.4,79.7,15.1,79.7}, + &.{31.1,69.6,18.0,68.6,12.6,64.1,11.0,58.9,5.7,58.6,5.0,62.0,19.2,69.8,28.2,71.2,31.1,69.6}, + &.{27.4,80.1,17.4,80.3,27.4,80.1}, + &.{24.7,77.9,20.7,77.7,24.7,77.9}, + }, + }, + .{ + .name = "Oman", + .iso = "OM", + .rings = &.{ + &.{55.2,22.7,56.4,24.9,59.8,22.3,57.7,18.9,53.1,16.7,52.0,19.0,55.0,20.0,55.2,22.7}, + }, + }, + .{ + .name = "Pakistan", + .iso = "PK", + .rings = &.{ + &.{77.8,35.5,73.7,34.3,75.3,32.3,69.5,26.9,71.0,24.4,61.5,25.1,63.3,26.8,60.9,29.8,66.3,29.9,71.8,36.5,75.2,37.1,77.8,35.5}, + }, + }, + .{ + .name = "Panama", + .iso = "PA", + .rings = &.{ + &.{-77.4,8.7,-77.9,7.2,-79.1,9.0,-80.9,7.2,-82.9,9.5,-77.4,8.7}, + }, + }, + .{ + .name = "Papua New Guinea", + .iso = "PG", + .rings = &.{ + &.{141.0,-2.6,147.6,-6.1,147.2,-7.4,150.7,-10.6,144.7,-7.6,141.0,-9.1,141.0,-2.6}, + &.{152.6,-3.7,152.8,-4.8,150.7,-2.7,152.6,-3.7}, + &.{151.3,-5.8,148.3,-5.7,152.1,-4.1,151.3,-5.8}, + &.{154.8,-5.3,155.9,-6.8,154.8,-5.3}, + }, + }, + .{ + .name = "Paraguay", + .iso = "PY", + .rings = &.{ + &.{-58.2,-20.2,-57.9,-22.1,-54.3,-24.0,-55.7,-27.4,-58.6,-27.1,-57.8,-25.2,-62.7,-22.2,-61.8,-19.6,-58.2,-20.2}, + }, + }, + .{ + .name = "Peru", + .iso = "PE", + .rings = &.{ + &.{-69.9,-4.3,-72.9,-5.3,-74.0,-7.5,-68.7,-12.6,-70.4,-18.3,-76.0,-14.6,-81.4,-4.7,-80.3,-3.4,-78.6,-4.5,-75.1,-0.1,-73.1,-2.3,-70.0,-2.7,-69.9,-4.3}, + }, + }, + .{ + .name = "Philippines", + .iso = "PH", + .rings = &.{ + &.{122.6,10.0,124.1,11.2,123.0,9.0,122.6,10.0}, + &.{126.4,8.4,125.4,5.6,123.6,7.8,121.9,7.2,125.4,9.8,126.4,8.4}, + &.{118.5,9.3,117.2,8.4,119.5,11.4,118.5,9.3}, + &.{122.3,18.2,121.7,14.3,124.1,12.5,119.9,15.4,120.7,18.5,122.3,18.2}, + &.{125.5,12.2,124.8,10.1,124.3,12.6,125.5,12.2}, + }, + }, + .{ + .name = "Poland", + .iso = "PL", + .rings = &.{ + &.{23.5,53.9,24.0,50.7,22.8,49.0,16.2,50.4,14.1,53.0,17.6,54.9,23.5,53.9}, + }, + }, + .{ + .name = "Portugal", + .iso = "PT", + .rings = &.{ + &.{-9.0,41.9,-6.4,41.4,-7.9,36.8,-9.5,38.7,-9.0,41.9}, + }, + }, + .{ + .name = "Puerto Rico", + .iso = "PR", + .rings = &.{ + &.{-66.3,18.5,-67.2,17.9,-66.3,18.5}, + }, + }, + .{ + .name = "Qatar", + .iso = "QA", + .rings = &.{ + &.{50.8,24.8,51.3,26.1,50.8,24.8}, + }, + }, + .{ + .name = "Romania", + .iso = "RO", + .rings = &.{ + &.{28.2,45.5,29.6,45.3,28.6,43.7,22.9,43.8,20.2,46.1,26.6,48.2,28.2,45.5}, + }, + }, + .{ + .name = "Russia", + .iso = "RU", + .rings = &.{ + &.{49.1,46.4,46.7,44.6,47.8,41.2,36.7,45.2,40.1,49.6,31.8,52.1,32.7,53.4,30.9,55.6,27.3,57.5,29.1,60.0,28.1,60.5,31.5,62.9,30.0,63.6,28.6,69.1,32.1,69.9,41.1,67.5,38.4,66.0,33.2,66.6,37.0,63.8,37.2,65.1,43.9,66.1,43.5,68.6,46.3,68.3,46.3,66.7,53.7,68.9,59.9,68.3,60.6,69.9,68.5,68.1,66.7,71.0,69.9,73.0,72.8,72.2,71.8,71.4,73.7,68.4,71.3,66.3,72.4,66.2,75.1,67.8,73.1,71.4,74.7,72.8,76.4,71.2,81.5,71.8,80.5,73.6,104.4,77.7,114.1,75.8,109.4,74.2,127.0,73.6,131.3,70.8,139.9,71.5,139.1,72.4,140.5,72.8,159.0,70.9,160.9,69.4,180.0,69.0,180.0,65.0,177.4,64.6,179.2,62.3,170.3,59.9,163.5,59.9,162.0,58.2,163.2,57.6,162.1,54.9,156.8,51.0,155.9,56.8,164.5,62.6,160.1,60.5,156.7,61.4,154.2,59.8,155.0,59.1,142.2,59.0,135.1,54.7,141.3,53.1,140.1,48.4,134.9,43.4,130.8,42.2,131.0,45.0,133.1,45.1,135.0,48.5,131.0,47.8,123.6,53.5,120.2,52.8,117.9,49.5,108.5,49.3,98.9,52.0,97.3,49.7,92.2,50.8,87.4,49.2,80.0,50.9,76.9,54.5,73.4,53.5,69.1,55.4,61.4,54.0,60.0,52.0,61.3,50.8,50.8,51.7,47.5,50.5,46.5,48.4,49.1,46.4}, + &.{93.8,81.0,100.2,79.8,97.8,78.8,91.2,80.3,93.8,81.0}, + &.{102.8,79.3,105.4,78.7,99.4,77.9,102.8,79.3}, + &.{138.8,76.1,145.1,75.6,137.0,75.3,138.8,76.1}, + &.{148.2,75.3,150.7,75.1,146.1,75.2,148.2,75.3}, + &.{139.9,73.4,143.6,73.2,139.9,73.4}, + &.{44.8,80.6,51.5,80.7,44.8,80.6}, + &.{22.7,54.3,19.7,54.4,22.7,54.3}, + &.{53.5,73.7,61.2,76.3,68.9,76.5,58.5,74.3,55.4,72.4,57.5,70.7,51.6,71.5,53.5,73.7}, + &.{142.9,53.7,144.7,49.0,143.2,49.3,143.5,46.1,142.1,46.0,141.7,53.3,142.9,53.7}, + &.{-174.9,67.2,-169.9,66.0,-173.0,64.3,-178.7,66.1,-180.0,65.0,-180.0,69.0,-174.9,67.2}, + &.{-178.7,70.9,-180.0,71.5,-177.6,71.3,-178.7,70.9}, + &.{33.4,46.0,36.5,45.5,33.9,44.4,32.5,45.3,33.4,46.0}, + }, + }, + .{ + .name = "Rwanda", + .iso = "RW", + .rings = &.{ + &.{30.4,-1.1,29.0,-2.8,30.4,-1.1}, + }, + }, + .{ + .name = "S. Sudan", + .iso = "SS", + .rings = &.{ + &.{30.8,3.5,23.9,8.6,25.8,10.4,31.4,9.8,33.2,12.2,33.0,7.8,35.3,5.5,30.8,3.5}, + }, + }, + .{ + .name = "Saudi Arabia", + .iso = "SA", + .rings = &.{ + &.{35.0,29.4,39.2,32.2,47.5,29.0,52.0,23.0,55.2,22.7,55.0,20.0,47.0,16.9,43.4,17.6,42.8,16.3,35.0,29.4}, + }, + }, + .{ + .name = "Senegal", + .iso = "SN", + .rings = &.{ + &.{-16.7,13.6,-17.6,14.7,-14.6,16.6,-11.5,12.4,-16.7,12.4,-13.8,13.5,-16.7,13.6}, + }, + }, + .{ + .name = "Serbia", + .iso = "RS", + .rings = &.{ + &.{18.8,45.9,22.7,44.6,22.5,42.5,19.2,43.5,18.8,45.9}, + }, + }, + .{ + .name = "Sierra Leone", + .iso = "SL", + .rings = &.{ + &.{-13.2,8.9,-11.1,10.0,-10.2,8.4,-11.4,6.8,-13.2,8.9}, + }, + }, + .{ + .name = "Slovakia", + .iso = "SK", + .rings = &.{ + &.{22.6,49.1,16.9,48.5,22.6,49.1}, + }, + }, + .{ + .name = "Slovenia", + .iso = "SI", + .rings = &.{ + &.{13.8,46.5,16.6,46.5,15.3,45.5,13.8,46.5}, + }, + }, + .{ + .name = "Solomon Is.", + .iso = "SB", + .rings = &.{ + &.{159.6,-8.0,158.2,-7.4,159.6,-8.0}, + }, + }, + .{ + .name = "Somalia", + .iso = "SO", + .rings = &.{ + &.{41.6,-1.7,41.0,2.8,45.0,5.0,48.9,9.5,48.9,11.4,51.1,12.0,48.6,5.3,41.6,-1.7}, + }, + }, + .{ + .name = "Somaliland", + .iso = "-99", + .rings = &.{ + &.{48.9,11.4,47.8,8.0,42.6,10.6,48.9,11.4}, + }, + }, + .{ + .name = "South Africa", + .iso = "ZA", + .rings = &.{ + &.{16.3,-28.6,19.9,-28.5,19.9,-24.8,21.6,-26.7,25.7,-25.5,29.4,-22.1,31.2,-22.3,31.9,-24.4,30.7,-26.7,32.8,-26.7,28.2,-32.8,20.1,-34.8,18.4,-34.1,16.3,-28.6}, + }, + }, + .{ + .name = "South Korea", + .iso = "KR", + .rings = &.{ + &.{126.2,37.7,128.3,38.6,129.1,35.1,126.5,34.4,126.2,37.7}, + }, + }, + .{ + .name = "Spain", + .iso = "ES", + .rings = &.{ + &.{-7.5,37.1,-6.4,41.4,-9.4,43.0,3.0,42.5,-2.1,36.7,-7.5,37.1}, + }, + }, + .{ + .name = "Sri Lanka", + .iso = "LK", + .rings = &.{ + &.{81.8,7.5,80.3,6.0,80.1,9.8,81.8,7.5}, + }, + }, + .{ + .name = "Sudan", + .iso = "SD", + .rings = &.{ + &.{24.6,8.2,21.9,12.6,25.0,22.0,36.9,22.0,38.4,18.0,34.0,8.7,32.7,12.2,31.4,9.8,25.1,10.3,24.6,8.2}, + }, + }, + .{ + .name = "Suriname", + .iso = "SR", + .rings = &.{ + &.{-54.5,2.3,-56.5,1.9,-57.6,3.3,-57.1,6.0,-54.0,5.8,-54.5,2.3}, + }, + }, + .{ + .name = "Sweden", + .iso = "SE", + .rings = &.{ + &.{11.0,58.9,12.6,61.3,11.9,63.1,16.8,68.0,20.6,69.1,23.5,67.9,23.9,66.0,17.8,62.7,17.1,61.3,18.8,60.1,15.9,56.1,12.9,55.4,11.0,58.9}, + }, + }, + .{ + .name = "Switzerland", + .iso = "CH", + .rings = &.{ + &.{9.6,47.5,10.4,46.5,6.0,46.3,9.6,47.5}, + }, + }, + .{ + .name = "Syria", + .iso = "SY", + .rings = &.{ + &.{35.7,32.7,36.7,36.8,42.3,37.2,41.0,34.4,35.7,32.7}, + }, + }, + .{ + .name = "Taiwan", + .iso = "TW", + .rings = &.{ + &.{121.8,24.4,120.7,22.0,120.1,23.6,121.8,24.4}, + }, + }, + .{ + .name = "Tajikistan", + .iso = "TJ", + .rings = &.{ + &.{67.8,37.1,67.7,39.6,70.7,41.0,69.5,39.5,73.7,39.4,75.0,37.4,71.8,36.7,70.8,38.5,67.8,37.1}, + }, + }, + .{ + .name = "Tanzania", + .iso = "TZ", + .rings = &.{ + &.{33.9,-0.9,39.2,-4.7,39.5,-10.9,34.6,-11.5,29.6,-6.5,30.4,-1.1,33.9,-0.9}, + }, + }, + .{ + .name = "Thailand", + .iso = "TH", + .rings = &.{ + &.{105.2,14.3,103.0,14.2,102.6,12.2,100.1,13.4,99.2,9.2,102.1,6.2,101.2,5.7,98.2,8.4,99.6,11.9,97.4,18.4,100.1,20.4,101.1,17.5,104.7,17.4,105.2,14.3}, + }, + }, + .{ + .name = "Timor-Leste", + .iso = "TL", + .rings = &.{ + &.{125.0,-8.9,127.3,-8.4,125.0,-8.9}, + }, + }, + .{ + .name = "Togo", + .iso = "TG", + .rings = &.{ + &.{0.9,11.0,1.1,5.9,0.9,11.0}, + }, + }, + .{ + .name = "Tunisia", + .iso = "TN", + .rings = &.{ + &.{9.5,30.3,7.5,34.1,9.5,37.3,11.0,37.1,10.1,34.3,11.5,33.1,9.5,30.3}, + }, + }, + .{ + .name = "Turkey", + .iso = "TR", + .rings = &.{ + &.{44.8,37.2,29.7,36.1,27.6,36.7,26.2,39.5,33.5,42.0,42.6,41.6,44.8,39.7,44.8,37.2}, + &.{26.1,41.8,29.0,41.3,26.4,40.2,26.1,41.8}, + }, + }, + .{ + .name = "Turkmenistan", + .iso = "TM", + .rings = &.{ + &.{52.5,41.8,57.1,41.3,58.6,42.8,66.5,37.4,62.2,35.3,57.3,38.0,53.9,37.2,52.7,40.0,54.7,41.0,52.5,41.8}, + }, + }, + .{ + .name = "Uganda", + .iso = "UG", + .rings = &.{ + &.{33.9,-0.9,29.6,-1.3,31.2,3.8,34.5,3.6,33.9,-0.9}, + }, + }, + .{ + .name = "Ukraine", + .iso = "UA", + .rings = &.{ + &.{31.8,52.1,40.1,49.6,39.7,47.9,35.0,45.7,31.7,46.7,28.7,45.3,30.0,46.4,28.7,48.1,22.1,48.4,23.5,51.6,31.8,52.1}, + }, + }, + .{ + .name = "United Arab Emirates", + .iso = "AE", + .rings = &.{ + &.{51.6,24.2,56.3,25.7,55.0,22.5,51.6,24.2}, + }, + }, + .{ + .name = "United Kingdom", + .iso = "GB", + .rings = &.{ + &.{-6.2,53.9,-7.6,55.1,-6.2,53.9}, + &.{-3.1,53.4,-6.1,56.8,-5.0,58.6,-2.0,57.7,-3.1,56.0,1.7,52.7,1.4,51.3,-5.8,50.2,-3.4,51.4,-5.3,52.0,-4.6,53.5,-3.1,53.4}, + }, + }, + .{ + .name = "United States of America", + .iso = "US", + .rings = &.{ + &.{-122.8,49.0,-88.4,48.3,-82.6,45.3,-82.7,41.7,-71.5,45.0,-69.2,47.4,-67.0,44.8,-70.1,43.7,-70.0,41.6,-75.5,39.5,-75.9,37.2,-76.3,39.2,-77.0,38.2,-75.7,35.6,-81.3,31.4,-80.4,25.2,-83.7,29.9,-86.4,30.4,-94.7,29.5,-97.5,25.8,-101.0,29.4,-103.9,29.3,-106.5,31.8,-117.1,32.5,-120.6,34.6,-124.4,40.3,-124.7,48.2,-122.6,47.1,-122.8,49.0}, + &.{-166.5,60.4,-165.6,59.9,-167.5,60.2,-166.5,60.4}, + &.{-153.2,58.0,-152.1,57.6,-154.5,57.0,-153.2,58.0}, + &.{-141.0,69.7,-141.0,60.3,-137.5,58.9,-135.5,59.8,-130.0,55.9,-130.5,54.8,-134.1,58.1,-139.9,59.5,-147.1,60.9,-151.7,59.2,-150.6,61.3,-158.4,56.0,-164.9,54.6,-157.0,58.9,-162.0,58.7,-165.3,60.5,-165.7,62.1,-160.8,64.8,-168.1,65.7,-161.7,66.1,-166.2,68.9,-156.6,71.4,-141.0,69.7}, + &.{-171.7,63.8,-168.7,63.3,-171.7,63.8}, + }, + }, + .{ + .name = "Uruguay", + .iso = "UY", + .rings = &.{ + &.{-57.6,-30.2,-53.8,-32.0,-53.8,-34.4,-58.4,-33.9,-57.6,-30.2}, + }, + }, + .{ + .name = "Uzbekistan", + .iso = "UZ", + .rings = &.{ + &.{56.0,41.3,55.9,45.0,58.5,45.6,62.0,43.5,64.9,43.7,68.3,40.7,71.0,42.3,73.1,40.9,67.7,39.6,67.8,37.1,58.6,42.8,56.0,41.3}, + }, + }, + .{ + .name = "Venezuela", + .iso = "VE", + .rings = &.{ + &.{-60.7,5.2,-64.8,4.1,-63.4,2.2,-66.3,0.7,-67.8,2.8,-67.3,6.1,-72.0,7.0,-72.9,10.5,-71.3,11.8,-71.3,9.1,-69.9,12.2,-68.2,10.6,-61.9,10.7,-59.8,8.4,-60.7,5.2}, + }, + }, + .{ + .name = "Vietnam", + .iso = "VN", + .rings = &.{ + &.{104.3,10.5,107.5,12.3,107.6,15.2,102.2,22.5,105.3,23.4,108.1,21.6,105.7,19.1,108.9,15.3,109.2,11.7,105.2,8.6,104.3,10.5}, + }, + }, + .{ + .name = "W. Sahara", + .iso = "EH", + .rings = &.{ + &.{-8.7,27.7,-8.7,25.9,-12.0,25.9,-12.9,21.3,-17.1,21.0,-14.8,21.5,-11.4,26.9,-8.7,27.7}, + }, + }, + .{ + .name = "Yemen", + .iso = "YE", + .rings = &.{ + &.{52.0,19.0,52.2,15.6,43.5,12.6,43.4,17.6,47.0,16.9,52.0,19.0}, + }, + }, + .{ + .name = "Zambia", + .iso = "ZM", + .rings = &.{ + &.{30.7,-8.3,33.2,-9.7,33.2,-14.0,27.0,-17.9,23.2,-17.5,21.9,-12.9,24.0,-12.9,23.9,-10.9,29.7,-13.3,28.4,-9.2,30.7,-8.3}, + }, + }, + .{ + .name = "Zimbabwe", + .iso = "ZW", + .rings = &.{ + &.{31.2,-22.3,28.0,-21.5,25.3,-17.7,30.3,-15.5,32.8,-16.7,31.2,-22.3}, + }, + }, +}; diff --git a/ports/zig/src/ui.zig b/ports/zig/src/ui.zig index 72f2837..6d78c07 100644 --- a/ports/zig/src/ui.zig +++ b/ports/zig/src/ui.zig @@ -375,6 +375,7 @@ const Node = union(enum) { calendar: w.CalendarOptions, chart: w.ChartOptions, shapes: graphics.CanvasOptions, + world_map: w.WorldMapOptions, clear: w.ClearOptions, fill: w.FillOptions, sparkline: w.SparklineWidgetOptions, @@ -467,6 +468,7 @@ fn drawNode(ctx: *Ctx, s: Surface, node: Node) anyerror!void { .calendar => |o| w.drawCalendar(s, o), .chart => |o| try w.drawChart(allocator, s, o), .shapes => |o| try graphics.drawCanvas(allocator, s, o), + .world_map => |o| try w.drawWorldMap(allocator, s, o), .clear => |o| w.drawClear(s, o), .fill => |o| w.drawFill(s, o), .sparkline => |o| w.drawSparkline(s, o), @@ -888,6 +890,16 @@ pub const Container = struct { try self.add(self.filling(), .{ .shapes = options }); } + /// A world map, and the country under whatever gets clicked. + /// + /// The click is answered by turning the cell back into degrees and testing + /// it against the outlines, so the answer is the country actually under the + /// cursor. Bounding boxes would be cheaper and wrong: Russia's covers most + /// of the northern hemisphere and Chile's covers Argentina. + pub fn worldMap(self: *Container, options: w.WorldMapOptions) !void { + try self.add(self.filling(), .{ .world_map = options }); + } + pub fn sparkline(self: *Container, options: w.SparklineWidgetOptions) !void { try self.add(self.leaf(1), .{ .sparkline = options }); } diff --git a/ports/zig/src/widgets.zig b/ports/zig/src/widgets.zig index ad4a2e4..acac111 100644 --- a/ports/zig/src/widgets.zig +++ b/ports/zig/src/widgets.zig @@ -7,6 +7,7 @@ pub const meters = @import("widgets/meters.zig"); pub const calendar = @import("widgets/calendar.zig"); pub const chart = @import("widgets/chart.zig"); pub const shadow = @import("widgets/shadow.zig"); +pub const world_map = @import("widgets/world.zig"); pub const surface_widgets = @import("widgets/surface.zig"); pub const scrollbar = @import("widgets/scrollbar.zig"); pub const table = @import("widgets/table.zig"); @@ -68,6 +69,9 @@ pub const CalendarOptions = calendar.CalendarOptions; pub const calendarHeight = calendar.calendarHeight; pub const drawCalendar = calendar.drawCalendar; pub const ChartOptions = chart.ChartOptions; +pub const WorldMapOptions = world_map.WorldMapOptions; +pub const drawWorldMap = world_map.drawWorldMap; +pub const countryAtCell = world_map.countryAtCell; pub const ShadowOptions = shadow.ShadowOptions; pub const dimRect = shadow.dimRect; pub const drawShadow = shadow.drawShadow; diff --git a/ports/zig/src/widgets/world.zig b/ports/zig/src/widgets/world.zig new file mode 100644 index 0000000..30e0665 --- /dev/null +++ b/ports/zig/src/widgets/world.zig @@ -0,0 +1,74 @@ +//! A world map you can click. +//! +//! The drawing is the canvas doing what it already does -- polylines in the +//! caller's own coordinates, which for a map are degrees. What this adds is the +//! other direction: turning a click back into a country. + +const std = @import("std"); + +const canvas_mod = @import("../graphics/canvas.zig"); +const color_mod = @import("../color.zig"); +const surface_mod = @import("../surface.zig"); +const world = @import("../graphics/world.zig"); + +const Bounds = canvas_mod.Bounds; +const Color = color_mod.Color; +const CountryOutline = world.CountryOutline; +const Surface = surface_mod.Surface; + +pub const WorldMapOptions = struct { + /// The window on the globe. Null means all of it. + x: ?Bounds = null, + y: ?Bounds = null, + /// Coastline colour. + color: ?Color = null, + /// Countries to pick out, by name or ISO code. + highlight: []const []const u8 = &.{}, + highlight_color: ?Color = null, + background: ?Color = null, + grid: bool = false, + + fn window(self: WorldMapOptions) struct { x: Bounds, y: Bounds } { + return .{ .x = self.x orelse world.WORLD_X, .y = self.y orelse world.WORLD_Y }; + } +}; + +pub fn drawWorldMap( + allocator: std.mem.Allocator, + s: Surface, + options: WorldMapOptions, +) !void { + if (s.isEmpty()) return; + const theme = s.theme; + const shapes = try world.worldShapes(allocator, .{ + .color = options.color orelse theme.border, + .highlight = options.highlight, + .highlight_color = options.highlight_color orelse theme.accent, + }); + defer world.freeShapes(allocator, shapes); + + const win = options.window(); + try canvas_mod.drawCanvas(allocator, s, .{ + .shapes = shapes, + .x = win.x, + .y = win.y, + .background = options.background, + .grid = options.grid, + }); +} + +/// The country under a cell of a map drawn with these bounds. +/// +/// Exposed so a caller can answer a hover as well as a click, and so the +/// arithmetic that has to agree with the drawing lives in one place. +pub fn countryAtCell( + column: usize, + row: usize, + width: usize, + height: usize, + options: WorldMapOptions, +) ?CountryOutline { + const win = options.window(); + const at = world.degreesAt(column, row, width, height, win.x, win.y) orelse return null; + return world.countryAt(at.lon, at.lat); +}