From 6314442770feb0448b3ed1d3b86a01561490daa7 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 14 Sep 2025 12:41:16 +0200 Subject: [PATCH] Calculate only centers of hexagons --- .../api/v1/maps/hexagons_controller.rb | 96 ++++- .../controllers/public_stat_map_controller.js | 35 +- app/models/stat.rb | 4 + app/services/maps/hexagon_centers.rb | 380 ++++++++++++++++++ app/services/stats/calculate_month.rb | 84 ++-- app/views/stats/public_month.html.erb | 2 +- ...0914094851_add_hexagon_centers_to_stats.rb | 5 + ...0914095157_add_index_to_hexagon_centers.rb | 7 + db/schema.rb | 4 +- 9 files changed, 518 insertions(+), 99 deletions(-) create mode 100644 app/services/maps/hexagon_centers.rb create mode 100644 db/migrate/20250914094851_add_hexagon_centers_to_stats.rb create mode 100644 db/migrate/20250914095157_add_index_to_hexagon_centers.rb diff --git a/app/controllers/api/v1/maps/hexagons_controller.rb b/app/controllers/api/v1/maps/hexagons_controller.rb index 6992388d..425d688c 100644 --- a/app/controllers/api/v1/maps/hexagons_controller.rb +++ b/app/controllers/api/v1/maps/hexagons_controller.rb @@ -6,25 +6,31 @@ class Api::V1::Maps::HexagonsController < ApiController before_action :set_user_and_dates def index - hex_size = bbox_params[:hex_size]&.to_f || 1000.0 - cache_service = HexagonCacheService.new( - user: @target_user, - stat: @stat, - start_date: @start_date, - end_date: @end_date - ) + # Try to use pre-calculated hexagon centers from stats + if @stat&.hexagon_centers.present? + result = build_hexagons_from_centers(@stat.hexagon_centers) + Rails.logger.debug "Using pre-calculated hexagon centers: #{@stat.hexagon_centers.size} centers" + return render json: result + end - # Try to use pre-calculated hexagon data if available - if cache_service.available?(hex_size) - cached_result = cache_service.cached_geojson(hex_size) - if cached_result - Rails.logger.debug 'Using cached hexagon data' - return render json: cached_result + # Handle legacy "area too large" entries - recalculate them now that we can handle large areas + if @stat&.hexagon_centers&.dig('area_too_large') + Rails.logger.info "Recalculating previously skipped large area hexagons for stat #{@stat.id}" + + # Trigger recalculation + service = Stats::CalculateMonth.new(@target_user.id, @stat.year, @stat.month) + new_centers = service.send(:calculate_hexagon_centers) + + if new_centers && !new_centers.dig(:area_too_large) + @stat.update(hexagon_centers: new_centers) + result = build_hexagons_from_centers(new_centers) + Rails.logger.debug "Successfully recalculated hexagon centers: #{new_centers.size} centers" + return render json: result end end - # Fall back to on-the-fly calculation - Rails.logger.debug 'Calculating hexagons on-the-fly' + # Fall back to on-the-fly calculation for legacy/missing data + Rails.logger.debug 'No pre-calculated data available, calculating hexagons on-the-fly' result = Maps::HexagonGrid.new(hexagon_params).call Rails.logger.debug "Hexagon service result: #{result['features']&.count || 0} features" render json: result @@ -77,6 +83,66 @@ class Api::V1::Maps::HexagonsController < ApiController private + def build_hexagons_from_centers(centers) + # Convert stored centers back to hexagon polygons + # Each center is [lng, lat, earliest_timestamp, latest_timestamp] + hexagon_features = centers.map.with_index do |center, index| + lng, lat, earliest, latest = center + + # Generate hexagon polygon from center point (1000m hexagons) + hexagon_geojson = generate_hexagon_polygon(lng, lat, 1000) + + { + type: 'Feature', + id: index + 1, + geometry: hexagon_geojson, + properties: { + hex_id: index + 1, + hex_size: 1000, + earliest_point: earliest ? Time.zone.at(earliest).iso8601 : nil, + latest_point: latest ? Time.zone.at(latest).iso8601 : nil + } + } + end + + { + 'type' => 'FeatureCollection', + 'features' => hexagon_features, + 'metadata' => { + 'hex_size_m' => 1000, + 'count' => hexagon_features.count, + 'user_id' => @target_user.id, + 'pre_calculated' => true + } + } + end + + def generate_hexagon_polygon(center_lng, center_lat, size_meters) + # Generate hexagon vertices around center point + # This is a simplified hexagon generation - for production you might want more precise calculations + earth_radius = 6_371_000 # meters + angular_size = size_meters / earth_radius + + vertices = [] + 6.times do |i| + angle = (i * 60) * Math::PI / 180 # 60 degrees between vertices + + # Calculate offset in degrees (rough approximation) + lat_offset = angular_size * Math.cos(angle) * 180 / Math::PI + lng_offset = angular_size * Math.sin(angle) * 180 / Math::PI / Math.cos(center_lat * Math::PI / 180) + + vertices << [center_lng + lng_offset, center_lat + lat_offset] + end + + # Close the polygon + vertices << vertices.first + + { + type: 'Polygon', + coordinates: [vertices] + } + end + def bbox_params params.permit(:min_lon, :min_lat, :max_lon, :max_lat, :hex_size, :viewport_width, :viewport_height) end diff --git a/app/javascript/controllers/public_stat_map_controller.js b/app/javascript/controllers/public_stat_map_controller.js index cb9e3e12..348d4abb 100644 --- a/app/javascript/controllers/public_stat_map_controller.js +++ b/app/javascript/controllers/public_stat_map_controller.js @@ -23,9 +23,7 @@ export default class extends BaseController { } disconnect() { - if (this.hexagonGrid) { - this.hexagonGrid.destroy(); - } + // No hexagonGrid to destroy for public sharing if (this.map) { this.map.remove(); } @@ -102,35 +100,24 @@ export default class extends BaseController { console.log('📊 After fitBounds overlay display:', afterFitBoundsElement?.style.display || 'default'); } - this.hexagonGrid = createHexagonGrid(this.map, { - apiEndpoint: '/api/v1/maps/hexagons', - style: { - fillColor: '#3388ff', - fillOpacity: 0.3, - color: '#3388ff', - weight: 1, - opacity: 0.7 - }, - debounceDelay: 300, - maxZoom: 15, - minZoom: 4 - }); + // Don't create hexagonGrid for public sharing - we handle hexagons manually + // this.hexagonGrid = createHexagonGrid(this.map, {...}); - // Force hide immediately after creation to prevent auto-showing - this.hexagonGrid.hide(); - - // Disable all dynamic behavior by removing event listeners - this.map.off('moveend'); - this.map.off('zoomend'); + console.log('🎯 Public sharing: skipping HexagonGrid creation, using manual loading'); + console.log('🔍 Debug values:'); + console.log(' dataBounds:', dataBounds); + console.log(' point_count:', dataBounds?.point_count); + console.log(' hexagonsAvailableValue:', this.hexagonsAvailableValue); + console.log(' hexagonsAvailableValue type:', typeof this.hexagonsAvailableValue); // Load hexagons only if they are pre-calculated and data exists if (dataBounds && dataBounds.point_count > 0 && this.hexagonsAvailableValue) { await this.loadStaticHexagons(); } else { if (!this.hexagonsAvailableValue) { - console.log('No pre-calculated hexagons available - skipping hexagon loading'); + console.log('📋 No pre-calculated hexagons available for public sharing - skipping hexagon loading'); } else { - console.warn('No data bounds or points available - not showing hexagons'); + console.warn('⚠️ No data bounds or points available - not showing hexagons'); } // Hide loading indicator if no hexagons to load const loadingElement = document.getElementById('map-loading'); diff --git a/app/models/stat.rb b/app/models/stat.rb index fe9d69cc..24ac4802 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -57,6 +57,10 @@ class Stat < ApplicationRecord end def hexagons_available?(hex_size = 1000) + # Check new optimized format (hexagon_centers) first + return true if hexagon_centers.present? && hexagon_centers.is_a?(Array) && hexagon_centers.any? + + # Fallback to legacy format (hexagon_data) for backwards compatibility hexagon_data&.dig(hex_size.to_s, 'geojson').present? end diff --git a/app/services/maps/hexagon_centers.rb b/app/services/maps/hexagon_centers.rb new file mode 100644 index 00000000..e03d1d19 --- /dev/null +++ b/app/services/maps/hexagon_centers.rb @@ -0,0 +1,380 @@ +# frozen_string_literal: true + +class Maps::HexagonCenters + include ActiveModel::Validations + + # Constants for configuration + HEX_SIZE = 1000 # meters - fixed 1000m hexagons + MAX_AREA_KM2 = 10_000 # Maximum area for simple calculation + TILE_SIZE_KM = 100 # Size of each tile for large area processing + MAX_TILES = 100 # Maximum number of tiles to process + + # Validation error classes + class BoundingBoxTooLargeError < StandardError; end + class InvalidCoordinatesError < StandardError; end + class PostGISError < StandardError; end + + attr_reader :user_id, :start_date, :end_date + + validates :user_id, presence: true + + def initialize(user_id:, start_date:, end_date:) + @user_id = user_id + @start_date = start_date + @end_date = end_date + end + + def call + validate! + + bounds = calculate_data_bounds + return nil unless bounds + + # Check if area requires tiled processing + area_km2 = calculate_bounding_box_area(bounds) + if area_km2 > MAX_AREA_KM2 + Rails.logger.info "Large area detected (#{area_km2.round} km²), using tiled processing for user #{user_id}" + return calculate_hexagon_centers_tiled(bounds, area_km2) + end + + calculate_hexagon_centers_simple + rescue ActiveRecord::StatementInvalid => e + message = "Failed to calculate hexagon centers: #{e.message}" + ExceptionReporter.call(e, message) + raise PostGISError, message + end + + private + + def calculate_data_bounds + start_timestamp = parse_date_to_timestamp(start_date) + end_timestamp = parse_date_to_timestamp(end_date) + + bounds_result = ActiveRecord::Base.connection.exec_query( + "SELECT MIN(ST_Y(lonlat::geometry)) as min_lat, MAX(ST_Y(lonlat::geometry)) as max_lat, + MIN(ST_X(lonlat::geometry)) as min_lng, MAX(ST_X(lonlat::geometry)) as max_lng + FROM points + WHERE user_id = $1 + AND timestamp BETWEEN $2 AND $3 + AND lonlat IS NOT NULL", + 'hexagon_centers_bounds_query', + [user_id, start_timestamp, end_timestamp] + ).first + + return nil unless bounds_result + + { + min_lat: bounds_result['min_lat'].to_f, + max_lat: bounds_result['max_lat'].to_f, + min_lng: bounds_result['min_lng'].to_f, + max_lng: bounds_result['max_lng'].to_f + } + end + + def calculate_bounding_box_area(bounds) + width = (bounds[:max_lng] - bounds[:min_lng]).abs + height = (bounds[:max_lat] - bounds[:min_lat]).abs + + # Convert degrees to approximate kilometers + avg_lat = (bounds[:min_lat] + bounds[:max_lat]) / 2 + width_km = width * 111 * Math.cos(avg_lat * Math::PI / 180) + height_km = height * 111 + + width_km * height_km + end + + def calculate_hexagon_centers_simple + start_timestamp = parse_date_to_timestamp(start_date) + end_timestamp = parse_date_to_timestamp(end_date) + + sql = <<~SQL + WITH bbox_geom AS ( + SELECT ST_SetSRID(ST_Envelope(ST_Collect(lonlat::geometry)), 4326) as geom + FROM points + WHERE user_id = $1 + AND timestamp BETWEEN $2 AND $3 + AND lonlat IS NOT NULL + ), + bbox_utm AS ( + SELECT ST_Transform(geom, 3857) as geom_utm FROM bbox_geom + ), + user_points AS ( + SELECT + lonlat::geometry as point_geom, + ST_Transform(lonlat::geometry, 3857) as point_geom_utm, + timestamp + FROM points + WHERE user_id = $1 + AND timestamp BETWEEN $2 AND $3 + AND lonlat IS NOT NULL + ), + hex_grid AS ( + SELECT + (ST_HexagonGrid($4, geom_utm)).geom as hex_geom_utm, + (ST_HexagonGrid($4, geom_utm)).i as hex_i, + (ST_HexagonGrid($4, geom_utm)).j as hex_j + FROM bbox_utm + ), + hexagons_with_points AS ( + SELECT DISTINCT + hg.hex_geom_utm, + hg.hex_i, + hg.hex_j + FROM hex_grid hg + JOIN user_points up ON ST_Intersects(hg.hex_geom_utm, up.point_geom_utm) + ), + hexagon_centers AS ( + SELECT + ST_Transform(ST_Centroid(hwp.hex_geom_utm), 4326) as center, + MIN(up.timestamp) as earliest_point, + MAX(up.timestamp) as latest_point + FROM hexagons_with_points hwp + JOIN user_points up ON ST_Intersects(hwp.hex_geom_utm, up.point_geom_utm) + GROUP BY hwp.hex_geom_utm, hwp.hex_i, hwp.hex_j + ) + SELECT + ST_X(center) as lng, + ST_Y(center) as lat, + earliest_point, + latest_point + FROM hexagon_centers + ORDER BY earliest_point; + SQL + + result = ActiveRecord::Base.connection.exec_query( + sql, + 'hexagon_centers_calculation', + [user_id, start_timestamp, end_timestamp, HEX_SIZE] + ) + + result.map do |row| + [ + row['lng'].to_f, + row['lat'].to_f, + row['earliest_point']&.to_i, + row['latest_point']&.to_i + ] + end + end + + def calculate_hexagon_centers_tiled(bounds, area_km2) + # Calculate optimal tile size based on area + tiles = generate_tiles(bounds, area_km2) + + if tiles.size > MAX_TILES + Rails.logger.warn "Area too large even for tiling (#{tiles.size} tiles), using sampling approach" + return calculate_hexagon_centers_sampled(bounds, area_km2) + end + + Rails.logger.info "Processing #{tiles.size} tiles for large area hexagon calculation" + + all_centers = [] + tiles.each_with_index do |tile, index| + Rails.logger.debug "Processing tile #{index + 1}/#{tiles.size}" + + centers = calculate_hexagon_centers_for_tile(tile) + all_centers.concat(centers) if centers.any? + end + + # Remove duplicates and sort by timestamp + deduplicate_and_sort_centers(all_centers) + end + + def generate_tiles(bounds, area_km2) + # Calculate number of tiles needed + tiles_needed = (area_km2 / (TILE_SIZE_KM * TILE_SIZE_KM)).ceil + tiles_per_side = Math.sqrt(tiles_needed).ceil + + lat_step = (bounds[:max_lat] - bounds[:min_lat]) / tiles_per_side + lng_step = (bounds[:max_lng] - bounds[:min_lng]) / tiles_per_side + + tiles = [] + tiles_per_side.times do |i| + tiles_per_side.times do |j| + tile_bounds = { + min_lat: bounds[:min_lat] + (i * lat_step), + max_lat: bounds[:min_lat] + ((i + 1) * lat_step), + min_lng: bounds[:min_lng] + (j * lng_step), + max_lng: bounds[:min_lng] + ((j + 1) * lng_step) + } + tiles << tile_bounds + end + end + + tiles + end + + def calculate_hexagon_centers_for_tile(tile_bounds) + start_timestamp = parse_date_to_timestamp(start_date) + end_timestamp = parse_date_to_timestamp(end_date) + + sql = <<~SQL + WITH tile_bounds AS ( + SELECT ST_MakeEnvelope($1, $2, $3, $4, 4326) as geom + ), + tile_utm AS ( + SELECT ST_Transform(geom, 3857) as geom_utm FROM tile_bounds + ), + user_points AS ( + SELECT + lonlat::geometry as point_geom, + ST_Transform(lonlat::geometry, 3857) as point_geom_utm, + timestamp + FROM points + WHERE user_id = $5 + AND timestamp BETWEEN $6 AND $7 + AND lonlat IS NOT NULL + AND lonlat && (SELECT geom FROM tile_bounds) + ), + hex_grid AS ( + SELECT + (ST_HexagonGrid($8, geom_utm)).geom as hex_geom_utm, + (ST_HexagonGrid($8, geom_utm)).i as hex_i, + (ST_HexagonGrid($8, geom_utm)).j as hex_j + FROM tile_utm + ), + hexagons_with_points AS ( + SELECT DISTINCT + hg.hex_geom_utm, + hg.hex_i, + hg.hex_j + FROM hex_grid hg + JOIN user_points up ON ST_Intersects(hg.hex_geom_utm, up.point_geom_utm) + ), + hexagon_centers AS ( + SELECT + ST_Transform(ST_Centroid(hwp.hex_geom_utm), 4326) as center, + MIN(up.timestamp) as earliest_point, + MAX(up.timestamp) as latest_point + FROM hexagons_with_points hwp + JOIN user_points up ON ST_Intersects(hwp.hex_geom_utm, up.point_geom_utm) + GROUP BY hwp.hex_geom_utm, hwp.hex_i, hwp.hex_j + ) + SELECT + ST_X(center) as lng, + ST_Y(center) as lat, + earliest_point, + latest_point + FROM hexagon_centers; + SQL + + result = ActiveRecord::Base.connection.exec_query( + sql, + 'hexagon_centers_tile_calculation', + [ + tile_bounds[:min_lng], tile_bounds[:min_lat], + tile_bounds[:max_lng], tile_bounds[:max_lat], + user_id, start_timestamp, end_timestamp, HEX_SIZE + ] + ) + + result.map do |row| + [ + row['lng'].to_f, + row['lat'].to_f, + row['earliest_point']&.to_i, + row['latest_point']&.to_i + ] + end + end + + def calculate_hexagon_centers_sampled(bounds, area_km2) + # For extremely large areas, use point density sampling + Rails.logger.info "Using density-based sampling for extremely large area (#{area_km2.round} km²)" + + start_timestamp = parse_date_to_timestamp(start_date) + end_timestamp = parse_date_to_timestamp(end_date) + + # Get point density distribution + sql = <<~SQL + WITH density_grid AS ( + SELECT + ST_SnapToGrid(lonlat::geometry, 0.1) as grid_point, + COUNT(*) as point_count, + MIN(timestamp) as earliest, + MAX(timestamp) as latest + FROM points + WHERE user_id = $1 + AND timestamp BETWEEN $2 AND $3 + AND lonlat IS NOT NULL + GROUP BY ST_SnapToGrid(lonlat::geometry, 0.1) + HAVING COUNT(*) >= 5 + ), + sampled_points AS ( + SELECT + ST_X(grid_point) as lng, + ST_Y(grid_point) as lat, + earliest, + latest + FROM density_grid + ORDER BY point_count DESC + LIMIT 1000 + ) + SELECT lng, lat, earliest, latest FROM sampled_points; + SQL + + result = ActiveRecord::Base.connection.exec_query( + sql, + 'hexagon_centers_sampled_calculation', + [user_id, start_timestamp, end_timestamp] + ) + + result.map do |row| + [ + row['lng'].to_f, + row['lat'].to_f, + row['earliest']&.to_i, + row['latest']&.to_i + ] + end + end + + def deduplicate_and_sort_centers(centers) + # Remove near-duplicate centers (within ~100m) + precision = 3 # ~111m precision at equator + unique_centers = {} + + centers.each do |center| + lng, lat, earliest, latest = center + key = "#{lng.round(precision)},#{lat.round(precision)}" + + if unique_centers[key] + # Keep the one with earlier timestamp or merge timestamps + existing = unique_centers[key] + unique_centers[key] = [ + lng, lat, + [earliest, existing[2]].compact.min, + [latest, existing[3]].compact.max + ] + else + unique_centers[key] = center + end + end + + unique_centers.values.sort_by { |center| center[2] || 0 } + end + + def parse_date_to_timestamp(date) + case date + when String + if date.match?(/^\d+$/) + date.to_i + else + Time.parse(date).to_i + end + when Integer + date + else + Time.parse(date.to_s).to_i + end + rescue ArgumentError => e + ExceptionReporter.call(e, "Invalid date format: #{date}") + raise ArgumentError, "Invalid date format: #{date}" + end + + def validate! + return if valid? + + raise InvalidCoordinatesError, errors.full_messages.join(', ') + end +end diff --git a/app/services/stats/calculate_month.rb b/app/services/stats/calculate_month.rb index 824122b0..b5434bd9 100644 --- a/app/services/stats/calculate_month.rb +++ b/app/services/stats/calculate_month.rb @@ -38,7 +38,7 @@ class Stats::CalculateMonth daily_distance: distance_by_day, distance: distance(distance_by_day), toponyms: toponyms, - hexagon_data: calculate_hexagons + hexagon_centers: calculate_hexagon_centers ) stat.save end @@ -84,71 +84,39 @@ class Stats::CalculateMonth Stat.where(year:, month:, user:).destroy_all end - def calculate_hexagons + def calculate_hexagon_centers return nil if points.empty? - # Calculate bounding box for the user's points in this month - bounds = calculate_data_bounds - return nil unless bounds + begin + service = Maps::HexagonCenters.new( + user_id: user.id, + start_date: start_date_iso8601, + end_date: end_date_iso8601 + ) - # Pre-calculate hexagons for 1000m size used across the system - hexagon_sizes = [1000] # 1000m hexagons for consistent visualization + result = service.call - hexagon_sizes.each_with_object({}) do |hex_size, result| - begin - service = Maps::HexagonGrid.new( - min_lon: bounds[:min_lng], - min_lat: bounds[:min_lat], - max_lon: bounds[:max_lng], - max_lat: bounds[:max_lat], - hex_size: hex_size, - user_id: user.id, - start_date: start_date_iso8601, - end_date: end_date_iso8601 - ) - - geojson_result = service.call - - # Store the complete GeoJSON result for instant serving - result[hex_size.to_s] = { - 'geojson' => geojson_result, - 'bbox' => bounds, - 'generated_at' => Time.current.iso8601 - } - - Rails.logger.info "Pre-calculated #{geojson_result['features']&.size || 0} hexagons (#{hex_size}m) for user #{user.id}, #{year}-#{month}" - rescue Maps::HexagonGrid::BoundingBoxTooLargeError, - Maps::HexagonGrid::InvalidCoordinatesError, - Maps::HexagonGrid::PostGISError => e - Rails.logger.warn "Hexagon calculation failed for user #{user.id}, #{year}-#{month}, size #{hex_size}m: #{e.message}" - # Continue with other sizes even if one fails - next + if result.nil? + Rails.logger.info "No hexagon centers calculated for user #{user.id}, #{year}-#{month} (no data)" + return nil end + + # The new service should handle large areas, so this shouldn't happen anymore + if result.is_a?(Hash) && result[:area_too_large] + Rails.logger.error "Unexpected area_too_large result from HexagonCenters service for user #{user.id}, #{year}-#{month}" + return { area_too_large: true } + end + + Rails.logger.info "Pre-calculated #{result.size} hexagon centers for user #{user.id}, #{year}-#{month}" + result + rescue Maps::HexagonCenters::BoundingBoxTooLargeError, + Maps::HexagonCenters::InvalidCoordinatesError, + Maps::HexagonCenters::PostGISError => e + Rails.logger.warn "Hexagon centers calculation failed for user #{user.id}, #{year}-#{month}: #{e.message}" + nil end end - def calculate_data_bounds - bounds_result = ActiveRecord::Base.connection.exec_query( - "SELECT MIN(ST_Y(lonlat::geometry)) as min_lat, MAX(ST_Y(lonlat::geometry)) as max_lat, - MIN(ST_X(lonlat::geometry)) as min_lng, MAX(ST_X(lonlat::geometry)) as max_lng - FROM points - WHERE user_id = $1 - AND timestamp BETWEEN $2 AND $3 - AND lonlat IS NOT NULL", - 'hexagon_bounds_query', - [user.id, start_timestamp, end_timestamp] - ).first - - return nil unless bounds_result - - { - min_lat: bounds_result['min_lat'].to_f, - max_lat: bounds_result['max_lat'].to_f, - min_lng: bounds_result['min_lng'].to_f, - max_lng: bounds_result['max_lng'].to_f - } - end - def start_date_iso8601 DateTime.new(year, month, 1).beginning_of_day.iso8601 end diff --git a/app/views/stats/public_month.html.erb b/app/views/stats/public_month.html.erb index 1cbbafef..da93c8e4 100644 --- a/app/views/stats/public_month.html.erb +++ b/app/views/stats/public_month.html.erb @@ -51,7 +51,7 @@ data-public-stat-map-month-value="<%= @month %>" data-public-stat-map-uuid-value="<%= @stat.sharing_uuid %>" data-public-stat-map-data-bounds-value="<%= @data_bounds.to_json if @data_bounds %>" - data-public-stat-map-hexagons-available-value="<%= @hexagons_available %>" + data-public-stat-map-hexagons-available-value="<%= @hexagons_available.to_s %>" data-public-stat-map-self-hosted-value="<%= @self_hosted %>"> diff --git a/db/migrate/20250914094851_add_hexagon_centers_to_stats.rb b/db/migrate/20250914094851_add_hexagon_centers_to_stats.rb new file mode 100644 index 00000000..9dbc5232 --- /dev/null +++ b/db/migrate/20250914094851_add_hexagon_centers_to_stats.rb @@ -0,0 +1,5 @@ +class AddHexagonCentersToStats < ActiveRecord::Migration[8.0] + def change + add_column :stats, :hexagon_centers, :jsonb + end +end diff --git a/db/migrate/20250914095157_add_index_to_hexagon_centers.rb b/db/migrate/20250914095157_add_index_to_hexagon_centers.rb new file mode 100644 index 00000000..9e301543 --- /dev/null +++ b/db/migrate/20250914095157_add_index_to_hexagon_centers.rb @@ -0,0 +1,7 @@ +class AddIndexToHexagonCenters < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def change + add_index :stats, :hexagon_centers, using: :gin, where: "hexagon_centers IS NOT NULL", algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 74ab775c..071c1860 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_09_13_194134) do +ActiveRecord::Schema[8.0].define(version: 2025_09_14_095157) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "postgis" @@ -223,7 +223,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_13_194134) do t.jsonb "sharing_settings", default: {} t.uuid "sharing_uuid" t.jsonb "hexagon_data" + t.jsonb "hexagon_centers" t.index ["distance"], name: "index_stats_on_distance" + t.index ["hexagon_centers"], name: "index_stats_on_hexagon_centers", where: "(hexagon_centers IS NOT NULL)", using: :gin t.index ["month"], name: "index_stats_on_month" t.index ["sharing_uuid"], name: "index_stats_on_sharing_uuid", unique: true t.index ["user_id"], name: "index_stats_on_user_id"