mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Extract hexagon query to separate class
This commit is contained in:
parent
57ecda2b1b
commit
88e9c85766
10 changed files with 479 additions and 288 deletions
|
|
@ -285,18 +285,7 @@ export class HexagonGrid {
|
|||
// Calculate opacity based on point density (0.2 to 0.8)
|
||||
const opacity = 0.2 + (pointCount / maxPoints) * 0.6;
|
||||
|
||||
// Calculate color based on density
|
||||
let color = '#3388ff'
|
||||
// let color = '#3388ff'; // Default blue
|
||||
// if (pointCount > maxPoints * 0.7) {
|
||||
// color = '#d73027'; // High density - red
|
||||
// } else if (pointCount > maxPoints * 0.4) {
|
||||
// color = '#fc8d59'; // Medium-high density - orange
|
||||
// } else if (pointCount > maxPoints * 0.2) {
|
||||
// color = '#fee08b'; // Medium density - yellow
|
||||
// } else {
|
||||
// color = '#91bfdb'; // Low density - light blue
|
||||
// }
|
||||
|
||||
return {
|
||||
fillColor: color,
|
||||
|
|
|
|||
104
app/queries/hexagon_query.rb
Normal file
104
app/queries/hexagon_query.rb
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HexagonQuery
|
||||
# Maximum number of hexagons to return in a single request
|
||||
MAX_HEXAGONS_PER_REQUEST = 5000
|
||||
|
||||
attr_reader :min_lon, :min_lat, :max_lon, :max_lat, :hex_size, :user_id, :start_date, :end_date
|
||||
|
||||
def initialize(min_lon:, min_lat:, max_lon:, max_lat:, hex_size:, user_id: nil, start_date: nil, end_date: nil)
|
||||
@min_lon = min_lon
|
||||
@min_lat = min_lat
|
||||
@max_lon = max_lon
|
||||
@max_lat = max_lat
|
||||
@hex_size = hex_size
|
||||
@user_id = user_id
|
||||
@start_date = start_date
|
||||
@end_date = end_date
|
||||
end
|
||||
|
||||
def call
|
||||
ActiveRecord::Base.connection.execute(build_hexagon_sql)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_hexagon_sql
|
||||
user_filter = user_id ? "user_id = #{user_id}" : '1=1'
|
||||
date_filter = build_date_filter
|
||||
|
||||
<<~SQL
|
||||
WITH bbox_geom AS (
|
||||
SELECT ST_MakeEnvelope(#{min_lon}, #{min_lat}, #{max_lon}, #{max_lat}, 4326) as geom
|
||||
),
|
||||
bbox_utm AS (
|
||||
SELECT
|
||||
ST_Transform(geom, 3857) as geom_utm,
|
||||
geom as geom_wgs84
|
||||
FROM bbox_geom
|
||||
),
|
||||
user_points AS (
|
||||
SELECT
|
||||
lonlat::geometry as point_geom,
|
||||
ST_Transform(lonlat::geometry, 3857) as point_geom_utm,
|
||||
id,
|
||||
timestamp
|
||||
FROM points
|
||||
WHERE #{user_filter}
|
||||
#{date_filter}
|
||||
AND ST_Intersects(
|
||||
lonlat::geometry,
|
||||
(SELECT geom FROM bbox_geom)
|
||||
)
|
||||
),
|
||||
hex_grid AS (
|
||||
SELECT
|
||||
(ST_HexagonGrid(#{hex_size}, bbox_utm.geom_utm)).geom as hex_geom_utm,
|
||||
(ST_HexagonGrid(#{hex_size}, bbox_utm.geom_utm)).i as hex_i,
|
||||
(ST_HexagonGrid(#{hex_size}, bbox_utm.geom_utm)).j as hex_j
|
||||
FROM bbox_utm
|
||||
),
|
||||
hexagons_with_points AS (
|
||||
SELECT DISTINCT
|
||||
hex_geom_utm,
|
||||
hex_i,
|
||||
hex_j
|
||||
FROM hex_grid hg
|
||||
INNER JOIN user_points up ON ST_Intersects(hg.hex_geom_utm, up.point_geom_utm)
|
||||
),
|
||||
hexagon_stats AS (
|
||||
SELECT
|
||||
hwp.hex_geom_utm,
|
||||
hwp.hex_i,
|
||||
hwp.hex_j,
|
||||
COUNT(up.id) as point_count,
|
||||
MIN(up.timestamp) as earliest_point,
|
||||
MAX(up.timestamp) as latest_point
|
||||
FROM hexagons_with_points hwp
|
||||
INNER 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_AsGeoJSON(ST_Transform(hex_geom_utm, 4326)) as geojson,
|
||||
hex_i,
|
||||
hex_j,
|
||||
point_count,
|
||||
earliest_point,
|
||||
latest_point,
|
||||
row_number() OVER (ORDER BY point_count DESC) as id
|
||||
FROM hexagon_stats
|
||||
ORDER BY point_count DESC
|
||||
LIMIT #{MAX_HEXAGONS_PER_REQUEST};
|
||||
SQL
|
||||
end
|
||||
|
||||
def build_date_filter
|
||||
return '' unless start_date || end_date
|
||||
|
||||
conditions = []
|
||||
conditions << "timestamp >= EXTRACT(EPOCH FROM '#{start_date}'::timestamp)" if start_date
|
||||
conditions << "timestamp <= EXTRACT(EPOCH FROM '#{end_date}'::timestamp)" if end_date
|
||||
|
||||
conditions.any? ? "AND #{conditions.join(' AND ')}" : ''
|
||||
end
|
||||
end
|
||||
|
|
@ -5,8 +5,6 @@ class Maps::HexagonGrid
|
|||
|
||||
# Constants for configuration
|
||||
DEFAULT_HEX_SIZE = 500 # meters (center to edge)
|
||||
TARGET_HEX_EDGE_PX = 20 # pixels (edge length target)
|
||||
MAX_HEXAGONS_PER_REQUEST = 5000
|
||||
MAX_AREA_KM2 = 250_000 # 500km x 500km
|
||||
|
||||
# Validation error classes
|
||||
|
|
@ -29,9 +27,9 @@ class Maps::HexagonGrid
|
|||
@min_lat = params[:min_lat].to_f
|
||||
@max_lon = params[:max_lon].to_f
|
||||
@max_lat = params[:max_lat].to_f
|
||||
@hex_size = params[:hex_size]&.to_f || DEFAULT_HEX_SIZE
|
||||
@viewport_width = params[:viewport_width]&.to_f
|
||||
@viewport_height = params[:viewport_height]&.to_f
|
||||
@hex_size = calculate_dynamic_hex_size(params)
|
||||
@user_id = params[:user_id]
|
||||
@start_date = params[:start_date]
|
||||
@end_date = params[:end_date]
|
||||
|
|
@ -39,61 +37,12 @@ class Maps::HexagonGrid
|
|||
|
||||
def call
|
||||
validate!
|
||||
|
||||
generate_hexagons
|
||||
end
|
||||
|
||||
def area_km2
|
||||
@area_km2 ||= calculate_area_km2
|
||||
end
|
||||
|
||||
def crosses_dateline?
|
||||
min_lon > max_lon
|
||||
end
|
||||
|
||||
def in_polar_region?
|
||||
max_lat.abs > 85 || min_lat.abs > 85
|
||||
end
|
||||
|
||||
def estimated_hexagon_count
|
||||
# Rough estimation based on area
|
||||
# A 500m radius hexagon covers approximately 0.65 km²
|
||||
hexagon_area_km2 = 0.65 * (hex_size / 500.0)**2
|
||||
(area_km2 / hexagon_area_km2).round
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def calculate_dynamic_hex_size(params)
|
||||
# If viewport dimensions are provided, calculate hex_size for 20px edge length
|
||||
if viewport_width && viewport_height && viewport_width > 0 && viewport_height > 0
|
||||
# Calculate the geographic width of the bounding box in meters
|
||||
avg_lat = (min_lat + max_lat) / 2
|
||||
bbox_width_degrees = (max_lon - min_lon).abs
|
||||
bbox_width_meters = bbox_width_degrees * 111_320 * Math.cos(avg_lat * Math::PI / 180)
|
||||
|
||||
# Calculate how many meters per pixel based on current viewport span (zoom-independent)
|
||||
meters_per_pixel = bbox_width_meters / viewport_width
|
||||
|
||||
# For a regular hexagon, the edge length is approximately 0.866 times the radius (center to vertex)
|
||||
# So if we want a 20px edge, we need: edge_length_meters = 20 * meters_per_pixel
|
||||
# And radius = edge_length / 0.866
|
||||
edge_length_meters = TARGET_HEX_EDGE_PX * meters_per_pixel
|
||||
hex_radius_meters = edge_length_meters / 0.866
|
||||
|
||||
# Clamp to reasonable bounds to prevent excessive computation
|
||||
calculated_size = hex_radius_meters.clamp(50, 10_000)
|
||||
|
||||
Rails.logger.debug "Dynamic hex size calculation: bbox_width=#{bbox_width_meters.round}m, viewport=#{viewport_width}px, meters_per_pixel=#{meters_per_pixel.round(2)}, hex_size=#{calculated_size.round}m"
|
||||
|
||||
calculated_size
|
||||
else
|
||||
# Fallback to provided hex_size or default
|
||||
fallback_size = params[:hex_size]&.to_f || DEFAULT_HEX_SIZE
|
||||
Rails.logger.debug "Using fallback hex size: #{fallback_size}m (no viewport dimensions provided)"
|
||||
fallback_size
|
||||
end
|
||||
end
|
||||
|
||||
def validate_bbox_order
|
||||
errors.add(:base, 'min_lon must be less than max_lon') if min_lon >= max_lon
|
||||
errors.add(:base, 'min_lat must be less than max_lat') if min_lat >= max_lat
|
||||
|
|
@ -105,114 +54,20 @@ class Maps::HexagonGrid
|
|||
errors.add(:base, "Area too large (#{area_km2.round} km²). Maximum allowed: #{MAX_AREA_KM2} km²")
|
||||
end
|
||||
|
||||
def calculate_area_km2
|
||||
width = (max_lon - min_lon).abs
|
||||
height = (max_lat - min_lat).abs
|
||||
|
||||
# Convert degrees to approximate kilometers
|
||||
# 1 degree latitude ≈ 111 km
|
||||
# 1 degree longitude ≈ 111 km * cos(latitude)
|
||||
avg_lat = (min_lat + max_lat) / 2
|
||||
width_km = width * 111 * Math.cos(avg_lat * Math::PI / 180)
|
||||
height_km = height * 111
|
||||
|
||||
width_km * height_km
|
||||
end
|
||||
|
||||
def generate_hexagons
|
||||
sql = build_hexagon_sql
|
||||
query = HexagonQuery.new(
|
||||
min_lon:, min_lat:, max_lon:, max_lat:,
|
||||
hex_size:, user_id:, start_date:, end_date:
|
||||
)
|
||||
|
||||
Rails.logger.debug "Generating hexagons for bbox: #{[min_lon, min_lat, max_lon, max_lat]}"
|
||||
Rails.logger.debug "Estimated hexagon count: #{estimated_hexagon_count}"
|
||||
result = query.call
|
||||
|
||||
result = execute_sql(sql)
|
||||
format_hexagons(result)
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
Rails.logger.error "PostGIS error generating hexagons: #{e.message}"
|
||||
raise PostGISError, "Failed to generate hexagon grid: #{e.message}"
|
||||
end
|
||||
message = "Failed to generate hexagon grid: #{e.message}"
|
||||
|
||||
def build_hexagon_sql
|
||||
user_filter = user_id ? "user_id = #{user_id}" : '1=1'
|
||||
date_filter = build_date_filter
|
||||
|
||||
<<~SQL
|
||||
WITH bbox_geom AS (
|
||||
SELECT ST_MakeEnvelope(#{min_lon}, #{min_lat}, #{max_lon}, #{max_lat}, 4326) as geom
|
||||
),
|
||||
bbox_utm AS (
|
||||
SELECT
|
||||
ST_Transform(geom, 3857) as geom_utm,
|
||||
geom as geom_wgs84
|
||||
FROM bbox_geom
|
||||
),
|
||||
user_points AS (
|
||||
SELECT
|
||||
lonlat::geometry as point_geom,
|
||||
ST_Transform(lonlat::geometry, 3857) as point_geom_utm,
|
||||
id,
|
||||
timestamp
|
||||
FROM points
|
||||
WHERE #{user_filter}
|
||||
#{date_filter}
|
||||
AND ST_Intersects(
|
||||
lonlat::geometry,
|
||||
(SELECT geom FROM bbox_geom)
|
||||
)
|
||||
),
|
||||
hex_grid AS (
|
||||
SELECT
|
||||
(ST_HexagonGrid(#{hex_size}, bbox_utm.geom_utm)).geom as hex_geom_utm,
|
||||
(ST_HexagonGrid(#{hex_size}, bbox_utm.geom_utm)).i as hex_i,
|
||||
(ST_HexagonGrid(#{hex_size}, bbox_utm.geom_utm)).j as hex_j
|
||||
FROM bbox_utm
|
||||
),
|
||||
hexagons_with_points AS (
|
||||
SELECT DISTINCT
|
||||
hex_geom_utm,
|
||||
hex_i,
|
||||
hex_j
|
||||
FROM hex_grid hg
|
||||
INNER JOIN user_points up ON ST_Intersects(hg.hex_geom_utm, up.point_geom_utm)
|
||||
),
|
||||
hexagon_stats AS (
|
||||
SELECT
|
||||
hwp.hex_geom_utm,
|
||||
hwp.hex_i,
|
||||
hwp.hex_j,
|
||||
COUNT(up.id) as point_count,
|
||||
MIN(up.timestamp) as earliest_point,
|
||||
MAX(up.timestamp) as latest_point
|
||||
FROM hexagons_with_points hwp
|
||||
INNER 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_AsGeoJSON(ST_Transform(hex_geom_utm, 4326)) as geojson,
|
||||
hex_i,
|
||||
hex_j,
|
||||
point_count,
|
||||
earliest_point,
|
||||
latest_point,
|
||||
row_number() OVER (ORDER BY point_count DESC) as id
|
||||
FROM hexagon_stats
|
||||
ORDER BY point_count DESC
|
||||
LIMIT #{MAX_HEXAGONS_PER_REQUEST};
|
||||
SQL
|
||||
end
|
||||
|
||||
def build_date_filter
|
||||
return '' unless start_date || end_date
|
||||
|
||||
conditions = []
|
||||
conditions << "timestamp >= EXTRACT(EPOCH FROM '#{start_date}'::timestamp)" if start_date
|
||||
conditions << "timestamp <= EXTRACT(EPOCH FROM '#{end_date}'::timestamp)" if end_date
|
||||
|
||||
conditions.any? ? "AND #{conditions.join(' AND ')}" : ''
|
||||
end
|
||||
|
||||
def execute_sql(sql)
|
||||
ActiveRecord::Base.connection.execute(sql)
|
||||
ExceptionReporter.call(e, message)
|
||||
raise PostGISError, message
|
||||
end
|
||||
|
||||
def format_hexagons(result)
|
||||
|
|
@ -223,8 +78,8 @@ class Maps::HexagonGrid
|
|||
total_points += point_count
|
||||
|
||||
# Parse timestamps and format dates
|
||||
earliest = row['earliest_point'] ? Time.at(row['earliest_point'].to_f).iso8601 : nil
|
||||
latest = row['latest_point'] ? Time.at(row['latest_point'].to_f).iso8601 : nil
|
||||
earliest = row['earliest_point'] ? Time.zone.at(row['earliest_point'].to_f).iso8601 : nil
|
||||
latest = row['latest_point'] ? Time.zone.at(row['latest_point'].to_f).iso8601 : nil
|
||||
|
||||
{
|
||||
type: 'Feature',
|
||||
|
|
@ -237,14 +92,11 @@ class Maps::HexagonGrid
|
|||
hex_size: hex_size,
|
||||
point_count: point_count,
|
||||
earliest_point: earliest,
|
||||
latest_point: latest,
|
||||
density: calculate_density(point_count)
|
||||
latest_point: latest
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
Rails.logger.info "Generated #{hexagons.count} hexagons containing #{total_points} points for area #{area_km2.round(2)} km²"
|
||||
|
||||
{
|
||||
type: 'FeatureCollection',
|
||||
features: hexagons,
|
||||
|
|
@ -260,20 +112,10 @@ class Maps::HexagonGrid
|
|||
}
|
||||
end
|
||||
|
||||
def calculate_density(point_count)
|
||||
# Calculate points per km² for the hexagon
|
||||
# A hexagon with radius 500m has area ≈ 0.65 km²
|
||||
hexagon_area_km2 = 0.65 * (hex_size / 500.0)**2
|
||||
(point_count / hexagon_area_km2).round(2)
|
||||
end
|
||||
|
||||
def build_date_range_metadata
|
||||
return nil unless start_date || end_date
|
||||
|
||||
{
|
||||
start_date: start_date,
|
||||
end_date: end_date
|
||||
}
|
||||
{ start_date:, end_date: }
|
||||
end
|
||||
|
||||
def validate!
|
||||
|
|
@ -283,4 +125,11 @@ class Maps::HexagonGrid
|
|||
|
||||
raise InvalidCoordinatesError, errors.full_messages.join(', ')
|
||||
end
|
||||
|
||||
def viewport_valid?
|
||||
viewport_width &&
|
||||
viewport_height &&
|
||||
viewport_width.positive? &&
|
||||
viewport_height.positive?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :with_sharing_enabled do
|
||||
after(:create) do |stat, evaluator|
|
||||
after(:create) do |stat, _evaluator|
|
||||
stat.enable_sharing!(expiration: 'permanent')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
245
spec/queries/hexagon_query_spec.rb
Normal file
245
spec/queries/hexagon_query_spec.rb
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe HexagonQuery, type: :query do
|
||||
let(:user) { create(:user) }
|
||||
let(:min_lon) { -74.1 }
|
||||
let(:min_lat) { 40.6 }
|
||||
let(:max_lon) { -73.9 }
|
||||
let(:max_lat) { 40.8 }
|
||||
let(:hex_size) { 500 }
|
||||
|
||||
describe '#initialize' do
|
||||
it 'sets required parameters' do
|
||||
query = described_class.new(
|
||||
min_lon: min_lon,
|
||||
min_lat: min_lat,
|
||||
max_lon: max_lon,
|
||||
max_lat: max_lat,
|
||||
hex_size: hex_size
|
||||
)
|
||||
|
||||
expect(query.min_lon).to eq(min_lon)
|
||||
expect(query.min_lat).to eq(min_lat)
|
||||
expect(query.max_lon).to eq(max_lon)
|
||||
expect(query.max_lat).to eq(max_lat)
|
||||
expect(query.hex_size).to eq(hex_size)
|
||||
end
|
||||
|
||||
it 'sets optional parameters' do
|
||||
start_date = '2024-06-01T00:00:00Z'
|
||||
end_date = '2024-06-30T23:59:59Z'
|
||||
|
||||
query = described_class.new(
|
||||
min_lon: min_lon,
|
||||
min_lat: min_lat,
|
||||
max_lon: max_lon,
|
||||
max_lat: max_lat,
|
||||
hex_size: hex_size,
|
||||
user_id: user.id,
|
||||
start_date: start_date,
|
||||
end_date: end_date
|
||||
)
|
||||
|
||||
expect(query.user_id).to eq(user.id)
|
||||
expect(query.start_date).to eq(start_date)
|
||||
expect(query.end_date).to eq(end_date)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
let(:query) do
|
||||
described_class.new(
|
||||
min_lon: min_lon,
|
||||
min_lat: min_lat,
|
||||
max_lon: max_lon,
|
||||
max_lat: max_lat,
|
||||
hex_size: hex_size,
|
||||
user_id: user.id
|
||||
)
|
||||
end
|
||||
|
||||
context 'with no points' do
|
||||
it 'executes without error and returns empty result' do
|
||||
result = query.call
|
||||
expect(result.to_a).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'with points in bounding box' do
|
||||
before do
|
||||
# Create test points within the bounding box
|
||||
create(:point,
|
||||
user:,
|
||||
latitude: 40.7,
|
||||
longitude: -74.0,
|
||||
timestamp: Time.new(2024, 6, 15, 12, 0).to_i)
|
||||
create(:point,
|
||||
user:,
|
||||
latitude: 40.75,
|
||||
longitude: -73.95,
|
||||
timestamp: Time.new(2024, 6, 16, 14, 0).to_i)
|
||||
end
|
||||
|
||||
it 'returns hexagon results with expected structure' do
|
||||
result = query.call
|
||||
result_array = result.to_a
|
||||
|
||||
expect(result_array).not_to be_empty
|
||||
|
||||
first_hex = result_array.first
|
||||
expect(first_hex).to have_key('geojson')
|
||||
expect(first_hex).to have_key('hex_i')
|
||||
expect(first_hex).to have_key('hex_j')
|
||||
expect(first_hex).to have_key('point_count')
|
||||
expect(first_hex).to have_key('earliest_point')
|
||||
expect(first_hex).to have_key('latest_point')
|
||||
expect(first_hex).to have_key('id')
|
||||
|
||||
# Verify geojson can be parsed
|
||||
geojson = JSON.parse(first_hex['geojson'])
|
||||
expect(geojson).to have_key('type')
|
||||
expect(geojson).to have_key('coordinates')
|
||||
end
|
||||
|
||||
it 'filters by user_id correctly' do
|
||||
other_user = create(:user)
|
||||
# Create points for a different user (should be excluded)
|
||||
create(:point,
|
||||
user: other_user,
|
||||
latitude: 40.72,
|
||||
longitude: -73.98,
|
||||
timestamp: Time.new(2024, 6, 17, 16, 0).to_i)
|
||||
|
||||
result = query.call
|
||||
result_array = result.to_a
|
||||
|
||||
# Should only include hexagons with the specified user's points
|
||||
total_points = result_array.sum { |row| row['point_count'].to_i }
|
||||
expect(total_points).to eq(2) # Only the 2 points from our user
|
||||
end
|
||||
end
|
||||
|
||||
context 'with date filtering' do
|
||||
let(:query_with_dates) do
|
||||
described_class.new(
|
||||
min_lon: min_lon,
|
||||
min_lat: min_lat,
|
||||
max_lon: max_lon,
|
||||
max_lat: max_lat,
|
||||
hex_size: hex_size,
|
||||
user_id: user.id,
|
||||
start_date: '2024-06-15T00:00:00Z',
|
||||
end_date: '2024-06-16T23:59:59Z'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
# Create points within and outside the date range
|
||||
create(:point,
|
||||
user:,
|
||||
latitude: 40.7,
|
||||
longitude: -74.0,
|
||||
timestamp: Time.new(2024, 6, 15, 12, 0).to_i) # Within range
|
||||
create(:point,
|
||||
user:,
|
||||
latitude: 40.71,
|
||||
longitude: -74.01,
|
||||
timestamp: Time.new(2024, 6, 20, 12, 0).to_i) # Outside range
|
||||
end
|
||||
|
||||
it 'filters points by date range' do
|
||||
result = query_with_dates.call
|
||||
result_array = result.to_a
|
||||
|
||||
expect(result_array).not_to be_empty
|
||||
|
||||
# Should only include the point within the date range
|
||||
total_points = result_array.sum { |row| row['point_count'].to_i }
|
||||
expect(total_points).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without user_id filter' do
|
||||
let(:query_no_user) do
|
||||
described_class.new(
|
||||
min_lon: min_lon,
|
||||
min_lat: min_lat,
|
||||
max_lon: max_lon,
|
||||
max_lat: max_lat,
|
||||
hex_size: hex_size
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
user1 = create(:user)
|
||||
user2 = create(:user)
|
||||
|
||||
create(:point, user: user1, latitude: 40.7, longitude: -74.0, timestamp: Time.current.to_i)
|
||||
create(:point, user: user2, latitude: 40.75, longitude: -73.95, timestamp: Time.current.to_i)
|
||||
end
|
||||
|
||||
it 'includes points from all users' do
|
||||
result = query_no_user.call
|
||||
result_array = result.to_a
|
||||
|
||||
expect(result_array).not_to be_empty
|
||||
|
||||
# Should include points from both users
|
||||
total_points = result_array.sum { |row| row['point_count'].to_i }
|
||||
expect(total_points).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_date_filter (private method behavior)' do
|
||||
context 'when testing date filter behavior through query execution' do
|
||||
it 'works correctly with start_date only' do
|
||||
query = described_class.new(
|
||||
min_lon: min_lon,
|
||||
min_lat: min_lat,
|
||||
max_lon: max_lon,
|
||||
max_lat: max_lat,
|
||||
hex_size: hex_size,
|
||||
user_id: user.id,
|
||||
start_date: '2024-06-15T00:00:00Z'
|
||||
)
|
||||
|
||||
# Should execute without SQL syntax errors
|
||||
expect { query.call }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'works correctly with end_date only' do
|
||||
query = described_class.new(
|
||||
min_lon: min_lon,
|
||||
min_lat: min_lat,
|
||||
max_lon: max_lon,
|
||||
max_lat: max_lat,
|
||||
hex_size: hex_size,
|
||||
user_id: user.id,
|
||||
end_date: '2024-06-30T23:59:59Z'
|
||||
)
|
||||
|
||||
# Should execute without SQL syntax errors
|
||||
expect { query.call }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'works correctly with both start_date and end_date' do
|
||||
query = described_class.new(
|
||||
min_lon: min_lon,
|
||||
min_lat: min_lat,
|
||||
max_lon: max_lon,
|
||||
max_lat: max_lat,
|
||||
hex_size: hex_size,
|
||||
user_id: user.id,
|
||||
start_date: '2024-06-01T00:00:00Z',
|
||||
end_date: '2024-06-30T23:59:59Z'
|
||||
)
|
||||
|
||||
# Should execute without SQL syntax errors
|
||||
expect { query.call }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -111,5 +111,4 @@ RSpec.describe '/stats', type: :request do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -179,18 +179,22 @@ RSpec.describe 'Users Export-Import Integration', type: :service do
|
|||
import_stats = import_service.import
|
||||
|
||||
# Verify all entities were imported correctly
|
||||
expect(import_stats[:places_created]).to eq(original_places_count),
|
||||
expect(import_stats[:places_created]).to \
|
||||
eq(original_places_count),
|
||||
"Expected #{original_places_count} places to be created, got #{import_stats[:places_created]}"
|
||||
expect(import_stats[:visits_created]).to eq(original_visits_count),
|
||||
expect(import_stats[:visits_created]).to \
|
||||
eq(original_visits_count),
|
||||
"Expected #{original_visits_count} visits to be created, got #{import_stats[:visits_created]}"
|
||||
|
||||
# Verify the imported user has access to all their data
|
||||
imported_places_count = import_user.places.distinct.count
|
||||
imported_visits_count = import_user.visits.count
|
||||
|
||||
expect(imported_places_count).to eq(original_places_count),
|
||||
expect(imported_places_count).to \
|
||||
eq(original_places_count),
|
||||
"Expected user to have access to #{original_places_count} places, got #{imported_places_count}"
|
||||
expect(imported_visits_count).to eq(original_visits_count),
|
||||
expect(imported_visits_count).to \
|
||||
eq(original_visits_count),
|
||||
"Expected user to have #{original_visits_count} visits, got #{imported_visits_count}"
|
||||
|
||||
# Verify specific visits have their place associations
|
||||
|
|
@ -211,7 +215,8 @@ RSpec.describe 'Users Export-Import Integration', type: :service do
|
|||
private
|
||||
|
||||
def create_full_user_dataset(user)
|
||||
user.update!(settings: {
|
||||
user.update!(settings:
|
||||
{
|
||||
'distance_unit' => 'km',
|
||||
'timezone' => 'America/New_York',
|
||||
'immich_url' => 'https://immich.example.com',
|
||||
|
|
|
|||
Loading…
Reference in a new issue