mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
159 lines
4.8 KiB
Ruby
159 lines
4.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Track < ApplicationRecord
|
|
include Calculateable
|
|
include DistanceConvertible
|
|
|
|
belongs_to :user
|
|
has_many :points, dependent: :nullify
|
|
|
|
validates :start_at, :end_at, :original_path, presence: true
|
|
validates :distance, :avg_speed, :duration, numericality: { greater_than_or_equal_to: 0 }
|
|
|
|
after_update :recalculate_path_and_distance!, if: -> { points.exists? && (saved_change_to_start_at? || saved_change_to_end_at?) }
|
|
after_create :broadcast_track_created
|
|
after_update :broadcast_track_updated
|
|
after_destroy :broadcast_track_destroyed
|
|
|
|
def self.last_for_day(user, day)
|
|
day_start = day.beginning_of_day
|
|
day_end = day.end_of_day
|
|
|
|
where(user: user)
|
|
.where(end_at: day_start..day_end)
|
|
.order(end_at: :desc)
|
|
.first
|
|
end
|
|
|
|
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
|
|
id,
|
|
timestamp,
|
|
lonlat,
|
|
LAG(lonlat) OVER (ORDER BY timestamp) as prev_lonlat,
|
|
LAG(timestamp) OVER (ORDER BY timestamp) as prev_timestamp,
|
|
ST_Distance(
|
|
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
|
|
#{where_clause}
|
|
ORDER BY timestamp
|
|
),
|
|
segment_breaks AS (
|
|
SELECT *,
|
|
CASE
|
|
WHEN prev_lonlat IS NULL THEN 1
|
|
WHEN time_diff_seconds > $4 THEN 1
|
|
WHEN distance_meters > $5 THEN 1
|
|
ELSE 0
|
|
END as is_break
|
|
FROM points_with_gaps
|
|
),
|
|
segments AS (
|
|
SELECT *,
|
|
SUM(is_break) OVER (ORDER BY timestamp ROWS UNBOUNDED PRECEDING) as segment_id
|
|
FROM segment_breaks
|
|
)
|
|
SELECT
|
|
segment_id,
|
|
array_agg(id ORDER BY timestamp) as point_ids,
|
|
count(*) as point_count,
|
|
min(timestamp) as start_timestamp,
|
|
max(timestamp) as end_timestamp,
|
|
sum(COALESCE(distance_meters, 0)) as total_distance_meters
|
|
FROM segments
|
|
GROUP BY segment_id
|
|
HAVING count(*) >= 2
|
|
ORDER BY segment_id
|
|
SQL
|
|
|
|
results = Point.connection.exec_query(
|
|
sql,
|
|
'segment_points_in_sql',
|
|
[user_id, start_timestamp, end_timestamp, time_threshold_seconds, distance_threshold_meters]
|
|
)
|
|
|
|
# Convert results to segment data
|
|
segments_data = []
|
|
results.each do |row|
|
|
segments_data << {
|
|
segment_id: row['segment_id'].to_i,
|
|
point_ids: parse_postgres_array(row['point_ids']),
|
|
point_count: row['point_count'].to_i,
|
|
start_timestamp: row['start_timestamp'].to_i,
|
|
end_timestamp: row['end_timestamp'].to_i,
|
|
total_distance_meters: row['total_distance_meters'].to_f
|
|
}
|
|
end
|
|
|
|
segments_data
|
|
end
|
|
|
|
# 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
|
|
)
|
|
|
|
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,
|
|
pre_calculated_distance: seg_data[:total_distance_meters],
|
|
start_timestamp: seg_data[:start_timestamp],
|
|
end_timestamp: seg_data[:end_timestamp]
|
|
}
|
|
end
|
|
end
|
|
|
|
# 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
|
|
|
|
private
|
|
|
|
def broadcast_track_created
|
|
broadcast_track_update('created')
|
|
end
|
|
|
|
def broadcast_track_updated
|
|
broadcast_track_update('updated')
|
|
end
|
|
|
|
def broadcast_track_destroyed
|
|
TracksChannel.broadcast_to(user, {
|
|
action: 'destroyed',
|
|
track_id: id
|
|
})
|
|
end
|
|
|
|
def broadcast_track_update(action)
|
|
TracksChannel.broadcast_to(user, {
|
|
action: action,
|
|
track: TrackSerializer.new(self).call
|
|
})
|
|
end
|
|
end
|