package com.jakehilborn.speedr.overpass; import android.util.Log; import com.crashlytics.android.Crashlytics; import com.jakehilborn.speedr.StatsCalculator; import com.jakehilborn.speedr.overpass.deserial.Element; import com.jakehilborn.speedr.overpass.deserial.OverpassResponse; import com.jakehilborn.speedr.utils.UnitUtils; import java.util.HashSet; import java.util.Set; public class OverpassManager { private final StatsCalculator statsCalculator; private String prevRoadName; private static final String NO_DATA = "no data"; public OverpassManager(StatsCalculator statsCalculator) { this.statsCalculator = statsCalculator; } public void handleResponse(OverpassResponse overpassResponse, Double lat, Double lon) { Set<LimitNode> limitNodes = normalizeResponse(overpassResponse); if (limitNodes.size() == 0) { //OpenStreetMaps is missing data for the user's location if (prevRoadName != null && prevRoadName.equals(NO_DATA)) { //Two responses in a row with no speed limit data, set limit to missing. statsCalculator.setLimit(0D, lat, lon); } else { //Don't set limit to missing just yet, wait for the next retry. prevRoadName = NO_DATA; statsCalculator.setLimit(null, lat, lon); } } else if (limitNodes.size() == 1) { //Normal case, user is on a single road. LimitNode limitNode = limitNodes.iterator().next(); prevRoadName = limitNode.getRoadName(); statsCalculator.setLimit(limitNode.getLimit(), lat, lon); } else { //User at intersection, overpass, or near a parallel road. LimitNode sticky = new LimitNode(0D, prevRoadName); LimitNode fastest = new LimitNode(); for (LimitNode limitNode : limitNodes) { //If the current response contains 1 or more results matching the road name //of the last response then prefer the higher speed limit for that roadName. if (limitNode.getRoadName().equals(sticky.getRoadName())) { sticky.setLimit(Math.max(limitNode.getLimit(), sticky.getLimit())); } //Of all the results in the response find the one with the highest speed limit. if (limitNode.getLimit() > fastest.getLimit()) { fastest = limitNode; } } //Since the user is near multiple roads force a limit refetch with forceLimitStale. We will keep refetching //while the user is near multiple roads to determine asap which road the user is actually driving on. if (sticky.getLimit() != 0) { //Use the limit that matches the roadName of previous response. statsCalculator.setForceLimitStale(true); statsCalculator.setLimit(sticky.getLimit(), lat, lon); } else { //User is on a new road, use the road that has the fastest speed limit in the response. prevRoadName = fastest.getRoadName(); statsCalculator.setForceLimitStale(true); statsCalculator.setLimit(fastest.getLimit(), lat, lon); } } } private Set<LimitNode> normalizeResponse(OverpassResponse overpassResponse) { Set<LimitNode> limitNodes = new HashSet<>(); if (overpassResponse == null || overpassResponse.getElements() == null) return limitNodes; for (Element element : overpassResponse.getElements()) { Double limit = parseLimit(element.getTags().getMaxSpeed()); String roadName = element.getTags().getName(); limitNodes.add(new LimitNode(limit, roadName)); } return limitNodes; } private Double parseLimit(String limit) { if (limit == null || limit.isEmpty()) return null; Integer num; try { //Overpass maxspeed uses whole numbers. Example limit: "35 mph" num = Integer.parseInt(limit.replaceAll("[^0-9]", "")); //remove all non-digit characters } catch (NumberFormatException e) { Crashlytics.log(Log.ERROR, OverpassManager.class.getSimpleName(), "Limit missing digits: " + limit); Crashlytics.logException(e); return null; } if (limit.contains("mph")) { return UnitUtils.mphToMs(num); } else if (limit.contains("knots")) { return UnitUtils.knotsToMs(num); } else { //kph if unit is not specified in response return UnitUtils.kphToMs(num); } } }