diff options
| author | Alban Peignier | 2018-04-04 10:03:40 +0200 |
|---|---|---|
| committer | GitHub | 2018-04-04 10:03:40 +0200 |
| commit | ecf0dd6406576f8a63ee056fab73b05171c3e767 (patch) | |
| tree | d48bf1be4052ca446bd6c715698d857a9102d914 | |
| parent | 6262b1cf8c536fb59997119a28420584ab08994c (diff) | |
| parent | 2cb5225efbb080707c060e4038ae6d48612515f0 (diff) | |
| download | chouette-core-ecf0dd6406576f8a63ee056fab73b05171c3e767.tar.bz2 | |
Merge pull request #413 from af83/6222-route-way-costs--use-TomTom-matrix-API-instead-of-batch
Route way costs use tom tom matrix api instead of batch. Refs #6222
| -rw-r--r-- | app/services/route_way_cost_calculator.rb | 2 | ||||
| -rw-r--r-- | lib/tom_tom.rb | 4 | ||||
| -rw-r--r-- | lib/tom_tom/matrix.rb | 114 | ||||
| -rw-r--r-- | lib/tom_tom/matrix/point.rb | 18 | ||||
| -rw-r--r-- | lib/tom_tom/matrix/request_json_serializer.rb | 25 | ||||
| -rw-r--r-- | spec/fixtures/tom_tom_matrix.json | 123 | ||||
| -rw-r--r-- | spec/lib/tom_tom/matrix/request_json_serializer_spec.rb | 39 | ||||
| -rw-r--r-- | spec/lib/tom_tom/matrix_spec.rb | 228 | ||||
| -rw-r--r-- | spec/services/route_way_cost_calculator_spec.rb | 10 |
9 files changed, 558 insertions, 5 deletions
diff --git a/app/services/route_way_cost_calculator.rb b/app/services/route_way_cost_calculator.rb index 2e30c94fc..d41a2e59a 100644 --- a/app/services/route_way_cost_calculator.rb +++ b/app/services/route_way_cost_calculator.rb @@ -5,7 +5,7 @@ class RouteWayCostCalculator def calculate! way_costs = StopAreasToWayCostsConverter.new(@route.stop_areas).convert - way_costs = TomTom.batch(way_costs) + way_costs = TomTom.matrix(way_costs) way_costs = WayCostCollectionJSONSerializer.dump(way_costs) @route.update(costs: way_costs) end diff --git a/lib/tom_tom.rb b/lib/tom_tom.rb index a1a2bda43..fcebcc7ac 100644 --- a/lib/tom_tom.rb +++ b/lib/tom_tom.rb @@ -19,4 +19,8 @@ module TomTom def self.batch(way_costs) TomTom::Batch.new(@connection).batch(way_costs) end + + def self.matrix(way_costs) + TomTom::Matrix.new(@connection).matrix(way_costs) + end end diff --git a/lib/tom_tom/matrix.rb b/lib/tom_tom/matrix.rb new file mode 100644 index 000000000..b0c8cc335 --- /dev/null +++ b/lib/tom_tom/matrix.rb @@ -0,0 +1,114 @@ +module TomTom + class Matrix + def initialize(connection) + @connection = connection + end + + def matrix(way_costs) + points_with_ids = points_from_way_costs(way_costs) + points = points_as_params(points_with_ids) + + Rails.logger.info "Invoke TomTom for #{points.size} points" + + response = @connection.post do |req| + req.url '/routing/1/matrix/json' + req.headers['Content-Type'] = 'application/json' + + req.params[:routeType] = 'shortest' + req.params[:travelMode] = 'bus' + + req.body = build_request_body(points) + end + + extract_costs_to_way_costs!( + way_costs, + points_with_ids, + JSON.parse(response.body) + ) + end + + def points_from_way_costs(way_costs) + points = [] + + way_costs.each do |way_cost| + departure_id, arrival_id = way_cost.id.split('-') + + departure = TomTom::Matrix::Point.new( + way_cost.departure, + departure_id + ) + arrival = TomTom::Matrix::Point.new( + way_cost.arrival, + arrival_id + ) + + # Don't add duplicate coordinates. This assumes that + # `way_costs` consists of an ordered route of points where + # each departure coordinate is the same as the preceding + # arrival coordinate. + if points.empty? || + points.last.coordinates != departure.coordinates + points << departure + end + + points << arrival + end + + points + end + + def points_as_params(points) + points.map do |point| + { + point: { + latitude: point.coordinates.lat, + longitude: point.coordinates.lng + } + } + end + end + + def build_request_body(points) + # Serialize `BigDecimal` values as floats to please the TomTom API + RequestJSONSerializer.dump({ + origins: points, + destinations: points + }) + end + + def extract_costs_to_way_costs!(way_costs, points, matrix_json) + way_costs = [] + + # `row` and `column` order is the same as `points` + matrix_json['matrix'].each_with_index do |row, row_i| + row.each_with_index do |column, column_i| + next if column['statusCode'] != 200 + + distance = column['response']['routeSummary']['lengthInMeters'] + + # Ignore costs between a point and itself (e.g. from A to A) + next if distance == 0 + + departure = points[row_i] + arrival = points[column_i] + + way_costs << WayCost.new( + departure: Geokit::LatLng.new( + departure.coordinates.lat, + departure.coordinates.lng + ), + arrival: Geokit::LatLng.new( + arrival.coordinates.lat, + arrival.coordinates.lng + ), + distance: distance, + time: column['response']['routeSummary']['travelTimeInSeconds'], + id: "#{departure.id}-#{arrival.id}" + ) + end + end + + way_costs + end + end +end diff --git a/lib/tom_tom/matrix/point.rb b/lib/tom_tom/matrix/point.rb new file mode 100644 index 000000000..435b4d4b0 --- /dev/null +++ b/lib/tom_tom/matrix/point.rb @@ -0,0 +1,18 @@ +module TomTom + class Matrix + class Point + attr_reader :coordinates, :id + + def initialize(coordinates, id) + @coordinates = coordinates + @id = id + end + + def ==(other) + other.is_a?(self.class) && + @coordinates == other.coordinates && + @id == other.id + end + end + end +end diff --git a/lib/tom_tom/matrix/request_json_serializer.rb b/lib/tom_tom/matrix/request_json_serializer.rb new file mode 100644 index 000000000..f4d12e482 --- /dev/null +++ b/lib/tom_tom/matrix/request_json_serializer.rb @@ -0,0 +1,25 @@ +module TomTom + class Matrix + class RequestJSONSerializer + def self.dump(hash) + hash[:origins].map! do |point| + point_to_f(point) + end + hash[:destinations].map! do |point| + point_to_f(point) + end + + JSON.dump(hash) + end + + private + + def self.point_to_f(point) + point[:point][:latitude] = point[:point][:latitude].to_f + point[:point][:longitude] = point[:point][:longitude].to_f + + point + end + end + end +end diff --git a/spec/fixtures/tom_tom_matrix.json b/spec/fixtures/tom_tom_matrix.json new file mode 100644 index 000000000..30048576c --- /dev/null +++ b/spec/fixtures/tom_tom_matrix.json @@ -0,0 +1,123 @@ +{ + "formatVersion": "0.0.1", + "matrix": [ + [ + { + "statusCode": 200, + "response": { + "routeSummary": { + "lengthInMeters": 0, + "travelTimeInSeconds": 0, + "trafficDelayInSeconds": 0, + "departureTime": "2018-03-23T11:20:17+01:00", + "arrivalTime": "2018-03-23T11:20:17+01:00" + } + } + }, + { + "statusCode": 200, + "response": { + "routeSummary": { + "lengthInMeters": 117947, + "travelTimeInSeconds": 8356, + "trafficDelayInSeconds": 0, + "departureTime": "2018-03-23T11:20:17+01:00", + "arrivalTime": "2018-03-23T13:39:32+01:00" + } + } + }, + { + "statusCode": 200, + "response": { + "routeSummary": { + "lengthInMeters": 999088, + "travelTimeInSeconds": 62653, + "trafficDelayInSeconds": 298, + "departureTime": "2018-03-23T11:20:17+01:00", + "arrivalTime": "2018-03-24T04:44:30+01:00" + } + } + } + ], + [ + { + "statusCode": 200, + "response": { + "routeSummary": { + "lengthInMeters": 117231, + "travelTimeInSeconds": 9729, + "trafficDelayInSeconds": 0, + "departureTime": "2018-03-23T11:20:17+01:00", + "arrivalTime": "2018-03-23T14:02:25+01:00" + } + } + }, + { + "statusCode": 200, + "response": { + "routeSummary": { + "lengthInMeters": 0, + "travelTimeInSeconds": 0, + "trafficDelayInSeconds": 0, + "departureTime": "2018-03-23T11:20:17+01:00", + "arrivalTime": "2018-03-23T11:20:17+01:00" + } + } + }, + { + "statusCode": 200, + "response": { + "routeSummary": { + "lengthInMeters": 1114635, + "travelTimeInSeconds": 72079, + "trafficDelayInSeconds": 298, + "departureTime": "2018-03-23T11:20:18+01:00", + "arrivalTime": "2018-03-24T07:21:36+01:00" + } + } + } + ], + [ + { + "statusCode": 200, + "response": { + "routeSummary": { + "lengthInMeters": 997232, + "travelTimeInSeconds": 63245, + "trafficDelayInSeconds": 179, + "departureTime": "2018-03-23T11:20:18+01:00", + "arrivalTime": "2018-03-24T04:54:23+01:00" + } + } + }, + { + "statusCode": 200, + "response": { + "routeSummary": { + "lengthInMeters": 1113108, + "travelTimeInSeconds": 68485, + "trafficDelayInSeconds": 52, + "departureTime": "2018-03-23T11:20:18+01:00", + "arrivalTime": "2018-03-24T06:21:43+01:00" + } + } + }, + { + "statusCode": 200, + "response": { + "routeSummary": { + "lengthInMeters": 344, + "travelTimeInSeconds": 109, + "trafficDelayInSeconds": 0, + "departureTime": "2018-03-23T11:20:18+01:00", + "arrivalTime": "2018-03-23T11:22:07+01:00" + } + } + } + ] + ], + "summary": { + "successfulRoutes": 9, + "totalRoutes": 9 + } +} 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 new file mode 100644 index 000000000..605f1d254 --- /dev/null +++ b/spec/lib/tom_tom/matrix_spec.rb @@ -0,0 +1,228 @@ +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), + id: '44-77' + ), + WayCost.new( + departure: Geokit::LatLng.new(47.91231, 1.87606), + arrival: Geokit::LatLng.new(52.50867, 13.42879), + id: '77-88' + ) + ] + + expect( + matrix.points_from_way_costs(way_costs) + ).to eq([ + 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 + + describe "#points_as_params" do + it "transforms a set of LatLng points into a hash for use by TomTom Matrix" do + points = [ + 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( + 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 "#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 = [ + WayCost.new( + departure: Geokit::LatLng.new(48.85086, 2.36143), + 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), + id: '99-22' + ) + ] + + 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, + 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, + 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, + 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, + 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, + 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, + 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, + id: '22-22' + ) + ] + + matrix_response = JSON.parse(read_fixture('tom_tom_matrix.json')) + + points = 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 diff --git a/spec/services/route_way_cost_calculator_spec.rb b/spec/services/route_way_cost_calculator_spec.rb index d5358fcf6..79b81e34d 100644 --- a/spec/services/route_way_cost_calculator_spec.rb +++ b/spec/services/route_way_cost_calculator_spec.rb @@ -7,18 +7,20 @@ RSpec.describe RouteWayCostCalculator do # things in the request or response. This is just to fake the request so # we don't actually call their API in tests. The test doesn't test # anything given in the response. - stub_request(:post, "https://api.tomtom.com/routing/1/batch/json?key") + stub_request( + :post, + "https://api.tomtom.com/routing/1/matrix/json?key&routeType=shortest&travelMode=bus" + ) .with( headers: { 'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type'=>'application/json', 'User-Agent'=>'Faraday v0.9.2' - } - ) + }) .to_return( status: 200, - body: "{\"formatVersion\":\"0.0.1\",\"batchItems\":[{\"statusCode\":200,\"response\":{\"routes\":[{\"summary\":{\"lengthInMeters\":117947,\"travelTimeInSeconds\":7969,\"trafficDelayInSeconds\":0,\"departureTime\":\"2018-03-12T12:32:26+01:00\",\"arrivalTime\":\"2018-03-12T14:45:14+01:00\"}}]}}]}", + body: "{\"formatVersion\":\"0.0.1\",\"matrix\":[[{\"statusCode\":200,\"response\":{\"routeSummary\":{\"lengthInMeters\":0,\"travelTimeInSeconds\":0,\"trafficDelayInSeconds\":0,\"departureTime\":\"2018-03-23T11:20:17+01:00\",\"arrivalTime\":\"2018-03-23T11:20:17+01:00\"}}}]]}", headers: {} ) |
