dawarich/app/models/concerns/distanceable.rb
2025-04-04 22:16:52 +02:00

109 lines
2.9 KiB
Ruby

# frozen_string_literal: true
module Distanceable
extend ActiveSupport::Concern
DISTANCE_UNITS = {
km: 1000, # to meters
mi: 1609.34, # to meters
m: 1, # already in meters
ft: 0.3048, # to meters
yd: 0.9144 # to meters
}.freeze
module ClassMethods
def total_distance(points = nil, unit = :km)
# Handle method being called directly on relation vs with array
if points.nil?
calculate_distance_for_relation(unit)
else
calculate_distance_for_array(points, unit)
end
end
private
def calculate_distance_for_relation(unit)
unless DISTANCE_UNITS.key?(unit.to_sym)
raise ArgumentError, "Invalid unit. Supported units are: #{DISTANCE_UNITS.keys.join(', ')}"
end
distance_in_meters = connection.select_value(<<-SQL.squish)
WITH points_with_previous AS (
SELECT
lonlat,
LAG(lonlat) OVER (ORDER BY timestamp) as prev_lonlat
FROM (#{to_sql}) AS points
)
SELECT COALESCE(
SUM(
ST_Distance(
lonlat::geography,
prev_lonlat::geography
)
),
0
)
FROM points_with_previous
WHERE prev_lonlat IS NOT NULL
SQL
distance_in_meters.to_f / DISTANCE_UNITS[unit.to_sym]
end
def calculate_distance_for_array(points, unit = :km)
unless DISTANCE_UNITS.key?(unit.to_sym)
raise ArgumentError, "Invalid unit. Supported units are: #{DISTANCE_UNITS.keys.join(', ')}"
end
return 0 if points.length < 2
total_meters = points.each_cons(2).sum do |point1, point2|
connection.select_value(
'SELECT ST_Distance(ST_GeomFromEWKT($1)::geography, ST_GeomFromEWKT($2)::geography)',
nil,
[point1.lonlat, point2.lonlat]
)
end
total_meters.to_f / DISTANCE_UNITS[unit.to_sym]
end
end
def distance_to(other_point, unit = :km)
unless DISTANCE_UNITS.key?(unit.to_sym)
raise ArgumentError, "Invalid unit. Supported units are: #{DISTANCE_UNITS.keys.join(', ')}"
end
# Extract coordinates based on what type other_point is
other_lonlat = extract_point(other_point)
return nil if other_lonlat.nil?
# Calculate distance in meters using PostGIS
distance_in_meters = self.class.connection.select_value(<<-SQL.squish)
SELECT ST_Distance(
ST_GeomFromEWKT('#{lonlat}')::geography,
ST_GeomFromEWKT('#{other_lonlat}')::geography
)
SQL
# Convert to requested unit
distance_in_meters.to_f / DISTANCE_UNITS[unit.to_sym]
end
private
def extract_point(point)
case point
when Array
unless point.length == 2
raise ArgumentError,
'Coordinates array must contain exactly 2 elements [latitude, longitude]'
end
RGeo::Geographic.spherical_factory(srid: 4326).point(point[1], point[0])
when self.class
point.lonlat
end
end
end