tiletanic 0.1.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.
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tiletanic
4
+ module TileSchemes
5
+ class BasicTiling
6
+ QUADKEY_PATTERN = /\A[0-3]+\z/
7
+
8
+ attr_reader :bounds
9
+
10
+ def initialize(xmin, ymin, xmax, ymax)
11
+ raise ArgumentError, 'xmax must be greater than xmin' unless xmax > xmin
12
+ raise ArgumentError, 'ymax must be greater than ymin' unless ymax > ymin
13
+
14
+ @functional_bounds = CoordsBBox.new(
15
+ xmin: Float(xmin),
16
+ ymin: Float(ymin),
17
+ xmax: Float(xmax),
18
+ ymax: Float(ymax)
19
+ )
20
+ @bounds = CoordsBBox.new(
21
+ xmin: Float(xmin),
22
+ ymin: Float(ymin),
23
+ xmax: Float(xmax),
24
+ ymax: Float(ymax)
25
+ )
26
+ end
27
+
28
+ def tile(xcoord, ycoord, zoom)
29
+ zoom = Integer(zoom)
30
+ Tile.new(
31
+ x: x_index(Float(xcoord), zoom),
32
+ y: y_index(Float(ycoord), zoom),
33
+ z: zoom
34
+ )
35
+ end
36
+
37
+ def parent(*tile)
38
+ x, y, z = extract_tile(*tile)
39
+ Tile.new(x: x / 2, y: y / 2, z: z - 1)
40
+ end
41
+
42
+ def children(*tile)
43
+ x, y, z = extract_tile(*tile)
44
+ [[0, 0], [1, 0], [0, 1], [1, 1]].map do |delta_x, delta_y|
45
+ Tile.new(x: (2 * x) + delta_x, y: (2 * y) + delta_y, z: z + 1)
46
+ end
47
+ end
48
+
49
+ def bbox(*tile)
50
+ upper_left = ul(*tile)
51
+ bottom_right = br(*tile)
52
+
53
+ CoordsBBox.new(
54
+ xmin: upper_left.x,
55
+ ymin: bottom_right.y,
56
+ xmax: bottom_right.x,
57
+ ymax: upper_left.y
58
+ )
59
+ end
60
+
61
+ def quadkey(*tile)
62
+ x, y, z = extract_tile(*tile)
63
+ return '' if z.zero?
64
+
65
+ z.downto(1).map do |zoom|
66
+ digit = 0
67
+ mask = 1 << (zoom - 1)
68
+ digit += 1 if x.anybits?(mask)
69
+ digit += 2 if y.anybits?(mask)
70
+ digit
71
+ end.join
72
+ end
73
+
74
+ def quadkey_to_tile(quadkey)
75
+ raise ArgumentError, 'Input quadkey is invalid.' unless QUADKEY_PATTERN.match?(quadkey)
76
+
77
+ x = 0
78
+ y = 0
79
+
80
+ quadkey.reverse.each_char.with_index do |digit, index|
81
+ mask = 1 << index
82
+
83
+ case digit
84
+ when '1'
85
+ x |= mask
86
+ when '2'
87
+ y |= mask
88
+ when '3'
89
+ x |= mask
90
+ y |= mask
91
+ end
92
+ end
93
+
94
+ Tile.new(x: x, y: y, z: quadkey.length)
95
+ end
96
+
97
+ private
98
+
99
+ attr_reader :functional_bounds
100
+
101
+ def extract_tile(*tile)
102
+ values =
103
+ if tile.length == 1
104
+ candidate = tile.first
105
+ raise ArgumentError, 'expected a Tile or x/y/z components' unless candidate.respond_to?(:to_a)
106
+
107
+ candidate.to_a
108
+ else
109
+ tile
110
+ end
111
+
112
+ raise ArgumentError, 'expected exactly three tile components' unless values.length == 3
113
+
114
+ values.map { |value| Integer(value) }
115
+ end
116
+
117
+ def x_coord(x, zoom)
118
+ ((x / (2.0**zoom) * (functional_bounds.xmax - functional_bounds.xmin)) + functional_bounds.xmin)
119
+ end
120
+
121
+ def x_index(xcoord, zoom)
122
+ ((2.0**zoom) * (xcoord - functional_bounds.xmin) / (functional_bounds.xmax - functional_bounds.xmin)).floor
123
+ end
124
+ end
125
+
126
+ class BasicTilingBottomLeft < BasicTiling
127
+ def ul(*tile)
128
+ x, y, z = send(:extract_tile, *tile)
129
+ Coords.new(x: send(:x_coord, x, z), y: y_coord(y + 1, z))
130
+ end
131
+
132
+ def br(*tile)
133
+ x, y, z = send(:extract_tile, *tile)
134
+ Coords.new(x: send(:x_coord, x + 1, z), y: y_coord(y, z))
135
+ end
136
+
137
+ private
138
+
139
+ def y_coord(y, zoom)
140
+ bounds = send(:functional_bounds)
141
+ ((y / (2.0**zoom) * (bounds.ymax - bounds.ymin)) + bounds.ymin)
142
+ end
143
+
144
+ def y_index(ycoord, zoom)
145
+ bounds = send(:functional_bounds)
146
+ ((2.0**zoom) * (ycoord - bounds.ymin) / (bounds.ymax - bounds.ymin)).floor
147
+ end
148
+ end
149
+
150
+ class BasicTilingTopLeft < BasicTiling
151
+ def ul(*tile)
152
+ x, y, z = send(:extract_tile, *tile)
153
+ Coords.new(x: send(:x_coord, x, z), y: y_coord(y, z))
154
+ end
155
+
156
+ def br(*tile)
157
+ x, y, z = send(:extract_tile, *tile)
158
+ Coords.new(x: send(:x_coord, x + 1, z), y: y_coord(y + 1, z))
159
+ end
160
+
161
+ private
162
+
163
+ def y_coord(y, zoom)
164
+ bounds = send(:functional_bounds)
165
+ bounds.ymax - (y / (2.0**zoom) * (bounds.ymax - bounds.ymin))
166
+ end
167
+
168
+ def y_index(ycoord, zoom)
169
+ bounds = send(:functional_bounds)
170
+ ((2.0**zoom) * (bounds.ymax - ycoord) / (bounds.ymax - bounds.ymin)).floor
171
+ end
172
+ end
173
+
174
+ class DGTiling < BasicTilingBottomLeft
175
+ def initialize
176
+ super(-180, -90, 180, 270)
177
+ @bounds = CoordsBBox.new(xmin: -180.0, ymin: -90.0, xmax: 180.0, ymax: 90.0)
178
+ end
179
+
180
+ def children(*tile)
181
+ x, y, z = send(:extract_tile, *tile)
182
+ return [Tile.new(x: 0, y: 0, z: 1), Tile.new(x: 1, y: 0, z: 1)] if z.zero?
183
+
184
+ super(x, y, z)
185
+ end
186
+ end
187
+
188
+ class WebMercatorBL < BasicTilingBottomLeft
189
+ WEB_MERCATOR_LIMIT = 20_037_508.342789244
190
+
191
+ def initialize
192
+ super(-WEB_MERCATOR_LIMIT, -WEB_MERCATOR_LIMIT, WEB_MERCATOR_LIMIT, WEB_MERCATOR_LIMIT)
193
+ end
194
+
195
+ def quadkey(*tile)
196
+ x, y, z = send(:extract_tile, *tile)
197
+ return '' if z.zero?
198
+
199
+ z.downto(1).map do |zoom|
200
+ digit = 0
201
+ mask = 1 << (zoom - 1)
202
+ digit += 1 if x.anybits?(mask)
203
+ digit += 2 if y.nobits?(mask)
204
+ digit
205
+ end.join
206
+ end
207
+
208
+ def quadkey_to_tile(quadkey)
209
+ tile = super
210
+ Tile.new(x: tile.x, y: (2**quadkey.length) - tile.y - 1, z: tile.z)
211
+ end
212
+ end
213
+
214
+ class WebMercator < BasicTilingTopLeft
215
+ WEB_MERCATOR_LIMIT = WebMercatorBL::WEB_MERCATOR_LIMIT
216
+
217
+ def initialize
218
+ super(-WEB_MERCATOR_LIMIT, -WEB_MERCATOR_LIMIT, WEB_MERCATOR_LIMIT, WEB_MERCATOR_LIMIT)
219
+ end
220
+ end
221
+
222
+ class UTMTiling < BasicTilingTopLeft
223
+ attr_reader :tile_size, :zoom
224
+
225
+ def initialize(tile_size)
226
+ raise ArgumentError, 'tile_size must be positive' unless tile_size.positive?
227
+
228
+ @tile_size = tile_size
229
+ internal_zoom = Math.log2(10_000_000.0 / tile_size).ceil
230
+ map_size = tile_size * (2**internal_zoom)
231
+ @zoom = internal_zoom + 1
232
+
233
+ super(-map_size + 500_000.0, -map_size, map_size + 500_000.0, map_size)
234
+ end
235
+ end
236
+
237
+ class UTM5kmTiling < UTMTiling
238
+ def initialize
239
+ super(5_000)
240
+ end
241
+ end
242
+
243
+ class UTM10kmTiling < UTMTiling
244
+ def initialize
245
+ super(10_000)
246
+ end
247
+ end
248
+
249
+ class UTM100kmTiling < UTMTiling
250
+ def initialize
251
+ super(100_000)
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tiletanic
4
+ VERSION = '0.1.0'
5
+ end
data/lib/tiletanic.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'tiletanic/version'
4
+ require_relative 'tiletanic/base'
5
+ require_relative 'tiletanic/tileschemes'
6
+ require_relative 'tiletanic/geos_bootstrap'
7
+ require_relative 'tiletanic/tile_cover'
8
+ require_relative 'tiletanic/tilecover'
9
+ require_relative 'tiletanic/cli'
10
+
11
+ module Tiletanic
12
+ class Error < StandardError; end
13
+
14
+ module_function
15
+
16
+ def cover_geometry(...)
17
+ TileCover.cover_geometry(...)
18
+ end
19
+
20
+ def geos_factory(**options)
21
+ factory_options = { uses_lenient_assertions: true }.merge(options)
22
+
23
+ if defined?(RGeo::Geos) && (!RGeo::Geos.respond_to?(:supported?) || RGeo::Geos.supported?)
24
+ return RGeo::Geos.factory(factory_options)
25
+ end
26
+
27
+ RGeo::Cartesian.preferred_factory(options)
28
+ rescue StandardError => e
29
+ raise Error, "Could not create an RGeo geometry factory: #{e.message}"
30
+ end
31
+ end
data/test/cli_test.rb ADDED
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class CLITest < Minitest::Test
6
+ ROOT = File.expand_path('..', __dir__)
7
+ EXECUTABLE = File.join(ROOT, 'exe', 'tiletanic')
8
+ FEATURE_COLLECTION_PAYLOAD = <<~JSON
9
+ {"type":"FeatureCollection","features":[{"geometry":{"type":"Polygon","coordinates":[[[-101.953125,43.59375],[-101.953125,44.296875],[-102.65625,44.296875],[-102.65625,43.59375],[-101.953125,43.59375]]]},"type":"Feature","properties":{}}]}
10
+ JSON
11
+ FEATURE_PAYLOAD = <<~JSON
12
+ {"geometry":{"coordinates":[[[-101.953125,43.59375],[-101.953125,44.296875],[-102.65625,44.296875],[-102.65625,43.59375],[-101.953125,43.59375]]],"type":"Polygon"},"type":"Feature"}
13
+ JSON
14
+ MULTIPOLYGON_PAYLOAD = <<~JSON
15
+ {"type":"MultiPolygon","coordinates":[[[[-101.953125,43.59375],[-101.953125,44.296875],[-102.65625,44.296875],[-102.65625,43.59375],[-101.953125,43.59375]]]]}
16
+ JSON
17
+ ZOOM_14_TILE_PAYLOAD = <<~JSON
18
+ {"geometry":{"coordinates":[[[0.0,0.0],[0.02197265625,0.0],[0.02197265625,0.02197265625],[0.0,0.02197265625],[0.0,0.0]]],"type":"Polygon"},"type":"Feature"}
19
+ JSON
20
+
21
+ def run_cli(*args, stdin_data: nil)
22
+ Open3.capture3(
23
+ Gem.ruby,
24
+ '-I',
25
+ File.join(ROOT, 'lib'),
26
+ EXECUTABLE,
27
+ *args,
28
+ stdin_data: stdin_data
29
+ )
30
+ end
31
+
32
+ def test_root_command
33
+ stdout, stderr, status = run_cli
34
+
35
+ assert status.success?, stderr
36
+ assert_includes stdout, 'cover_geometry'
37
+ end
38
+
39
+ def test_version
40
+ stdout, stderr, status = run_cli('--version')
41
+
42
+ assert status.success?, stderr
43
+ assert_equal "tiletanic #{Tiletanic::VERSION}\n", stdout
44
+ end
45
+
46
+ def test_cover_geometry_feature_collection
47
+ stdout, stderr, status = run_cli('cover_geometry', '-', stdin_data: FEATURE_COLLECTION_PAYLOAD)
48
+
49
+ assert status.success?, stderr
50
+ assert_equal "021323330\n", stdout
51
+ end
52
+
53
+ def test_cover_geometry_multipolygon_geometry
54
+ stdout, stderr, status = run_cli('cover_geometry', '-', stdin_data: MULTIPOLYGON_PAYLOAD)
55
+
56
+ assert status.success?, stderr
57
+ assert_equal "021323330\n", stdout
58
+ end
59
+
60
+ def test_cover_geometry_adjacent_tiles
61
+ stdout, stderr, status = run_cli('cover_geometry', '--adjacent', '-', stdin_data: FEATURE_PAYLOAD)
62
+
63
+ assert status.success?, stderr
64
+ assert_equal <<~OUTPUT, stdout
65
+ 021323303
66
+ 021323312
67
+ 021323313
68
+ 021323321
69
+ 021323323
70
+ 021323330
71
+ 021323331
72
+ 021323332
73
+ 021323333
74
+ OUTPUT
75
+ end
76
+
77
+ def test_cover_geometry_geojson_stdout
78
+ stdout, stderr, status = run_cli('cover_geometry', '--geojson', '-', stdin_data: FEATURE_COLLECTION_PAYLOAD)
79
+
80
+ assert status.success?, stderr
81
+
82
+ payload = JSON.parse(stdout)
83
+ assert_equal 'FeatureCollection', payload['type']
84
+ assert_equal 1, payload['features'].length
85
+
86
+ feature = payload['features'].first
87
+ assert_equal 'Feature', feature['type']
88
+ assert_equal(
89
+ { 'quadkey' => '021323330', 'x' => 110, 'y' => 190, 'z' => 9 },
90
+ feature['properties']
91
+ )
92
+ assert_equal(
93
+ [[[-102.65625, 43.59375], [-101.953125, 43.59375], [-101.953125, 44.296875],
94
+ [-102.65625, 44.296875], [-102.65625, 43.59375]]],
95
+ feature.dig('geometry', 'coordinates')
96
+ )
97
+ end
98
+
99
+ def test_cover_geometry_geojson_file_output
100
+ Tempfile.create(%w[tiles .geojson]) do |file|
101
+ file.close
102
+
103
+ stdout, stderr, status = run_cli(
104
+ 'cover_geometry',
105
+ '--geojson',
106
+ '--output',
107
+ file.path,
108
+ '-',
109
+ stdin_data: FEATURE_COLLECTION_PAYLOAD
110
+ )
111
+
112
+ assert status.success?, stderr
113
+ assert_equal '', stdout
114
+
115
+ payload = JSON.parse(File.read(file.path))
116
+ assert_equal 'FeatureCollection', payload['type']
117
+ assert_equal 1, payload['features'].length
118
+ assert_equal '021323330', payload.dig('features', 0, 'properties', 'quadkey')
119
+ end
120
+ end
121
+
122
+ def test_cover_geometry_geojson_rounds_tile_edges_to_zoom_precision
123
+ stdout, stderr, status = run_cli(
124
+ 'cover_geometry',
125
+ '--zoom',
126
+ '14',
127
+ '--geojson',
128
+ '-',
129
+ stdin_data: ZOOM_14_TILE_PAYLOAD
130
+ )
131
+
132
+ assert status.success?, stderr
133
+ assert_includes stdout, '0.02197265625'
134
+
135
+ payload = JSON.parse(stdout)
136
+ feature = payload.fetch('features').first
137
+ assert_equal(
138
+ { 'quadkey' => '12000000000000', 'x' => 8192, 'y' => 4096, 'z' => 14 },
139
+ feature['properties']
140
+ )
141
+ assert_equal(
142
+ [[[0.0, 0.0], [0.02197265625, 0.0], [0.02197265625, 0.02197265625], [0.0, 0.02197265625], [0.0, 0.0]]],
143
+ feature.dig('geometry', 'coordinates')
144
+ )
145
+ end
146
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'minitest/autorun'
5
+ require 'json'
6
+ require 'open3'
7
+ require 'set'
8
+ require 'tempfile'
9
+
10
+ require 'tiletanic'
11
+
12
+ module GeometryAssertions
13
+ def geos_factory(srid: 4326)
14
+ @geos_factories ||= {}
15
+ @geos_factories[srid] ||= Tiletanic.geos_factory(srid: srid)
16
+ end
17
+
18
+ def decode_geometry(hash = nil, srid: 4326, **kwargs)
19
+ hash ||= kwargs
20
+ RGeo::GeoJSON.decode(JSON.generate(hash), geo_factory: geos_factory(srid:), json_parser: :json)
21
+ end
22
+
23
+ def assert_coords(expected_x, expected_y, actual, delta: 1e-9)
24
+ assert_in_delta(expected_x, actual.x, delta)
25
+ assert_in_delta(expected_y, actual.y, delta)
26
+ end
27
+
28
+ def assert_bbox(expected, actual, delta: 1e-9)
29
+ assert_in_delta(expected[:xmin], actual.xmin, delta)
30
+ assert_in_delta(expected[:ymin], actual.ymin, delta)
31
+ assert_in_delta(expected[:xmax], actual.xmax, delta)
32
+ assert_in_delta(expected[:ymax], actual.ymax, delta)
33
+ end
34
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class TileCoverTest < Minitest::Test
6
+ include GeometryAssertions
7
+
8
+ def setup
9
+ @dg = Tiletanic::TileSchemes::DGTiling.new
10
+ @wm = Tiletanic::TileSchemes::WebMercator.new
11
+ end
12
+
13
+ def test_rejects_non_rgeo_input
14
+ assert_raises(ArgumentError) { Tiletanic.cover_geometry(@dg, nil, 4).to_a }
15
+ end
16
+
17
+ def test_point_cover_matches_python_reference_values
18
+ point = decode_geometry(type: 'Point', coordinates: [-94.39453125, 15.908203125])
19
+
20
+ assert_equal [Tiletanic::Tile.new(x: 3, y: 4, z: 4)], Tiletanic.cover_geometry(@dg, point, 4).to_a
21
+ assert_equal Set[
22
+ Tiletanic::Tile.new(x: 973, y: 1204, z: 12),
23
+ Tiletanic::Tile.new(x: 973, y: 1205, z: 12),
24
+ Tiletanic::Tile.new(x: 974, y: 1204, z: 12),
25
+ Tiletanic::Tile.new(x: 974, y: 1205, z: 12)
26
+ ], Tiletanic.cover_geometry(@dg, point, 12).to_a.to_set
27
+ end
28
+
29
+ def test_multi_zoom_cover_collapses_to_larger_tiles_when_possible
30
+ point = decode_geometry(type: 'Point', coordinates: [-94.39453125, 15.908203125])
31
+
32
+ assert_equal Set[
33
+ Tiletanic::Tile.new(x: 486, y: 602, z: 11),
34
+ Tiletanic::Tile.new(x: 487, y: 602, z: 11)
35
+ ], Tiletanic.cover_geometry(@dg, point, [11, 12]).to_a.to_set
36
+ end
37
+
38
+ def test_polygon_cover_matches_python_reference_values
39
+ polygon = decode_geometry(
40
+ type: 'Polygon',
41
+ coordinates: [[
42
+ [74.5751953125, 46.86019101567027],
43
+ [73.916015625, 46.45299704748289],
44
+ [73.564453125, 46.21785176740299],
45
+ [73.32275390625, 45.920587344733654],
46
+ [73.289794921875, 45.56021795715051],
47
+ [73.465576171875, 45.27488643704894],
48
+ [74.036865234375, 44.96479793033104],
49
+ [74.256591796875, 45.07352060670971],
50
+ [74.322509765625, 45.48324350868221],
51
+ [74.46533203125, 45.91294412737392],
52
+ [74.849853515625, 46.057985244793024],
53
+ [74.99267578125, 46.354510837365254],
54
+ [75.465087890625, 46.40756396630067],
55
+ [76.102294921875, 46.42271253466719],
56
+ [76.904296875, 46.34692761055676],
57
+ [77.376708984375, 46.27863122156088],
58
+ [78.2666015625, 46.195042108660154],
59
+ [78.94775390625, 46.31658418182218],
60
+ [79.29931640625, 46.5286346952717],
61
+ [79.398193359375, 46.77749276376827],
62
+ [79.12353515625, 47.017716353979225],
63
+ [78.42041015625, 46.912750956378915],
64
+ [77.574462890625, 46.76244305208004],
65
+ [76.46484375, 46.81509864599243],
66
+ [76.036376953125, 46.98025235521883],
67
+ [75.399169921875, 46.830133640447386],
68
+ [74.893798828125, 46.93526088057719],
69
+ [74.5751953125, 46.86019101567027]
70
+ ]]
71
+ )
72
+
73
+ assert_equal Set[
74
+ Tiletanic::Tile.new(x: 90, y: 48, z: 7),
75
+ Tiletanic::Tile.new(x: 91, y: 48, z: 7),
76
+ Tiletanic::Tile.new(x: 180, y: 95, z: 8),
77
+ Tiletanic::Tile.new(x: 184, y: 96, z: 8),
78
+ Tiletanic::Tile.new(x: 184, y: 97, z: 8)
79
+ ], Tiletanic.cover_geometry(@dg, polygon, [7, 8]).to_a.to_set
80
+ end
81
+
82
+ def test_polygon_with_hole_cover_matches_python_reference_values
83
+ polygon = decode_geometry(
84
+ type: 'Polygon',
85
+ coordinates: [
86
+ [
87
+ [31.794433593749996, -28.979312036722447],
88
+ [31.289062500000004, -29.401319510041485],
89
+ [31.036376953125, -29.897805610155864],
90
+ [30.377197265625, -30.845647420182598],
91
+ [29.278564453125, -31.886886525780806],
92
+ [26.74072265625, -31.94283997285307],
93
+ [23.9501953125, -31.184609135743237],
94
+ [23.917236328125, -28.98892237190413],
95
+ [25.5322265625, -27.615406013399603],
96
+ [27.894287109374996, -27.019984007982554],
97
+ [30.377197265625, -27.32297494724568],
98
+ [31.794433593749996, -28.979312036722447]
99
+ ],
100
+ [
101
+ [28.652343749999996, -28.584521719370393],
102
+ [28.399658203125, -28.632746799225856],
103
+ [28.3282470703125, -28.724313406473463],
104
+ [28.1634521484375, -28.729130483430154],
105
+ [28.015136718749996, -28.8831596093235],
106
+ [27.745971679687496, -28.92163128242129],
107
+ [27.531738281249996, -29.200123477644983],
108
+ [27.410888671874996, -29.382175075145277],
109
+ [27.257080078125, -29.54956657394792],
110
+ [26.987915039062496, -29.640320395351402],
111
+ [27.1966552734375, -30.002516938570686],
112
+ [27.31201171875, -30.140376821599734],
113
+ [27.3834228515625, -30.14987731644208],
114
+ [27.366943359375, -30.249577240467637],
115
+ [27.39990234375, -30.334953881988564],
116
+ [27.454833984375, -30.33021268543272],
117
+ [27.745971679687496, -30.60954979719083],
118
+ [27.8997802734375, -30.619004797647793],
119
+ [28.108520507812496, -30.680439786468128],
120
+ [28.1744384765625, -30.50075098029068],
121
+ [28.14697265625, -30.462879341709876],
122
+ [28.229370117187496, -30.410781790845878],
123
+ [28.2513427734375, -30.29701788337205],
124
+ [28.3612060546875, -30.202113679097216],
125
+ [28.71826171875, -30.135626231134587],
126
+ [28.9544677734375, -30.026299582223675],
127
+ [29.179687499999996, -29.912090918781477],
128
+ [29.1192626953125, -29.831113764737136],
129
+ [29.141235351562504, -29.67850809103362],
130
+ [29.256591796874996, -29.640320395351402],
131
+ [29.300537109374996, -29.492206334848714],
132
+ [29.410400390625, -29.40610505570927],
133
+ [29.443359375, -29.31993078977759],
134
+ [29.393920898437496, -29.195328267099118],
135
+ [29.2950439453125, -29.08977693862319],
136
+ [29.091796875, -28.936054482136658],
137
+ [28.976440429687496, -28.90239722855847],
138
+ [28.916015625, -28.762843805266016],
139
+ [28.800659179687496, -28.772474183943018],
140
+ [28.789672851562496, -28.6905876542507],
141
+ [28.7017822265625, -28.656851034203406],
142
+ [28.652343749999996, -28.584521719370393]
143
+ ]
144
+ ]
145
+ )
146
+
147
+ assert_equal 11, Tiletanic.cover_geometry(@dg, polygon, 7).to_a.size
148
+ end
149
+
150
+ def test_web_mercator_cover_uses_top_left_tilescheme
151
+ bbox = @wm.bbox(3, 5, 4)
152
+ polygon = decode_geometry(
153
+ {
154
+ type: 'Polygon',
155
+ coordinates: [[
156
+ [bbox.xmin + 10, bbox.ymin + 10],
157
+ [bbox.xmax - 10, bbox.ymin + 10],
158
+ [bbox.xmax - 10, bbox.ymax - 10],
159
+ [bbox.xmin + 10, bbox.ymax - 10],
160
+ [bbox.xmin + 10, bbox.ymin + 10]
161
+ ]]
162
+ },
163
+ srid: 3857
164
+ )
165
+
166
+ assert_equal [Tiletanic::Tile.new(x: 3, y: 5, z: 4)], Tiletanic.cover_geometry(@wm, polygon, 4).to_a
167
+ end
168
+ end