Skip to content

Commit

Permalink
Added LeafletMap component as a Maven submodule.
Browse files Browse the repository at this point in the history
  • Loading branch information
ssaring committed Feb 25, 2017
1 parent c971f5a commit c0a594b
Show file tree
Hide file tree
Showing 42 changed files with 11,600 additions and 0 deletions.
56 changes: 56 additions & 0 deletions leafletmap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## LeafletMap

LeafletMap is a JavaFX component for displaying an OpenSteetMap based map
inside a JavaFX WebView by using the Leaflet JavaScript library.

There is a demo application inside the test directory which shows how to
use the LeafletMap component.

Both the LeafletMap component and the demo application are written in Kotlin.


#### Dependencies and used libraries

* Kotlin 1.0.6
* JavaSE 8 (tested with 8u121)
* Leaflet 1.0.3 (included)
* Homepage: http://leafletjs.com/
* License: 2-clause BSD License
* Documentation: http://leafletjs.com/reference-1.0.3.html
* leaflet-color-markers (included, modified)
* Homepage: https://github.com/pointhi/leaflet-color-markers
* License: not specified
* jackson-module-kotlin 2.8.6 (for the demo only, uses Jackson 2.8)
* Homepage: https://github.com/FasterXML/jackson-module-kotlin
* License: not specified


#### Status

* colored markers are supported by the embedded "leaflet-color-markers" library
(modified, e.g. added proper retina icon support)
* the leaflet and the leaflet-color-markers libraries are included locally, no
download at runtime needed
* map viewer features:
* supports OpenSteetMap, OpenCycleMap, HikeBikeMap, MtbMap and MapBox
layers, more can be added easily
* MapBox layer: a project specific token is required for MapBox! A test
token for a limited time can be found in the Leaflet tutorial.
* layers can be switched at runtime by the user
* zoom and scale controls can be configured
* scale supports metric and imperial units
* markers in multiple colors can be displayed
* tracks (routes) can be displayed
* the map is zoomed properly to fit the track
* tooltips can be displayed on the map
* map viewer can be used offline, the route and the markers are shown without
the map data
* the LeafletMapView component API can also be used from Java without problems
(default method parameters are supported via @JvmOverloads)
* the demo application displays a GPS track read from a JSON file, the user can
replay the track by using a position slider


#### TODO

* test memory usage
66 changes: 66 additions & 0 deletions leafletmap/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>de.saring</groupId>
<artifactId>leafletmap</artifactId>
<name>leafletmap</name>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>

<organization>
<name>Saring</name>
<url>http://www.saring.de</url>
</organization>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.version>1.0.6</kotlin.version>
</properties>

<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<version>2.8.6</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<defaultGoal>clean package</defaultGoal>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
18 changes: 18 additions & 0 deletions leafletmap/src/main/kotlin/de/saring/leafletmap/ColorMarker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.saring.leafletmap

/**
* Enumeration for all marker colors of the leaflet-color-markers JavaScript library.
*
* @author Stefan Saring
*/
enum class ColorMarker(val iconName: String) {

BLUE_MARKER("blueIcon"),
RED_MARKER("redIcon"),
GREEN_MARKER("greenIcon"),
ORANGE_MARKER("orangeIcon"),
YELLOW_MARKER("yellowIcon"),
VIOLET_MARKER("violetIcon"),
GREY_MARKER("greyIcon"),
BLACK_MARKER("blackIcon")
}
14 changes: 14 additions & 0 deletions leafletmap/src/main/kotlin/de/saring/leafletmap/ControlPosition.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package de.saring.leafletmap

/**
* Enumeration for all possible map control positions.
*
* @author Stefan Saring
*/
enum class ControlPosition(val positionName: String) {

TOP_LEFT("topleft"),
TOP_RIGHT("topright"),
BOTTOM_LEFT("bottomleft"),
BOTTOM_RIGHT("bottomright")
}
8 changes: 8 additions & 0 deletions leafletmap/src/main/kotlin/de/saring/leafletmap/LatLong.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package de.saring.leafletmap

/**
* Immutable value class for defining a geo position.
*
* @author Stefan Saring
*/
data class LatLong(val latitude: Double, val longitude: Double)
153 changes: 153 additions & 0 deletions leafletmap/src/main/kotlin/de/saring/leafletmap/LeafletMapView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package de.saring.leafletmap

import javafx.scene.layout.StackPane
import javafx.scene.web.WebEngine
import javafx.scene.web.WebView

import java.net.URL
import javafx.concurrent.Worker


/**
* JavaFX component for displaying OpenStreetMap based maps by using the Leaflet.js JavaScript library inside a WebView
* browser component.<br/>
* This component can be embedded most easily by placing it inside a StackPane, the component uses then the size of the
* parent automatically.
*
* @author Stefan Saring
*/
class LeafletMapView : StackPane() {

private val webView = WebView()
private val webEngine: WebEngine = webView.engine

private var varNameSuffix: Int = 1

/**
* Creates the LeafletMapView component, it does not show any map yet.
*/
init {
this.children.add(webView)
}

/**
* Displays the initial map in the web view. Needs to be called and complete before adding any markers or tracks.
* The completionNotifier callback can be used for notification when the map is initially displayed.
*
* @param mapConfig configuration of the map layers and controls
* @param completionNotifier notifies the caller when the map is displayed or the loading has failed (optional)
*/
@JvmOverloads
fun displayMap(mapConfig: MapConfig, completionNotifier: ((Worker.State) -> Unit)? = null) {

webEngine.loadWorker.stateProperty().addListener { observable, oldValue, newValue ->

if (newValue == Worker.State.SUCCEEDED) {
executeMapSetupScripts(mapConfig)
}

if (completionNotifier != null && (newValue == Worker.State.SUCCEEDED || newValue == Worker.State.FAILED)) {
completionNotifier(newValue)
}
}

val localFileUrl: URL = LeafletMapView::class.java.getResource("/leafletmap/leafletmap.html")
webEngine.load(localFileUrl.toExternalForm())
}

private fun executeMapSetupScripts(mapConfig: MapConfig) {

// execute scripts for layer definition
mapConfig.layers.forEachIndexed { i, layer ->
execScript("var layer${i + 1} = ${layer.javaScriptCode};")
}

val jsLayers = mapConfig.layers
.mapIndexed { i, layer -> "'${layer.displayName}': layer${i + 1}" }
.joinToString(", ")
execScript("var baseMaps = { $jsLayers };")

// execute script for map view creation (Leaflet attribution must not be a clickable link)
execScript("""
|var myMap = L.map('map', {
| center: new L.LatLng(${mapConfig.initialCenter.latitude}, ${mapConfig.initialCenter.longitude}),
| zoom: 8,
| zoomControl: false,
| layers: [layer1]
|});
|
|var attribution = myMap.attributionControl;
|attribution.setPrefix('Leaflet');""".trimMargin())

// execute script for layer control definition if there are multiple layers
if (mapConfig.layers.size > 1) {
execScript("""
|var overlayMaps = {};
|L.control.layers(baseMaps, overlayMaps).addTo(myMap);""".trimMargin())

}

// execute script for scale control definition
if (mapConfig.scaleControlConfig.show) {
execScript("L.control.scale({position: '${mapConfig.scaleControlConfig.position.positionName}', " +
"metric: ${mapConfig.scaleControlConfig.metric}, " +
"imperial: ${!mapConfig.scaleControlConfig.metric}})" +
".addTo(myMap);")
}

// execute script for zoom control definition
if (mapConfig.zoomControlConfig.show) {
execScript("L.control.zoom({position: '${mapConfig.zoomControlConfig.position.positionName}'})" +
".addTo(myMap);")
}
}

/**
* Sets a marker at the specified position.
*
* @param position marker position
* @param title marker title shown in tooltip (pass empty string when tooltip not needed)
* @param marker marker color
* @param zIndexOffset zIndexOffset (higher number means on top)
* @return variable name of the created marker
*/
fun addMarker(position: LatLong, title: String, marker: ColorMarker, zIndexOffset: Int): String {
val varName = "marker${varNameSuffix++}"

execScript("var $varName = L.marker([${position.latitude}, ${position.longitude}], "
+ "{title: '$title', icon: ${marker.iconName}, zIndexOffset: $zIndexOffset}).addTo(myMap);")
return varName;
}

/**
* Moves the existing marker specified by the variable name to the new position.
*
* @param markerName variable name of the marker
* @param position new marker position
*/
fun moveMarker(markerName: String, position: LatLong) {
execScript("$markerName.setLatLng([${position.latitude}, ${position.longitude}]);")
}

/**
* Draws a track path anlong the specified positions in the color red and zooms the map to fit the track perfectly.
*
* @param positions list of track positions
*/
fun addTrack(positions: List<LatLong>) {

val jsPositions = positions
.map { " [${it.latitude}, ${it.longitude}]" }
.joinToString(", \n")

execScript("""
|var latLngs = [
|$jsPositions
|];
|var polyline = L.polyline(latLngs, {color: 'red', weight: 2}).addTo(myMap);
|myMap.fitBounds(polyline.getBounds());""".trimMargin())
}

private fun execScript(script: String) = webEngine.executeScript(script)
}
24 changes: 24 additions & 0 deletions leafletmap/src/main/kotlin/de/saring/leafletmap/MapConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.saring.leafletmap

/**
* Class for defining the layers and controls in the map to be shown.
* @author Stefan Saring
*/
class MapConfig @JvmOverloads constructor(

/**
* List of layers to be shown in the map, the default layer is OpenStreetMap. If more than one layer is
* specified, then a layer selection control will be shown in the top right corner.
*/
val layers: List<MapLayer> = listOf(MapLayer.OPENSTREETMAP),

/** Zoom control definition, by default it's shown in the top left corner. */
val zoomControlConfig: ZoomControlConfig = ZoomControlConfig(),

/** Scale control definition, by default it's not shown. */
val scaleControlConfig: ScaleControlConfig = ScaleControlConfig(),

/** Initial center position of the map (default is London city). */
val initialCenter: LatLong = LatLong(51.505, -0.09)
)
40 changes: 40 additions & 0 deletions leafletmap/src/main/kotlin/de/saring/leafletmap/MapLayer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.saring.leafletmap

/**
* Enumeration for all supported map layers.
*
* @author Stefan Saring
*/
enum class MapLayer(val displayName: String, val javaScriptCode: String) {

/** OpenStreetMap layer. */
OPENSTREETMAP("OpenStreetMap", """
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data &copy; OpenStreetMap and contributors',
})"""),

/** OpenCycleMap layer. */
OPENCYCLEMAP("OpenCycleMap", """
L.tileLayer('http://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png', {
attribution: '&copy; OpenCycleMap, Map data &copy; OpenStreetMap contributors',
})"""),

/** Hike & bike maps layer (HikeBikeMap.org). */
HIKE_BIKE_MAP("Hike & Bike Map", """
L.tileLayer('http://{s}.tiles.wmflabs.org/hikebike/{z}/{x}/{y}.png', {
attribution: '&copy; HikeBikeMap.org, Map data &copy; OpenStreetMap and contributors',
})"""),

/** MTB map (mtbmap.cz). */
MTB_MAP("MTB Map", """
L.tileLayer('http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap and USGS',
})"""),

/** MapBox layer in streets mode (consider: a project specific access token is required!). */
MAPBOX("MapBox", """
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
id: 'mapbox.streets',
attribution: 'Map data &copy; OpenStreetMap contributors, Imagery &copy; Mapbox'
})""")
}
Loading

0 comments on commit c0a594b

Please sign in to comment.