A proof-of-concept Elixir application which performs a few Geolocation operations.
First make sure you have all dependencies:
# Fetch dependencies
$ mix deps.get
The easyest way to try it out is by running iex -S mix
:
$ iex -S mix
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.6.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> RidesElixir.run()
:ok
This will perform two actions:
- Process
pairs.csv
file and create bounding boxes for each pair or coordinates (i.e. line 1 + line 2, line 2 + line 3, line 3 + line 4 etc.). - Process
coordinates.csv
files and assign each of its coordinates to a a previously created bounding box. Unmatched coordinates are discarded.
After this processing you can interact with data using both %RidesElixir.Geo.Point{}
and %RidesElixir.Geo.Box{}
structs. For your convenience, run the following lines in iex
:
iex(2)> alias RidesElixir.Geo.Box
RidesElixir.Geo.Box
iex(3)> alias RidesElixir.Geo.Point
RidesElixir.Geo.Point
%Point{}
is the representation of a lon/lat coordinate, while %Box{}
is the representation of the bounding box of two %Point{}
s. %Box{}
also holds a list of %Point{}
s which are within its boundaries.
%Box{}
has several operations for manipulating data. You can list previously processed boxes with list/0
:
iex(4)> Box.list()
[
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.756699999999999, lon: 120.99206},
%RidesElixir.Geo.Point{lat: 14.75659, lon: 120.99287}
]
},
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.756939999999998, lon: 120.99203999999999},
%RidesElixir.Geo.Point{lat: 14.756699999999999, lon: 120.99206}
]
},
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.757139999999998, lon: 120.99203999999999},
%RidesElixir.Geo.Point{lat: 14.756939999999998, lon: 120.99207999999999}
]
},
...
You can create a new Box with new/1
, which receives a pair of %Point{}
and calculates the upper-left and bottom-right coordinates of the bounding box:
iex(5)> Box.new([%Point{lon: 120, lat: 14}, %Point{lon: 121, lat: 15}])
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 15, lon: 120},
%RidesElixir.Geo.Point{lat: 14, lon: 121}
]
}
Please note that new/1
only creates the structure of a %Box{}
and does not persist it locally. If you want to do so, use put/1
:
iex(6)> Box.new([%Point{lon: 120, lat: 14}, %Point{lon: 121, lat: 15}]) |> Box.put()
:ok
You can search for the first matching box of a given %Point{}
using find/1
, which returns a tuple with the index of the Box and the %Box{}
struct itself:
iex(7)> Box.find(%Point{lon: 120, lat: 14})
{327,
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 15, lon: 120},
%RidesElixir.Geo.Point{lat: 14, lon: 121}
]
}}
You can also use find/1
to look for a specific %Box{}
:
iex(8)> [%Point{lon: 120.99844000000004, lat: 14.65445}, %Point{lon: 120.99775000000004, lat: 14.65323}] |> Box.new() |> Box.find()
{101,
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.65445, lon: 120.99775000000004},
%RidesElixir.Geo.Point{lat: 14.65323, lon: 120.99844000000004}
]
}}
Assign a particular %Point{}
to the first matching box with find_and_assign/1
. It leverages MapSet
to make sure that no duplicates will occur -- trying to add a duplicated %Point{}
is a no-op
:
iex(10)> %Point{lon: 120.9984, lat: 14.65445} |> Box.find_and_assign()
:ok
iex(12)> %Point{lon: 120.9984, lat: 14.65445} |> Box.find()
{101,
%RidesElixir.Geo.Box{
list: #MapSet<[%RidesElixir.Geo.Point{lat: 14.65445, lon: 120.9984}]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.65445, lon: 120.99775000000004},
%RidesElixir.Geo.Point{lat: 14.65323, lon: 120.99844000000004}
]
}}
filter/1
retrieves every %Box{}
that matches a given %Point{}
:
iex(13)> %Point{lat: 14.65445, lon: 120.99775000000004} |> Box.filter()
[
%RidesElixir.Geo.Box{
list: #MapSet<[%RidesElixir.Geo.Point{lat: 14.65445, lon: 120.9984}]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.65445, lon: 120.99775000000004},
%RidesElixir.Geo.Point{lat: 14.65323, lon: 120.99844000000004}
]
},
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 15, lon: 120},
%RidesElixir.Geo.Point{lat: 14, lon: 121}
]
}
]
Finally, find_and_put/1
receives a pair of %Point{}
and retrieves a Keyword list with matching boxes for each one of them (i.e. origin and destination). It also creates the bounding box for the received pair if it does not exist already:
iex(14)> [%Point{lon: 121.0111, lat: 14.787}, %Point{lon: 120.9853, lat: 14.6097}] |> Box.find_and_put()
[
origin: [
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.787, lon: 120.9853},
%RidesElixir.Geo.Point{lat: 14.6097, lon: 121.0111}
]
}
],
destination: [
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 15, lon: 120},
%RidesElixir.Geo.Point{lat: 14, lon: 121}
]
},
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.787, lon: 120.9853},
%RidesElixir.Geo.Point{lat: 14.6097, lon: 121.0111}
]
}
]
]
Your can call find_and_put/2
with :origin
or :destination
to receive only the matching boxes of the first or second %Point{}
respectively:
iex(15)> [%Point{lon: 121.0111, lat: 14.787}, %Point{lon: 120.9853, lat: 14.6097}] |> Box.find_and_put(:origin)
[
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.787, lon: 120.9853},
%RidesElixir.Geo.Point{lat: 14.6097, lon: 121.0111}
]
}
]
iex(16)> [%Point{lon: 121.0111, lat: 14.787}, %Point{lon: 120.9853, lat: 14.6097}] |> Box.find_and_put(:destination)
[
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 15, lon: 120},
%RidesElixir.Geo.Point{lat: 14, lon: 121}
]
},
%RidesElixir.Geo.Box{
list: #MapSet<[]>,
pair: [
%RidesElixir.Geo.Point{lat: 14.787, lon: 120.9853},
%RidesElixir.Geo.Point{lat: 14.6097, lon: 121.0111}
]
}
]
Run mix test
to run the test suite:
$ mix test
..................
Finished in 0.3 seconds
18 tests, 0 failures
Randomized with seed 67962
Run mix credo
for static code analysis:
$ mix credo
Checking 8 source files ...
Please report incorrect results: https://github.com/rrrene/credo/issues
Analysis took 0.5 seconds (0.02s to load, 0.4s running checks)
25 mods/funs, found no issues.
Use `--strict` to show all issues, `--help` for options.