Skip to content

Commit 0501c15

Browse files
authored
Merge pull request #1177 from Freika/feature/store-geodata
Feature/store geodata
2 parents 20fb0bb + 108239f commit 0501c15

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1144
-373
lines changed

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,38 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

77

8-
# 0.26.1 - 2025-05-12
8+
# 0.26.1 - 2025-05-15
9+
10+
## Geodata on demand
11+
12+
This release introduces a new environment variable `STORE_GEODATA` to control whether to store geodata in the database.
13+
14+
When `STORE_GEODATA` is disabled, each feature that uses geodata will now make a direct request to the geocoding service to calculate required data.
15+
16+
Geodata is being used:
17+
18+
- Fetching places geodata
19+
- Fetching countries for a trip
20+
- Suggesting place name for a visit
21+
22+
If you prefer to keep the old behavior, you can set `STORE_GEODATA` to `true`. By default, starting this release, it's set to `false`.
23+
24+
If you're running your own Photon instance, you can safely set `STORE_GEODATA` to `false`, otherwise it'd be better to keep it enabled, because that way Dawarich will be using existing geodata for its calculations.
25+
26+
## Added
27+
28+
- Map page now has a button to go to the previous and next day. #296 #631 #904
29+
30+
## Changed
31+
32+
- Reverse geocoding is now working as on-demand job instead of storing the result in the database.
33+
- Stats cards now show the last update time. #733
934

1035
## Fixed
1136

1237
- Fixed a bug with an attempt to write points with same lonlat and timestamp from iOS app. #1170
1338
- Importing GeoJSON files now saves velocity if it was stored in either `velocity` or `speed` property.
39+
- `rake points:migrate_to_lonlat` should work properly now. #1083 #1161
1440

1541

1642
# 0.26.0 - 2025-05-08

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ gem 'rails', '~> 8.0'
3030
gem 'rexml'
3131
gem 'rgeo'
3232
gem 'rgeo-activerecord'
33+
gem 'rgeo-geojson'
3334
gem 'rswag-api'
3435
gem 'rswag-ui'
3536
gem 'sentry-ruby'

Gemfile.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ GEM
219219
mini_portile2 (2.8.8)
220220
minitest (5.25.5)
221221
msgpack (1.7.3)
222+
multi_json (1.15.0)
222223
multi_xml (0.7.1)
223224
bigdecimal (~> 3.1)
224225
net-imap (0.5.8)
@@ -339,6 +340,9 @@ GEM
339340
rgeo-activerecord (8.0.0)
340341
activerecord (>= 7.0)
341342
rgeo (>= 3.0)
343+
rgeo-geojson (2.2.0)
344+
multi_json (~> 1.15)
345+
rgeo (>= 1.0.0)
342346
rspec-core (3.13.3)
343347
rspec-support (~> 3.13.0)
344348
rspec-expectations (3.13.3)
@@ -513,6 +517,7 @@ DEPENDENCIES
513517
rexml
514518
rgeo
515519
rgeo-activerecord
520+
rgeo-geojson
516521
rspec-rails
517522
rswag-api
518523
rswag-specs

app/assets/builds/tailwind.css

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controllers/stats_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class StatsController < ApplicationController
55
before_action :authenticate_active_user!, only: %i[update update_all]
66

77
def index
8-
@stats = current_user.stats.group_by(&:year).sort.reverse
8+
@stats = current_user.stats.group_by(&:year).transform_values { |stats| stats.sort_by(&:updated_at).reverse }.sort.reverse
99
@points_total = current_user.tracked_points.count
1010
@points_reverse_geocoded = current_user.total_reverse_geocoded_points
1111
@points_reverse_geocoded_without_data = current_user.total_reverse_geocoded_points_without_data

app/jobs/trips/create_path_job.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class Trips::CreatePathJob < ApplicationJob
66
def perform(trip_id)
77
trip = Trip.find(trip_id)
88

9-
trip.calculate_path_and_distance
9+
trip.calculate_trip_data
1010

1111
trip.save!
1212
end

app/models/place.rb

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,19 @@ def lat
2222
lonlat.y
2323
end
2424

25-
def async_reverse_geocode
26-
return unless DawarichSettings.reverse_geocoding_enabled?
27-
28-
ReverseGeocodingJob.perform_later(self.class.to_s, id)
29-
end
30-
31-
def reverse_geocoded?
32-
geodata.present?
33-
end
34-
3525
def osm_id
36-
geodata['properties']['osm_id']
26+
geodata.dig('properties', 'osm_id')
3727
end
3828

3929
def osm_key
40-
geodata['properties']['osm_key']
30+
geodata.dig('properties', 'osm_key')
4131
end
4232

4333
def osm_value
44-
geodata['properties']['osm_value']
34+
geodata.dig('properties', 'osm_value')
4535
end
4636

4737
def osm_type
48-
geodata['properties']['osm_type']
38+
geodata.dig('properties', 'osm_type')
4939
end
5040
end

app/models/point.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Point < ApplicationRecord
2828
scope :visited, -> { where.not(visit_id: nil) }
2929
scope :not_visited, -> { where(visit_id: nil) }
3030

31-
after_create :async_reverse_geocode
31+
after_create :async_reverse_geocode, if: -> { DawarichSettings.store_geodata? }
3232
after_create_commit :broadcast_coordinates
3333

3434
def self.without_raw_data

app/models/trip.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ class Trip < ApplicationRecord
77

88
validates :name, :started_at, :ended_at, presence: true
99

10-
before_save :calculate_path_and_distance
10+
before_save :calculate_trip_data
1111

12-
def calculate_path_and_distance
12+
def calculate_trip_data
1313
calculate_path
1414
calculate_distance
15+
calculate_countries
1516
end
1617

1718
def points
1819
user.tracked_points.where(timestamp: started_at.to_i..ended_at.to_i).order(:timestamp)
1920
end
2021

2122
def countries
22-
points.pluck(:country).uniq.compact
23+
return points.pluck(:country).uniq.compact if DawarichSettings.store_geodata?
24+
25+
visited_countries
2326
end
2427

2528
def photo_previews
@@ -56,4 +59,10 @@ def calculate_distance
5659

5760
self.distance = distance.round
5861
end
62+
63+
def calculate_countries
64+
countries = Trips::Countries.new(self).call
65+
66+
self.visited_countries = countries
67+
end
5968
end

app/models/visit.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ class Visit < ApplicationRecord
1212

1313
enum :status, { suggested: 0, confirmed: 1, declined: 2 }
1414

15-
def reverse_geocoded?
16-
place.geodata.present?
17-
end
18-
1915
def coordinates
2016
points.pluck(:latitude, :longitude).map { [_1[0].to_f, _1[1].to_f] }
2117
end

app/serializers/api/place_serializer.rb

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ def initialize(place)
77

88
def call
99
{
10-
id: place.id,
11-
name: place.name,
12-
longitude: place.lon,
13-
latitude: place.lat,
14-
city: place.city,
15-
country: place.country,
16-
source: place.source,
17-
geodata: place.geodata,
10+
id: place.id,
11+
name: place.name,
12+
longitude: place.lon,
13+
latitude: place.lat,
14+
city: place.city,
15+
country: place.country,
16+
source: place.source,
17+
geodata: place.geodata,
18+
created_at: place.created_at,
19+
updated_at: place.updated_at,
1820
reverse_geocoded_at: place.reverse_geocoded_at
1921
}
2022
end

app/services/jobs/create.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ def call
2121
raise InvalidJobName, 'Invalid job name'
2222
end
2323

24-
points.find_each(batch_size: 1_000) do |point|
25-
point.async_reverse_geocode
26-
end
24+
points.find_each(&:async_reverse_geocode)
2725
end
2826
end

app/services/reverse_geocoding/places/fetch_data.rb

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,19 @@ def call
1717
return
1818
end
1919

20-
first_place = reverse_geocoded_places.shift
20+
places = reverse_geocoded_places
21+
first_place = places.shift
2122
update_place(first_place)
2223

23-
reverse_geocoded_places.each { |reverse_geocoded_place| fetch_and_create_place(reverse_geocoded_place) }
24+
# Extract all osm_ids for preloading
25+
osm_ids = places.map { |place| place.data['properties']['osm_id'].to_s }
26+
27+
# Preload all existing places with these osm_ids in a single query
28+
existing_places = Place.where("geodata->'properties'->>'osm_id' IN (?)", osm_ids)
29+
.index_by { |p| p.geodata.dig('properties', 'osm_id').to_s }
30+
31+
# Process with preloaded data
32+
places.each { |reverse_geocoded_place| fetch_and_create_place(reverse_geocoded_place, existing_places) }
2433
end
2534

2635
private
@@ -41,9 +50,9 @@ def update_place(reverse_geocoded_place)
4150
)
4251
end
4352

44-
def fetch_and_create_place(reverse_geocoded_place)
53+
def fetch_and_create_place(reverse_geocoded_place, existing_places = nil)
4554
data = reverse_geocoded_place.data
46-
new_place = find_place(data)
55+
new_place = find_place(data, existing_places)
4756

4857
new_place.name = place_name(data)
4958
new_place.city = data['properties']['city']
@@ -57,16 +66,17 @@ def fetch_and_create_place(reverse_geocoded_place)
5766
new_place.save!
5867
end
5968

60-
def reverse_geocoded?
61-
place.geodata.present?
62-
end
69+
def find_place(place_data, existing_places = nil)
70+
osm_id = place_data['properties']['osm_id'].to_s
6371

64-
def find_place(place_data)
65-
found_place = Place.where(
66-
"geodata->'properties'->>'osm_id' = ?", place_data['properties']['osm_id'].to_s
67-
).first
68-
69-
return found_place if found_place.present?
72+
# Use the preloaded data if available
73+
if existing_places
74+
return existing_places[osm_id] if existing_places[osm_id].present?
75+
else
76+
# Fall back to individual query if no preloaded data
77+
found_place = Place.where("geodata->'properties'->>'osm_id' = ?", osm_id).first
78+
return found_place if found_place.present?
79+
end
7080

7181
Place.find_or_initialize_by(
7282
lonlat: "POINT(#{place_data['geometry']['coordinates'][0].to_f.round(5)} #{place_data['geometry']['coordinates'][1].to_f.round(5)})",

app/services/trips/countries.rb

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# frozen_string_literal: true
2+
3+
class Trips::Countries
4+
FILE_PATH = Rails.root.join('lib/assets/countries.json')
5+
6+
def initialize(trip, batch_count = 2)
7+
@trip = trip
8+
@batch_count = batch_count
9+
@factory = RGeo::Geographic.spherical_factory
10+
@file = File.read(FILE_PATH)
11+
@countries_features =
12+
RGeo::GeoJSON.decode(@file, json_parser: :json, geo_factory: @factory)
13+
end
14+
15+
def call
16+
all_points = @trip.points.to_a
17+
total_points = all_points.size
18+
19+
# Return empty hash if no points
20+
return {} if total_points.zero?
21+
22+
batches = split_into_batches(all_points, @batch_count)
23+
threads_results = process_batches_in_threads(batches, total_points)
24+
25+
merge_thread_results(threads_results).uniq.compact
26+
end
27+
28+
private
29+
30+
def split_into_batches(points, batch_count)
31+
batch_count = [batch_count, 1].max # Ensure batch_count is at least 1
32+
batch_size = (points.size / batch_count.to_f).ceil
33+
points.each_slice(batch_size).to_a
34+
end
35+
36+
def process_batches_in_threads(batches, total_points)
37+
threads_results = []
38+
threads = []
39+
40+
batches.each do |batch|
41+
threads << Thread.new do
42+
threads_results << process_batch(batch)
43+
end
44+
end
45+
46+
threads.each(&:join)
47+
threads_results
48+
end
49+
50+
def merge_thread_results(threads_results)
51+
countries = []
52+
53+
threads_results.each do |result|
54+
countries.concat(result)
55+
end
56+
57+
countries
58+
end
59+
60+
def process_batch(points)
61+
points.map do |point|
62+
country_code = geocode_point(point)
63+
next unless country_code
64+
65+
country_code
66+
end
67+
end
68+
69+
def geocode_point(point)
70+
lonlat = point.lonlat
71+
return nil unless lonlat
72+
73+
latitude = lonlat.y
74+
longitude = lonlat.x
75+
76+
fetch_country_code(latitude, longitude)
77+
end
78+
79+
def fetch_country_code(latitude, longitude)
80+
results = Geocoder.search([latitude, longitude], limit: 1)
81+
return nil unless results.any?
82+
83+
result = results.first
84+
result.data['properties']['countrycode']
85+
rescue StandardError => e
86+
Rails.logger.error("Error geocoding point: #{e.message}")
87+
88+
ExceptionReporter.call(e)
89+
90+
nil
91+
end
92+
end

0 commit comments

Comments
 (0)