# 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(h3_resolution) end private attr_reader :user, :year, :month def calculate_h3_hexagon_centers(h3_resolution) result = calculate_hexagons(h3_resolution) return [] if result.nil? # Convert to array format: [h3_index_string, point_count, earliest_timestamp, latest_timestamp] result.map do |h3_index_string, data| [ h3_index_string, data[0], # count data[1], # earliest data[2] # latest ] end 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}" # Recursively call with lower resolution return 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 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) .where.not(lonlat: nil) .select(:lonlat, :timestamp) .order(timestamp: :asc) end def calculate_h3_indexes(points, h3_resolution) h3_data = {} 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)) h3_index_string = h3_index.to_s(16) # Convert to hex string immediately # 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