mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
211 lines
6.2 KiB
Ruby
211 lines
6.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Tracks::CreateFromPoints
|
|
attr_reader :user, :distance_threshold_meters, :time_threshold_minutes, :start_at, :end_at
|
|
|
|
def initialize(user, start_at: nil, end_at: nil)
|
|
@user = user
|
|
@start_at = start_at
|
|
@end_at = end_at
|
|
@distance_threshold_meters = user.safe_settings.meters_between_routes.to_i || 500
|
|
@time_threshold_minutes = user.safe_settings.minutes_between_routes.to_i || 60
|
|
end
|
|
|
|
def call
|
|
time_range_info = start_at || end_at ? " for time range #{start_at} - #{end_at}" : ""
|
|
Rails.logger.info "Creating tracks for user #{user.id} with thresholds: #{distance_threshold_meters}m, #{time_threshold_minutes}min#{time_range_info}"
|
|
|
|
tracks_created = 0
|
|
|
|
Track.transaction do
|
|
# Clear existing tracks for this user (optionally scoped to time range)
|
|
tracks_to_delete = start_at || end_at ? scoped_tracks_for_deletion : user.tracks
|
|
tracks_to_delete.destroy_all
|
|
|
|
track_segments = split_points_into_tracks
|
|
|
|
track_segments.each do |segment_points|
|
|
next if segment_points.size < 2
|
|
|
|
track = create_track_from_points(segment_points)
|
|
tracks_created += 1 if track&.persisted?
|
|
end
|
|
end
|
|
|
|
Rails.logger.info "Created #{tracks_created} tracks for user #{user.id}#{time_range_info}"
|
|
tracks_created
|
|
end
|
|
|
|
private
|
|
|
|
def user_points
|
|
@user_points ||= begin
|
|
points = Point.where(user: user)
|
|
.where.not(lonlat: nil)
|
|
.where.not(timestamp: nil)
|
|
|
|
# Apply timestamp filtering if provided
|
|
if start_at.present?
|
|
points = points.where('timestamp >= ?', start_at)
|
|
end
|
|
|
|
if end_at.present?
|
|
points = points.where('timestamp <= ?', end_at)
|
|
end
|
|
|
|
points.order(:timestamp)
|
|
end
|
|
end
|
|
|
|
def scoped_tracks_for_deletion
|
|
user.tracks.where(
|
|
'start_at <= ? AND end_at >= ?',
|
|
Time.zone.at(end_at), Time.zone.at(start_at)
|
|
)
|
|
end
|
|
|
|
def split_points_into_tracks
|
|
return [] if user_points.empty?
|
|
|
|
track_segments = []
|
|
current_segment = []
|
|
|
|
# Use .each instead of find_each to preserve sequential processing
|
|
# find_each processes in batches which breaks track segmentation logic
|
|
user_points.each do |point|
|
|
if should_start_new_track?(point, current_segment.last)
|
|
# Finalize current segment if it has enough points
|
|
track_segments << current_segment if current_segment.size >= 2
|
|
current_segment = [point]
|
|
else
|
|
current_segment << point
|
|
end
|
|
end
|
|
|
|
# Don't forget the last segment
|
|
track_segments << current_segment if current_segment.size >= 2
|
|
|
|
track_segments
|
|
end
|
|
|
|
def should_start_new_track?(current_point, previous_point)
|
|
return false if previous_point.nil?
|
|
|
|
# Check time threshold (convert minutes to seconds)
|
|
time_diff_seconds = current_point.timestamp - previous_point.timestamp
|
|
time_threshold_seconds = time_threshold_minutes.to_i * 60
|
|
|
|
return true if time_diff_seconds > time_threshold_seconds
|
|
|
|
# Check distance threshold - convert km to meters to match frontend logic
|
|
distance_km = calculate_distance_kilometers(previous_point, current_point)
|
|
distance_meters = distance_km * 1000 # Convert km to meters
|
|
return true if distance_meters > distance_threshold_meters
|
|
|
|
false
|
|
end
|
|
|
|
def calculate_distance_kilometers(point1, point2)
|
|
# Use Geocoder to match behavior with frontend (same library used elsewhere in app)
|
|
Geocoder::Calculations.distance_between(
|
|
[point1.lat, point1.lon], [point2.lat, point2.lon], units: :km
|
|
)
|
|
end
|
|
|
|
def create_track_from_points(points)
|
|
track = Track.new(
|
|
user_id: user.id,
|
|
start_at: Time.zone.at(points.first.timestamp),
|
|
end_at: Time.zone.at(points.last.timestamp),
|
|
original_path: build_path(points)
|
|
)
|
|
|
|
# Calculate track statistics
|
|
track.distance = calculate_track_distance(points)
|
|
track.duration = calculate_duration(points)
|
|
track.avg_speed = calculate_average_speed(track.distance, track.duration)
|
|
|
|
# Calculate elevation statistics
|
|
elevation_stats = calculate_elevation_stats(points)
|
|
track.elevation_gain = elevation_stats[:gain]
|
|
track.elevation_loss = elevation_stats[:loss]
|
|
track.elevation_max = elevation_stats[:max]
|
|
track.elevation_min = elevation_stats[:min]
|
|
|
|
if track.save!
|
|
Point.where(id: points.map(&:id)).update_all(track_id: track.id)
|
|
|
|
track
|
|
else
|
|
Rails.logger.error "Failed to create track for user #{user.id}: #{track.errors.full_messages.join(', ')}"
|
|
|
|
nil
|
|
end
|
|
end
|
|
|
|
def build_path(points)
|
|
Tracks::BuildPath.new(points.map(&:lonlat)).call
|
|
end
|
|
|
|
def calculate_track_distance(points)
|
|
# Use the existing total_distance method with user's preferred unit
|
|
distance_in_user_unit = Point.total_distance(points, user.safe_settings.distance_unit || 'km')
|
|
|
|
# Convert to meters for storage (Track model expects distance in meters)
|
|
case user.safe_settings.distance_unit
|
|
when 'miles', 'mi'
|
|
(distance_in_user_unit * 1609.344).round # miles to meters
|
|
else
|
|
(distance_in_user_unit * 1000).round # km to meters
|
|
end
|
|
end
|
|
|
|
def calculate_duration(points)
|
|
# Duration in seconds
|
|
points.last.timestamp - points.first.timestamp
|
|
end
|
|
|
|
def calculate_average_speed(distance_meters, duration_seconds)
|
|
return 0.0 if duration_seconds <= 0 || distance_meters <= 0
|
|
|
|
# Speed in meters per second, then convert to km/h for storage
|
|
speed_mps = distance_meters.to_f / duration_seconds
|
|
(speed_mps * 3.6).round(2) # m/s to km/h
|
|
end
|
|
|
|
def calculate_elevation_stats(points)
|
|
altitudes = points.map(&:altitude).compact
|
|
|
|
return default_elevation_stats if altitudes.empty?
|
|
|
|
elevation_gain = 0
|
|
elevation_loss = 0
|
|
previous_altitude = altitudes.first
|
|
|
|
altitudes[1..].each do |altitude|
|
|
diff = altitude - previous_altitude
|
|
if diff > 0
|
|
elevation_gain += diff
|
|
else
|
|
elevation_loss += diff.abs
|
|
end
|
|
previous_altitude = altitude
|
|
end
|
|
|
|
{
|
|
gain: elevation_gain.round,
|
|
loss: elevation_loss.round,
|
|
max: altitudes.max,
|
|
min: altitudes.min
|
|
}
|
|
end
|
|
|
|
def default_elevation_stats
|
|
{
|
|
gain: 0,
|
|
loss: 0,
|
|
max: 0,
|
|
min: 0
|
|
}
|
|
end
|
|
end
|