DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Coding
  3. Java
  4. Implementing a Common-Profile Map Renderer for JavaFX (Part 1: the Model)

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

Fabrizio Giudici user avatar by
Fabrizio Giudici
·
Jun. 15, 09 · Interview
Like (0)
Save
Tweet
Share
10.66K Views

Join the DZone community and get the full member experience.

Join For Free
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
};

 

JavaFX

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Reconciling Java and DevOps with JeKa
  • 10 Most Popular Frameworks for Building RESTful APIs
  • Isolating Noisy Neighbors in Distributed Systems: The Power of Shuffle-Sharding
  • Data Stream Using Apache Kafka and Camel Application

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: