dawarich/app/services/location_search/result_aggregator.rb

112 lines
3.5 KiB
Ruby
Raw Normal View History

2025-08-30 17:18:16 -04:00
# frozen_string_literal: true
module LocationSearch
class ResultAggregator
2025-09-02 15:21:22 -04:00
include ActionView::Helpers::TextHelper
2025-08-30 17:18:16 -04:00
# Time threshold for grouping consecutive points into visits (minutes)
VISIT_TIME_THRESHOLD = 30
def group_points_into_visits(points)
return [] if points.empty?
2025-09-02 15:55:47 -04:00
# Sort points by timestamp to handle unordered input
sorted_points = points.sort_by { |p| p[:timestamp] }
2025-08-30 17:18:16 -04:00
visits = []
current_visit_points = []
2025-09-02 15:21:22 -04:00
2025-09-02 15:55:47 -04:00
sorted_points.each do |point|
2025-08-30 17:18:16 -04:00
if current_visit_points.empty? || within_visit_threshold?(current_visit_points.last, point)
current_visit_points << point
else
# Finalize current visit and start a new one
visits << create_visit_from_points(current_visit_points) if current_visit_points.any?
current_visit_points = [point]
end
end
2025-09-02 15:21:22 -04:00
2025-08-30 17:18:16 -04:00
# Don't forget the last visit
visits << create_visit_from_points(current_visit_points) if current_visit_points.any?
2025-09-02 15:21:22 -04:00
2025-08-30 17:18:16 -04:00
visits.sort_by { |visit| -visit[:timestamp] } # Most recent first
end
private
def within_visit_threshold?(previous_point, current_point)
time_diff = (current_point[:timestamp] - previous_point[:timestamp]).abs / 60.0 # minutes
time_diff <= VISIT_TIME_THRESHOLD
end
def create_visit_from_points(points)
return nil if points.empty?
# Sort points by timestamp to get chronological order
sorted_points = points.sort_by { |p| p[:timestamp] }
first_point = sorted_points.first
last_point = sorted_points.last
2025-09-02 15:21:22 -04:00
2025-08-30 17:18:16 -04:00
# Calculate visit duration
duration_minutes =
2025-09-22 18:18:04 -04:00
if sorted_points.length > 1
((last_point[:timestamp] - first_point[:timestamp]) / 60.0).round
else
# Single point visit - estimate based on typical stay time
15 # minutes
end
2025-08-30 17:18:16 -04:00
# Find the most accurate point (lowest accuracy value means higher precision)
most_accurate_point = points.min_by { |p| p[:accuracy] || 999_999 }
2025-09-02 15:21:22 -04:00
2025-08-30 17:18:16 -04:00
# Calculate average distance from search center
average_distance = (points.sum { |p| p[:distance_meters] } / points.length).round(2)
{
timestamp: first_point[:timestamp],
date: first_point[:date],
coordinates: most_accurate_point[:coordinates],
distance_meters: average_distance,
duration_estimate: format_duration(duration_minutes),
points_count: points.length,
accuracy_meters: most_accurate_point[:accuracy],
visit_details: {
start_time: first_point[:date],
end_time: last_point[:date],
duration_minutes: duration_minutes,
city: most_accurate_point[:city],
country: most_accurate_point[:country],
altitude_range: calculate_altitude_range(points)
}
}
end
def format_duration(minutes)
2025-09-02 15:21:22 -04:00
return "~#{pluralize(minutes, 'minute')}" if minutes < 60
2025-08-30 17:18:16 -04:00
hours = minutes / 60
remaining_minutes = minutes % 60
2025-09-02 15:21:22 -04:00
if remaining_minutes.zero?
2025-09-02 15:21:22 -04:00
"~#{pluralize(hours, 'hour')}"
2025-08-30 17:18:16 -04:00
else
2025-09-02 15:21:22 -04:00
"~#{pluralize(hours, 'hour')} #{pluralize(remaining_minutes, 'minute')}"
2025-08-30 17:18:16 -04:00
end
end
def calculate_altitude_range(points)
altitudes = points.map { |p| p[:altitude] }.compact
return nil if altitudes.empty?
2025-09-02 15:21:22 -04:00
2025-08-30 17:18:16 -04:00
min_altitude = altitudes.min
max_altitude = altitudes.max
2025-09-02 15:21:22 -04:00
2025-08-30 17:18:16 -04:00
if min_altitude == max_altitude
"#{min_altitude}m"
else
"#{min_altitude}m - #{max_altitude}m"
end
end
end
2025-09-02 15:21:22 -04:00
end