mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
205 lines
5.8 KiB
Ruby
205 lines
5.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Stats::CalculateMonth
|
|
include ActiveModel::Validations
|
|
|
|
# H3 Configuration
|
|
DEFAULT_H3_RESOLUTION = 8 # Small hexagons for good detail
|
|
MAX_HEXAGONS = 10_000 # Maximum number of hexagons to prevent memory issues
|
|
|
|
class PostGISError < StandardError; end
|
|
|
|
def initialize(user_id, year, month)
|
|
@user = User.find(user_id)
|
|
@year = year.to_i
|
|
@month = month.to_i
|
|
end
|
|
|
|
def call
|
|
if points.empty?
|
|
destroy_month_stats(year, month)
|
|
|
|
return
|
|
end
|
|
|
|
update_month_stats(year, month)
|
|
rescue StandardError => e
|
|
create_stats_update_failed_notification(user, e)
|
|
end
|
|
|
|
# Public method for calculating H3 hexagon centers with custom parameters
|
|
def calculate_h3_hexagon_centers(user_id: nil, start_date: nil, end_date: nil, h3_resolution: DEFAULT_H3_RESOLUTION)
|
|
target_start_date = start_date || start_date_iso8601
|
|
target_end_date = end_date || end_date_iso8601
|
|
|
|
points = fetch_user_points_for_period(user_id, target_start_date, target_end_date)
|
|
return [] if points.empty?
|
|
|
|
h3_indexes_with_counts = calculate_h3_indexes(points, h3_resolution)
|
|
|
|
if h3_indexes_with_counts.size > MAX_HEXAGONS
|
|
Rails.logger.warn "Too many hexagons (#{h3_indexes_with_counts.size}), using lower resolution"
|
|
# Try with lower resolution (larger hexagons)
|
|
lower_resolution = [h3_resolution - 2, 0].max
|
|
Rails.logger.info "Recalculating with lower H3 resolution: #{lower_resolution}"
|
|
return calculate_h3_hexagon_centers(
|
|
user_id: user_id,
|
|
start_date: target_start_date,
|
|
end_date: target_end_date,
|
|
h3_resolution: lower_resolution
|
|
)
|
|
end
|
|
|
|
Rails.logger.info "Generated #{h3_indexes_with_counts.size} H3 hexagons at resolution #{h3_resolution} for user #{user_id}"
|
|
|
|
# Convert to format: [h3_index_string, point_count, earliest_timestamp, latest_timestamp]
|
|
h3_indexes_with_counts.map do |h3_index, data|
|
|
[
|
|
h3_index.to_s(16), # Store as hex string
|
|
data[:count],
|
|
data[:earliest],
|
|
data[:latest]
|
|
]
|
|
end
|
|
rescue StandardError => e
|
|
message = "Failed to calculate H3 hexagon centers: #{e.message}"
|
|
ExceptionReporter.call(e, message) if defined?(ExceptionReporter)
|
|
raise PostGISError, message
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :user, :year, :month
|
|
|
|
def start_timestamp = DateTime.new(year, month, 1).to_i
|
|
|
|
def end_timestamp
|
|
DateTime.new(year, month, -1).to_i # -1 returns last day of month
|
|
end
|
|
|
|
def update_month_stats(year, month)
|
|
Stat.transaction do
|
|
stat = Stat.find_or_initialize_by(year:, month:, user:)
|
|
distance_by_day = stat.distance_by_day
|
|
|
|
stat.assign_attributes(
|
|
daily_distance: distance_by_day,
|
|
distance: distance(distance_by_day),
|
|
toponyms: toponyms,
|
|
hexagon_centers: calculate_hexagon_centers
|
|
)
|
|
stat.save
|
|
end
|
|
end
|
|
|
|
def points
|
|
return @points if defined?(@points)
|
|
|
|
@points = user
|
|
.points
|
|
.without_raw_data
|
|
.where(timestamp: start_timestamp..end_timestamp)
|
|
.select(:lonlat, :timestamp)
|
|
.order(timestamp: :asc)
|
|
end
|
|
|
|
def distance(distance_by_day)
|
|
distance_by_day.sum { |day| day[1] }
|
|
end
|
|
|
|
def toponyms
|
|
toponym_points =
|
|
user
|
|
.points
|
|
.without_raw_data
|
|
.where(timestamp: start_timestamp..end_timestamp)
|
|
.select(:city, :country_name)
|
|
.distinct
|
|
|
|
CountriesAndCities.new(toponym_points).call
|
|
end
|
|
|
|
def create_stats_update_failed_notification(user, error)
|
|
Notifications::Create.new(
|
|
user:,
|
|
kind: :error,
|
|
title: 'Stats update failed',
|
|
content: "#{error.message}, stacktrace: #{error.backtrace.join("\n")}"
|
|
).call
|
|
end
|
|
|
|
def destroy_month_stats(year, month)
|
|
Stat.where(year:, month:, user:).destroy_all
|
|
end
|
|
|
|
def calculate_hexagon_centers
|
|
return nil if points.empty?
|
|
|
|
begin
|
|
result = calculate_h3_hexagon_centers
|
|
|
|
if result.empty?
|
|
Rails.logger.info "No H3 hexagon centers calculated for user #{user.id}, #{year}-#{month} (no data)"
|
|
return nil
|
|
end
|
|
|
|
Rails.logger.info "Pre-calculated #{result.size} H3 hexagon centers for user #{user.id}, #{year}-#{month}"
|
|
result
|
|
rescue PostGISError => e
|
|
Rails.logger.warn "H3 hexagon centers calculation failed for user #{user.id}, #{year}-#{month}: #{e.message}"
|
|
nil
|
|
end
|
|
end
|
|
|
|
def start_date_iso8601
|
|
DateTime.new(year, month, 1).beginning_of_day.iso8601
|
|
end
|
|
|
|
def end_date_iso8601
|
|
DateTime.new(year, month, -1).end_of_day.iso8601
|
|
end
|
|
|
|
def fetch_user_points_for_period(user_id, start_date, end_date)
|
|
start_timestamp = parse_date_parameter(start_date)
|
|
end_timestamp = parse_date_parameter(end_date)
|
|
|
|
Point.where(user_id: user_id)
|
|
.where(timestamp: start_timestamp..end_timestamp)
|
|
.where.not(lonlat: nil)
|
|
.select(:id, :lonlat, :timestamp)
|
|
end
|
|
|
|
def calculate_h3_indexes(points, h3_resolution)
|
|
h3_data = Hash.new { |h, k| h[k] = { count: 0, earliest: nil, latest: nil } }
|
|
|
|
points.find_each do |point|
|
|
# Extract lat/lng from PostGIS point
|
|
coordinates = [point.lonlat.y, point.lonlat.x] # [lat, lng] for H3
|
|
|
|
# Get H3 index for this point
|
|
h3_index = H3.from_geo_coordinates(coordinates, h3_resolution.clamp(0, 15))
|
|
|
|
# Aggregate data for this hexagon
|
|
data = h3_data[h3_index]
|
|
data[:count] += 1
|
|
data[:earliest] = [data[:earliest], point.timestamp].compact.min
|
|
data[:latest] = [data[:latest], point.timestamp].compact.max
|
|
end
|
|
|
|
h3_data
|
|
end
|
|
|
|
def parse_date_parameter(param)
|
|
case param
|
|
when String
|
|
param.match?(/^\d+$/) ? param.to_i : Time.zone.parse(param).to_i
|
|
when Integer
|
|
param
|
|
else
|
|
param.to_i
|
|
end
|
|
rescue ArgumentError => e
|
|
Rails.logger.error "Invalid date format: #{param} - #{e.message}"
|
|
raise ArgumentError, "Invalid date format: #{param}"
|
|
end
|
|
end
|