mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Unify timestamps
This commit is contained in:
parent
a66f41d9fb
commit
e64e706b0f
13 changed files with 39 additions and 44 deletions
|
|
@ -1913,6 +1913,7 @@ export default class extends BaseController {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
api_key: this.apiKey
|
api_key: this.apiKey
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class Point < ApplicationRecord
|
||||||
after_create :async_reverse_geocode, if: -> { DawarichSettings.store_geodata? && !reverse_geocoded? }
|
after_create :async_reverse_geocode, if: -> { DawarichSettings.store_geodata? && !reverse_geocoded? }
|
||||||
after_create :set_country
|
after_create :set_country
|
||||||
after_create_commit :broadcast_coordinates
|
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
|
after_commit :recalculate_track, on: :update
|
||||||
|
|
||||||
def self.without_raw_data
|
def self.without_raw_data
|
||||||
|
|
@ -66,6 +66,20 @@ class Point < ApplicationRecord
|
||||||
Country.containing_point(lon, lat)
|
Country.containing_point(lon, lat)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
# rubocop:disable Metrics/MethodLength Metrics/AbcSize
|
# rubocop:disable Metrics/MethodLength Metrics/AbcSize
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class Gpx::TrackImporter
|
||||||
{
|
{
|
||||||
lonlat: "POINT(#{point['lon'].to_d} #{point['lat'].to_d})",
|
lonlat: "POINT(#{point['lon'].to_d} #{point['lat'].to_d})",
|
||||||
altitude: point['ele'].to_i,
|
altitude: point['ele'].to_i,
|
||||||
timestamp: Time.parse(point['time']).to_i,
|
timestamp: Point.normalize_timestamp(point['time']),
|
||||||
import_id: import.id,
|
import_id: import.id,
|
||||||
velocity: speed(point),
|
velocity: speed(point),
|
||||||
raw_data: point,
|
raw_data: point,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ class Immich::ImportGeodata
|
||||||
latitude: asset['exifInfo']['latitude'],
|
latitude: asset['exifInfo']['latitude'],
|
||||||
longitude: asset['exifInfo']['longitude'],
|
longitude: asset['exifInfo']['longitude'],
|
||||||
lonlat: "SRID=4326;POINT(#{asset['exifInfo']['longitude']} #{asset['exifInfo']['latitude']})",
|
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
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class Overland::Params
|
||||||
lonlat: "POINT(#{point[:geometry][:coordinates][0]} #{point[:geometry][:coordinates][1]})",
|
lonlat: "POINT(#{point[:geometry][:coordinates][0]} #{point[:geometry][:coordinates][1]})",
|
||||||
battery_status: point[:properties][:battery_state],
|
battery_status: point[:properties][:battery_state],
|
||||||
battery: battery_level(point[:properties][:battery_level]),
|
battery: battery_level(point[:properties][:battery_level]),
|
||||||
timestamp: DateTime.parse(point[:properties][:timestamp]),
|
timestamp: Point.normalize_timestamp(point[:properties][:timestamp]),
|
||||||
altitude: point[:properties][:altitude],
|
altitude: point[:properties][:altitude],
|
||||||
velocity: point[:properties][:speed],
|
velocity: point[:properties][:speed],
|
||||||
tracker_id: point[:properties][:device_id],
|
tracker_id: point[:properties][:device_id],
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ class Photoprism::ImportGeodata
|
||||||
latitude: asset['Lat'],
|
latitude: asset['Lat'],
|
||||||
longitude: asset['Lng'],
|
longitude: asset['Lng'],
|
||||||
lonlat: "SRID=4326;POINT(#{asset['Lng']} #{asset['Lat']})",
|
lonlat: "SRID=4326;POINT(#{asset['Lng']} #{asset['Lat']})",
|
||||||
timestamp: Time.zone.parse(asset['TakenAt']).to_i
|
timestamp: Point.normalize_timestamp(asset['TakenAt'])
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class Points::Params
|
||||||
lonlat: lonlat(point),
|
lonlat: lonlat(point),
|
||||||
battery_status: point[:properties][:battery_state],
|
battery_status: point[:properties][:battery_state],
|
||||||
battery: battery_level(point[:properties][:battery_level]),
|
battery: battery_level(point[:properties][:battery_level]),
|
||||||
timestamp: DateTime.parse(point[:properties][:timestamp]),
|
timestamp: normalize_timestamp(point[:properties][:timestamp]),
|
||||||
altitude: point[:properties][:altitude],
|
altitude: point[:properties][:altitude],
|
||||||
tracker_id: point[:properties][:device_id],
|
tracker_id: point[:properties][:device_id],
|
||||||
velocity: point[:properties][:speed],
|
velocity: point[:properties][:speed],
|
||||||
|
|
@ -48,4 +48,8 @@ class Points::Params
|
||||||
def lonlat(point)
|
def lonlat(point)
|
||||||
"POINT(#{point[:geometry][:coordinates][0]} #{point[:geometry][:coordinates][1]})"
|
"POINT(#{point[:geometry][:coordinates][0]} #{point[:geometry][:coordinates][1]})"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalize_timestamp(timestamp)
|
||||||
|
Point.normalize_timestamp(DateTime.parse(timestamp))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ module Tracks
|
||||||
|
|
||||||
Point.transaction do
|
Point.transaction do
|
||||||
# Clean up existing tracks if needed
|
# Clean up existing tracks if needed
|
||||||
track_cleaner.cleanup_if_needed
|
track_cleaner.cleanup
|
||||||
|
|
||||||
# Load points using the configured strategy
|
# Load points using the configured strategy
|
||||||
points = point_loader.load_points
|
points = point_loader.load_points
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ class Tracks::RedisBuffer
|
||||||
@day = day.is_a?(Date) ? day : Date.parse(day.to_s)
|
@day = day.is_a?(Date) ? day : Date.parse(day.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Store buffered points for an incomplete track segment
|
|
||||||
# @param points [Array<Point>] array of Point objects to buffer
|
|
||||||
def store(points)
|
def store(points)
|
||||||
return if points.empty?
|
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}"
|
Rails.logger.debug "Stored #{points.size} points in buffer for user #{user_id}, day #{day}"
|
||||||
end
|
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
|
def retrieve
|
||||||
redis_key = buffer_key
|
redis_key = buffer_key
|
||||||
cached_data = Rails.cache.read(redis_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}"
|
Rails.logger.debug "Cleared buffer for user #{user_id}, day #{day}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check if a buffer exists for the user/day combination
|
|
||||||
# @return [Boolean] true if buffer exists, false otherwise
|
|
||||||
def exists?
|
def exists?
|
||||||
Rails.cache.exist?(buffer_key)
|
Rails.cache.exist?(buffer_key)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,6 @@ module Tracks::Segmentation
|
||||||
|
|
||||||
private
|
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)
|
def split_points_into_segments(points)
|
||||||
return [] if points.empty?
|
return [] if points.empty?
|
||||||
|
|
||||||
|
|
@ -67,10 +64,6 @@ module Tracks::Segmentation
|
||||||
segments
|
segments
|
||||||
end
|
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)
|
def should_start_new_segment?(current_point, previous_point)
|
||||||
return false if previous_point.nil?
|
return false if previous_point.nil?
|
||||||
|
|
||||||
|
|
@ -91,10 +84,6 @@ module Tracks::Segmentation
|
||||||
false
|
false
|
||||||
end
|
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)
|
def calculate_distance_kilometers_between_points(point1, point2)
|
||||||
lat1, lon1 = point_coordinates(point1)
|
lat1, lon1 = point_coordinates(point1)
|
||||||
lat2, lon2 = point_coordinates(point2)
|
lat2, lon2 = point_coordinates(point2)
|
||||||
|
|
@ -103,10 +92,6 @@ module Tracks::Segmentation
|
||||||
Geocoder::Calculations.distance_between([lat1, lon1], [lat2, lon2], units: :km)
|
Geocoder::Calculations.distance_between([lat1, lon1], [lat2, lon2], units: :km)
|
||||||
end
|
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)
|
def should_finalize_segment?(segment_points, grace_period_minutes = 5)
|
||||||
return false if segment_points.size < 2
|
return false if segment_points.size < 2
|
||||||
|
|
||||||
|
|
@ -121,22 +106,19 @@ module Tracks::Segmentation
|
||||||
time_since_last_point > grace_period_seconds
|
time_since_last_point > grace_period_seconds
|
||||||
end
|
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)
|
def point_timestamp(point)
|
||||||
if point.respond_to?(:timestamp)
|
if point.respond_to?(:timestamp)
|
||||||
|
# Point objects from database always have integer timestamps
|
||||||
point.timestamp
|
point.timestamp
|
||||||
elsif point.is_a?(Hash)
|
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
|
else
|
||||||
raise ArgumentError, "Invalid point type: #{point.class}"
|
raise ArgumentError, "Invalid point type: #{point.class}"
|
||||||
end
|
end
|
||||||
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)
|
def point_coordinates(point)
|
||||||
if point.respond_to?(:lat) && point.respond_to?(:lon)
|
if point.respond_to?(:lat) && point.respond_to?(:lon)
|
||||||
[point.lat, point.lon]
|
[point.lat, point.lon]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ module Tracks
|
||||||
@user = user
|
@user = user
|
||||||
end
|
end
|
||||||
|
|
||||||
def cleanup_if_needed
|
def cleanup
|
||||||
# No cleanup needed for incremental processing
|
# No cleanup needed for incremental processing
|
||||||
# We only append new tracks, don't remove existing ones
|
# We only append new tracks, don't remove existing ones
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
#
|
#
|
||||||
# Example usage:
|
# Example usage:
|
||||||
# cleaner = Tracks::TrackCleaners::ReplaceCleaner.new(user, start_at: 1.week.ago, end_at: Time.current)
|
# cleaner = Tracks::TrackCleaners::ReplaceCleaner.new(user, start_at: 1.week.ago, end_at: Time.current)
|
||||||
# cleaner.cleanup_if_needed
|
# cleaner.cleanup
|
||||||
#
|
#
|
||||||
module Tracks
|
module Tracks
|
||||||
module TrackCleaners
|
module TrackCleaners
|
||||||
|
|
@ -37,7 +37,7 @@ module Tracks
|
||||||
@end_at = end_at
|
@end_at = end_at
|
||||||
end
|
end
|
||||||
|
|
||||||
def cleanup_if_needed
|
def cleanup
|
||||||
tracks_to_remove = find_tracks_to_remove
|
tracks_to_remove = find_tracks_to_remove
|
||||||
|
|
||||||
if tracks_to_remove.any?
|
if tracks_to_remove.any?
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ RSpec.describe Tracks::Generator do
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'with no points to process' do
|
context 'with no points to process' do
|
||||||
before 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([])
|
allow(point_loader).to receive(:load_points).and_return([])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ RSpec.describe Tracks::Generator do
|
||||||
end
|
end
|
||||||
|
|
||||||
before 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(points)
|
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(:should_finalize_segment?).and_return(true)
|
||||||
allow(incomplete_segment_handler).to receive(:cleanup_processed_data)
|
allow(incomplete_segment_handler).to receive(:cleanup_processed_data)
|
||||||
|
|
@ -91,7 +91,7 @@ RSpec.describe Tracks::Generator do
|
||||||
end
|
end
|
||||||
|
|
||||||
before 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(points)
|
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(:should_finalize_segment?).and_return(false)
|
||||||
allow(incomplete_segment_handler).to receive(:handle_incomplete_segment)
|
allow(incomplete_segment_handler).to receive(:handle_incomplete_segment)
|
||||||
|
|
@ -129,7 +129,7 @@ RSpec.describe Tracks::Generator do
|
||||||
end
|
end
|
||||||
|
|
||||||
before 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(old_points + recent_points)
|
allow(point_loader).to receive(:load_points).and_return(old_points + recent_points)
|
||||||
|
|
||||||
# First segment (old points) should be finalized
|
# First segment (old points) should be finalized
|
||||||
|
|
@ -168,7 +168,7 @@ RSpec.describe Tracks::Generator do
|
||||||
end
|
end
|
||||||
|
|
||||||
before 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(single_point)
|
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(:should_finalize_segment?).and_return(true)
|
||||||
allow(incomplete_segment_handler).to receive(:cleanup_processed_data)
|
allow(incomplete_segment_handler).to receive(:cleanup_processed_data)
|
||||||
|
|
@ -186,7 +186,7 @@ RSpec.describe Tracks::Generator do
|
||||||
|
|
||||||
context 'error handling' do
|
context 'error handling' do
|
||||||
before 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')
|
allow(point_loader).to receive(:load_points).and_raise(StandardError, 'Point loading failed')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue