diff --git a/.gitignore b/.gitignore index 9583fb0a..b3a85915 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,5 @@ .ash_history .cache/ .dotnet/ +.cursorrules +.cursormemory.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 61cec6c5..3fbd7518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -# 0.24.2 - 2025-02-15 +# 0.24.2 - 2025-02-24 ## Added @@ -12,8 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Fixed -- Fixed a bug where background jobs to import Immich and Photoprism geolocation data data could not be created by non-admin users. -- Fixed a bug where upon point deletion there was an error it was not being removed from the map, while it was actually deleted from the database. #883 +- Fixed a bug where non-admin users could not import Immich and Photoprism geolocation data. +- Fixed a bug where upon point deletion it was not being removed from the map, while it was actually deleted from the database. #883 - Fixed a bug where upon import deletion stats were not being recalculated. #824 ### Changed @@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Restrict access to Sidekiq in non self-hosted mode. - Restrict access to background jobs in non self-hosted mode. - Restrict access to users management in non self-hosted mode. +- Points are now using `lonlat` column for storing longitude and latitude. +- Semantic history points are now being imported much faster. +- GPX files are now being imported much faster. +- Distance calculation are now using Postgis functions and expected to be more accurate. # 0.24.1 - 2025-02-13 diff --git a/Gemfile b/Gemfile index 592c2fd3..4ed5dad3 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ gem 'bootsnap', require: false gem 'chartkick' gem 'data_migrate' gem 'devise' -gem 'geocoder', git: 'https://github.com/alexreisner/geocoder.git', ref: '04ee293' +gem 'geocoder' gem 'gpx' gem 'groupdate' gem 'httparty' @@ -19,11 +19,12 @@ gem 'lograge' gem 'oj' gem 'pg' gem 'prometheus_exporter' -gem 'activerecord-postgis-adapter', github: 'StoneGod/activerecord-postgis-adapter', branch: 'rails-8' +gem 'activerecord-postgis-adapter' gem 'puma' gem 'pundit' gem 'rails', '~> 8.0' gem 'rgeo' +gem 'rgeo-activerecord' gem 'rswag-api' gem 'rswag-ui' gem 'shrine', '~> 3.6' diff --git a/Gemfile.lock b/Gemfile.lock index 63875f90..52c66373 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,21 +1,3 @@ -GIT - remote: https://github.com/StoneGod/activerecord-postgis-adapter.git - revision: 147fd43191ef703e2a1b3654f31d9139201a87e8 - branch: rails-8 - specs: - activerecord-postgis-adapter (10.0.1) - activerecord (~> 8.0.0) - rgeo-activerecord (~> 8.0.0) - -GIT - remote: https://github.com/alexreisner/geocoder.git - revision: 04ee2936a30b30a23ded5231d7faf6cf6c27c099 - ref: 04ee293 - specs: - geocoder (1.8.3) - base64 (>= 0.1.0) - csv (>= 3.0.0) - GEM remote: https://rubygems.org/ specs: @@ -71,6 +53,9 @@ GEM activemodel (= 8.0.1) activesupport (= 8.0.1) timeout (>= 0.4.0) + activerecord-postgis-adapter (11.0.0) + activerecord (~> 8.0.0) + rgeo-activerecord (~> 8.0.0) activestorage (8.0.1) actionpack (= 8.0.1) activejob (= 8.0.1) @@ -153,6 +138,9 @@ GEM fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) + geocoder (1.8.5) + base64 (>= 0.1.0) + csv (>= 3.0.0) globalid (1.2.1) activesupport (>= 6.1) gpx (1.2.0) @@ -192,7 +180,7 @@ GEM kaminari-core (= 1.2.2) kaminari-core (1.2.2) language_server-protocol (3.17.0.4) - logger (1.6.5) + logger (1.6.6) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) @@ -461,7 +449,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - activerecord-postgis-adapter! + activerecord-postgis-adapter bootsnap chartkick data_migrate @@ -473,7 +461,7 @@ DEPENDENCIES fakeredis ffaker foreman - geocoder! + geocoder gpx groupdate httparty @@ -490,6 +478,7 @@ DEPENDENCIES rails (~> 8.0) redis rgeo + rgeo-activerecord rspec-rails rswag-api rswag-specs diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index bad160d5..11082b6e 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -7,8 +7,8 @@ class MapController < ApplicationController @points = points.where('timestamp >= ? AND timestamp <= ?', start_at, end_at) @coordinates = - @points.pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id, :country) - .map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7.to_s, _8.to_s] } + @points.pluck(:lonlat, :battery, :altitude, :timestamp, :velocity, :id, :country) + .map { |lonlat, *rest| [lonlat.y, lonlat.x, *rest.map(&:to_s)] } @distance = distance @start_at = Time.zone.at(start_at) @end_at = Time.zone.at(end_at) diff --git a/app/jobs/data_migrations/migrate_points_latlon_job.rb b/app/jobs/data_migrations/migrate_points_latlon_job.rb new file mode 100644 index 00000000..6a831e34 --- /dev/null +++ b/app/jobs/data_migrations/migrate_points_latlon_job.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class DataMigrations::MigratePointsLatlonJob < ApplicationJob + queue_as :default + + def perform(user_id) + user = User.find(user_id) + + # rubocop:disable Rails/SkipsModelValidations + user.tracked_points.update_all('lonlat = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)') + # rubocop:enable Rails/SkipsModelValidations + end +end diff --git a/app/jobs/points/create_job.rb b/app/jobs/points/create_job.rb index 964c50f7..7dc3d261 100644 --- a/app/jobs/points/create_job.rb +++ b/app/jobs/points/create_job.rb @@ -9,7 +9,7 @@ class Points::CreateJob < ApplicationJob data.each_slice(1000) do |location_batch| Point.upsert_all( location_batch, - unique_by: %i[latitude longitude timestamp user_id], + unique_by: %i[lonlat timestamp user_id], returning: false ) end diff --git a/app/models/concerns/distanceable.rb b/app/models/concerns/distanceable.rb new file mode 100644 index 00000000..6b2d1546 --- /dev/null +++ b/app/models/concerns/distanceable.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module Distanceable + extend ActiveSupport::Concern + + DISTANCE_UNITS = { + km: 1000, # to meters + mi: 1609.34, # to meters + m: 1, # already in meters + ft: 0.3048, # to meters + yd: 0.9144 # to meters + }.freeze + + module ClassMethods + def total_distance(points = nil, unit = :km) + # Handle method being called directly on relation vs with array + if points.nil? + calculate_distance_for_relation(unit) + else + calculate_distance_for_array(points, unit) + end + end + + private + + def calculate_distance_for_relation(unit) + unless DISTANCE_UNITS.key?(unit.to_sym) + raise ArgumentError, "Invalid unit. Supported units are: #{DISTANCE_UNITS.keys.join(', ')}" + end + + distance_in_meters = connection.select_value(<<-SQL.squish) + WITH points_with_previous AS ( + SELECT + lonlat, + LAG(lonlat) OVER (ORDER BY timestamp) as prev_lonlat + FROM (#{to_sql}) AS points + ) + SELECT COALESCE( + SUM( + ST_Distance( + lonlat::geography, + prev_lonlat::geography + ) + ), + 0 + ) + FROM points_with_previous + WHERE prev_lonlat IS NOT NULL + SQL + + distance_in_meters.to_f / DISTANCE_UNITS[unit.to_sym] + end + + def calculate_distance_for_array(points, unit = :km) + unless DISTANCE_UNITS.key?(unit.to_sym) + raise ArgumentError, "Invalid unit. Supported units are: #{DISTANCE_UNITS.keys.join(', ')}" + end + + return 0 if points.length < 2 + + total_meters = points.each_cons(2).sum do |point1, point2| + connection.select_value(<<-SQL.squish) + SELECT ST_Distance( + ST_GeomFromEWKT('#{point1.lonlat}')::geography, + ST_GeomFromEWKT('#{point2.lonlat}')::geography + ) + SQL + end + + total_meters.to_f / DISTANCE_UNITS[unit.to_sym] + end + end + + def distance_to(other_point, unit = :km) + unless DISTANCE_UNITS.key?(unit.to_sym) + raise ArgumentError, "Invalid unit. Supported units are: #{DISTANCE_UNITS.keys.join(', ')}" + end + + # Extract coordinates based on what type other_point is + other_lonlat = extract_point(other_point) + return nil if other_lonlat.nil? + + # Calculate distance in meters using PostGIS + distance_in_meters = self.class.connection.select_value(<<-SQL.squish) + SELECT ST_Distance( + ST_GeomFromEWKT('#{lonlat}')::geography, + ST_GeomFromEWKT('#{other_lonlat}')::geography + ) + SQL + + # Convert to requested unit + distance_in_meters.to_f / DISTANCE_UNITS[unit.to_sym] + end + + private + + def extract_point(point) + case point + when Array + unless point.length == 2 + raise ArgumentError, + 'Coordinates array must contain exactly 2 elements [latitude, longitude]' + end + + RGeo::Geographic.spherical_factory(srid: 4326).point(point[1], point[0]) + when self.class + point.lonlat + end + end +end diff --git a/app/models/concerns/nearable.rb b/app/models/concerns/nearable.rb new file mode 100644 index 00000000..fd90de2b --- /dev/null +++ b/app/models/concerns/nearable.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Nearable + extend ActiveSupport::Concern + + DISTANCE_UNITS = { + km: 1000, # to meters + mi: 1609.34, # to meters + m: 1, # already in meters + ft: 0.3048, # to meters + yd: 0.9144 # to meters + }.freeze + + class_methods do + # rubocop:disable Metrics/MethodLength + def near(*args) + latitude, longitude, radius, unit = extract_coordinates_and_options(*args) + + unless DISTANCE_UNITS.key?(unit.to_sym) + raise ArgumentError, "Invalid unit. Supported units are: #{DISTANCE_UNITS.keys.join(', ')}" + end + + # Convert radius to meters for ST_DWithin + radius_in_meters = radius * DISTANCE_UNITS[unit.to_sym] + + # Create a point from the given coordinates + point = "SRID=4326;POINT(#{longitude} #{latitude})" + + where(<<-SQL.squish) + ST_DWithin( + lonlat::geography, + ST_GeomFromEWKT('#{point}')::geography, + #{radius_in_meters} + ) + SQL + end + + def with_distance(*args) + latitude, longitude, unit = extract_coordinates_and_options(*args) + + unless DISTANCE_UNITS.key?(unit.to_sym) + raise ArgumentError, "Invalid unit. Supported units are: #{DISTANCE_UNITS.keys.join(', ')}" + end + + point = "SRID=4326;POINT(#{longitude} #{latitude})" + conversion_factor = 1.0 / DISTANCE_UNITS[unit.to_sym] + + select(<<-SQL.squish) + #{table_name}.*, + ST_Distance( + lonlat::geography, + ST_GeomFromEWKT('#{point}')::geography + ) * #{conversion_factor} as distance_in_#{unit} + SQL + end + # rubocop:enable Metrics/MethodLength + + private + + def extract_coordinates_and_options(*args) + coords = args.first + if !coords.is_a?(Array) || coords.length != 2 + raise ArgumentError, + 'First argument must be coordinates array containing exactly 2 elements [latitude, longitude]' + end + + [coords[0], coords[1], *args[1..]].tap do |extracted| + # Set default values for missing options + extracted[2] ||= 1 if extracted.length < 3 # default radius + extracted[3] ||= :km if extracted.length < 4 # default unit + end + end + end +end diff --git a/app/models/point.rb b/app/models/point.rb index f28b8043..4de7fa36 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -1,18 +1,20 @@ # frozen_string_literal: true class Point < ApplicationRecord - reverse_geocoded_by :latitude, :longitude + include Nearable + include Distanceable belongs_to :import, optional: true, counter_cache: true belongs_to :visit, optional: true belongs_to :user - validates :latitude, :longitude, :timestamp, presence: true - validates :timestamp, uniqueness: { - scope: %i[latitude longitude user_id], + validates :timestamp, :lonlat, presence: true + validates :lonlat, uniqueness: { + scope: %i[timestamp user_id], message: 'already has a point at this location and time for this user', index: true } + enum :battery_status, { unknown: 0, unplugged: 1, charging: 2, full: 3 }, suffix: true enum :trigger, { unknown: 0, background_event: 1, circular_region_event: 2, beacon_event: 3, @@ -47,14 +49,23 @@ class Point < ApplicationRecord reverse_geocoded_at.present? end + def lon + lonlat.x + end + + def lat + lonlat.y + end + private + # rubocop:disable Metrics/MethodLength Metrics/AbcSize def broadcast_coordinates PointsChannel.broadcast_to( user, [ - latitude.to_f, - longitude.to_f, + lat, + lon, battery.to_s, altitude.to_s, timestamp.to_s, @@ -64,4 +75,5 @@ class Point < ApplicationRecord ] ) end + # rubocop:enable Metrics/MethodLength end diff --git a/app/models/stat.rb b/app/models/stat.rb index 6b2d56dd..36bb0be3 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -37,7 +37,7 @@ class Stat < ApplicationRecord def calculate_daily_distances(monthly_points) timespan.to_a.map.with_index(1) do |day, index| daily_points = filter_points_for_day(monthly_points, day) - distance = calculate_distance(daily_points) + distance = Point.total_distance(daily_points, DISTANCE_UNIT) [index, distance.round(2)] end end @@ -48,10 +48,4 @@ class Stat < ApplicationRecord points.select { |p| p.timestamp.between?(beginning_of_day, end_of_day) } end - - def calculate_distance(points) - points.each_cons(2).sum do |point1, point2| - DistanceCalculator.new(point1, point2).call - end - end end diff --git a/app/models/trip.rb b/app/models/trip.rb index 5e094078..098feb82 100644 --- a/app/models/trip.rb +++ b/app/models/trip.rb @@ -14,7 +14,6 @@ class Trip < ApplicationRecord calculate_distance end - def points user.tracked_points.where(timestamp: started_at.to_i..ended_at.to_i).order(:timestamp) end @@ -47,18 +46,13 @@ class Trip < ApplicationRecord end def calculate_path - trip_path = Tracks::BuildPath.new(points.pluck(:latitude, :longitude)).call + trip_path = Tracks::BuildPath.new(points.pluck(:lonlat)).call self.path = trip_path end - def calculate_distance - distance = 0 - - points.each_cons(2) do |point1, point2| - distance += DistanceCalculator.new(point1, point2).call - end + distance = Point.total_distance(points, DISTANCE_UNIT) self.distance = distance.round end diff --git a/app/models/visit.rb b/app/models/visit.rb index f46d219b..2fadfb2e 100644 --- a/app/models/visit.rb +++ b/app/models/visit.rb @@ -29,7 +29,7 @@ class Visit < ApplicationRecord return area&.radius if area.present? radius = points.map do |point| - Geocoder::Calculations.distance_between(center, [point.latitude, point.longitude]) + Geocoder::Calculations.distance_between(center, [point.lat, point.lon]) end.max radius && radius >= 15 ? radius : 15 diff --git a/app/serializers/api/slim_point_serializer.rb b/app/serializers/api/slim_point_serializer.rb index 76436116..dfe224e0 100644 --- a/app/serializers/api/slim_point_serializer.rb +++ b/app/serializers/api/slim_point_serializer.rb @@ -8,8 +8,8 @@ class Api::SlimPointSerializer def call { id: point.id, - latitude: point.latitude, - longitude: point.longitude, + latitude: point.lat.to_s, + longitude: point.lon.to_s, timestamp: point.timestamp } end diff --git a/app/serializers/export_serializer.rb b/app/serializers/export_serializer.rb index 15f5f948..b7351314 100644 --- a/app/serializers/export_serializer.rb +++ b/app/serializers/export_serializer.rb @@ -22,8 +22,8 @@ class ExportSerializer def export_point(point) { - lat: point.latitude, - lon: point.longitude, + lat: point.lat.to_s, + lon: point.lon.to_s, bs: battery_status(point), batt: point.battery, p: point.ping, diff --git a/app/serializers/point_serializer.rb b/app/serializers/point_serializer.rb index 270e3e25..6dfe5502 100644 --- a/app/serializers/point_serializer.rb +++ b/app/serializers/point_serializer.rb @@ -1,14 +1,20 @@ # frozen_string_literal: true class PointSerializer - EXCLUDED_ATTRIBUTES = %w[created_at updated_at visit_id id import_id user_id raw_data].freeze + EXCLUDED_ATTRIBUTES = %w[ + created_at updated_at visit_id id import_id user_id raw_data lonlat + reverse_geocoded_at + ].freeze def initialize(point) @point = point end def call - point.attributes.except(*EXCLUDED_ATTRIBUTES) + point.attributes.except(*EXCLUDED_ATTRIBUTES).tap do |attributes| + attributes['latitude'] = point.lat.to_s + attributes['longitude'] = point.lon.to_s + end end private diff --git a/app/serializers/points/geojson_serializer.rb b/app/serializers/points/geojson_serializer.rb index 40c1048f..1fd9a810 100644 --- a/app/serializers/points/geojson_serializer.rb +++ b/app/serializers/points/geojson_serializer.rb @@ -14,7 +14,7 @@ class Points::GeojsonSerializer type: 'Feature', geometry: { type: 'Point', - coordinates: [point.longitude, point.latitude] + coordinates: [point.lon.to_s, point.lat.to_s] }, properties: PointSerializer.new(point).call } diff --git a/app/serializers/points/gpx_serializer.rb b/app/serializers/points/gpx_serializer.rb index d4fd2929..fa088ecd 100644 --- a/app/serializers/points/gpx_serializer.rb +++ b/app/serializers/points/gpx_serializer.rb @@ -17,8 +17,8 @@ class Points::GpxSerializer points.each do |point| track_segment.points << GPX::TrackPoint.new( - lat: point.latitude.to_f, - lon: point.longitude.to_f, + lat: point.lat, + lon: point.lon, elevation: point.altitude.to_f, time: point.recorded_at ) diff --git a/app/services/areas/visits/create.rb b/app/services/areas/visits/create.rb index 768f5f9f..16efd3ef 100644 --- a/app/services/areas/visits/create.rb +++ b/app/services/areas/visits/create.rb @@ -38,7 +38,7 @@ class Areas::Visits::Create end points = Point.where(user_id: user.id) - .near([area.latitude, area.longitude], area_radius, units: DISTANCE_UNIT) + .near([area.latitude, area.longitude], area_radius, DISTANCE_UNIT) .order(timestamp: :asc) # check if all points within the area are assigned to a visit diff --git a/app/services/distance_calculator.rb b/app/services/distance_calculator.rb deleted file mode 100644 index d00d070b..00000000 --- a/app/services/distance_calculator.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class DistanceCalculator - def initialize(point1, point2) - @point1 = point1 - @point2 = point2 - end - - def call - Geocoder::Calculations.distance_between( - point1.to_coordinates, point2.to_coordinates, units: ::DISTANCE_UNIT - ) - end - - private - - attr_reader :point1, :point2 -end diff --git a/app/services/geojson/import_parser.rb b/app/services/geojson/import_parser.rb index ff78e6f6..13b8651c 100644 --- a/app/services/geojson/import_parser.rb +++ b/app/services/geojson/import_parser.rb @@ -27,8 +27,7 @@ class Geojson::ImportParser def point_exists?(params, user_id) Point.exists?( - latitude: params[:latitude], - longitude: params[:longitude], + lonlat: params[:lonlat], timestamp: params[:timestamp], user_id: ) diff --git a/app/services/geojson/params.rb b/app/services/geojson/params.rb index 21faf941..8ae4fb41 100644 --- a/app/services/geojson/params.rb +++ b/app/services/geojson/params.rb @@ -33,8 +33,7 @@ class Geojson::Params def build_point(feature) { - latitude: feature[:geometry][:coordinates][1], - longitude: feature[:geometry][:coordinates][0], + lonlat: "POINT(#{feature[:geometry][:coordinates][0]} #{feature[:geometry][:coordinates][1]})", battery_status: feature[:properties][:battery_state], battery: battery_level(feature[:properties][:battery_level]), timestamp: timestamp(feature), @@ -64,8 +63,7 @@ class Geojson::Params def build_line_point(point) { - latitude: point[1], - longitude: point[0], + lonlat: "POINT(#{point[0]} #{point[1]})", timestamp: timestamp(point), raw_data: point } diff --git a/app/services/google_maps/phone_takeout_parser.rb b/app/services/google_maps/phone_takeout_parser.rb index 8721f8d5..a30b34d3 100644 --- a/app/services/google_maps/phone_takeout_parser.rb +++ b/app/services/google_maps/phone_takeout_parser.rb @@ -16,14 +16,12 @@ class GoogleMaps::PhoneTakeoutParser points_data.compact.each.with_index(1) do |point_data, index| next if Point.exists?( timestamp: point_data[:timestamp], - latitude: point_data[:latitude], - longitude: point_data[:longitude], + lonlat: point_data[:lonlat], user_id: ) Point.create( - latitude: point_data[:latitude], - longitude: point_data[:longitude], + lonlat: point_data[:lonlat], timestamp: point_data[:timestamp], raw_data: point_data[:raw_data], accuracy: point_data[:accuracy], @@ -72,8 +70,7 @@ class GoogleMaps::PhoneTakeoutParser def point_hash(lat, lon, timestamp, raw_data) { - latitude: lat.to_f, - longitude: lon.to_f, + lonlat: "POINT(#{lon.to_f} #{lat.to_f})", timestamp:, raw_data:, accuracy: raw_data['accuracyMeters'], diff --git a/app/services/google_maps/records_importer.rb b/app/services/google_maps/records_importer.rb index c7edfb1f..ec9555f7 100644 --- a/app/services/google_maps/records_importer.rb +++ b/app/services/google_maps/records_importer.rb @@ -25,8 +25,7 @@ class GoogleMaps::RecordsImporter # rubocop:disable Metrics/MethodLength def prepare_location_data(location) { - latitude: location['latitudeE7'].to_f / 10**7, - longitude: location['longitudeE7'].to_f / 10**7, + lonlat: "POINT(#{location['longitudeE7'].to_f / 10**7} #{location['latitudeE7'].to_f / 10**7})", timestamp: parse_timestamp(location), altitude: location['altitude'], velocity: location['velocity'], @@ -47,7 +46,7 @@ class GoogleMaps::RecordsImporter # rubocop:disable Rails/SkipsModelValidations Point.upsert_all( unique_batch, - unique_by: %i[latitude longitude timestamp user_id], + unique_by: %i[lonlat timestamp user_id], returning: false, on_duplicate: :skip ) @@ -59,8 +58,7 @@ class GoogleMaps::RecordsImporter def deduplicate_batch(batch) batch.uniq do |record| [ - record[:latitude].round(7), - record[:longitude].round(7), + record[:lonlat], record[:timestamp], record[:user_id] ] diff --git a/app/services/google_maps/semantic_history_parser.rb b/app/services/google_maps/semantic_history_parser.rb index 83d2486b..77984c09 100644 --- a/app/services/google_maps/semantic_history_parser.rb +++ b/app/services/google_maps/semantic_history_parser.rb @@ -3,87 +3,135 @@ class GoogleMaps::SemanticHistoryParser include Imports::Broadcaster + BATCH_SIZE = 1000 attr_reader :import, :user_id def initialize(import, user_id) @import = import @user_id = user_id + @current_index = 0 end def call points_data = parse_json - points_data.each.with_index(1) do |point_data, index| - next if Point.exists?( - timestamp: point_data[:timestamp], - latitude: point_data[:latitude], - longitude: point_data[:longitude], - user_id: - ) - - Point.create( - latitude: point_data[:latitude], - longitude: point_data[:longitude], - timestamp: point_data[:timestamp], - raw_data: point_data[:raw_data], - topic: 'Google Maps Timeline Export', - tracker_id: 'google-maps-timeline-export', - import_id: import.id, - user_id: - ) - - broadcast_import_progress(import, index) + points_data.each_slice(BATCH_SIZE) do |batch| + @current_index += batch.size + process_batch(batch) + broadcast_import_progress(import, @current_index) end end private + def process_batch(batch) + records = batch.map { |point_data| prepare_point_data(point_data) } + + # rubocop:disable Rails/SkipsModelValidations + Point.upsert_all( + records, + unique_by: %i[lonlat timestamp user_id], + returning: false, + on_duplicate: :skip + ) + # rubocop:enable Rails/SkipsModelValidations + rescue StandardError => e + create_notification("Failed to process location batch: #{e.message}") + end + + def prepare_point_data(point_data) + { + lonlat: point_data[:lonlat], + timestamp: point_data[:timestamp], + raw_data: point_data[:raw_data], + topic: 'Google Maps Timeline Export', + tracker_id: 'google-maps-timeline-export', + import_id: import.id, + user_id: user_id, + created_at: Time.current, + updated_at: Time.current + } + end + + def create_notification(message) + Notification.create!( + user_id: user_id, + title: 'Google Maps Timeline Import Error', + content: message, + kind: :error + ) + end + def parse_json import.raw_data['timelineObjects'].flat_map do |timeline_object| - if timeline_object['activitySegment'].present? - if timeline_object['activitySegment']['startLocation'].blank? - next if timeline_object['activitySegment']['waypointPath'].blank? + parse_timeline_object(timeline_object) + end.compact + end - timeline_object['activitySegment']['waypointPath']['waypoints'].map do |waypoint| - { - latitude: waypoint['latE7'].to_f / 10**7, - longitude: waypoint['lngE7'].to_f / 10**7, - timestamp: Timestamps.parse_timestamp(timeline_object['activitySegment']['duration']['startTimestamp'] || timeline_object['activitySegment']['duration']['startTimestampMs']), - raw_data: timeline_object - } - end - else - { - latitude: timeline_object['activitySegment']['startLocation']['latitudeE7'].to_f / 10**7, - longitude: timeline_object['activitySegment']['startLocation']['longitudeE7'].to_f / 10**7, - timestamp: Timestamps.parse_timestamp(timeline_object['activitySegment']['duration']['startTimestamp'] || timeline_object['activitySegment']['duration']['startTimestampMs']), - raw_data: timeline_object - } - end - elsif timeline_object['placeVisit'].present? - if timeline_object.dig('placeVisit', 'location', 'latitudeE7').present? && - timeline_object.dig('placeVisit', 'location', 'longitudeE7').present? - { - latitude: timeline_object['placeVisit']['location']['latitudeE7'].to_f / 10**7, - longitude: timeline_object['placeVisit']['location']['longitudeE7'].to_f / 10**7, - timestamp: Timestamps.parse_timestamp(timeline_object['placeVisit']['duration']['startTimestamp'] || timeline_object['placeVisit']['duration']['startTimestampMs']), - raw_data: timeline_object - } - elsif timeline_object.dig('placeVisit', 'otherCandidateLocations')&.any? - point = timeline_object['placeVisit']['otherCandidateLocations'][0] + def parse_timeline_object(timeline_object) + if timeline_object['activitySegment'].present? + parse_activity_segment(timeline_object['activitySegment']) + elsif timeline_object['placeVisit'].present? + parse_place_visit(timeline_object['placeVisit']) + end + end - next unless point['latitudeE7'].present? && point['longitudeE7'].present? + def parse_activity_segment(activity) + if activity['startLocation'].blank? + parse_waypoints(activity) + else + build_point_from_location( + longitude: activity['startLocation']['longitudeE7'], + latitude: activity['startLocation']['latitudeE7'], + timestamp: activity['duration']['startTimestamp'] || activity['duration']['startTimestampMs'], + raw_data: activity + ) + end + end - { - latitude: point['latitudeE7'].to_f / 10**7, - longitude: point['longitudeE7'].to_f / 10**7, - timestamp: Timestamps.parse_timestamp(timeline_object['placeVisit']['duration']['startTimestamp'] || timeline_object['placeVisit']['duration']['startTimestampMs']), - raw_data: timeline_object - } - else - next - end - end - end.reject(&:blank?) + def parse_waypoints(activity) + return if activity['waypointPath'].blank? + + activity['waypointPath']['waypoints'].map do |waypoint| + build_point_from_location( + longitude: waypoint['lngE7'], + latitude: waypoint['latE7'], + timestamp: activity['duration']['startTimestamp'] || activity['duration']['startTimestampMs'], + raw_data: activity + ) + end + end + + def parse_place_visit(place_visit) + if place_visit.dig('location', 'latitudeE7').present? && + place_visit.dig('location', 'longitudeE7').present? + build_point_from_location( + longitude: place_visit['location']['longitudeE7'], + latitude: place_visit['location']['latitudeE7'], + timestamp: place_visit['duration']['startTimestamp'] || place_visit['duration']['startTimestampMs'], + raw_data: place_visit + ) + elsif (candidate = place_visit.dig('otherCandidateLocations', 0)) + parse_candidate_location(candidate, place_visit) + end + end + + def parse_candidate_location(candidate, place_visit) + return unless candidate['latitudeE7'].present? && candidate['longitudeE7'].present? + + build_point_from_location( + longitude: candidate['longitudeE7'], + latitude: candidate['latitudeE7'], + timestamp: place_visit['duration']['startTimestamp'] || place_visit['duration']['startTimestampMs'], + raw_data: place_visit + ) + end + + def build_point_from_location(longitude:, latitude:, timestamp:, raw_data:) + { + lonlat: "POINT(#{longitude.to_f / 10**7} #{latitude.to_f / 10**7})", + timestamp: Timestamps.parse_timestamp(timestamp), + raw_data: raw_data + } end end diff --git a/app/services/gpx/track_importer.rb b/app/services/gpx/track_importer.rb new file mode 100644 index 00000000..62f327cc --- /dev/null +++ b/app/services/gpx/track_importer.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +class Gpx::TrackImporter + include Imports::Broadcaster + + attr_reader :import, :json, :user_id + + def initialize(import, user_id) + @import = import + @json = import.raw_data + @user_id = user_id + end + + def call + tracks = json['gpx']['trk'] + tracks_arr = tracks.is_a?(Array) ? tracks : [tracks] + + points = tracks_arr.map { parse_track(_1) }.flatten.compact + points_data = points.map.with_index(1) { |point, index| prepare_point(point, index) }.compact + + bulk_insert_points(points_data) + end + + private + + def parse_track(track) + return if track['trkseg'].blank? + + segments = track['trkseg'] + segments_array = segments.is_a?(Array) ? segments : [segments] + + segments_array.compact.map { |segment| segment['trkpt'] } + end + + def prepare_point(point, index) + return if point['lat'].blank? || point['lon'].blank? || point['time'].blank? + + { + lonlat: "POINT(#{point['lon'].to_d} #{point['lat'].to_d})", + altitude: point['ele'].to_i, + timestamp: Time.parse(point['time']).to_i, + import_id: import.id, + velocity: speed(point), + raw_data: point, + user_id: user_id, + created_at: Time.current, + updated_at: Time.current + } + end + + def bulk_insert_points(batch) + unique_batch = batch.uniq { |record| [record[:lonlat], record[:timestamp], record[:user_id]] } + + # rubocop:disable Rails/SkipsModelValidations + Point.upsert_all( + unique_batch, + unique_by: %i[lonlat timestamp user_id], + returning: false, + on_duplicate: :skip + ) + # rubocop:enable Rails/SkipsModelValidations + + broadcast_import_progress(import, unique_batch.size) + rescue StandardError => e + create_notification("Failed to process GPX track: #{e.message}") + end + + def create_notification(message) + Notification.create!( + user_id: user_id, + title: 'GPX Import Error', + content: message, + kind: :error + ) + end + + def speed(point) + return if point['extensions'].blank? + + ( + point.dig('extensions', 'speed') || point.dig('extensions', 'TrackPointExtension', 'speed') + ).to_f.round(1) + end +end diff --git a/app/services/gpx/track_parser.rb b/app/services/gpx/track_parser.rb deleted file mode 100644 index 20c2837a..00000000 --- a/app/services/gpx/track_parser.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -class Gpx::TrackParser - include Imports::Broadcaster - - attr_reader :import, :json, :user_id - - def initialize(import, user_id) - @import = import - @json = import.raw_data - @user_id = user_id - end - - def call - tracks = json['gpx']['trk'] - tracks_arr = tracks.is_a?(Array) ? tracks : [tracks] - - tracks_arr.map { parse_track(_1) }.flatten.compact.each.with_index(1) do |point, index| - create_point(point, index) - end - end - - private - - def parse_track(track) - return if track['trkseg'].blank? - - segments = track['trkseg'] - segments_array = segments.is_a?(Array) ? segments : [segments] - - segments_array.compact.map { |segment| segment['trkpt'] } - end - - def create_point(point, index) - return if point['lat'].blank? || point['lon'].blank? || point['time'].blank? - return if point_exists?(point) - - Point.create( - latitude: point['lat'].to_d, - longitude: point['lon'].to_d, - altitude: point['ele'].to_i, - timestamp: Time.parse(point['time']).to_i, - import_id: import.id, - velocity: speed(point), - raw_data: point, - user_id: - ) - - broadcast_import_progress(import, index) - end - - def point_exists?(point) - Point.exists?( - latitude: point['lat'].to_d, - longitude: point['lon'].to_d, - timestamp: Time.parse(point['time']).to_i, - user_id: - ) - end - - def speed(point) - return if point['extensions'].blank? - - ( - point.dig('extensions', 'speed') || point.dig('extensions', 'TrackPointExtension', 'speed') - ).to_f.round(1) - end -end diff --git a/app/services/imports/create.rb b/app/services/imports/create.rb index 16374170..e34661b1 100644 --- a/app/services/imports/create.rb +++ b/app/services/imports/create.rb @@ -26,8 +26,8 @@ class Imports::Create case source when 'google_semantic_history' then GoogleMaps::SemanticHistoryParser when 'google_phone_takeout' then GoogleMaps::PhoneTakeoutParser - when 'owntracks' then OwnTracks::ExportParser - when 'gpx' then Gpx::TrackParser + when 'owntracks' then OwnTracks::Importer + when 'gpx' then Gpx::TrackImporter when 'geojson' then Geojson::ImportParser when 'immich_api', 'photoprism_api' then Photos::ImportParser end diff --git a/app/services/overland/params.rb b/app/services/overland/params.rb index b712ffce..40c33599 100644 --- a/app/services/overland/params.rb +++ b/app/services/overland/params.rb @@ -13,8 +13,7 @@ class Overland::Params next if point[:geometry].nil? || point.dig(:properties, :timestamp).nil? { - latitude: point[:geometry][:coordinates][1], - longitude: point[:geometry][:coordinates][0], + lonlat: "POINT(#{point[:geometry][:coordinates][0]} #{point[:geometry][:coordinates][1]})", battery_status: point[:properties][:battery_state], battery: battery_level(point[:properties][:battery_level]), timestamp: DateTime.parse(point[:properties][:timestamp]), diff --git a/app/services/own_tracks/export_parser.rb b/app/services/own_tracks/export_parser.rb deleted file mode 100644 index 5f4d9613..00000000 --- a/app/services/own_tracks/export_parser.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -class OwnTracks::ExportParser - include Imports::Broadcaster - - attr_reader :import, :data, :user_id - - def initialize(import, user_id) - @import = import - @data = import.raw_data - @user_id = user_id - end - - def call - points_data = data.map { |point| OwnTracks::Params.new(point).call } - - points_data.each.with_index(1) do |point_data, index| - next if Point.exists?( - timestamp: point_data[:timestamp], - latitude: point_data[:latitude], - longitude: point_data[:longitude], - user_id: - ) - - point = Point.new(point_data).tap do |p| - p.user_id = user_id - p.import_id = import.id - end - - point.save - - broadcast_import_progress(import, index) - end - end -end diff --git a/app/services/own_tracks/importer.rb b/app/services/own_tracks/importer.rb new file mode 100644 index 00000000..20dbc706 --- /dev/null +++ b/app/services/own_tracks/importer.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class OwnTracks::Importer + include Imports::Broadcaster + + attr_reader :import, :data, :user_id + + def initialize(import, user_id) + @import = import + @data = import.raw_data + @user_id = user_id + end + + def call + points_data = data.map.with_index(1) do |point, index| + OwnTracks::Params.new(point).call.merge( + import_id: import.id, + user_id: user_id, + created_at: Time.current, + updated_at: Time.current + ) + end + + bulk_insert_points(points_data) + end + + private + + def bulk_insert_points(batch) + unique_batch = batch.uniq { |record| [record[:lonlat], record[:timestamp], record[:user_id]] } + + # rubocop:disable Rails/SkipsModelValidations + Point.upsert_all( + unique_batch, + unique_by: %i[lonlat timestamp user_id], + returning: false, + on_duplicate: :skip + ) + # rubocop:enable Rails/SkipsModelValidations + rescue StandardError => e + create_notification("Failed to process OwnTracks data: #{e.message}") + end + + def create_notification(message) + Notification.create!( + user_id: user_id, + title: 'OwnTracks Import Error', + content: message, + kind: :error + ) + end +end diff --git a/app/services/own_tracks/params.rb b/app/services/own_tracks/params.rb index e5319893..68f8c751 100644 --- a/app/services/own_tracks/params.rb +++ b/app/services/own_tracks/params.rb @@ -7,10 +7,11 @@ class OwnTracks::Params @params = params.to_h.deep_symbolize_keys end + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize def call { - latitude: params[:lat], - longitude: params[:lon], + lonlat: "POINT(#{params[:lon]} #{params[:lat]})", battery: params[:batt], ping: params[:p], altitude: params[:alt], @@ -30,6 +31,8 @@ class OwnTracks::Params raw_data: params.deep_stringify_keys } end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength private diff --git a/app/services/photos/import_parser.rb b/app/services/photos/import_parser.rb index 97b9c9d4..610681fb 100644 --- a/app/services/photos/import_parser.rb +++ b/app/services/photos/import_parser.rb @@ -20,8 +20,7 @@ class Photos::ImportParser return 0 if point_exists?(point, point['timestamp']) Point.create( - latitude: point['latitude'].to_d, - longitude: point['longitude'].to_d, + lonlat: "POINT(#{point['longitude']} #{point['latitude']})", timestamp: point['timestamp'], raw_data: point, import_id: import.id, @@ -33,8 +32,7 @@ class Photos::ImportParser def point_exists?(point, timestamp) Point.exists?( - latitude: point['latitude'].to_d, - longitude: point['longitude'].to_d, + lonlat: "POINT(#{point['longitude']} #{point['latitude']})", timestamp:, user_id: ) diff --git a/app/services/reverse_geocoding/points/fetch_data.rb b/app/services/reverse_geocoding/points/fetch_data.rb index b6798c35..87e4faa4 100644 --- a/app/services/reverse_geocoding/points/fetch_data.rb +++ b/app/services/reverse_geocoding/points/fetch_data.rb @@ -18,7 +18,7 @@ class ReverseGeocoding::Points::FetchData private def update_point_with_geocoding_data - response = Geocoder.search([point.latitude, point.longitude]).first + response = Geocoder.search([point.lat, point.lon]).first return if response.blank? || response.data['error'].present? point.update!( diff --git a/app/services/stats/calculate_month.rb b/app/services/stats/calculate_month.rb index 324cc3a7..b303d39f 100644 --- a/app/services/stats/calculate_month.rb +++ b/app/services/stats/calculate_month.rb @@ -46,7 +46,7 @@ class Stats::CalculateMonth .tracked_points .without_raw_data .where(timestamp: start_timestamp..end_timestamp) - .select(:latitude, :longitude, :timestamp, :city, :country) + .select(:lonlat, :timestamp, :city, :country) .order(timestamp: :asc) end diff --git a/app/services/tracks/build_path.rb b/app/services/tracks/build_path.rb index 4feaf49c..a3e14b9c 100644 --- a/app/services/tracks/build_path.rb +++ b/app/services/tracks/build_path.rb @@ -7,7 +7,7 @@ class Tracks::BuildPath def call factory.line_string( - coordinates.map { |point| factory.point(point[1].to_f.round(5), point[0].to_f.round(5)) } + coordinates.map { |point| factory.point(point.lon.to_f.round(5), point.lat.to_f.round(5)) } ) end diff --git a/app/services/visits/prepare.rb b/app/services/visits/prepare.rb index 0bb9c2b7..fb60d90f 100644 --- a/app/services/visits/prepare.rb +++ b/app/services/visits/prepare.rb @@ -16,7 +16,9 @@ class Visits::Prepare grouped_points = Visits::GroupPoints.new(day_points).group_points_by_radius day_result = prepare_day_result(grouped_points) - # Iterate through the day_result, check if there are any points outside of visits that are between two consecutive visits. If there are none, merge the visits. + # Iterate through the day_result, check if there are any points outside + # of visits that are between two consecutive visits. If there are none, + # merge the visits. day_result.each_cons(2) do |visit1, visit2| next if visit1[:points].last == visit2[:points].first @@ -65,8 +67,8 @@ class Visits::Prepare center_point = group.first { - latitude: center_point.latitude, - longitude: center_point.longitude, + latitude: center_point.lat, + longitude: center_point.lon, radius: calculate_radius(center_point, group), points: group, duration: (group.last.timestamp - group.first.timestamp).to_i / 60, diff --git a/db/data/20250222213848_migrate_points_latlon.rb b/db/data/20250222213848_migrate_points_latlon.rb new file mode 100644 index 00000000..efe3d563 --- /dev/null +++ b/db/data/20250222213848_migrate_points_latlon.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class MigratePointsLatlon < ActiveRecord::Migration[8.0] + def up + User.find_each do |user| + DataMigrations::MigratePointsLatlonJob.perform_later(user.id) + end + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20250221181805_add_lonlat_to_points.rb b/db/migrate/20250221181805_add_lonlat_to_points.rb new file mode 100644 index 00000000..b7e90edf --- /dev/null +++ b/db/migrate/20250221181805_add_lonlat_to_points.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddLonlatToPoints < ActiveRecord::Migration[8.0] + def change + add_column :points, :lonlat, :st_point, geographic: true + end +end diff --git a/db/migrate/20250221185032_add_lonlat_index.rb b/db/migrate/20250221185032_add_lonlat_index.rb new file mode 100644 index 00000000..4127c4dc --- /dev/null +++ b/db/migrate/20250221185032_add_lonlat_index.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddLonlatIndex < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def change + add_index :points, :lonlat, using: :gist, algorithm: :concurrently + end +end diff --git a/db/migrate/20250221194430_remove_points_latitude_longitude_uniqueness_index.rb b/db/migrate/20250221194430_remove_points_latitude_longitude_uniqueness_index.rb new file mode 100644 index 00000000..69d864aa --- /dev/null +++ b/db/migrate/20250221194430_remove_points_latitude_longitude_uniqueness_index.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class RemovePointsLatitudeLongitudeUniquenessIndex < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def up + return unless index_exists?( + :points, %i[latitude longitude timestamp user_id], + name: 'unique_points_lat_long_timestamp_user_id_index' + ) + + remove_index :points, + name: 'unique_points_lat_long_timestamp_user_id_index', + algorithm: :concurrently + end + + def down + return if index_exists?( + :points, %i[latitude longitude timestamp user_id], + name: 'unique_points_lat_long_timestamp_user_id_index' + ) + + add_index :points, %i[latitude longitude timestamp user_id], + unique: true, + name: 'unique_points_lat_long_timestamp_user_id_index', + algorithm: :concurrently + end +end diff --git a/db/migrate/20250221194509_add_unique_lon_lat_index_to_points.rb b/db/migrate/20250221194509_add_unique_lon_lat_index_to_points.rb new file mode 100644 index 00000000..6a135944 --- /dev/null +++ b/db/migrate/20250221194509_add_unique_lon_lat_index_to_points.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddUniqueLonLatIndexToPoints < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def change + return if index_exists?(:points, %i[lonlat timestamp user_id], name: 'index_points_on_lonlat_timestamp_user_id') + + add_index :points, %i[lonlat timestamp user_id], unique: true, + name: 'index_points_on_lonlat_timestamp_user_id', + algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 512330a8..eccd2a45 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_02_19_195822) do +ActiveRecord::Schema[8.0].define(version: 2025_02_21_194509) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "postgis" @@ -160,6 +160,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_19_195822) do t.decimal "course", precision: 8, scale: 5 t.decimal "course_accuracy", precision: 8, scale: 5 t.string "external_track_id" + t.geography "lonlat", limit: {srid: 4326, type: "st_point", geographic: true} t.index ["altitude"], name: "index_points_on_altitude" t.index ["battery"], name: "index_points_on_battery" t.index ["battery_status"], name: "index_points_on_battery_status" @@ -169,8 +170,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_19_195822) do t.index ["external_track_id"], name: "index_points_on_external_track_id" t.index ["geodata"], name: "index_points_on_geodata", using: :gin t.index ["import_id"], name: "index_points_on_import_id" - t.index ["latitude", "longitude", "timestamp", "user_id"], name: "unique_points_lat_long_timestamp_user_id_index", unique: true t.index ["latitude", "longitude"], name: "index_points_on_latitude_and_longitude" + t.index ["lonlat", "timestamp", "user_id"], name: "index_points_on_lonlat_timestamp_user_id", unique: true + t.index ["lonlat"], name: "index_points_on_lonlat", using: :gist t.index ["reverse_geocoded_at"], name: "index_points_on_reverse_geocoded_at" t.index ["timestamp"], name: "index_points_on_timestamp" t.index ["trigger"], name: "index_points_on_trigger" diff --git a/spec/factories/points.rb b/spec/factories/points.rb index 2288a07d..ca350cdd 100644 --- a/spec/factories/points.rb +++ b/spec/factories/points.rb @@ -29,11 +29,11 @@ FactoryBot.define do course { nil } course_accuracy { nil } external_track_id { nil } + lonlat { "POINT(#{FFaker::Geolocation.lng} #{FFaker::Geolocation.lat})" } user trait :with_known_location do - latitude { 55.755826 } - longitude { 37.6173 } + lonlat { 'POINT(37.6173 55.755826)' } end trait :with_geodata do diff --git a/spec/fixtures/files/geojson/export.json b/spec/fixtures/files/geojson/export.json index b86f4b43..8b916ab1 100644 --- a/spec/fixtures/files/geojson/export.json +++ b/spec/fixtures/files/geojson/export.json @@ -1 +1,295 @@ -{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"0.1","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459201,"latitude":"0.1","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}},{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"0.2","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459202,"latitude":"0.2","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}},{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"0.3","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459203,"latitude":"0.3","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}},{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"0.4","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459204,"latitude":"0.4","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}},{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"0.5","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459205,"latitude":"0.5","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}},{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"0.6","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459206,"latitude":"0.6","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}},{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"0.7","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459207,"latitude":"0.7","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}},{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"0.8","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459208,"latitude":"0.8","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}},{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"0.9","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459209,"latitude":"0.9","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}},{"type":"Feature","geometry":{"type":"Point","coordinates":["0.0","0.0"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"1.0","velocity":"MyString","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459210,"latitude":"1.0","mode":1,"inrids":[],"in_regions":[],"raw_data":"","city":null,"country":null,"geodata":{}}}]} +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["0.1", "0.1"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "0.1", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459201, + "latitude": "0.1", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["0.2", "0.2"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "0.2", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459202, + "latitude": "0.2", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["0.3", "0.3"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "0.3", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459203, + "latitude": "0.3", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["0.4", "0.4"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "0.4", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459204, + "latitude": "0.4", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["0.5", "0.5"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "0.5", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459205, + "latitude": "0.5", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["0.6", "0.6"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "0.6", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459206, + "latitude": "0.6", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["0.7", "0.7"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "0.7", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459207, + "latitude": "0.7", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["0.8", "0.8"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "0.8", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459208, + "latitude": "0.8", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["0.9", "0.9"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "0.9", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459209, + "latitude": "0.9", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": ["1.0", "1.0"] }, + "properties": { + "battery_status": "unplugged", + "ping": "MyString", + "battery": 1, + "tracker_id": "MyString", + "topic": "MyString", + "altitude": 1, + "longitude": "1.0", + "velocity": "MyString", + "trigger": "background_event", + "bssid": "MyString", + "ssid": "MyString", + "connection": "wifi", + "vertical_accuracy": 1, + "accuracy": 1, + "timestamp": 1609459210, + "latitude": "1.0", + "mode": 1, + "inrids": [], + "in_regions": [], + "raw_data": "", + "city": null, + "country": null, + "geodata": {} + } + } + ] +} diff --git a/spec/fixtures/files/geojson/export_same_points.json b/spec/fixtures/files/geojson/export_same_points.json index f3961b32..f51960a5 100644 --- a/spec/fixtures/files/geojson/export_same_points.json +++ b/spec/fixtures/files/geojson/export_same_points.json @@ -1 +1 @@ -{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459200,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459201,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459202,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459203,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459204,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459205,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459206,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459207,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459208,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459209,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"reverse_geocoded_at":"2021-01-01T00:00:00.000+01:00","course":null,"course_accuracy":null,"external_track_id":null}}]} +{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459200,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459201,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459202,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459203,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459204,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459205,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459206,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459207,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459208,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":["37.6173","55.755826"]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459209,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}}]} diff --git a/spec/jobs/data_migrations/migrate_points_latlon_job_spec.rb b/spec/jobs/data_migrations/migrate_points_latlon_job_spec.rb new file mode 100644 index 00000000..2bb19f2a --- /dev/null +++ b/spec/jobs/data_migrations/migrate_points_latlon_job_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe DataMigrations::MigratePointsLatlonJob, type: :job do + describe '#perform' do + it 'updates the lonlat column for all tracked points' do + user = create(:user) + point = create(:point, latitude: 2.0, longitude: 1.0, user: user) + + expect { subject.perform(user.id) }.to change { + point.reload.lonlat + }.to(RGeo::Geographic.spherical_factory.point(1.0, 2.0)) + end + end +end diff --git a/spec/jobs/import_job_spec.rb b/spec/jobs/import_job_spec.rb index aa6d3e22..b6655a0c 100644 --- a/spec/jobs/import_job_spec.rb +++ b/spec/jobs/import_job_spec.rb @@ -26,7 +26,7 @@ RSpec.describe ImportJob, type: :job do context 'when there is an error' do before do - allow_any_instance_of(OwnTracks::ExportParser).to receive(:call).and_raise(StandardError) + allow_any_instance_of(OwnTracks::Importer).to receive(:call).and_raise(StandardError) end it 'does not create points' do diff --git a/spec/jobs/trips/create_path_job_spec.rb b/spec/jobs/trips/create_path_job_spec.rb index 60d288e3..17a02418 100644 --- a/spec/jobs/trips/create_path_job_spec.rb +++ b/spec/jobs/trips/create_path_job_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Trips::CreatePathJob, type: :job do let(:points) { trip.points } let(:trip_path) do "LINESTRING (#{points.map do |point| - "#{point.longitude.to_f.round(5)} #{point.latitude.to_f.round(5)}" + "#{point.lon.to_f.round(5)} #{point.lat.to_f.round(5)}" end.join(', ')})" end diff --git a/spec/models/point_spec.rb b/spec/models/point_spec.rb index c1972838..7f5f03e9 100644 --- a/spec/models/point_spec.rb +++ b/spec/models/point_spec.rb @@ -9,9 +9,8 @@ RSpec.describe Point, type: :model do end describe 'validations' do - it { is_expected.to validate_presence_of(:latitude) } - it { is_expected.to validate_presence_of(:longitude) } it { is_expected.to validate_presence_of(:timestamp) } + it { is_expected.to validate_presence_of(:lonlat) } end describe 'scopes' do @@ -63,5 +62,21 @@ RSpec.describe Point, type: :model do end end end + + describe '#lon' do + let(:point) { create(:point, lonlat: 'POINT(1 2)') } + + it 'returns longitude' do + expect(point.lon).to eq(1) + end + end + + describe '#lat' do + let(:point) { create(:point, lonlat: 'POINT(1 2)') } + + it 'returns latitude' do + expect(point.lat).to eq(2) + end + end end end diff --git a/spec/models/stat_spec.rb b/spec/models/stat_spec.rb index 1208e006..90337f2f 100644 --- a/spec/models/stat_spec.rb +++ b/spec/models/stat_spec.rb @@ -25,11 +25,11 @@ RSpec.describe Stat, type: :model do context 'when there are points' do let!(:points) do - create(:point, user:, latitude: 1, longitude: 1, timestamp: DateTime.new(year, 1, 1, 1)) - create(:point, user:, latitude: 2, longitude: 2, timestamp: DateTime.new(year, 1, 1, 2)) + create(:point, user:, lonlat: 'POINT(1 1)', timestamp: DateTime.new(year, 1, 1, 1)) + create(:point, user:, lonlat: 'POINT(2 2)', timestamp: DateTime.new(year, 1, 1, 2)) end - before { expected_distance[0][1] = 157.23 } + before { expected_distance[0][1] = 156.88 } it 'returns distance by day' do expect(subject).to eq(expected_distance) diff --git a/spec/serializers/api/slim_point_serializer_spec.rb b/spec/serializers/api/slim_point_serializer_spec.rb index 759713b3..26be1d99 100644 --- a/spec/serializers/api/slim_point_serializer_spec.rb +++ b/spec/serializers/api/slim_point_serializer_spec.rb @@ -6,8 +6,15 @@ RSpec.describe Api::SlimPointSerializer do describe '#call' do subject(:serializer) { described_class.new(point).call } - let(:point) { create(:point) } - let(:expected_json) { point.attributes.slice('id', 'latitude', 'longitude', 'timestamp') } + let!(:point) { create(:point, :with_known_location) } + let(:expected_json) do + { + id: point.id, + latitude: point.lat.to_s, + longitude: point.lon.to_s, + timestamp: point.timestamp + } + end it 'returns JSON with correct attributes' do expect(serializer.to_json).to eq(expected_json.to_json) diff --git a/spec/serializers/export_serializer_spec.rb b/spec/serializers/export_serializer_spec.rb index 353d53fb..158cd984 100644 --- a/spec/serializers/export_serializer_spec.rb +++ b/spec/serializers/export_serializer_spec.rb @@ -18,8 +18,8 @@ RSpec.describe ExportSerializer do user_email => { 'dawarich-export' => [ { - lat: points.first.latitude, - lon: points.first.longitude, + lat: points.first.lat.to_s, + lon: points.first.lon.to_s, bs: 'u', batt: points.first.battery, p: points.first.ping, @@ -39,8 +39,8 @@ RSpec.describe ExportSerializer do raw_data: points.first.raw_data }, { - lat: points.second.latitude, - lon: points.second.longitude, + lat: points.second.lat.to_s, + lon: points.second.lon.to_s, bs: 'u', batt: points.second.battery, p: points.second.ping, diff --git a/spec/serializers/point_serializer_spec.rb b/spec/serializers/point_serializer_spec.rb index fb35eeef..2f2a9742 100644 --- a/spec/serializers/point_serializer_spec.rb +++ b/spec/serializers/point_serializer_spec.rb @@ -8,15 +8,37 @@ RSpec.describe PointSerializer do let(:point) { create(:point) } let(:expected_json) do - point.attributes.except(*PointSerializer::EXCLUDED_ATTRIBUTES) + { + 'battery_status' => point.battery_status, + 'ping' => point.ping, + 'battery' => point.battery, + 'tracker_id' => point.tracker_id, + 'topic' => point.topic, + 'altitude' => point.altitude, + 'longitude' => point.lon.to_s, + 'velocity' => point.velocity, + 'trigger' => point.trigger, + 'bssid' => point.bssid, + 'ssid' => point.ssid, + 'connection' => point.connection, + 'vertical_accuracy' => point.vertical_accuracy, + 'accuracy' => point.accuracy, + 'timestamp' => point.timestamp, + 'latitude' => point.lat.to_s, + 'mode' => point.mode, + 'inrids' => point.inrids, + 'in_regions' => point.in_regions, + 'city' => point.city, + 'country' => point.country, + 'geodata' => point.geodata, + 'course' => point.course, + 'course_accuracy' => point.course_accuracy, + 'external_track_id' => point.external_track_id + } end - it 'returns JSON' do + it 'returns JSON with correct attributes' do expect(serializer.to_json).to eq(expected_json.to_json) end - - it 'does not include excluded attributes' do - expect(serializer).not_to include(*PointSerializer::EXCLUDED_ATTRIBUTES) - end end end diff --git a/spec/serializers/points/geojson_serializer_spec.rb b/spec/serializers/points/geojson_serializer_spec.rb index e125c7b3..6ba9fd37 100644 --- a/spec/serializers/points/geojson_serializer_spec.rb +++ b/spec/serializers/points/geojson_serializer_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Points::GeojsonSerializer do type: 'Feature', geometry: { type: 'Point', - coordinates: [point.longitude, point.latitude] + coordinates: [point.lon.to_s, point.lat.to_s] }, properties: PointSerializer.new(point).call } diff --git a/spec/serializers/points/gpx_serializer_spec.rb b/spec/serializers/points/gpx_serializer_spec.rb index 1434ca5d..7445862d 100644 --- a/spec/serializers/points/gpx_serializer_spec.rb +++ b/spec/serializers/points/gpx_serializer_spec.rb @@ -24,8 +24,8 @@ RSpec.describe Points::GpxSerializer do serializer.tracks[0].points.each_with_index do |track_point, index| point = points[index] - expect(track_point.lat).to eq(point.latitude) - expect(track_point.lon).to eq(point.longitude) + expect(track_point.lat.to_s).to eq(point.lat.to_s) + expect(track_point.lon.to_s).to eq(point.lon.to_s) expect(track_point.time).to eq(point.recorded_at) end end diff --git a/spec/services/areas/visits/create_spec.rb b/spec/services/areas/visits/create_spec.rb index 28d4aa48..18865d6a 100644 --- a/spec/services/areas/visits/create_spec.rb +++ b/spec/services/areas/visits/create_spec.rb @@ -23,14 +23,14 @@ RSpec.describe Areas::Visits::Create do context 'when there are points' do let(:home_visit_date) { DateTime.new(2021, 1, 1, 10, 0, 0, Time.zone.formatted_offset) } - let!(:home_point1) { create(:point, user:, latitude: 0, longitude: 0, timestamp: home_visit_date) } - let!(:home_point2) { create(:point, user:, latitude: 0, longitude: 0, timestamp: home_visit_date + 10.minutes) } - let!(:home_point3) { create(:point, user:, latitude: 0, longitude: 0, timestamp: home_visit_date + 20.minutes) } + let!(:home_point1) { create(:point, user:, lonlat: 'POINT(0 0)', timestamp: home_visit_date) } + let!(:home_point2) { create(:point, user:, lonlat: 'POINT(0 0)', timestamp: home_visit_date + 10.minutes) } + let!(:home_point3) { create(:point, user:, lonlat: 'POINT(0 0)', timestamp: home_visit_date + 20.minutes) } let(:work_visit_date) { DateTime.new(2021, 1, 1, 12, 0, 0, Time.zone.formatted_offset) } - let!(:work_point1) { create(:point, user:, latitude: 1, longitude: 1, timestamp: work_visit_date) } - let!(:work_point2) { create(:point, user:, latitude: 1, longitude: 1, timestamp: work_visit_date + 10.minutes) } - let!(:work_point3) { create(:point, user:, latitude: 1, longitude: 1, timestamp: work_visit_date + 20.minutes) } + let!(:work_point1) { create(:point, user:, lonlat: 'POINT(1 1)', timestamp: work_visit_date) } + let!(:work_point2) { create(:point, user:, lonlat: 'POINT(1 1)', timestamp: work_visit_date + 10.minutes) } + let!(:work_point3) { create(:point, user:, lonlat: 'POINT(1 1)', timestamp: work_visit_date + 20.minutes) } it 'creates visits' do expect { create_visits }.to change { Visit.count }.by(2) @@ -47,7 +47,7 @@ RSpec.describe Areas::Visits::Create do end context 'when there are points outside the time threshold' do - let(:home_point4) { create(:point, user:, latitude: 0, longitude: 0, timestamp: home_visit_date + 40.minutes) } + let(:home_point4) { create(:point, user:, lonlat: 'POINT(0 0)', timestamp: home_visit_date + 40.minutes) } it 'does not create visits' do expect { create_visits }.to change { Visit.count }.by(2) diff --git a/spec/services/geojson/params_spec.rb b/spec/services/geojson/params_spec.rb index ed0e245b..817cd85f 100644 --- a/spec/services/geojson/params_spec.rb +++ b/spec/services/geojson/params_spec.rb @@ -18,8 +18,7 @@ RSpec.describe Geojson::Params do it 'returns the correct data for each point' do expect(subject.first).to eq( - latitude: '0.0', - longitude: '0.0', + lonlat: 'POINT(0.1 0.1)', battery_status: nil, battery: nil, timestamp: Time.zone.at(1_609_459_201), @@ -34,8 +33,8 @@ RSpec.describe Geojson::Params do 'geometry' => { 'type' => 'Point', 'coordinates' => [ - '0.0', - '0.0' + '0.1', + '0.1' ] }, 'properties' => { @@ -72,8 +71,7 @@ RSpec.describe Geojson::Params do it 'returns the correct data for each point' do expect(subject.first).to eq( - latitude: 10.758321212464024, - longitude: 106.64234449272531, + lonlat: 'POINT(106.64234449272531 10.758321212464024)', battery_status: nil, battery: nil, timestamp: Time.parse('2024-11-03T16:30:11.331+07:00').to_i, diff --git a/spec/services/google_maps/phone_takeout_parser_spec.rb b/spec/services/google_maps/phone_takeout_parser_spec.rb index ff4505dd..3050abb7 100644 --- a/spec/services/google_maps/phone_takeout_parser_spec.rb +++ b/spec/services/google_maps/phone_takeout_parser_spec.rb @@ -9,6 +9,7 @@ RSpec.describe GoogleMaps::PhoneTakeoutParser do let(:user) { create(:user) } context 'when file content is an object' do + # This file contains 3 duplicates let(:file_path) { Rails.root.join('spec/fixtures/files/google/phone-takeout.json') } let(:raw_data) { JSON.parse(File.read(file_path)) } let(:import) { create(:import, user:, name: 'phone_takeout.json', raw_data:) } @@ -21,6 +22,7 @@ RSpec.describe GoogleMaps::PhoneTakeoutParser do end context 'when file content is an array' do + # This file contains 4 duplicates let(:file_path) { Rails.root.join('spec/fixtures/files/google/location-history.json') } let(:raw_data) { JSON.parse(File.read(file_path)) } let(:import) { create(:import, user:, name: 'phone_takeout.json', raw_data:) } @@ -33,12 +35,12 @@ RSpec.describe GoogleMaps::PhoneTakeoutParser do it 'creates points with correct data' do parser - expect(Point.all[6].latitude).to eq(27.696576.to_d) - expect(Point.all[6].longitude).to eq(-97.376949.to_d) + expect(Point.all[6].lat).to eq(27.696576) + expect(Point.all[6].lon).to eq(-97.376949) expect(Point.all[6].timestamp).to eq(1_693_180_140) - expect(Point.last.latitude).to eq(27.709617.to_d) - expect(Point.last.longitude).to eq(-97.375988.to_d) + expect(Point.last.lat).to eq(27.709617) + expect(Point.last.lon).to eq(-97.375988) expect(Point.last.timestamp).to eq(1_693_180_320) end end diff --git a/spec/services/google_maps/records_importer_spec.rb b/spec/services/google_maps/records_importer_spec.rb index 8ce4d69d..e2761d4f 100644 --- a/spec/services/google_maps/records_importer_spec.rb +++ b/spec/services/google_maps/records_importer_spec.rb @@ -56,8 +56,7 @@ RSpec.describe GoogleMaps::RecordsImporter do :point, user: import.user, import: import, - latitude: 12.3456789, - longitude: 12.3456789, + lonlat: 'POINT(12.3456789 12.3456789)', timestamp: time.to_i ) end diff --git a/spec/services/google_maps/semantic_history_parser_spec.rb b/spec/services/google_maps/semantic_history_parser_spec.rb index 2a2fc207..9859c930 100644 --- a/spec/services/google_maps/semantic_history_parser_spec.rb +++ b/spec/services/google_maps/semantic_history_parser_spec.rb @@ -80,7 +80,7 @@ RSpec.describe GoogleMaps::SemanticHistoryParser do { 'activitySegment' => { 'startLocation' => { 'latitudeE7' => 123_456_789, 'longitudeE7' => 123_456_789 }, - 'duration' => { 'startTimestamp' => (time.to_i).to_s } + 'duration' => { 'startTimestamp' => time.to_i.to_s } } } end diff --git a/spec/services/gpx/track_parser_spec.rb b/spec/services/gpx/track_importer_spec.rb similarity index 85% rename from spec/services/gpx/track_parser_spec.rb rename to spec/services/gpx/track_importer_spec.rb index 02fa3110..bef491c9 100644 --- a/spec/services/gpx/track_parser_spec.rb +++ b/spec/services/gpx/track_importer_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Gpx::TrackParser do +RSpec.describe Gpx::TrackImporter do describe '#call' do subject(:parser) { described_class.new(import, user.id).call } @@ -17,7 +17,7 @@ RSpec.describe Gpx::TrackParser do end it 'broadcasts importing progress' do - expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(10).times + expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(1).time parser end @@ -31,7 +31,7 @@ RSpec.describe Gpx::TrackParser do end it 'broadcasts importing progress' do - expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(43).times + expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(1).time parser end @@ -45,7 +45,7 @@ RSpec.describe Gpx::TrackParser do end it 'broadcasts importing progress' do - expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(34).times + expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(1).time parser end @@ -53,8 +53,8 @@ RSpec.describe Gpx::TrackParser do it 'creates points with correct data' do parser - expect(Point.first.latitude).to eq(37.17221.to_d) - expect(Point.first.longitude).to eq(-3.55468.to_d) + expect(Point.first.lat).to eq(37.1722103) + expect(Point.first.lon).to eq(-3.55468) expect(Point.first.altitude).to eq(1066) expect(Point.first.timestamp).to eq(Time.zone.parse('2024-04-21T10:19:55Z').to_i) expect(Point.first.velocity).to eq('2.9') @@ -67,8 +67,8 @@ RSpec.describe Gpx::TrackParser do it 'creates points with correct data' do parser - expect(Point.first.latitude).to eq(10.758321.to_d) - expect(Point.first.longitude).to eq(106.642344.to_d) + expect(Point.first.lat).to eq(10.758321212464024) + expect(Point.first.lon).to eq(106.64234449272531) expect(Point.first.altitude).to eq(17) expect(Point.first.timestamp).to eq(1_730_626_211) expect(Point.first.velocity).to eq('2.8') diff --git a/spec/services/imports/create_spec.rb b/spec/services/imports/create_spec.rb index 08b1c60d..961d014c 100644 --- a/spec/services/imports/create_spec.rb +++ b/spec/services/imports/create_spec.rb @@ -30,8 +30,8 @@ RSpec.describe Imports::Create do context 'when source is owntracks' do let(:import) { create(:import, source: 'owntracks') } - it 'calls the OwnTracks::ExportParser' do - expect(OwnTracks::ExportParser).to \ + it 'calls the OwnTracks::Importer' do + expect(OwnTracks::Importer).to \ receive(:new).with(import, user.id).and_return(double(call: true)) service.call end @@ -59,7 +59,7 @@ RSpec.describe Imports::Create do context 'when import fails' do before do - allow(OwnTracks::ExportParser).to receive(:new).with(import, user.id).and_raise(StandardError) + allow(OwnTracks::Importer).to receive(:new).with(import, user.id).and_raise(StandardError) end it 'creates a failed notification' do @@ -73,8 +73,8 @@ RSpec.describe Imports::Create do context 'when source is gpx' do let(:import) { create(:import, source: 'gpx') } - it 'calls the Gpx::TrackParser' do - expect(Gpx::TrackParser).to \ + it 'calls the Gpx::TrackImporter' do + expect(Gpx::TrackImporter).to \ receive(:new).with(import, user.id).and_return(double(call: true)) service.call end diff --git a/spec/services/overland/params_spec.rb b/spec/services/overland/params_spec.rb index 8ba3fd86..78bc5bf8 100644 --- a/spec/services/overland/params_spec.rb +++ b/spec/services/overland/params_spec.rb @@ -11,8 +11,7 @@ RSpec.describe Overland::Params do let(:expected_json) do { - latitude: 37.3318, - longitude: -122.030581, + lonlat: 'POINT(-122.030581 37.3318)', battery_status: 'charging', battery: 89, altitude: 0, @@ -31,8 +30,6 @@ RSpec.describe Overland::Params do it 'returns a hash with the correct keys' do expect(params[0].keys).to match_array( %i[ - latitude - longitude battery_status battery altitude @@ -43,6 +40,7 @@ RSpec.describe Overland::Params do tracker_id timestamp raw_data + lonlat ] ) end diff --git a/spec/services/own_tracks/export_parser_spec.rb b/spec/services/own_tracks/importer_spec.rb similarity index 88% rename from spec/services/own_tracks/export_parser_spec.rb rename to spec/services/own_tracks/importer_spec.rb index 260b81ec..2e3e16e9 100644 --- a/spec/services/own_tracks/export_parser_spec.rb +++ b/spec/services/own_tracks/importer_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe OwnTracks::ExportParser do +RSpec.describe OwnTracks::Importer do describe '#call' do subject(:parser) { described_class.new(import, user.id).call } @@ -17,9 +17,10 @@ RSpec.describe OwnTracks::ExportParser do it 'correctly writes attributes' do parser - expect(Point.first.attributes).to include( - 'latitude' => 52.225, - 'longitude' => 13.332, + point = Point.first + expect(point.lonlat.x).to be_within(0.001).of(13.332) + expect(point.lonlat.y).to be_within(0.001).of(52.225) + expect(point.attributes.except('lonlat')).to include( 'battery_status' => 'charging', 'battery' => 94, 'ping' => '100.266', diff --git a/spec/services/own_tracks/params_spec.rb b/spec/services/own_tracks/params_spec.rb index 40605759..d08f5b30 100644 --- a/spec/services/own_tracks/params_spec.rb +++ b/spec/services/own_tracks/params_spec.rb @@ -13,8 +13,7 @@ RSpec.describe OwnTracks::Params do let(:expected_json) do { - latitude: 52.225, - longitude: 13.332, + lonlat: 'POINT(13.332 52.225)', battery: 94, ping: 100.266, altitude: 36, diff --git a/spec/services/photos/import_parser_spec.rb b/spec/services/photos/import_parser_spec.rb index 33460398..25cae787 100644 --- a/spec/services/photos/import_parser_spec.rb +++ b/spec/services/photos/import_parser_spec.rb @@ -23,13 +23,13 @@ RSpec.describe Photos::ImportParser do it 'creates points with correct attributes' do service - expect(Point.first.latitude.to_f).to eq(59.0000) - expect(Point.first.longitude.to_f).to eq(30.0000) + expect(Point.first.lat.to_f).to eq(59.0000) + expect(Point.first.lon.to_f).to eq(30.0000) expect(Point.first.timestamp).to eq(978_296_400) expect(Point.first.import_id).to eq(import.id) - expect(Point.second.latitude.to_f).to eq(55.0001) - expect(Point.second.longitude.to_f).to eq(37.0001) + expect(Point.second.lat.to_f).to eq(55.0001) + expect(Point.second.lon.to_f).to eq(37.0001) expect(Point.second.timestamp).to eq(978_296_400) expect(Point.second.import_id).to eq(import.id) end @@ -37,7 +37,7 @@ RSpec.describe Photos::ImportParser do context 'when there are points with the same coordinates' do let!(:existing_point) do - create(:point, latitude: 59.0000, longitude: 30.0000, timestamp: 978_296_400, user:) + create(:point, lonlat: 'POINT(30.0000 59.0000)', timestamp: 978_296_400, user:) end it 'creates only new points' do diff --git a/spec/services/reverse_geocoding/points/fetch_data_spec.rb b/spec/services/reverse_geocoding/points/fetch_data_spec.rb index a33ab054..ecef9bbb 100644 --- a/spec/services/reverse_geocoding/points/fetch_data_spec.rb +++ b/spec/services/reverse_geocoding/points/fetch_data_spec.rb @@ -27,7 +27,7 @@ RSpec.describe ReverseGeocoding::Points::FetchData do it 'calls Geocoder' do fetch_data - expect(Geocoder).to have_received(:search).with([point.latitude, point.longitude]) + expect(Geocoder).to have_received(:search).with([point.lat, point.lon]) end end diff --git a/spec/services/stats/calculate_month_spec.rb b/spec/services/stats/calculate_month_spec.rb index a5ec4383..fb2b26b1 100644 --- a/spec/services/stats/calculate_month_spec.rb +++ b/spec/services/stats/calculate_month_spec.rb @@ -26,24 +26,21 @@ RSpec.describe Stats::CalculateMonth do user:, import:, timestamp: timestamp1, - latitude: 52.107902115161316, - longitude: 14.452712811406352) + lonlat: 'POINT(14.452712811406352 52.107902115161316)') end let!(:point2) do create(:point, user:, import:, timestamp: timestamp2, - latitude: 51.9746598171507, - longitude: 12.291519487061901) + lonlat: 'POINT(12.291519487061901 51.9746598171507)') end let!(:point3) do create(:point, user:, import:, timestamp: timestamp3, - latitude: 52.72859111523629, - longitude: 9.77973105800526) + lonlat: 'POINT(9.77973105800526 52.72859111523629)') end context 'when units are kilometers' do @@ -56,7 +53,7 @@ RSpec.describe Stats::CalculateMonth do it 'calculates distance' do calculate_stats - expect(user.stats.last.distance).to eq(338) + expect(user.stats.last.distance).to eq(339) end context 'when there is an error' do @@ -84,7 +81,7 @@ RSpec.describe Stats::CalculateMonth do it 'calculates distance' do calculate_stats - expect(user.stats.last.distance).to eq(210) + expect(user.stats.last.distance).to eq(211) end context 'when there is an error' do diff --git a/spec/services/tracks/build_path_spec.rb b/spec/services/tracks/build_path_spec.rb index 1d2db10a..e42dbb83 100644 --- a/spec/services/tracks/build_path_spec.rb +++ b/spec/services/tracks/build_path_spec.rb @@ -6,9 +6,9 @@ RSpec.describe Tracks::BuildPath do describe '#call' do let(:coordinates) do [ - [45.123456, -122.654321], # [lat, lng] - [45.234567, -122.765432], - [45.345678, -122.876543] + RGeo::Geographic.spherical_factory.point(-122.654321, 45.123456), + RGeo::Geographic.spherical_factory.point(-122.765432, 45.234567), + RGeo::Geographic.spherical_factory.point(-122.876543, 45.345678) ] end @@ -26,9 +26,9 @@ RSpec.describe Tracks::BuildPath do it 'correctly converts coordinates to points with rounded values' do points = result.points - coordinates.each_with_index do |(lat, lng), index| - expect(points[index].x).to eq(lng.to_f.round(5)) - expect(points[index].y).to eq(lat.to_f.round(5)) + coordinates.each_with_index do |coordinate_pair, index| + expect(points[index].x).to eq(coordinate_pair.lon.to_f.round(5)) + expect(points[index].y).to eq(coordinate_pair.lat.to_f.round(5)) end end end diff --git a/spec/services/visits/group_points_spec.rb b/spec/services/visits/group_points_spec.rb index 3dff1296..f672ddee 100644 --- a/spec/services/visits/group_points_spec.rb +++ b/spec/services/visits/group_points_spec.rb @@ -6,17 +6,17 @@ RSpec.describe Visits::GroupPoints do describe '#group_points_by_radius' do it 'groups points by radius' do day_points = [ - build(:point, latitude: 0, longitude: 0, timestamp: 1.day.ago), - build(:point, latitude: 0.00001, longitude: 0.00001, timestamp: 1.day.ago + 1.minute), - build(:point, latitude: 0.00002, longitude: 0.00002, timestamp: 1.day.ago + 2.minutes), - build(:point, latitude: 0.00003, longitude: 0.00003, timestamp: 1.day.ago + 3.minutes), - build(:point, latitude: 0.00004, longitude: 0.00004, timestamp: 1.day.ago + 4.minutes), - build(:point, latitude: 0.00005, longitude: 0.00005, timestamp: 1.day.ago + 5.minutes), - build(:point, latitude: 0.00006, longitude: 0.00006, timestamp: 1.day.ago + 6.minutes), - build(:point, latitude: 0.00007, longitude: 0.00007, timestamp: 1.day.ago + 7.minutes), - build(:point, latitude: 0.00008, longitude: 0.00008, timestamp: 1.day.ago + 8.minutes), - build(:point, latitude: 0.00009, longitude: 0.00009, timestamp: 1.day.ago + 9.minutes), - build(:point, latitude: 0.0001, longitude: 0.0009, timestamp: 1.day.ago + 9.minutes) + build(:point, lonlat: 'POINT(0 0)', timestamp: 1.day.ago), + build(:point, lonlat: 'POINT(0.00001 0.00001)', timestamp: 1.day.ago + 1.minute), + build(:point, lonlat: 'POINT(0.00002 0.00002)', timestamp: 1.day.ago + 2.minutes), + build(:point, lonlat: 'POINT(0.00003 0.00003)', timestamp: 1.day.ago + 3.minutes), + build(:point, lonlat: 'POINT(0.00004 0.00004)', timestamp: 1.day.ago + 4.minutes), + build(:point, lonlat: 'POINT(0.00005 0.00005)', timestamp: 1.day.ago + 5.minutes), + build(:point, lonlat: 'POINT(0.00006 0.00006)', timestamp: 1.day.ago + 6.minutes), + build(:point, lonlat: 'POINT(0.00007 0.00007)', timestamp: 1.day.ago + 7.minutes), + build(:point, lonlat: 'POINT(0.00008 0.00008)', timestamp: 1.day.ago + 8.minutes), + build(:point, lonlat: 'POINT(0.00009 0.00009)', timestamp: 1.day.ago + 9.minutes), + build(:point, lonlat: 'POINT(0.001 0.001)', timestamp: 1.day.ago + 10.minutes) ] grouped_points = described_class.new(day_points).group_points_by_radius diff --git a/spec/services/visits/group_spec.rb b/spec/services/visits/group_spec.rb index f9726243..5b69e66e 100644 --- a/spec/services/visits/group_spec.rb +++ b/spec/services/visits/group_spec.rb @@ -14,21 +14,21 @@ RSpec.describe Visits::Group do context 'when points are too far apart' do it 'groups points into separate visits' do points = [ - build(:point, latitude: 0, longitude: 0, timestamp: 1.day.ago), - build(:point, latitude: 0.00001, longitude: 0.00001, timestamp: 1.day.ago + 5.minutes), - build(:point, latitude: 0.00002, longitude: 0.00002, timestamp: 1.day.ago + 10.minutes), - build(:point, latitude: 0.00003, longitude: 0.00003, timestamp: 1.day.ago + 15.minutes), - build(:point, latitude: 0.00004, longitude: 0.00004, timestamp: 1.day.ago + 20.minutes), - build(:point, latitude: 0.00005, longitude: 0.00005, timestamp: 1.day.ago + 25.minutes), - build(:point, latitude: 0.00006, longitude: 0.00006, timestamp: 1.day.ago + 30.minutes), - build(:point, latitude: 0.00007, longitude: 0.00007, timestamp: 1.day.ago + 35.minutes), - build(:point, latitude: 0.00008, longitude: 0.00008, timestamp: 1.day.ago + 40.minutes), - build(:point, latitude: 0.00009, longitude: 0.00009, timestamp: 1.day.ago + 45.minutes), - build(:point, latitude: 0.0001, longitude: 0.0001, timestamp: 1.day.ago + 50.minutes), - build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 55.minutes), - build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 95.minutes), - build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 100.minutes), - build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 105.minutes) + build(:point, lonlat: 'POINT(0 0)', timestamp: 1.day.ago), + build(:point, lonlat: 'POINT(0.00001 0.00001)', timestamp: 1.day.ago + 5.minutes), + build(:point, lonlat: 'POINT(0.00002 0.00002)', timestamp: 1.day.ago + 10.minutes), + build(:point, lonlat: 'POINT(0.00003 0.00003)', timestamp: 1.day.ago + 15.minutes), + build(:point, lonlat: 'POINT(0.00004 0.00004)', timestamp: 1.day.ago + 20.minutes), + build(:point, lonlat: 'POINT(0.00005 0.00005)', timestamp: 1.day.ago + 25.minutes), + build(:point, lonlat: 'POINT(0.00006 0.00006)', timestamp: 1.day.ago + 30.minutes), + build(:point, lonlat: 'POINT(0.00007 0.00007)', timestamp: 1.day.ago + 35.minutes), + build(:point, lonlat: 'POINT(0.00008 0.00008)', timestamp: 1.day.ago + 40.minutes), + build(:point, lonlat: 'POINT(0.00009 0.00009)', timestamp: 1.day.ago + 45.minutes), + build(:point, lonlat: 'POINT(0.0001 0.0001)', timestamp: 1.day.ago + 50.minutes), + build(:point, lonlat: 'POINT(0.00011 0.00011)', timestamp: 1.day.ago + 55.minutes), + build(:point, lonlat: 'POINT(0.00011 0.00011)', timestamp: 1.day.ago + 95.minutes), + build(:point, lonlat: 'POINT(0.00011 0.00011)', timestamp: 1.day.ago + 100.minutes), + build(:point, lonlat: 'POINT(0.00011 0.00011)', timestamp: 1.day.ago + 105.minutes) ] expect(group.call(points)).to \ eq({ diff --git a/spec/services/visits/prepare_spec.rb b/spec/services/visits/prepare_spec.rb index 89ec0143..39935bd1 100644 --- a/spec/services/visits/prepare_spec.rb +++ b/spec/services/visits/prepare_spec.rb @@ -7,21 +7,21 @@ RSpec.describe Visits::Prepare do let(:static_time) { Time.zone.local(2021, 1, 1, 0, 0, 0) } let(:points) do [ - build(:point, latitude: 0, longitude: 0, timestamp: static_time), - build(:point, latitude: 0.00001, longitude: 0.00001, timestamp: static_time + 5.minutes), - build(:point, latitude: 0.00002, longitude: 0.00002, timestamp: static_time + 10.minutes), - build(:point, latitude: 0.00003, longitude: 0.00003, timestamp: static_time + 15.minutes), - build(:point, latitude: 0.00004, longitude: 0.00004, timestamp: static_time + 20.minutes), - build(:point, latitude: 0.00005, longitude: 0.00005, timestamp: static_time + 25.minutes), - build(:point, latitude: 0.00006, longitude: 0.00006, timestamp: static_time + 30.minutes), - build(:point, latitude: 0.00007, longitude: 0.00007, timestamp: static_time + 35.minutes), - build(:point, latitude: 0.00008, longitude: 0.00008, timestamp: static_time + 40.minutes), - build(:point, latitude: 0.00009, longitude: 0.00009, timestamp: static_time + 45.minutes), - build(:point, latitude: 0.0001, longitude: 0.0001, timestamp: static_time + 50.minutes), - build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: static_time + 55.minutes), - build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: static_time + 95.minutes), - build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: static_time + 100.minutes), - build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: static_time + 105.minutes) + build(:point, lonlat: 'POINT(0 0)', timestamp: static_time), + build(:point, lonlat: 'POINT(0.00001 0.00001)', timestamp: static_time + 5.minutes), + build(:point, lonlat: 'POINT(0.00002 0.00002)', timestamp: static_time + 10.minutes), + build(:point, lonlat: 'POINT(0.00003 0.00003)', timestamp: static_time + 15.minutes), + build(:point, lonlat: 'POINT(0.00004 0.00004)', timestamp: static_time + 20.minutes), + build(:point, lonlat: 'POINT(0.00005 0.00005)', timestamp: static_time + 25.minutes), + build(:point, lonlat: 'POINT(0.00006 0.00006)', timestamp: static_time + 30.minutes), + build(:point, lonlat: 'POINT(0.00007 0.00007)', timestamp: static_time + 35.minutes), + build(:point, lonlat: 'POINT(0.00008 0.00008)', timestamp: static_time + 40.minutes), + build(:point, lonlat: 'POINT(0.00009 0.00009)', timestamp: static_time + 45.minutes), + build(:point, lonlat: 'POINT(0.0001 0.0001)', timestamp: static_time + 50.minutes), + build(:point, lonlat: 'POINT(0.00011 0.00011)', timestamp: static_time + 55.minutes), + build(:point, lonlat: 'POINT(0.00011 0.00011)', timestamp: static_time + 95.minutes), + build(:point, lonlat: 'POINT(0.00011 0.00011)', timestamp: static_time + 100.minutes), + build(:point, lonlat: 'POINT(0.00011 0.00011)', timestamp: static_time + 105.minutes) ] end