mapjfx-demo
Note March 2022: I started mapjfx in December 2014 as a side project. In these seven years I learned quite some things by developing and maintaining it. But my focus has shifted and I don’t have the time anymore to take care of this project. So from March 2022 on, mapjfx will be out of maintenance. I’ll do no further development on it.
mapjfx-demo is a JavaFX application that uses the mapjfx component. It is a showcase application demoing all the features from the library and shows how to use it. It is a standalone application that is built using an FXML file.
The source can be found on GitHub . The current versions is 3.1.0 (the version numbers will always be the same as that of the used mapjfx library) and is based on JDK17.
The following sections describe how the program uses the mapjfx component, how it is incorporated and set up and shows screenshots and sample code.
building and running the demo application
You need to have git to check out the sources, Java JDK and maven to build and run the program.
After cloning the project from GitHub, change into the project directory (assuming this is mapjfx-demo). Make sure you switched to the correct branch for your JDK version (main for JDK15 or main-2.x for JDK11) and then run:
cd mapjfx-demo
mvn package
cd target/mapjfx-demo
./bin/mapjfx-demo
The maven command builds the demo program and creates a directory target/mapjfx-demo
which contains startup-scripts and the necessary libraries. On Windows, use the command:
./bin/mapjfx-demo.bat
This starts the program:
Add the mapjfx component to an application
For the demo application I used an FXML file for creating the GUI. I put the MapView in the center of a BoxLayout, the important lines from the FXML file are:
<?xml version="1.0" encoding="UTF-8"?>
<?import com.sothawo.mapjfx.MapView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0"
prefWidth="800.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.sothawo.mapjfx.demo.Controller">
<top>
<!-- top controls here -->
</top>
<center>
<MapView fx:id="mapView" BorderPane.alignment="CENTER"/>
</center>
<bottom>
<!-- botto controls here -->
</bottom>
<left>
<!-- left controls here -->
</left>
</BorderPane>
In the Controller class, there is a corresponding field:
package com.sothawo.mapjfx.demo;
import com.sothawo.mapjfx.*;
/**
* Controller for the FXML defined code.
*
* @author P.J. Meisch (pj.meisch@sothawo.com).
*/
public class Controller {
/** the MapView containing the map */
@FXML
private MapView mapView;
// rest of code skipped
}
I skip describing the other elements as these are plain JavaFX elements and can be looked up in the source code.
Connecting the zoom controls
At the top of the program window is a button setting the map’s zoom to the value of 14, and a slider that is bidirectional bound to the MapView’s zoom property. By this binding, changing the slider values changes the zoom of the map, and (shift)doubleclicking or a 2 finger move on a trackpad which cause the map to zoom are reported back to the application. The button and slider are setup in the Controller’s initMapAndControls()
method:
// wire the zoom button and connect the slider to the map's zoom
buttonZoom.setOnAction(event -> mapView.setZoom(ZOOM_DEFAULT));
sliderZoom.valueProperty().bindBidirectional(mapView.zoomProperty());
Configuring the map
At the end of the same method, the MapView object is initialized by calling it’s initialize()
method. This method takes an optional Parameter of type Configuration
which can be used to customize the map (to create a Configuraton the class has a builder() method). One possible customization is the projection which specifies the base projection of the map. If it is not specified, WebMercator (EPSG:3857 ) projection is used- this is the default value used by nearly all maps on the internet like Open Streetmap. As an alternative, WGS84 (EPSG:4326) can be specified.
Configuration options are:
- projection
- disable the showing of the zoom controls
- disable user interaction with the map
The status bar
At the bottom of the program is a status bar that contains labels for the actual center coordinate, the coordinates of the shown extent, the zoom level of the map and the last event from MapView. These labels are bound to the corresponding properties of the MapView so that they are updated automatically when the values change:
// bind the map's center and zoom properties to the corrsponding labels and format them
labelCenter.textProperty().bind(Bindings.format("center: %s", mapView.centerProperty()));
labelZoom.textProperty().bind(Bindings.format("zoom: %.0f", mapView.zoomProperty()));
Custom events of the MapView
When the user single clicks a point in the map (double click is for zooming), the MapView fires a custom event that contains the geographical coordinate of the clicked point. This is used in the demo application to set a marker; this is described later on on this page. The other custom events are fired when the user clicks a marker or a label. These events are displayed in the status line with the following code:
mapView.addEventHandler(MarkerEvent.MARKER_CLICKED, event -> {
event.consume();
labelEvent.setText("Event: marker clicked: " + event.getMarker().getId());
});
mapView.addEventHandler(MapLabelEvent.MAPLABEL_CLICKED, event -> {
event.consume();
labelEvent.setText("Event: label clicked: " + event.getMapLabel().getText());
});
Events are also fired for rightclick, doubleclick, mousedown and mouseup events on markers and labels. Also available is a right clicked event for the map. The demo shows these events in the status bar. There is evene an event triggered when the mouse moves over the map, but this is not shown anywhere in the demo app.
When the user holds the cmd key (on Mac OSX) or the ctrl key (on Windows) and drags a rectangle with the mouse, on release of the mouse a custom event for the extent selection is triggered that contains the coordinates of the upper left and lower right corner of the extent. The demo application uses this to set the current extent in the map:
<pre class="EnlighterJSRAW" data-enlighter-language="java">mapView.addEventHandler(MapViewEvent.MAP_EXTENT, event -> {
event.consume();
mapView.setExtent(event.getExtent());
});
Another event MapViewEvent.MAP_BOUNDING_EXTENT
is triggered whenever then extent of the whole map changes, for example by resizing the window, changing the zoom or the center of the map.
The location buttons
The application uses different locations from Karlsruhe, the harbour, the castle, the station and the stadium of the local soccer club. The coordinates for these locations are defined as static fields in the Controller class:
private static final Coordinate coordKarlsruheCastle = new Coordinate(49.013517, 8.404435);
private static final Coordinate coordKarlsruheHarbour = new Coordinate(49.015511, 8.323497);
private static final Coordinate coordKarlsruheStation = new Coordinate(48.993284, 8.402186);
private static final Coordinate coordKarlsruheSoccer = new Coordinate(49.020035, 8.412975);
private static final Coordinate coordKarlsruheUniversity = new Coordinate(49.011809, 8.413639);
private static final Extent extentAllLocations = Extent.forCoordinates(coordKarlsruheCastle,
coordKarlsruheHarbour, coordKarlsruheStation, coordKarlsruheSoccer);
There is also an extent defined which contains these four coordinates. An extent is a bounding rectangle that contains all given coordinates.
On the left side of the application in the locations section there are four buttons for the locations. A click on a button sets the map’s center to the corresponding coordinate and moves the map accordingly. The zoom level is not changed by these operations. The button labeled All sets the map to the extent, so that all the given places are in view.
The code to setup these button actions is:
// wire up the location buttons
buttonKaHarbour.setOnAction(event -> mapView.setCenter(coordKarlsruheHarbour));
buttonKaCastle.setOnAction(event -> mapView.setCenter(coordKarlsruheCastle));
buttonKaStation.setOnAction(event -> mapView.setCenter(coordKarlsruheStation));
buttonKaSoccer.setOnAction(event -> mapView.setCenter(coordKarlsruheSoccer));
buttonAllLocations.setOnAction(event -> mapView.setExtent(extentAllLocations));
Changing the map type
mapjfx allows the user to choose between different map types. At the moment these are OSM (OpenStreetMap), Stamen Watercolor (OSM based), Bing Maps Road, Bing Maps Aerial, custom WMS servers and servers providing XYZ sources. To be able to switch to a Bing Maps, a Bing Map API-Key must be entered in the corresponding field of the misc section. The sample program has configured two custom WMS servers, one is gis-lab.info with landsat tiles, the other is from the world food programme showing administration boundaries. Another additoion as from October 28th 2018 are XYZ map sources (contribution by Erik Jaehne ).
The map style section on the left side of the program offers radio buttons for that purpose. Selecting a map type changes the map accordingly (the screenshot with Bing Maps Aerial shows markers, which are covered in the next section):
Note: as of version 1.7.3 the support for MapQuest was removed, due to OpenLayers not supporting it any more (this was caused by MapQuest changes, further information can be found here .
The radio buttons are contained in a ToggleGroup that is handled by the following code:
// observe the map type radiobuttons
mapTypeGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
logger.debug("map type toggled to {}", newValue.toString());
MapType mapType = MapType.OSM;
if (newValue == radioMsOSM) {
mapType = MapType.OSM;
} else if (newValue == radioMsMQ) {
mapType = MapType.MAPQUEST_OSM;
} else if (newValue == radioMsBR) {
mapType = MapType.BINGMAPS_ROAD;
} else if (newValue == radioMsBA) {
mapType = MapType.BINGMAPS_AERIAL;
} else if (newValue == radioMsWMSGisLandsat) {
mapView.setWMSParam(wmsParamGisLandsat);
if (oldValue == radioMsWMSWFPAdminBoundaries) {
mapView.setMapType(MapType.OSM);
}
mapType = MapType.WMS;
} else if (newValue == radioMsWMSWFPAdminBoundaries) {
mapView.setWMSParam(wmsParamWFPAdminBoundaries);
if (oldValue == radioMsWMSGisLandsat) {
mapView.setMapType(MapType.OSM);
}
mapType = MapType.WMS;
}
mapView.setBingMapsApiKey(bingMapsApiKey.getText());
mapView.setMapType(mapType);
});
The wmsParams object were initialized with:
/** params for the first WMS server. */
private WMSParam wmsParamGisLandsat = new WMSParam()
.setUrl("http://irs.gis-lab.info/?")
.addParam("layers", "landsat")
.addParam("REQUEST", "GetTile");
/** params for the second WMS server. */
private WMSParam wmsParamWFPAdminBoundaries = new WMSParam()
.setUrl("http://geonode.wfp.org:80/geoserver/ows")
.addParam("layers", "geonode:admin_2_gaul_2015");
Using markers and labels
mapjfx can display markers and labels on the map. Both are objects that are displayed at a given position on the map. A marker has a graphic and a label has a text.
Markers are defined by an image. There are four predefined markers in the library, these are the blue, green, red and orange markers seen in the demo application. It is also possible to define markers with a custom icon as is seen with the logo of the local soccer club KSC. When using a custom icon, it might be necessary to define the image offset in relation to the point where the marker should be place; this can be seen in the code as well.
Labels have a text and can optionally have a css-style asssigned, more about the css later. Like markers, labels may have an offset in relation to the position.
Labels also may be attached to a marker. This means, that the Label will be shown, hidden, moved and removed together with the Marker that it is attached to, so that only the Marker must be watched or moved.
The following image shows a blue marker without a label, an orange, a green and a red label with a label and a custom image without a label. The orange marker might be moved by clicking in the map. Additionally a label without a marker is shown (‘university’).
When the user clicks a marker or a label a custom event is triggered as already described.
The demo application creates the markers as follows:
// a couple of markers using the provided ones
markerKaHarbour = Marker.createProvided(Marker.Provided.BLUE).setPosition(coordKarlsruheHarbour).setVisible(
false);
markerKaCastle = Marker.createProvided(Marker.Provided.GREEN).setPosition(coordKarlsruheCastle).setVisible(
false);
markerKaStation =
Marker.createProvided(Marker.Provided.RED).setPosition(coordKarlsruheStation).setVisible(false);
// no position for click marker yet
markerClick = Marker.createProvided(Marker.Provided.ORANGE).setVisible(false);
// a marker with a custom icon
markerKaSoccer = new Marker(getClass().getResource("/ksc.png"), -20, -20).setPosition(coordKarlsruheSoccer)
.setVisible(false);
// the fix label, default style
labelKaUniversity = new MapLabel("university").setPosition(coordKarlsruheUniversity).setVisible(true);
// the attached labels, custom style
labelKaCastle = new MapLabel("castle", 10, -10).setVisible(false).setCssClass("green-label");
labelKaStation = new MapLabel("station", 10, -10).setVisible(false).setCssClass("red-label");
labelClick = new MapLabel("click!", 10, -10).setVisible(false).setCssClass("orange-label");
markerKaCastle.attachLabel(labelKaCastle);
markerKaStation.attachLabel(labelKaStation);
markerClick.attachLabel(labelClick);
Markers must be added to the MapView in order to be observed:
// add the markers to the map - they are still invisible
mapView.addMarker(markerKaHarbour);
mapView.addMarker(markerKaCastle);
mapView.addMarker(markerKaStation);
mapView.addMarker(markerKaSoccer);
// can't add the markerClick at this moment, it has no position, so it would not be added to the map
// add the fix label, the other's are attached to markers.
mapView.addLabel(labelKaUniversity);
They can also be removed from the MapView, which automatically removes them from the map display, they might later be readded.
A marker – and a label as well – has a position and a visible property which are both observed by the MapView – as long as the marker is added to the MapView – so that changes to these properties are reflected in the map. The visible property of the markers is bound to the checkboxes in the markers section on the left side of the application:
// bind the checkboxes to the markers visibility
checkKaHarbourMarker.selectedProperty().bindBidirectional(markerKaHarbour.visibleProperty());
checkKaCastleMarker.selectedProperty().bindBidirectional(markerKaCastle.visibleProperty());
checkKaStationMarker.selectedProperty().bindBidirectional(markerKaStation.visibleProperty());
checkKaSoccerMarker.selectedProperty().bindBidirectional(markerKaSoccer.visibleProperty());
checkClickMarker.selectedProperty().bindBidirectional(markerClick.visibleProperty());
A marker has a coordinate property that is observed by the MapView so that setting this property to a new value is immediately reflected by the map.
The orange marker is a marker that follows the places where the user clicks in the map, therefore it is called click marker. To make the marker follow the click, an event handler is registered that sets the marker’s position. To make it a little nicer, an animation is added that moves the marker:
// add an event handler for singleclicks, set the click marker to the new position
// add an event handler for singleclicks, set the click marker to the new position when it's visible
mapView.addEventHandler(MapViewEvent.MAP_CLICKED, event -> {
event.consume();
final Coordinate newPosition = event.getCoordinate();
labelEvent.setText("Event: map clicked at: " + newPosition);
if (checkDrawPolygon.isSelected()) {
handlePolygonClick(event);
}
if (markerClick.getVisible()) {
final Coordinate oldPosition = markerClick.getPosition();
if (oldPosition != null) {
animateClickMarker(oldPosition, newPosition);
} else {
markerClick.setPosition(newPosition);
// adding can only be done after coordinate is set
mapView.addMarker(markerClick);
}
}
});
private void animateClickMarker(Coordinate oldPosition, Coordinate newPosition) {
// animate the marker to the new position
final Transition transition = new Transition() {
private final Double oldPositionLongitude = oldPosition.getLongitude();
private final Double oldPositionLatitude = oldPosition.getLatitude();
private final double deltaLatitude = newPosition.getLatitude() - oldPositionLatitude;
private final double deltaLongitude = newPosition.getLongitude() - oldPositionLongitude;
{
setCycleDuration(Duration.seconds(1.0));
setOnFinished(evt -> markerClick.setPosition(newPosition));
}
@Override
protected void interpolate(double v) {
final double latitude = oldPosition.getLatitude() + v * deltaLatitude;
final double longitude = oldPosition.getLongitude() + v * deltaLongitude;
markerClick.setPosition(new Coordinate(latitude, longitude));
}
};
transition.play();
}
CSS Styles
Labels just are <div>
elements that are added to the shown page, so they can be styled with css. A label always has the css class mapview-label. The default style contained within the library for labels is:
/*default style for a mapview label */
.mapview-label {
border-radius: 5px;
border: 1px solid black;
background: #eeeeee linear-gradient(#eeeeee, #aaaaaa);
padding: 2px;
}
An additional css class for a Label can be set as follows:
labelKaCastle = new MapLabel("castle", 10, -10).setVisible(false).setCssClass("green-label");
The css class of a Label can be set as well later when the label already is visible, the change will be shown on the map immediately. To be able to style the labels it is necessary to define the stylesheet containing the information. This is done with the following call on the MapView element:
mapView.setCustomMapviewCssURL(getClass().getResource("/custom_mapview.css"));
The css file used in the demo has the following content:
.red-label {
padding: 2px 10px;
background: #e6262b linear-gradient(#e67b7b, #ad2125);
}
.orange-label {
padding: 2px 10px;
background: #e69327 linear-gradient(#e6a38f, #ae6d1f);
}
.green-label {
padding: 2px 10px;
background: #356425 linear-gradient(#93ee93, #356425);
}
Markers and Labels have a rotation property which can be set to values from 0..360 and which is used to rotate the corresponding HTML element on the map. The demo application uses this with an animation to rotate the marker for the soccer club.
Tracks
Since version 1.4.0 it is possible to display coordinate-lines – or tracks – on the map. The following screenshot shows the demo application with two tracks in different colors and with a different line-thickness:
These objects are created in the demo application by reading two files in csv format containing the coordinates:
// load two coordinate lines
trackMagenta = loadCoordinateLine(getClass().getResource("/M1.csv")).orElse(new CoordinateLine
()).setColor(Color.MAGENTA);
trackCyan = loadCoordinateLine(getClass().getResource("/M2.csv")).orElse(new CoordinateLine
()).setColor(Color.CYAN).setWidth(7);
The method that is called reads the csv file, splits the lines in two, creates Coordinate Objects and adds them to a list to finally pass this list to the CoordinateLine
constructor.
private Optional<CoordinateLine> loadCoordinateLine(URL url) {
try (
Stream<String> lines = new BufferedReader(
new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)).lines()
) {
return Optional.ofNullable(new CoordinateLine(
lines.map(line -> line.split(";")).filter(array -> array.length == 2)
.map(values -> new Coordinate(Double.valueOf(values[0]), Double.valueOf(values[1])))
.collect(Collectors.toList())));
} catch (IOException | NumberFormatException e) {
logger.error("load {}", url, e);
}
return Optional.empty();
}
CoordinateLine objects have a visibility property that in the demo application is bound to the checkboxes. On change of these properties the demo application zooms the extent so that both tracks are in view.
Polygons
Since version 2.2.0 (1.21.0 for the JDK8 version), CoordinateLines can be set to be closed, so that they are polygons with a fill color. After the draw a polygon checkbox is checked in the left pane, the demo app uses the clicks in the map to build a filled polygon:
When trying this out, after the first click you will see no change, after the second a line and only after the third click a polygon.
The code in the handler for the MAP_CLICKED event just gets the coordinates from the previous polygon, adds the new one and then recreates the polygon (Coordinate lines cannot be modified once their are created in the map:
/**
* shows a new polygon with the coordinate from the added.
*
* @param event
* event with coordinates
*/
private void handlePolygonClick(MapViewEvent event) {
final List<Coordinate> coordinates = new ArrayList<>();
if (polygonLine != null) {
polygonLine.getCoordinateStream().forEach(coordinates::add);
mapView.removeCoordinateLine(polygonLine);
polygonLine = null;
}
coordinates.add(event.getCoordinate());
polygonLine = new CoordinateLine(coordinates)
.setColor(Color.DODGERBLUE)
.setFillColor(Color.web("lawngreen", 0.4))
.setClosed(true);
mapView.addCoordinateLine(polygonLine);
polygonLine.setVisible(true);
}
Like with markers and labels, coordinate lines and polygons are independent from the map style, so you change change the style while displaying them:
Circles
since version 1.33.0/2.15.0 it is now possible add circles to the map as well. The API is very similar to the Coordinate line API.
Miscellaneous options
In this section on the left side of the demo application there are the following options:
animationDuration. This is a value in milliseconds that is used for animation when the map’s center or zoom is modified using the setCenter(c)
, setZoom(z)
or setExtend(e)
calls. It does not change the animation that is used by the OpenLayers map component when the controls in the map are used for zooming. Setting the value to zero switches the animation off.
Bing Maps API Key. Enter your API key for Bing Maps and then the map style can be changed to one of the Bing Map styles.
constrain to Germany: when selected, the map cannot be panned or zoomed to show areas outside of the rectangle that is defined by the points that are furthest north, south, east and west of Germany
Caching of data
Version 1.7.0 adds the possibility to cache the data that is loaded from the web in a directory to both speed loading at a later time and to be able to use the application later when offline.
technical information
Alas the JavaFX WebView does not offer the possibility to enable caching, so I had to integrate my solution deep into the Java networking by installing a custom URLStreamHandlerFactory
. This means that all http and http connections that are made from the application are passing through this caching mechanism. As a custom URLStreamHandlerFactory
can only be installed once in a JVM, this also means that mapjfx caching cannot be used together with another component that also does install a URLStreamHandlerFactory.
As long as the caching in mapjfx is not explicitly enabled, the URLStreamHandlerFactory
is not set, this is done when the cache is programmatically set to active for the first time.
prevent URLs from being cached.
As of version 1.3.0 it is possible to define a list of Java Regex Patterns which will prevent URLs that match these patterns from being cached. The code for this is for example:
offlineCache.setNoCacheFilters(Collections.singletonList(".*\\.sothawo\\.com/.*"));
offlineCache.setActive(true);
The argument to setNoCacheFilters is an Collection of String objects containing regular expressions.
From version 2.10.2/1.29.2 on alternatively a list of patterns to be cached can be set instead of a list of patterns that should not be cached.
restrictions / Problems
The following restrictions or problems have been found during the development:
- caching of Bing Maps data only works with an internet connection. The image data is cached quite well, but when reloading, there is a REST call to bing which I have not further investigated, so that this prevents loading and using the cache data when offline. When online, the cache can still be used to speed up loading. OpenStreetmap data does not have this problem.
- Caching will probably removed for the JDK11+ versions. The implementation needs to get the default URLStreamHandlers from the default URLStreamHandlerFactory and this is only possible with a reflective access the is not allowed anymore in the JDK versions since version 9. Currently it is possible to use the
--add-opens java.base/java.net=com.sothawo.mapjfx
flag on application startup-
code example
The following code shows how the cache directory is set and how the cache is activated in the mapjfx-demo application:
// init MapView-Cache
final OfflineCache offlineCache = mapView.getOfflineCache();
final String cacheDir = System.getProperty("java.io.tmpdir") + "/mapjfx-cache";
logger.info("using dir for cache: " + cacheDir);
try {
Files.createDirectories(Paths.get(cacheDir));
offlineCache.setCacheDirectory(cacheDir);
offlineCache.setActive(true);
} catch (IOException e) {
logger.warn("could not activate offline cache", e);
}
Have fun testing and using the mapjfx component.
Comments and contributions are welcome!