geokit
Spherical-earth math, location encodings, and geometry operations for Gleam. Runs on the Erlang and JavaScript targets.
gleam add geokit
Usage
Haversine distance and initial bearing:
import geokit/bearing
import geokit/distance
import geokit/latlng
pub fn tokyo_to_osaka() -> #(Float, Float) {
let assert Ok(tokyo) = latlng.new(lat: 35.6812, lng: 139.7671)
let assert Ok(osaka) = latlng.new(lat: 34.6937, lng: 135.5023)
#(
distance.haversine(a: tokyo, b: osaka),
// ≈ 402_784.74 m
bearing.initial(from: tokyo, to: osaka),
// ≈ 255.42°
)
}
Normalising denormalised coordinates with latlng.wrap:
import geokit/latlng
pub fn antimeridian_wrap() -> #(Float, Float) {
// A reading 1° past the antimeridian wraps to the western
// hemisphere; latitudes above 90° are clamped to the pole.
let point = latlng.wrap(lat: 91.0, lng: 181.0)
#(latlng.lat(point), latlng.lng(point))
// == #(90.0, -179.0)
}
Geohash encode, decode, and neighbour lookup:
import geokit/geohash
import geokit/latlng
pub fn around_tokyo() -> Nil {
let assert Ok(tokyo) = latlng.new(lat: 35.6812, lng: 139.7671)
let assert Ok(hash) = geohash.encode(point: tokyo, precision: 8)
// hash == "xn76urx6"
let assert Ok(centre) = geohash.decode(hash: hash)
// centre ≈ tokyo (within the cell's precision)
let assert Ok(neighbours) = geohash.neighbors(hash: hash)
// neighbours.north, neighbours.east, ... eight directions
Nil
}
Google Encoded Polyline:
import geokit/latlng
import geokit/polyline
pub fn route() -> Nil {
let assert Ok(p1) = latlng.new(lat: 38.5, lng: -120.2)
let assert Ok(p2) = latlng.new(lat: 40.7, lng: -120.95)
let assert Ok(p3) = latlng.new(lat: 43.252, lng: -126.453)
let encoded = polyline.encode(points: [p1, p2, p3])
// encoded == "_p~iF~ps|U_ulLnnqC_mqNvxq`@"
let assert Ok(_decoded) = polyline.decode(input: encoded)
Nil
}
GeoJSON encode and decode (RFC 7946) — Feature with typed properties:
import gleam/dynamic/decode
import gleam/json
import gleam/option.{None}
import geokit/geojson
import geokit/geometry
import geokit/latlng
type City {
City(name: String, population: Int)
}
fn city_to_json(c: City) -> json.Json {
json.object([
#("name", json.string(c.name)),
#("population", json.int(c.population)),
])
}
fn city_decoder() -> decode.Decoder(City) {
use name <- decode.field("name", decode.string)
use population <- decode.field("population", decode.int)
decode.success(City(name: name, population: population))
}
pub fn round_trip() -> Nil {
let assert Ok(p) = latlng.new(lat: 35.6812, lng: 139.7671)
let feature =
geojson.Feature(
geometry: geometry.Point(p),
properties: City(name: "Tokyo", population: 13_960_000),
id: None,
)
let encoded =
geojson.encode_feature(feature: feature, properties: city_to_json)
let assert Ok(_decoded) =
geojson.decode_feature(input: encoded, properties: city_decoder())
Nil
}
Decoding a FeatureCollection from an external source — pass
decode.dynamic when you don’t care about typed properties:
import gleam/dynamic/decode
import gleam/list
import geokit/geojson
pub fn parse_collection(input: String) -> Int {
let assert Ok(features) =
geojson.decode_feature_collection(
input: input,
properties: decode.dynamic,
)
list.length(features)
}
Web Mercator tile and Bing quadkey:
import geokit/latlng
import geokit/mercator
pub fn tokyo_tile_at_zoom_5() -> #(Int, Int, String) {
let assert Ok(tokyo) = latlng.new(lat: 35.6812, lng: 139.7671)
let assert Ok(tile) = mercator.from_lat_lng(point: tokyo, zoom: 5)
#(
mercator.x(tile: tile),
mercator.y(tile: tile),
mercator.to_quadkey(tile: tile),
)
}
Bounding box, centroid, line simplification:
import geokit/bbox
import geokit/centroid
import geokit/geometry
import geokit/latlng
import geokit/simplify
pub fn polygon_ops() {
let assert Ok(a) = latlng.new(lat: 0.0, lng: 0.0)
let assert Ok(b) = latlng.new(lat: 0.0, lng: 10.0)
let assert Ok(c) = latlng.new(lat: 10.0, lng: 10.0)
let assert Ok(d) = latlng.new(lat: 10.0, lng: 0.0)
let polygon = geometry.Polygon([[a, b, c, d, a]])
let assert Ok(#(sw, ne)) = bbox.compute(geometry: polygon)
let assert Ok(centre) = centroid.compute(geometry: polygon)
let assert Ok(reduced) =
simplify.line_string(points: [a, b, c, d], tolerance: 0.001)
#(sw, ne, centre, reduced)
}
Full API reference: https://hexdocs.pm/geokit/.
Notes
- Latitudes are in degrees in
[-90, 90]; longitudes in degrees in[-180, 180]. The opaqueLatLngtype enforces this at construction; uselatlng.wrapwhen your source data may be denormalised. - Distance and bearing use the spherical-earth approximation with the WGS84 mean radius of 6_371_008.8 m. The error against an ellipsoidal (Vincenty) distance is bounded by 0.5 % anywhere on Earth.
- All polygon and line operations treat the lat/lng plane as flat — no
projection is applied. For polygons spanning more than a few
degrees, project to Web Mercator via
geokit/mercatorfirst. - Bounding boxes do not wrap around the antimeridian.