Fabrizio Giudici is a Senior Java Architect with a long Java experience in the industrial field. He runs Tidalwave, his own consultancy company, and has contributed to Java success stories in a number of fields, including Formula One. Fabrizio often appears as a speaker at international Java conferences such as JavaOne and Devoxx and is member of JUG Milano and the NetBeans Dream Team. Fabrizio is a DZone MVB and is not an employee of DZone and has posted 67 posts at DZone. You can read more from them at their website. View Full User Profile

Implementing a Common-Profile Map Renderer for JavaFX (Part 1: the Model)

06.15.2009
| 8088 views |
  • submit to reddit
If you look at one of the JavaFX demos at javafx.com, you'll find something with a map renderer. While it works fine, unfortunately the demo is wrapping the JXMapViewer component from SwingX, that can work only in the desktop profile; in other words, it won't work on a mobile phone.

As part of the blueBill Mobile project, I've developed a clean implementation of a map renderer, based on the common profile, thus able to run everywhere, including on a mobile phone. This library is flexible and customizable, and of course includes the cability of overlaying layers with various kinds of information.

The work is basically a port of a similar API for JME which is part of windRose, with some specific refactoring for JavaFX (and also some generic refactoring that the JME library has to catch up).

Because of this bug, I haven't been able yet to spin a subproject off blueBill Mobile. You can check out the sources I'm describing from:

svn co -r 167 https://kenai.com/svn/bluebill-mobile~svn/trunk/src/FX/blueBill-mobileFX/src/it/tidalwave/geo/mapfx

The JME counterpart can be checked out from:

svn co -r 490 https://windrose.dev.java.net/svn/windrose/trunk/src/MobileMaps


In this first article I'm going to describe the easier part of the library, that is its model.

 

Point, Coordinates, Sector

The first model classes are quite obvious:

  • Point: represents a point in a cartesian system with integer coordinates. It can both represent a point of the rendered map and as point in the bitmap of the map.
  • Coordinates: represents a point in geographic coordinates (latitude, longitude, altitude).
  • Sector: represents a geographic area delimited by min/max latitude and min/max longitude.

There's not much to say, but a short note for Coordinates. JSR-179 (Location API) provides a valid implementation of this concept, that I initially used for my JavaFX code. The reason for which I preferred a fresh implementation is because in this way I can fully enjoy JavaFX binding.

package it.tidalwave.geo.mapfx.model;

public class Point
{
public var x: Integer;
public var y: Integer;

public function withTranslation (deltaX : Integer, deltaY : Integer)
{
return Point
{
x: x + deltaX
y: y + deltaY
};
}

public function withAntiTranslation (deltaX : Integer, deltaY : Integer)
{
return Point
{
x: x - deltaX
y: y - deltaY
};
}

public function withTranslation (point : Point)
{
return withTranslation(point.x, point.y);
}

public function withAntiTranslation (point : Point)
{
return withAntiTranslation(point.x, point.y);
}

override function toString()
{
return "Point[{x};{y}]";
}
}

package it.tidalwave.geo.mapfx.model;

public class Coordinates
{
public var latitude : Number = 0;
public var longitude : Number = 0;
public var altitude : Number = 0;

override function toString()
{
return "Coordinates[{latitude},{longitude},{altitude}]";
}
}


package it.tidalwave.geo.mapfx.model;

public class Sector
{
public var minLatitude : Number = 0;
public var maxLatitude : Number = 0;
public var minLongitude : Number = 0;
public var maxLongitude : Number = 0;

override function toString()
{
return "Sector[{minLatitude},{minLongitude} -> {maxLatitude},{maxLongitude}]";
}
}

 

Projection, MercatorProjection

A (map) projection is a method of representing the surface of a sphere or other shape on a plane. From the software point of view, it is a class that must provide methods for converting Coordinates into a Point and vice-versa. Projection is an abstract class that can be implemented in different ways, according to the map projection system we need. MercatorProjection is a common method, used both by OpenStreetMap and Microsoft Virtual Earth.

There are only a few things to say about MercatorProjection:

  • JavaFXScript doesn't provide bitwise operators (neither shift or bitwise operations). In their place, there's a javafx.util.Bits class with some static methods.
  • While you can use java.lang.Math for accessing some mathematic functions, with the mobile profile this class is missing a lot of stuff (because it's the JME implementation), including exponential and trigonometric functions that are needed by a map projection system. Starting from JavaFX 1.2 you have a javafx.util.Math class with all the things you need, and this is a small but important improvement.
package it.tidalwave.geo.mapfx.model;

public abstract class Projection
{
public abstract function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer;

public abstract function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer;

public abstract function xToLongitude (x : Integer, zoomLevel : Integer) : Number;

public abstract function yToLatitude (y : Integer, zoomLevel : Integer) : Number;

public function coordinatesToPoint (coordinates : Coordinates, zoomLevel : Integer) : Point
{
return Point
{
y: latitudeToY(coordinates.latitude, zoomLevel)
x: longitudeToX(coordinates.longitude, zoomLevel)
};
}

public function pointToCoordinates (point : Point, zoomLevel : Integer) : Coordinates
{
return Coordinates
{
latitude: yToLatitude(point.y, zoomLevel)
longitude: xToLongitude(point.x, zoomLevel)
}
}

public abstract function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number;
}


package it.tidalwave.geo.mapfx.model;

import javafx.util.Bits;
import javafx.util.Math;

public class MercatorProjection extends Projection
{
public-init var maxZoomLevel : Integer;

public-init var tileSize : Integer;

def EARTH_RADIUS = 6378137;
def EARTH_CIRCUMFERENCE = EARTH_RADIUS * 2.0 * Math.PI;
def EARTH_HALF_CIRCUMFERENCE = EARTH_CIRCUMFERENCE / 2;

override function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number
{
return earthArc(zoomLevel) * Math.cos(Math.toRadians(coordinates.latitude));
}

override function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer
{
def metersX = EARTH_RADIUS * Math.toRadians(longitude);
return Math.round((EARTH_HALF_CIRCUMFERENCE + metersX) / earthArc(zoomLevel));
}

override function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer
{
def sinLat = Math.sin(Math.toRadians(latitude));
def metersY = EARTH_RADIUS / 2 * Math.log((1 + sinLat) / (1 - sinLat));
return Math.round((EARTH_HALF_CIRCUMFERENCE - metersY) / earthArc(zoomLevel));
}

override function xToLongitude (x : Integer, zoomLevel : Integer) : Number
{
def metersX = x * earthArc(zoomLevel) - EARTH_HALF_CIRCUMFERENCE;
return Math.toDegrees(metersX / EARTH_RADIUS);
}

override function yToLatitude (y : Integer, zoomLevel : Integer) : Number
{
def metersY = EARTH_HALF_CIRCUMFERENCE - y * earthArc(zoomLevel);
def exp = Math.exp(metersY / (EARTH_RADIUS / 2));
return Math.toDegrees(Math.asin((exp - 1) / (exp + 1)));
}

function earthArc (zoomLevel : Integer): Number
{
return EARTH_CIRCUMFERENCE / ((Bits.shiftLeft(1, maxZoomLevel - zoomLevel)) * tileSize);
}
}

 

TileSource, TileSourceSupport

TileSource is a class that provides the map tiles for the given coordinates; more specifically, it provides the same conversion capabilities of Projection, with an additional function, findTileURL(), that returns the URL of the map tile that should be downloaded from a remote map providing service such as OpenStreetMap or Microsoft Visual Earth.

TileSourceSupport is a partial implementation that delegates the coordinate conversion functions to an instance of Projection; concrete implementations should inherit from this class, providing an implementation of findTileURL().

package it.tidalwave.geo.mapfx.model;

public abstract class TileSource
{
public-read protected var displayName : String;

public-read protected var maxZoomLevel : Integer;

public-read protected var minZoomLevel : Integer;

public-read protected var defaultZoomLevel : Integer;

public-read protected var cachePrefix : String;

public-read protected var tileSize : Integer;

public abstract function findTileURL (x : Integer, y : Integer, zoomLevel : Integer) : String;

public abstract function coordinatesToPoint (coordinates : Coordinates, zoomLevel : Integer) : Point;

public abstract function pointToCoordinates (point : Point, zoomLevel : Integer): Coordinates;

public abstract function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer;

public abstract function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer;

public abstract function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number;

override function toString()
{
return "TileSource[{displayName}, zoom: {minZoomLevel}-{maxZoomLevel};{defaultZoomLevel} "
"cachePrefix: {cachePrefix} tileSize:{tileSize}";
}
}


package it.tidalwave.geo.mapfx.model;

public abstract class TileSourceSupport extends TileSource
{
public-read protected var projection : Projection;

override function coordinatesToPoint (coordinates : Coordinates, zoomLevel : Integer) : Point
{
return projection.coordinatesToPoint(coordinates, zoomLevel);
}

override function pointToCoordinates (point : Point, zoomLevel : Integer) : Coordinates
{
return projection.pointToCoordinates(point, zoomLevel);
}

override function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer
{
return projection.latitudeToY(latitude, zoomLevel);
}

override function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer
{
return projection.longitudeToX(longitude, zoomLevel);
}

override function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number
{
return projection.metersPerPixel(coordinates, zoomLevel);
}
}

 

TileSource implementations for OpenStreetMaps and Microsoft Visual Earth

Below you can find the sources of concrete implementations of TileSource for two of the most common map providing web services, OpenStreetMap (OSM) and Microsoft Virtual Earth (MVE). The MVE implementation accepts a parameter that allows to choose the pure map service, the satellite service and the map + satellite service.

I'm not going into details of the algorithm used for computing the URLs, since you can find them in the documentations of the two web services.

The only point worth noting is that the MVE implementation demonstrates again the use of javafx.lang.Bits for both bit shifts and bitwise and.

As a note, while OSM is fully open sourced and can be freely used, recall that MVE isn't open and you should get in touch with a Microsoft representative for getting the authorisation to connect to it.

The code could easily work with Google Maps, but the use of their maps outside of a web browser component, directly using the Google Maps APIs in JavaScript, is strictly forbidden.

package it.tidalwave.geo.mapfx.model.osm;

import it.tidalwave.geo.mapfx.model.TileSourceSupport;
import it.tidalwave.geo.mapfx.model.MercatorProjection;

public class OSMTileSource extends TileSourceSupport
{
init
{
displayName = "OpenStreetMap";
cachePrefix = "OSM/";
minZoomLevel = 1;
maxZoomLevel = 17;
defaultZoomLevel = 9;
tileSize = 256;

projection = MercatorProjection
{
maxZoomLevel: maxZoomLevel
tileSize: tileSize
};
}

override function findTileURL (x : Integer, y : Integer, zoom : Integer) : String
{
return "http://tile.openstreetmap.org/{(maxZoomLevel - zoom)}/{x}/{y}.png";
}
}





package it.tidalwave.geo.mapfx.model.mve;

import javafx.util.Bits;
import it.tidalwave.geo.mapfx.model.TileSourceSupport;
import it.tidalwave.geo.mapfx.model.MercatorProjection;

public class Mode
{
var type : String;
var ext : String;
var displayName : String;
}

public def MAP = Mode
{
displayName : "map"
type : "r"
ext : ".png"
};

public def SATELLITE = Mode
{
displayName : "satellite"
type : "a"
ext : ".jpeg"
};

public def MAP_AND_SATELLITE = Mode
{
displayName : "map + satellite"
type : "h"
ext : ".jpeg"
};

public class MVETileSource extends TileSourceSupport
{
public-init var mode = MAP on replace
{
displayName = "Microsoft Virtual Earth ({mode.displayName})";
};

init
{
displayName = "Microsoft Virtual Earth";
cachePrefix = "MVE/";
minZoomLevel = 1;
maxZoomLevel = 19;
defaultZoomLevel = 7;
tileSize = 256;

projection = MercatorProjection
{
maxZoomLevel: maxZoomLevel
tileSize: tileSize
};
}

override function findTileURL (x : Integer, y : Integer, zoomLevel : Integer) : String
{
def quad = tileToQuadKey(x, y, maxZoomLevel - zoomLevel);
return "http://{mode.type}{quad.charAt(quad.length() - 1)}.ortho.tiles.virtualearth.net/"
"tiles/{mode.type}{quad}{mode.ext}?g=1";
}

function tileToQuadKey (x : Integer, y : Integer, zoomLevel : Integer): String
{
var quad = "";
var i = zoomLevel;

while (i > 0)
{
def mask = Bits.shiftLeft(1, (i - 1));
var cell = 0;

if (Bits.bitAnd(x, mask) != 0)
{
cell++;
}

if (Bits.bitAnd(y, mask) != 0)
{
cell += 2;
}

quad += "{cell}";
i--;
}

return quad;
}
}

 

 

MapModel (and MapView)

The last class we're going to see is MapModel. It's basically a façade for the whole system, as it's the only class of the model that you have to instantiate in order to render a map. It can be configured with a TileSource and basically delegates its functions to it.

The current implementation is not complete. In windRose, the equivalent class (still named TileProvider) also includes support for downloading tiles from the internet with background threads (something that we don't need in JavaFX since background downloading is performed by the runtime. But it also provides the capability of caching map tiles on the local storage, a thing that I've not implemented yet since JavaFX 1.2 has introduced a portable way to access the local storage (being a filesystem or anything else) that I've to try yet.

package it.tidalwave.geo.mapfx.model;

public class MapModel
{
public var tileSource : TileSource;

public-read var maxZoomLevel = bind tileSource.maxZoomLevel;

public-read var minZoomLevel = bind tileSource.minZoomLevel;

public-read var defaultZoomLevel = bind tileSource.defaultZoomLevel;

public-read var cachePrefix = bind tileSource.cachePrefix;

public var internetDownloadAllowed = false;

public function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number
{
return tileSource.metersPerPixel(coordinates, zoomLevel);
}

public function coordinatesToPoint (coordinates : Coordinates, zoomLevel : Integer) : Point
{
return tileSource.coordinatesToPoint(coordinates, zoomLevel);
}

public function pointToCoordinates (point : Point, zoomLevel : Integer) : Coordinates
{
return tileSource.pointToCoordinates(point, zoomLevel);
}

public function findTileURL (x : Integer, y : Integer, zoomLevel : Integer) : String
{
return tileSource.findTileURL(x, y, zoomLevel);
}

public function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer
{
return tileSource.latitudeToY(latitude, zoomLevel);
}

public function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer
{
return tileSource.longitudeToX(longitude, zoomLevel);
}
}

While the full description of the rendering component (MapView and companions) will be published in a future article, I'm just giving you an example on how the MapModel can be used together MapView in an application:

import it.tidalwave.geo.mapfx.model.Coordinates;
import it.tidalwave.geo.mapfx.model.MapModel;
import it.tidalwave.geo.mapfx.model.mve.MVETileSource;

def mapModel = MapModel
{
tileSource: MVETileSource{};
};

var centerCoordinates = Coordinates {latitude: 44.410208; longitude: 8.926315 };

def mapView = MapView
{
mapModel: bind mapModel
width: 480
height: 640
centerCoordinates: bind centerCoordinates
enabled: true
};

 

Published at DZone with permission of Fabrizio Giudici, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

sub online replied on Fri, 2009/06/26 - 2:56am

Just wait and see.Tiffany JewelleryYou give me the money. I'll get you the goods.You think I like it like this!links of londonYou'll pay for this.links of londonit sucks.We are even.If you insist.So what?Bad influence (on the kids). I don't want you to hang out with him anymore. He's such a bad influence on the children.Take a seat.Let's see you do it.playing with fire.

Mark Paul replied on Wed, 2009/11/04 - 4:13am

Very interesting article and great start to mapping with JavaFX. Will there be additional installments on this topic?

Fabrizio Giudici replied on Fri, 2010/01/01 - 3:03pm

Yes Mark, I'm writing a second part for describing the remainder of the API.

Jitendra Kumar replied on Tue, 2011/01/04 - 4:48am

Hi Fabrizio Giudici , A good article for mapping solutions in JAVAFX. Have you implemented remainder part of API?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.