Road-based speed limits

Ryan Pannell6 years ago

I've noticed a few requests for per-road speed limits and no implementation has surfaced as of yet, so I am working on this now.
To start with I am only implementing this with Nominatim Geocoder (extends jsonGeocoder).

I am extending Geocoder->Address with a speedLimit property.

Handler->Events->OverspeedEventHandler is being extended to use the following priority:

Address Limit (if available) -> Geofence Limit (if available) -> Global Limit

I welcome any suggestions or feedback before I get too dug in.

Anton Tananaev6 years ago

Approach sounds reasonable. Does geocoder return speed limit in the API response?

Ryan Pannell6 years ago

Not the default response, but you can call /details with place_id and that will have an extras->maxspeed object if applicable.

I still need to work out the unit to store in, nominatim will return km/h as a number or 'xx mph' if in mph. This will probably be related to the attributes by the look of it.

The approach I am taking is to extend Geocoder with:

    String getSpeedLimit(String way, ReverseGeocoderCallback callback);

And extend JsonGeocoder as:

    public JsonGeocoder(String url, final int cacheSize, AddressFormat addressFormat, String speedurl) {}

with a fallback for providers that do not support speed limits:

public JsonGeocoder(String url, final int cacheSize, AddressFormat addressFormat) {}

Then the providers are extended with:

private static String formatSpeedUrl(String url, String key, String language) {}

and in parseAddress call:

address.setWay(result.getString("place_id"));

Then after a succesful call

handleSpeedResponse(String way, JsonObject json, ReverseGeocoderCallback callback){}

will format the speed and return it.

Then I need to figure out how to link that to the device attribute... getting there.

Ryan Pannell6 years ago

Changes so far:
``` java
diff --git a/src/org/traccar/geocoder/Address.java b/src/org/traccar/geocoder/Address.java
index fe39da8e..ea2570c1 100644
--- a/src/org/traccar/geocoder/Address.java
+++ b/src/org/traccar/geocoder/Address.java
@@ -106,5 +106,24 @@ public class Address {
public void setFormattedAddress(String formattedAddress) {
this.formattedAddress = formattedAddress;
}
+
+ private String speedLimit;

+ public String getSpeedLimit() {
+ return speedLimit;
+ }
+
+ public void setSpeedLimit(String speedLimit) {
+ this.speedLimit = speedLimit;
+ }
+
+ private String way;
+
+ public String getWay() {
+ return way;
+ }
+
+ public void setWay(String way) {
+ this.way = way;
+ }
}
diff --git a/src/org/traccar/geocoder/AddressFormat.java b/src/org/traccar/geocoder/AddressFormat.java
index ad19432b..08c18193 100644
--- a/src/org/traccar/geocoder/AddressFormat.java
+++ b/src/org/traccar/geocoder/AddressFormat.java
@@ -68,6 +68,7 @@ public class AddressFormat extends Format {
result = replace(result, "%r", address.getStreet());
result = replace(result, "%h", address.getHouse());
result = replace(result, "%f", address.getFormattedAddress());
+ result = replace(result, "%z", address.getSpeedLimit());

result = result.replaceAll("^[, ]*", "");

diff --git a/src/org/traccar/geocoder/Geocoder.java b/src/org/traccar/geocoder/Geocoder.java
index 587a2752..460cf6b1 100644
--- a/src/org/traccar/geocoder/Geocoder.java
+++ b/src/org/traccar/geocoder/Geocoder.java
@@ -26,5 +26,6 @@ public interface Geocoder {
}

String getAddress(double latitude, double longitude, ReverseGeocoderCallback callback);
+ String getSpeedLimit(String way, ReverseGeocoderCallback callback);

}
diff --git a/src/org/traccar/geocoder/JsonGeocoder.java b/src/org/traccar/geocoder/JsonGeocoder.java
index ed59a1d8..bcd1581f 100644
--- a/src/org/traccar/geocoder/JsonGeocoder.java
+++ b/src/org/traccar/geocoder/JsonGeocoder.java
@@ -33,9 +33,31 @@ public abstract class JsonGeocoder implements Geocoder {
private static final Logger LOGGER = LoggerFactory.getLogger(JsonGeocoder.class);

private final String url;
+ private String speedurl = null;
private final AddressFormat addressFormat;

private Map<Map.Entry<Double, Double>, String> cache;
+ private Map<String, String> speedcache;
+
+ public JsonGeocoder(String url, final int cacheSize, AddressFormat addressFormat, String speedurl) {
+ this.url = url;
+ this.speedurl = speedurl;
+ this.addressFormat = addressFormat;
+ if (cacheSize > 0) {
+ this.cache = Collections.synchronizedMap(new LinkedHashMap<Map.Entry<Double, Double>, String>() {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > cacheSize;
+ }
+ });
+ this.speedcache = Collections.synchronizedMap(new LinkedHashMap<String, String>() {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > cacheSize;
+ }
+ });
+ }
+ }

public JsonGeocoder(String url, final int cacheSize, AddressFormat addressFormat) {
this.url = url;
@@ -73,6 +95,23 @@ public abstract class JsonGeocoder implements Geocoder {
}
return null;
}
+
+ private String handleSpeedResponse(
+ String way, JsonObject json, ReverseGeocoderCallback callback) {
+
+ Address address = parseAddress(json);
+ if (address != null) {
+ String formattedSpeed = addressFormat.format(address);
+ if (speedcache != null) {
+ speedcache.put(way, formattedSpeed);
+ }
+ if (callback != null) {
+ callback.onSuccess(formattedSpeed);
+ }
+ return formattedSpeed;
+ }
+ return null;
+ }

@Override
public String getAddress(
@@ -111,6 +150,44 @@ public abstract class JsonGeocoder implements Geocoder {
}
return null;
}
+
+ @Override
+ public String getSpeedLimit(
+ final String way, final ReverseGeocoderCallback callback) {
+
+ if (speedcache != null) {
+ String cachedAddress = speedcache.get(way);
+ if (cachedAddress != null) {
+ if (callback != null) {
+ callback.onSuccess(cachedAddress);
+ }
+ return cachedAddress;
+ }
+ }
+
+ Invocation.Builder request = Context.getClient().target(String.format(url, way)).request();
+
+ if (callback != null) {
+ request.async().get(new InvocationCallback<JsonObject>() {
+ @Override
+ public void completed(JsonObject json) {
+ handleSpeedResponse(way, json, callback);
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ callback.onFailure(throwable);
+ }
+ });
+ } else {
+ try {
+ return handleSpeedResponse(way, request.get(JsonObject.class), null);
+ } catch (ClientErrorException e) {
+ LOGGER.warn("Geocoder network error", e);
+ }
+ }
+ return null;
+ }

public abstract Address parseAddress(JsonObject json);

diff --git a/src/org/traccar/geocoder/NominatimGeocoder.java b/src/org/traccar/geocoder/NominatimGeocoder.java
index 8db25bf1..94e66a4c 100644
--- a/src/org/traccar/geocoder/NominatimGeocoder.java
+++ b/src/org/traccar/geocoder/NominatimGeocoder.java
@@ -16,14 +16,33 @@
package org.traccar.geocoder;

import javax.json.JsonObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+

public class NominatimGeocoder extends JsonGeocoder {

+ private static final Logger LOGGER = LoggerFactory.getLogger(NominatimGeocoder.class);
+
private static String formatUrl(String url, String key, String language) {
if (url == null) {
- url = "https://nominatim.openstreetmap.org/reverse";
+ url = "https://nominatim.openstreetmap.org/";
+ }
+ url += "reverse?format=json&lat=%f&lon=%f&zoom=18&addressdetails=1";
+ if (key != null) {
+ url += "&key=" + key;
+ }
+ if (language != null) {
+ url += "&accept-language=" + language;
+ }
+ return url;
+ }
+
+ private static String formatSpeedUrl(String url, String key, String language) {
+ if (url == null) {
+ url = "https://nominatim.openstreetmap.org/";
}
- url += "?format=json&lat=%f&lon=%f&zoom=18&addressdetails=1";
+ url += "details?format=json&place_id=%f";
if (key != null) {
url += "&key=" + key;
}
@@ -34,16 +53,31 @@ public class NominatimGeocoder extends JsonGeocoder {
}

public NominatimGeocoder(String url, String key, String language, int cacheSize, AddressFormat addressFormat) {
- super(formatUrl(url, key, language), cacheSize, addressFormat);
+ super(formatUrl(url, key, language), cacheSize, addressFormat, formatSpeedUrl(url,key,language));
+ }
+
+ public static void printJsonObject(JsonObject jsonObj) {
+ for (Object key : jsonObj.keySet()) {
+ String keyStr = (String) key;
+ Object keyvalue = jsonObj.get(keyStr);
+ LOGGER.info("NOMINATIM: key: " + keyStr + " value: " + keyvalue);
+ if (keyvalue instanceof JsonObject) {
+ LOGGER.info("NOMINATIM: Recursed:");
+ printJsonObject((JsonObject) keyvalue);
+ LOGGER.info("NOMINATIM: End Recursed.");
+ }
+ }
}
ic Address parseAddress(JsonObject json) {
+
JsonObject result = json.getJsonObject("address");

if (result != null) {
+ LOGGER.info("Geocode called.");
Address address = new Address();

  •        printJsonObject(result);
           if (json.containsKey("display_name")) {
               address.setFormattedAddress(json.getString("display_name"));
           }

@@ -81,6 +115,9 @@ public class NominatimGeocoder extends JsonGeocoder {
if (result.containsKey("postcode")) {
address.setPostcode(result.getString("postcode"));
}

  •        if (result.containsKey("place_id")) {
  •            address.setWay(result.getString("place_id"));
  •        }
    
           return address;
       }
    @Override
    public Address parseAddress(JsonObject json) {
  •     JsonObject result = json.getJsonObject("address");
    
        if (result != null) {
  •        LOGGER.info("Geocode called.");
           Address address = new Address();
  •        printJsonObject(result);
           if (json.containsKey("display_name")) {
               address.setFormattedAddress(json.getString("display_name"));
           }

@@ -81,6 +115,9 @@ public class NominatimGeocoder extends JsonGeocoder {
if (result.containsKey("postcode")) {
address.setPostcode(result.getString("postcode"));
}

  •        if (result.containsKey("place_id")) {
  •            address.setWay(result.getString("place_id"));
  •        }
    
           return address;
       }
Ryan Pannell6 years ago