diff --git a/src/main/kotlin/map/Circle.kt b/src/main/kotlin/map/Circle.kt
new file mode 100644
index 0000000..cceb0d7
--- /dev/null
+++ b/src/main/kotlin/map/Circle.kt
@@ -0,0 +1,60 @@
+package fdit.leafletmap
+
+import javafx.scene.paint.Color
+
+class Circle private constructor(private var center: LatLong, private var title: String, private var zIndexOffset: Int) {
+ private var color = Color(0.0, 0.0, 0.0, 0.0)
+ private lateinit var map: LeafletMapView
+ private var isAttached = false
+ private var isDisplayed = false
+ private var radius = 0.0
+
+ constructor(position: LatLong, radius: Double, title: String, color: Color, zIndexOffset: Int) : this(position, title, zIndexOffset) {
+ this.color = color
+ this.title = title.replace("-", "")
+ this.center = position
+ this.radius = nauticalMilesToMeter(radius)
+ }
+
+ internal fun addToMap(map: LeafletMapView) {
+ this.map = map
+ if (map.execScript("typeof circle$title == 'undefined'") as Boolean) {
+ map.execScript("var circle$title;")
+ }
+ if (!this.isAttached) {
+ val hexColor = "%02x".format((color.red * 255).toInt()) + "%02x".format((color.green * 255).toInt()) + "%02x".format((color.blue * 255).toInt())
+ map.execScript("circle$title = L.circle([${center.latitude}, ${center.longitude}], $radius, {color:'#$hexColor'}).addTo(myMap);")
+ this.isAttached = true
+ this.isDisplayed = true
+ } else if (!this.isDisplayed) {
+ map.execScript("circle$title.addTo(myMap)")
+ this.isDisplayed = true
+ }
+ }
+
+ fun modifyCircle(latLong: LatLong, radius: Double) {
+ this.center = latLong
+ this.radius = radius
+ this.radius = nauticalMilesToMeter(radius)
+ }
+
+ fun uppdateMap() {
+ if (this.isAttached && !this.isDisplayed) {
+ map.execScript("myMap.removeLayer(circle$title);" +
+ "circle$title = L.circle([${center.latitude}, ${center.longitude}], $radius).addTo(myMap);")
+ this.isDisplayed = true
+ }
+ }
+
+ internal fun removeCircle(map: LeafletMapView) {
+ if (this.isAttached && this.isDisplayed) {
+ map.execScript("myMap.removeLayer(circle$title);")
+ this.isDisplayed = false
+ }
+ }
+
+ private fun nauticalMilesToMeter(nauticalMiles: Double): Double {
+ return nauticalMiles * 1.852
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/map/ColorMarker.kt b/src/main/kotlin/map/ColorMarker.kt
new file mode 100644
index 0000000..e80cec8
--- /dev/null
+++ b/src/main/kotlin/map/ColorMarker.kt
@@ -0,0 +1,18 @@
+package fdit.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")
+}
diff --git a/src/main/kotlin/map/ControlPosition.kt b/src/main/kotlin/map/ControlPosition.kt
new file mode 100644
index 0000000..f87c6d3
--- /dev/null
+++ b/src/main/kotlin/map/ControlPosition.kt
@@ -0,0 +1,14 @@
+package fdit.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")
+}
diff --git a/src/main/kotlin/map/LatLong.kt b/src/main/kotlin/map/LatLong.kt
new file mode 100644
index 0000000..3c6b42b
--- /dev/null
+++ b/src/main/kotlin/map/LatLong.kt
@@ -0,0 +1,8 @@
+package fdit.leafletmap
+
+/**
+ * Immutable value class for defining a geo position.
+ *
+ * @author Stefan Saring
+ */
+data class LatLong(val latitude: Double, val longitude: Double)
diff --git a/src/main/kotlin/map/LeafletMapView.kt b/src/main/kotlin/map/LeafletMapView.kt
new file mode 100644
index 0000000..8197c42
--- /dev/null
+++ b/src/main/kotlin/map/LeafletMapView.kt
@@ -0,0 +1,411 @@
+package fdit.leafletmap
+
+import fdit.leafletmap.events.*
+import fdit.leafletmap.events.MapClickEventMaker
+import fdit.leafletmap.events.MapMoveEventMaker
+import fdit.leafletmap.events.MarkerClickEventMaker
+import javafx.concurrent.Worker
+import javafx.scene.layout.StackPane
+import javafx.scene.paint.Color
+import javafx.scene.shape.Polygon
+import javafx.scene.web.WebEngine
+import javafx.scene.web.WebView
+import fdit.leafletmap.events.*
+import netscape.javascript.JSObject
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.IOException
+import java.net.URL
+import java.util.*
+import java.util.concurrent.CompletableFuture
+import javax.imageio.ImageIO
+
+
+/**
+ * JavaFX component for displaying OpenStreetMap based maps by using the Leaflet.js JavaScript library inside a WebView
+ * browser component.
+ * 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
+ * @author Niklas Kellner
+ */
+class LeafletMapView : StackPane() {
+
+ private val webView = WebView()
+ private val webEngine: WebEngine = webView.engine
+
+ private var varNameSuffix: Int = 1
+ private val mapClickEvent = MapClickEventMaker()
+ private val markerClickEvent = MarkerClickEventMaker()
+ private val mapMoveEvent = MapMoveEventMaker()
+ internal val zoomLimitSmallMarker = 8
+
+ /**
+ * 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 returned CompletableFuture will provide the final map load state, the map can be used when the load has
+ * completed with state SUCCEEDED (use CompletableFuture#whenComplete() for waiting to complete).
+ *
+ * @param mapConfig configuration of the map layers and controls
+ * @return the CompletableFuture which will provide the final map load state
+ */
+ fun displayMap(mapConfig: MapConfig): CompletableFuture {
+ val finalMapLoadState = CompletableFuture()
+
+ webEngine.loadWorker.stateProperty().addListener { _, _, newValue ->
+
+ if (newValue == Worker.State.SUCCEEDED) {
+ executeMapSetupScripts(mapConfig)
+ }
+
+ if (newValue == Worker.State.SUCCEEDED || newValue == Worker.State.FAILED) {
+ finalMapLoadState.complete(newValue)
+ }
+ }
+
+ val localFileUrl: URL = LeafletMapView::class.java.getResource("/leafletmap/leafletmap.html")
+ webEngine.load(localFileUrl.toExternalForm())
+ return finalMapLoadState
+ }
+
+ 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: 5,
+ | zoomControl: false,
+ | layers: [layer1]
+ |});
+ |
+ |var markersGroup = L.featureGroup();
+ |myMap.addLayer(markersGroup);
+ |var trackGroup = L.featureGroup();
+ |myMap.addLayer(trackGroup);
+ |
+ |myMap.addEventListener("contextmenu", function(e){});
+ |var attribution = myMap.attributionControl;
+ |attribution.setPrefix('Leaflet');""".trimMargin())
+
+ eventZoomChangeIcon()
+
+ // 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 the view of the map to the specified geographical center position and zoom level.
+ *
+ * @param position map center position
+ * @param zoomLevel zoom level (0 - 19 for OpenStreetMap)
+ */
+ fun setView(position: LatLong, zoomLevel: Int) =
+ execScript("myMap.setView([${position.latitude}, ${position.longitude}], $zoomLevel);")
+
+ /**
+ * Pans the map to the specified geographical center position.
+ *
+ * @param position map center position
+ */
+ fun panTo(position: LatLong) =
+ execScript("myMap.panTo([${position.latitude}, ${position.longitude}]);")
+
+ /**
+ * Sets the zoom of the map to the specified level.
+ *
+ * @param zoomLevel zoom level (0 - 19 for OpenStreetMap)
+ */
+ fun setZoom(zoomLevel: Int) =
+ execScript("myMap.setZoom([$zoomLevel]);")
+
+ /**
+ * Adds a Marker Object to a map
+ *
+ * @param marker the Marker Object
+ */
+ fun addMarker(marker: Marker) {
+ marker.addToMap(getNextMarkerName(), this)
+ }
+
+ fun addCircle(circle: Circle) {
+ circle.addToMap(this)
+ }
+
+ fun addZone(zone: Zone) {
+ zone.addToMap(this)
+ }
+
+ /**
+ * Removes an existing marker from the map
+ *
+ * @param marker the Marker object
+ */
+ fun removeMarker(marker: Marker) {
+ execScript("myMap.removeLayer(${marker.getName()});")
+ }
+
+ fun removeCircle(circle: Circle) {
+ circle.removeCircle(this)
+ }
+
+ fun removeZone(zone: Zone) {
+ zone.removeZone()
+ }
+
+ fun removeZone(id: String) {
+ val idSanitized = id.replace("-", "")
+ execScript("myMap.removeLayer(polygon$idSanitized);")
+ }
+
+
+ fun uppdateCircle(circle: Circle, latLong: LatLong, radius: Double) {
+ circle.modifyCircle(latLong, radius)
+ circle.uppdateMap()
+ }
+
+ fun setEventMousePosition() {
+ execScript("var lat=0.0, lng=0.0;\n" +
+ "myMap.addEventListener('mousemove', function(ev) {\n" +
+ " lat = ev.latlng.lat;\n" +
+ " lng = ev.latlng.lng;\n" +
+ "});"
+ )
+ }
+
+ fun getMousePosition(): LatLong {
+ val lat = execScript("lat;") as Double
+ val lng = execScript("lng;") as Double
+ return LatLong(lat, lng)
+ }
+
+ /**
+ * Adds a custom marker type
+ *
+ * @param markerName the name of the marker type
+ * @param iconUrl the url if the marker icon
+ */
+ fun addCustomMarker(markerName: String, iconUrl: String): String {
+ execScript("var $markerName = L.icon({\n" +
+ "iconUrl: '${createImage(iconUrl, "png")}',\n" +
+ "iconSize: [24, 24],\n" +
+ "iconAnchor: [12, 12],\n" +
+ "});")
+ return markerName
+ }
+
+ private fun createImage(path: String, type: String): String {
+ val image = ImageIO.read(File(path))
+ var imageString: String? = null
+ val bos = ByteArrayOutputStream()
+
+ try {
+ ImageIO.write(image, type, bos)
+ val imageBytes = bos.toByteArray()
+
+ val encoder = Base64.getEncoder()
+ imageString = encoder.encodeToString(imageBytes)
+
+ bos.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ return "data:image/$type;base64,$imageString"
+ }
+
+ /**
+ * Sets the onMarkerClickListener
+ *
+ * @param listener the onMarerClickEventListener
+ */
+ fun onMarkerClick(listener: MarkerClickEventListener) {
+ val win = execScript("document") as JSObject
+ win.setMember("java", this)
+ markerClickEvent.addListener(listener)
+ }
+
+ /**
+ * Handles the callback from the markerClickEvent
+ */
+ fun markerClick(title: String) {
+ markerClickEvent.MarkerClickEvent(title)
+ }
+
+ /**
+ * Sets the onMapMoveListener
+ *
+ * @param listener the MapMoveEventListener
+ */
+ fun onMapMove(listener: MapMoveEventListener) {
+ val win = execScript("document") as JSObject
+ win.setMember("java", this)
+ execScript("myMap.on('moveend', function(e){ document.java.mapMove(myMap.getCenter().lat, myMap.getCenter().lng);});")
+ mapMoveEvent.addListener(listener)
+ }
+
+ /**
+ * Handles the callback from the mapMoveEvent
+ */
+ fun mapMove(lat: Double, lng: Double) {
+ val latlng = LatLong(lat, lng)
+ mapMoveEvent.MapMoveEvent(latlng)
+ }
+
+ /**
+ * Sets the onMapClickListener
+ *
+ * @param listener the onMapClickEventListener
+ */
+ fun onMapClick(listener: MapClickEventListener) {
+ val win = execScript("document") as JSObject
+ win.setMember("java", this)
+ execScript("myMap.on('click', function(e){ document.java.mapClick(e.latlng.lat, e.latlng.lng);});")
+ mapClickEvent.addListener(listener)
+ }
+
+ /**
+ * Handles the callback from the mapClickEvent
+ */
+ fun mapClick(lat: Double, lng: Double) {
+ val latlng = LatLong(lat, lng)
+ mapClickEvent.MapClickEvent(latlng)
+ }
+
+ /**
+ * Draws a track path along the specified positions.
+ *
+ * @param positions list of track positions
+ */
+ fun addTrack(positions: List) {
+
+ 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);""".trimMargin())
+ }
+
+ fun addTrack(positions: List, id: String, color: Color, tooltip: String) {
+
+ val jsPositions = positions
+ .map { " [${it.latitude}, ${it.longitude}]" }
+ .joinToString(", \n")
+
+ val cleanTooltip = tooltip.replace("'", "'")
+ execScript("""
+ |var latLngs = [
+ |$jsPositions
+ |];
+ |var color = "rgb(${Math.floor(color.getRed() * 255).toInt()} ,${Math.floor(color.getGreen() * 255).toInt()},${Math.floor(color.getBlue() * 255).toInt()})";
+ |var polyline$id = L.polyline(latLngs, {color: color, weight: 2, zIndexOffset: 200}).bindTooltip('$cleanTooltip', {sticky: true}).addTo(trackGroup)""".trimMargin())
+ }
+
+ fun makeVesselTrackTransparent(id: String) {
+ execScript("polyline$id.setStyle({opacity: 0.5});")
+ }
+
+ fun highlightTrack(id: String) {
+ execScript("polyline$id.setStyle({weight: 4});")
+ }
+
+ fun normalizeVesselTrack(id: String) {
+ execScript("polyline$id.setStyle({opacity: 1,weight: 2});")
+ }
+
+ fun eventZoomChangeIcon() {
+ execScript("""
+ |myMap.on('zoomend', function() {
+ |var currentZoom = myMap.getZoom();
+ |if (currentZoom < $zoomLimitSmallMarker) {
+ |markersGroup.eachLayer(function(layer) {
+ return layer.setIcon(aircraftSmallIcon)
+ |});
+ |} else {
+ |markersGroup.eachLayer(function(layer) {
+ return layer.setIcon(aircraftIcon)
+ |});
+ |}
+ |});
+ """.trimMargin())
+ }
+
+ fun removeTrack(id: String) {
+ execScript("myMap.removeLayer(polyline$id);")
+ }
+
+ fun fitBoundsMarkers() {
+ execScript("setTimeout(() => {myMap.fitBounds(markersGroup.getBounds().pad(0.05));}, 500);")
+ }
+
+ fun addZone(polygon: Polygon, id: String, color: Color) {
+ val points = polygon.points
+ val latLongs = arrayListOf()
+ var lat: Double
+ var lon = 0.0
+
+ for (i in 0 until points.size) {
+ if (i % 2 == 0) {
+ lon = points[i]
+ } else {
+ lat = points[i]
+ latLongs.add(LatLong(lat, lon))
+ }
+ }
+
+ val jsPositions = latLongs
+ .map { " [${it.latitude}, ${it.longitude}]" }
+ .joinToString(", \n")
+ val idSanitized = id.replace("-", "")
+ execScript("""
+ |var latLngs = [
+ |$jsPositions
+ |];
+ |var color = "rgb(${Math.floor(color.getRed() * 255).toInt()} ,${Math.floor(color.getGreen() * 255).toInt()},${Math.floor(color.getBlue() * 255).toInt()})";
+ |var polygon$idSanitized = L.polygon(latLngs, {color: color}).addTo(myMap);""".trimMargin())
+
+ }
+
+ internal fun execScript(script: String) = webEngine.executeScript(script)
+
+ private fun getNextMarkerName(): String = "marker${varNameSuffix++}"
+}
diff --git a/src/main/kotlin/map/MapConfig.kt b/src/main/kotlin/map/MapConfig.kt
new file mode 100644
index 0000000..b8f7f99
--- /dev/null
+++ b/src/main/kotlin/map/MapConfig.kt
@@ -0,0 +1,20 @@
+package fdit.leafletmap
+
+/**
+ * Class for defining the layers and controls in the map to be shown.
+ *
+ * @property layers 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.
+ * @property zoomControlConfig Zoom control definition, by default it's shown in the top left corner.
+ * @property scaleControlConfig Scale control definition, by default it's not shown.
+ * @property initialCenter Initial center position of the map (default is London city).
+ *
+ * @author Stefan Saring
+ */
+class MapConfig @JvmOverloads constructor(
+
+ val layers: List = listOf(MapLayer.OPENSTREETMAP),
+ val zoomControlConfig: ZoomControlConfig = ZoomControlConfig(),
+ val scaleControlConfig: ScaleControlConfig = ScaleControlConfig(),
+ val initialCenter: LatLong = LatLong(0.0, 0.0)
+)
\ No newline at end of file
diff --git a/src/main/kotlin/map/MapLayer.kt b/src/main/kotlin/map/MapLayer.kt
new file mode 100644
index 0000000..ed3e412
--- /dev/null
+++ b/src/main/kotlin/map/MapLayer.kt
@@ -0,0 +1,40 @@
+package fdit.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 © OpenStreetMap and contributors', noWrap: true
+ })"""),
+
+ /** OpenCycleMap layer. */
+ OPENCYCLEMAP("OpenCycleMap", """
+ L.tileLayer('http://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png', {
+ attribution: '© OpenCycleMap, Map data © OpenStreetMap contributors', noWrap: true
+ })"""),
+
+ /** 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: '© HikeBikeMap.org, Map data © OpenStreetMap and contributors', noWrap: true
+ })"""),
+
+ /** MTB map (mtbmap.cz). */
+ MTB_MAP("MTB Map", """
+ L.tileLayer('http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap and USGS', noWrap: true
+ })"""),
+
+ /** 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 © OpenStreetMap contributors, Imagery © Mapbox', noWrap: true
+ })""")
+}
diff --git a/src/main/kotlin/map/Marker.kt b/src/main/kotlin/map/Marker.kt
new file mode 100644
index 0000000..b2ce16a
--- /dev/null
+++ b/src/main/kotlin/map/Marker.kt
@@ -0,0 +1,142 @@
+package fdit.leafletmap
+
+import fdit.gui.graphicalScenarioEditor.GraphicalScenarioEditorContext
+import fdit.gui.utils.tooltip.VesselTooltipUtils.formatVesselSnapshotTooltip
+import fdit.metamodel.vessel.Vessel
+
+/**
+ * Creates a marker at the specified geographical position.
+ *
+ * @author Niklas Kellner
+ *
+ * @param position marker position
+ * @param title marker title shown in tooltip (pass empty string when tooltip not needed)
+ * @param zIndexOffset zIndexOffset (higher number means on top)
+ *
+ */
+class Marker private constructor(private var position: LatLong, private var zIndexOffset: Int) {
+ private var marker = "aircraftIcon"
+ private var markerSmall = "aircraftSmallIcon"
+ private lateinit var map: LeafletMapView
+ private var attached = false
+ private var clickable = false
+ private var name = ""
+ private var tooltip = ""
+ private var rotation = 0
+ private lateinit var aircraft: Vessel
+ private lateinit var context: GraphicalScenarioEditorContext
+ private var relativeDate: Double = 0.0
+
+
+ constructor(position: LatLong, aircraft: Vessel, relativeDate: Double, context: GraphicalScenarioEditorContext, aircraftIcon: String, zIndexOffset: Int) : this(position, zIndexOffset){
+ this.aircraft = aircraft
+ this.context = context
+ this.relativeDate = relativeDate
+ this.marker = aircraftIcon
+ }
+
+ /**
+ * Adds the marker to a map, gets called from the mapAddMarker
+ *
+ * @param nextMarkerName the variable name of the marker
+ * @param map the LeafetMapView
+ */
+ internal fun addToMap(nextMarkerName: String, map: LeafletMapView) {
+ this.name = nextMarkerName
+ this.map = map
+ this.attached = true
+ map.execScript("""
+ |var currentZoom = myMap.getZoom();
+ |var $name;
+ |if (currentZoom < ${map.zoomLimitSmallMarker}) {
+ |$name = L.marker([${position.latitude}, ${position.longitude}], {title: '', icon: $markerSmall, zIndexOffset: $zIndexOffset}).addTo(markersGroup);
+ |} else {
+ |$name = L.marker([${position.latitude}, ${position.longitude}], {title: '', icon: $marker, zIndexOffset: $zIndexOffset}).addTo(markersGroup);
+ |}
+ """.trimMargin())
+ setTooltip()
+ if (clickable) {
+ setClickable()
+ }
+ }
+
+ fun setTooltip() {
+ this.tooltip = formatVesselSnapshotTooltip(aircraft,
+ context.getGraphicalScenario().getRecording(),
+ relativeDate)
+ this.tooltip = tooltip.replace("\n", "
")
+ this.tooltip = tooltip.replace("'", "'")
+ map.execScript("$name.bindTooltip('${this.tooltip}
');")
+ }
+
+
+ /**
+ * Changes the icon of the marker
+ *
+ * @param newIcon the name of the new icon
+ */
+ fun changeIcon(newIcon: String) {
+ this.marker = newIcon
+ if (attached) {
+ map.execScript("$name.setIcon($marker);")
+ }
+ }
+
+ /**
+ * Changes the icon of the marker
+ *
+ * @param newIcon the new ColorMarker
+ */
+ fun changeIcon(newIcon: ColorMarker) {
+ this.marker = newIcon.iconName
+ if (attached) {
+ map.execScript("$name.setIcon(${newIcon.iconName});")
+ }
+ }
+
+ /**
+ * Moves the existing marker specified by the variable name to the new geographical position.
+ *
+ * @param position new marker position
+ */
+ fun move(position: LatLong) {
+ this.position = position
+ if (attached) {
+ map.execScript("$name.setLatLng([${this.position.latitude}, ${this.position.longitude}]);")
+ setTooltip()
+ }
+ }
+
+ fun move(position: LatLong, aircraft: Vessel, relativeDate: Double) {
+ this.aircraft = aircraft
+ this.relativeDate = relativeDate
+ this.position = position
+ if (attached) {
+ map.execScript("$name.setLatLng([${this.position.latitude}, ${this.position.longitude}]);")
+ }
+ }
+
+ fun setRotation(rotation: Int) {
+ if (rotation > 360 || rotation < 0) {
+ this.rotation = 0
+ } else {
+ this.rotation = rotation
+ }
+ if (attached) {
+ map.execScript("$name.setRotationAngle(${this.rotation})")
+ }
+ }
+
+
+ /**
+ * Sets the marker clickable
+ */
+ private fun setClickable() {
+ this.clickable = true
+ if (attached) {
+ map.execScript("$name.on('click', function(e){ document.java.markerClick($name.options.title)})")
+ }
+ }
+
+ internal fun getName(): String = this.name
+}
\ No newline at end of file
diff --git a/src/main/kotlin/map/ScaleControlConfig.kt b/src/main/kotlin/map/ScaleControlConfig.kt
new file mode 100644
index 0000000..0d6eb64
--- /dev/null
+++ b/src/main/kotlin/map/ScaleControlConfig.kt
@@ -0,0 +1,11 @@
+package fdit.leafletmap
+
+/**
+ * Class for defining the scale control of the map. The scale can show either metric or imperial units.
+
+ * @author Stefan Saring
+ */
+class ScaleControlConfig @JvmOverloads constructor(
+ val show: Boolean = false,
+ val position: ControlPosition = ControlPosition.BOTTOM_LEFT,
+ val metric: Boolean = true)
\ No newline at end of file
diff --git a/src/main/kotlin/map/Zone.kt b/src/main/kotlin/map/Zone.kt
new file mode 100644
index 0000000..869ceb2
--- /dev/null
+++ b/src/main/kotlin/map/Zone.kt
@@ -0,0 +1,59 @@
+package fdit.leafletmap
+
+class Zone constructor(private var title: String) {
+ private lateinit var map: LeafletMapView
+ private var isAttached = false
+ private var isDisplayed = false
+ private var positions = listOf()
+
+
+ fun addToMap(map: LeafletMapView) {
+ this.map = map
+
+ if (map.execScript("typeof zone$title == 'undefined';") as Boolean) {
+ map.execScript("var zone$title")
+ }
+ if (!this.isAttached) {
+
+ map.execScript("var points$title = [];" +
+ "zone$title = L.polygon(points$title).addTo(myMap);")
+ this.isAttached = true
+ this.isDisplayed = true
+ } else if (!this.isDisplayed) {
+ map.execScript("zone$title.addTo(myMap);")
+ this.isDisplayed = true
+ }
+ }
+
+ private fun addPoint(latLong: LatLong) {
+ map.execScript("points$title.push([${latLong.latitude}, ${latLong.longitude}]);")
+ }
+
+ fun updatePoints(positions: List) {
+ this.positions = positions
+ if (map.execScript("typeof points$title == 'undefined'") as Boolean) {
+ map.execScript("var points$title = [];")
+ } else {
+ map.execScript("points$title = [];")
+ }
+ for (position in positions) {
+ addPoint(position)
+ }
+ }
+
+ fun updateMap() {
+ if (this.isAttached) {
+ map.execScript("myMap.removeLayer(zone$title);" +
+ "zone$title = L.polygon(points$title).addTo(myMap);")
+ this.isDisplayed = true
+ }
+ }
+
+ internal fun removeZone() {
+ if (this.isAttached && this.isDisplayed) {
+ map.execScript("myMap.removeLayer(zone$title);")
+ this.isDisplayed = false
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/map/ZoomControlConfig.kt b/src/main/kotlin/map/ZoomControlConfig.kt
new file mode 100644
index 0000000..3441826
--- /dev/null
+++ b/src/main/kotlin/map/ZoomControlConfig.kt
@@ -0,0 +1,10 @@
+package fdit.leafletmap
+
+/**
+ * Class for defining the zoom control of the map.
+
+ * @author Stefan Saring
+ */
+class ZoomControlConfig @JvmOverloads constructor(
+ val show: Boolean = true,
+ val position: ControlPosition = ControlPosition.TOP_LEFT)
\ No newline at end of file
diff --git a/src/main/kotlin/map/events/MapClickEvent.kt b/src/main/kotlin/map/events/MapClickEvent.kt
new file mode 100644
index 0000000..360da13
--- /dev/null
+++ b/src/main/kotlin/map/events/MapClickEvent.kt
@@ -0,0 +1,26 @@
+package fdit.leafletmap.events
+
+import fdit.leafletmap.LatLong
+import java.util.*
+
+/**
+ * Handles the MapClickEvent
+ * @author Niklas Kellner
+ */
+interface MapClickEventListener {
+ fun onMapClick(latLong: LatLong)
+}
+
+internal class MapClickEventMaker {
+ private val listeners = ArrayList()
+
+ fun addListener(toAdd: MapClickEventListener) {
+ listeners.add(toAdd)
+ }
+
+ fun MapClickEvent(latLong: LatLong) {
+ // Notify everybody that may be interested.
+ for (hl in listeners)
+ hl.onMapClick(latLong)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/map/events/MapMoveEvent.kt b/src/main/kotlin/map/events/MapMoveEvent.kt
new file mode 100644
index 0000000..f0b4a66
--- /dev/null
+++ b/src/main/kotlin/map/events/MapMoveEvent.kt
@@ -0,0 +1,27 @@
+package fdit.leafletmap.events
+
+import fdit.leafletmap.LatLong
+import java.util.*
+
+/**
+ * Handles the MapMoveEvent
+ *
+ * @author Niklas Kellner
+ */
+interface MapMoveEventListener {
+ fun onMapMove(center: LatLong)
+}
+
+internal class MapMoveEventMaker {
+ private val listeners = ArrayList()
+
+ fun addListener(toAdd: MapMoveEventListener) {
+ listeners.add(toAdd)
+ }
+
+ fun MapMoveEvent(latLong: LatLong) {
+ // Notify everybody that may be interested.
+ for (hl in listeners)
+ hl.onMapMove(latLong)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/map/events/MarkerClickEvent.kt b/src/main/kotlin/map/events/MarkerClickEvent.kt
new file mode 100644
index 0000000..e5bf43b
--- /dev/null
+++ b/src/main/kotlin/map/events/MarkerClickEvent.kt
@@ -0,0 +1,33 @@
+package fdit.leafletmap.events
+
+import java.util.*
+
+/**
+ * Handles the MarkerClickEvent
+ *
+ * @author Niklas Kellner
+ */
+interface MarkerClickEventListener {
+ fun onMarkerClick(title: String)
+}
+
+internal class MarkerClickEventMaker {
+ private val listeners = ArrayList()
+ private var listenerSet = false
+
+ fun addListener(toAdd: MarkerClickEventListener) {
+ listeners.add(toAdd)
+ listenerSet = true
+ }
+
+ fun MarkerClickEvent(title: String){
+ // Notify everybody that may be interested.
+ for (hl in listeners)
+ hl.onMarkerClick(title)
+ }
+
+
+ fun isListenerSet(): Boolean{
+ return listenerSet
+ }
+}
\ No newline at end of file