git-fit 0.7.1 → 0.7.2
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 +4 -4
- data/lib/git-fit.rb +1 -0
- data/lib/git_fit/cli/geo_cli.rb +29 -0
- data/lib/git_fit/cli.rb +3 -0
- data/lib/git_fit/geo/amap_client.rb +55 -0
- data/lib/git_fit/geo/division_detector.rb +245 -0
- data/lib/git_fit/geo/nominatim_client.rb +57 -0
- data/lib/git_fit/geo/reverse_geocode.rb +48 -0
- data/lib/git_fit/geo.rb +4 -0
- data/lib/git_fit/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5d6722be96ccba39d45fd0484e446158b826fe858eb33223ab31c15a8020db8
|
|
4
|
+
data.tar.gz: d0947ed101afc498ef96d1826c5560b93288b36719d88cab324c52205197405c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aaf3ba7f83299545ec1958713baa75a4af80a4945a30d4d13480b47a09a670b137519e6cb275f2a423407e0f849483092d3b6aec04bf4f7af52783e202e1be28
|
|
7
|
+
data.tar.gz: 6591ac01cfc11b881c9b2d9c8168564c511a3da8a32e52523da6ab8c0d050482172d85b2435e89a3ed010db09f443a27e3b1cf69a3d94b272cde10e11de51e85
|
data/lib/git-fit.rb
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
|
|
3
|
+
module GitFit
|
|
4
|
+
class GeoCLI < Thor
|
|
5
|
+
desc "detect", "Detect administrative divisions for activities with GPS data"
|
|
6
|
+
option :limit, type: :numeric, aliases: "-l", desc: "Max activities to process"
|
|
7
|
+
option :batch, type: :numeric, aliases: "-b", desc: "Batch size", default: 20
|
|
8
|
+
option :strategy, type: :string, aliases: "-s", desc: "Strategy: proportional|start_end", default: "proportional"
|
|
9
|
+
option :time, type: :numeric, aliases: "-t", desc: "Time budget in seconds"
|
|
10
|
+
option :"dry-run", type: :boolean, desc: "Don't write to DB"
|
|
11
|
+
def detect
|
|
12
|
+
config = GitFit::Config.new
|
|
13
|
+
db = GitFit::DB::Connection.new(config.db_path).db
|
|
14
|
+
amap_key = ENV["AMAP_API_KEY"] || config.sync_config("amap")["api_key"]
|
|
15
|
+
detector = GitFit::Geo::DivisionDetector.new(
|
|
16
|
+
db: db,
|
|
17
|
+
amap_key: amap_key,
|
|
18
|
+
batch: options[:batch],
|
|
19
|
+
limit: options[:limit],
|
|
20
|
+
strategy: options[:strategy],
|
|
21
|
+
time_budget: options[:time],
|
|
22
|
+
dry_run: options[:"dry-run"],
|
|
23
|
+
)
|
|
24
|
+
detector.run
|
|
25
|
+
rescue => e
|
|
26
|
+
say_status :error, "Geo detection failed: #{e.message}", :red
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/git_fit/cli.rb
CHANGED
|
@@ -72,6 +72,9 @@ module GitFit
|
|
|
72
72
|
desc "gh SUBCOMMAND", "Manage GitHub secrets and variables"
|
|
73
73
|
subcommand "gh", GhCLI
|
|
74
74
|
|
|
75
|
+
desc "geo SUBCOMMAND", "Detect geographical administrative regions"
|
|
76
|
+
subcommand "geo", GeoCLI
|
|
77
|
+
|
|
75
78
|
desc "install SUBCOMMAND", "Install project assets (actions)"
|
|
76
79
|
subcommand "install", InstallCLI
|
|
77
80
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module GitFit
|
|
6
|
+
module Geo
|
|
7
|
+
class AMapClient
|
|
8
|
+
BASE_URL = "https://restapi.amap.com/v3/geocode/regeo".freeze
|
|
9
|
+
|
|
10
|
+
def initialize(api_key:)
|
|
11
|
+
@api_key = api_key
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@last_request = 0.0
|
|
14
|
+
@min_interval = 0.2
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reverse_geocode(lat, lng)
|
|
18
|
+
rate_limit
|
|
19
|
+
uri = URI(BASE_URL)
|
|
20
|
+
uri.query = URI.encode_www_form(
|
|
21
|
+
key: @api_key,
|
|
22
|
+
location: "#{lng},#{lat}",
|
|
23
|
+
output: "JSON",
|
|
24
|
+
radius: 1000,
|
|
25
|
+
extensions: "all",
|
|
26
|
+
)
|
|
27
|
+
resp = Net::HTTP.get_response(uri)
|
|
28
|
+
body = JSON.parse(resp.body)
|
|
29
|
+
if body["status"] != "1"
|
|
30
|
+
raise "AMap API error: #{body["info"]}"
|
|
31
|
+
end
|
|
32
|
+
parse_response(body)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def parse_response(body)
|
|
38
|
+
comp = body.dig("regeocode", "addressComponent") || {}
|
|
39
|
+
province = comp["province"].to_s.empty? ? nil : comp["province"]
|
|
40
|
+
city = comp["city"].to_s.empty? || comp["city"] == comp["province"] ? nil : comp["city"]
|
|
41
|
+
district = comp["district"].to_s.empty? ? nil : comp["district"]
|
|
42
|
+
township = comp["township"].to_s.empty? ? nil : comp["township"]
|
|
43
|
+
{ province: province, city: city, district: district, township: township, source: "amap" }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def rate_limit
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
elapsed = Time.now.to_f - @last_request
|
|
49
|
+
sleep(@min_interval - elapsed) if elapsed < @min_interval
|
|
50
|
+
@last_request = Time.now.to_f
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require_relative "reverse_geocode"
|
|
4
|
+
|
|
5
|
+
module GitFit
|
|
6
|
+
module Geo
|
|
7
|
+
class DivisionDetector
|
|
8
|
+
KEEP_PLACEHOLDER = "gqqrFurkeU??".freeze
|
|
9
|
+
SAMPLE_INTERVAL_DEG = 0.01
|
|
10
|
+
CACHE_DIR = "data/cache"
|
|
11
|
+
POINT_CACHE_FILE = File.join(CACHE_DIR, "geo-point.jsonl")
|
|
12
|
+
|
|
13
|
+
def initialize(db:, amap_key: nil, batch: 20, limit: nil, strategy: "proportional", time_budget: nil, dry_run: false)
|
|
14
|
+
@db = db
|
|
15
|
+
@geocoder = ReverseGeocode.new(amap_key: amap_key)
|
|
16
|
+
@amap_key = amap_key
|
|
17
|
+
@batch = batch
|
|
18
|
+
@limit = limit
|
|
19
|
+
@strategy = strategy
|
|
20
|
+
@time_budget = time_budget
|
|
21
|
+
@start_time = Time.now
|
|
22
|
+
@dry_run = dry_run
|
|
23
|
+
@processed = 0
|
|
24
|
+
@skipped = 0
|
|
25
|
+
@errors = 0
|
|
26
|
+
@attempted = 0
|
|
27
|
+
@total = 0
|
|
28
|
+
FileUtils.mkdir_p(CACHE_DIR)
|
|
29
|
+
load_caches
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run
|
|
33
|
+
$stdout.sync = true
|
|
34
|
+
activities = pending_activities
|
|
35
|
+
@total = activities.size
|
|
36
|
+
|
|
37
|
+
if @total > 0 && @amap_key.nil?
|
|
38
|
+
$stdout.puts "ERROR: AMAP_API_KEY not configured."
|
|
39
|
+
$stdout.puts "Set via: ENV[\"AMAP_API_KEY\"] or config.yml sync.amap.api_key"
|
|
40
|
+
$stdout.puts "Run again after configuring the key to process #{@total} pending activities."
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
$stdout.puts "Geo detection: #{@total} activities to process"
|
|
45
|
+
activities.each_slice(@batch) do |batch|
|
|
46
|
+
break if budget_exhausted?
|
|
47
|
+
process_batch(batch)
|
|
48
|
+
break if @limit && @processed >= @limit
|
|
49
|
+
end
|
|
50
|
+
print_summary
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def pending_activities
|
|
56
|
+
dataset = @db[:activities]
|
|
57
|
+
.where(Sequel.lit("summary_polyline IS NOT NULL"))
|
|
58
|
+
.where(Sequel.lit("summary_polyline != ''"))
|
|
59
|
+
.where(Sequel.lit("divisions IS NULL"))
|
|
60
|
+
.order(Sequel.desc(:start_date))
|
|
61
|
+
dataset = dataset.limit(@limit) if @limit
|
|
62
|
+
dataset.all
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def process_batch(batch)
|
|
66
|
+
@db.transaction do
|
|
67
|
+
batch.each do |activity|
|
|
68
|
+
break if budget_exhausted? || (@limit && @processed >= @limit)
|
|
69
|
+
process_one(activity)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def process_one(activity)
|
|
75
|
+
run_id = activity[:run_id]
|
|
76
|
+
polyline = activity[:summary_polyline]
|
|
77
|
+
|
|
78
|
+
if polyline.nil? || polyline.empty? || polyline == KEEP_PLACEHOLDER
|
|
79
|
+
mark_empty_divisions(run_id) unless @dry_run
|
|
80
|
+
@skipped += 1
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
points = decode_points(polyline)
|
|
85
|
+
unless points && points.size >= 2
|
|
86
|
+
mark_empty_divisions(run_id) unless @dry_run
|
|
87
|
+
@skipped += 1
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
samples = sample_points(points)
|
|
92
|
+
if samples.empty?
|
|
93
|
+
mark_empty_divisions(run_id) unless @dry_run
|
|
94
|
+
@skipped += 1
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@attempted += 1
|
|
99
|
+
prefix = "[#{@attempted}/#{@total}]"
|
|
100
|
+
|
|
101
|
+
divisions = resolve_divisions(samples)
|
|
102
|
+
if divisions.empty?
|
|
103
|
+
$stdout.puts " #{prefix} #{run_id} → skipped (no divisions resolved)"
|
|
104
|
+
@skipped += 1
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
summary = build_summary(divisions, samples.size)
|
|
109
|
+
update_db(run_id, summary) unless @dry_run
|
|
110
|
+
$stdout.puts " #{prefix} #{run_id} → #{format_divisions(divisions)} (#{samples.size} samples)"
|
|
111
|
+
@processed += 1
|
|
112
|
+
rescue => e
|
|
113
|
+
$stdout.puts " #{prefix} #{run_id} → error: #{e.message}"
|
|
114
|
+
@errors += 1
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def format_divisions(divisions)
|
|
118
|
+
divisions.map { |d| [d[:province], d[:city], d[:district], d[:township]].compact.reject(&:empty?).join(" ") }.join(" / ")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def decode_points(polyline)
|
|
122
|
+
GitFit::Geo::Polyline.decode(polyline)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def sample_points(points)
|
|
126
|
+
samples = []
|
|
127
|
+
last_lat = nil
|
|
128
|
+
last_lng = nil
|
|
129
|
+
points.each do |lat, lng|
|
|
130
|
+
if last_lat.nil? ||
|
|
131
|
+
(lat - last_lat).abs >= SAMPLE_INTERVAL_DEG ||
|
|
132
|
+
(lng - last_lng).abs >= SAMPLE_INTERVAL_DEG
|
|
133
|
+
samples << [lat.round(4), lng.round(4)]
|
|
134
|
+
last_lat = lat
|
|
135
|
+
last_lng = lng
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
samples
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def resolve_divisions(samples)
|
|
142
|
+
key_counts = Hash.new(0)
|
|
143
|
+
samples.each do |lat, lng|
|
|
144
|
+
coord_key = "#{lat},#{lng}"
|
|
145
|
+
raw = lookup_point_cache(coord_key)
|
|
146
|
+
unless raw
|
|
147
|
+
raw = @geocoder.lookup(lat, lng)
|
|
148
|
+
save_point_cache(coord_key, raw) if raw[:province]
|
|
149
|
+
end
|
|
150
|
+
result = normalize_entry(raw)
|
|
151
|
+
next unless result
|
|
152
|
+
key = division_key(result)
|
|
153
|
+
key_counts[key] += 1
|
|
154
|
+
end
|
|
155
|
+
key_counts.map do |key, count|
|
|
156
|
+
province, city, district, township = key.split("|")
|
|
157
|
+
{ province: province, city: city, district: district, township: township, count: count }
|
|
158
|
+
end.sort_by { |r| -r[:count] }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def normalize_entry(raw)
|
|
162
|
+
return nil unless raw
|
|
163
|
+
province = raw[:province] || raw["p"] || raw["province"]
|
|
164
|
+
province = nil if province.is_a?(Array) || province.to_s.empty? || province == "中华人民共和国"
|
|
165
|
+
return nil unless province
|
|
166
|
+
{
|
|
167
|
+
province: province,
|
|
168
|
+
city: pick_field(raw, :city, "city", "c", "city") { |v| valid_name?(v) },
|
|
169
|
+
district: pick_field(raw, :district, "dist", "d"),
|
|
170
|
+
township: pick_field(raw, :township, "town", "t"),
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def pick_field(raw, *keys)
|
|
175
|
+
keys.each do |k|
|
|
176
|
+
v = raw[k] || raw[k.to_s]
|
|
177
|
+
return v if block_given? ? yield(v) : v.is_a?(String) && !v.empty?
|
|
178
|
+
end
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def valid_name?(v)
|
|
183
|
+
v.is_a?(String) && !v.empty? && !v.match?(/^-?\d+\.\d+,-?\d+\.\d+$/)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def division_key(result)
|
|
187
|
+
[result[:province], result[:city], result[:district], result[:township]].join("|")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def build_summary(divisions, total_points)
|
|
191
|
+
total = total_points.to_f
|
|
192
|
+
division_list = divisions.map do |r|
|
|
193
|
+
h = { p: r[:province] }
|
|
194
|
+
h[:c] = r[:city] if r[:city]
|
|
195
|
+
h[:d] = r[:district] if r[:district]
|
|
196
|
+
h[:t] = r[:township] if r[:township]
|
|
197
|
+
h[:w] = (r[:count] / total).round(4)
|
|
198
|
+
h[:n] = r[:count]
|
|
199
|
+
h
|
|
200
|
+
end
|
|
201
|
+
{ divisions: division_list, total_sampled: total_points, source: "amap", processed_at: Time.now.utc.iso8601 }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def lookup_point_cache(coord_key)
|
|
205
|
+
return nil unless File.exist?(POINT_CACHE_FILE)
|
|
206
|
+
File.open(POINT_CACHE_FILE, "r") do |f|
|
|
207
|
+
f.each_line do |line|
|
|
208
|
+
entry = JSON.parse(line) rescue next
|
|
209
|
+
return entry if entry["ck"] == coord_key
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def save_point_cache(coord_key, result)
|
|
216
|
+
entry = { ck: coord_key, p: result[:province], source: result[:source] }
|
|
217
|
+
entry[:city] = result[:city] if result[:city]
|
|
218
|
+
entry[:dist] = result[:district] if result[:district]
|
|
219
|
+
entry[:town] = result[:township] if result[:township]
|
|
220
|
+
File.open(POINT_CACHE_FILE, "a") { |f| f.puts(JSON.generate(entry)) }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def update_db(run_id, summary)
|
|
224
|
+
@db[:activities].where(run_id: run_id).update(divisions: JSON.generate(summary[:divisions]))
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def mark_empty_divisions(run_id)
|
|
228
|
+
@db[:activities].where(run_id: run_id).update(divisions: "[]")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def load_caches
|
|
232
|
+
# Preload: nothing needed, read on-demand
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def budget_exhausted?
|
|
236
|
+
return false unless @time_budget
|
|
237
|
+
Time.now - @start_time > @time_budget
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def print_summary
|
|
241
|
+
$stdout.puts "Geo detection complete: #{@processed} processed, #{@skipped} skipped, #{@errors} errors"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module GitFit
|
|
6
|
+
module Geo
|
|
7
|
+
class NominatimClient
|
|
8
|
+
BASE_URL = "https://nominatim.openstreetmap.org/reverse".freeze
|
|
9
|
+
USER_AGENT = "git-fit/1.0 (geo-detector)"
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@last_request = 0.0
|
|
14
|
+
@min_interval = 1.0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reverse_geocode(lat, lng)
|
|
18
|
+
rate_limit
|
|
19
|
+
uri = URI(BASE_URL)
|
|
20
|
+
uri.query = URI.encode_www_form(
|
|
21
|
+
lat: lat,
|
|
22
|
+
lon: lng,
|
|
23
|
+
format: "jsonv2",
|
|
24
|
+
addressdetails: 1,
|
|
25
|
+
zoom: 10,
|
|
26
|
+
)
|
|
27
|
+
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
28
|
+
req = Net::HTTP::Get.new(uri)
|
|
29
|
+
req["User-Agent"] = USER_AGENT
|
|
30
|
+
http.request(req)
|
|
31
|
+
end
|
|
32
|
+
body = JSON.parse(resp.body)
|
|
33
|
+
if body["error"]
|
|
34
|
+
raise "Nominatim error: #{body["error"]}"
|
|
35
|
+
end
|
|
36
|
+
parse_response(body)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def parse_response(body)
|
|
42
|
+
addr = body["address"] || {}
|
|
43
|
+
country = addr["country"]
|
|
44
|
+
state = addr["state"]
|
|
45
|
+
{ province: country, city: state, district: nil, township: nil, source: "nominatim" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def rate_limit
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
elapsed = Time.now.to_f - @last_request
|
|
51
|
+
sleep(@min_interval - elapsed) if elapsed < @min_interval
|
|
52
|
+
@last_request = Time.now.to_f
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative "coord_transform"
|
|
2
|
+
require_relative "amap_client"
|
|
3
|
+
require_relative "nominatim_client"
|
|
4
|
+
|
|
5
|
+
module GitFit
|
|
6
|
+
module Geo
|
|
7
|
+
class ReverseGeocode
|
|
8
|
+
CHINA_BBOX = {
|
|
9
|
+
min_lng: 73, max_lng: 135,
|
|
10
|
+
min_lat: 18, max_lat: 54,
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(amap_key: nil)
|
|
14
|
+
@amap = amap_key ? AMapClient.new(api_key: amap_key) : nil
|
|
15
|
+
@nominatim = NominatimClient.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def amap_available?
|
|
19
|
+
!@amap.nil?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def lookup(lat, lng)
|
|
23
|
+
in_china?(lat, lng) ? lookup_china(lat, lng) : lookup_intl(lat, lng)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def in_china?(lat, lng)
|
|
29
|
+
lat >= CHINA_BBOX[:min_lat] && lat <= CHINA_BBOX[:max_lat] &&
|
|
30
|
+
lng >= CHINA_BBOX[:min_lng] && lng <= CHINA_BBOX[:max_lng]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def lookup_china(lat, lng)
|
|
34
|
+
raise "AMap API key not configured" unless @amap
|
|
35
|
+
gcj_lat, gcj_lng = CoordTransform.wgs84_to_gcj02(lat, lng)
|
|
36
|
+
@amap.reverse_geocode(gcj_lat, gcj_lng)
|
|
37
|
+
rescue => e
|
|
38
|
+
{ province: nil, city: nil, district: nil, township: nil, source: "error", error: e.message }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def lookup_intl(lat, lng)
|
|
42
|
+
@nominatim.reverse_geocode(lat, lng)
|
|
43
|
+
rescue => e
|
|
44
|
+
{ province: nil, city: nil, district: nil, township: nil, source: "error", error: e.message }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/git_fit/geo.rb
CHANGED
data/lib/git_fit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: git-fit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lax
|
|
@@ -190,6 +190,7 @@ files:
|
|
|
190
190
|
- lib/git-fit.rb
|
|
191
191
|
- lib/git_fit/cli.rb
|
|
192
192
|
- lib/git_fit/cli/export.rb
|
|
193
|
+
- lib/git_fit/cli/geo_cli.rb
|
|
193
194
|
- lib/git_fit/cli/gh_cli.rb
|
|
194
195
|
- lib/git_fit/cli/import_cli.rb
|
|
195
196
|
- lib/git_fit/cli/install_cli.rb
|
|
@@ -206,8 +207,12 @@ files:
|
|
|
206
207
|
- lib/git_fit/fit.rb
|
|
207
208
|
- lib/git_fit/fit/decoder.rb
|
|
208
209
|
- lib/git_fit/geo.rb
|
|
210
|
+
- lib/git_fit/geo/amap_client.rb
|
|
209
211
|
- lib/git_fit/geo/coord_transform.rb
|
|
212
|
+
- lib/git_fit/geo/division_detector.rb
|
|
213
|
+
- lib/git_fit/geo/nominatim_client.rb
|
|
210
214
|
- lib/git_fit/geo/polyline.rb
|
|
215
|
+
- lib/git_fit/geo/reverse_geocode.rb
|
|
211
216
|
- lib/git_fit/import.rb
|
|
212
217
|
- lib/git_fit/import/apple_health.rb
|
|
213
218
|
- lib/git_fit/import/local_file.rb
|