2025-08-30 17:18:16 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
|
|
module LocationSearch
|
|
|
|
|
class SpatialMatcher
|
|
|
|
|
def initialize
|
|
|
|
|
# Using PostGIS for efficient spatial queries
|
|
|
|
|
end
|
|
|
|
|
|
2025-08-31 06:08:33 -04:00
|
|
|
# Debug method to test spatial queries directly
|
|
|
|
|
def debug_points_near(user, latitude, longitude, radius_meters = 1000)
|
|
|
|
|
query = <<~SQL
|
|
|
|
|
SELECT
|
|
|
|
|
p.id,
|
|
|
|
|
p.timestamp,
|
|
|
|
|
ST_Y(p.lonlat::geometry) as latitude,
|
|
|
|
|
ST_X(p.lonlat::geometry) as longitude,
|
|
|
|
|
p.city,
|
|
|
|
|
p.country,
|
|
|
|
|
ST_Distance(p.lonlat, ST_Point(#{longitude}, #{latitude})::geography) as distance_meters
|
|
|
|
|
FROM points p
|
|
|
|
|
WHERE p.user_id = #{user.id}
|
|
|
|
|
AND ST_DWithin(p.lonlat, ST_Point(#{longitude}, #{latitude})::geography, #{radius_meters})
|
|
|
|
|
ORDER BY distance_meters ASC
|
|
|
|
|
LIMIT 10;
|
|
|
|
|
SQL
|
|
|
|
|
|
|
|
|
|
puts "=== DEBUG SPATIAL QUERY ==="
|
|
|
|
|
puts "Searching for user #{user.id} near [#{latitude}, #{longitude}] within #{radius_meters}m"
|
|
|
|
|
puts "Query: #{query}"
|
|
|
|
|
|
|
|
|
|
results = ActiveRecord::Base.connection.exec_query(query)
|
|
|
|
|
puts "Found #{results.count} points:"
|
|
|
|
|
|
|
|
|
|
results.each do |row|
|
|
|
|
|
puts "- Point #{row['id']}: [#{row['latitude']}, #{row['longitude']}] - #{row['distance_meters'].to_f.round(2)}m away"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
results
|
|
|
|
|
end
|
|
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
def find_points_near(user, latitude, longitude, radius_meters, date_options = {})
|
|
|
|
|
points_query = build_spatial_query(user, latitude, longitude, radius_meters, date_options)
|
|
|
|
|
|
|
|
|
|
# Execute query and return results with calculated distance
|
|
|
|
|
ActiveRecord::Base.connection.exec_query(points_query)
|
|
|
|
|
.map { |row| format_point_result(row) }
|
|
|
|
|
.sort_by { |point| point[:timestamp] }
|
|
|
|
|
.reverse # Most recent first
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def build_spatial_query(user, latitude, longitude, radius_meters, date_options = {})
|
|
|
|
|
date_filter = build_date_filter(date_options)
|
|
|
|
|
|
|
|
|
|
<<~SQL
|
|
|
|
|
SELECT
|
|
|
|
|
p.id,
|
|
|
|
|
p.timestamp,
|
2025-08-31 06:08:33 -04:00
|
|
|
ST_Y(p.lonlat::geometry) as latitude,
|
|
|
|
|
ST_X(p.lonlat::geometry) as longitude,
|
2025-08-30 17:18:16 -04:00
|
|
|
p.city,
|
|
|
|
|
p.country,
|
|
|
|
|
p.altitude,
|
|
|
|
|
p.accuracy,
|
|
|
|
|
ST_Distance(p.lonlat, ST_Point(#{longitude}, #{latitude})::geography) as distance_meters,
|
|
|
|
|
TO_TIMESTAMP(p.timestamp) as recorded_at
|
|
|
|
|
FROM points p
|
|
|
|
|
WHERE p.user_id = #{user.id}
|
|
|
|
|
AND ST_DWithin(p.lonlat, ST_Point(#{longitude}, #{latitude})::geography, #{radius_meters})
|
|
|
|
|
#{date_filter}
|
|
|
|
|
ORDER BY p.timestamp DESC;
|
|
|
|
|
SQL
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def build_date_filter(date_options)
|
|
|
|
|
return '' unless date_options[:date_from] || date_options[:date_to]
|
|
|
|
|
|
|
|
|
|
filters = []
|
|
|
|
|
|
|
|
|
|
if date_options[:date_from]
|
|
|
|
|
timestamp_from = date_options[:date_from].to_time.to_i
|
|
|
|
|
filters << "p.timestamp >= #{timestamp_from}"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if date_options[:date_to]
|
|
|
|
|
# Add one day to include the entire end date
|
|
|
|
|
timestamp_to = (date_options[:date_to] + 1.day).to_time.to_i
|
|
|
|
|
filters << "p.timestamp < #{timestamp_to}"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return '' if filters.empty?
|
|
|
|
|
|
|
|
|
|
"AND #{filters.join(' AND ')}"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def format_point_result(row)
|
|
|
|
|
{
|
|
|
|
|
id: row['id'].to_i,
|
|
|
|
|
timestamp: row['timestamp'].to_i,
|
|
|
|
|
coordinates: [row['latitude'].to_f, row['longitude'].to_f],
|
|
|
|
|
city: row['city'],
|
|
|
|
|
country: row['country'],
|
|
|
|
|
altitude: row['altitude']&.to_i,
|
|
|
|
|
accuracy: row['accuracy']&.to_i,
|
|
|
|
|
distance_meters: row['distance_meters'].to_f.round(2),
|
|
|
|
|
recorded_at: row['recorded_at'],
|
|
|
|
|
date: Time.zone.at(row['timestamp'].to_i).iso8601
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|