dryade/georuby-ext

View on GitHub
lib/georuby-ext/georuby/locators.rb

Summary

Maintainability
B
4 hrs
Test Coverage
class GeoRuby::SimpleFeatures::MultiLineString

  alias_method :lines, :geometries

  def locate_point(target)
    nearest_locator = nearest_locator(target)
    nearest_locator.location + nearest_locator.index
  end

  def interpolate_point(location)
    line_index, line_location = location.to_i, location % 1
    if line = lines[line_index]
      line.interpolate_point(line_location)
    end
  end

  def nearest_locator(target)
    locators(target).min_by(&:distance_from_line)
  end

  def locators(point)
    [].tap do |locators|
      lines.each_with_index do |line, index| 
        locators << PointLocator.new(point, line, index) 
      end
    end
  end

  class PointLocator

    attr_reader :target, :line, :index

    def initialize(target, line, index)
      @target = target
      @line = line
      @index = index
    end

    def location
      line.locate_point(target)
    end

    def distance_on_line
      line.distance_on_line(target)
    end

    def distance_from_line
      line.distance_from_line(target)
    end

  end

end

class GeoRuby::SimpleFeatures::LineString

  def locate_point(target)
    distance_on_line(target) / spherical_distance
  end

  def distance_on_line(target)
    nearest_locator = nearest_locator(target)
    nearest_locator.distance_on_segment + nearest_locator.segment.line_distance_at_departure
  end

  def distance_from_line(target)
    nearest_locator(target).distance_from_segment
  end

  def nearest_locator(target)
    locators(target).min_by(&:distance_from_segment)
  end

  def locators(point)
    segments.collect { |segment| segment.locator(point) }
  end

  def segments_without_cache
    previous_point = nil
    distance_from_departure = 0

    
    points.inject([]) do |segments, point|
      Segment.new(previous_point, point).tap do |segment|
        segment.line = self
        segment.line_distance_at_departure = distance_from_departure

        distance_from_departure += segment.distance
        
        segments << segment
      end if previous_point
      
      previous_point = point
      segments
    end
  end

  def segments_with_cache
    @segments ||= segments_without_cache
  end
  alias_method :segments, :segments_with_cache

  def interpolate_point(location)
    return points.last if location >= 1
    return points.first if location <= 0

    distance_on_line = location * spherical_distance

    segment = segments.find do |segment|
      segment.line_distance_at_arrival > distance_on_line
    end

    location_on_segment =
      (distance_on_line - segment.line_distance_at_departure) / segment.distance

    segment.interpolate_point location_on_segment
  end

  class Segment

    attr_reader :departure, :arrival
    
    def initialize(departure, arrival)
      @departure, @arrival = departure, arrival
    end

    attr_accessor :line, :line_distance_at_departure

    def line_distance_at_arrival
      line_distance_at_departure + distance
    end

    def locator(target)
      PointLocator.new target, self
    end

    def location_in_line
      if line and line_distance_at_departure
        line_distance_at_departure / line.distance
      end
    end

    def square_of_distance
      distance**2
    end

    def distance
      @distance ||= departure.spherical_distance(arrival)
    end

    def to_s
      "#{departure.x},#{departure.y}..#{arrival.x},#{arrival.y}"
    end

    def interpolate_point(location)
      dx, dy = 
        (arrival.x - departure.x)*location, (arrival.y - departure.y)*location
      GeoRuby::SimpleFeatures::Point.from_x_y departure.x + dx, departure.y + dy, line.srid
    end

  end

  class PointLocator
    include Math

    attr_reader :target, :segment
    delegate :departure, :arrival, :to => :segment
    
    def initialize(target, segment_or_departure, arrival = nil)
      @segment = 
        if arrival
          Segment.new(segment_or_departure, arrival)
        else
          segment_or_departure
        end
      @target = target

      raise "Target is not defined" unless target
    end

    def distance_from_segment
      return 0 if [segment.departure, segment.arrival].include?(target)
        
      sin_angle * target_distance_from_departure
    end

    def distance_on_segment
      # like cos_angle * target_distance_from_departure
      # scalar_product / square_of_segment_distance
      scalar_product / segment_distance
    end

    def locate_point
      scalar_product / square_of_segment_distance
    end

    # def scalar_product
    #   (target.x-departure.x)*(arrival.x-departure.x) + (target.y-departure.y)*(arrival.y-departure.y).to_f
    # end

    def scalar_product
      departure_target_metric_delta = departure.metric_delta(target)
      departure_arrival_metric_delta = departure.metric_delta(arrival)

      departure_target_metric_delta[0]*departure_arrival_metric_delta[0] +
        departure_target_metric_delta[1]*departure_arrival_metric_delta[1]
    end

    def angle
      acos cos_angle
    end

    def sin_angle
      sin angle
    end

    def cos_angle
      [-1, [1, scalar_product / segment_distance / target_distance_from_departure].min].max
    end

    def square_of_segment_distance
      segment_distance ** 2
    end

    def segment_distance
      segment.distance
    end

    def target_distance_from_departure
      departure.spherical_distance target
    end

  end

end