src/geokit@geojson.erl

-module(geokit@geojson).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/geokit/geojson.gleam").
-export([encode_geometry/1, encode_feature/2, encode_feature_collection/2, decode_geometry/1, decode_feature/2, decode_feature_collection/2]).
-export_type([geo_json_error/0, feature_id/0, feature/1]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " GeoJSON (RFC 7946) encode / decode.\n"
    "\n"
    " Maps geokit's [`Geometry`](../geometry.html) ADT to and from the\n"
    " JSON shapes defined in RFC 7946:\n"
    "\n"
    " - `Point` / `LineString` / `Polygon` / `MultiPolygon` round-trip\n"
    "   through this module.\n"
    " - `MultiPoint`, `MultiLineString`, and `GeometryCollection` are\n"
    "   valid GeoJSON types but are not currently representable in\n"
    "   `Geometry`; decoding returns [`UnsupportedType`](#GeoJsonError).\n"
    "\n"
    " Coordinate order in GeoJSON is `[longitude, latitude]`, opposite\n"
    " of the `lat: ..., lng: ...` constructors elsewhere in geokit.\n"
    " The encoder and decoder handle the swap so callers never see the\n"
    " reversed order. Altitude (a third coordinate) is accepted on\n"
    " decode but discarded.\n"
    "\n"
    " Properties on [`Feature`](#Feature) and `FeatureCollection` are\n"
    " user-typed: pass a `Json` builder when encoding and a\n"
    " `decode.Decoder` when decoding. Use `gleam/dynamic/decode.dynamic`\n"
    " and `gleam/json.null()` if you don't care about properties.\n"
).

-type geo_json_error() :: {invalid_json, binary()} |
    {invalid_structure, binary()} |
    {unknown_type, binary()} |
    {unsupported_type, binary()} |
    {invalid_position, list(float())} |
    {invalid_lat_lng, geokit@latlng:lat_lng_error()}.

-type feature_id() :: {string_id, binary()} | {int_id, integer()}.

-type feature(GZF) :: {feature,
        geokit@geometry:geometry(),
        GZF,
        gleam@option:option(feature_id())}.

-file("src/geokit/geojson.gleam", 167).
-spec position_to_json(geokit@latlng:lat_lng()) -> gleam@json:json().
position_to_json(P) ->
    gleam@json:preprocessed_array(
        [gleam@json:float(geokit@latlng:lng(P)),
            gleam@json:float(geokit@latlng:lat(P))]
    ).

-file("src/geokit/geojson.gleam", 159).
-spec rings_to_json(list(list(geokit@latlng:lat_lng()))) -> gleam@json:json().
rings_to_json(Rings) ->
    gleam@json:preprocessed_array(
        gleam@list:map(
            Rings,
            fun(Ring) ->
                gleam@json:preprocessed_array(
                    gleam@list:map(Ring, fun position_to_json/1)
                )
            end
        )
    ).

-file("src/geokit/geojson.gleam", 175).
-spec object_with_coords(binary(), gleam@json:json()) -> gleam@json:json().
object_with_coords(Type_name, Coordinates) ->
    gleam@json:object(
        [{<<"type"/utf8>>, gleam@json:string(Type_name)},
            {<<"coordinates"/utf8>>, Coordinates}]
    ).

-file("src/geokit/geojson.gleam", 141).
-spec geometry_to_json(geokit@geometry:geometry()) -> gleam@json:json().
geometry_to_json(G) ->
    case G of
        {point, P} ->
            object_with_coords(<<"Point"/utf8>>, position_to_json(P));

        {line_string, Points} ->
            object_with_coords(
                <<"LineString"/utf8>>,
                gleam@json:preprocessed_array(
                    gleam@list:map(Points, fun position_to_json/1)
                )
            );

        {polygon, Rings} ->
            object_with_coords(<<"Polygon"/utf8>>, rings_to_json(Rings));

        {multi_polygon, Polygons} ->
            object_with_coords(
                <<"MultiPolygon"/utf8>>,
                gleam@json:preprocessed_array(
                    gleam@list:map(Polygons, fun rings_to_json/1)
                )
            )
    end.

-file("src/geokit/geojson.gleam", 85).
?DOC(
    " Encode a `Geometry` as a GeoJSON string. The result is a compact\n"
    " JSON document with no whitespace.\n"
    "\n"
    " ```gleam\n"
    " import geokit/geojson\n"
    " import geokit/geometry\n"
    " import geokit/latlng\n"
    "\n"
    " let assert Ok(p) = latlng.new(lat: 35.0, lng: 139.0)\n"
    " geojson.encode_geometry(geometry: geometry.Point(p))\n"
    " // == \"{\\\"type\\\":\\\"Point\\\",\\\"coordinates\\\":[139.0,35.0]}\"\n"
    " ```\n"
).
-spec encode_geometry(geokit@geometry:geometry()) -> binary().
encode_geometry(Geometry) ->
    _pipe = Geometry,
    _pipe@1 = geometry_to_json(_pipe),
    gleam@json:to_string(_pipe@1).

-file("src/geokit/geojson.gleam", 197).
-spec id_to_json(feature_id()) -> gleam@json:json().
id_to_json(Id) ->
    case Id of
        {string_id, Value} ->
            gleam@json:string(Value);

        {int_id, Value@1} ->
            gleam@json:int(Value@1)
    end.

-file("src/geokit/geojson.gleam", 184).
-spec feature_to_json(feature(HAA), fun((HAA) -> gleam@json:json())) -> gleam@json:json().
feature_to_json(Feature, To_json) ->
    Base = [{<<"type"/utf8>>, gleam@json:string(<<"Feature"/utf8>>)},
        {<<"geometry"/utf8>>, geometry_to_json(erlang:element(2, Feature))},
        {<<"properties"/utf8>>, To_json(erlang:element(3, Feature))}],
    Entries = case erlang:element(4, Feature) of
        none ->
            Base;

        {some, Id} ->
            lists:append(Base, [{<<"id"/utf8>>, id_to_json(Id)}])
    end,
    gleam@json:object(Entries).

-file("src/geokit/geojson.gleam", 91).
?DOC(
    " Encode a `Feature` as a GeoJSON string. Pass a function that turns\n"
    " your properties type into a `Json` value.\n"
).
-spec encode_feature(feature(GZG), fun((GZG) -> gleam@json:json())) -> binary().
encode_feature(Feature, To_json) ->
    _pipe = Feature,
    _pipe@1 = feature_to_json(_pipe, To_json),
    gleam@json:to_string(_pipe@1).

-file("src/geokit/geojson.gleam", 99).
?DOC(" Encode a list of features as a GeoJSON `FeatureCollection`.\n").
-spec encode_feature_collection(
    list(feature(GZI)),
    fun((GZI) -> gleam@json:json())
) -> binary().
encode_feature_collection(Features, To_json) ->
    Entries = [{<<"type"/utf8>>,
            gleam@json:string(<<"FeatureCollection"/utf8>>)},
        {<<"features"/utf8>>,
            gleam@json:preprocessed_array(
                gleam@list:map(
                    Features,
                    fun(F) -> feature_to_json(F, To_json) end
                )
            )}],
    _pipe = gleam@json:object(Entries),
    gleam@json:to_string(_pipe).

-file("src/geokit/geojson.gleam", 217).
-spec json_error_to_geojson(gleam@json:decode_error()) -> geo_json_error().
json_error_to_geojson(Err) ->
    case Err of
        unexpected_end_of_input ->
            {invalid_json, <<"unexpected end of input"/utf8>>};

        {unexpected_byte, Byte} ->
            {invalid_json, <<"unexpected byte: "/utf8, Byte/binary>>};

        {unexpected_sequence, Seq} ->
            {invalid_json, <<"unexpected sequence: "/utf8, Seq/binary>>};

        {unable_to_decode, _} ->
            {invalid_structure,
                <<"JSON shape did not match expected GeoJSON"/utf8>>}
    end.

-file("src/geokit/geojson.gleam", 206).
-spec parse_with(
    binary(),
    gleam@dynamic@decode:decoder({ok, HAC} | {error, geo_json_error()})
) -> {ok, HAC} | {error, geo_json_error()}.
parse_with(Input, Decoder) ->
    case gleam@json:parse(Input, Decoder) of
        {error, Err} ->
            {error, json_error_to_geojson(Err)};

        {ok, {ok, Value}} ->
            {ok, Value};

        {ok, {error, Geojson_err}} ->
            {error, Geojson_err}
    end.

-file("src/geokit/geojson.gleam", 301).
-spec wrap_position(float(), float()) -> {ok, geokit@latlng:lat_lng()} |
    {error, geo_json_error()}.
wrap_position(Lng, Lat) ->
    case geokit@latlng:new(Lat, Lng) of
        {ok, P} ->
            {ok, P};

        {error, E} ->
            {error, {invalid_lat_lng, E}}
    end.

-file("src/geokit/geojson.gleam", 293).
-spec parse_position(list(float())) -> {ok, geokit@latlng:lat_lng()} |
    {error, geo_json_error()}.
parse_position(Coords) ->
    case Coords of
        [Lng, Lat] ->
            wrap_position(Lng, Lat);

        [Lng@1, Lat@1, _] ->
            wrap_position(Lng@1, Lat@1);

        _ ->
            {error, {invalid_position, Coords}}
    end.

-file("src/geokit/geojson.gleam", 261).
-spec raw_point_to_geometry(list(float())) -> {ok, geokit@geometry:geometry()} |
    {error, geo_json_error()}.
raw_point_to_geometry(Coords) ->
    gleam@result:map(parse_position(Coords), fun(Point) -> {point, Point} end).

-file("src/geokit/geojson.gleam", 266).
-spec raw_line_string_to_geometry(list(list(float()))) -> {ok,
        geokit@geometry:geometry()} |
    {error, geo_json_error()}.
raw_line_string_to_geometry(Coords) ->
    gleam@result:map(
        gleam@list:try_map(Coords, fun parse_position/1),
        fun(Points) -> {line_string, Points} end
    ).

-file("src/geokit/geojson.gleam", 287).
-spec parse_rings(list(list(list(float())))) -> {ok,
        list(list(geokit@latlng:lat_lng()))} |
    {error, geo_json_error()}.
parse_rings(Raw) ->
    gleam@list:try_map(
        Raw,
        fun(Ring) -> gleam@list:try_map(Ring, fun parse_position/1) end
    ).

-file("src/geokit/geojson.gleam", 273).
-spec raw_polygon_to_geometry(list(list(list(float())))) -> {ok,
        geokit@geometry:geometry()} |
    {error, geo_json_error()}.
raw_polygon_to_geometry(Coords) ->
    gleam@result:map(parse_rings(Coords), fun(Rings) -> {polygon, Rings} end).

-file("src/geokit/geojson.gleam", 280).
-spec raw_multi_polygon_to_geometry(list(list(list(list(float()))))) -> {ok,
        geokit@geometry:geometry()} |
    {error, geo_json_error()}.
raw_multi_polygon_to_geometry(Coords) ->
    gleam@result:map(
        gleam@list:try_map(Coords, fun parse_rings/1),
        fun(Polygons) -> {multi_polygon, Polygons} end
    ).

-file("src/geokit/geojson.gleam", 227).
-spec geometry_decoder() -> gleam@dynamic@decode:decoder({ok,
        geokit@geometry:geometry()} |
    {error, geo_json_error()}).
geometry_decoder() ->
    gleam@dynamic@decode:field(
        <<"type"/utf8>>,
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        fun(Type_) -> case Type_ of
                <<"Point"/utf8>> ->
                    gleam@dynamic@decode:field(
                        <<"coordinates"/utf8>>,
                        gleam@dynamic@decode:list(
                            {decoder, fun gleam@dynamic@decode:decode_float/1}
                        ),
                        fun(Coords) ->
                            gleam@dynamic@decode:success(
                                raw_point_to_geometry(Coords)
                            )
                        end
                    );

                <<"LineString"/utf8>> ->
                    gleam@dynamic@decode:field(
                        <<"coordinates"/utf8>>,
                        gleam@dynamic@decode:list(
                            gleam@dynamic@decode:list(
                                {decoder,
                                    fun gleam@dynamic@decode:decode_float/1}
                            )
                        ),
                        fun(Coords@1) ->
                            gleam@dynamic@decode:success(
                                raw_line_string_to_geometry(Coords@1)
                            )
                        end
                    );

                <<"Polygon"/utf8>> ->
                    gleam@dynamic@decode:field(
                        <<"coordinates"/utf8>>,
                        gleam@dynamic@decode:list(
                            gleam@dynamic@decode:list(
                                gleam@dynamic@decode:list(
                                    {decoder,
                                        fun gleam@dynamic@decode:decode_float/1}
                                )
                            )
                        ),
                        fun(Coords@2) ->
                            gleam@dynamic@decode:success(
                                raw_polygon_to_geometry(Coords@2)
                            )
                        end
                    );

                <<"MultiPolygon"/utf8>> ->
                    gleam@dynamic@decode:field(
                        <<"coordinates"/utf8>>,
                        gleam@dynamic@decode:list(
                            gleam@dynamic@decode:list(
                                gleam@dynamic@decode:list(
                                    gleam@dynamic@decode:list(
                                        {decoder,
                                            fun gleam@dynamic@decode:decode_float/1}
                                    )
                                )
                            )
                        ),
                        fun(Coords@3) ->
                            gleam@dynamic@decode:success(
                                raw_multi_polygon_to_geometry(Coords@3)
                            )
                        end
                    );

                <<"MultiPoint"/utf8>> ->
                    gleam@dynamic@decode:success(
                        {error, {unsupported_type, Type_}}
                    );

                <<"MultiLineString"/utf8>> ->
                    gleam@dynamic@decode:success(
                        {error, {unsupported_type, Type_}}
                    );

                <<"GeometryCollection"/utf8>> ->
                    gleam@dynamic@decode:success(
                        {error, {unsupported_type, Type_}}
                    );

                Other ->
                    gleam@dynamic@decode:success({error, {unknown_type, Other}})
            end end
    ).

-file("src/geokit/geojson.gleam", 118).
?DOC(" Decode a GeoJSON geometry string back to a `Geometry`.\n").
-spec decode_geometry(binary()) -> {ok, geokit@geometry:geometry()} |
    {error, geo_json_error()}.
decode_geometry(Input) ->
    parse_with(Input, geometry_decoder()).

-file("src/geokit/geojson.gleam", 332).
-spec id_decoder() -> gleam@dynamic@decode:decoder(feature_id()).
id_decoder() ->
    gleam@dynamic@decode:one_of(
        gleam@dynamic@decode:map(
            {decoder, fun gleam@dynamic@decode:decode_string/1},
            fun(Field@0) -> {string_id, Field@0} end
        ),
        [gleam@dynamic@decode:map(
                {decoder, fun gleam@dynamic@decode:decode_int/1},
                fun(Field@0) -> {int_id, Field@0} end
            )]
    ).

-file("src/geokit/geojson.gleam", 310).
-spec feature_decoder(gleam@dynamic@decode:decoder(HBP)) -> gleam@dynamic@decode:decoder({ok,
        feature(HBP)} |
    {error, geo_json_error()}).
feature_decoder(Properties_decoder) ->
    gleam@dynamic@decode:field(
        <<"type"/utf8>>,
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        fun(Type_) -> case Type_ of
                <<"Feature"/utf8>> ->
                    gleam@dynamic@decode:field(
                        <<"geometry"/utf8>>,
                        geometry_decoder(),
                        fun(Raw_geometry) ->
                            gleam@dynamic@decode:field(
                                <<"properties"/utf8>>,
                                Properties_decoder,
                                fun(Props) ->
                                    gleam@dynamic@decode:optional_field(
                                        <<"id"/utf8>>,
                                        none,
                                        gleam@dynamic@decode:optional(
                                            id_decoder()
                                        ),
                                        fun(Id) ->
                                            gleam@dynamic@decode:success(
                                                gleam@result:map(
                                                    Raw_geometry,
                                                    fun(G) ->
                                                        {feature, G, Props, Id}
                                                    end
                                                )
                                            )
                                        end
                                    )
                                end
                            )
                        end
                    );

                Other ->
                    gleam@dynamic@decode:then(
                        gleam@dynamic@decode:success(nil),
                        fun(_) ->
                            gleam@dynamic@decode:success(
                                {error, {unknown_type, Other}}
                            )
                        end
                    )
            end end
    ).

-file("src/geokit/geojson.gleam", 124).
?DOC(
    " Decode a GeoJSON Feature. Pass a decoder for your properties type.\n"
    " To accept any shape, pass `decode.dynamic`.\n"
).
-spec decode_feature(binary(), gleam@dynamic@decode:decoder(GZN)) -> {ok,
        feature(GZN)} |
    {error, geo_json_error()}.
decode_feature(Input, Properties) ->
    parse_with(Input, feature_decoder(Properties)).

-file("src/geokit/geojson.gleam", 338).
-spec feature_collection_decoder(gleam@dynamic@decode:decoder(HBW)) -> gleam@dynamic@decode:decoder({ok,
        list(feature(HBW))} |
    {error, geo_json_error()}).
feature_collection_decoder(Properties_decoder) ->
    gleam@dynamic@decode:field(
        <<"type"/utf8>>,
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        fun(Type_) -> case Type_ of
                <<"FeatureCollection"/utf8>> ->
                    gleam@dynamic@decode:field(
                        <<"features"/utf8>>,
                        gleam@dynamic@decode:list(
                            feature_decoder(Properties_decoder)
                        ),
                        fun(Features) ->
                            gleam@dynamic@decode:success(
                                gleam@result:all(Features)
                            )
                        end
                    );

                Other ->
                    gleam@dynamic@decode:then(
                        gleam@dynamic@decode:success(nil),
                        fun(_) ->
                            gleam@dynamic@decode:success(
                                {error, {unknown_type, Other}}
                            )
                        end
                    )
            end end
    ).

-file("src/geokit/geojson.gleam", 132).
?DOC(" Decode a GeoJSON FeatureCollection into a list of features.\n").
-spec decode_feature_collection(binary(), gleam@dynamic@decode:decoder(GZS)) -> {ok,
        list(feature(GZS))} |
    {error, geo_json_error()}.
decode_feature_collection(Input, Properties) ->
    parse_with(Input, feature_collection_decoder(Properties)).