mongoid/mongoid-geospatial

View on GitHub
lib/mongoid/geospatial/fields/point.rb

Summary

Maintainability
A
1 hr
Test Coverage
module Mongoid
  module Geospatial
    # Point
    #
    class Point
      include Enumerable
      attr_accessor :x, :y, :z

      def initialize(x, y, z = nil)
        @x = x
        @y = y
        @z = z
      end

      # Object -> Database
      # Let's store NilClass if we are invalid.
      #
      # @return (Array)
      def mongoize
        return nil unless x && y

        [x, y]
      end
      alias to_a mongoize
      alias to_xy mongoize

      def [](args)
        mongoize[args]
      end

      def each
        yield x
        yield y
      end

      #
      # Point representation as a Hash
      #
      # @return [Hash] with { xl => x, yl => y }
      #
      def to_hsh(xl = :x, yl = :y)
        { xl => x, yl => y }
      end
      alias to_hash to_hsh

      #
      # Helper for [self, radius]
      #
      # @return [Array] with [self, radius]
      #
      def radius(r = 1)
        [mongoize, r]
      end

      #
      # Radius Sphere
      #
      # Validates that #x & #y are `Numeric`
      #
      # @return [Array] with [self, radius / earth radius]
      #
      def radius_sphere(r = 1, unit = :km)
        radius r.to_f / Mongoid::Geospatial.earth_radius[unit]
      end

      #
      # Am I valid?
      #
      # Validates that #x & #y are `Numeric`
      #
      # @return [Boolean] if self #x && #y are valid
      #
      def valid?
        x && y && x.is_a?(Numeric) && y.is_a?(Numeric)
      end

      #
      # Point definition as string
      #
      # "x, y"
      #
      # @return [String] Point as comma separated String
      #
      def to_s
        "#{x}, #{y}"
      end

      #
      # Point inverse/reverse
      #
      # MongoDB: "x, y"
      # Reverse: "y, x"
      #
      # @return [Array] Point reversed: "y, x"
      #
      def reverse
        [y, x]
      end

      #
      # Distance calculation methods. Thinking about not using it
      # One needs to choose and external lib. GeoRuby or RGeo
      #
      # Return the distance between the 2D points (ie taking care
      # only of the x and y coordinates), assuming the points are
      # in projected coordinates. Euclidian distance in whatever
      # unit the x and y ordinates are.
      # def euclidian_distance(point)
      #   Math.sqrt((point.x - x)**2 + (point.y - y)**2)
      # end

      # # Spherical distance in meters, using 'Haversine' formula.
      # # with a radius of 6471000m
      # # Assumes x is the lon and y the lat, in degrees (Changed
      # in version 1.1).
      # # The user has to make sure using this distance makes sense
      # (ie she should be in latlon coordinates)
      # def spherical_distance(point,r=6370997.0)
      #   dlat = (point.lat - lat) * DEG2RAD / 2
      #   dlon = (point.lon - lon) * DEG2RAD / 2

      #   a = Math.sin(dlat)**2 + Math.cos(lat * DEG2RAD) *
      #         Math.cos(point.lat * DEG2RAD) * Math.sin(dlon)**2
      #   c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
      #   r * c
      # end

      class << self
        #
        # Database -> Object
        # Get it back
        def demongoize(obj)
          obj && new(*obj)
        end

        #
        # Object -> Database
        # Send it to MongoDB
        def mongoize(obj)
          case obj
          when Point  then obj.mongoize
          when String then from_string(obj)
          when Array  then from_array(obj)
          when Hash   then from_hash(obj)
          when NilClass then nil
          else
            return obj.to_xy if obj.respond_to?(:to_xy)

            raise 'Invalid Point'
          end
        end

        # Converts the object that was supplied to a criteria
        # into a database friendly form.
        def evolve(obj)
          case obj
          when Point then obj.mongoize
          else obj
          end
        end

        private

        #
        # Sanitize a `Point` from a `String`
        #
        # Makes life easier:
        # ""         ->    []
        # "1, 2"     ->    [1.0, 2.0]
        # "1.1 2.2"  ->    [1.1, 2.2]
        #
        # @return (Array)
        #
        def from_string(str)
          return nil if str.empty?

          str.split(/,|\s/).reject(&:empty?).map(&:to_f)
        end

        #
        # Sanitize a `Point` from an `Array`
        #
        # Also makes life easier:
        # []          ->   []
        # [1,2]       ->   [1.0, 2.0]
        #
        # @return (Array)
        #
        def from_array(array)
          return nil if array.empty?

          array.flatten[0..1].map(&:to_f)
        end

        #
        # Sanitize a `Point` from a `Hash`
        #
        # Uses Mongoid::Geospatial.lat_symbols & lng_symbols
        #
        # Also makes life easier:
        # {x: 1.0, y: 2.0}       ->  [1.0, 2.0]
        # {lat: 1.0, lon: 2.0}   ->  [1.0, 2.0]
        # {lat: 1.0, long: 2.0}  ->  [1.0, 2.0]
        #
        # Throws error if hash has less than 2 items.
        #
        # @return (Array)
        #
        def from_hash(hsh)
          raise 'Hash must have at least 2 items' if hsh.size < 2

          [from_hash_x(hsh), from_hash_y(hsh)]
        end

        def from_hash_y(hsh)
          v = (Mongoid::Geospatial::Config::Point.y & hsh.keys).first
          return hsh[v].to_f if !v.nil? && hsh[v]

          raise "Hash must contain #{Mongoid::Geospatial::Config::Point.y.inspect}"
        end

        def from_hash_x(hsh)
          v = (Mongoid::Geospatial::Config::Point.x & hsh.keys).first
          return hsh[v].to_f if !v.nil? && hsh[v]

          raise "Hash must contain #{Mongoid::Geospatial::Config::Point.x.inspect}"
        end
      end # << self
    end # Point
  end # Geospatial
end # Mongoid