2025-07-22 16:41:12 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
|
|
class Stats::DailyDistanceQuery
|
2025-07-22 18:10:48 -04:00
|
|
|
def initialize(monthly_points, timespan, timezone = nil)
|
2025-07-22 16:41:12 -04:00
|
|
|
@monthly_points = monthly_points
|
|
|
|
|
@timespan = timespan
|
2025-07-22 18:10:48 -04:00
|
|
|
@timezone = validate_timezone(timezone)
|
2025-07-22 16:41:12 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def call
|
|
|
|
|
daily_distances = daily_distances(monthly_points)
|
|
|
|
|
distance_by_day_map = distance_by_day_map(daily_distances)
|
|
|
|
|
|
|
|
|
|
convert_to_daily_distances(distance_by_day_map)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
2025-07-22 18:10:48 -04:00
|
|
|
attr_reader :monthly_points, :timespan, :timezone
|
2025-07-22 16:41:12 -04:00
|
|
|
|
|
|
|
|
def daily_distances(monthly_points)
|
|
|
|
|
Stat.connection.select_all(<<-SQL.squish)
|
|
|
|
|
WITH points_with_distances AS (
|
|
|
|
|
SELECT
|
2025-07-22 18:10:48 -04:00
|
|
|
EXTRACT(day FROM (to_timestamp(timestamp) AT TIME ZONE 'UTC' AT TIME ZONE '#{timezone}')) as day_of_month,
|
2025-07-22 16:41:12 -04:00
|
|
|
CASE
|
|
|
|
|
WHEN LAG(lonlat) OVER (
|
2025-07-22 18:10:48 -04:00
|
|
|
PARTITION BY EXTRACT(day FROM (to_timestamp(timestamp) AT TIME ZONE 'UTC' AT TIME ZONE '#{timezone}'))
|
2025-07-22 16:41:12 -04:00
|
|
|
ORDER BY timestamp
|
|
|
|
|
) IS NOT NULL THEN
|
|
|
|
|
ST_Distance(
|
|
|
|
|
lonlat::geography,
|
|
|
|
|
LAG(lonlat) OVER (
|
2025-07-22 18:10:48 -04:00
|
|
|
PARTITION BY EXTRACT(day FROM (to_timestamp(timestamp) AT TIME ZONE 'UTC' AT TIME ZONE '#{timezone}'))
|
2025-07-22 16:41:12 -04:00
|
|
|
ORDER BY timestamp
|
|
|
|
|
)::geography
|
|
|
|
|
)
|
|
|
|
|
ELSE 0
|
|
|
|
|
END as segment_distance
|
|
|
|
|
FROM (#{monthly_points.to_sql}) as points
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
day_of_month,
|
|
|
|
|
ROUND(COALESCE(SUM(segment_distance), 0)) as distance_meters
|
|
|
|
|
FROM points_with_distances
|
|
|
|
|
GROUP BY day_of_month
|
|
|
|
|
ORDER BY day_of_month
|
|
|
|
|
SQL
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def distance_by_day_map(daily_distances)
|
|
|
|
|
daily_distances.index_by do |row|
|
|
|
|
|
row['day_of_month'].to_i
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def convert_to_daily_distances(distance_by_day_map)
|
|
|
|
|
timespan.to_a.map.with_index(1) do |day, index|
|
|
|
|
|
distance_meters =
|
|
|
|
|
distance_by_day_map[day.day]&.fetch('distance_meters', 0) || 0
|
|
|
|
|
|
|
|
|
|
[index, distance_meters.to_i]
|
|
|
|
|
end
|
|
|
|
|
end
|
2025-07-22 18:10:48 -04:00
|
|
|
|
|
|
|
|
def validate_timezone(timezone)
|
|
|
|
|
return timezone if ActiveSupport::TimeZone.all.any? { |tz| tz.name == timezone }
|
|
|
|
|
|
|
|
|
|
'UTC'
|
|
|
|
|
end
|
2025-07-22 16:41:12 -04:00
|
|
|
end
|