diff --git a/src/main/java/org/matsim/prepare/network/FreeSpeedOptimizer.java b/src/main/java/org/matsim/prepare/network/FreeSpeedOptimizer.java index 27a50797..3d6ca0de 100644 --- a/src/main/java/org/matsim/prepare/network/FreeSpeedOptimizer.java +++ b/src/main/java/org/matsim/prepare/network/FreeSpeedOptimizer.java @@ -37,6 +37,7 @@ import org.matsim.core.router.util.LeastCostPathCalculator; import org.matsim.core.trafficmonitoring.FreeSpeedTravelTime; import org.matsim.core.utils.geometry.CoordUtils; +import org.matsim.prepare.traveltime.SampleValidationRoutes; import picocli.CommandLine; import java.io.File; @@ -72,7 +73,7 @@ public class FreeSpeedOptimizer implements MATSimAppCommand { private List validationFiles; private Network network; - private Object2DoubleMap validationSet; + private Object2DoubleMap validationSet; private Map, PrepareNetworkParams.Feature> features; private ObjectMapper mapper; @@ -198,9 +199,9 @@ private Result evaluateNetwork(Request request, String save) throws IOException List rbl = new ArrayList<>(); List traffic_light = new ArrayList<>(); - for (Object2DoubleMap.Entry e : validationSet.object2DoubleEntrySet()) { + for (Object2DoubleMap.Entry e : validationSet.object2DoubleEntrySet()) { - Entry r = e.getKey(); + SampleValidationRoutes.FromToNodes r = e.getKey(); Node fromNode = network.getNodes().get(r.fromNode()); Node toNode = network.getNodes().get(r.toNode()); @@ -249,72 +250,31 @@ private Result evaluateNetwork(Request request, String save) throws IOException } /** - * Collect highest observed speed. + * Calculate the target speed. */ - static Object2DoubleMap readValidation(List validationFiles) throws IOException { + static Object2DoubleMap readValidation(List validationFiles) throws IOException { // entry to hour and list of speeds - Map> entries = new LinkedHashMap<>(); + Map> entries = SampleValidationRoutes.readValidation(validationFiles); - if (validationFiles != null) - for (String file : validationFiles) { + Object2DoubleMap result = new Object2DoubleOpenHashMap<>(); - log.info("Loading {}", file); + // Target values + for (Map.Entry> e : entries.entrySet()) { - try (CSVParser parser = new CSVParser(Files.newBufferedReader(Path.of(file)), - CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build())) { + Int2ObjectMap perHour = e.getValue(); - for (CSVRecord r : parser) { - Entry e = new Entry(Id.createNodeId(r.get("from_node")), Id.createNodeId(r.get("to_node"))); - double speed = Double.parseDouble(r.get("dist")) / Double.parseDouble(r.get("travel_time")); + // Use avg from all values for 3:00 and 21:00 + double avg = DoubleStream.concat(perHour.get(3).doubleStream(), perHour.get(21).doubleStream()) + .average().orElseThrow(); - if (!Double.isFinite(speed)) { - log.warn("Invalid entry {}", r); - continue; - } - Int2ObjectMap perHour = entries.computeIfAbsent(e, (k) -> new Int2ObjectLinkedOpenHashMap<>()); - perHour.computeIfAbsent(Integer.parseInt(r.get("hour")), k -> new DoubleArrayList()).add(speed); - } - } - } - - Object2DoubleMap result = new Object2DoubleOpenHashMap<>(); - - try (CSVPrinter printer = new CSVPrinter(Files.newBufferedWriter(Path.of("routes-ref.csv")), CSVFormat.DEFAULT)) { - - printer.printRecord("from_node", "to_node", "hour", "min", "max", "mean", "std"); - - // Target values - for (Map.Entry> e : entries.entrySet()) { - - Int2ObjectMap perHour = e.getValue(); - - // Use avg from all values for 3:00 and 21:00 - double avg = DoubleStream.concat(perHour.get(3).doubleStream(), perHour.get(21).doubleStream()) - .average().orElseThrow(); - - - for (Int2ObjectMap.Entry e2 : perHour.int2ObjectEntrySet()) { - - SummaryStatistics stats = new SummaryStatistics(); - // This is as kmh - e2.getValue().forEach(v -> stats.addValue(v * 3.6)); - - printer.printRecord(e.getKey().fromNode, e.getKey().toNode, e2.getIntKey(), - stats.getMin(), stats.getMax(), stats.getMean(), stats.getStandardDeviation()); - } - - result.put(e.getKey(), avg); - } + result.put(e.getKey(), avg); } return result; } - private record Entry(Id fromNode, Id toNode) { - } - private record Data(double[] x, double yPred, double yTrue) { } diff --git a/src/main/java/org/matsim/prepare/traveltime/FetchRoutesTask.java b/src/main/java/org/matsim/prepare/traveltime/FetchRoutesTask.java new file mode 100644 index 00000000..1904deaa --- /dev/null +++ b/src/main/java/org/matsim/prepare/traveltime/FetchRoutesTask.java @@ -0,0 +1,138 @@ +package org.matsim.prepare.traveltime; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.CSVRecord; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Node; +import org.matsim.prepare.traveltime.api.*; + +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Task to fetch routes from api service. + */ +final class FetchRoutesTask implements Runnable { + + private static final Logger log = LogManager.getLogger(FetchRoutesTask.class); + + private final RouteApi api; + private final String apiKey; + + private final List routes; + private final List hours; + private final Path out; + + FetchRoutesTask(RouteApi api, String apiKey, List routes, List hours, Path out) { + this.api = api; + this.apiKey = apiKey; + this.routes = routes; + this.hours = hours; + this.out = out; + } + + + private void fetch() throws Exception { + + // Collect existing entries to support resuming + Set entries = new HashSet<>(); + OpenOption open; + if (Files.exists(out)) { + open = StandardOpenOption.APPEND; + try (CSVParser csv = new CSVParser(Files.newBufferedReader(out), CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build())) { + for (CSVRecord r : csv) { + entries.add(new Entry( + Id.createNodeId(r.get("from_node")), + Id.createNodeId(r.get("to_node")), + r.get("api"), + Integer.parseInt(r.get("hour"))) + ); + } + } + + } else + open = StandardOpenOption.CREATE_NEW; + + try (RouteValidator val = switch (api) { + case google -> new GoogleRouteValidator(apiKey); + case woosmap -> new WoosMapRouteValidator(apiKey); + case mapbox -> new MapboxRouteValidator(apiKey); + case here -> new HereRouteValidator(apiKey); + case tomtom -> new TomTomRouteValidator(apiKey); + }) { + + try (CSVPrinter csv = new CSVPrinter(Files.newBufferedWriter(out, open), CSVFormat.DEFAULT)) { + + if (open == StandardOpenOption.CREATE_NEW) { + csv.printRecord("from_node", "to_node", "api", "hour", "dist", "travel_time"); + csv.flush(); + } + + int i = 0; + for (SampleValidationRoutes.Route route : routes) { + for (int h : hours) { + + // Skip entries already present + Entry e = new Entry(route.fromNode(), route.toNode(), val.name(), h); + if (entries.contains(e)) + continue; + + try { + RouteValidator.Result res = fetchWithBackoff(val, route, h, 0); + csv.printRecord(route.fromNode(), route.toNode(), val.name(), h, res.dist(), res.travelTime()); + } catch (Exception ex) { + log.warn("Could not retrieve result for route {} {}", api, route, ex); + } + } + + csv.flush(); + + if (i++ % 50 == 0) { + log.info("{}: processed {} routes", api, i - 1); + } + } + } + } + } + + /** + * Fetch route with increasing delay. + */ + private RouteValidator.Result fetchWithBackoff(RouteValidator val, SampleValidationRoutes.Route route, int h, int i) throws InterruptedException { + try { + return val.retrieve(route.from(), route.to(), h); + } catch (Exception e) { + if (i < 3) { + long backoff = (long) (10000d * Math.pow(2, i)); + log.warn("Failed to fetch result for {} {}: {}, (retrying after {}s)", api, route, e.getMessage(), backoff / 1000); + + Thread.sleep(backoff); + return fetchWithBackoff(val, route, h, ++i); + } + + throw e; + } + } + + @Override + public void run() { + try { + fetch(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + record Entry(Id fromNode, Id toNode, String api, int hour) { + } + +} diff --git a/src/main/java/org/matsim/prepare/traveltime/RouteApi.java b/src/main/java/org/matsim/prepare/traveltime/RouteApi.java new file mode 100644 index 00000000..a3012ffc --- /dev/null +++ b/src/main/java/org/matsim/prepare/traveltime/RouteApi.java @@ -0,0 +1,12 @@ +package org.matsim.prepare.traveltime; + +/** + * Defines different available API services. + */ +public enum RouteApi { + google, + woosmap, + mapbox, + here, + tomtom +} diff --git a/src/main/java/org/matsim/prepare/traveltime/SampleValidationRoutes.java b/src/main/java/org/matsim/prepare/traveltime/SampleValidationRoutes.java index b559234b..9494c56f 100644 --- a/src/main/java/org/matsim/prepare/traveltime/SampleValidationRoutes.java +++ b/src/main/java/org/matsim/prepare/traveltime/SampleValidationRoutes.java @@ -1,9 +1,14 @@ package org.matsim.prepare.traveltime; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; +import org.apache.commons.math3.stat.descriptive.SummaryStatistics; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.matsim.api.core.v01.Coord; @@ -28,11 +33,13 @@ import org.matsim.prepare.RunOpenBerlinCalibration; import picocli.CommandLine; +import java.io.IOException; import java.nio.file.Files; -import java.nio.file.OpenOption; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; @CommandLine.Command( name = "sample-validation-routes", @@ -54,11 +61,8 @@ public class SampleValidationRoutes implements MATSimAppCommand { @CommandLine.Mixin private ShpOptions shp; - @CommandLine.Option(names = "--api", description = "API service that should be used", defaultValue = "google") - private Api api; - - @CommandLine.Option(names = "--api-key", description = "API key.") - private String apiKey; + @CommandLine.Option(names = "--api", description = "Mapping of api to key") + private Map api; @CommandLine.Option(names = "--num-routes", description = "Number of routes (per time bin)", defaultValue = "1000") private int numRoutes; @@ -94,6 +98,39 @@ public static void main(String[] args) { new SampleValidationRoutes().execute(args); } + /** + * Read the produced API files and collect the speeds by hour. + */ + public static Map> readValidation(List validationFiles) throws IOException { + + // entry to hour and list of speeds + Map> entries = new LinkedHashMap<>(); + + for (String file : validationFiles) { + + log.info("Loading {}", file); + + try (CSVParser parser = new CSVParser(Files.newBufferedReader(Path.of(file)), + CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build())) { + + for (CSVRecord r : parser) { + FromToNodes e = new FromToNodes(Id.createNodeId(r.get("from_node")), Id.createNodeId(r.get("to_node"))); + double speed = Double.parseDouble(r.get("dist")) / Double.parseDouble(r.get("travel_time")); + + if (!Double.isFinite(speed)) { + log.warn("Invalid entry {}", r); + continue; + } + + Int2ObjectMap perHour = entries.computeIfAbsent(e, (k) -> new Int2ObjectLinkedOpenHashMap<>()); + perHour.computeIfAbsent(Integer.parseInt(r.get("hour")), k -> new DoubleArrayList()).add(speed); + } + } + } + + return entries; + } + @Override @SuppressWarnings({"IllegalCatch", "NestedTryDepth"}) public Integer call() throws Exception { @@ -120,83 +157,59 @@ public Integer call() throws Exception { } } - Path out = Path.of(output.getPath().toString().replace(".csv", "-api-" + api + ".csv")); - - if (api == Api.none) { + if (api.isEmpty()) { log.info("Not querying API."); return 0; } - // Collect existing entries to support resuming - Set entries = new HashSet<>(); - OpenOption open; - if (Files.exists(out)) { - open = StandardOpenOption.APPEND; - try (CSVParser csv = new CSVParser(Files.newBufferedReader(out), CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build())) { - for (CSVRecord r : csv) { - entries.add(new Entry( - Id.createNodeId(r.get("from_node")), - Id.createNodeId(r.get("to_node")), - r.get("api"), - Integer.parseInt(r.get("hour"))) - ); - } + List files = new ArrayList<>(); + + log.info("Fetching APIs: {}", api.keySet()); + + // Run all services in parallel + try (ExecutorService executor = Executors.newCachedThreadPool()) { + List> futures = new ArrayList<>(); + + for (Map.Entry e : api.entrySet()) { + + Path out = Path.of(output.getPath().toString().replace(".csv", "-api-" + api + ".csv")); + futures.add( + CompletableFuture.runAsync(new FetchRoutesTask(e.getKey(), e.getValue(), routes, hours, out), executor) + ); + + files.add(out.toString()); } - } else - open = StandardOpenOption.CREATE_NEW; + CompletableFuture all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + all.join(); + } + Map> res = readValidation(files); - try (RouteValidator val = switch (api) { - case google -> new GoogleRouteValidator(apiKey); - case woosmap -> new WoosMapRouteValidator(apiKey); - case mapbox -> new MapboxRouteValidator(apiKey); - case here -> new HereRouteValidator(apiKey); - case tomtom -> new TomTomRouteValidator(apiKey); - case none -> null; - }) { + Path ref = Path.of(output.getPath().toString().replace(".csv", "-ref.csv")); - try (CSVPrinter csv = new CSVPrinter(Files.newBufferedWriter(out, open), CSVFormat.DEFAULT)) { + // Write the reference file + try (CSVPrinter printer = new CSVPrinter(Files.newBufferedWriter(ref), CSVFormat.DEFAULT)) { + printer.printRecord("from_node", "to_node", "hour", "min", "max", "mean", "std"); - if (open == StandardOpenOption.CREATE_NEW) { - csv.printRecord("from_node", "to_node", "api", "hour", "dist", "travel_time"); - csv.flush(); - } + // Target values + for (Map.Entry> e : res.entrySet()) { - int i = 0; - for (Route route : routes) { - for (int h : hours) { - - // Skip entries already present - Entry e = new Entry(route.fromNode, route.toNode, val.name(), h); - if (entries.contains(e)) - continue; - - try { - RouteValidator.Result res = val.calculate(route.from, route.to, h); - csv.printRecord(route.fromNode, route.toNode, val.name(), h, res.dist(), res.travelTime()); - } catch (Exception ex) { - log.warn("Could not retrieve result for route {} (retrying)", route, ex); - - try { - Thread.sleep(30_000); - RouteValidator.Result res = val.calculate(route.from, route.to, h); - csv.printRecord(route.fromNode, route.toNode, val.name(), h, res.dist(), res.travelTime()); - } catch (Exception ex2) { - log.error("Failed to retrieve result for {}", route, ex); - } - } - } + Int2ObjectMap perHour = e.getValue(); + for (Int2ObjectMap.Entry e2 : perHour.int2ObjectEntrySet()) { - csv.flush(); + SummaryStatistics stats = new SummaryStatistics(); + // This is as kmh + e2.getValue().forEach(v -> stats.addValue(v * 3.6)); - if (i++ % 50 == 0) { - log.info("Processed {} routes", i - 1); - } + printer.printRecord(e.getKey().fromNode, e.getKey().toNode, e2.getIntKey(), + stats.getMin(), stats.getMax(), stats.getMean(), stats.getStandardDeviation()); } } } + log.info("All done."); + return 0; } @@ -255,21 +268,12 @@ private List sampleRoutes(Network network, LeastCostPathCalculator router } /** - * Defines different available API services. + * Key as pair of from and to node. */ - public enum Api { - none, - google, - woosmap, - mapbox, - here, - tomtom - } - - private record Route(Id fromNode, Id toNode, Coord from, Coord to, double travelTime, double dist) { + public record FromToNodes(Id fromNode, Id toNode) { } - private record Entry(Id fromNode, Id toNode, String api, int hour) { + record Route(Id fromNode, Id toNode, Coord from, Coord to, double travelTime, double dist) { } } diff --git a/src/main/java/org/matsim/prepare/traveltime/AbstractRouteValidator.java b/src/main/java/org/matsim/prepare/traveltime/api/AbstractRouteValidator.java similarity index 96% rename from src/main/java/org/matsim/prepare/traveltime/AbstractRouteValidator.java rename to src/main/java/org/matsim/prepare/traveltime/api/AbstractRouteValidator.java index d105b62c..0f86d237 100644 --- a/src/main/java/org/matsim/prepare/traveltime/AbstractRouteValidator.java +++ b/src/main/java/org/matsim/prepare/traveltime/api/AbstractRouteValidator.java @@ -1,4 +1,4 @@ -package org.matsim.prepare.traveltime; +package org.matsim.prepare.traveltime.api; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; diff --git a/src/main/java/org/matsim/prepare/traveltime/GoogleRouteValidator.java b/src/main/java/org/matsim/prepare/traveltime/api/GoogleRouteValidator.java similarity index 95% rename from src/main/java/org/matsim/prepare/traveltime/GoogleRouteValidator.java rename to src/main/java/org/matsim/prepare/traveltime/api/GoogleRouteValidator.java index a823c019..633384eb 100644 --- a/src/main/java/org/matsim/prepare/traveltime/GoogleRouteValidator.java +++ b/src/main/java/org/matsim/prepare/traveltime/api/GoogleRouteValidator.java @@ -1,4 +1,4 @@ -package org.matsim.prepare.traveltime; +package org.matsim.prepare.traveltime.api; import com.fasterxml.jackson.databind.JsonNode; import org.apache.hc.core5.http.ContentType; @@ -27,7 +27,7 @@ public String name() { } @Override - public Result calculate(Coord from, Coord to, int hour) { + public Result retrieve(Coord from, Coord to, int hour) { try { diff --git a/src/main/java/org/matsim/prepare/traveltime/HereRouteValidator.java b/src/main/java/org/matsim/prepare/traveltime/api/HereRouteValidator.java similarity index 95% rename from src/main/java/org/matsim/prepare/traveltime/HereRouteValidator.java rename to src/main/java/org/matsim/prepare/traveltime/api/HereRouteValidator.java index e8e13eec..7ed8370d 100644 --- a/src/main/java/org/matsim/prepare/traveltime/HereRouteValidator.java +++ b/src/main/java/org/matsim/prepare/traveltime/api/HereRouteValidator.java @@ -1,4 +1,4 @@ -package org.matsim.prepare.traveltime; +package org.matsim.prepare.traveltime.api; import com.fasterxml.jackson.databind.JsonNode; import org.apache.hc.core5.http.ClassicHttpRequest; @@ -29,7 +29,7 @@ public String name() { } @Override - public Result calculate(Coord from, Coord to, int hour) { + public Result retrieve(Coord from, Coord to, int hour) { // Rate limit of 10 request per seconds try { diff --git a/src/main/java/org/matsim/prepare/traveltime/MapboxRouteValidator.java b/src/main/java/org/matsim/prepare/traveltime/api/MapboxRouteValidator.java similarity index 94% rename from src/main/java/org/matsim/prepare/traveltime/MapboxRouteValidator.java rename to src/main/java/org/matsim/prepare/traveltime/api/MapboxRouteValidator.java index 67ccc615..db9d3dce 100644 --- a/src/main/java/org/matsim/prepare/traveltime/MapboxRouteValidator.java +++ b/src/main/java/org/matsim/prepare/traveltime/api/MapboxRouteValidator.java @@ -1,4 +1,4 @@ -package org.matsim.prepare.traveltime; +package org.matsim.prepare.traveltime.api; import com.fasterxml.jackson.databind.JsonNode; import org.apache.hc.core5.http.ClassicHttpRequest; @@ -26,7 +26,7 @@ public String name() { } @Override - public Result calculate(Coord from, Coord to, int hour) { + public Result retrieve(Coord from, Coord to, int hour) { // https://docs.mapbox.com/api/overview/#rate-limits // 300 per minute diff --git a/src/main/java/org/matsim/prepare/traveltime/RouteValidator.java b/src/main/java/org/matsim/prepare/traveltime/api/RouteValidator.java similarity index 85% rename from src/main/java/org/matsim/prepare/traveltime/RouteValidator.java rename to src/main/java/org/matsim/prepare/traveltime/api/RouteValidator.java index 7760f4d1..6ec52588 100644 --- a/src/main/java/org/matsim/prepare/traveltime/RouteValidator.java +++ b/src/main/java/org/matsim/prepare/traveltime/api/RouteValidator.java @@ -1,4 +1,4 @@ -package org.matsim.prepare.traveltime; +package org.matsim.prepare.traveltime.api; import org.matsim.api.core.v01.Coord; @@ -34,10 +34,12 @@ static LocalDateTime createLocalDateTime(int hour) { /** * Calculate route information between two coordinates. Coordinates are always in WGS84. */ - Result calculate(Coord from, Coord to, int hour); + Result retrieve(Coord from, Coord to, int hour); /** * Result for one query. + * @param travelTime travel time in seconds + * @param dist distance of route in meter */ record Result(int hour, int travelTime, int dist) { } diff --git a/src/main/java/org/matsim/prepare/traveltime/TomTomRouteValidator.java b/src/main/java/org/matsim/prepare/traveltime/api/TomTomRouteValidator.java similarity index 94% rename from src/main/java/org/matsim/prepare/traveltime/TomTomRouteValidator.java rename to src/main/java/org/matsim/prepare/traveltime/api/TomTomRouteValidator.java index 34412c62..af09e032 100644 --- a/src/main/java/org/matsim/prepare/traveltime/TomTomRouteValidator.java +++ b/src/main/java/org/matsim/prepare/traveltime/api/TomTomRouteValidator.java @@ -1,4 +1,4 @@ -package org.matsim.prepare.traveltime; +package org.matsim.prepare.traveltime.api; import com.fasterxml.jackson.databind.JsonNode; import org.apache.hc.core5.http.ClassicHttpRequest; @@ -29,7 +29,7 @@ public String name() { } @Override - public Result calculate(Coord from, Coord to, int hour) { + public Result retrieve(Coord from, Coord to, int hour) { // Rate limit of 5 queries per seconds try { diff --git a/src/main/java/org/matsim/prepare/traveltime/WoosMapRouteValidator.java b/src/main/java/org/matsim/prepare/traveltime/api/WoosMapRouteValidator.java similarity index 94% rename from src/main/java/org/matsim/prepare/traveltime/WoosMapRouteValidator.java rename to src/main/java/org/matsim/prepare/traveltime/api/WoosMapRouteValidator.java index 8ad78a30..c52740af 100644 --- a/src/main/java/org/matsim/prepare/traveltime/WoosMapRouteValidator.java +++ b/src/main/java/org/matsim/prepare/traveltime/api/WoosMapRouteValidator.java @@ -1,4 +1,4 @@ -package org.matsim.prepare.traveltime; +package org.matsim.prepare.traveltime.api; import com.fasterxml.jackson.databind.JsonNode; import org.apache.hc.core5.http.ClassicHttpRequest; @@ -28,7 +28,7 @@ public String name() { } @Override - public Result calculate(Coord from, Coord to, int hour) { + public Result retrieve(Coord from, Coord to, int hour) { // Rate limit of 10 request per seconds try {