# frozen_string_literal: true # Track creation and statistics calculation module for building Track records from GPS points. # # This module provides the core functionality for converting arrays of GPS points into # Track database records with calculated statistics including distance, duration, speed, # and elevation metrics. # # How it works: # 1. Takes an array of Point objects representing a track segment # 2. Creates a Track record with basic temporal and spatial boundaries # 3. Calculates comprehensive statistics: distance, duration, average speed # 4. Computes elevation metrics: gain, loss, maximum, minimum # 5. Builds a LineString path representation for mapping # 6. Associates all points with the created track # # Statistics calculated: # - Distance: Always stored in meters as integers for consistency # - Duration: Total time in seconds between first and last point # - Average speed: In km/h regardless of user's distance unit preference # - Elevation gain/loss: Cumulative ascent and descent in meters # - Elevation max/min: Highest and lowest altitudes in the track # # Distance is converted to user's preferred unit only at display time, not storage time. # This ensures consistency when users change their distance unit preferences. # # Used by: # - Tracks::Generator for creating tracks during generation # - Any class that needs to convert point arrays to Track records # # Example usage: # class MyTrackProcessor # include Tracks::TrackBuilder # # def initialize(user) # @user = user # end # # def process_segment(points) # track = create_track_from_points(points) # # Track now exists with calculated statistics # end # # private # # attr_reader :user # end # module Tracks::TrackBuilder extend ActiveSupport::Concern def create_track_from_points(points) return nil if points.size < 2 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).call end def calculate_track_distance(points) # Always calculate and store distance in meters for consistency distance_in_meters = Point.total_distance(points, :m) distance_in_meters.round end def calculate_duration(points) points.last.timestamp - points.first.timestamp end def calculate_average_speed(distance_in_meters, duration_seconds) return 0.0 if duration_seconds <= 0 || distance_in_meters <= 0 # Speed in meters per second, then convert to km/h for storage speed_mps = distance_in_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 private def user raise NotImplementedError, "Including class must implement user method" end end