hackedteam/rcs-db

View on GitHub
lib/rcs-db/position/point.rb

Summary

Maintainability
A
1 hr
Test Coverage
#
#  Handling of points on the map (lat, lon, radius)
#

require 'rcs-common/trace'
require 'rvincenty'
require 'active_support'

class Point
  attr_accessor :lat, :lon, :r, :time, :start, :end

  # to obtain 2sigma on gps precision = 80%
  # http://en.wikipedia.org/wiki/Standard_deviation
  INTERSECT_DELTA = 1.281552
  # the distance to be considered near (in meters)
  NEAR_DISTANCE = 500
  # minimum radius of a point for the best_similar method
  MINIMUM_SIMILAR_RADIUS = 30
  # minimum radius assigned for invalid values
  MIN_RADIUS = 30
  # Earth radius in kilometers
  EARTH_RADIUS = 6371
  # Earth equator in kilometers
  EARTH_EQUATOR = 40075.017
  # minimum intersection time between two timeframes
  MINIMUM_INTERSECT_TIME = 10.minutes

  def initialize(params = {})
    set_attributes_default_values

    if params[:time]
      [:time, :start, :end].each do |param_name|
        send :"#{param_name}=", convert_to_time(params[:time])
      end
    end

    [:start, :end].each do |param_name|
      value = params[param_name]
      next unless value
      send :"#{param_name}=", convert_to_time(value)
    end

    [:lat, :lon, :r].each do |param_name|
      value = params[param_name]
      next unless value
      send :"#{param_name}=", params[param_name]
    end

    # set a minimum radius
    self.r = MIN_RADIUS if r <= 0
  end

  def set_attributes_default_values
    now = Time.now
    hash = {lat: 0.0, lon: 0.0, r: MIN_RADIUS, time: now, start: now, end: now}
    hash.each do |attribute_name, default_value|
      send :"#{attribute_name}=", default_value
    end
  end

  def convert_to_time value
    return Time.at(value) if value.is_a? Integer
    raise "invalid time [#{value}] (#{value.class})" unless value.is_a? Time
    value
  end

  def to_s
    "#{self.lat} #{self.lon} #{self.r} - #{self.time} (#{self.start} #{self.end})"
  end

  def same_point?(b)
    self.lat == b.lat and self.lon == b.lon and self.r == b.r
  end

  def ==(other)
    self.class == other.class and
    self.lat == other.lat and
    self.lon == other.lon and
    self.r == other.r and
    self.time == other.time and
    self.start == other.start and
    self.end == other.end
  end
  alias_method :eql?, :==

=begin
  # Haversine formula to calculate the distance between two coordinates
  def distance(point)
    a = [self.lat, self.lon]
    b = [point.lat, point.lon]

    rad_per_deg = Math::PI/180  # PI / 180
    rm = EARTH_RADIUS * 1000

    # Delta, converted to rad
    dlon_rad = (b[1] - a[1]) * rad_per_deg  
    dlat_rad = (b[0] - a[0]) * rad_per_deg

    lat1_rad, lon1_rad = a.map! {|i| i * rad_per_deg }
    lat2_rad, lon2_rad = b.map! {|i| i * rad_per_deg }

    a = Math.sin(dlat_rad/2)**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon_rad/2)**2
    c = 2 * Math.asin(Math.sqrt(a))

    # distance in meters
    (rm * c).to_i
  end
=end

  # Vincenty formula to calculate the distance between two coordinates
  def distance(point)
    a = [self.lat, self.lon]
    b = [point.lat, point.lon]

    RVincenty.distance(a, b).to_i
  end

  def intersect?(point)
    # two circles intersect if the distance between the centers is 
    # less than the sum of the two radius

    # add al little delta to the radius since that radius represent the 65% of probability to be within
    distance(point) < self.r * INTERSECT_DELTA + point.r * INTERSECT_DELTA
  end

  def overlap?(point)
    # a circle overlaps another if the radius is bigger than
    # the distance between the centers plus the radius of the second point
    self.r >= self.distance(point) + point.r
  end

  def self.overlapped?(a, b)
    # two circles overlap if the distance between the centers
    # plus the minimum radius is less than the bigger radius
    a.distance(b) + [a.r, b.r].min <= [a.r, b.r].max
  end

  def near?(b)
    distance(b) <= NEAR_DISTANCE
  end

  def similar_to?(b)
    # two circles are considered similar if:
    # - they overlap
    # - they intersect but are near each other
    return true if self.class.overlapped?(self, b)

    return true if intersect?(b) and near?(b)

    return false
  end

  def self.best_similar(*points)
    # to find the best similar, just check if they are all similar
    return nil if not points.combination(2).all? {|c| c.first.similar_to? c.last}

    # and then take the smaller (more precise) one
    best = points.min do |x, y|
      if x.r == y.r
        x.time <=> y.time
      else
        x.r <=> y.r
      end
    end

    # adjust the radius to avoid too precise points
    best.r = MINIMUM_SIMILAR_RADIUS if best.r < MINIMUM_SIMILAR_RADIUS

    return best
  end


  def intersect_timeframes? timeframes
    timeframes = [timeframes].flatten
    range1 = self.start.to_i..self.end.to_i

    timeframes.each do |timeframe|
      range2 = timeframe['start'].to_i..timeframe['end'].to_i
      intersection = range1.to_a & range2.to_a
      next if intersection.empty?
      delta = intersection.max - intersection.min
      return true if delta >= MINIMUM_INTERSECT_TIME
    end

    false
  end
end