From d06a68ec6652741effa395633fb202ce34163a51 Mon Sep 17 00:00:00 2001 From: lsagona Date: Fri, 17 Jul 2020 09:30:12 +0200 Subject: [PATCH] add Leaflet Kotlin API --- src/main/kotlin/map/Circle.kt | 60 ++++ src/main/kotlin/map/ColorMarker.kt | 18 ++ src/main/kotlin/map/ControlPosition.kt | 14 + src/main/kotlin/map/LatLong.kt | 8 + src/main/kotlin/map/LeafletMapView.kt | 411 ++++++++++++++++++++++++ src/main/kotlin/map/MapConfig.kt | 20 ++ src/main/kotlin/map/MapLayer.kt | 40 +++ src/main/kotlin/map/Marker.kt | 142 ++++++++ src/main/kotlin/map/ScaleControlConfig.kt | 11 + src/main/kotlin/map/Zone.kt | 59 ++++ src/main/kotlin/map/ZoomControlConfig.kt | 10 + src/main/kotlin/map/events/MapClickEvent.kt | 26 ++ src/main/kotlin/map/events/MapMoveEvent.kt | 27 ++ src/main/kotlin/map/events/MarkerClickEvent.kt | 33 ++ 14 files changed, 879 insertions(+) create mode 100644 src/main/kotlin/map/Circle.kt create mode 100644 src/main/kotlin/map/ColorMarker.kt create mode 100644 src/main/kotlin/map/ControlPosition.kt create mode 100644 src/main/kotlin/map/LatLong.kt create mode 100644 src/main/kotlin/map/LeafletMapView.kt create mode 100644 src/main/kotlin/map/MapConfig.kt create mode 100644 src/main/kotlin/map/MapLayer.kt create mode 100644 src/main/kotlin/map/Marker.kt create mode 100644 src/main/kotlin/map/ScaleControlConfig.kt create mode 100644 src/main/kotlin/map/Zone.kt create mode 100644 src/main/kotlin/map/ZoomControlConfig.kt create mode 100644 src/main/kotlin/map/events/MapClickEvent.kt create mode 100644 src/main/kotlin/map/events/MapMoveEvent.kt create mode 100644 src/main/kotlin/map/events/MarkerClickEvent.kt 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 -- 1.7.10.4