From 4bc33f14a148feb4056db1a2839303b07c67a6b3 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Fri, 23 Mar 2018 12:26:04 +0100 Subject: Add `TomTom::Matrix` A new component to the `TomTom` module that asks TomTom's Matrix API endpoint (https://developer.tomtom.com/online-routing/online-routing-documentation/matrix-routing) to compute `WayCost`s. The matrix API will give us all costs between each pair of coordinates. This will enable us to provide costs for any combination of points in a journey pattern. Given a list of `WayCost`s, it will send all points from those costs to the matrix API and return a list of all non-zero `WayCost`s between all pairs of coordinates. `points_from_way_costs()` extracts unique coordinates from the `WayCost`s. `points_as_params()` builds a list of points in the format expected by the matrix API. The response from the matrix API is formatted as a two-dimensional array consisting of rows and columns that pair each "origin" point with each "destination" point. We loop through this matrix and construct new `WayCost` objects for each pair of coordinates. At the moment, I haven't figured out how I want to save `WayCost` IDs when creating the new pairs. Leaving that for later. Refs #6222 --- spec/lib/tom_tom/matrix_spec.rb | 127 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 spec/lib/tom_tom/matrix_spec.rb (limited to 'spec/lib') diff --git a/spec/lib/tom_tom/matrix_spec.rb b/spec/lib/tom_tom/matrix_spec.rb new file mode 100644 index 000000000..257bbd5fd --- /dev/null +++ b/spec/lib/tom_tom/matrix_spec.rb @@ -0,0 +1,127 @@ +RSpec.describe TomTom::Matrix do + let(:matrix) { TomTom::Matrix.new(nil) } + + describe "#points_from_way_costs" do + it "extracts a set of lat/lng coordinates from a list of WayCosts" do + way_costs = [ + WayCost.new( + departure: Geokit::LatLng.new(48.85086, 2.36143), + arrival: Geokit::LatLng.new(47.91231, 1.87606) + ), + WayCost.new( + departure: Geokit::LatLng.new(47.91231, 1.87606), + arrival: Geokit::LatLng.new(52.50867, 13.42879) + ) + ] + + expect( + matrix.points_from_way_costs(way_costs) + ).to eq(Set.new([ + Geokit::LatLng.new(48.85086, 2.36143), + Geokit::LatLng.new(47.91231, 1.87606), + Geokit::LatLng.new(52.50867, 13.42879) + ])) + end + end + + describe "#points_as_params" do + it "transforms a set of LatLng points into a hash for use by TomTom Matrix" do + points = Set.new([ + Geokit::LatLng.new(48.85086, 2.36143), + Geokit::LatLng.new(47.91231, 1.87606), + Geokit::LatLng.new(52.50867, 13.42879) + ]) + + expect( + matrix.points_as_params(points) + ).to eq([ + { + point: { + latitude: 48.85086, + longitude: 2.36143 + }, + }, + { + point: { + latitude: 47.91231, + longitude: 1.87606 + }, + }, + { + point: { + latitude: 52.50867, + longitude: 13.42879 + }, + } + ]) + end + end + + describe "#extract_costs_to_way_costs!" do + it "puts distance & time costs in way_costs" do + way_costs = [ + WayCost.new( + departure: Geokit::LatLng.new(48.85086, 2.36143), + arrival: Geokit::LatLng.new(47.91231, 1.87606) + ), + WayCost.new( + departure: Geokit::LatLng.new(47.91231, 1.87606), + arrival: Geokit::LatLng.new(52.50867, 13.42879) + ) + ] + + expected_way_costs = [ + WayCost.new( + departure: Geokit::LatLng.new(48.85086, 2.36143), + arrival: Geokit::LatLng.new(47.91231, 1.87606), + distance: 117947, + time: 8356 + ), + WayCost.new( + departure: Geokit::LatLng.new(48.85086, 2.36143), + arrival: Geokit::LatLng.new(52.50867, 13.42879), + distance: 999088, + time: 62653 + ), + WayCost.new( + departure: Geokit::LatLng.new(47.91231, 1.87606), + arrival: Geokit::LatLng.new(48.85086, 2.36143), + distance: 117231, + time: 9729 + ), + WayCost.new( + departure: Geokit::LatLng.new(47.91231, 1.87606), + arrival: Geokit::LatLng.new(52.50867, 13.42879), + distance: 1114635, + time: 72079 + ), + WayCost.new( + departure: Geokit::LatLng.new(52.50867, 13.42879), + arrival: Geokit::LatLng.new(48.85086, 2.36143), + distance: 997232, + time: 63245 + ), + WayCost.new( + departure: Geokit::LatLng.new(52.50867, 13.42879), + arrival: Geokit::LatLng.new(47.91231, 1.87606), + distance: 1113108, + time: 68485 + ), + WayCost.new( + departure: Geokit::LatLng.new(52.50867, 13.42879), + arrival: Geokit::LatLng.new(52.50867, 13.42879), + distance: 344, + time: 109 + ) + ] + + matrix_response = JSON.parse(read_fixture('tom_tom_matrix.json')) + + points = matrix.points_as_params(matrix.points_from_way_costs(way_costs)) + + expect( + matrix.extract_costs_to_way_costs!(way_costs, points, matrix_response) + ).to match_array(expected_way_costs) + end + end +end -- cgit v1.2.3 From e2cbcdb4dc32db05e2c6d7a7bb57952e1da6dab3 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Fri, 23 Mar 2018 17:01:16 +0100 Subject: TomTom::Matrix#points_from_way_costs: Include stop IDs with points We need to persist stop IDs in order to properly construct `WayCost` objects from the costs returned from the TomTom matrix API. In order to persist stop IDs, my idea here is to group together a point and its corresponding ID into a bucket. When we later `#extract_costs_to_way_costs!`, we'll be able to grab the correct ID for a given coordinate to create a `WayCost` from it. Here, we create a new `TomTom::Matrix::Point` class that encapsulates a coordinate and an ID, and build a `Set` of those. I needed an `#eql?` and `#hash` method on `Point` as described in the `Set` documentation (https://ruby-doc.org/stdlib-1.9.3/libdoc/set/rdoc/Set.html) in order to properly maintain a unique set. Refs #6222 --- spec/lib/tom_tom/matrix_spec.rb | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'spec/lib') diff --git a/spec/lib/tom_tom/matrix_spec.rb b/spec/lib/tom_tom/matrix_spec.rb index 257bbd5fd..1cb47752d 100644 --- a/spec/lib/tom_tom/matrix_spec.rb +++ b/spec/lib/tom_tom/matrix_spec.rb @@ -6,20 +6,31 @@ RSpec.describe TomTom::Matrix do way_costs = [ WayCost.new( departure: Geokit::LatLng.new(48.85086, 2.36143), - arrival: Geokit::LatLng.new(47.91231, 1.87606) + arrival: Geokit::LatLng.new(47.91231, 1.87606), + id: '44-77' ), WayCost.new( departure: Geokit::LatLng.new(47.91231, 1.87606), - arrival: Geokit::LatLng.new(52.50867, 13.42879) + arrival: Geokit::LatLng.new(52.50867, 13.42879), + id: '77-88' ) ] expect( matrix.points_from_way_costs(way_costs) ).to eq(Set.new([ - Geokit::LatLng.new(48.85086, 2.36143), - Geokit::LatLng.new(47.91231, 1.87606), - Geokit::LatLng.new(52.50867, 13.42879) + TomTom::Matrix::Point.new( + Geokit::LatLng.new(48.85086, 2.36143), + '44' + ), + TomTom::Matrix::Point.new( + Geokit::LatLng.new(47.91231, 1.87606), + '77' + ), + TomTom::Matrix::Point.new( + Geokit::LatLng.new(52.50867, 13.42879), + '88' + ) ])) end end -- cgit v1.2.3 From 0a45ba811eefebb97bdc75f5e70da2be13186c28 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Fri, 23 Mar 2018 18:05:02 +0100 Subject: TomTom::Matrix#points_as_params: Use `TomTom::Matrix::Point`s Rewrite this method to accept `TomTom::Matrix::Point`s instead of plain `Geokit::LatLng` coordinates. Do this because this method needs to take the result of `#points_from_way_costs` as input, and that method now returns a `Set` of `Point`s. Refs #6222 --- spec/lib/tom_tom/matrix_spec.rb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'spec/lib') diff --git a/spec/lib/tom_tom/matrix_spec.rb b/spec/lib/tom_tom/matrix_spec.rb index 1cb47752d..b27d99a64 100644 --- a/spec/lib/tom_tom/matrix_spec.rb +++ b/spec/lib/tom_tom/matrix_spec.rb @@ -38,9 +38,18 @@ RSpec.describe TomTom::Matrix do describe "#points_as_params" do it "transforms a set of LatLng points into a hash for use by TomTom Matrix" do points = Set.new([ - Geokit::LatLng.new(48.85086, 2.36143), - Geokit::LatLng.new(47.91231, 1.87606), - Geokit::LatLng.new(52.50867, 13.42879) + TomTom::Matrix::Point.new( + Geokit::LatLng.new(48.85086, 2.36143), + '44' + ), + TomTom::Matrix::Point.new( + Geokit::LatLng.new(47.91231, 1.87606), + '77' + ), + TomTom::Matrix::Point.new( + Geokit::LatLng.new(52.50867, 13.42879), + '88' + ) ]) expect( -- cgit v1.2.3 From ddfc41f75cb52f02511e2acd6429270bc9c0ab19 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Mon, 26 Mar 2018 11:49:40 +0200 Subject: TomTom::Matrix#extract_costs_to_way_costs!: Try to include stop IDs Change this code to get stop IDs based on the change in be3e1effcdea87909a181c7e9b12cf6867b1839d. It should use these IDs to construct new `WayCost`s with the combination of stop IDs from departure and arrival stops. This doesn't work currently because the method has code that tries to index the `points` collection, but it's a `Set`, and sets don't support indexing and aren't ordered, so this code errors. Need to change the `Set` to something else that will work here. Refs #6222 --- spec/lib/tom_tom/matrix_spec.rb | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) (limited to 'spec/lib') diff --git a/spec/lib/tom_tom/matrix_spec.rb b/spec/lib/tom_tom/matrix_spec.rb index b27d99a64..e67e539b7 100644 --- a/spec/lib/tom_tom/matrix_spec.rb +++ b/spec/lib/tom_tom/matrix_spec.rb @@ -82,11 +82,13 @@ RSpec.describe TomTom::Matrix do way_costs = [ WayCost.new( departure: Geokit::LatLng.new(48.85086, 2.36143), - arrival: Geokit::LatLng.new(47.91231, 1.87606) + arrival: Geokit::LatLng.new(47.91231, 1.87606), + id: '55-99' ), WayCost.new( departure: Geokit::LatLng.new(47.91231, 1.87606), - arrival: Geokit::LatLng.new(52.50867, 13.42879) + arrival: Geokit::LatLng.new(52.50867, 13.42879), + id: '99-22' ) ] @@ -95,49 +97,56 @@ RSpec.describe TomTom::Matrix do departure: Geokit::LatLng.new(48.85086, 2.36143), arrival: Geokit::LatLng.new(47.91231, 1.87606), distance: 117947, - time: 8356 + time: 8356, + id: '55-99' ), WayCost.new( departure: Geokit::LatLng.new(48.85086, 2.36143), arrival: Geokit::LatLng.new(52.50867, 13.42879), distance: 999088, - time: 62653 + time: 62653, + id: '55-22' ), WayCost.new( departure: Geokit::LatLng.new(47.91231, 1.87606), arrival: Geokit::LatLng.new(48.85086, 2.36143), distance: 117231, - time: 9729 + time: 9729, + id: '99-55' ), WayCost.new( departure: Geokit::LatLng.new(47.91231, 1.87606), arrival: Geokit::LatLng.new(52.50867, 13.42879), distance: 1114635, - time: 72079 + time: 72079, + id: '99-22' ), WayCost.new( departure: Geokit::LatLng.new(52.50867, 13.42879), arrival: Geokit::LatLng.new(48.85086, 2.36143), distance: 997232, - time: 63245 + time: 63245, + id: '22-55' ), WayCost.new( departure: Geokit::LatLng.new(52.50867, 13.42879), arrival: Geokit::LatLng.new(47.91231, 1.87606), distance: 1113108, - time: 68485 + time: 68485, + id: '22-99' ), WayCost.new( departure: Geokit::LatLng.new(52.50867, 13.42879), arrival: Geokit::LatLng.new(52.50867, 13.42879), distance: 344, - time: 109 + time: 109, + id: '22-22' ) ] matrix_response = JSON.parse(read_fixture('tom_tom_matrix.json')) - points = matrix.points_as_params(matrix.points_from_way_costs(way_costs)) + points = matrix.points_from_way_costs(way_costs) expect( matrix.extract_costs_to_way_costs!(way_costs, points, matrix_response) -- cgit v1.2.3 From ebd46f068fbf11ea8793e9f7982b3a5291e21398 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Mon, 26 Mar 2018 15:52:43 +0200 Subject: TomTom::Matrix#points_from_way_costs: Use array instead of set Using a set ended up not working out. I needed to be able to index into the list in `#extract_costs_to_way_costs!`, and sets aren't indexable. This is because they're supposed to be unordered, though modern Ruby implements `Set` with `Hash` under the hood, which is ordered in Ruby. I like the idea of having a data structure that automatically eliminates duplicates, but it wasn't meant to be, because for the extraction to `WayCost`s, I need an ordered list. Rather than create a new `OrderedSet` type, I just went the simple route and used an Array, eliminating the duplicates manually because I know when duplicates are supposed to occur due to the nature of the data set. Remove the `#eql?` and `#hash` methods from `TomTom::Matrix::Point`. Because we're not longer using `Set`, these methods don't need to be implemented. Refs #6222 --- spec/lib/tom_tom/matrix_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'spec/lib') diff --git a/spec/lib/tom_tom/matrix_spec.rb b/spec/lib/tom_tom/matrix_spec.rb index e67e539b7..b63ebc70b 100644 --- a/spec/lib/tom_tom/matrix_spec.rb +++ b/spec/lib/tom_tom/matrix_spec.rb @@ -18,7 +18,7 @@ RSpec.describe TomTom::Matrix do expect( matrix.points_from_way_costs(way_costs) - ).to eq(Set.new([ + ).to eq([ TomTom::Matrix::Point.new( Geokit::LatLng.new(48.85086, 2.36143), '44' @@ -31,13 +31,13 @@ RSpec.describe TomTom::Matrix do Geokit::LatLng.new(52.50867, 13.42879), '88' ) - ])) + ]) end end describe "#points_as_params" do it "transforms a set of LatLng points into a hash for use by TomTom Matrix" do - points = Set.new([ + points = [ TomTom::Matrix::Point.new( Geokit::LatLng.new(48.85086, 2.36143), '44' @@ -50,7 +50,7 @@ RSpec.describe TomTom::Matrix do Geokit::LatLng.new(52.50867, 13.42879), '88' ) - ]) + ] expect( matrix.points_as_params(points) -- cgit v1.2.3 From c17665c0cc064c8a14af812dedd645977d110388 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Tue, 27 Mar 2018 15:24:27 +0200 Subject: TomTom::Matrix: Serialize `BigDecimal` as float Rails serialises `BigDecimal`s as JSON strings to prevent loss of precision. The `latitude` and `longitude` columns in `StopArea` are stored as `BigDecimal`s. The trouble is that TomTom's API requires the latitude & longitude values to be JSON floats, not strings. Make a new JSON serialiser that converts the `BigDecimal` coordinates to float to allow the values to be correctly interpreted by the API. Refs #6222 --- .../tom_tom/matrix/request_json_serializer_spec.rb | 39 ++++++++++++ spec/lib/tom_tom/matrix_spec.rb | 72 ++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 spec/lib/tom_tom/matrix/request_json_serializer_spec.rb (limited to 'spec/lib') diff --git a/spec/lib/tom_tom/matrix/request_json_serializer_spec.rb b/spec/lib/tom_tom/matrix/request_json_serializer_spec.rb new file mode 100644 index 000000000..1fafad302 --- /dev/null +++ b/spec/lib/tom_tom/matrix/request_json_serializer_spec.rb @@ -0,0 +1,39 @@ +RSpec.describe TomTom::Matrix::RequestJSONSerializer do + describe ".dump" do + it "serializes BigDecimal values to floats" do + points = [{ + point: { + latitude: 52.50867.to_d, + longitude: 13.42879.to_d + }, + }] + data = { + origins: points, + destinations: points + } + + expect( + TomTom::Matrix::RequestJSONSerializer.dump(data) + ).to eq(<<-JSON.delete(" \n")) + { + "origins": [ + { + "point": { + "latitude": 52.50867, + "longitude": 13.42879 + } + } + ], + "destinations": [ + { + "point": { + "latitude": 52.50867, + "longitude": 13.42879 + } + } + ] + } + JSON + end + end +end diff --git a/spec/lib/tom_tom/matrix_spec.rb b/spec/lib/tom_tom/matrix_spec.rb index b63ebc70b..605f1d254 100644 --- a/spec/lib/tom_tom/matrix_spec.rb +++ b/spec/lib/tom_tom/matrix_spec.rb @@ -77,6 +77,78 @@ RSpec.describe TomTom::Matrix do end end + describe "#build_request_body" do + it "serializes BigDecimal coordinates to floats" do + points = [ + { + point: { + latitude: 48.85086.to_d, + longitude: 2.36143.to_d + }, + }, + { + point: { + latitude: 47.91231.to_d, + longitude: 1.87606.to_d + }, + }, + { + point: { + latitude: 52.50867.to_d, + longitude: 13.42879.to_d + }, + } + ] + + expect( + matrix.build_request_body(points) + ).to eq(<<-JSON.delete(" \n")) + { + "origins": [ + { + "point": { + "latitude": 48.85086, + "longitude": 2.36143 + } + }, + { + "point": { + "latitude": 47.91231, + "longitude": 1.87606 + } + }, + { + "point": { + "latitude": 52.50867, + "longitude": 13.42879 + } + } + ], + "destinations": [ + { + "point": { + "latitude": 48.85086, + "longitude": 2.36143 + } + }, + { + "point": { + "latitude": 47.91231, + "longitude": 1.87606 + } + }, + { + "point": { + "latitude": 52.50867, + "longitude": 13.42879 + } + } + ] + } + JSON + end + end + describe "#extract_costs_to_way_costs!" do it "puts distance & time costs in way_costs" do way_costs = [ -- cgit v1.2.3