I write solutions to the problems I can't find much about elsewhere on the Web, also some code/script snippets that are absolutely awesome and make my life easier. Will be glad if someone finds these posts interesting and helpful!

Friday, May 6, 2011

Cloudmade routing on OSMDroid

Another mystery without much relevant results on Google search that turned out to be quite an easy task.
Cloudmade seems to make a lot of effort to provide developers with a lot of mapping features, and as I haven't found anywhere on the site how to pay them, I take it that their service is free. It seems also that they use OpenStreetMaps as a back-end, therefore (here goes the disclaimer) ROUTING IS NOT VERY ACCURATE. At least not yet, but it's being improved a lot; I've added a Starbucks shop about a month ago, and now it's visible on the map!
Among other services Cloudmade offers Routing HTTP API, which seems to be a breeze for use within HTML code (they support JSONP-style callback for script injection to go around cross-origin resource sharing restrictions). This is the one to be used in the open map view with open map controller, because Google Maps doesn't allow routing anywhere outside their domain (Google even removed routing API from Android right after the very first release!).
My implementation of the BlueLine does everything from requesting directions, all the way to drawing them on the map.
Create a class extending PathOverlay with the following code:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import javax.xml.parsers.SAXParserFactory;
import org.osmdroid.tileprovider.util.CloudmadeUtil;
import org.osmdroid.views.overlay.PathOverlay;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import android.content.Context;
import android.graphics.Point;
public class BlueLine extends PathOverlay {
String CMURL;
public BlueLine(int lineColor, Context ctx) {
super(lineColor, ctx);
getPaint().setStrokeWidth(5f);
CloudmadeUtil.retrieveCloudmadeKey(ctx);
//prebuild the URL http://routes.cloudmade.com/YOUR-API-KEY-GOES-HERE/api/0.3/PARAMS-GO-HERE
CMURL = "http://routes.cloudmade.com/" + CloudmadeUtil.getCloudmadeKey() + "/api/0.3/";
}
double endLat, endLng;
public void showDirectionsToPredefinedLocation(double startLat, double startLng, boolean shortest, String lang, boolean metric) throws Exception {
showDirections(startLat, startLng, endLat, endLng, shortest, lang, metric);
}
/**
* Request directions from a point to another point.
* Line is drawn automatically in the draw method, need to just request directions and parse the XML into the List of Point's.
* @param startLat Latitude of starting point
* @param startLng Longitude of starting point
* @param endLat Latitude of ending point
* @param endLng Longitude of ending point
* @param shortest Whether select shortest or fastest route
* @param lang 2-char ISO code of the language of directions
* @param metric Whether use metric or imperial system
* @throws Exception
*/
public void showDirections(double startLat, double startLng, double endLat, double endLng, boolean shortest, String lang, boolean metric) throws Exception {
/*
* Query up the routing service to figure out the turn points
* documentation for cloudmade routing service http://developers.cloudmade.com/wiki/routing-http-api/Documentation
*/
//add PARAMS to the URL
//start_point,[transit_point1,...,transit_pointN],end_point/route_type[/route_type_modifier].output_format[?lang=(Two letter ISO 3166-1 code)][&units=(km|miles)]
CMURL += startLat + "," + startLng + "," + endLat + "," + endLng
+ "/car/" + (shortest? "shortest" : "fastest") + ".gpx?lang=" + lang + "&units=" + (metric? "km" : "miles");
//fetch directions and parse returned XML
String directionsXML = getContent(CMURL, null, "UTF8");
//parse XML
TurnPointsParser parser = new TurnPointsParser();
XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
xmlReader.setContentHandler(parser);
if(!"".equals(directionsXML)) {
xmlReader.parse(new InputSource(new StringReader(directionsXML)));
}
}
public class TurnPointsParser extends DefaultHandler {
public static final String ROOT_ELEMENT = "wpt";
ArrayList<Point> tempPoints = new ArrayList<Point>();
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
if(qName.equals(ROOT_ELEMENT)) {
try {
int late6 = degreesToMicrodegrees(Double.parseDouble(atts.getValue("lat")));
int lnge6 = degreesToMicrodegrees(Double.parseDouble(atts.getValue("lon")));
tempPoints.add(new Point(late6, lnge6));
}
catch(Exception e) {
e.printStackTrace();
}
}
}
@Override
public void endDocument() {
setPoints(tempPoints);
}
}
void setPoints(ArrayList<Point> points) {
clearPath();
for(Point p : points) {
addPoint(p.x, p.y);
}
}
/**
* Fetch the content of service at the given URL with provided parameters.
* Also transcode the input into UTF-8 according to specified charset used by the service.
* @param serviceUrl
* @param requestParams
* @param serviceCharset
* @return String with fetched content
* @throws Exception
*/
public static String getContent(String serviceUrl, String requestParams, String serviceCharset) throws Exception {
URL service = new URL(serviceUrl);
URLConnection serviceConnection = service.openConnection();
serviceConnection.setDoOutput(true);
//write request to the connection
OutputStreamWriter request = new OutputStreamWriter(serviceConnection.getOutputStream());
if (requestParams != null) {
request.write(requestParams);
}
request.flush();
//read returned output
BufferedReader in =
new BufferedReader(new InputStreamReader(serviceConnection.getInputStream(), serviceCharset));
String inputLine;
String returnedContent = "";
while ((inputLine = in.readLine()) != null) {
returnedContent += inputLine;
}
in.close();
request.close();
return returnedContent;
}
public static final double MICRODEGREES_COEFF = 1E6;
public static int degreesToMicrodegrees(double degrees) {
return (int) (degrees * MICRODEGREES_COEFF);
}
public static double microdegreesToDegrees(int microdegrees) {
return (double) microdegrees / (double) MICRODEGREES_COEFF;
}
public void setEndLat(double endLat) {
this.endLat = endLat;
}
public void setEndLng(double endLng) {
this.endLng = endLng;
}
}
view raw BlueLine.java hosted with ❤ by GitHub

The Cloudmade API key goes into the manifest file within the application element like here
<application android:name=".App"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:debuggable="true"
android:theme="@android:style/Theme.NoTitleBar">
<meta-data android:name="CLOUDMADE_KEY" android:value="API key obtained from Cloudmade"/>
.
.
.
</application>
view raw gistfile1.xml hosted with ❤ by GitHub

And that's pretty much it. It's very easy to make it work now with the application:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
.
.
.
BlueLine blueLine = new BlueLine(Color.BLUE, this);
}
Location lastKnownLocation;
//this activity implements LocationListener and has registered for location updates with the system
@Override
public void onLocationChanged(Location loc) {
lastKnownLocation = loc; //save latest location
}
protected void showDirectionsTo(double endLat, double endLng) {
//the routing will be requested from another thread, need to make coordinates accessible from there
blueLine.setEndLat(endLat);
blueLine.setEndLng(endLng);
new Handler().post(new Runnable() {
public void run() {
try {
//will always request directions from current location, destination was assigned previously
blueLine.showDirectionsToPredefinedLocation(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude(), false, "en", true);
mapView.getOverlays().add(blueLine);
}
catch(Exception e) {
e.printStackTrace();
}
}
});
}
view raw gistfile1.java hosted with ❤ by GitHub



That's really it! Hope it helps.

7 comments:

  1. Hi Elijah, I use your code in my android application. There is no error but I can not see route. I do not understand what the problem is. On the other hand start point is GPS location point, it is Ok, but I do not understand how do we determine the destination points. Please say what are my mistakes. Thank you for your atttention.

    ReplyDelete
  2. To determine a destination point from an address you can use Geocoding (http://code.google.com/apis/maps/documentation/geocoding/). That is, if that's your problem. Other than that, just for the sake of an example, drop a random coordinates pair into this method.
    To be honest I don't really see what is the exact issue you are running into. Would you like me to take a look at your code?

    ReplyDelete
  3. Hi Elijah, I created a class (BlueLine Class) with your code and then I try to use this class in my application code. My code is running but the route application could not start. I get start point coordinates by GPS, I used random coordinates pair for destination point for testing but the route application could not start too. I would like you to examine my code. But I need your mail adress because I can not send my code in this platform because of limitation. My email adress is hzselvi@yahoo.com. Thank you for your attention.

    ReplyDelete
  4. Just bear in mind that start and end coordinates have to have the same format (if I remember correctly, it has to be in the same format as the one used by the GPS receiver, e.g. [37.9, -122.5] for San Francisco). Just watch out to avoid converting them to/from microdegrees, as the Android platform uses latter.
    As a test, create a link to the routing service with required coordinates and insert it into the browser to see what the service returns.

    ReplyDelete
  5. For testing, I create a link and insert it into the browser and service returns gpx file to me. But I can not succeed in android application. There is no error, application runs well but route line can not be shown. I think I have a defect but I do not know what it is.
    And I can not understand this sentence. Which thread?
    "the routing will be requested from another thread, need to make coordinates accessible from there".
    Thank you for your interest.

    I can not understand this sentence.

    ReplyDelete
  6. this example is missing the Draw method, right?

    ReplyDelete
  7. I have tried to implement your codes into my program sir Elijah. But i encountered a lot of issues during execution example, force close. I posted a question on stackoverflow, please check it out: http://stackoverflow.com/questions/8728737/shortest-path-calculation-on-maps-using-osmdroid-library


    Sir please help me this. here's my email: lordzden_1991@yahoo.com

    ReplyDelete