locatable 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +108 -0
- data/Rakefile +10 -0
- data/lib/locatable/helpers.rb +33 -0
- data/lib/locatable/migration_helpers.rb +27 -0
- data/lib/locatable/model.rb +10 -0
- data/lib/locatable/railtie.rb +19 -0
- data/lib/locatable/scopes.rb +80 -0
- data/lib/locatable/version.rb +5 -0
- data/lib/locatable.rb +35 -0
- data/sig/locatable.rbs +4 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b00bd3e85ad8ba1435a8368b0ba2c3ca71d15d1e2e288e4a9d36068b51ef6e91
|
|
4
|
+
data.tar.gz: 45bb56e4b676f1cc7411e6a42d2788a205ae092271924201320f60649b2497fd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b3318c653b4e4f07e842e9352c5a01264d0bf38ed27f4542d719ddf58d573c4ab6e1a5cae4e5367abe12209095ba3af83381c76c36527e08cc8d79b08c4390e8
|
|
7
|
+
data.tar.gz: dbf11ce695414d9daed6cf5791dff0e7a2f86b2c9f787bdd79f63009781734af049dded028a9563bbca76c4b4fe8338b0324f78675003d4dc5f2f9cb3ec12af9
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Locatable
|
|
2
|
+
|
|
3
|
+
Simple, fast, Geocoder-compatible PostGIS-backed location scopes for Active Record models.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby 3.2+
|
|
8
|
+
- Active Record
|
|
9
|
+
- PostgreSQL with PostGIS
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add the gem to your Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "locatable"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then run:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
Add latitude and longitude columns, then call `make_locatable` in the migration.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
class CreatePlaces < ActiveRecord::Migration[8.1]
|
|
31
|
+
def change
|
|
32
|
+
create_table :places do |t|
|
|
33
|
+
t.float :latitude
|
|
34
|
+
t.float :longitude
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
make_locatable :places, latitude: :latitude, longitude: :longitude
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`make_locatable` adds generated PostGIS geography and geometry columns, plus GiST indexes.
|
|
44
|
+
|
|
45
|
+
In the model, call `locatable`:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
class Place < ApplicationRecord
|
|
49
|
+
locatable
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
Coordinates are passed as `[latitude, longitude]`.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
origin = [40.7128, -74.0060]
|
|
59
|
+
|
|
60
|
+
Place.within_bounding_box([[40.70, -74.02], [40.73, -73.99]]) # Geocoder-compatible
|
|
61
|
+
Place.near(origin, 5) # Geocoder-compatible
|
|
62
|
+
Place.within_radius(origin, 5)
|
|
63
|
+
Place.order_by_closest_to(origin)
|
|
64
|
+
Place.select_distance_to(origin)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
These 2 calls are equivalent:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
Place.near(origin, 2)
|
|
71
|
+
Place.within_radius(origin, 2).order_by_closest_to(origin)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Distances use miles by default. Supported units are `:km`, `:mi`, and `:nm`.
|
|
75
|
+
|
|
76
|
+
Set the units directly on a scope call:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
Place.near(origin, 10, units: :km)
|
|
80
|
+
Place.within_radius(origin, 5, units: :nm)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or set the default once in an initializer:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# config/initializers/locatable.rb
|
|
87
|
+
Locatable.default_units = :km
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
Install dependencies:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
bin/setup
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Run tests with a PostGIS database URL:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
LOCATABLE_DATABASE_URL=postgis://user:password@localhost/locatable_test bundle exec rake test
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Run formatting:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
bundle exec rake standard
|
|
108
|
+
```
|
data/Rakefile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Locatable::Helpers
|
|
2
|
+
module_function
|
|
3
|
+
|
|
4
|
+
def extract_lat_lng(origin)
|
|
5
|
+
lat, lng = case origin
|
|
6
|
+
when Array
|
|
7
|
+
[origin[0], origin[1]]
|
|
8
|
+
when Hash
|
|
9
|
+
[origin[:lat] || origin["lat"], origin[:lng] || origin["lng"]]
|
|
10
|
+
else
|
|
11
|
+
[origin.try(:lat), origin.try(:lng)]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
[parse_float(lat), parse_float(lng)]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse_float(value)
|
|
18
|
+
Float(value)
|
|
19
|
+
rescue ArgumentError, TypeError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def meters_per_units(units)
|
|
24
|
+
Locatable::METERS_PER_UNIT.fetch(Locatable.normalize_units(units))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def convert_to_meters(distance, units)
|
|
28
|
+
distance = parse_float(distance)
|
|
29
|
+
return nil if distance.nil?
|
|
30
|
+
|
|
31
|
+
distance * meters_per_units(units)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Locatable::MigrationHelpers
|
|
2
|
+
def make_locatable(table, latitude:, longitude:)
|
|
3
|
+
reversible do |dir|
|
|
4
|
+
dir.up do
|
|
5
|
+
execute <<~SQL
|
|
6
|
+
ALTER TABLE #{table}
|
|
7
|
+
ADD COLUMN location_geography geography(Point, 4326)
|
|
8
|
+
GENERATED ALWAYS AS (
|
|
9
|
+
ST_Point(#{longitude}, #{latitude}, 4326)::geography
|
|
10
|
+
) STORED,
|
|
11
|
+
ADD COLUMN location_geometry geometry(Point, 4326)
|
|
12
|
+
GENERATED ALWAYS AS (
|
|
13
|
+
ST_Point(#{longitude}, #{latitude}, 4326)
|
|
14
|
+
) STORED;
|
|
15
|
+
SQL
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
dir.down do
|
|
19
|
+
remove_column table, :location_geometry
|
|
20
|
+
remove_column table, :location_geography
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
add_index table, :location_geography, using: :gist
|
|
25
|
+
add_index table, :location_geometry, using: :gist
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "rails/railtie"
|
|
2
|
+
require "locatable/migration_helpers"
|
|
3
|
+
require "locatable/model"
|
|
4
|
+
|
|
5
|
+
module Locatable
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer "locatable.migration_helpers" do
|
|
8
|
+
ActiveSupport.on_load(:active_record) do
|
|
9
|
+
ActiveRecord::Migration.include Locatable::MigrationHelpers
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "locatable.active_record" do
|
|
14
|
+
ActiveSupport.on_load(:active_record) do
|
|
15
|
+
include Locatable::Model
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require "active_support/concern"
|
|
2
|
+
require "active_record"
|
|
3
|
+
require "locatable/helpers"
|
|
4
|
+
|
|
5
|
+
module Locatable::Scopes
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
SRID = 4326
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
scope :within_bounding_box, ->(sw_ne_corners) do
|
|
12
|
+
next none if sw_ne_corners.nil?
|
|
13
|
+
|
|
14
|
+
sw_corner = sw_ne_corners.flatten[0..1]
|
|
15
|
+
ne_corner = sw_ne_corners.flatten[2..3]
|
|
16
|
+
|
|
17
|
+
sw_lat, sw_lng = Locatable::Helpers.extract_lat_lng(sw_corner)
|
|
18
|
+
ne_lat, ne_lng = Locatable::Helpers.extract_lat_lng(ne_corner)
|
|
19
|
+
|
|
20
|
+
next none if [sw_lat, sw_lng, ne_lat, ne_lng].any?(&:nil?)
|
|
21
|
+
|
|
22
|
+
location_column = :location_geometry
|
|
23
|
+
|
|
24
|
+
within_bbox_sql = if ne_lng > sw_lng
|
|
25
|
+
<<~SQL.squish
|
|
26
|
+
#{location_column} && ST_MakeEnvelope(#{sw_lng}, #{sw_lat}, #{ne_lng}, #{ne_lat}, #{SRID})
|
|
27
|
+
SQL
|
|
28
|
+
else
|
|
29
|
+
<<~SQL.squish
|
|
30
|
+
#{location_column} && ST_MakeEnvelope(#{sw_lng}, #{sw_lat}, 180, #{ne_lat}, #{SRID}) OR
|
|
31
|
+
#{location_column} && ST_MakeEnvelope(-180, #{sw_lat}, #{ne_lng}, #{ne_lat}, #{SRID})
|
|
32
|
+
SQL
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
where(Arel.sql(within_bbox_sql))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
scope :order_by_closest_to, ->(origin) do
|
|
39
|
+
lat, lng = Locatable::Helpers.extract_lat_lng(origin)
|
|
40
|
+
|
|
41
|
+
next all if lat.nil? || lng.nil?
|
|
42
|
+
|
|
43
|
+
order(Arel.sql("location_geography <-> ST_Point(#{lng}, #{lat}, #{SRID})::geography"))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
scope :select_distance_to, ->(origin, units: nil) do
|
|
47
|
+
units ||= Locatable.default_units
|
|
48
|
+
lat, lng = Locatable::Helpers.extract_lat_lng(origin)
|
|
49
|
+
meters_per_units = Locatable::Helpers.meters_per_units(units)
|
|
50
|
+
|
|
51
|
+
distance_sql = if lat.nil? || lng.nil?
|
|
52
|
+
"NULL"
|
|
53
|
+
else
|
|
54
|
+
"ST_Distance(location_geography, ST_Point(#{lng}, #{lat}, #{SRID})::geography) / #{meters_per_units}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
select("#{distance_sql} AS distance")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
scope :within_radius, ->(origin, radius, units: nil) do
|
|
61
|
+
units ||= Locatable.default_units
|
|
62
|
+
lat, lng = Locatable::Helpers.extract_lat_lng(origin)
|
|
63
|
+
radius = Locatable::Helpers.convert_to_meters(radius, units)
|
|
64
|
+
|
|
65
|
+
next none if lat.nil? || lng.nil?
|
|
66
|
+
next all if radius.nil?
|
|
67
|
+
|
|
68
|
+
where(Arel.sql("ST_DWithin(location_geography, ST_Point(#{lng}, #{lat}, #{SRID})::geography, #{radius})"))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
scope :near, ->(origin, radius = 20, units: nil, order_by_closest: true, select_distance: false) do
|
|
72
|
+
units ||= Locatable.default_units
|
|
73
|
+
scope = all
|
|
74
|
+
scope = scope.within_radius(origin, radius, units: units) if radius.present?
|
|
75
|
+
scope = scope.order_by_closest_to(origin) if order_by_closest
|
|
76
|
+
scope = scope.select_distance_to(origin, units: units) if select_distance
|
|
77
|
+
scope
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/locatable.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require_relative "locatable/version"
|
|
2
|
+
require_relative "locatable/migration_helpers"
|
|
3
|
+
require_relative "locatable/scopes"
|
|
4
|
+
|
|
5
|
+
require "activerecord-postgis-adapter"
|
|
6
|
+
|
|
7
|
+
require "locatable/railtie" if defined?(Rails::Railtie)
|
|
8
|
+
|
|
9
|
+
module Locatable
|
|
10
|
+
VALID_UNITS = %i[km mi nm].freeze
|
|
11
|
+
METERS_PER_UNIT = {
|
|
12
|
+
km: 1_000.0,
|
|
13
|
+
mi: 1_609.344,
|
|
14
|
+
nm: 1_852.0
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def default_units
|
|
19
|
+
@default_units ||= :mi
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def default_units=(units)
|
|
23
|
+
@default_units = normalize_units(units)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def normalize_units(units)
|
|
27
|
+
units = units.to_sym
|
|
28
|
+
return units if VALID_UNITS.include?(units)
|
|
29
|
+
|
|
30
|
+
raise ArgumentError, "units must be one of: #{VALID_UNITS.join(", ")}"
|
|
31
|
+
rescue NoMethodError
|
|
32
|
+
raise ArgumentError, "units must be one of: #{VALID_UNITS.join(", ")}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/sig/locatable.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: locatable
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- simon
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord-postgis-adapter
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: pg
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: railties
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: Location scopes for Active Record models backed by PostGIS.
|
|
55
|
+
email:
|
|
56
|
+
- simonrmurcia@gmail.com
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- CHANGELOG.md
|
|
62
|
+
- README.md
|
|
63
|
+
- Rakefile
|
|
64
|
+
- lib/locatable.rb
|
|
65
|
+
- lib/locatable/helpers.rb
|
|
66
|
+
- lib/locatable/migration_helpers.rb
|
|
67
|
+
- lib/locatable/model.rb
|
|
68
|
+
- lib/locatable/railtie.rb
|
|
69
|
+
- lib/locatable/scopes.rb
|
|
70
|
+
- lib/locatable/version.rb
|
|
71
|
+
- sig/locatable.rbs
|
|
72
|
+
homepage: https://github.com/simon/locatable
|
|
73
|
+
licenses:
|
|
74
|
+
- MIT
|
|
75
|
+
metadata:
|
|
76
|
+
allowed_push_host: https://rubygems.org
|
|
77
|
+
homepage_uri: https://github.com/simon/locatable
|
|
78
|
+
source_code_uri: https://github.com/simon/locatable
|
|
79
|
+
changelog_uri: https://github.com/simon/locatable/blob/main/CHANGELOG.md
|
|
80
|
+
rdoc_options: []
|
|
81
|
+
require_paths:
|
|
82
|
+
- lib
|
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: 3.2.0
|
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '0'
|
|
93
|
+
requirements: []
|
|
94
|
+
rubygems_version: 4.0.6
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: Location scopes for Active Record models.
|
|
97
|
+
test_files: []
|