dawarich/app/services/stats/hexagon_calculator.rb

139 lines
4.3 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
class Stats::HexagonCalculator
# 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(h3_resolution: DEFAULT_H3_RESOLUTION)
calculate_h3_hexagon_centers_with_resolution(h3_resolution)
end
def calculate_h3_hex_ids
return {} if points.empty?
begin
result = call
if result.empty?
Rails.logger.info "No H3 hex IDs calculated for user #{user.id}, #{year}-#{month} (no data)"
return {}
end
# Convert array format to hash format: { h3_index => [count, earliest, latest] }
hex_hash = result.each_with_object({}) do |hex_data, hash|
h3_index, count, earliest, latest = hex_data
hash[h3_index] = [count, earliest, latest]
end
Rails.logger.info "Pre-calculated #{hex_hash.size} H3 hex IDs for user #{user.id}, #{year}-#{month}"
hex_hash
rescue PostGISError => e
Rails.logger.warn "H3 hex IDs calculation failed for user #{user.id}, #{year}-#{month}: #{e.message}"
{}
end
end
private
attr_reader :user, :year, :month
def calculate_h3_hexagon_centers_with_resolution(h3_resolution)
points = fetch_user_points_for_period
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}"
# Create a new instance with lower resolution for recursion
return self.class.new(user.id, year, month)
.calculate_h3_hexagon_centers_with_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
def start_timestamp
DateTime.new(year, month, 1).to_i
end
def end_timestamp
DateTime.new(year, month, -1).to_i # -1 returns last day of month
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 start_date_iso8601
@start_date_iso8601 ||= DateTime.new(year, month, 1).beginning_of_day.iso8601
end
def end_date_iso8601
@end_date_iso8601 ||= DateTime.new(year, month, -1).end_of_day.iso8601
end
def fetch_user_points_for_period
start_timestamp = DateTime.parse(start_date_iso8601).to_i
end_timestamp = DateTime.parse(end_date_iso8601).to_i
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
end