diff --git a/README.md b/README.md index e7f3048..9a28b21 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,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( diff --git a/src/gleojson.gleam b/src/gleojson.gleam index b09a154..e3849bb 100644 --- a/src/gleojson.gleam +++ b/src/gleojson.gleam @@ -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) @@ -55,29 +53,98 @@ 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, fn(line) { + json.array(line, encode_position) + }), ), ]) Polygon(polygon) -> @@ -85,7 +152,7 @@ fn encode_geometry(geometry: Geometry) -> json.Json { #("type", json.string("Polygon")), #( "coordinates", - json.array(polygon, json.array(_, json.array(_, json.float))), + json.array(polygon, fn(ring) { json.array(ring, encode_position) }), ), ]) MultiPolygon(multipolygon) -> @@ -93,10 +160,9 @@ fn encode_geometry(geometry: Geometry) -> json.Json { #("type", json.string("MultiPolygon")), #( "coordinates", - json.array(multipolygon, json.array(_, json.array(_, json.array( - _, - json.float, - )))), + json.array(multipolygon, fn(polygon) { + json.array(polygon, fn(ring) { json.array(ring, encode_position) }) + }), ), ]) GeometryCollection(collection) -> @@ -184,7 +250,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), @@ -212,7 +278,23 @@ pub fn encode_geojson( fn position_decoder( dyn_value: dynamic.Dynamic, ) -> Result(Position, List(dynamic.DecodeError)) { - dynamic.list(dynamic.float)(dyn_value) + 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), + ), + ), + ])(dyn_value) } fn positions_decoder( @@ -268,7 +350,7 @@ fn geometry_decoder( _ -> Error([ dynamic.DecodeError( - expected: "Known Geometry Type", + expected: "one of [Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection]", found: type_str, path: ["type"], ), @@ -279,9 +361,10 @@ fn geometry_decoder( 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) }) + dynamic.any([ + dynamic.decode1(StringId, dynamic.string), + dynamic.decode1(NumberId, dynamic.float), + ])(dyn_value) } fn feature_decoder(properties_decoder: dynamic.Decoder(properties)) { diff --git a/test/gleojson_test.gleam b/test/gleojson_test.gleam index 74a9b54..fa290a0 100644 --- a/test/gleojson_test.gleam +++ b/test/gleojson_test.gleam @@ -134,7 +134,10 @@ fn assert_encode_decode( // Test functions for separate geometries pub fn point_encode_decode_test() { - let geojson = gleojson.GeoJSONGeometry(gleojson.Point([1.0, 2.0])) + let geojson = + gleojson.GeoJSONGeometry( + gleojson.Point(gleojson.position_2d(lon: 1.0, lat: 2.0)), + ) assert_encode_decode( geojson, @@ -146,7 +149,12 @@ pub fn point_encode_decode_test() { pub fn multipoint_encode_decode_test() { let geojson = - gleojson.GeoJSONGeometry(gleojson.MultiPoint([[1.0, 2.0], [3.0, 4.0]])) + gleojson.GeoJSONGeometry( + gleojson.MultiPoint([ + gleojson.position_2d(lon: 1.0, lat: 2.0), + gleojson.position_2d(lon: 3.0, lat: 4.0), + ]), + ) assert_encode_decode( geojson, @@ -158,7 +166,12 @@ pub fn multipoint_encode_decode_test() { pub fn linestring_encode_decode_test() { let geojson = - gleojson.GeoJSONGeometry(gleojson.LineString([[1.0, 2.0], [3.0, 4.0]])) + gleojson.GeoJSONGeometry( + gleojson.LineString([ + gleojson.position_2d(lon: 1.0, lat: 2.0), + gleojson.position_2d(lon: 3.0, lat: 4.0), + ]), + ) assert_encode_decode( geojson, @@ -171,7 +184,14 @@ pub fn linestring_encode_decode_test() { pub fn polygon_encode_decode_test() { let geojson = gleojson.GeoJSONGeometry( - gleojson.Polygon([[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [1.0, 2.0]]]), + gleojson.Polygon([ + [ + gleojson.position_2d(lon: 1.0, lat: 2.0), + gleojson.position_2d(lon: 3.0, lat: 4.0), + gleojson.position_2d(lon: 5.0, lat: 6.0), + gleojson.position_2d(lon: 1.0, lat: 2.0), + ], + ]), ) assert_encode_decode( @@ -186,8 +206,22 @@ pub fn multipolygon_encode_decode_test() { let geojson = gleojson.GeoJSONGeometry( gleojson.MultiPolygon([ - [[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [1.0, 2.0]]], - [[[7.0, 8.0], [9.0, 10.0], [11.0, 12.0], [7.0, 8.0]]], + [ + [ + gleojson.position_2d(lon: 1.0, lat: 2.0), + gleojson.position_2d(lon: 3.0, lat: 4.0), + gleojson.position_2d(lon: 5.0, lat: 6.0), + gleojson.position_2d(lon: 1.0, lat: 2.0), + ], + ], + [ + [ + gleojson.position_2d(lon: 7.0, lat: 8.0), + gleojson.position_2d(lon: 9.0, lat: 10.0), + gleojson.position_2d(lon: 11.0, lat: 12.0), + gleojson.position_2d(lon: 7.0, lat: 8.0), + ], + ], ]), ) @@ -203,8 +237,11 @@ pub fn geometrycollection_encode_decode_test() { let geojson = gleojson.GeoJSONGeometry( gleojson.GeometryCollection([ - gleojson.Point([1.0, 2.0]), - gleojson.LineString([[3.0, 4.0], [5.0, 6.0]]), + gleojson.Point(gleojson.position_2d(lon: 1.0, lat: 2.0)), + gleojson.LineString([ + gleojson.position_2d(lon: 3.0, lat: 4.0), + gleojson.position_2d(lon: 5.0, lat: 6.0), + ]), ]), ) @@ -221,7 +258,9 @@ pub fn feature_encode_decode_test() { let feature = gleojson.Feature( - geometry: option.Some(gleojson.Point([1.0, 2.0])), + geometry: option.Some( + gleojson.Point(gleojson.position_2d(lon: 1.0, lat: 2.0)), + ), properties: option.Some(properties), id: option.Some(gleojson.StringId("feature-id")), ) @@ -244,10 +283,10 @@ pub fn real_life_feature_test() { geometry: option.Some( gleojson.Polygon([ [ - [-119.5383, 37.8651], - [-119.5127, 37.8777], - [-119.4939, 37.8685], - [-119.5383, 37.8651], + gleojson.position_2d(lon: -119.5383, lat: 37.8651), + gleojson.position_2d(lon: -119.5127, lat: 37.8777), + gleojson.position_2d(lon: -119.4939, lat: 37.8685), + gleojson.position_2d(lon: -119.5383, lat: 37.8651), ], ]), ), @@ -270,7 +309,9 @@ pub fn real_life_featurecollection_test() { let city_feature = gleojson.Feature( - geometry: option.Some(gleojson.Point([139.6917, 35.6895])), + geometry: option.Some( + gleojson.Point(gleojson.position_2d(lon: 139.6917, lat: 35.6895)), + ), properties: option.Some(city_properties), id: option.Some(gleojson.StringId("tokyo")), ) @@ -282,9 +323,9 @@ pub fn real_life_featurecollection_test() { gleojson.Feature( geometry: option.Some( gleojson.LineString([ - [-115.1728, 36.1147], - [-116.2139, 36.5674], - [-117.1522, 36.6567], + gleojson.position_2d(lon: -115.1728, lat: 36.1147), + gleojson.position_2d(lon: -116.2139, lat: 36.5674), + gleojson.position_2d(lon: -117.1522, lat: 36.6567), ]), ), properties: option.Some(river_properties),