Road-based speed limits

Ryan Pannell 7 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 Tananaev 7 years ago

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

Ryan Pannell 7 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 Pannell 7 years ago

Changes so far:

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 Pannell 7 years ago