package com.jakehilborn.speedr.overpass;
import android.util.Log;
import com.crashlytics.android.Crashlytics;
import com.jakehilborn.speedr.LimitFetcher;
import com.jakehilborn.speedr.utils.ErrorReporter;
import com.jakehilborn.speedr.utils.UnitUtils;
import java.io.IOException;
import java.net.HttpURLConnection;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class OverpassInterceptor implements Interceptor {
private Server DE; //Primary
private Server RU; //Primary
private Server FR; //Secondary
private Server COM; //Secondary - Thank you to Daniel Ciao for letting me use the personal server he set up for Velociraptor: https://github.com/plusCubed/velociraptor
public OverpassInterceptor() {
DE = new Server("https://overpass-api.de/api/interpreter");
RU = new Server("http://overpass.osm.rambler.ru/cgi/interpreter");
FR = new Server("https://api.openstreetmap.fr/oapi/interpreter");
COM = new Server("http://overpass.pluscubed.com/api/interpreter");
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String query = request.url().encodedQuery();
Server overpassServer = chooseServer();
Crashlytics.log(Log.INFO, OverpassInterceptor.class.getSimpleName(), "Using server " + overpassServer.getBaseUrl());
request = request.newBuilder()
.url(overpassServer.getBaseUrl() + "?" + query)
.addHeader("Connection", "keep-alive")
.addHeader("DNT", "1")
.addHeader("Upgrade-Insecure-Requests", "1")
.addHeader("User-Agent", LimitFetcher.USER_AGENT)
.build();
overpassServer.setDelay(System.nanoTime());
Response response;
try {
response = chain.proceed(request);
} catch (Throwable error) {
ErrorReporter.logOverpassError(error, overpassServer.getBaseUrl());
overpassServer.setDelay(System.nanoTime() + UnitUtils.secondsToNanos(60)); //Don't retry this server for 60 seconds
throw error;
}
//OkHttp response body is stored in a buffer that is consumed upon read. We consume the buffer
//into a string and then re-insert it into the response so that Gson can deserialize later.
String bodyString = response.body().string();
ResponseBody rebuildBody = ResponseBody.create(response.body().contentType(), bodyString);
response = response.newBuilder().body(rebuildBody).build();
//Catches non-200 responses, empty 200 responses, and 200 responses that contain warning/error strings.
//Requests for coordinates that don't return any data will still have an empty "elements" array.
if (response.code() == HttpURLConnection.HTTP_OK) {
if (bodyString.contains("\"elements\"")) { //success
overpassServer.addLatency(response.receivedResponseAtMillis() - response.sentRequestAtMillis());
} else {
overpassServer.setDelay(System.nanoTime() + UnitUtils.secondsToNanos(60)); //Don't retry this server for 60 seconds
ErrorReporter.logOverpassError(response.code(), overpassServer.getBaseUrl(), bodyString);
}
} else {
overpassServer.setDelay(System.nanoTime() + UnitUtils.secondsToNanos(60)); //Don't retry this server for 60 seconds
ErrorReporter.logOverpassError(response.code(), overpassServer.getBaseUrl(), bodyString);
}
return response;
}
//Round robin between primary servers DE and RU. However, if either DE or RU is 5 times slower
//than the other then use the faster of DE,RU and don't retry the slow one for 60s. We also clear
//the last 5 stored latencies for the slower server. If both DE and RU have been penalized due to
//slowness/errors then use whichever server has the least recent penalty.
private Server chooseServer() {
if (DE.getDelay() <= System.nanoTime() || RU.getDelay() <= System.nanoTime()) { //Use primary
if (DE.getLatency() == 0 || RU.getLatency() == 0) {
//Latencies can't be compared, use return value below.
} else if (DE.getLatency() / RU.getLatency() >= 5) {
DE.clearLatencies();
DE.setDelay(System.nanoTime() + UnitUtils.secondsToNanos(60)); //Don't retry this server for 60 seconds
return RU;
} else if (RU.getLatency() / DE.getLatency() >= 5) {
RU.clearLatencies();
RU.setDelay(System.nanoTime() + UnitUtils.secondsToNanos(60)); //Don't retry this server for 60 seconds
return DE;
}
return DE.getDelay() <= RU.getDelay() ? DE : RU;
} else { //Use whichever server has been penalized least recently, this is a secondary server in most cases
Server leastDelayed = COM;
if (DE.getDelay() <= leastDelayed.getDelay()) leastDelayed = DE;
if (RU.getDelay() <= leastDelayed.getDelay()) leastDelayed = RU;
if (FR.getDelay() <= leastDelayed.getDelay()) leastDelayed = FR;
return leastDelayed;
}
}
}