From c756346569225d1566684fa938ef178efce57b63 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 20 Sep 2025 13:23:24 +0200 Subject: [PATCH] Refactor H3 hexagon calculation logic to improve clarity and maintainability --- app/services/stats/calculate_month.rb | 4 +- app/services/stats/hexagon_calculator.rb | 131 ++++++++++------------- 2 files changed, 57 insertions(+), 78 deletions(-) diff --git a/app/services/stats/calculate_month.rb b/app/services/stats/calculate_month.rb index 35a5cfee..dafabf28 100644 --- a/app/services/stats/calculate_month.rb +++ b/app/services/stats/calculate_month.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Stats::CalculateMonth - include ActiveModel::Validations - def initialize(user_id, year, month) @user = User.find(user_id) @year = year.to_i @@ -87,6 +85,6 @@ class Stats::CalculateMonth end def calculate_h3_hex_ids - Stats::HexagonCalculator.new(user.id, year, month).calculate_h3_hex_ids + Stats::HexagonCalculator.new(user.id, year, month).call end end diff --git a/app/services/stats/hexagon_calculator.rb b/app/services/stats/hexagon_calculator.rb index f76b65de..1767c7bc 100644 --- a/app/services/stats/hexagon_calculator.rb +++ b/app/services/stats/hexagon_calculator.rb @@ -14,70 +14,63 @@ class Stats::HexagonCalculator end def call(h3_resolution: DEFAULT_H3_RESOLUTION) - calculate_h3_hexagon_centers_with_resolution(h3_resolution) + calculate_h3_hexagon_centers(h3_resolution) end def calculate_h3_hex_ids - return {} if points.empty? + result = calculate_hexagons(DEFAULT_H3_RESOLUTION) + return {} if result.nil? - 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 + result end private attr_reader :user, :year, :month - def calculate_h3_hexagon_centers_with_resolution(h3_resolution) - points = fetch_user_points_for_period + def calculate_h3_hexagon_centers(h3_resolution) + result = calculate_hexagons(h3_resolution) + return [] if result.nil? - 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| + # Convert to array format: [h3_index_string, point_count, earliest_timestamp, latest_timestamp] + result.map do |h3_index_string, data| [ - h3_index.to_s(16), # Store as hex string - data[:count], - data[:earliest], - data[:latest] + h3_index_string, + data[0], # count + data[1], # earliest + data[2] # 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 + + # Unified hexagon calculation method + def calculate_hexagons(h3_resolution) + return nil if points.empty? + + begin + h3_hash = calculate_h3_indexes(points, h3_resolution) + + if h3_hash.empty? + Rails.logger.info "No H3 hex IDs calculated for user #{user.id}, #{year}-#{month} (no data)" + return nil + end + + if h3_hash.size > MAX_HEXAGONS + Rails.logger.warn "Too many hexagons (#{h3_hash.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_hexagons(lower_resolution) + end + + Rails.logger.info "Generated #{h3_hash.size} H3 hexagons at resolution #{h3_resolution} for user #{user.id}" + h3_hash + rescue StandardError => e + message = "Failed to calculate H3 hexagon centers: #{e.message}" + ExceptionReporter.call(e, message) if defined?(ExceptionReporter) + raise PostGISError, message + end end def start_timestamp @@ -95,30 +88,13 @@ class Stats::HexagonCalculator .points .without_raw_data .where(timestamp: start_timestamp..end_timestamp) + .where.not(lonlat: nil) .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 } } + h3_data = {} points.find_each do |point| # Extract lat/lng from PostGIS point @@ -126,14 +102,19 @@ class Stats::HexagonCalculator # Get H3 index for this point h3_index = H3.from_geo_coordinates(coordinates, h3_resolution.clamp(0, 15)) + h3_index_string = h3_index.to_s(16) # Convert to hex string immediately - # 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 + # Initialize or update data for this hexagon + if h3_data[h3_index_string] + data = h3_data[h3_index_string] + data[0] += 1 # increment count + data[1] = [data[1], point.timestamp].min # update earliest + data[2] = [data[2], point.timestamp].max # update latest + else + h3_data[h3_index_string] = [1, point.timestamp, point.timestamp] # [count, earliest, latest] + end end h3_data end -end \ No newline at end of file +end