changed CHANGELOG.md
 
@@ -1,5 +1,72 @@
1
1
# Changelog
2
2
3
+ ## v4.0.0 — 2024-09-17
4
+
5
+ ### Potentially breaking change: [Default decoded GeoJSON to SRID 4326 (WGS 84)](https://github.com/felt/geo/pull/219)
6
+
7
+ This aligns our GeoJSON decoding with [the GeoJSON spec](https://tools.ietf.org/html/rfc7946#section-4) by making all decoded GeoJSON infer the WGS 84 datum (SRID 4326) by default. Whereas previously when you called `Geo.JSON.decode/1` or `decode!/1`, we would return geometries with an `:srid` of `nil`, we now return `srid: 4326`. Likewise when encoding GeoJSON, we explicitly output a `crs` field indicating the datum.
8
+
9
+ This is unlikely to break real-world usage unless your implementation was assuming a different datum by default.
10
+
11
+ A couple examples of the changes:
12
+
13
+ **Before**:
14
+
15
+ ```elixir
16
+ iex> Geo.JSON.decode!(%{"type" => "Point", "coordinates" => [1.0, 2.0]})
17
+ %Geo.Point{
18
+ coordinates: {1.0, 2.0},
19
+ # Note the old default nil SRID!
20
+ srid: nil
21
+ }
22
+ ```
23
+
24
+ **After**
25
+
26
+ ```elixir
27
+ iex> Geo.JSON.decode!(%{"type" => "Point", "coordinates" => [1.0, 2.0]})
28
+ %Geo.Point{
29
+ coordinates: {1.0, 2.0},
30
+ # New explicit default of WGS 84
31
+ srid: 4326
32
+ }
33
+ ```
34
+
35
+ If you were to then encode this value again, you'd end up with a new `crs` field in the output GeoJSON:
36
+
37
+ ```elixir
38
+ iex> %{"type" => "Point", "coordinates" => [1.0, 2.0]}
39
+ ...> |> Geo.JSON.decode!()
40
+ ...> |> GeoJSON.encode!()
41
+ %{
42
+ "type" => "Point",
43
+ "coordinates" => [1.0, 2.0],
44
+ # Note the new `crs` field which was not present in the input to Geo.JSON.decode!/1
45
+ "crs" => %{"properties" => %{"name" => "EPSG:4326"}, "type" => "name"}
46
+ }
47
+ ```
48
+
49
+ This last behavior is the most potentially troublesome. However, we don't have a good way of distinguishing a case where you explicitly had the `crs` set in the input to the decoding function (in which case you would probably also like to have it present in the re-encoded version) compared to one in which it's been inferred.
50
+
51
+ Thanks to @gworkman for reporting this issue ([#129](https://github.com/felt/geo/issues/129)).
52
+
53
+ ### Potentially breaking change: [Convert string coordinates to floats, or raise an error](https://github.com/felt/geo/pull/218)
54
+
55
+ This fixes an issue where we were silently accepting non-numeric coordinates in the GeoJSON decoder, such that you could wind up doing things like decoding a point like `%Geo.Point{coordinates: {"100.0", "-10.0"}}`. This would obviously not have gone well for you later in your processing pipeline, and it violates our typespecs.
56
+
57
+ The fix here, suggested by @LostKobrakai, is to convert those strings to numbers where we can do so unambiguously. While such inputs are clearly invalid, it's easy enough to handle them in the way that the user was hoping that we should probably just do it. In cases where there's any ambiguity at all, we raise an `ArgumentError`.
58
+
59
+ ### Other bug fixes in v4.0.0
60
+
61
+ - [Support GeoJSON Feature object with nested GeometryCollection](https://github.com/felt/geo/pull/194) by new contributor @carstenpiepel (🎉)
62
+
63
+ ### Other changes in v4.0.0
64
+
65
+ - [Fix typo in the README](https://github.com/felt/geo/pull/197) by @caspg
66
+ - [Fix typo](https://github.com/felt/geo/pull/216) by new contributor @preciz (🎉)
67
+ - [Optional dependency bump for `jason` to v1.4.4](https://github.com/felt/geo/pull/215)
68
+ - Dev dependency bumps for ex_doc, benchee, stream_data
69
+
3
70
## v3.6.0 — 2023-10-19
4
71
5
72
As of v3.6.0, `geo` (like [`geo_postgis`](https://github.com/felt/geo_postgis)) is being maintained by the Felt team. As a company building a geospatial product on Elixir, with a track record of [supporting open source software](https://felt.com/open-source), we're excited for the future of the project.
changed README.md
 
@@ -20,8 +20,8 @@ A collection of GIS functions. Handles conversions to and from well-known text (
20
20
* PolygonZ
21
21
* MultiPoint
22
22
* MultiPointZ
23
- * MuliLineString
24
- * MuliLineStringZ
23
+ * MultiLineString
24
+ * MultiLineStringZ
25
25
* MultiPolygon
26
26
* MultiPolygonZ
27
27
* GeometryCollection
 
@@ -33,7 +33,7 @@ _Note_: If you are looking to do geospatial calculations in memory with Geo's st
33
33
```elixir
34
34
defp deps do
35
35
[
36
- {:geo, "~> 3.6"}
36
+ {:geo, "~> 4.0"}
37
37
]
38
38
end
39
39
```
changed hex_metadata.config
 
@@ -1,6 +1,6 @@
1
1
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/felt/geo">>}]}.
2
2
{<<"name">>,<<"geo">>}.
3
- {<<"version">>,<<"3.6.0">>}.
3
+ {<<"version">>,<<"4.0.0">>}.
4
4
{<<"description">>,<<"Encodes and decodes WKB, WKT, and GeoJSON formats.">>}.
5
5
{<<"elixir">>,<<"~> 1.10">>}.
6
6
{<<"app">>,<<"geo">>}.
changed lib/geo/json.ex
 
@@ -7,13 +7,16 @@ defmodule Geo.JSON do
7
7
so that you can use the resulting GeoJSON structure as a property
8
8
in larger JSON structures.
9
9
10
+ Note that, per [the GeoJSON spec](https://tools.ietf.org/html/rfc7946#section-4),
11
+ all geometries are assumed to use the WGS 84 datum (SRID 4326) by default.
12
+
10
13
## Examples
11
14
12
15
# Using Jason as the JSON parser for these examples
13
16
14
17
iex> json = "{ \\"type\\": \\"Point\\", \\"coordinates\\": [100.0, 0.0] }"
15
18
...> json |> Jason.decode!() |> Geo.JSON.decode!()
16
- %Geo.Point{coordinates: {100.0, 0.0}, srid: nil}
19
+ %Geo.Point{coordinates: {100.0, 0.0}, srid: 4326}
17
20
18
21
iex> geom = %Geo.Point{coordinates: {100.0, 0.0}, srid: nil}
19
22
...> Jason.encode!(geom)
changed lib/geo/json/decoder.ex
 
@@ -46,7 +46,7 @@ defmodule Geo.JSON.Decoder do
46
46
Enum.map(Map.get(geo_json, "geometries"), fn x ->
47
47
do_decode(
48
48
Map.get(x, "type"),
49
- Map.get(x, "coordinates"),
49
+ ensure_numeric(x["coordinates"]),
50
50
Map.get(x, "properties", %{}),
51
51
crs
52
52
)
 
@@ -62,7 +62,7 @@ defmodule Geo.JSON.Decoder do
62
62
63
63
do_decode(
64
64
Map.get(geo_json, "type"),
65
- Map.get(geo_json, "coordinates"),
65
+ ensure_numeric(geo_json["coordinates"]),
66
66
Map.get(geo_json, "properties", %{}),
67
67
crs
68
68
)
 
@@ -96,6 +96,9 @@ defmodule Geo.JSON.Decoder do
96
96
true ->
97
97
raise DecodeError, value: geo_json
98
98
end
99
+ # Per #129, the GeoJSON spec says all GeoJSON coordinates default to SRID 4326 (WGS 84)
100
+ # https://tools.ietf.org/html/rfc7946#section-4
101
+ |> default_srid_4326()
99
102
end
100
103
101
104
@doc """
 
@@ -197,7 +200,21 @@ defmodule Geo.JSON.Decoder do
197
200
defp do_decode("Feature", nil, _properties, _id), do: nil
198
201
199
202
defp do_decode("Feature", geometry, properties, _id) do
200
- do_decode(Map.get(geometry, "type"), Map.get(geometry, "coordinates"), properties, nil)
203
+ if geometry["type"] == "GeometryCollection" do
204
+ geometry_collection = decode!(geometry)
205
+
206
+ %GeometryCollection{
207
+ geometries: geometry_collection.geometries,
208
+ properties: properties
209
+ }
210
+ else
211
+ do_decode(
212
+ Map.get(geometry, "type"),
213
+ ensure_numeric(geometry["coordinates"]),
214
+ properties,
215
+ nil
216
+ )
217
+ end
201
218
end
202
219
203
220
defp do_decode(type, [x, y, _z], properties, crs) do
 
@@ -222,4 +239,55 @@ defmodule Geo.JSON.Decoder do
222
239
defp get_srid(nil) do
223
240
nil
224
241
end
242
+
243
+ # Fast paths for the common (correct) cases
244
+ defp ensure_numeric(num) when is_number(num), do: num
245
+ defp ensure_numeric([x, y] = l) when is_number(x) and is_number(y), do: l
246
+
247
+ defp ensure_numeric([x, y, z] = l)
248
+ when is_number(x) and is_number(y) and (is_number(z) or is_nil(z)) do
249
+ l
250
+ end
251
+
252
+ defp ensure_numeric([x, y, z, m] = l)
253
+ when is_number(x) and is_number(y) and (is_number(z) or is_nil(z)) and
254
+ (is_number(m) or is_nil(z)) do
255
+ l
256
+ end
257
+
258
+ defp ensure_numeric(l) when is_list(l) do
259
+ Enum.map(l, fn
260
+ num when is_number(num) ->
261
+ num
262
+
263
+ str when is_binary(str) ->
264
+ try do
265
+ String.to_float(str)
266
+ catch
267
+ ArgumentError ->
268
+ raise ArgumentError, "expected a numeric coordinate, got the string #{inspect(str)}"
269
+ end
270
+
271
+ nil ->
272
+ nil
273
+
274
+ l when is_list(l) ->
275
+ Enum.map(l, &ensure_numeric/1)
276
+
277
+ other ->
278
+ raise ArgumentError, "expected a numeric coordinate, got: #{inspect(other)}"
279
+ end)
280
+ end
281
+
282
+ defp ensure_numeric(other) do
283
+ raise ArgumentError, "expected a numeric coordinate, got: #{inspect(other)}"
284
+ end
285
+
286
+ defp default_srid_4326(%{srid: nil} = geom), do: %{geom | srid: 4326}
287
+
288
+ defp default_srid_4326(%{geometries: geometries} = geom) when is_list(geometries) do
289
+ %{geom | geometries: Enum.map(geometries, &default_srid_4326/1)}
290
+ end
291
+
292
+ defp default_srid_4326(geom), do: geom
225
293
end
changed lib/geo/polygonz.ex
 
@@ -1,6 +1,6 @@
1
1
defmodule Geo.PolygonZ do
2
2
@moduledoc """
3
- Defines the Polygon struct.
3
+ Defines the PolygonZ struct.
4
4
"""
5
5
6
6
@type t :: %__MODULE__{
changed mix.exs
 
@@ -2,7 +2,7 @@ defmodule Geo.Mixfile do
2
2
use Mix.Project
3
3
4
4
@source_url "https://github.com/felt/geo"
5
- @version "3.6.0"
5
+ @version "4.0.0"
6
6
7
7
def project do
8
8
[
 
@@ -33,7 +33,7 @@ defmodule Geo.Mixfile do
33
33
[
34
34
{:jason, "~> 1.4", optional: true},
35
35
{:ex_doc, "~> 0.29", only: :dev, runtime: false},
36
- {:stream_data, "~> 0.5", only: :test, runtime: false},
36
+ {:stream_data, "~> 0.5 or ~> 1.0", only: :test, runtime: false},
37
37
{:benchee, "~> 1.1", only: :dev, runtime: false}
38
38
]
39
39
end