Repository: graphhopper/graphhopper
Pull request #2162 (2020-10-24 09:44:39)

CustomWeighting: ensure Map is ordered
There is a bug which could occur for Java or the API when using multiple encoded values and/or multiple cases for one encoded value. The next goal should be to make CustomModel.getPriority/getSpeedFactor/getMaxSpeed type safe which we might reach when we limit the language to only "clause" as key and number as value (which is a next but much bigger step).


Patch:
diff --git a/core/files/changelog.txt b/core/files/changelog.txt
index f3aee46f8..b80fb06b3 100644
--- a/core/files/changelog.txt
+++ b/core/files/changelog.txt
@@ -1,4 +1,5 @@
 3.0
+    CustomWeighting requires LinkedHashMap throw exception if this is not provided
     removed Dockerfile
     the second argument of the VirtualEdgeIteratorState constructor is now an edge key (was an edge id before)
 
diff --git a/core/src/main/java/com/graphhopper/routing/util/CustomModel.java b/core/src/main/java/com/graphhopper/routing/util/CustomModel.java
index 430324f0c..5ca5790f6 100644
--- a/core/src/main/java/com/graphhopper/routing/util/CustomModel.java
+++ b/core/src/main/java/com/graphhopper/routing/util/CustomModel.java
@@ -20,10 +20,7 @@ package com.graphhopper.routing.util;
 import com.graphhopper.json.geo.JsonFeature;
 import com.graphhopper.util.Parameters;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**
  * This class is used in combination with CustomProfile.
@@ -38,9 +35,9 @@ public class CustomModel {
     private Double headingPenalty = Parameters.Routing.DEFAULT_HEADING_PENALTY;
     // default value derived from the cost for time e.g. 25€/hour and for distance 0.5€/km, for trucks this is usually larger
     private double distanceInfluence = DEFAULT_D_I;
-    private Map<String, Object> speedFactorMap = new HashMap<>();
-    private Map<String, Object> maxSpeedMap = new HashMap<>();
-    private Map<String, Object> priorityMap = new HashMap<>();
+    private Map<String, Object> speedFactorMap = new LinkedHashMap<>();
+    private Map<String, Object> maxSpeedMap = new LinkedHashMap<>();
+    private Map<String, Object> priorityMap = new LinkedHashMap<>();
     private Map<String, JsonFeature> areas = new HashMap<>();
 
     public CustomModel() {
@@ -66,7 +63,8 @@ public class CustomModel {
             }
             return (T) newList;
         } else if (originalObject instanceof Map) {
-            Map copy = new HashMap<>(((Map) originalObject).size());
+            Map copy = originalObject instanceof LinkedHashMap ? new LinkedHashMap<>(((Map) originalObject).size()) :
+                    new HashMap<>(((Map) originalObject).size());
             for (Object o : ((Map) originalObject).entrySet()) {
                 Map.Entry entry = (Map.Entry) o;
                 copy.put(entry.getKey(), deepCopy(entry.getValue()));
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/PriorityCalculator.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/PriorityCalculator.java
index 3d17b2632..e64da469f 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/PriorityCalculator.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/PriorityCalculator.java
@@ -24,6 +24,7 @@ import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -45,8 +46,8 @@ final class PriorityCalculator {
                 priorityList.add(GeoToValueEntry.create(priorityKey, new PreparedGeometryFactory().create(geometry),
                         (Number) value, 1, 0, 1));
             } else {
-                if (!(value instanceof Map))
-                    throw new IllegalArgumentException(priorityKey + ": non-root entries require a map but was: " + value.getClass().getSimpleName());
+                if (!(value instanceof LinkedHashMap))
+                    throw new IllegalArgumentException(priorityKey + ": non-root entries require a sorted map (LinkedHashMap) but was: " + value.getClass().getSimpleName());
                 final double defaultPriority = 1, minPriority = 0, maxPriority = 1;
                 EncodedValue encodedValue = getEV(lookup, "priority", key);
                 if (encodedValue instanceof EnumEncodedValue) {
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/SpeedCalculator.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/SpeedCalculator.java
index 0a9b5e839..03ddffbd1 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/SpeedCalculator.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/SpeedCalculator.java
@@ -24,6 +24,7 @@ import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -60,8 +61,8 @@ final class SpeedCalculator {
                 maxSpeedList.add(GeoToValueEntry.create(maxSpeedKey, new PreparedGeometryFactory().create(geometry),
                         (Number) value, maxSpeed, 0, maxSpeed));
             } else {
-                if (!(value instanceof Map))
-                    throw new IllegalArgumentException(maxSpeedKey + ": non-root entries require a map but was: " + value.getClass().getSimpleName());
+                if (!(value instanceof LinkedHashMap))
+                    throw new IllegalArgumentException(maxSpeedKey + ": non-root entries require a sorted map (LinkedHashMap), but was: " + value.getClass().getSimpleName());
                 final double defaultMaxSpeed = maxSpeed, minMaxSpeed = 0, maxMaxSpeed = maxSpeed;
                 EncodedValue encodedValue = getEV(lookup, "max_speed", key);
                 if (encodedValue instanceof EnumEncodedValue) {
@@ -98,8 +99,8 @@ final class SpeedCalculator {
                 speedFactorList.add(GeoToValueEntry.create(speedFactorKey, new PreparedGeometryFactory().create(geometry),
                         (Number) value, 1, 0, 1));
             } else {
-                if (!(value instanceof Map))
-                    throw new IllegalArgumentException(speedFactorKey + ": non-root entries require a map but was: " + value.getClass().getSimpleName());
+                if (!(value instanceof LinkedHashMap))
+                    throw new IllegalArgumentException(speedFactorKey + ": non-root entries require a sorted map (LinkedHashMap), but was: " + value.getClass().getSimpleName());
                 final double defaultSpeedFactor = 1, minSpeedFactor = 0, maxSpeedFactor = 1;
                 EncodedValue encodedValue = getEV(lookup, "speed_factor", key);
                 if (encodedValue instanceof EnumEncodedValue) {
diff --git a/example/src/main/java/com/graphhopper/example/RoutingExample.java b/example/src/main/java/com/graphhopper/example/RoutingExample.java
index 9f971d15c..4a1ba4437 100644
--- a/example/src/main/java/com/graphhopper/example/RoutingExample.java
+++ b/example/src/main/java/com/graphhopper/example/RoutingExample.java
@@ -15,10 +15,7 @@ import com.graphhopper.routing.weighting.custom.CustomProfile;
 import com.graphhopper.util.*;
 import com.graphhopper.util.shapes.GHPoint;
 
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
+import java.util.*;
 
 public class RoutingExample {
     public static void main(String[] args) {
@@ -59,12 +56,9 @@ public class RoutingExample {
                         setLocale(Locale.US);
         GHResponse rsp = hopper.route(req);
 
-        // first check for errors
-        if (rsp.hasErrors()) {
-            // handle them!
-            // rsp.getErrors()
-            return;
-        }
+        // handle errors
+        if (rsp.hasErrors())
+            throw new RuntimeException(rsp.getErrors().toString());
 
         // use the best path, see the GHResponse class for more possibilities.
         ResponsePath path = rsp.getBest();
@@ -88,6 +82,8 @@ public class RoutingExample {
         GHRequest req = new GHRequest(42.508552, 1.532936, 42.507508, 1.528773).
                 setProfile("car").setAlgorithm(Parameters.Algorithms.ASTAR_BI).putHint(Parameters.CH.DISABLE, true);
         GHResponse res = hopper.route(req);
+        if (res.hasErrors())
+            throw new RuntimeException(res.getErrors().toString());
         assert Helper.round(res.getBest().getDistance(), -2) == 900;
     }
 
@@ -101,6 +97,8 @@ public class RoutingExample {
         // if you have via points you can avoid U-turns there with
         // req.getHints().putObject(Parameters.Routing.PASS_THROUGH, true);
         GHResponse res = hopper.route(req);
+        if (res.hasErrors())
+            throw new RuntimeException(res.getErrors().toString());
         assert res.getAll().size() == 1;
         assert Helper.round(res.getBest().getDistance(), -2) == 800;
 
@@ -110,6 +108,8 @@ public class RoutingExample {
                 setAlgorithm(Parameters.Algorithms.ALT_ROUTE);
         req.getHints().putObject(Parameters.Algorithms.AltRoute.MAX_PATHS, 3);
         res = hopper.route(req);
+        if (res.hasErrors())
+            throw new RuntimeException(res.getErrors().toString());
         assert res.getAll().size() == 2;
         assert Helper.round(res.getBest().getDistance(), -2) == 1600;
     }
@@ -129,21 +129,27 @@ public class RoutingExample {
         // ... but for the hybrid mode we can customize the route calculation even at request time:
         // 1. a request with default preferences
         GHRequest req = new GHRequest().setProfile("car_custom").
-                addPoint(new GHPoint(42.506472,1.522475)).addPoint(new GHPoint(42.513108,1.536005));
+                addPoint(new GHPoint(42.506472, 1.522475)).addPoint(new GHPoint(42.513108, 1.536005));
 
         GHResponse res = hopper.route(req);
+        if (res.hasErrors())
+            throw new RuntimeException(res.getErrors().toString());
+
         assert Math.round(res.getBest().getTime() / 1000d) == 96;
 
         // 2. now avoid primary roads and reduce maximum speed, see docs/core/profiles.md for an in-depth explanation
         // and also the blog posts https://www.graphhopper.com/?s=customizable+routing
         CustomModel model = new CustomModel();
         req.putHint(CustomModel.KEY, model);
-        Map<String, Double> map = new HashMap<>();
+        Map<String, Double> map = new LinkedHashMap<>();
         model.setMaxSpeedFallback(100d);
         model.getPriority().put(RoadClass.KEY, map);
         map.put(RoadClass.PRIMARY.toString(), 0.5);
 
         res = hopper.route(req);
+        if (res.hasErrors())
+            throw new RuntimeException(res.getErrors().toString());
+
         assert Math.round(res.getBest().getTime() / 1000d) == 165;
     }
 }
diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/fare/Fares.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/fare/Fares.java
index 113a2f887..e030f534a 100644
--- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/fare/Fares.java
+++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/fare/Fares.java
@@ -44,7 +44,7 @@ public class Fares {
                         .map(e -> new Amount(e.getValue(), e.getKey())));
     }
 
-    private static Optional<List<Ticket>> ticketsBruteForce(Map<String, Map<String, Fare>> fares, Trip trip) {
+    static Optional<List<Ticket>> ticketsBruteForce(Map<String, Map<String, Fare>> fares, Trip trip) {
         // Recursively enumerate all packages of tickets with which the trip can be done.
         // Take the cheapest.
         TicketPurchaseScoreCalculator ticketPurchaseScoreCalculator = new TicketPurchaseScoreCalculator();
diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/fare/TicketPurchase.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/fare/TicketPurchase.java
index 2a00566ac..c815e2381 100644
--- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/fare/TicketPurchase.java
+++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/fare/TicketPurchase.java
@@ -21,7 +21,7 @@ package com.graphhopper.gtfs.fare;
 import java.util.*;
 
 class TicketPurchase {
-    private List<FareAssignment> fareAssignments = new ArrayList<>();
+    public final List<FareAssignment> fareAssignments;
 
     TicketPurchase(List<FareAssignment> fareAssignments) {
         this.fareAssignments = fareAssignments;
@@ -31,8 +31,8 @@ class TicketPurchase {
         Map<String, TicketPurchaseScoreCalculator.TempTicket> currentTickets = new HashMap<>();
         for (FareAssignment fareAssignment : fareAssignments) {
             if (fareAssignment.fare != null) {
-                currentTickets.computeIfAbsent(fareAssignment.fare.fare_id, fareId -> new TicketPurchaseScoreCalculator.TempTicket());
-                currentTickets.compute(fareAssignment.fare.fare_id, (s, tempTicket) -> {
+                currentTickets.computeIfAbsent(fareKey(fareAssignment), fareId -> new TicketPurchaseScoreCalculator.TempTicket());
+                currentTickets.compute(fareKey(fareAssignment), (s, tempTicket) -> {
                     if (fareAssignment.segment.getStartTime() > tempTicket.validUntil
                             || tempTicket.nMoreTransfers == 0) {
                         tempTicket.feed_id = fareAssignment.segment.feed_id;
@@ -57,6 +57,10 @@ class TicketPurchase {
         return tickets;
     }
 
+    private String fareKey(FareAssignment fareAssignment) {
+        return fareAssignment.fare.fare_id+"_"+fareAssignment.fare.fare_attribute.feed_id;
+    }
+
     int getNSchwarzfahrTrips() {
         return (int) fareAssignments.stream().filter(assignment -> assignment.fare == null).count();
     }


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/util/CustomModelTest.java b/core/src/test/java/com/graphhopper/routing/util/CustomModelTest.java
index 7edc9d5ed..772aace6f 100644
--- a/core/src/test/java/com/graphhopper/routing/util/CustomModelTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/CustomModelTest.java
@@ -52,31 +52,19 @@ public class CustomModelTest {
         assertNull(getValue(car, "max_weight"));
 
         assertEquals("<3.0", getValue(CustomModel.merge(car, truck), "max_width"));
-        try {
-            CustomModel.merge(truck, car);
-            fail("car is incompatible to truck (base)");
-        } catch (Exception ex) {
-            assertTrue(ex.getMessage().contains("max_width: only use a comparison key with a bigger value than 3.0 but was 2.0"), ex.getMessage());
-        }
+        Exception ex = assertThrows(IllegalArgumentException.class, () -> CustomModel.merge(truck, car));
+        assertTrue(ex.getMessage().contains("max_width: only use a comparison key with a bigger value than 3.0 but was 2.0"), ex.getMessage());
 
         CustomModel car2 = setValue(new CustomModel(), ">", "max_width", 2);
-        try {
-            CustomModel.merge(truck, car2);
-            fail("car is incompatible to car2 (base)");
-        } catch (Exception ex) {
-            assertTrue(ex.getMessage().contains("max_width: comparison keys must match but did not: "), ex.getMessage());
-        }
+        ex = assertThrows(IllegalArgumentException.class, () -> CustomModel.merge(truck, car2));
+        assertTrue(ex.getMessage().contains("max_width: comparison keys must match but did not: "), ex.getMessage());
 
         Map<String, Object> map = new HashMap<>();
         map.put("<2.0", 0.5);
         CustomModel customModel = new CustomModel();
         customModel.getPriority().put("max_width", map);
-        try {
-            CustomModel.merge(customModel, customModel);
-            fail("models are incompatible");
-        } catch (Exception ex) {
-            assertTrue(ex.getMessage().contains("only blocking comparisons are allowed, but query was 0.5 and server side: 0.5"), ex.getMessage());
-        }
+        ex = assertThrows(IllegalArgumentException.class, () -> CustomModel.merge(customModel, customModel));
+        assertTrue(ex.getMessage().contains("only blocking comparisons are allowed, but query was 0.5 and server side: 0.5"), ex.getMessage());
     }
 
     @Test
@@ -110,11 +98,7 @@ public class CustomModelTest {
         assertEquals(1.5, (Double) ((Map) truck.getPriority().get("road_class")).get("primary"), .1);
 
         truck.getPriority().put("road_class", createMap("primary", "incompatible"));
-        try {
-            CustomModel.merge(car, truck);
-            fail("we cannot merge this");
-        } catch (Exception ex) {
-        }
+        assertThrows(IllegalArgumentException.class, () -> CustomModel.merge(car, truck));
     }
 
     Map createMap(Object... objects) {
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/CustomWeightingTest.java b/core/src/test/java/com/graphhopper/routing/weighting/CustomWeightingTest.java
index 5cbe696a9..9cea03c84 100644
--- a/core/src/test/java/com/graphhopper/routing/weighting/CustomWeightingTest.java
+++ b/core/src/test/java/com/graphhopper/routing/weighting/CustomWeightingTest.java
@@ -36,12 +36,12 @@ import org.locationtech.jts.geom.GeometryFactory;
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 import static com.graphhopper.routing.ev.RoadClass.*;
 import static com.graphhopper.routing.weighting.TurnCostProvider.NO_TURN_COST_PROVIDER;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class CustomWeightingTest {
 
@@ -85,7 +85,7 @@ public class CustomWeightingTest {
         assertEquals(72, new FastestWeighting(carFE, NO_TURN_COST_PROVIDER).calcEdgeWeight(medium, false), .1);
         assertEquals(36, new FastestWeighting(carFE, NO_TURN_COST_PROVIDER).calcEdgeWeight(fast, false), .1);
 
-        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> map = new LinkedHashMap<>();
         CustomModel model = new CustomModel().setDistanceInfluence(0);
         assertEquals(144, createWeighting(model).calcEdgeWeight(slow, false), .1);
         assertEquals(72, createWeighting(model).calcEdgeWeight(medium, false), .1);
@@ -128,7 +128,7 @@ public class CustomWeightingTest {
                 set(roadClassEnc, SECONDARY).set(avSpeedEnc, 70);
 
         CustomModel vehicleModel = new CustomModel();
-        Map map = new HashMap();
+        Map map = new LinkedHashMap();
 
         map.put(PRIMARY.toString(), 1.0);
         map.put(CustomWeighting.CATCH_ALL, 0.5);
@@ -158,7 +158,7 @@ public class CustomWeightingTest {
         assertEquals(1.15, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
 
         // now reduce speed for road class 'primary' -> the weight increases
-        Map map = new HashMap();
+        Map map = new LinkedHashMap();
         map.put(PRIMARY.toString(), 60);
         vehicleModel.getMaxSpeed().put(KEY, map);
         assertEquals(1.3, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
@@ -170,7 +170,7 @@ public class CustomWeightingTest {
         CustomModel vehicleModel = new CustomModel();
         assertEquals(3.1, createWeighting(vehicleModel).calcEdgeWeight(edge, false), 0.01);
         // here we increase weight for edges that are road class links
-        Map map = new HashMap<>();
+        Map map = new LinkedHashMap<>();
         map.put("true", 0.5);
         vehicleModel.getPriority().put(RoadClassLink.KEY, map);
         CustomWeighting weighting = createWeighting(vehicleModel);
@@ -183,7 +183,7 @@ public class CustomWeightingTest {
     public void testPriority() {
         CustomModel vehicleModel = new CustomModel();
 
-        Map map = new HashMap();
+        Map map = new LinkedHashMap();
         map.put(MOTORWAY.toString(), 0.1);
         vehicleModel.getPriority().put(KEY, map);
         CustomWeighting weighting = createWeighting(vehicleModel);
@@ -207,7 +207,7 @@ public class CustomWeightingTest {
         assertEquals(3.10, weighting.calcEdgeWeight(slowEdge, false), 0.01);
         assertEquals(1.30, weighting.calcEdgeWeight(fastEdge, false), 0.01);
 
-        Map map = new HashMap();
+        Map map = new LinkedHashMap();
         map.put(">69", 0.2);
         CustomModel vehicleModel = new CustomModel();
         vehicleModel.getPriority().put("max_speed", map);
@@ -216,7 +216,7 @@ public class CustomWeightingTest {
         assertEquals(3.70, weighting.calcEdgeWeight(fastEdge, false), 0.01);
 
         // this is currently a bit hidden feature as only the shared encoded values are suggested in the UI
-        map = new HashMap();
+        map = new LinkedHashMap();
         map.put(">50", 0.2);
         vehicleModel = new CustomModel();
         vehicleModel.getPriority().put(EncodingManager.getKey("car", "average_speed"), map);
@@ -234,7 +234,7 @@ public class CustomWeightingTest {
         assertEquals(3.10, weighting.calcEdgeWeight(slowEdge, false), 0.01);
         assertEquals(1.30, weighting.calcEdgeWeight(fastEdge, false), 0.01);
 
-        Map map = new HashMap();
+        Map map = new LinkedHashMap();
         map.put(" > 1.5", 0.2); // allow decimal values in range even for int encoded value
         CustomModel vehicleModel = new CustomModel();
         vehicleModel.getPriority().put("lanes", map);
@@ -243,6 +243,15 @@ public class CustomWeightingTest {
         assertEquals(3.70, weighting.calcEdgeWeight(fastEdge, false), 0.01);
     }
 
+    @Test
+    public void unorderedMapShouldFail() {
+        Map map = new HashMap<>();
+        map.put(" > 1.5", 0.2); // allow decimal values in range even for int encoded value
+        CustomModel vehicleModel = new CustomModel();
+        vehicleModel.getPriority().put("lanes", map);
+        assertThrows(IllegalArgumentException.class, () -> createWeighting(vehicleModel));
+    }
+
     @Test
     public void testAvoidArea() {
         CustomModel vehicleModel = new CustomModel();
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/PriorityCalculatorTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/PriorityCalculatorTest.java
index ee4ca9173..4f4ad5090 100644
--- a/core/src/test/java/com/graphhopper/routing/weighting/custom/PriorityCalculatorTest.java
+++ b/core/src/test/java/com/graphhopper/routing/weighting/custom/PriorityCalculatorTest.java
@@ -26,6 +26,7 @@ import com.graphhopper.util.EdgeIteratorState;
 import org.junit.jupiter.api.Test;
 
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -45,7 +46,7 @@ class PriorityCalculatorTest {
     @Test
     public void priority() {
         CustomModel model = new CustomModel();
-        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> map = new LinkedHashMap<>();
         map.put("*", 0.3);
         model.getPriority().put(RoadClass.KEY, map);
         assertEquals(0.3, calcPriority(edge, model));
@@ -63,7 +64,7 @@ class PriorityCalculatorTest {
     @Test
     public void invalidPriority() {
         CustomModel model = new CustomModel();
-        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> map = new LinkedHashMap<>();
         map.put("*", 1.1);
         model.getPriority().put(RoadClass.KEY, map);
         try {
@@ -88,9 +89,9 @@ class PriorityCalculatorTest {
         EnumEncodedValue<RoadEnvironment> roadEnvironment = em.getEnumEncodedValue(RoadEnvironment.KEY, RoadEnvironment.class);
         edge.set(roadClass, RoadClass.PRIMARY);
         edge.set(roadEnvironment, RoadEnvironment.BRIDGE);
-        Map<String, Object> roadClassMap = new HashMap<>();
+        Map<String, Object> roadClassMap = new LinkedHashMap<>();
         roadClassMap.put(RoadClass.PRIMARY.toString(), 0.7);
-        Map<String, Object> roadEnvironmentMap = new HashMap<>();
+        Map<String, Object> roadEnvironmentMap = new LinkedHashMap<>();
         roadEnvironmentMap.put(RoadEnvironment.BRIDGE.toString(), 0.5);
         CustomModel model = new CustomModel();
         model.getPriority().put(RoadClass.KEY, roadClassMap);
@@ -106,7 +107,7 @@ class PriorityCalculatorTest {
         edge.set(maxSpeedEnc, 110);
         edge.setReverse(maxSpeedEnc, 50);
 
-        Map<String, Object> maxSpeedMap = new HashMap<>();
+        Map<String, Object> maxSpeedMap = new LinkedHashMap<>();
         maxSpeedMap.put("<100", 0.5);
 
         CustomModel model = new CustomModel();
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/SpeedCalculatorTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/SpeedCalculatorTest.java
index 9a72da98e..2b1291f9d 100644
--- a/core/src/test/java/com/graphhopper/routing/weighting/custom/SpeedCalculatorTest.java
+++ b/core/src/test/java/com/graphhopper/routing/weighting/custom/SpeedCalculatorTest.java
@@ -29,6 +29,7 @@ import com.graphhopper.util.EdgeIteratorState;
 import org.junit.jupiter.api.Test;
 
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -57,7 +58,7 @@ class SpeedCalculatorTest {
     public void maxSpeed() {
         // here we use max_speed to limit speed
         CustomModel model = new CustomModel();
-        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> map = new LinkedHashMap<>();
         map.put("*", 25);
         model.getMaxSpeed().put(RoadClass.KEY, map);
         assertEquals(25, calcSpeed(edge, model));
@@ -78,7 +79,7 @@ class SpeedCalculatorTest {
     @Test
     public void invalidMaxSpeed() {
         CustomModel model = new CustomModel();
-        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> map = new LinkedHashMap<>();
         map.put("*", 300);
         model.getMaxSpeed().put(RoadClass.KEY, map);
         try {
@@ -103,9 +104,9 @@ class SpeedCalculatorTest {
         EnumEncodedValue<RoadEnvironment> roadEnvironment = em.getEnumEncodedValue(RoadEnvironment.KEY, RoadEnvironment.class);
         edge.set(roadClass, RoadClass.PRIMARY);
         edge.set(roadEnvironment, RoadEnvironment.BRIDGE);
-        Map<String, Object> roadClassMap = new HashMap<>();
+        Map<String, Object> roadClassMap = new LinkedHashMap<>();
         roadClassMap.put(RoadClass.PRIMARY.toString(), 40);
-        Map<String, Object> roadEnvironmentMap = new HashMap<>();
+        Map<String, Object> roadEnvironmentMap = new LinkedHashMap<>();
         roadEnvironmentMap.put(RoadEnvironment.BRIDGE.toString(), 20);
         CustomModel model = new CustomModel();
         model.getMaxSpeed().put(RoadClass.KEY, roadClassMap);
@@ -118,7 +119,7 @@ class SpeedCalculatorTest {
     public void speedFactor() {
         // here we use speed_factor to adjust speed
         CustomModel model = new CustomModel();
-        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> map = new LinkedHashMap<>();
         map.put("*", 0.1);
         model.getSpeedFactor().put(RoadClass.KEY, map);
         assertEquals(6, calcSpeed(edge, model));
@@ -136,7 +137,7 @@ class SpeedCalculatorTest {
     @Test
     public void invalidSpeedFactor() {
         CustomModel model = new CustomModel();
-        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> map = new LinkedHashMap<>();
         map.put("*", 1.1);
         model.getSpeedFactor().put(RoadClass.KEY, map);
         try {
@@ -154,9 +155,9 @@ class SpeedCalculatorTest {
         EnumEncodedValue<RoadEnvironment> roadEnvironment = em.getEnumEncodedValue(RoadEnvironment.KEY, RoadEnvironment.class);
         edge.set(roadClass, RoadClass.PRIMARY);
         edge.set(roadEnvironment, RoadEnvironment.BRIDGE);
-        Map<String, Object> roadClassMap = new HashMap<>();
+        Map<String, Object> roadClassMap = new LinkedHashMap<>();
         roadClassMap.put(RoadClass.PRIMARY.toString(), 0.7);
-        Map<String, Object> roadEnvironmentMap = new HashMap<>();
+        Map<String, Object> roadEnvironmentMap = new LinkedHashMap<>();
         roadEnvironmentMap.put(RoadEnvironment.BRIDGE.toString(), 0.5);
         CustomModel model = new CustomModel();
         model.getSpeedFactor().put(RoadClass.KEY, roadClassMap);
diff --git a/reader-gtfs/src/test/java/com/graphhopper/gtfs/fare/FareTest.java b/reader-gtfs/src/test/java/com/graphhopper/gtfs/fare/FareTest.java
index c8afde3bb..c69f7da4d 100644
--- a/reader-gtfs/src/test/java/com/graphhopper/gtfs/fare/FareTest.java
+++ b/reader-gtfs/src/test/java/com/graphhopper/gtfs/fare/FareTest.java
@@ -23,16 +23,19 @@ import com.conveyal.gtfs.model.Fare;
 import com.conveyal.gtfs.model.FareAttribute;
 import com.conveyal.gtfs.model.FareRule;
 import com.csvreader.CsvReader;
+import org.hamcrest.Matcher;
 import org.junit.Assert;
 import org.junit.experimental.theories.DataPoint;
 import org.junit.experimental.theories.Theories;
 import org.junit.experimental.theories.Theory;
+import org.junit.jupiter.api.Test;
 import org.junit.runner.RunWith;
 
 import java.io.IOException;
 import java.io.StringReader;
 import java.math.BigDecimal;
 import java.util.*;
+import java.util.function.BiFunction;
 import java.util.stream.Collectors;
 
 import static org.hamcrest.CoreMatchers.equalTo;
@@ -46,57 +49,84 @@ import static org.junit.Assume.assumeTrue;
 @RunWith(Theories.class)
 public class FareTest {
 
-    public static String feed_id = "only_feed_id";
-
     // See https://code.google.com/archive/p/googletransitdatafeed/wikis/FareExamples.wiki
 
-    public static @DataPoint Map<String, Map<String, Fare>> oneDollarUnlimitedTransfers = parseFares("only_fare,1.00,USD,0\n", "");
-    public static @DataPoint Map<String, Map<String, Fare>> oneDollarNoTransfers = parseFares("only_fare,1.00,USD,0,0\n", "");
-    public static @DataPoint Map<String, Map<String, Fare>> oneDollarTimeLimitedTransfers = parseFares("only_fare,1.00,USD,0,,5400\n", "");
-    public static @DataPoint Map<String, Map<String, Fare>> regularAndExpress = parseFares("local_fare,1.75,USD,0,0\n"+"express_fare,5.00,USD,0,0\n", "local_fare,Route_1\nexpress_fare,Route_2\nexpress_fare,Route3\n");
-    public static @DataPoint Map<String, Map<String, Fare>> withTransfersOrWithout = parseFares("simple_fare,2.00,USD,0,0\n"+"plustransfer_fare,2.50,USD,0,,5400", "");
-    public static @DataPoint Map<String, Map<String, Fare>> stationPairs = parseFares("!S1_to_S2,1.75,USD,0\n!S1_to_S3,3.25,USD,0\n!S1_to_S4,4.55,USD,0\n!S4_to_S1,5.65,USD,0\n", "!S1_to_S2,,S1,S2\n!S1_to_S3,,S1,S3\n!S1_to_S4,,S1,S4\n!S4_to_S1,,S4,S1\n");
-    public static @DataPoint Map<String, Map<String, Fare>> zones = parseFares("F1,4.15,USD,0\nF2,2.20,USD,0\nF3,2.20,USD,0\nF4,2.95,USD,0\nF5,1.25,USD,0\nF6,1.95,USD,0\nF7,1.95,USD,0\n", "F1,,,,1\nF1,,,,2\nF1,,,,3\nF2,,,,1\nF2,,,,2\nF3,,,,1\nF3,,,,3\nF4,,,,2\nF4,,,,3\nF5,,,,1\nF6,,,,2\nF7,,,,3\n");
-
+    public static @DataPoint Map<String, Map<String, Fare>> oneDollarUnlimitedTransfers = map(
+            "only_feed_id", parseFares("only_feed_id","only_fare,1.00,USD,0\n", ""),
+            "feed_id_2", parseFares("feed_id_2","", "")
+    );
+    public static @DataPoint Map<String, Map<String, Fare>> oneDollarNoTransfers = map(
+            "only_feed_id", parseFares("only_feed_id","only_fare,1.00,USD,0,0\n", ""),
+            "feed_id_2", parseFares("feed_id_2","", "")
+    );
+    public static @DataPoint Map<String, Map<String, Fare>> oneDollarTimeLimitedTransfers = map(
+            "only_feed_id", parseFares("only_feed_id","only_fare,1.00,USD,0,,5400\n", ""),
+            "feed_id_2", parseFares("feed_id_2","", "")
+    );
+    public static @DataPoint Map<String, Map<String, Fare>> regularAndExpress = map(
+            "only_feed_id", parseFares("only_feed_id","local_fare,1.75,USD,0,0\n"+"express_fare,5.00,USD,0,0\n", "local_fare,Route_1\nexpress_fare,Route_2\nexpress_fare,Route3\n"),
+            "feed_id_2", parseFares("feed_id_2","", "")
+    );
+    public static @DataPoint Map<String, Map<String, Fare>> withTransfersOrWithout = map(
+            "only_feed_id", parseFares("only_feed_id","simple_fare,2.00,USD,0,0\n"+"plustransfer_fare,2.50,USD,0,,5400", ""),
+            "feed_id_2", parseFares("feed_id_2","", "")
+    );
+    public static @DataPoint Map<String, Map<String, Fare>> stationPairs = map(
+            "only_feed_id", parseFares("only_feed_id","!S1_to_S2,1.75,USD,0\n!S1_to_S3,3.25,USD,0\n!S1_to_S4,4.55,USD,0\n!S4_to_S1,5.65,USD,0\n", "!S1_to_S2,,S1,S2\n!S1_to_S3,,S1,S3\n!S1_to_S4,,S1,S4\n!S4_to_S1,,S4,S1\n"),
+            "feed_id_2", parseFares("feed_id_2","", "")
+    );
+    public static @DataPoint Map<String, Map<String, Fare>> zones = map(
+            "only_feed_id", parseFares("only_feed_id","F1,4.15,USD,0\nF2,2.20,USD,0\nF3,2.20,USD,0\nF4,2.95,USD,0\nF5,1.25,USD,0\nF6,1.95,USD,0\nF7,1.95,USD,0\n", "F1,,,,1\nF1,,,,2\nF1,,,,3\nF2,,,,1\nF2,,,,2\nF3,,,,1\nF3,,,,3\nF4,,,,2\nF4,,,,3\nF5,,,,1\nF6,,,,2\nF7,,,,3\n"),
+            "feed_id_2", parseFares("feed_id_2","", "")
+    );
+    public static @DataPoint Map<String, Map<String, Fare>> twoFeeds = map(
+            "only_feed_id", parseFares("only_feed_id","only_fare,1.00,USD,0,0\n", ""),
+            "feed_id_2", parseFares("feed_id_2","only_fare,2.00,USD,0,0\n", "")
+    );
 
     public static @DataPoint Trip tripWithOneSegment;
     public static @DataPoint Trip tripWithTwoSegments;
     public static @DataPoint Trip shortTripWithTwoSegments;
     public static @DataPoint Trip twoLegsWithDistinctZones;
-
+    public static @DataPoint Trip twoLegsWithDistinctFeeds;
 
     static {
         tripWithOneSegment = new Trip();
-        tripWithOneSegment.segments.add(new Trip.Segment(feed_id, "Route_1", 0, "S1", "S2", new HashSet<>(Arrays.asList("1","2","3"))));
+        tripWithOneSegment.segments.add(new Trip.Segment("only_feed_id", "Route_1", 0, "S1", "S2", new HashSet<>(Arrays.asList("1","2","3"))));
 
         tripWithTwoSegments = new Trip();
-        tripWithTwoSegments.segments.add(new Trip.Segment(feed_id, "Route_1", 0, "S1", "S4", new HashSet<>(Arrays.asList("1"))));
-        tripWithTwoSegments.segments.add(new Trip.Segment(feed_id, "Route_2", 6000, "S4", "S1", new HashSet<>(Arrays.asList("1"))));
+        tripWithTwoSegments.segments.add(new Trip.Segment("only_feed_id", "Route_1", 0, "S1", "S4", new HashSet<>(Arrays.asList("1"))));
+        tripWithTwoSegments.segments.add(new Trip.Segment("only_feed_id", "Route_2", 6000, "S4", "S1", new HashSet<>(Arrays.asList("1"))));
 
         shortTripWithTwoSegments = new Trip();
-        shortTripWithTwoSegments.segments.add(new Trip.Segment(feed_id, "Route_1",0, "S1", "S4", new HashSet<>(Arrays.asList("2", "3"))));
-        shortTripWithTwoSegments.segments.add(new Trip.Segment(feed_id, "Route_2",5000, "S4", "S1", new HashSet<>(Arrays.asList("2", "3"))));
+        shortTripWithTwoSegments.segments.add(new Trip.Segment("only_feed_id", "Route_1",0, "S1", "S4", new HashSet<>(Arrays.asList("2", "3"))));
+        shortTripWithTwoSegments.segments.add(new Trip.Segment("only_feed_id", "Route_2",5000, "S4", "S1", new HashSet<>(Arrays.asList("2", "3"))));
 
         twoLegsWithDistinctZones = new Trip();
-        twoLegsWithDistinctZones.segments.add(new Trip.Segment(feed_id, "Route_1",0, "S1", "S4", new HashSet<>(Arrays.asList("1"))));
-        twoLegsWithDistinctZones.segments.add(new Trip.Segment(feed_id, "Route_2",5000, "S4", "S1", new HashSet<>(Arrays.asList("2"))));
-        twoLegsWithDistinctZones.segments.add(new Trip.Segment(feed_id, "Route_1",6000, "S1", "S4", new HashSet<>(Arrays.asList("3"))));
+        twoLegsWithDistinctZones.segments.add(new Trip.Segment("only_feed_id", "Route_1",0, "S1", "S4", new HashSet<>(Arrays.asList("1"))));
+        twoLegsWithDistinctZones.segments.add(new Trip.Segment("only_feed_id", "Route_2",5000, "S4", "S1", new HashSet<>(Arrays.asList("2"))));
+        twoLegsWithDistinctZones.segments.add(new Trip.Segment("only_feed_id", "Route_1",6000, "S1", "S4", new HashSet<>(Arrays.asList("3"))));
+
+        twoLegsWithDistinctFeeds = new Trip();
+        twoLegsWithDistinctFeeds.segments.add(new Trip.Segment("only_feed_id", "Route_1",0, "S1", "S4", new HashSet<>()));
+        twoLegsWithDistinctFeeds.segments.add(new Trip.Segment("feed_id_2", "Route_2",5000, "T", "T", new HashSet<>()));
+
     }
 
     @Theory
     public void irrelevantAlternatives(Map<String, Map<String, Fare>> fares, Trip trip) {
-        assumeThat("There are at least two fares.", fares.get(feed_id).entrySet().size(), is(greaterThanOrEqualTo(2)));
+        assumeThat("There are at least two fares.", fares.get("only_feed_id").entrySet().size(), is(greaterThanOrEqualTo(2)));
 
         // If we only use one fare, say, the most expensive one...
-        Fare mostExpensiveFare = fares.get(feed_id).values().stream().max(Comparator.comparingDouble(f -> f.fare_attribute.price)).get();
-        Map<String, Map<String, Fare>> singleFare = new HashMap<>();
-        Map<String, Fare> singleFaree = new HashMap<>();
-        singleFaree.put(mostExpensiveFare.fare_id, mostExpensiveFare);
-        singleFare.put(feed_id, singleFaree);
+        Fare mostExpensiveFare = fares.get("only_feed_id").values().stream().max(Comparator.comparingDouble(f -> f.fare_attribute.price)).get();
+        Map<String, Map<String, Fare>> singleFare = map(
+                "only_feed_id", map(mostExpensiveFare.fare_id, mostExpensiveFare),
+                "feed_id_2", parseFares("feed_id_2","", "")
+        );
 
         // ..and that still works for our trip..
         assumeThat("There is at least one fare for each segment.",
-                trip.segments.stream().map(segment -> Fares.possibleFares(singleFaree, segment)).collect(Collectors.toList()),
+                trip.segments.stream().map(segment -> Fares.possibleFares(singleFare.get(segment.feed_id), segment)).collect(Collectors.toList()),
                 everyItem(is(not(empty()))));
         double priceWithOneOption = Fares.cheapestFare(singleFare, trip).get().getAmount().doubleValue();
 
@@ -108,7 +138,7 @@ public class FareTest {
 
     @Theory
     public void everySegmentHasAFare(Map<String, Map<String, Fare>> fares, Trip trip) {
-        assumeThat("There are fares.", fares.entrySet(), not(empty()));
+        assumeEachFeedHasAFare(fares);
         assertThat("There is at least one fare for each segment.",
                 trip.segments.stream().map(segment -> Fares.possibleFares(fares.get(segment.feed_id), segment)).collect(Collectors.toList()),
                 everyItem(is(not(empty()))));
@@ -116,20 +146,21 @@ public class FareTest {
 
     @Theory
     public void withNoTransfersAndNoAlternativesBuyOneTicketForEachSegment(Map<String, Map<String, Fare>> fares, Trip trip) throws IOException {
+        assumeEachFeedHasAFare(fares);
         fares.values().stream().flatMap(fs -> fs.values().stream()).forEach(fare -> {
             assumeThat("No Transfers allowed.", fare.fare_attribute.transfers, equalTo(0));
         });
         trip.segments.stream()
                 .map(segment -> Fares.possibleFares(fares.get(segment.feed_id), segment))
                 .forEach(candidateFares -> assertThat("Only one fare candidate per segment.", candidateFares.size(), equalTo(1)));
-        assertThat("Total fare is the sum of all individual fares.",
-                Fares.cheapestFare(fares, trip).get().getAmount().doubleValue(),
-                equalTo(trip.segments.stream().flatMap(segment -> Fares.possibleFares(fares.get(segment.feed_id), segment).stream()).mapToDouble(fare -> fare.fare_attribute.price).sum()));
+        double totalFare = Fares.cheapestFare(fares, trip).get().getAmount().doubleValue();
+        double sumOfIndividualFares = trip.segments.stream().flatMap(segment -> Fares.possibleFares(fares.get(segment.feed_id), segment).stream()).mapToDouble(fare -> fare.fare_attribute.price).sum();
+        assertThat("Total fare is the sum of all individual fares.", totalFare, equalTo(sumOfIndividualFares));
     }
 
     @Theory
     public void canGoAllTheWayOnOneTicket(Map<String, Map<String, Fare>> fares, Trip trip) throws IOException {
-        // TODO: assumeThat all segments have same feedId
+        assumeThat(trip.segments.stream().map(s -> s.feed_id).distinct().count(), equalTo(1L));
         Optional<Fare> obviouslyCheapestFare = fares.values().stream().flatMap(fs -> fs.values().stream())
                 .filter(fare -> fare.fare_rules.isEmpty()) // Fare has no restrictions except transfer count/duration
                 .filter(fare -> fare.fare_attribute.transfers >= trip.segments.size()-1) // Fare allows the number of transfers we need for our trip
@@ -154,6 +185,7 @@ public class FareTest {
 
     @Theory
     public void ifAllLegsPassThroughAllZonesOfTheTripItCantGetCheaper(Map<String, Map<String, Fare>> fares, Trip trip) {
+        assumeEachFeedHasAFare(fares);
         double cheapestFare = Fares.cheapestFare(fares, trip).get().getAmount().doubleValue();
         Set<String> allZones = trip.segments.stream().flatMap(seg -> seg.getZones().stream()).collect(Collectors.toSet());
         Trip otherTrip = new Trip();
@@ -176,9 +208,9 @@ public class FareTest {
                 });
     }
 
-    private static Map<String, Map<String, Fare>> parseFares(String fareAttributes, String fareRules) {
+    public static Map<String, Fare> parseFares(String feedId, String fareAttributes, String fareRules) {
         GTFSFeed feed = new GTFSFeed();
-        HashMap<String, Map<String, Fare>> allFares = new HashMap<>();
+        feed.feedId = feedId;
         HashMap<String, Fare> fares = new HashMap<>();
         new FareAttribute.Loader(feed, fares) {
             void load(String input){
@@ -206,8 +238,29 @@ public class FareTest {
                 }
             }
         }.load(fareRules);
-        allFares.put(feed_id, fares);
-        return allFares;
+        return fares;
+    }
+
+    private void assumeEachFeedHasAFare(Map<String, Map<String, Fare>> fares) {
+        fares.values().forEach(faresForOneFeed -> assumeThat("There are fares.", faresForOneFeed.entrySet(), not(empty())));
+    }
+
+    // https://bitbucket.org/assylias/bigblue-utils/src/master/src/main/java/com/assylias/bigblue/utils/Maps.java?at=master
+    public static <K, V> Map<K, V> map(K key, V value, Object... kvs) {
+        return map(HashMap::new, key, value, kvs);
+    }
+
+    public static <K, V, T extends Map<K, V>> T map(BiFunction<Integer, Float, T> mapFactory, K key, V value, Object... kvs) {
+        T m = mapFactory.apply(kvs.length / 2 + 1, 1f);
+        m.put(key, value);
+        for (int i = 0; i < kvs.length;) {
+            K k = (K) kvs[i++];
+            V v = (V) kvs[i++];
+            if (k != null && v != null) {
+                m.put(k, v);
+            }
+        }
+        return m;
     }
 
 }
diff --git a/reader-gtfs/src/test/java/com/graphhopper/gtfs/fare/MoreFareTests.java b/reader-gtfs/src/test/java/com/graphhopper/gtfs/fare/MoreFareTests.java
new file mode 100644
index 000000000..c92854cc3
--- /dev/null
+++ b/reader-gtfs/src/test/java/com/graphhopper/gtfs/fare/MoreFareTests.java
@@ -0,0 +1,65 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.gtfs.fare;
+
+import com.conveyal.gtfs.model.Fare;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static com.graphhopper.gtfs.fare.FareTest.map;
+import static com.graphhopper.gtfs.fare.FareTest.parseFares;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeThat;
+
+public class MoreFareTests {
+
+    @Test
+    public void wurst() {
+        Map<String, Map<String, Fare>> twoFeeds = map(
+                "only_feed_id", parseFares("only_feed_id", "only_fare,1.00,USD,0,0\n", ""),
+                "feed_id_2", parseFares("feed_id_2","only_fare,2.00,USD,0,0\n", "")
+        );
+        for (Map.Entry<String, Map<String, Fare>> entry : twoFeeds.entrySet()) {
+            for (Fare fare : entry.getValue().values()) {
+                assumeThat(fare.fare_attribute.feed_id, equalTo(entry.getKey()));
+            }
+        }
+        Trip twoLegsWithDistinctFeeds = new Trip();
+        twoLegsWithDistinctFeeds.segments.add(new Trip.Segment("only_feed_id", "Route_1",0, "S1", "S4", new HashSet<>()));
+        twoLegsWithDistinctFeeds.segments.add(new Trip.Segment("feed_id_2", "Route_2",5000, "T", "T", new HashSet<>()));
+        List<TicketPurchase> ticketPurchases = Fares.allShoppingCarts(twoFeeds, twoLegsWithDistinctFeeds).collect(Collectors.toList());
+        for (TicketPurchase ticketPurchase : ticketPurchases) {
+            for (FareAssignment fareAssignment : ticketPurchase.fareAssignments) {
+                assertThat(fareAssignment.segment.feed_id, equalTo(fareAssignment.fare.fare_attribute.feed_id));
+            }
+            assertThat(ticketPurchase.getNSchwarzfahrTrips(), equalTo(0));
+            List<Ticket> tickets = ticketPurchase.getTickets();
+            assertThat(tickets.stream().map(t -> t.feed_id).collect(Collectors.toSet()), equalTo(new HashSet<>(Arrays.asList("only_feed_id","feed_id_2"))));
+        }
+        assertThat(Fares.cheapestFare(twoFeeds, twoLegsWithDistinctFeeds).get().getAmount(), equalTo(new BigDecimal("3.0")));
+    }
+
+}