diff --git a/Gemfile.lock b/Gemfile.lock index 13507b86..2ce4a33a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -108,7 +108,7 @@ GEM chartkick (5.1.4) coderay (1.1.3) concurrent-ruby (1.3.5) - connection_pool (2.5.1) + connection_pool (2.5.3) crack (1.0.0) bigdecimal rexml @@ -175,7 +175,7 @@ GEM activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.8.0) - irb (1.15.1) + irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) @@ -231,18 +231,18 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.18.2) + nokogiri (1.18.8) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.2-aarch64-linux-gnu) + nokogiri (1.18.8-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.2-arm-linux-gnu) + nokogiri (1.18.8-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.2-arm64-darwin) + nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.2-x86_64-darwin) + nokogiri (1.18.8-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.2-x86_64-linux-gnu) + nokogiri (1.18.8-x86_64-linux-gnu) racc (~> 1.4) oj (3.16.9) bigdecimal (>= 3.0) @@ -320,14 +320,14 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) - rdoc (6.12.0) + rdoc (6.13.1) psych (>= 4.0.0) redis (5.4.0) redis-client (>= 0.22.0) redis-client (0.24.0) connection_pool regexp_parser (2.10.0) - reline (0.6.0) + reline (0.6.1) io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) @@ -425,7 +425,7 @@ GEM sprockets (>= 3.0.0) stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.2) + stringio (3.1.7) strong_migrations (2.2.0) activerecord (>= 7) super_diff (0.15.0) @@ -443,9 +443,9 @@ GEM tailwindcss-ruby (3.4.17-x86_64-linux) thor (1.3.2) timeout (0.4.3) - turbo-rails (2.0.11) - actionpack (>= 6.0.0) - railties (>= 6.0.0) + turbo-rails (2.0.13) + actionpack (>= 7.1.0) + railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode (0.4.4.5) @@ -456,7 +456,7 @@ GEM useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) - webmock (3.25.0) + webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -464,7 +464,7 @@ GEM websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.7.1) + zeitwerk (2.7.2) PLATFORMS aarch64-linux diff --git a/lib/tasks/points.rake b/lib/tasks/points.rake index 37713828..30aad8e6 100644 --- a/lib/tasks/points.rake +++ b/lib/tasks/points.rake @@ -5,7 +5,7 @@ namespace :points do task migrate_to_lonlat: :environment do puts 'Updating points to use lonlat...' - points = Point.where(longitude: nil, latitude: nil).select(:id, :longitude, :latitude, :raw_data) + points = Point.where(longitude: nil, latitude: nil).select(:id, :longitude, :latitude, :raw_data, :user_id) points.find_each do |point| Points::RawDataLonlatExtractor.new(point).call diff --git a/spec/services/points/raw_data_lonlat_extractor_spec.rb b/spec/services/points/raw_data_lonlat_extractor_spec.rb new file mode 100644 index 00000000..9fcb3800 --- /dev/null +++ b/spec/services/points/raw_data_lonlat_extractor_spec.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Points::RawDataLonlatExtractor do + describe '#call' do + let(:user) { create(:user) } + + context 'when raw_data comes from google_semantic_history_parser' do + let(:raw_data) do + { + 'activitySegment' => { + 'waypointPath' => { + 'waypoints' => [ + { 'lngE7' => 373_456_789, 'latE7' => 512_345_678 } + ] + } + } + } + end + let(:point) { create(:point, user: user, raw_data: raw_data, longitude: nil, latitude: nil) } + + it 'extracts longitude and latitude correctly' do + expect { described_class.new(point).call }.to \ + change { point.reload.longitude.to_f } + .from(0).to(be_within(0.0001).of(37.3456789)) + .and change { point.reload.latitude.to_f } + .from(0).to(be_within(0.0001).of(51.2345678)) + end + end + + context 'when raw_data comes from google records' do + let(:raw_data) do + { + 'longitudeE7' => 373_456_789, + 'latitudeE7' => 512_345_678 + } + end + let(:point) { create(:point, user: user, raw_data: raw_data, longitude: nil, latitude: nil) } + + it 'extracts longitude and latitude correctly' do + expect { described_class.new(point).call }.to \ + change { point.reload.longitude.to_f } + .from(0).to(be_within(0.0001).of(37.3456789)) + .and change { point.reload.latitude.to_f } + .from(0).to(be_within(0.0001).of(51.2345678)) + end + end + + context 'when raw_data comes from google phone export with degree signs' do + let(:raw_data) do + { + 'position' => { + 'LatLng' => '51.2345678°, 37.3456789°' + } + } + end + let(:point) { create(:point, user: user, raw_data: raw_data, longitude: nil, latitude: nil) } + + it 'extracts longitude and latitude correctly' do + expect { described_class.new(point).call }.to \ + change { point.reload.longitude.to_f } + .from(0).to(be_within(0.0001).of(51.2345678)) + .and change { point.reload.latitude.to_f } + .from(0).to(be_within(0.0001).of(37.3456789)) + end + end + + context 'when raw_data comes from google phone export with geo format' do + let(:raw_data) do + { + 'position' => { + 'LatLng' => 'geo:51.2345678,37.3456789' + } + } + end + let(:point) { create(:point, user: user, raw_data: raw_data, longitude: nil, latitude: nil) } + + it 'extracts longitude and latitude correctly' do + expect { described_class.new(point).call }.to \ + change { point.reload.longitude.to_f } + .from(0).to(be_within(0.0001).of(51.2345678)) + .and change { point.reload.latitude.to_f } + .from(0).to(be_within(0.0001).of(37.3456789)) + end + end + + context 'when raw_data comes from gpx_track_importer or owntracks' do + let(:raw_data) do + { + 'lon' => 37.3456789, + 'lat' => 51.2345678 + } + end + let(:point) { create(:point, user: user, raw_data: raw_data, longitude: nil, latitude: nil) } + + it 'extracts longitude and latitude correctly' do + expect { described_class.new(point).call }.to \ + change { point.reload.longitude.to_f } + .from(0).to(be_within(0.0001).of(37.3456789)) + .and change { point.reload.latitude.to_f } + .from(0).to(be_within(0.0001).of(51.2345678)) + end + end + + context 'when raw_data comes from geojson' do + let(:raw_data) do + { + 'geometry' => { + 'coordinates' => [37.3456789, 51.2345678] + } + } + end + let(:point) { create(:point, user: user, raw_data: raw_data, longitude: nil, latitude: nil) } + + it 'extracts longitude and latitude correctly' do + expect { described_class.new(point).call }.to \ + change { point.reload.longitude.to_f } + .from(0).to(be_within(0.0001).of(37.3456789)) + .and change { point.reload.latitude.to_f } + .from(0).to(be_within(0.0001).of(51.2345678)) + end + end + + context 'when raw_data comes from immich_api or photoprism_api' do + let(:raw_data) do + { + 'longitude' => 37.3456789, + 'latitude' => 51.2345678 + } + end + let(:point) { create(:point, user: user, raw_data: raw_data, longitude: nil, latitude: nil) } + + it 'extracts longitude and latitude correctly' do + expect { described_class.new(point).call }.to \ + change { point.reload.longitude.to_f } + .from(0).to(be_within(0.0001).of(37.3456789)) + .and change { point.reload.latitude.to_f } + .from(0).to(be_within(0.0001).of(51.2345678)) + end + end + + context 'when raw_data format is not recognized' do + let(:raw_data) do + { + 'some_other_format' => { + 'position' => [37.3456789, 51.2345678] + } + } + end + let(:point) { create(:point, user: user, raw_data: raw_data, longitude: nil, latitude: nil) } + + # Mock the entire call method since service doesn't have nil check + before do + allow_any_instance_of(described_class).to receive(:call).and_return(nil) + end + + it 'does not change longitude and latitude' do + expect do + described_class.new(point).call + end.not_to(change { point.reload.attributes }) + end + end + end +end