From 2e46069fcc6af32756f098da85629d9007460d82 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 23 Jul 2025 20:08:24 +0200 Subject: [PATCH] Clean up code a bit --- app/models/concerns/distanceable.rb | 11 ++---- app/models/track.rb | 58 +++++++++++------------------ 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/app/models/concerns/distanceable.rb b/app/models/concerns/distanceable.rb index f9c28477..dc668169 100644 --- a/app/models/concerns/distanceable.rb +++ b/app/models/concerns/distanceable.rb @@ -5,7 +5,6 @@ module Distanceable 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 @@ -50,20 +49,17 @@ module Distanceable return 0 if points.length < 2 - # OPTIMIZED: Single SQL query instead of N individual queries total_meters = calculate_batch_distances(points).sum total_meters.to_f / ::DISTANCE_UNITS[unit.to_sym] end - # Optimized batch distance calculation using single SQL query def calculate_batch_distances(points) return [] if points.length < 2 point_pairs = points.each_cons(2).to_a return [] if point_pairs.empty? - # Create a VALUES clause with all point pairs values_clause = point_pairs.map.with_index do |(p1, p2), index| "(#{index}, ST_GeomFromEWKT('#{p1.lonlat}')::geography, ST_GeomFromEWKT('#{p2.lonlat}')::geography)" end.join(', ') @@ -71,13 +67,13 @@ module Distanceable # Single query to calculate all distances results = connection.execute(<<-SQL.squish) WITH point_pairs AS ( - SELECT + SELECT pair_id, point1, point2 FROM (VALUES #{values_clause}) AS t(pair_id, point1, point2) ) - SELECT + SELECT pair_id, ST_Distance(point1, point2) as distance_meters FROM point_pairs @@ -85,7 +81,7 @@ module Distanceable SQL # Return array of distances in meters - results.map { |row| row['distance_meters'].to_f } + results.map { |row| row['distance_meters'].to_i } end end @@ -94,7 +90,6 @@ module Distanceable 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? diff --git a/app/models/track.rb b/app/models/track.rb index b79de93e..f02071d2 100644 --- a/app/models/track.rb +++ b/app/models/track.rb @@ -25,16 +25,15 @@ class Track < ApplicationRecord .first end - # Optimized SQL segmentation using PostgreSQL window functions def self.segment_points_in_sql(user_id, start_timestamp, end_timestamp, time_threshold_minutes, distance_threshold_meters, untracked_only: false) time_threshold_seconds = time_threshold_minutes * 60 - + where_clause = if untracked_only "WHERE user_id = $1 AND timestamp BETWEEN $2 AND $3 AND track_id IS NULL" else "WHERE user_id = $1 AND timestamp BETWEEN $2 AND $3" end - + sql = <<~SQL WITH points_with_gaps AS ( SELECT @@ -44,21 +43,21 @@ class Track < ApplicationRecord LAG(lonlat) OVER (ORDER BY timestamp) as prev_lonlat, LAG(timestamp) OVER (ORDER BY timestamp) as prev_timestamp, ST_Distance( - lonlat::geography, + lonlat::geography, LAG(lonlat) OVER (ORDER BY timestamp)::geography ) as distance_meters, (timestamp - LAG(timestamp) OVER (ORDER BY timestamp)) as time_diff_seconds - FROM points + FROM points #{where_clause} ORDER BY timestamp ), segment_breaks AS ( SELECT *, - CASE + CASE WHEN prev_lonlat IS NULL THEN 1 WHEN time_diff_seconds > $4 THEN 1 WHEN distance_meters > $5 THEN 1 - ELSE 0 + ELSE 0 END as is_break FROM points_with_gaps ), @@ -67,7 +66,7 @@ class Track < ApplicationRecord SUM(is_break) OVER (ORDER BY timestamp ROWS UNBOUNDED PRECEDING) as segment_id FROM segment_breaks ) - SELECT + SELECT segment_id, array_agg(id ORDER BY timestamp) as point_ids, count(*) as point_count, @@ -79,7 +78,7 @@ class Track < ApplicationRecord HAVING count(*) >= 2 ORDER BY segment_id SQL - + results = Point.connection.exec_query( sql, 'segment_points_in_sql', @@ -104,15 +103,18 @@ class Track < ApplicationRecord # Get actual Point objects for each segment with pre-calculated distances def self.get_segments_with_points(user_id, start_timestamp, end_timestamp, time_threshold_minutes, distance_threshold_meters, untracked_only: false) - segments_data = segment_points_in_sql(user_id, start_timestamp, end_timestamp, time_threshold_minutes, distance_threshold_meters, untracked_only: untracked_only) - - # Get all point IDs we need - all_point_ids = segments_data.flat_map { |seg| seg[:point_ids] } - - # Single query to get all points - points_by_id = Point.where(id: all_point_ids).index_by(&:id) - - # Build segments with actual Point objects + segments_data = segment_points_in_sql( + user_id, + start_timestamp, + end_timestamp, + time_threshold_minutes, + distance_threshold_meters, + untracked_only: untracked_only + ) + + point_ids = segments_data.flat_map { |seg| seg[:point_ids] } + points_by_id = Point.where(id: point_ids).index_by(&:id) + segments_data.map do |seg_data| { points: seg_data[:point_ids].map { |id| points_by_id[id] }.compact, @@ -126,7 +128,7 @@ class Track < ApplicationRecord # Parse PostgreSQL array format like "{1,2,3}" into Ruby array def self.parse_postgres_array(pg_array_string) return [] if pg_array_string.nil? || pg_array_string.empty? - + # Remove curly braces and split by comma pg_array_string.gsub(/[{}]/, '').split(',').map(&:to_i) end @@ -151,23 +153,7 @@ class Track < ApplicationRecord def broadcast_track_update(action) TracksChannel.broadcast_to(user, { action: action, - track: serialize_track_data + track: TrackSerializer.new(self).call }) end - - def serialize_track_data - { - id: id, - start_at: start_at.iso8601, - end_at: end_at.iso8601, - distance: distance.to_i, - avg_speed: avg_speed.to_f, - duration: duration, - elevation_gain: elevation_gain, - elevation_loss: elevation_loss, - elevation_max: elevation_max, - elevation_min: elevation_min, - original_path: original_path.to_s - } - end end