# frozen_string_literal: true module Nearable extend ActiveSupport::Concern class_methods do # It accepts an array of coordinates [latitude, longitude] # and an optional radius and distance unit # rubocop:disable Metrics/MethodLength def near(*args) latitude, longitude, radius, unit = extract_coordinates_and_options(*args) unless ::DISTANCE_UNITS.key?(unit.to_sym) raise ArgumentError, "Invalid unit. Supported units are: #{::DISTANCE_UNITS.keys.join(', ')}" end # Convert radius to meters for ST_DWithin radius_in_meters = radius * ::DISTANCE_UNITS[unit.to_sym] # Create a point from the given coordinates point = "SRID=4326;POINT(#{longitude} #{latitude})" where(<<-SQL.squish) ST_DWithin( lonlat::geography, ST_GeomFromEWKT('#{point}')::geography, #{radius_in_meters} ) SQL end def with_distance(*args) latitude, longitude, unit = extract_coordinates_and_options(*args) unless ::DISTANCE_UNITS.key?(unit.to_sym) raise ArgumentError, "Invalid unit. Supported units are: #{::DISTANCE_UNITS.keys.join(', ')}" end point = "SRID=4326;POINT(#{longitude} #{latitude})" conversion_factor = 1.0 / ::DISTANCE_UNITS[unit.to_sym] select(<<-SQL.squish) #{table_name}.*, ST_Distance( lonlat::geography, ST_GeomFromEWKT('#{point}')::geography ) * #{conversion_factor} as distance_in_#{unit} SQL end # rubocop:enable Metrics/MethodLength private def extract_coordinates_and_options(*args) coords = args.first if !coords.is_a?(Array) || coords.length != 2 raise ArgumentError, 'First argument must be coordinates array containing exactly 2 elements [latitude, longitude]' end [coords[0], coords[1], *args[1..]].tap do |extracted| # Set default values for missing options extracted[2] ||= 1 if extracted.length < 3 # default radius extracted[3] ||= :km if extracted.length < 4 # default unit end end end end