overture_maps 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE.txt +21 -0
- data/README.md +416 -0
- data/app/controllers/overture_maps/application_controller.rb +28 -0
- data/app/controllers/overture_maps/attribution_controller.rb +15 -0
- data/app/controllers/overture_maps/features_controller.rb +125 -0
- data/app/controllers/overture_maps/search_controller.rb +31 -0
- data/app/controllers/overture_maps/tiles_controller.rb +85 -0
- data/config/routes.rb +13 -0
- data/exe/overture-maps-mcp +6 -0
- data/lib/generators/overture_maps/address_generator.rb +19 -0
- data/lib/generators/overture_maps/base_features_generator.rb +19 -0
- data/lib/generators/overture_maps/base_generator.rb +22 -0
- data/lib/generators/overture_maps/building_generator.rb +19 -0
- data/lib/generators/overture_maps/division_generator.rb +19 -0
- data/lib/generators/overture_maps/install_generator.rb +42 -0
- data/lib/generators/overture_maps/place_generator.rb +19 -0
- data/lib/generators/overture_maps/transportation_generator.rb +20 -0
- data/lib/generators/templates/address_migration.rb.tt +25 -0
- data/lib/generators/templates/address_model.rb.tt +4 -0
- data/lib/generators/templates/base_feature_model.rb.tt +4 -0
- data/lib/generators/templates/base_features_migration.rb.tt +27 -0
- data/lib/generators/templates/building_migration.rb.tt +24 -0
- data/lib/generators/templates/building_model.rb.tt +4 -0
- data/lib/generators/templates/category_migration.rb.tt +15 -0
- data/lib/generators/templates/category_model.rb.tt +4 -0
- data/lib/generators/templates/connector_model.rb.tt +4 -0
- data/lib/generators/templates/division_migration.rb.tt +31 -0
- data/lib/generators/templates/division_model.rb.tt +4 -0
- data/lib/generators/templates/imported_areas_migration.rb.tt +20 -0
- data/lib/generators/templates/place_migration.rb.tt +30 -0
- data/lib/generators/templates/place_model.rb.tt +4 -0
- data/lib/generators/templates/postgis_migration.rb.tt +5 -0
- data/lib/generators/templates/segment_model.rb.tt +4 -0
- data/lib/generators/templates/transportation_migration.rb.tt +38 -0
- data/lib/overture_maps/attribution.rb +66 -0
- data/lib/overture_maps/bounding_box.rb +80 -0
- data/lib/overture_maps/changelog.rb +70 -0
- data/lib/overture_maps/configuration.rb +43 -0
- data/lib/overture_maps/database.rb +74 -0
- data/lib/overture_maps/division_search.rb +44 -0
- data/lib/overture_maps/engine.rb +32 -0
- data/lib/overture_maps/geometry_parser.rb +41 -0
- data/lib/overture_maps/gers.rb +30 -0
- data/lib/overture_maps/import/downloader.rb +248 -0
- data/lib/overture_maps/import/location_based_runner.rb +176 -0
- data/lib/overture_maps/import/parquet_reader.rb +32 -0
- data/lib/overture_maps/import/record_mapper.rb +182 -0
- data/lib/overture_maps/import/runner.rb +125 -0
- data/lib/overture_maps/import.rb +27 -0
- data/lib/overture_maps/mcp_server.rb +222 -0
- data/lib/overture_maps/models/address.rb +32 -0
- data/lib/overture_maps/models/base.rb +39 -0
- data/lib/overture_maps/models/base_feature.rb +31 -0
- data/lib/overture_maps/models/building.rb +35 -0
- data/lib/overture_maps/models/category.rb +32 -0
- data/lib/overture_maps/models/connector.rb +11 -0
- data/lib/overture_maps/models/division.rb +43 -0
- data/lib/overture_maps/models/imported_area.rb +42 -0
- data/lib/overture_maps/models/place.rb +43 -0
- data/lib/overture_maps/models/segment.rb +22 -0
- data/lib/overture_maps/models.rb +25 -0
- data/lib/overture_maps/query.rb +168 -0
- data/lib/overture_maps/query_engine.rb +268 -0
- data/lib/overture_maps/releases.rb +74 -0
- data/lib/overture_maps/storage.rb +121 -0
- data/lib/overture_maps/syncer.rb +99 -0
- data/lib/overture_maps/util.rb +30 -0
- data/lib/overture_maps/version.rb +3 -0
- data/lib/overture_maps.rb +42 -0
- data/lib/tasks/overture_maps.rake +554 -0
- metadata +301 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "overture_maps"
|
|
4
|
+
|
|
5
|
+
module OvertureMaps
|
|
6
|
+
# Interactive glue for the rake tasks. Prompts and process exits live here
|
|
7
|
+
# and only here — the library classes raise or take callbacks instead.
|
|
8
|
+
module RakeUI
|
|
9
|
+
# Which feature types are imported per theme, and into which model.
|
|
10
|
+
# building_part is skipped for now (needs a building_id relationship).
|
|
11
|
+
IMPORT_THEMES = {
|
|
12
|
+
"places" => { "place" => "OverturePlace" },
|
|
13
|
+
"buildings" => { "building" => "OvertureBuilding" },
|
|
14
|
+
"addresses" => { "address" => "OvertureAddress" },
|
|
15
|
+
"divisions" => { "division_area" => "OvertureDivision" },
|
|
16
|
+
"transportation" => {
|
|
17
|
+
"segment" => "OvertureSegment",
|
|
18
|
+
"connector" => "OvertureConnector"
|
|
19
|
+
},
|
|
20
|
+
"base" => OvertureMaps::Import::Downloader::TYPES["base"]
|
|
21
|
+
.to_h { |t| [t, "OvertureBaseFeature"] }
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def interactive?
|
|
27
|
+
!OvertureMaps.configuration.non_interactive && $stdin.tty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def abort!(message)
|
|
31
|
+
puts message
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Resolves { type => model_name } to { type => model_class }, verifying
|
|
36
|
+
# tables exist.
|
|
37
|
+
def require_models!(theme)
|
|
38
|
+
mapping = IMPORT_THEMES[theme] or abort!("No import models for theme #{theme}")
|
|
39
|
+
mapping.to_h do |type, name|
|
|
40
|
+
model = begin
|
|
41
|
+
name.constantize
|
|
42
|
+
rescue NameError
|
|
43
|
+
abort!("#{name} model not found. Run: rails generate overture_maps:install")
|
|
44
|
+
end
|
|
45
|
+
abort!("#{name} table does not exist. Run: rails db:migrate") unless model.table_exists?
|
|
46
|
+
[type, model]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def select_division_callback
|
|
51
|
+
return nil unless interactive?
|
|
52
|
+
|
|
53
|
+
->(results) { prompt_division(results) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def confirm_cached_callback
|
|
57
|
+
return nil unless interactive?
|
|
58
|
+
|
|
59
|
+
->(path) { prompt_cached(path) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def prompt_division(results)
|
|
63
|
+
puts "Multiple matches found:"
|
|
64
|
+
results.each_with_index do |r, i|
|
|
65
|
+
location_info = [r[:country], r[:region]].compact.join(" / ")
|
|
66
|
+
area_info = r[:area_km2]&.positive? ? " (#{r[:area_km2]} km²)" : ""
|
|
67
|
+
puts " #{i + 1}. #{r[:name]} (#{r[:subtype]}) - #{location_info}#{area_info}"
|
|
68
|
+
end
|
|
69
|
+
puts
|
|
70
|
+
print "Enter number to select (or 'q' to quit): "
|
|
71
|
+
input = $stdin.gets&.strip
|
|
72
|
+
|
|
73
|
+
return nil if input.nil? || input.downcase == "q"
|
|
74
|
+
|
|
75
|
+
index = input.to_i - 1
|
|
76
|
+
return nil unless index >= 0 && index < results.length
|
|
77
|
+
|
|
78
|
+
results[index]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def prompt_cached(path)
|
|
82
|
+
size = OvertureMaps::Util.format_size(File.size(path))
|
|
83
|
+
puts "Found cached extract: #{path} (#{size})"
|
|
84
|
+
print "Use it? (y = use / d = download fresh / q = quit): "
|
|
85
|
+
|
|
86
|
+
case $stdin.gets&.strip&.downcase
|
|
87
|
+
when "y", "yes", "", nil then :use
|
|
88
|
+
when "d", "download" then :refresh
|
|
89
|
+
else :abort
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Resolves a location argument (bbox string or division name) to a
|
|
94
|
+
# BoundingBox for the download tasks.
|
|
95
|
+
def resolve_bbox(location, release: nil)
|
|
96
|
+
bbox = OvertureMaps::BoundingBox.parse(location)
|
|
97
|
+
return bbox if bbox
|
|
98
|
+
|
|
99
|
+
results = OvertureMaps::DivisionSearch.search(query: location, release: release)
|
|
100
|
+
abort!("No divisions found matching '#{location}'") if results.empty?
|
|
101
|
+
|
|
102
|
+
division = results.length == 1 || !interactive? ? results.first : prompt_division(results)
|
|
103
|
+
if division.nil?
|
|
104
|
+
puts "Cancelled."
|
|
105
|
+
exit 0
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
info = [division[:country], division[:region]].compact.join(" / ")
|
|
109
|
+
puts "Using #{division[:name]} (#{division[:subtype]}#{info.empty? ? "" : ", #{info}"})"
|
|
110
|
+
division[:bbox]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def print_divisions(results)
|
|
114
|
+
results.each_with_index do |r, i|
|
|
115
|
+
location_parts = [r[:country], r[:region]].compact
|
|
116
|
+
location = location_parts.any? ? " - #{location_parts.join(" / ")}" : ""
|
|
117
|
+
area_info = r[:area_km2]&.positive? ? " [#{r[:area_km2]} km²]" : ""
|
|
118
|
+
puts " #{i + 1}. #{r[:name]} (#{r[:subtype]})#{location}#{area_info}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def release_arg
|
|
123
|
+
ENV["OVERTURE_RELEASE"]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
namespace :overture_maps do
|
|
129
|
+
namespace :import do
|
|
130
|
+
OvertureMaps::RakeUI::IMPORT_THEMES.each_key do |theme|
|
|
131
|
+
desc "Import #{theme} by location name or bounding box, e.g. rails overture_maps:import:#{theme}[Seattle]"
|
|
132
|
+
task theme.to_sym, [:location, :categories] => :environment do |_t, args|
|
|
133
|
+
location = args[:location]
|
|
134
|
+
unless location
|
|
135
|
+
puts "Usage:"
|
|
136
|
+
puts " rails overture_maps:import:#{theme}[Seattle]"
|
|
137
|
+
puts " rails \"overture_maps:import:#{theme}[47.606_-122.336_47.609_-122.333]\""
|
|
138
|
+
puts " rails overture_maps:import:#{theme}[Seattle,\"cafe,restaurant\"] # categories or groups" if theme == "places"
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
categories = args[:categories]&.split(",")&.map(&:strip)
|
|
143
|
+
models = OvertureMaps::RakeUI.require_models!(theme)
|
|
144
|
+
|
|
145
|
+
begin
|
|
146
|
+
runner = OvertureMaps::Import::LocationBasedRunner.new(
|
|
147
|
+
theme: theme,
|
|
148
|
+
location: location,
|
|
149
|
+
models: models,
|
|
150
|
+
categories: categories,
|
|
151
|
+
release: OvertureMaps::RakeUI.release_arg,
|
|
152
|
+
select_division: OvertureMaps::RakeUI.select_division_callback,
|
|
153
|
+
confirm_cached: OvertureMaps::RakeUI.confirm_cached_callback
|
|
154
|
+
).run
|
|
155
|
+
rescue OvertureMaps::CancelledError
|
|
156
|
+
puts "Cancelled."
|
|
157
|
+
exit 0
|
|
158
|
+
rescue OvertureMaps::Error => e
|
|
159
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
puts
|
|
163
|
+
puts "Import complete: #{runner.imported_count} imported, #{runner.error_count} errors"
|
|
164
|
+
if runner.errors.any? && ENV["VERBOSE"]
|
|
165
|
+
runner.errors.first(10).each { |err| puts " - #{err[:error]} (#{err[:record_id]})" }
|
|
166
|
+
end
|
|
167
|
+
exit 1 if runner.error_count.positive? && !ENV["IGNORE_ERRORS"]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
desc "Import all themes for a location"
|
|
172
|
+
task :all, [:location] => :environment do |_t, args|
|
|
173
|
+
location = args[:location] or OvertureMaps::RakeUI.abort!(
|
|
174
|
+
"Usage: rails overture_maps:import:all[Seattle]"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Resolve the location once so each theme reuses the same bbox instead
|
|
178
|
+
# of re-searching (and re-prompting).
|
|
179
|
+
bbox = OvertureMaps::RakeUI.resolve_bbox(location, release: OvertureMaps::RakeUI.release_arg)
|
|
180
|
+
|
|
181
|
+
failures = 0
|
|
182
|
+
OvertureMaps::RakeUI::IMPORT_THEMES.each_key do |theme|
|
|
183
|
+
puts "=" * 60
|
|
184
|
+
puts "Importing #{theme}..."
|
|
185
|
+
puts "=" * 60
|
|
186
|
+
|
|
187
|
+
models = OvertureMaps::RakeUI.require_models!(theme)
|
|
188
|
+
runner = OvertureMaps::Import::LocationBasedRunner.new(
|
|
189
|
+
theme: theme, location: bbox, models: models,
|
|
190
|
+
release: OvertureMaps::RakeUI.release_arg,
|
|
191
|
+
confirm_cached: OvertureMaps::RakeUI.confirm_cached_callback
|
|
192
|
+
).run
|
|
193
|
+
puts "#{theme}: #{runner.imported_count} imported, #{runner.error_count} errors"
|
|
194
|
+
failures += runner.error_count
|
|
195
|
+
puts
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
puts "All imports complete!"
|
|
199
|
+
exit 1 if failures.positive? && !ENV["IGNORE_ERRORS"]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
desc "Search for geographic divisions"
|
|
203
|
+
task :search, [:query] => :environment do |_t, args|
|
|
204
|
+
query = args[:query] or OvertureMaps::RakeUI.abort!(
|
|
205
|
+
"Usage: rails overture_maps:import:search[Seattle]"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
results = OvertureMaps::DivisionSearch.search(
|
|
209
|
+
query: query, release: OvertureMaps::RakeUI.release_arg
|
|
210
|
+
)
|
|
211
|
+
if results.empty?
|
|
212
|
+
puts "No divisions found matching '#{query}'"
|
|
213
|
+
else
|
|
214
|
+
puts "Results:"
|
|
215
|
+
OvertureMaps::RakeUI.print_divisions(results)
|
|
216
|
+
puts
|
|
217
|
+
puts "Tip: rails overture_maps:import:places[#{results.first[:name]}]"
|
|
218
|
+
end
|
|
219
|
+
rescue OvertureMaps::Error => e
|
|
220
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
desc "Show import statistics"
|
|
224
|
+
task stats: :environment do
|
|
225
|
+
OvertureMaps::RakeUI::IMPORT_THEMES.values.flat_map(&:values).uniq.each do |model_name|
|
|
226
|
+
count = begin
|
|
227
|
+
model_name.constantize.count
|
|
228
|
+
rescue StandardError
|
|
229
|
+
"N/A (run rails generate overture_maps:install && rails db:migrate)"
|
|
230
|
+
end
|
|
231
|
+
puts format(" %-24s %s", "#{model_name}:", count)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
namespace :download do
|
|
237
|
+
OvertureMaps::Import::Downloader::THEMES.each do |theme|
|
|
238
|
+
desc "Download #{theme} data by location name or bbox; without a location downloads complete theme files (large!)"
|
|
239
|
+
task theme.to_sym, [:location, :release, :output_dir] do |_t, args|
|
|
240
|
+
release = args[:release] || OvertureMaps::RakeUI.release_arg
|
|
241
|
+
downloader = OvertureMaps::Import::Downloader.new(
|
|
242
|
+
theme: theme, release: release, output_dir: args[:output_dir]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
begin
|
|
246
|
+
if args[:location]
|
|
247
|
+
bbox = OvertureMaps::RakeUI.resolve_bbox(args[:location], release: release)
|
|
248
|
+
files = downloader.extract_bbox_all_types(bbox)
|
|
249
|
+
puts "\nDownloaded #{files.count} extract(s)"
|
|
250
|
+
else
|
|
251
|
+
puts "No location given — downloading ALL #{theme} files for #{downloader.release}."
|
|
252
|
+
puts "This can be very large; prefer overture_maps:download:#{theme}[<location>]."
|
|
253
|
+
count = downloader.download_theme_files
|
|
254
|
+
puts "\nDownloaded #{count} file(s)"
|
|
255
|
+
end
|
|
256
|
+
rescue OvertureMaps::Error => e
|
|
257
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
desc "Download data within a bounding box, e.g. rails overture_maps:download:bbox[places,47.6,-122.4,47.7,-122.2]"
|
|
263
|
+
task :bbox, [:theme, :lat1, :lng1, :lat2, :lng2, :type, :release, :output_dir, :format] do |_t, args|
|
|
264
|
+
unless args[:lat1] && args[:lng1] && args[:lat2] && args[:lng2]
|
|
265
|
+
OvertureMaps::RakeUI.abort!("Usage: rails overture_maps:download:bbox[theme,lat1,lng1,lat2,lng2]")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
bbox = OvertureMaps::BoundingBox.new(
|
|
269
|
+
lat1: args[:lat1], lng1: args[:lng1], lat2: args[:lat2], lng2: args[:lng2]
|
|
270
|
+
)
|
|
271
|
+
downloader = OvertureMaps::Import::Downloader.new(
|
|
272
|
+
theme: args[:theme] || "places", type: args[:type],
|
|
273
|
+
release: args[:release] || OvertureMaps::RakeUI.release_arg,
|
|
274
|
+
output_dir: args[:output_dir]
|
|
275
|
+
)
|
|
276
|
+
files = downloader.extract_bbox_all_types(bbox, format: args[:format] || "parquet")
|
|
277
|
+
puts "\nDownloaded #{files.count} extract(s)"
|
|
278
|
+
rescue OvertureMaps::Error, ArgumentError => e
|
|
279
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
desc "Download data near a point, e.g. rails overture_maps:download:nearby[places,47.6,-122.3,5000]"
|
|
283
|
+
task :nearby, [:theme, :lat, :lng, :radius, :type, :release, :output_dir] do |_t, args|
|
|
284
|
+
unless args[:lat] && args[:lng]
|
|
285
|
+
OvertureMaps::RakeUI.abort!("Usage: rails overture_maps:download:nearby[theme,lat,lng,radius_meters]")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
downloader = OvertureMaps::Import::Downloader.new(
|
|
289
|
+
theme: args[:theme] || "places", type: args[:type],
|
|
290
|
+
release: args[:release] || OvertureMaps::RakeUI.release_arg,
|
|
291
|
+
output_dir: args[:output_dir]
|
|
292
|
+
)
|
|
293
|
+
files = downloader.extract_nearby(
|
|
294
|
+
lat: args[:lat], lng: args[:lng],
|
|
295
|
+
radius_meters: (args[:radius] || 5000).to_i
|
|
296
|
+
)
|
|
297
|
+
puts "\nDownloaded #{files.count} extract(s)"
|
|
298
|
+
rescue OvertureMaps::Error, ArgumentError => e
|
|
299
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
desc "Search for geographic divisions"
|
|
303
|
+
task :search_divisions, [:query] do |_t, args|
|
|
304
|
+
query = args[:query] or OvertureMaps::RakeUI.abort!(
|
|
305
|
+
"Usage: rails overture_maps:download:search_divisions[query]"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
results = OvertureMaps::Import::Downloader.search_divisions(
|
|
309
|
+
query: query, release: OvertureMaps::RakeUI.release_arg
|
|
310
|
+
)
|
|
311
|
+
if results.empty?
|
|
312
|
+
puts "No divisions found matching '#{query}'"
|
|
313
|
+
else
|
|
314
|
+
puts "Results:"
|
|
315
|
+
OvertureMaps::RakeUI.print_divisions(results)
|
|
316
|
+
end
|
|
317
|
+
rescue OvertureMaps::Error => e
|
|
318
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
desc "List available Overture releases"
|
|
322
|
+
task :versions do
|
|
323
|
+
releases = OvertureMaps::Releases.all
|
|
324
|
+
puts "Available releases:"
|
|
325
|
+
releases.each_with_index do |release, i|
|
|
326
|
+
puts " - #{release}#{i.zero? ? " (latest)" : ""}"
|
|
327
|
+
end
|
|
328
|
+
rescue OvertureMaps::Error => e
|
|
329
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
desc "List available themes and their types"
|
|
333
|
+
task :themes do
|
|
334
|
+
OvertureMaps::Import::Downloader.themes_with_types.each do |theme, types|
|
|
335
|
+
puts " - #{theme}"
|
|
336
|
+
types.each { |t| puts " #{t}" }
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
desc "List types available for a theme in the current release"
|
|
341
|
+
task :types, [:theme] do |_t, args|
|
|
342
|
+
theme = args[:theme] || "places"
|
|
343
|
+
types = OvertureMaps::Import::Downloader.list_types(
|
|
344
|
+
theme: theme, release: OvertureMaps::RakeUI.release_arg
|
|
345
|
+
)
|
|
346
|
+
puts "Available types for #{theme}:"
|
|
347
|
+
types.each { |t| puts " - #{t}" }
|
|
348
|
+
rescue OvertureMaps::Error => e
|
|
349
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
desc "List files for a theme/type without downloading"
|
|
353
|
+
task :list, [:theme, :type, :release] do |_t, args|
|
|
354
|
+
downloader = OvertureMaps::Import::Downloader.new(
|
|
355
|
+
theme: args[:theme] || "places", type: args[:type],
|
|
356
|
+
release: args[:release] || OvertureMaps::RakeUI.release_arg
|
|
357
|
+
)
|
|
358
|
+
files = downloader.list_files
|
|
359
|
+
if files.empty?
|
|
360
|
+
puts "No files found"
|
|
361
|
+
else
|
|
362
|
+
files.each do |f|
|
|
363
|
+
puts " #{File.basename(f[:key])} (#{OvertureMaps::Util.format_size(f[:size])})"
|
|
364
|
+
end
|
|
365
|
+
puts "Total: #{files.count} file(s)"
|
|
366
|
+
end
|
|
367
|
+
rescue OvertureMaps::Error => e
|
|
368
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
desc "Sync imported areas to the latest (or given) Overture release"
|
|
373
|
+
task :sync, [:release] => :environment do |_t, args|
|
|
374
|
+
unless OvertureMaps::Models::ImportedArea.table_exists?
|
|
375
|
+
OvertureMaps::RakeUI.abort!(
|
|
376
|
+
"No sync tracking table. Run: rails generate overture_maps:install && rails db:migrate\n" \
|
|
377
|
+
"(Areas are tracked from their next import.)"
|
|
378
|
+
)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
syncer = OvertureMaps::Syncer.new(target_release: args[:release])
|
|
382
|
+
areas = OvertureMaps::Models::ImportedArea.count
|
|
383
|
+
if areas.zero?
|
|
384
|
+
puts "No imported areas tracked yet. Run an import first."
|
|
385
|
+
next
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
puts "Syncing #{areas} imported area(s) to #{syncer.target}..."
|
|
389
|
+
puts
|
|
390
|
+
|
|
391
|
+
failures = 0
|
|
392
|
+
syncer.sync_all.each do |result|
|
|
393
|
+
area = result.area
|
|
394
|
+
label = "#{area.theme}/#{area.feature_type} #{area.slug}"
|
|
395
|
+
case result.status
|
|
396
|
+
when :up_to_date
|
|
397
|
+
puts " #{label}: already at #{syncer.target}"
|
|
398
|
+
when :synced
|
|
399
|
+
puts " #{label}: synced (removed #{result.removed}, upserted #{result.imported}, errors #{result.errors})"
|
|
400
|
+
when :refreshed
|
|
401
|
+
puts " #{label}: fully refreshed (#{result.message})"
|
|
402
|
+
when :failed
|
|
403
|
+
puts " #{label}: FAILED — #{result.message}"
|
|
404
|
+
failures += 1
|
|
405
|
+
end
|
|
406
|
+
failures += 1 if result.errors.positive? && result.status != :failed
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
puts
|
|
410
|
+
puts failures.zero? ? "Sync complete." : "Sync finished with #{failures} failure(s)."
|
|
411
|
+
exit 1 if failures.positive? && !ENV["IGNORE_ERRORS"]
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
namespace :sync do
|
|
415
|
+
desc "Show tracked areas and their releases"
|
|
416
|
+
task status: :environment do
|
|
417
|
+
unless OvertureMaps::Models::ImportedArea.table_exists?
|
|
418
|
+
OvertureMaps::RakeUI.abort!("No sync tracking table. Run: rails generate overture_maps:install && rails db:migrate")
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
latest = OvertureMaps::Releases.latest
|
|
422
|
+
puts "Latest Overture release: #{latest}"
|
|
423
|
+
puts
|
|
424
|
+
|
|
425
|
+
areas = OvertureMaps::Models::ImportedArea.order(:theme, :feature_type, :slug)
|
|
426
|
+
if areas.none?
|
|
427
|
+
puts "No imported areas tracked yet."
|
|
428
|
+
else
|
|
429
|
+
areas.each do |area|
|
|
430
|
+
marker = area.release == latest ? " " : "! "
|
|
431
|
+
puts "#{marker}#{area.theme}/#{area.feature_type} #{area.slug}: " \
|
|
432
|
+
"#{area.release} (#{area.records_count} records)"
|
|
433
|
+
end
|
|
434
|
+
behind = areas.count { |a| a.release != latest }
|
|
435
|
+
puts
|
|
436
|
+
puts behind.zero? ? "All areas up to date." : "#{behind} area(s) behind — run rails overture_maps:sync"
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
namespace :gers do
|
|
442
|
+
desc "Look up a GERS id in the Overture registry"
|
|
443
|
+
task :lookup, [:id] do |_t, args|
|
|
444
|
+
id = args[:id] or OvertureMaps::RakeUI.abort!("Usage: rails overture_maps:gers:lookup[<gers-id>]")
|
|
445
|
+
|
|
446
|
+
begin
|
|
447
|
+
row = OvertureMaps::GERS.lookup(id)
|
|
448
|
+
if row
|
|
449
|
+
row.each { |key, value| puts format(" %-14s %s", "#{key}:", value) }
|
|
450
|
+
else
|
|
451
|
+
puts "Not found in the registry."
|
|
452
|
+
end
|
|
453
|
+
rescue ArgumentError => e
|
|
454
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
455
|
+
rescue OvertureMaps::Error => e
|
|
456
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
namespace :cache do
|
|
462
|
+
desc "List cached extracts"
|
|
463
|
+
task :list do
|
|
464
|
+
dir = OvertureMaps.configuration.cache_dir
|
|
465
|
+
files = Dir.glob(File.join(dir, "*.{parquet,geojson,geojsonseq,gpkg}")).sort
|
|
466
|
+
|
|
467
|
+
if files.empty?
|
|
468
|
+
puts "No cached extracts in #{dir}"
|
|
469
|
+
else
|
|
470
|
+
total = 0
|
|
471
|
+
files.each do |f|
|
|
472
|
+
size = File.size(f)
|
|
473
|
+
total += size
|
|
474
|
+
puts " #{File.basename(f)} (#{OvertureMaps::Util.format_size(size)})"
|
|
475
|
+
end
|
|
476
|
+
puts "Total: #{files.count} file(s), #{OvertureMaps::Util.format_size(total)}"
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
desc "Remove cached extracts (optionally matching a pattern)"
|
|
481
|
+
task :clear, [:pattern] do |_t, args|
|
|
482
|
+
dir = OvertureMaps.configuration.cache_dir
|
|
483
|
+
pattern = args[:pattern] ? "*#{args[:pattern]}*" : "*"
|
|
484
|
+
files = Dir.glob(File.join(dir, "#{pattern}.{parquet,geojson,geojsonseq,gpkg}"))
|
|
485
|
+
|
|
486
|
+
if files.empty?
|
|
487
|
+
puts "Nothing to remove"
|
|
488
|
+
else
|
|
489
|
+
files.each { |f| File.delete(f) }
|
|
490
|
+
puts "Removed #{files.count} file(s)"
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
namespace :categories do
|
|
496
|
+
CATEGORIES_CSV_URL = "https://raw.githubusercontent.com/OvertureMaps/schema/main/docs/schema/concepts/by-theme/places/overture_categories.csv"
|
|
497
|
+
|
|
498
|
+
desc "Populate the categories taxonomy from the Overture schema repo"
|
|
499
|
+
task populate: :environment do
|
|
500
|
+
require "csv"
|
|
501
|
+
|
|
502
|
+
begin
|
|
503
|
+
OvertureCategory
|
|
504
|
+
rescue NameError
|
|
505
|
+
OvertureMaps::RakeUI.abort!("OvertureCategory model not found. Run: rails generate overture_maps:install")
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
puts "Fetching categories from the Overture Maps taxonomy..."
|
|
509
|
+
body = OvertureMaps::Storage.get(CATEGORIES_CSV_URL)
|
|
510
|
+
|
|
511
|
+
count = 0
|
|
512
|
+
skipped = 0
|
|
513
|
+
CSV.parse(body, headers: true, col_sep: ";").each do |row|
|
|
514
|
+
name = row[0]&.strip
|
|
515
|
+
taxonomy_str = row[1]&.strip
|
|
516
|
+
|
|
517
|
+
if name.nil? || name.empty? || taxonomy_str.nil? || taxonomy_str.empty?
|
|
518
|
+
skipped += 1 unless name.nil? && taxonomy_str.nil?
|
|
519
|
+
next
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
taxonomy = taxonomy_str.gsub(/[\[\]]/, "").split(",").map(&:strip)
|
|
523
|
+
category = OvertureCategory.find_or_initialize_by(name: name)
|
|
524
|
+
category.update!(
|
|
525
|
+
primary_category: taxonomy.first,
|
|
526
|
+
hierarchy_level: taxonomy.size - 1,
|
|
527
|
+
taxonomy: taxonomy
|
|
528
|
+
)
|
|
529
|
+
count += 1
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
puts "Imported #{count} categories#{skipped.positive? ? " (skipped #{skipped} malformed rows)" : ""}."
|
|
533
|
+
puts "\nPrimary categories:"
|
|
534
|
+
OvertureCategory.primary_categories.each { |pc| puts " - #{pc}" }
|
|
535
|
+
rescue OvertureMaps::Error => e
|
|
536
|
+
OvertureMaps::RakeUI.abort!("Error: #{e.message}")
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
desc "List all categories grouped by primary category"
|
|
540
|
+
task list: :environment do
|
|
541
|
+
OvertureCategory.primary_categories.each do |pc|
|
|
542
|
+
puts " - #{pc}"
|
|
543
|
+
OvertureCategory.by_primary(pc).order(:hierarchy_level, :name).each do |cat|
|
|
544
|
+
puts " #{cat.name}"
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
desc "List primary categories only"
|
|
550
|
+
task primary: :environment do
|
|
551
|
+
OvertureCategory.primary_categories.each { |pc| puts pc }
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
end
|