Module: Philiprehberger::GeoPoint

Defined in:
lib/philiprehberger/geo_point.rb,
lib/philiprehberger/geo_point/point.rb,
lib/philiprehberger/geo_point/version.rb,
lib/philiprehberger/geo_point/bounding_box.rb

Defined Under Namespace

Classes: BoundingBox, Point

Constant Summary collapse

VERSION =
'0.4.0'

Class Method Summary collapse

Class Method Details

.cluster(points, radius_km:) ⇒ Array<Array<Point>>

Group nearby points into clusters using simple distance-based clustering

Parameters:

  • points (Array<Point>)

    points to cluster

  • radius_km (Float)

    maximum distance between cluster members

Returns:

  • (Array<Array<Point>>)

    array of point clusters



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/philiprehberger/geo_point.rb', line 100

def self.cluster(points, radius_km:)
  remaining = points.dup
  clusters = []

  until remaining.empty?
    seed = remaining.shift
    group = [seed]

    remaining.reject! do |p|
      if seed.distance_to(p, unit: :km) <= radius_km
        group << p
        true
      else
        false
      end
    end

    clusters << group
  end

  clusters
end

.from_geohash(hash) ⇒ Object

Raises:

  • (ArgumentError)


13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/philiprehberger/geo_point.rb', line 13

def self.from_geohash(hash)
  raise ArgumentError, 'Geohash string cannot be empty' if hash.nil? || hash.empty?

  lat_range = [-90.0, 90.0]
  lon_range = [-180.0, 180.0]
  is_lon = true

  hash.each_char do |c|
    idx = Point::GEOHASH_BASE32.index(c)
    raise ArgumentError, "Invalid geohash character: #{c}" if idx.nil?

    4.downto(0) do |bit|
      if is_lon
        mid = (lon_range[0] + lon_range[1]) / 2.0
        if (idx >> bit) & 1 == 1
          lon_range[0] = mid
        else
          lon_range[1] = mid
        end
      else
        mid = (lat_range[0] + lat_range[1]) / 2.0
        if (idx >> bit) & 1 == 1
          lat_range[0] = mid
        else
          lat_range[1] = mid
        end
      end
      is_lon = !is_lon
    end
  end

  lat = (lat_range[0] + lat_range[1]) / 2.0
  lon = (lon_range[0] + lon_range[1]) / 2.0

  Point.new(lat, lon)
end

.inside_polygon?(point, vertices) ⇒ Boolean

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/philiprehberger/geo_point.rb', line 50

def self.inside_polygon?(point, vertices)
  raise ArgumentError, 'Polygon must have at least 3 vertices' if vertices.length < 3

  inside = false
  n = vertices.length
  j = n - 1

  n.times do |i|
    yi = vertices[i].lat
    xi = vertices[i].lon
    yj = vertices[j].lat
    xj = vertices[j].lon

    if ((yi > point.lat) != (yj > point.lat)) &&
       (point.lon < (((xj - xi) * (point.lat - yi)) / (yj - yi)) + xi)
      inside = !inside
    end

    j = i
  end

  inside
end

.nearest(origin, points) ⇒ Point?

Find the nearest point from an array

Parameters:

  • origin (Point)

    the reference point

  • points (Array<Point>)

    candidate points

Returns:

  • (Point, nil)

    the closest point



79
80
81
82
83
# File 'lib/philiprehberger/geo_point.rb', line 79

def self.nearest(origin, points)
  return nil if points.empty?

  points.min_by { |p| origin.distance_to(p) }
end

.point(lat, lon) ⇒ Object



9
10
11
# File 'lib/philiprehberger/geo_point.rb', line 9

def self.point(lat, lon)
  Point.new(lat, lon)
end

.within_radius(origin, points, radius_km) ⇒ Array<Point>

Filter points within a given radius

Parameters:

  • origin (Point)

    the reference point

  • points (Array<Point>)

    candidate points

  • radius_km (Float)

    maximum distance in kilometers

Returns:

  • (Array<Point>)

    points within the radius



91
92
93
# File 'lib/philiprehberger/geo_point.rb', line 91

def self.within_radius(origin, points, radius_km)
  points.select { |p| origin.distance_to(p, unit: :km) <= radius_km }
end