Skip to content

Commit 13739e5

Browse files
committed
Change for a more strict geojson handling.
- Do not handle m coordinate. - Raise for unknown geojson `type`. - Allow less strict polygons (not simple). Which is ok per spec. - Add various tests.
1 parent 7316c96 commit 13739e5

8 files changed

+276
-51
lines changed

lib/rgeo-geojson.rb

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# frozen_string_literal: true
22

3-
require "rgeo/geo_json"
3+
# Helper for bundler's `require: true` option.
4+
# See {file:lib/rgeo/geo_json/interface.rb} for documentation entry point.
5+
6+
require_relative "rgeo/geo_json"

lib/rgeo/geo_json.rb

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
# frozen_string_literal: true
22

3-
require "rgeo"
4-
require "rgeo/geo_json/version"
5-
require "rgeo/geo_json/entities"
6-
require "rgeo/geo_json/coder"
7-
require "rgeo/geo_json/interface"
83
require "multi_json"
4+
require "rgeo"
5+
6+
module RGeo
7+
module GeoJSON
8+
class Error < RGeo::Error::RGeoError
9+
end
10+
end
11+
end
12+
13+
require_relative "geo_json/version"
14+
require_relative "geo_json/entities"
15+
require_relative "geo_json/coder"
16+
require_relative "geo_json/interface"

lib/rgeo/geo_json/coder.rb

+47-35
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ module GeoJSON
66
# the RGeo::Feature::Factory and the RGeo::GeoJSON::EntityFactory to
77
# be used) so that you can encode and decode without specifying those
88
# settings every time.
9-
109
class Coder
10+
class Error < RGeo::GeoJSON::Error
11+
end
12+
1113
# Create a new coder settings object. The geo factory is passed as
1214
# a required argument.
1315
#
@@ -23,11 +25,37 @@ class Coder
2325
# RGeo::GeoJSON::Feature or RGeo::GeoJSON::FeatureCollection.
2426
# See RGeo::GeoJSON::EntityFactory for more information.
2527
def initialize(opts = {})
26-
@geo_factory = opts[:geo_factory] || RGeo::Cartesian.preferred_factory
27-
@entity_factory = opts[:entity_factory] || EntityFactory.instance
28+
@geo_factory = opts.fetch(
29+
:geo_factory,
30+
RGeo::Cartesian.preferred_factory(uses_lenient_assertions: true)
31+
)
32+
@entity_factory = opts.fetch(:entity_factory, EntityFactory.instance)
33+
if @geo_factory.property(:has_m_coordinate)
34+
# If a GeoJSON has more than 2 elements, the first one should be
35+
# longitude and the second one latitude. M is not part of GeoJSON
36+
# specifications and only kept here for backward compatibilities.
37+
#
38+
# Quote from https://tools.ietf.org/html/rfc7946#section-3.1.1:
39+
#
40+
# > A position is an array of numbers. There MUST be two or more
41+
# > elements. The first two elements are longitude and latitude, or
42+
# > easting and northing, precisely in that order and using decimal
43+
# > numbers. Altitude or elevation MAY be included as an optional third
44+
# > element.
45+
# >
46+
# > Implementations SHOULD NOT extend positions beyond three elements
47+
# > because the semantics of extra elements are unspecified and
48+
# > ambiguous. Historically, some implementations have used a fourth
49+
# > element to carry a linear referencing measure (sometimes denoted as
50+
# > "M") or a numerical timestamp, but in most situations a parser will
51+
# > not be able to properly interpret these values. The interpretation
52+
# > and meaning of additional elements is beyond the scope of this
53+
# > specification, and additional elements MAY be ignored by parsers.
54+
raise Error, "GeoJSON format cannot handle m coordinate."
55+
end
56+
2857
@num_coordinates = 2
2958
@num_coordinates += 1 if @geo_factory.property(:has_z_coordinate)
30-
@num_coordinates += 1 if @geo_factory.property(:has_m_coordinate)
3159
end
3260

3361
# Encode the given object as GeoJSON. The object may be one of the
@@ -41,17 +69,16 @@ def initialize(opts = {})
4169
# appropriate JSON library installed.
4270
#
4371
# Returns nil if nil is passed in as the object.
44-
4572
def encode(object)
73+
return nil if object.nil?
74+
4675
if @entity_factory.is_feature_collection?(object)
4776
{
4877
"type" => "FeatureCollection",
4978
"features" => @entity_factory.map_feature_collection(object) { |f| encode_feature(f) },
5079
}
5180
elsif @entity_factory.is_feature?(object)
5281
encode_feature(object)
53-
elsif object.nil?
54-
nil
5582
else
5683
encode_geometry(object)
5784
end
@@ -111,35 +138,20 @@ def encode_feature(object)
111138
end
112139

113140
def encode_geometry(object)
141+
return nil if object.nil?
142+
if object.factory.property(:has_m_coordinate)
143+
raise Error, "GeoJSON format cannot handle m coordinate."
144+
end
145+
114146
case object
115-
when RGeo::Feature::Point
116-
{
117-
"type" => "Point",
118-
"coordinates" => object.coordinates
119-
}
120-
when RGeo::Feature::LineString
147+
when RGeo::Feature::Point,
148+
RGeo::Feature::LineString,
149+
RGeo::Feature::Polygon,
150+
RGeo::Feature::MultiPoint,
151+
RGeo::Feature::MultiLineString,
152+
RGeo::Feature::MultiPolygon
121153
{
122-
"type" => "LineString",
123-
"coordinates" => object.coordinates
124-
}
125-
when RGeo::Feature::Polygon
126-
{
127-
"type" => "Polygon",
128-
"coordinates" => object.coordinates
129-
}
130-
when RGeo::Feature::MultiPoint
131-
{
132-
"type" => "MultiPoint",
133-
"coordinates" => object.coordinates
134-
}
135-
when RGeo::Feature::MultiLineString
136-
{
137-
"type" => "MultiLineString",
138-
"coordinates" => object.coordinates
139-
}
140-
when RGeo::Feature::MultiPolygon
141-
{
142-
"type" => "MultiPolygon",
154+
"type" => object.geometry_type.type_name,
143155
"coordinates" => object.coordinates
144156
}
145157
when RGeo::Feature::GeometryCollection
@@ -178,7 +190,7 @@ def decode_geometry(input)
178190
when "MultiPolygon"
179191
decode_multi_polygon_coords(input["coordinates"])
180192
else
181-
nil
193+
raise Error, "'#{input['type']}' type is not part of GeoJSON spec."
182194
end
183195
end
184196

lib/rgeo/geo_json/entities.rb

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ module GeoJSON
1111
# implementation need not subclass or even duck-type this class.
1212
# the entity factory mediates all interaction between the GeoJSON
1313
# engine and features.
14-
1514
class Feature
1615
# Create a feature wrapping the given geometry, with the given ID
1716
# and properties.

lib/rgeo/geo_json/interface.rb

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
# frozen_string_literal: true
22

33
module RGeo
4+
# `RGeo::GeoJSON` is a part of `RGeo` designed to decode GeoJSON into
5+
# `RGeo::Feature::Geometry`, or encode `RGeo::Feature::Geometry` objects as
6+
# GeoJSON.
7+
#
8+
# This implementation tries to stick to GeoJSON specifications, and may raise
9+
# when trying to decode and invalid GeoJSON string. It may also raise if one
10+
# tries to encode a feature that cannot be handled per GeoJSON spec.
11+
#
12+
# @example Basic usage
13+
# require 'rgeo/geo_json'
14+
#
15+
# str1 = '{"type":"Point","coordinates":[1,2]}'
16+
# geom = RGeo::GeoJSON.decode(str1)
17+
# geom.as_text # => "POINT (1.0 2.0)"
18+
#
19+
# str2 = '{"type":"Feature","geometry":{"type":"Point","coordinates":[2.5,4.0]},"properties":{"color":"red"}}'
20+
# feature = RGeo::GeoJSON.decode(str2)
21+
# feature['color'] # => 'red'
22+
# feature.geometry.as_text # => "POINT (2.5 4.0)"
23+
#
24+
# hash = RGeo::GeoJSON.encode(feature)
25+
# hash.to_json == str2 # => true
26+
#
27+
# @see https://tools.ietf.org/html/rfc7946
428
module GeoJSON
529
class << self
630
# High-level convenience routine for encoding an object as GeoJSON.
@@ -13,7 +37,6 @@ class << self
1337
# RGeo::GeoJSON::EntityFactory for more information. By default,
1438
# encode supports objects of type RGeo::GeoJSON::Feature and
1539
# RGeo::GeoJSON::FeatureCollection.
16-
1740
def encode(object, opts = {})
1841
Coder.new(opts).encode(object)
1942
end

test/basic_test.rb

+23-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# frozen_string_literal: true
22

3-
require "minitest/autorun"
4-
require "rgeo/geo_json"
3+
require_relative "test_helper"
54

65
class BasicTest < Minitest::Test # :nodoc:
76
def setup
8-
@geo_factory = RGeo::Cartesian.simple_factory(srid: 4326)
7+
@geo_factory = RGeo::GeoJSON.coder.instance_variable_get(:@geo_factory)
98
@geo_factory_z = RGeo::Cartesian.simple_factory(srid: 4326, has_z_coordinate: true)
109
@geo_factory_m = RGeo::Cartesian.simple_factory(srid: 4326, has_m_coordinate: true)
1110
@geo_factory_zm = RGeo::Cartesian.simple_factory(srid: 4326, has_z_coordinate: true, has_m_coordinate: true)
@@ -22,7 +21,7 @@ def test_nil
2221
end
2322

2423
def test_decode_simple_point
25-
json = %({"type":"Point","coordinates":[1,2]})
24+
json = '{"type":"Point","coordinates":[1,2]}'
2625
point = RGeo::GeoJSON.decode(json)
2726
assert_equal "POINT (1.0 2.0)", point.as_text
2827
end
@@ -60,8 +59,8 @@ def test_point_m
6059
"type" => "Point",
6160
"coordinates" => [10.0, 20.0, -1.0],
6261
}
63-
assert_equal(json, RGeo::GeoJSON.encode(object))
64-
assert(RGeo::GeoJSON.decode(json, geo_factory: @geo_factory_m).eql?(object))
62+
assert_raises(RGeo::GeoJSON::Coder::Error) { RGeo::GeoJSON.encode(object) }
63+
assert_raises(RGeo::GeoJSON::Coder::Error) { RGeo::GeoJSON.decode(json, geo_factory: @geo_factory_m) }
6564
end
6665

6766
def test_point_zm
@@ -70,8 +69,8 @@ def test_point_zm
7069
"type" => "Point",
7170
"coordinates" => [10.0, 20.0, -1.0, -2.0],
7271
}
73-
assert_equal(json, RGeo::GeoJSON.encode(object))
74-
assert(RGeo::GeoJSON.decode(json, geo_factory: @geo_factory_zm).eql?(object))
72+
assert_raises(RGeo::GeoJSON::Coder::Error) { RGeo::GeoJSON.encode(object) }
73+
assert_raises(RGeo::GeoJSON::Coder::Error) { RGeo::GeoJSON.decode(json, geo_factory: @geo_factory_zm) }
7574
end
7675

7776
def test_line_string
@@ -94,6 +93,22 @@ def test_polygon
9493
assert(RGeo::GeoJSON.decode(json, geo_factory: @geo_factory).eql?(object))
9594
end
9695

96+
def test_not_simple_polygon
97+
coordinates = [[0, 0], [2, 2], [2, 0], [0, 2], [0, 0]]
98+
object = @geo_factory.polygon(
99+
@geo_factory.line_string(coordinates.map { |x, y| @geo_factory.point(x, y) })
100+
)
101+
json = {
102+
"type" => "Polygon",
103+
"coordinates" => [coordinates]
104+
}
105+
assert_equal(json, RGeo::GeoJSON.encode(object))
106+
assert(
107+
RGeo::GeoJSON.decode(json).eql?(object),
108+
"It should decodes with the uses_lenient_assertions param"
109+
)
110+
end
111+
97112
def test_polygon_complex
98113
object = @geo_factory.polygon(@geo_factory.linear_ring([@geo_factory.point(0, 0), @geo_factory.point(10, 0), @geo_factory.point(10, 10), @geo_factory.point(0, 10), @geo_factory.point(0, 0)]), [@geo_factory.linear_ring([@geo_factory.point(4, 4), @geo_factory.point(6, 5), @geo_factory.point(4, 6), @geo_factory.point(4, 4)])])
99114
json = {

0 commit comments

Comments
 (0)