Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: strongly typed position type #5

Merged
merged 2 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ While **gleojson** aims to fully implement the GeoJSON specification (RFC 7946),
1. Antimeridian and pole handling
1. Bounding box support
1. Right-hand rule enforcement for polygon orientation
1. Position array length limitation
1. GeometryCollection usage recommendations

Despite these limitations, **gleojson** is fully functional for most common GeoJSON use cases.
Expand All @@ -47,7 +46,7 @@ import gleam/io

pub fn main() {
// Create a Point geometry
let point = gleojson.Point([125.6, 10.1])
let point = gleojson.Point(gleojson.position_2d(lon: 125.6, lat: 10.1))

// Create a Feature with the Point geometry
let feature = gleojson.Feature(
Expand Down
232 changes: 148 additions & 84 deletions src/gleojson.gleam
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
//// Functions for working with GeoJSON data.
////
//// This module provides types and functions for encoding and decoding GeoJSON data.
//// It supports all GeoJSON object types including Point, MultiPoint, LineString,
//// MultiLineString, Polygon, MultiPolygon, GeometryCollection, Feature, and FeatureCollection.
////
//// ## Usage
////
//// To use this module, you can import it in your Gleam code:
////
//// ```gleam
//// import gleojson
//// ```
////
//// Then you can use the provided functions to encode and decode GeoJSON data.

import gleam/dynamic
import gleam/json
import gleam/option
import gleam/result

pub type Position =
List(Float)
pub type Lon {
Lon(Float)
}

pub type Lat {
Lat(Float)
}

pub type Alt {
Alt(Float)
}

pub type Position {
Position2D(#(Lon, Lat))
Position3D(#(Lon, Lat, Alt))
}

pub type Geometry {
Point(coordinates: Position)
Expand Down Expand Up @@ -55,48 +53,109 @@ pub type GeoJSON(properties) {
GeoJSONFeatureCollection(FeatureCollection(properties))
}

/// Creates a 2D Position object from longitude and latitude values.
///
/// This function is a convenience helper for creating a Position object
/// with two dimensions (longitude and latitude).
///
/// ## Arguments
///
/// - `lon`: The longitude value as a Float.
/// - `lat`: The latitude value as a Float.
///
/// ## Returns
///
/// A Position object representing a 2D coordinate.
///
/// ## Example
///
/// ```gleam
/// import gleojson
///
/// pub fn main() {
/// let position = gleojson.position_2d(lon: 125.6, lat: 10.1)
/// // Use this position in your GeoJSON objects, e.g., in a Point geometry
/// let point = gleojson.Point(coordinates: position)
/// }
/// ```
pub fn position_2d(lon lon: Float, lat lat: Float) -> Position {
Position2D(#(Lon(lon), Lat(lat)))
}

/// Creates a 3D Position object from longitude, latitude, and altitude values.
///
/// This function is a convenience helper for creating a Position object
/// with three dimensions (longitude, latitude, and altitude).
///
/// ## Arguments
///
/// - `lon`: The longitude value as a Float.
/// - `lat`: The latitude value as a Float.
/// - `alt`: The altitude value as a Float.
///
/// ## Returns
///
/// A Position object representing a 3D coordinate.
///
/// ## Example
///
/// ```gleam
/// import gleojson
///
/// pub fn main() {
/// let position = gleojson.position_3d(lon: 125.6, lat: 10.1, alt: 100.0)
/// // Use this position in your GeoJSON objects, e.g., in a Point geometry
/// let point = gleojson.Point(coordinates: position)
/// }
/// ```
pub fn position_3d(lon lon: Float, lat lat: Float, alt alt: Float) -> Position {
Position3D(#(Lon(lon), Lat(lat), Alt(alt)))
}

fn encode_position(position: Position) -> json.Json {
case position {
Position2D(#(Lon(lon), Lat(lat))) -> json.array([lon, lat], json.float)
Position3D(#(Lon(lon), Lat(lat), Alt(alt))) ->
json.array([lon, lat, alt], json.float)
}
}

fn encode_geometry(geometry: Geometry) -> json.Json {
case geometry {
Point(coordinates) ->
json.object([
#("type", json.string("Point")),
#("coordinates", json.array(coordinates, json.float)),
#("coordinates", encode_position(coordinates)),
])
MultiPoint(multipoint) ->
json.object([
#("type", json.string("MultiPoint")),
#("coordinates", json.array(multipoint, json.array(_, json.float))),
#("coordinates", json.array(multipoint, encode_position)),
])
LineString(linestring) ->
json.object([
#("type", json.string("LineString")),
#("coordinates", json.array(linestring, json.array(_, json.float))),
#("coordinates", json.array(linestring, encode_position)),
])
MultiLineString(multilinestring) ->
json.object([
#("type", json.string("MultiLineString")),
#(
"coordinates",
json.array(multilinestring, json.array(_, json.array(_, json.float))),
json.array(multilinestring, json.array(_, encode_position)),
),
])
Polygon(polygon) ->
json.object([
#("type", json.string("Polygon")),
#(
"coordinates",
json.array(polygon, json.array(_, json.array(_, json.float))),
),
#("coordinates", json.array(polygon, json.array(_, encode_position))),
])
MultiPolygon(multipolygon) ->
json.object([
#("type", json.string("MultiPolygon")),
#(
"coordinates",
json.array(multipolygon, json.array(_, json.array(_, json.array(
_,
json.float,
)))),
json.array(multipolygon, json.array(_, json.array(_, encode_position))),
),
])
GeometryCollection(collection) ->
Expand Down Expand Up @@ -184,7 +243,7 @@ fn encode_featurecollection(
/// }
///
/// pub fn main() {
/// let point = gleojson.Point([0.0, 0.0])
/// let point = gleojson.Point(gleojson.position_2d(lon: 0.0, lat: 0.0))
/// let properties = CustomProperties("Example", 42.0)
/// let feature = gleojson.Feature(
/// geometry: option.Some(point),
Expand All @@ -209,97 +268,102 @@ pub fn encode_geojson(
}
}

fn position_decoder(
dyn_value: dynamic.Dynamic,
) -> Result(Position, List(dynamic.DecodeError)) {
dynamic.list(dynamic.float)(dyn_value)
fn position_decoder() {
dynamic.any([
dynamic.decode1(
Position3D,
dynamic.tuple3(
dynamic.decode1(Lon, dynamic.float),
dynamic.decode1(Lat, dynamic.float),
dynamic.decode1(Alt, dynamic.float),
),
),
dynamic.decode1(
Position2D,
dynamic.tuple2(
dynamic.decode1(Lon, dynamic.float),
dynamic.decode1(Lat, dynamic.float),
),
),
])
}

fn positions_decoder() {
dynamic.list(position_decoder())
}

fn positions_decoder(
dyn_value: dynamic.Dynamic,
) -> Result(List(Position), List(dynamic.DecodeError)) {
dynamic.list(position_decoder)(dyn_value)
fn positions_list_decoder() {
dynamic.list(positions_decoder())
}

fn positions_list_decoder(
dyn_value: dynamic.Dynamic,
) -> Result(List(List(Position)), List(dynamic.DecodeError)) {
dynamic.list(positions_decoder)(dyn_value)
fn positions_list_list_decoder() {
dynamic.list(positions_list_decoder())
}

fn positions_list_list_decoder(
dyn_value: dynamic.Dynamic,
) -> Result(List(List(List(Position))), List(dynamic.DecodeError)) {
dynamic.list(positions_list_decoder)(dyn_value)
fn type_decoder() {
dynamic.field("type", dynamic.string)
}

fn decode_type_field(
dyn_value: dynamic.Dynamic,
) -> Result(String, List(dynamic.DecodeError)) {
dynamic.field("type", dynamic.string)(dyn_value)
fn coords_decoder(decoder) {
dynamic.field("coordinates", decoder)
}

fn geometry_decoder(
dyn_value: dynamic.Dynamic,
) -> Result(Geometry, List(dynamic.DecodeError)) {
use type_str <- result.try(decode_type_field(dyn_value))
fn geometry_decoder(dyn_value: dynamic.Dynamic) {
use type_str <- result.try(type_decoder()(dyn_value))
case type_str {
"Point" ->
dynamic.field("coordinates", position_decoder)(dyn_value)
|> result.map(Point)
"Point" -> dynamic.decode1(Point, coords_decoder(position_decoder()))
"MultiPoint" ->
dynamic.field("coordinates", positions_decoder)(dyn_value)
|> result.map(MultiPoint)
dynamic.decode1(MultiPoint, coords_decoder(positions_decoder()))
"LineString" ->
dynamic.field("coordinates", positions_decoder)(dyn_value)
|> result.map(LineString)
dynamic.decode1(LineString, coords_decoder(positions_decoder()))
"MultiLineString" ->
dynamic.field("coordinates", positions_list_decoder)(dyn_value)
|> result.map(MultiLineString)
dynamic.decode1(MultiLineString, coords_decoder(positions_list_decoder()))
"Polygon" ->
dynamic.field("coordinates", positions_list_decoder)(dyn_value)
|> result.map(Polygon)
dynamic.decode1(Polygon, coords_decoder(positions_list_decoder()))
"MultiPolygon" ->
dynamic.field("coordinates", positions_list_list_decoder)(dyn_value)
|> result.map(MultiPolygon)
dynamic.decode1(
MultiPolygon,
coords_decoder(positions_list_list_decoder()),
)
"GeometryCollection" ->
dynamic.field("geometries", dynamic.list(geometry_decoder))(dyn_value)
|> result.map(GeometryCollection)
_ ->
dynamic.decode1(
GeometryCollection,
dynamic.field("geometries", dynamic.list(geometry_decoder)),
)
_ -> fn(_) {
Error([
dynamic.DecodeError(
expected: "Known Geometry Type",
expected: "one of [Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection]",
found: type_str,
path: ["type"],
),
])
}
}
}(dyn_value)
}

fn feature_id_decoder(
dyn_value: dynamic.Dynamic,
) -> Result(FeatureId, List(dynamic.DecodeError)) {
dynamic.string(dyn_value)
|> result.map(StringId)
|> result.lazy_or(fn() { dynamic.float(dyn_value) |> result.map(NumberId) })
fn feature_id_decoder() {
dynamic.any([
dynamic.decode1(StringId, dynamic.string),
dynamic.decode1(NumberId, dynamic.float),
])
}

fn feature_decoder(properties_decoder: dynamic.Decoder(properties)) {
fn(dyn_value: dynamic.Dynamic) -> Result(
Feature(properties),
List(dynamic.DecodeError),
) {
use type_str <- result.try(decode_type_field(dyn_value))
use type_str <- result.try(type_decoder()(dyn_value))
case type_str {
"Feature" -> {
dynamic.decode3(
Feature,
dynamic.field("geometry", dynamic.optional(geometry_decoder)),
dynamic.field("properties", dynamic.optional(properties_decoder)),
dynamic.optional_field("id", feature_id_decoder),
dynamic.optional_field("id", feature_id_decoder()),
)(dyn_value)
}

_ ->
Error([
dynamic.DecodeError(expected: "Feature", found: type_str, path: [
Expand All @@ -315,7 +379,7 @@ fn featurecollection_decoder(properties_decoder: dynamic.Decoder(properties)) {
FeatureCollection(properties),
List(dynamic.DecodeError),
) {
use type_str <- result.try(decode_type_field(dyn_value))
use type_str <- result.try(type_decoder()(dyn_value))
case type_str {
"FeatureCollection" ->
dynamic.decode1(
Expand Down Expand Up @@ -409,7 +473,7 @@ pub fn geojson_decoder(properties_decoder: dynamic.Decoder(properties)) {
GeoJSON(properties),
List(dynamic.DecodeError),
) {
use type_str <- result.try(decode_type_field(dyn_value))
use type_str <- result.try(type_decoder()(dyn_value))
case type_str {
"Feature" ->
dynamic.decode1(GeoJSONFeature, feature_decoder(properties_decoder))
Expand Down
Loading