tg_geometry 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +107 -14
  4. data/benchmark/ewkb_roundtrip.rb +29 -0
  5. data/benchmark/geom_query.rb +33 -0
  6. data/benchmark/nearest_segment.rb +20 -0
  7. data/docs/GEOMETRY_QUERIES.md +23 -0
  8. data/docs/LIMITATIONS.md +12 -10
  9. data/docs/NEAREST_SEGMENT.md +17 -0
  10. data/docs/SRID_AND_EWKB.md +23 -0
  11. data/ext/tg_geometry/tg_geometry_ext.c +1176 -4
  12. data/lib/tg/geometry/active_record_source.rb +16 -34
  13. data/lib/tg/geometry/active_record_type.rb +61 -0
  14. data/lib/tg/geometry/registry.rb +17 -68
  15. data/lib/tg/geometry/version.rb +1 -1
  16. data/lib/tg/geometry.rb +85 -0
  17. data/spec/active_record_type_spec.rb +45 -0
  18. data/spec/constructors_spec.rb +104 -0
  19. data/spec/fixtures/feature_source/invalid_geometry_middle.geojson +8 -0
  20. data/spec/fixtures/feature_source/malformed_json.geojson +1 -0
  21. data/spec/fixtures/feature_source/mixed_geometry_types.geojson +8 -0
  22. data/spec/fixtures/feature_source/osm_like_feature_collection.geojson +10 -0
  23. data/spec/fixtures/feature_source/properties_null_missing.geojson +7 -0
  24. data/spec/fixtures/feature_source/simple_feature_collection.geojson +15 -0
  25. data/spec/fixtures/postgis/README.md +16 -0
  26. data/spec/fixtures/postgis/boundary_point_cases.geojson +83 -0
  27. data/spec/fixtures/postgis/multipolygon_large.ewkb +0 -0
  28. data/spec/fixtures/postgis/point_4326.ewkb +0 -0
  29. data/spec/fixtures/postgis/polygon_3857.ewkb +0 -0
  30. data/spec/fixtures/postgis/polygon_4326_simple.ewkb +0 -0
  31. data/spec/fixtures/postgis/polygon_4326_with_hole.ewkb +0 -0
  32. data/spec/index_geom_query_spec.rb +68 -0
  33. data/spec/keyword_validation_spec.rb +31 -0
  34. data/spec/nearest_segment_spec.rb +62 -0
  35. data/spec/postgis_fixtures_spec.rb +68 -0
  36. data/spec/srid_spec.rb +43 -0
  37. data/spec/to_ewkb_spec.rb +37 -0
  38. metadata +50 -2
@@ -6,52 +6,34 @@ module TG
6
6
  module_function
7
7
 
8
8
  def call(scope, id:, geometry:, batch_size: 1_000)
9
- entries = []
10
-
11
- each_record(scope, batch_size: batch_size) do |record|
12
- entries << [read_field(record, id), read_field(record, geometry)]
9
+ enumerator(scope, batch_size: batch_size).map do |record|
10
+ [read_field(record, id), read_field(record, geometry)]
13
11
  end
14
-
15
- entries
16
12
  end
17
13
 
18
14
  def registry_source(scope, id:, geometry:, batch_size: 1_000)
19
- proc do
20
- TG::Geometry::ActiveRecordSource.call(
21
- scope,
22
- id: id,
23
- geometry: geometry,
24
- batch_size: batch_size
25
- )
26
- end
15
+ -> { TG::Geometry::ActiveRecordSource.call(scope, id: id, geometry: geometry, batch_size: batch_size) }
27
16
  end
28
17
 
29
- def each_record(scope, batch_size:)
30
- if scope.respond_to?(:find_each)
31
- scope.find_each(batch_size: batch_size) { |record| yield record }
32
- else
33
- scope.each { |record| yield record }
18
+ class << self
19
+ private
20
+
21
+ def enumerator(scope, batch_size:)
22
+ scope.respond_to?(:find_each) ? scope.find_each(batch_size: batch_size) : scope.each
34
23
  end
35
- end
36
- private_class_method :each_record
37
24
 
38
- def read_field(record, reader)
39
- case reader
40
- when Proc
41
- reader.call(record)
42
- when Symbol, String
43
- if record.respond_to?(reader)
44
- record.public_send(reader)
45
- elsif record.respond_to?(:[])
46
- record[reader]
47
- else
25
+ def read_field(record, reader)
26
+ case reader
27
+ in Proc then reader.call(record)
28
+ in Symbol | String if record.respond_to?(reader) then record.public_send(reader)
29
+ in Symbol | String if record.respond_to?(:[]) then record[reader]
30
+ in Symbol | String
48
31
  raise TG::Geometry::ArgumentError, "record does not expose #{reader.inspect}"
32
+ else
33
+ raise TG::Geometry::ArgumentError, "field reader must be Symbol, String, or Proc"
49
34
  end
50
- else
51
- raise TG::Geometry::ArgumentError, "field reader must be Symbol, String, or Proc"
52
35
  end
53
36
  end
54
- private_class_method :read_field
55
37
  end
56
38
  end
57
39
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "tg/geometry"
5
+
6
+ module TG
7
+ module Geometry
8
+ class ActiveRecordType < ActiveModel::Type::Value
9
+ HEX_PATTERN = /\A[0-9A-Fa-f]+\z/
10
+
11
+ def initialize(strict: true)
12
+ super()
13
+ @strict = strict
14
+ end
15
+
16
+ def type = :tg_geometry
17
+
18
+ def deserialize(value)
19
+ case value
20
+ when nil then nil
21
+ when TG::Geometry::Geom then value
22
+ when String then parse_string(value)
23
+ else
24
+ @strict ? raise(TG::Geometry::ArgumentError, "cannot deserialize #{value.class} as TG::Geometry::Geom") : nil
25
+ end
26
+ end
27
+
28
+ alias cast deserialize
29
+
30
+ def serialize(value)
31
+ case value
32
+ when nil, String then value
33
+ else raise TG::Geometry::ArgumentError, "TG::Geometry::ActiveRecordType is read-only in v0.3.0"
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def parse_string(str)
40
+ stripped = str.strip
41
+
42
+ if stripped.start_with?("\\x") && stripped[2..].match?(HEX_PATTERN)
43
+ TG::Geometry.parse_hex(stripped[2..])
44
+ elsif stripped.match?(HEX_PATTERN)
45
+ TG::Geometry.parse_hex(stripped)
46
+ elsif str.encoding == Encoding::ASCII_8BIT
47
+ TG::Geometry.parse_wkb(str)
48
+ elsif stripped.start_with?("{")
49
+ TG::Geometry.parse_geojson(str)
50
+ else
51
+ TG::Geometry.parse_wkt(str)
52
+ end
53
+ rescue TG::Geometry::ParseError => e
54
+ raise if @strict
55
+
56
+ warn "TG::Geometry::ActiveRecordType: parse failed: #{e.message}"
57
+ nil
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module TG
4
6
  module Geometry
5
7
  class Registry
8
+ extend Forwardable
9
+
6
10
  DEFAULT_INDEX_OPTIONS = {
7
11
  via: :geojson,
8
12
  strategy: :rtree,
@@ -11,41 +15,24 @@ module TG
11
15
  }.freeze
12
16
 
13
17
  class << self
14
- def source(&definition)
15
- if definition
16
- @source = definition
17
- elsif instance_variable_defined?(:@source)
18
- @source
19
- elsif superclass.respond_to?(:source)
20
- superclass.source
21
- end
18
+ def source(&block)
19
+ @source = block if block
20
+ @source || (superclass.source if superclass.respond_to?(:source))
22
21
  end
23
22
 
24
23
  def index_options(**options)
25
- if options.empty?
26
- inherited = superclass.respond_to?(:index_options) ? superclass.index_options : DEFAULT_INDEX_OPTIONS
27
- inherited.merge(@index_options || {}).freeze
28
- else
29
- @index_options = index_options.merge(options).freeze
30
- end
31
- end
24
+ inherited = superclass.respond_to?(:index_options) ? superclass.index_options : DEFAULT_INDEX_OPTIONS
25
+ return inherited.merge(@index_options || {}).freeze if options.empty?
32
26
 
33
- def active_record_source(scope, id:, geometry:, batch_size: 1_000)
34
- require_relative "active_record_source"
35
-
36
- source do
37
- TG::Geometry::ActiveRecordSource.call(
38
- scope,
39
- id: id,
40
- geometry: geometry,
41
- batch_size: batch_size
42
- )
43
- end
27
+ @index_options = inherited.merge(@index_options || {}, options).freeze
44
28
  end
45
29
  end
46
30
 
47
31
  attr_reader :index_options
48
32
 
33
+ def_delegators :index, :size, :bbox, :find_covering, :covering_ids,
34
+ :intersecting_rect, :covering_ids_batch_packed
35
+
49
36
  def initialize(entries: nil, source: nil, **index_options)
50
37
  @entries = entries
51
38
  @source = source
@@ -56,11 +43,7 @@ module TG
56
43
 
57
44
  def reload!
58
45
  new_index = TG::Geometry::Index.build(resolve_entries, **@index_options)
59
-
60
- @reload_mutex.synchronize do
61
- @index = new_index
62
- end
63
-
46
+ @reload_mutex.synchronize { @index = new_index }
64
47
  new_index
65
48
  end
66
49
 
@@ -68,51 +51,17 @@ module TG
68
51
  @index || raise(TG::Geometry::Error, "registry index is not loaded; call reload! first")
69
52
  end
70
53
 
71
- def loaded?
72
- !@index.nil?
73
- end
74
-
75
- def size
76
- current_index.size
77
- end
78
-
79
- def bbox
80
- current_index.bbox
81
- end
82
-
83
- def find_covering(lon, lat)
84
- current_index.find_covering(lon, lat)
85
- end
86
-
87
- def covering_ids(lon, lat)
88
- current_index.covering_ids(lon, lat)
89
- end
90
-
91
- def intersecting_rect(min_x, min_y, max_x, max_y)
92
- current_index.intersecting_rect(min_x, min_y, max_x, max_y)
93
- end
94
-
95
- def covering_ids_batch_packed(binary_string)
96
- current_index.covering_ids_batch_packed(binary_string)
97
- end
54
+ def loaded? = !@index.nil?
98
55
 
99
56
  private
100
57
 
101
- def current_index
102
- @index || raise(TG::Geometry::Error, "registry index is not loaded; call reload! first")
103
- end
104
-
105
58
  def resolve_entries
106
- return @entries unless @entries.nil?
59
+ return @entries if @entries
107
60
 
108
61
  callable = @source || self.class.source
109
62
  raise TG::Geometry::Error, "registry source is not configured" unless callable
110
63
 
111
- if callable.respond_to?(:call)
112
- instance_exec(&callable)
113
- else
114
- raise TG::Geometry::Error, "registry source must be callable"
115
- end
64
+ instance_exec(&callable)
116
65
  end
117
66
  end
118
67
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TG
4
4
  module Geometry
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/tg/geometry.rb CHANGED
@@ -4,3 +4,88 @@ require_relative "geometry/version"
4
4
  require "tg_geometry_ext_geometry_ext"
5
5
  require_relative "geometry/registry"
6
6
  require_relative "geometry/active_record_source"
7
+
8
+ module TG
9
+ # Fast immutable planar geometry engine with proper indexes and clean
10
+ # EWKB/PostGIS boundaries. Not a GIS. Not an rgeo replacement.
11
+ #
12
+ # All distances and coordinates are in input units. SRID is metadata only;
13
+ # no reprojection is performed.
14
+ module Geometry
15
+ # @!method self.line_string(points, index: :natural, srid: nil)
16
+ # @param points [Array<Array<Float>>]
17
+ # @param index [:default, :none, :natural, :ystripes]
18
+ # @param srid [Integer, nil]
19
+ # @return [TG::Geometry::Geom]
20
+ #
21
+ # @!method self.polygon(exterior, holes: [], index: :ystripes, srid: nil)
22
+ # @param exterior [Array<Array<Float>>]
23
+ # @param holes [Array<Array<Array<Float>>>]
24
+ # @param index [:default, :none, :natural, :ystripes]
25
+ # @param srid [Integer, nil]
26
+ # @return [TG::Geometry::Geom]
27
+ #
28
+ # @!method self.multi_polygon(polygons, index: :ystripes, srid: nil)
29
+ # Each polygon is a Hash with :exterior and optional :holes, or an Array
30
+ # shorthand for an exterior with no holes.
31
+ # @param polygons [Array<Hash, Array>]
32
+ # @return [TG::Geometry::Geom]
33
+ #
34
+ # @!method self.parse_wkb(bytes, index: :ystripes)
35
+ # Parses WKB or EWKB. SRID is preserved when the EWKB SRID flag is set.
36
+ # @return [TG::Geometry::Geom]
37
+ #
38
+ # @!method self.parse_hex(hex, index: :ystripes)
39
+ # Parses HEXWKB or HEXEWKB. SRID is preserved when the EWKB SRID flag is set.
40
+ # @return [TG::Geometry::Geom]
41
+
42
+ class Geom
43
+ # @!method srid
44
+ # @return [Integer, nil] SRID metadata; not used for reprojection
45
+ #
46
+ # @!method to_ewkb(srid: nil)
47
+ # Writes EWKB with the SRID flag set. Uses explicit srid: when provided,
48
+ # otherwise Geom#srid. Raises if no SRID is available. to_wkb remains plain.
49
+ # @return [String] frozen ASCII-8BIT EWKB string
50
+ end
51
+
52
+ class Index
53
+ # @!method self.build(entries, via:, strategy:, predicate: :covers, geometry_index: :ystripes)
54
+ # predicate: affects only legacy point query methods: find_covering,
55
+ # covering_ids(x, y), covering_ids_batch_packed. The *_geom_ids methods
56
+ # use their own predicates derived from the method name.
57
+ #
58
+ # @!method intersecting_geom_ids(geom)
59
+ # Stored geometries for which tg_geom_intersects(stored, query) is true.
60
+ # @return [Array<Object>] ids in insertion order
61
+ #
62
+ # @!method covering_geom_ids(geom)
63
+ # Stored geometries for which tg_geom_covers(stored, query) is true.
64
+ # Direction: stored covers query. Boundary points are covered.
65
+ # @return [Array<Object>] ids in insertion order
66
+ #
67
+ # @!method containing_geom_ids(geom)
68
+ # Stored geometries for which tg_geom_contains(stored, query) is true.
69
+ # Direction: stored contains query. Boundary points are not contained.
70
+ # @return [Array<Object>] ids in insertion order
71
+ end
72
+
73
+ class Line
74
+ # @!method nearest_segment(x, y)
75
+ # @return [TG::Geometry::NearestSegment, nil] planar Euclidean
76
+ end
77
+
78
+ class Ring
79
+ # @!method nearest_segment(x, y)
80
+ # @return [TG::Geometry::NearestSegment, nil] planar Euclidean
81
+ end
82
+
83
+ # Result of Line#nearest_segment / Ring#nearest_segment.
84
+ class NearestSegment
85
+ # @!attribute [r] segment @return [TG::Geometry::Segment]
86
+ # @!attribute [r] index @return [Integer]
87
+ # @!attribute [r] distance @return [Float] planar Euclidean in input units
88
+ # @!attribute [r] point @return [Array(Float, Float)] projection
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tg/geometry/active_record_type"
5
+
6
+ RSpec.describe TG::Geometry::ActiveRecordType do
7
+ let(:type) { described_class.new }
8
+ let(:ewkb) { TG::Geometry.parse_wkt("POINT (1 2)").to_ewkb(srid: 4326) }
9
+ let(:geom) { TG::Geometry.parse_wkb(ewkb) }
10
+
11
+ it "deserializes supported read shapes" do
12
+ expect(type.type).to eq(:tg_geometry)
13
+ expect(type.deserialize(nil)).to be_nil
14
+ expect(type.deserialize(geom)).to equal(geom)
15
+ expect(type.deserialize(ewkb).srid).to eq(4326)
16
+ expect(type.deserialize(ewkb.unpack1("H*")).srid).to eq(4326)
17
+ expect(type.deserialize("\\x#{ewkb.unpack1('H*')}").srid).to eq(4326)
18
+ expect(type.deserialize('{"type":"Point","coordinates":[1,2]}').srid).to be_nil
19
+ expect(type.deserialize("POINT (1 2)").srid).to be_nil
20
+ end
21
+
22
+ it "detects ASCII-8BIT hex before WKB fallback" do
23
+ hex = ewkb.unpack1("H*").b
24
+
25
+ expect(type.deserialize(hex).srid).to eq(4326)
26
+ end
27
+
28
+ it "is strict by default and can warn+nil in non-strict mode" do
29
+ expect { type.deserialize("not geometry") }.to raise_error(TG::Geometry::ParseError)
30
+
31
+ non_strict = described_class.new(strict: false)
32
+ expect { expect(non_strict.deserialize("not geometry")).to be_nil }
33
+ .to output(/parse failed/).to_stderr
34
+ end
35
+
36
+ it "is read-only on serialize" do
37
+ expect(type.serialize(nil)).to be_nil
38
+ expect(type.serialize("raw")).to eq("raw")
39
+ expect { type.serialize(geom) }.to raise_error(TG::Geometry::ArgumentError)
40
+ end
41
+
42
+ it "uses cast as deserialize" do
43
+ expect(type.cast(ewkb).srid).to eq(4326)
44
+ end
45
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Constructors" do
6
+ let(:square) { [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]] }
7
+ let(:hole_a) { [[2.0, 2.0], [4.0, 2.0], [4.0, 4.0], [2.0, 4.0], [2.0, 2.0]] }
8
+ let(:hole_b) { [[6.0, 6.0], [8.0, 6.0], [8.0, 8.0], [6.0, 8.0], [6.0, 6.0]] }
9
+
10
+ describe ".line_string" do
11
+ it "builds a frozen two-point linestring" do
12
+ geom = TG::Geometry.line_string([[0.0, 0.0], [1.0, 1.0]])
13
+
14
+ expect(geom).to be_frozen
15
+ expect(geom.type).to eq(:linestring)
16
+ expect(geom.line.num_points).to eq(2)
17
+ expect(geom.srid).to be_nil
18
+ end
19
+
20
+ it "builds a 100-point linestring" do
21
+ points = 100.times.map { |i| [i.to_f, (i * 2).to_f] }
22
+ geom = TG::Geometry.line_string(points)
23
+
24
+ expect(geom.line.num_points).to eq(100)
25
+ end
26
+
27
+ it "supports explicit index values and srid metadata" do
28
+ %i[default natural ystripes none].each do |index|
29
+ geom = TG::Geometry.line_string([[0, 0], [1, 1]], index: index, srid: 4326)
30
+ expect(geom.srid).to eq(4326)
31
+ end
32
+ end
33
+
34
+ it "rejects too few points" do
35
+ expect { TG::Geometry.line_string([[0, 0]]) }
36
+ .to raise_error(TG::Geometry::ArgumentError, "line_string requires at least 2 points, got 1")
37
+ expect { TG::Geometry.line_string([]) }
38
+ .to raise_error(TG::Geometry::ArgumentError, "line_string requires at least 2 points, got 0")
39
+ end
40
+
41
+ it "rejects non-finite coordinates with the bad point index in the message" do
42
+ expect { TG::Geometry.line_string([[0, 0], [Float::NAN, 1]]) }
43
+ .to raise_error(TG::Geometry::ArgumentError, /point 1/)
44
+ expect { TG::Geometry.line_string([[0, 0], [1, Float::INFINITY]]) }
45
+ .to raise_error(TG::Geometry::ArgumentError, /point 1/)
46
+ end
47
+
48
+ it "rejects out-of-range srid" do
49
+ expect { TG::Geometry.line_string([[0, 0], [1, 1]], srid: -1) }
50
+ .to raise_error(TG::Geometry::ArgumentError)
51
+ end
52
+ end
53
+
54
+ describe ".polygon" do
55
+ it "builds a square polygon" do
56
+ geom = TG::Geometry.polygon(square)
57
+
58
+ expect(geom.type).to eq(:polygon)
59
+ expect(geom.polygon.num_holes).to eq(0)
60
+ expect(geom.covers_xy?(5, 5)).to be(true)
61
+ end
62
+
63
+ it "builds polygons with one or two holes" do
64
+ one_hole = TG::Geometry.polygon(square, holes: [hole_a], srid: 4326)
65
+ two_holes = TG::Geometry.polygon(square, holes: [hole_a, hole_b])
66
+
67
+ expect(one_hole.srid).to eq(4326)
68
+ expect(one_hole.polygon.num_holes).to eq(1)
69
+ expect(one_hole.covers_xy?(3, 3)).to be(false)
70
+ expect(two_holes.polygon.num_holes).to eq(2)
71
+ end
72
+
73
+ it "treats holes: [] like omitted holes" do
74
+ expect(TG::Geometry.polygon(square, holes: []).polygon.num_holes).to eq(0)
75
+ expect(TG::Geometry.polygon(square).polygon.num_holes).to eq(0)
76
+ end
77
+
78
+ it "rejects invalid rings without autoclosing" do
79
+ expect { TG::Geometry.polygon(square[0...-1]) }
80
+ .to raise_error(TG::Geometry::ArgumentError, "polygon exterior ring is not closed")
81
+ expect { TG::Geometry.polygon(square[0, 3]) }
82
+ .to raise_error(TG::Geometry::ArgumentError, "polygon exterior ring requires at least 4 points")
83
+ expect { TG::Geometry.polygon(square, holes: [hole_a[0...-1]]) }
84
+ .to raise_error(TG::Geometry::ArgumentError, "polygon hole 0 is not closed")
85
+ expect { TG::Geometry.polygon(square, holes: [hole_a[0, 3]]) }
86
+ .to raise_error(TG::Geometry::ArgumentError, "polygon hole 0 requires at least 4 points")
87
+ end
88
+ end
89
+
90
+ describe ".multi_polygon" do
91
+ it "builds empty and non-empty multipolygons" do
92
+ expect(TG::Geometry.multi_polygon([]).type).to eq(:multipolygon)
93
+
94
+ geom = TG::Geometry.multi_polygon([
95
+ { exterior: square, holes: [hole_a] },
96
+ [[20.0, 20.0], [21.0, 20.0], [21.0, 21.0], [20.0, 21.0], [20.0, 20.0]]
97
+ ], srid: 3857)
98
+
99
+ expect(geom.type).to eq(:multipolygon)
100
+ expect(geom.srid).to eq(3857)
101
+ expect(geom.num_polygons).to eq(2)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,8 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {"type":"Feature","properties":{"@id":"ok-a"},"geometry":{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,1],[1,0],[0,0]]] }},
5
+ {"type":"Feature","properties":{"@id":"bad"},"geometry":{"type":"Polygon","coordinates":"not coordinates"}},
6
+ {"type":"Feature","properties":{"@id":"ok-b"},"geometry":{"type":"Polygon","coordinates":[[[2,2],[2,3],[3,3],[3,2],[2,2]]] }}
7
+ ]
8
+ }
@@ -0,0 +1 @@
1
+ {"type":"FeatureCollection","features":[
@@ -0,0 +1,8 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {"type":"Feature","properties":{"@id":"poly"},"geometry":{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,1],[1,0],[0,0]]]}},
5
+ {"type":"Feature","properties":{"@id":"point"},"geometry":{"type":"Point","coordinates":[5,5]}},
6
+ {"type":"Feature","properties":{"@id":"line"},"geometry":{"type":"LineString","coordinates":[[0,0],[1,1]]}}
7
+ ]
8
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {
5
+ "type": "Feature",
6
+ "properties": {"@id": "relation/100", "admin_level": "8"},
7
+ "geometry": {"type": "Polygon", "coordinates": [[[0,0],[0,1],[1,1],[1,0],[0,0]]]}
8
+ }
9
+ ]
10
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {"type":"Feature","properties":null,"geometry":{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,1],[1,0],[0,0]]]}},
5
+ {"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[2,2],[2,3],[3,3],[3,2],[2,2]]]}}
6
+ ]
7
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {
5
+ "type": "Feature",
6
+ "properties": {"@id": "zone/a", "name": "A"},
7
+ "geometry": {"type": "Polygon", "coordinates": [[[0,0],[0,10],[10,10],[10,0],[0,0]]]}
8
+ },
9
+ {
10
+ "type": "Feature",
11
+ "properties": {"@id": 2},
12
+ "geometry": {"type": "MultiPolygon", "coordinates": [[[[20,20],[20,30],[30,30],[30,20],[20,20]]]]}
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,16 @@
1
+ # PostGIS / EWKB fixtures
2
+
3
+ These files are committed binary EWKB fixtures used by the v0.3.0 SRID/EWKB tests.
4
+ They are intentionally small and are not generated at spec runtime.
5
+
6
+ Canonical PostGIS generation examples:
7
+
8
+ ```sql
9
+ SELECT ST_AsEWKB(ST_GeomFromText('POINT(37.6 55.7)', 4326));
10
+ SELECT ST_AsEWKB(ST_GeomFromText('POLYGON((0 0,10 0,10 10,0 10,0 0))', 4326));
11
+ SELECT ST_AsEWKB(ST_GeomFromText('POLYGON((0 0,10 0,10 10,0 10,0 0),(2 2,4 2,4 4,2 4,2 2))', 4326));
12
+ SELECT ST_AsEWKB(ST_GeomFromText('POLYGON((0 0,10 0,10 10,0 10,0 0))', 3857));
13
+ ```
14
+
15
+ `multipolygon_large.ewkb` is a synthetic large multipolygon fixture with about 7000 points.
16
+ It stands in for a real imported polygon while keeping the repository small.
@@ -0,0 +1,83 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {
5
+ "type": "Feature",
6
+ "properties": {
7
+ "name": "inside_simple"
8
+ },
9
+ "geometry": {
10
+ "type": "Point",
11
+ "coordinates": [
12
+ 1.0,
13
+ 1.0
14
+ ]
15
+ }
16
+ },
17
+ {
18
+ "type": "Feature",
19
+ "properties": {
20
+ "name": "boundary_simple"
21
+ },
22
+ "geometry": {
23
+ "type": "Point",
24
+ "coordinates": [
25
+ 0.0,
26
+ 5.0
27
+ ]
28
+ }
29
+ },
30
+ {
31
+ "type": "Feature",
32
+ "properties": {
33
+ "name": "hole_center"
34
+ },
35
+ "geometry": {
36
+ "type": "Point",
37
+ "coordinates": [
38
+ 3.0,
39
+ 3.0
40
+ ]
41
+ }
42
+ },
43
+ {
44
+ "type": "Feature",
45
+ "properties": {
46
+ "name": "between_hole_and_outer"
47
+ },
48
+ "geometry": {
49
+ "type": "Point",
50
+ "coordinates": [
51
+ 1.0,
52
+ 1.0
53
+ ]
54
+ }
55
+ },
56
+ {
57
+ "type": "Feature",
58
+ "properties": {
59
+ "name": "large_inside"
60
+ },
61
+ "geometry": {
62
+ "type": "Point",
63
+ "coordinates": [
64
+ 100.0,
65
+ 50.0
66
+ ]
67
+ }
68
+ },
69
+ {
70
+ "type": "Feature",
71
+ "properties": {
72
+ "name": "large_outside"
73
+ },
74
+ "geometry": {
75
+ "type": "Point",
76
+ "coordinates": [
77
+ 200.0,
78
+ 50.0
79
+ ]
80
+ }
81
+ }
82
+ ]
83
+ }