Sunday, March 28, 2010

Using Java Map Projection Library in Android

Recently I was writing a little mapping application on Android, which basically takes Open Street Map tiles as base layer, and also has the ability to overlay some vector features on top. In this application there is need to convert coordinates back and forth between WGS84 and Mercator projection, so I need certain library to do the job. Given the popularity of PROJ.4, the pure Java port of it, Java Map Projection Library (name it “PROJ4Java” below), seems to be an obvious choice so I spend some hours during the weekend to see how it works.
First you can get the latest version (1.0.6) of PROJ4Java from this link. The size of the library is very small and lightweight with only 1.17MB for complete source code and 247KB for compiled jar file. If you’re familiar with PROJ.4 itself then this Java version should be quite straight forward, otherwise there isn’t much sample code or tutorial on its homepage. But the basic workflow is quite simple as described by its project homepage:
To use the library, you need to create a projection, either directly by calling its constructor or via one of the factory methods. Once you have a Projection, you can use it to convert between latitude/longitude and projected map units (by default, metres) by calling the transform and transformInverse methods.
A major issue of using PROJ4Java on Android is that out of box it can’t be run due to the fact that it is using some classes in java.awt.geom.* which is not available in Android SDK. But since the only major AWT class used by PROJ4Java is Point2D, it’s not difficult at all to work around by replacing it with your own 2D point class. In my case I use the point class from JTS library. After taking out all Java awt class references from source code and replacing them with JTS class, everything works pretty well on Android.
Another thing I modifies on the source code is to get rid of those projection implementations I don’t need. Mercator projection is the only one I need so far so that I am able to reduce the size of compile jar file to only 168KB. Here is a list of classes I extracted from original source code to support only Mercator projection:
    • com.jhlabs.map.*
    • com.jhlabs.map.proj.CylindricalProjection
    • com.jhlabs.map.proj.Ellipsoid
    • com.jhlabs.map.proj.MercatorProjection
    • com.jhlabs.map.proj.Projection
    • com.jhlabs.map.proj.ProjectionFactory
    • com.jhlabs.map.proj.ProjectionException
    • com.jhlabs.map.proj.Ellipsoid
ProjectionFactory class is the entry point for you to use PROJ4Java, which provides several static methods to create an instance of Projection. Below are some use cases I figured out by briefly reading through the source code:
(1) ProjectionFactory.getNamedPROJ4CoordinateSystem(String name) takes a well-known projection code like “epsg:4326” and returns a Projection:
1: String name = "epsg:3785";    
2: Projection proj = ProjectionFactory.getNamedPROJ4CoordinateSystem(name);
(2) ProjectionFactory.fromPROJ4Specification(String[] params) takes a set of PROJ.4 parameters and returns a Projection:
1: String[] params = {
2:     "proj=tmerc",
3:     "lat_0=37.5",
4:     "lon_0=-85.66666666666667",
5:     "k=0.999966667",
6:     "x_0=99999.99989839978",
7:     "y_0=249999.9998983998",
8:     "ellps=GRS80",
9:     "datum=NAD83",
10:     "to_meter=0.3048006096012192",
11:     "no_defs"
12: };
13: ProjectionFactory.fromPROJ4Specification(params);
(3) Create an instance of customized projection. All supported projection definition strings (PROJ.4 parameters for ) for PROJ4Java are listed in text files under folder “nad” that is coming with the library. These files are “epsg”, “esri”, “nad27”, “nad83”, and “world”, and a sample projection definition string is like below:
1: <2965> +proj=tmerc +lat_0=37.5 +lon_0=-85.66666666666667 +k=0.999966667 +x_0=99999.99989839978 +y_0=249999.9998983998 +ellps=GRS80 +datum=NAD83 +to_meter=0.3048006096012192 +no_defs
2: 
To add a new customized projection just create a definition string (usually modify certain parameter values upon an existing projection) and add it into one of those files, or create a new text file under “nad” folder with the definition string, then use the code below:
1: Projection proj = null;
2: try {
3:     // assume that "epsg:900913" projection defintion 
4:     //   has been added in text file "others"
5:     proj = ProjectionFactory.readProjectionFile("others", "900913");    
6: } catch(IOException e) {
7:     e.printStackTrace();
8: }
(4) Transform between lat/lon and projection units with a Projection instance:
1: Projection epsg3785 = ProjectionFactory.getNamedPROJ4CoordinateSystem("epsg:3785");    
2:               
3: System.out.println("transform from latlon to epsg:3785");
4: System.out.println("latlon: -117.5931084, 34.1063989");
5: Point pEpsg3785 = epsg3785.transform(-117.5931084, 34.1063989);
6: System.out.println("epsg:3785: " + pEpsg3785.getX() + ", " + pEpsg3785.getY());
7:     
8: System.out.println("transform from epsg:3785 to latlon");
9: System.out.println("epsg:3785: " + pEpsg3785.getX() + ", " + pEpsg3785.getY());
10: Point latlon = epsg3785.inverseTransform(pEpsg3785);
11: System.out.println("latlon: " + latlon.getX() + ", " + latlon.getY());
12: 
13:   

7 comments:

  1. String [] proj4_w = new String [] {
    "+ Proj = tmerc",
    "+ Lat_0 = 38N",
    "+ Lon_0 = 127.00289027777777777776E",
    "+ Ellps = bessel",
    "+ Units = m",
    "+ X_0 = 200000",
    "+ Y_0 = 500000",
    "+ K = 1.0"
    ;}

    Point2D.Double srcProjec = null;
    Point2D.Double dstProjec = null;
    Projection proj = ProjectionFactory.fromPROJ4Specification (proj4_w);

    srcProjec = new Point2D.Double (132,37);
    dstProjec = proj.transform (srcProjec, new Point2D.Double ());
    System.out.println ("TM:" + dstProjec);
    / / TM: Point2D.Double [644904.399587292, 400717.8948938238]

    srcProjec = new Point2D.Double (644904.399587292, 400717.8948938238);
    dstProjec = proj.inverseTransform (srcProjec, new Point2D.Double ());


    I am using the above code to convert the lat/lon to another projection , but while passing the params it is givving error as a null pointer exception , could u tell me whatis the mistake

    ReplyDelete
  2. I have made available a JAR compatible with Android with references to AWT classes removed. It contains source as well as .class files and is available at

    http://www.free-map.org.uk/downloads/javalib/javaproj-1.0.6-noawt.jar.

    ReplyDelete
  3. Hi,

    I am facing an issue in my app i.e. suppose i have a map view and a list view, i select an area to be searched in map view but do the actual search in list view, now i get lots of deal in response but only those POIs will be displayed which fall under the currently selected area in map view, i dont want to use the mapview projection because it can change even if someone zoom and pan around but dint select the area to search (or the final area to be searched is not changed for me but the projections has changed) so now it can affect my POIs to be displayed in list view.

    please help me in this regard.
    Thanks
    DC

    ReplyDelete
  4. I can't get the library to work (com.jhlabs.map.proj)
    Using this info:
    West_Bounding_Coordinate: -86.595961
    East_Bounding_Coordinate: -77.718276
    North_Bounding_Coordinate: 40.301023
    South_Bounding_Coordinate: 37.815679
    Map_Projection_Name: Lambert Conformal Conic
    Lambert_Conformal_Conic:
    Standard_Parallel: 38.666667
    Standard_Parallel: 33.333333
    Longitude_of_Central_Meridian: -81.450000
    Latitude_of_Projection_Origin: 38.116667
    False_Easting: 0.000000
    False_Northing: 0.000000

    I came up with this:

    String[] params = {
    "+proj=lcc", "+lat_1=38.666667", "+lat_2=33.333333",
    "+lat_0=38.116667", "+lon_0=-81.450000", "+x_0=0", "+y_0=0",
    "+ellps=GRS80", "+units=m", "+no_defs", "+datum=NAD83" };
    Projection lccProjection = ProjectionFactory.fromPROJ4Specification(params);
    lccProjection.initialize();
    point = new Point2D.Double(5000d, 500d);
    System.out.println("transform from point to latlon");
    System.out.println("point: " + point.getX() + ", " + point.getY());
    Point2D.Double latlon = lccDirect.inverseTransform(point, new Point2D.Double());
    System.out.println("latlon: " + latlon.getX() + ", " + latlon.getY());


    I chose the point (5000, 500) because when I view the TIFF there's an airport at that spot.
    The airport has GPS coordinates of - 82.89188888, 39.99797222 but the inverseTransform is returning -81.3929566, 38.12116.

    Any ideas what I'm doing wrong?

    ReplyDelete
  5. Hi,
    i have the same problem as you Doug.
    The inverseTransform method return wrong value.

    Have you find a solution to fixe it ?

    ReplyDelete
  6. hello,
    do you know how to create the transform point, because when i define the point var the softwre calls a library that not recognize.

    import com.jhlabs.map.Point2D;
    import com.jhlabs.map.proj.Projection;
    import com.jhlabs.map.proj.ProjectionFactory;
    import com.vividsolutions.jts.geom.Point;
    public class Conversion {

    String name = "epsg:3785";
    Projection origen = ProjectionFactory.getNamedPROJ4CoordinateSystem(name);





    Point pEpsg3785 = origen.transform(-117.5931084, 34.1063989); //here's the wrong.....
    public double x(){


    }

    }


    thank's

    ReplyDelete
  7. All Proj4 libraries are SHIT and are probably untested.

    ReplyDelete