Unify timestamps

This commit is contained in:
Eugene Burmakin 2025-07-07 23:38:10 +02:00
parent a66f41d9fb
commit e64e706b0f
13 changed files with 39 additions and 44 deletions

View file

@ -1913,6 +1913,7 @@ export default class extends BaseController {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
api_key: this.apiKey

View file

@ -33,7 +33,7 @@ class Point < ApplicationRecord
after_create :async_reverse_geocode, if: -> { DawarichSettings.store_geodata? && !reverse_geocoded? }
after_create :set_country
after_create_commit :broadcast_coordinates
after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? } # Only for real-time points
after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? }
after_commit :recalculate_track, on: :update
def self.without_raw_data
@ -66,6 +66,20 @@ class Point < ApplicationRecord
Country.containing_point(lon, lat)
end
def self.normalize_timestamp(timestamp)
case timestamp
when Integer
timestamp
when String, Numeric, DateTime, Time
timestamp.to_i
when nil
raise ArgumentError, 'Timestamp cannot be nil'
else
raise ArgumentError, "Cannot convert timestamp to integer: #{timestamp.class}"
end
end
private
# rubocop:disable Metrics/MethodLength Metrics/AbcSize

View file

@ -42,7 +42,7 @@ class Gpx::TrackImporter
{
lonlat: "POINT(#{point['lon'].to_d} #{point['lat'].to_d})",
altitude: point['ele'].to_i,
timestamp: Time.parse(point['time']).to_i,
timestamp: Point.normalize_timestamp(point['time']),
import_id: import.id,
velocity: speed(point),
raw_data: point,

View file

@ -56,7 +56,7 @@ class Immich::ImportGeodata
latitude: asset['exifInfo']['latitude'],
longitude: asset['exifInfo']['longitude'],
lonlat: "SRID=4326;POINT(#{asset['exifInfo']['longitude']} #{asset['exifInfo']['latitude']})",
timestamp: Time.zone.parse(asset['exifInfo']['dateTimeOriginal']).to_i
timestamp: Point.normalize_timestamp(asset['exifInfo']['dateTimeOriginal'])
}
end

View file

@ -16,7 +16,7 @@ class Overland::Params
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]),
timestamp: Point.normalize_timestamp(point[:properties][:timestamp]),
altitude: point[:properties][:altitude],
velocity: point[:properties][:speed],
tracker_id: point[:properties][:device_id],

View file

@ -66,7 +66,7 @@ class Photoprism::ImportGeodata
latitude: asset['Lat'],
longitude: asset['Lng'],
lonlat: "SRID=4326;POINT(#{asset['Lng']} #{asset['Lat']})",
timestamp: Time.zone.parse(asset['TakenAt']).to_i
timestamp: Point.normalize_timestamp(asset['TakenAt'])
}
end

View file

@ -17,7 +17,7 @@ class Points::Params
lonlat: lonlat(point),
battery_status: point[:properties][:battery_state],
battery: battery_level(point[:properties][:battery_level]),
timestamp: DateTime.parse(point[:properties][:timestamp]),
timestamp: normalize_timestamp(point[:properties][:timestamp]),
altitude: point[:properties][:altitude],
tracker_id: point[:properties][:device_id],
velocity: point[:properties][:speed],
@ -48,4 +48,8 @@ class Points::Params
def lonlat(point)
"POINT(#{point[:geometry][:coordinates][0]} #{point[:geometry][:coordinates][1]})"
end
def normalize_timestamp(timestamp)
Point.normalize_timestamp(DateTime.parse(timestamp))
end
end

View file

@ -51,7 +51,7 @@ module Tracks
Point.transaction do
# Clean up existing tracks if needed
track_cleaner.cleanup_if_needed
track_cleaner.cleanup
# Load points using the configured strategy
points = point_loader.load_points

View file

@ -11,8 +11,6 @@ class Tracks::RedisBuffer
@day = day.is_a?(Date) ? day : Date.parse(day.to_s)
end
# Store buffered points for an incomplete track segment
# @param points [Array<Point>] array of Point objects to buffer
def store(points)
return if points.empty?
@ -23,8 +21,6 @@ class Tracks::RedisBuffer
Rails.logger.debug "Stored #{points.size} points in buffer for user #{user_id}, day #{day}"
end
# Retrieve buffered points for the user/day combination
# @return [Array<Hash>] array of point hashes or empty array if no buffer exists
def retrieve
redis_key = buffer_key
cached_data = Rails.cache.read(redis_key)
@ -44,8 +40,6 @@ class Tracks::RedisBuffer
Rails.logger.debug "Cleared buffer for user #{user_id}, day #{day}"
end
# Check if a buffer exists for the user/day combination
# @return [Boolean] true if buffer exists, false otherwise
def exists?
Rails.cache.exist?(buffer_key)
end

View file

@ -42,9 +42,6 @@ module Tracks::Segmentation
private
# Split an array of points into track segments based on time and distance thresholds
# @param points [Array] array of Point objects or point hashes
# @return [Array<Array>] array of point segments
def split_points_into_segments(points)
return [] if points.empty?
@ -67,10 +64,6 @@ module Tracks::Segmentation
segments
end
# Check if a new segment should start based on time and distance thresholds
# @param current_point [Point, Hash] current point (Point object or hash)
# @param previous_point [Point, Hash, nil] previous point or nil
# @return [Boolean] true if new segment should start
def should_start_new_segment?(current_point, previous_point)
return false if previous_point.nil?
@ -91,10 +84,6 @@ module Tracks::Segmentation
false
end
# Calculate distance between two points in kilometers
# @param point1 [Point, Hash] first point
# @param point2 [Point, Hash] second point
# @return [Float] distance in kilometers
def calculate_distance_kilometers_between_points(point1, point2)
lat1, lon1 = point_coordinates(point1)
lat2, lon2 = point_coordinates(point2)
@ -103,10 +92,6 @@ module Tracks::Segmentation
Geocoder::Calculations.distance_between([lat1, lon1], [lat2, lon2], units: :km)
end
# Check if a segment should be finalized (has a large enough gap at the end)
# @param segment_points [Array] array of points in the segment
# @param grace_period_minutes [Integer] grace period in minutes (default 5)
# @return [Boolean] true if segment should be finalized
def should_finalize_segment?(segment_points, grace_period_minutes = 5)
return false if segment_points.size < 2
@ -121,22 +106,19 @@ module Tracks::Segmentation
time_since_last_point > grace_period_seconds
end
# Extract timestamp from point (handles both Point objects and hashes)
# @param point [Point, Hash] point object or hash
# @return [Integer] timestamp as integer
def point_timestamp(point)
if point.respond_to?(:timestamp)
# Point objects from database always have integer timestamps
point.timestamp
elsif point.is_a?(Hash)
point[:timestamp] || point['timestamp']
# Hash might come from Redis buffer or test data
timestamp = point[:timestamp] || point['timestamp']
timestamp.to_i
else
raise ArgumentError, "Invalid point type: #{point.class}"
end
end
# Extract coordinates from point (handles both Point objects and hashes)
# @param point [Point, Hash] point object or hash
# @return [Array<Float>] [lat, lon] coordinates
def point_coordinates(point)
if point.respond_to?(:lat) && point.respond_to?(:lon)
[point.lat, point.lon]

View file

@ -7,7 +7,7 @@ module Tracks
@user = user
end
def cleanup_if_needed
def cleanup
# No cleanup needed for incremental processing
# We only append new tracks, don't remove existing ones
end

View file

@ -24,7 +24,7 @@
#
# Example usage:
# cleaner = Tracks::TrackCleaners::ReplaceCleaner.new(user, start_at: 1.week.ago, end_at: Time.current)
# cleaner.cleanup_if_needed
# cleaner.cleanup
#
module Tracks
module TrackCleaners
@ -37,7 +37,7 @@ module Tracks
@end_at = end_at
end
def cleanup_if_needed
def cleanup
tracks_to_remove = find_tracks_to_remove
if tracks_to_remove.any?

View file

@ -26,7 +26,7 @@ RSpec.describe Tracks::Generator do
describe '#call' do
context 'with no points to process' do
before do
allow(track_cleaner).to receive(:cleanup_if_needed)
allow(track_cleaner).to receive(:cleanup)
allow(point_loader).to receive(:load_points).and_return([])
end
@ -54,7 +54,7 @@ RSpec.describe Tracks::Generator do
end
before do
allow(track_cleaner).to receive(:cleanup_if_needed)
allow(track_cleaner).to receive(:cleanup)
allow(point_loader).to receive(:load_points).and_return(points)
allow(incomplete_segment_handler).to receive(:should_finalize_segment?).and_return(true)
allow(incomplete_segment_handler).to receive(:cleanup_processed_data)
@ -91,7 +91,7 @@ RSpec.describe Tracks::Generator do
end
before do
allow(track_cleaner).to receive(:cleanup_if_needed)
allow(track_cleaner).to receive(:cleanup)
allow(point_loader).to receive(:load_points).and_return(points)
allow(incomplete_segment_handler).to receive(:should_finalize_segment?).and_return(false)
allow(incomplete_segment_handler).to receive(:handle_incomplete_segment)
@ -129,7 +129,7 @@ RSpec.describe Tracks::Generator do
end
before do
allow(track_cleaner).to receive(:cleanup_if_needed)
allow(track_cleaner).to receive(:cleanup)
allow(point_loader).to receive(:load_points).and_return(old_points + recent_points)
# First segment (old points) should be finalized
@ -168,7 +168,7 @@ RSpec.describe Tracks::Generator do
end
before do
allow(track_cleaner).to receive(:cleanup_if_needed)
allow(track_cleaner).to receive(:cleanup)
allow(point_loader).to receive(:load_points).and_return(single_point)
allow(incomplete_segment_handler).to receive(:should_finalize_segment?).and_return(true)
allow(incomplete_segment_handler).to receive(:cleanup_processed_data)
@ -186,7 +186,7 @@ RSpec.describe Tracks::Generator do
context 'error handling' do
before do
allow(track_cleaner).to receive(:cleanup_if_needed)
allow(track_cleaner).to receive(:cleanup)
allow(point_loader).to receive(:load_points).and_raise(StandardError, 'Point loading failed')
end