Commit d06a68ec6652741effa395633fb202ce34163a51
1 parent
2b6c492d4d
Exists in
master
and in
1 other branch
add Leaflet Kotlin API
Showing 14 changed files with 879 additions and 0 deletions Inline Diff
- src/main/kotlin/map/Circle.kt
 - src/main/kotlin/map/ColorMarker.kt
 - src/main/kotlin/map/ControlPosition.kt
 - src/main/kotlin/map/LatLong.kt
 - src/main/kotlin/map/LeafletMapView.kt
 - src/main/kotlin/map/MapConfig.kt
 - src/main/kotlin/map/MapLayer.kt
 - src/main/kotlin/map/Marker.kt
 - src/main/kotlin/map/ScaleControlConfig.kt
 - src/main/kotlin/map/Zone.kt
 - src/main/kotlin/map/ZoomControlConfig.kt
 - src/main/kotlin/map/events/MapClickEvent.kt
 - src/main/kotlin/map/events/MapMoveEvent.kt
 - src/main/kotlin/map/events/MarkerClickEvent.kt
 
src/main/kotlin/map/Circle.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | import javafx.scene.paint.Color | |||
| 4 | ||||
| 5 | class Circle private constructor(private var center: LatLong, private var title: String, private var zIndexOffset: Int) { | |||
| 6 | private var color = Color(0.0, 0.0, 0.0, 0.0) | |||
| 7 | private lateinit var map: LeafletMapView | |||
| 8 | private var isAttached = false | |||
| 9 | private var isDisplayed = false | |||
| 10 | private var radius = 0.0 | |||
| 11 | ||||
| 12 | constructor(position: LatLong, radius: Double, title: String, color: Color, zIndexOffset: Int) : this(position, title, zIndexOffset) { | |||
| 13 | this.color = color | |||
| 14 | this.title = title.replace("-", "") | |||
| 15 | this.center = position | |||
| 16 | this.radius = nauticalMilesToMeter(radius) | |||
| 17 | } | |||
| 18 | ||||
| 19 | internal fun addToMap(map: LeafletMapView) { | |||
| 20 | this.map = map | |||
| 21 | if (map.execScript("typeof circle$title == 'undefined'") as Boolean) { | |||
| 22 | map.execScript("var circle$title;") | |||
| 23 | } | |||
| 24 | if (!this.isAttached) { | |||
| 25 | val hexColor = "%02x".format((color.red * 255).toInt()) + "%02x".format((color.green * 255).toInt()) + "%02x".format((color.blue * 255).toInt()) | |||
| 26 | map.execScript("circle$title = L.circle([${center.latitude}, ${center.longitude}], $radius, {color:'#$hexColor'}).addTo(myMap);") | |||
| 27 | this.isAttached = true | |||
| 28 | this.isDisplayed = true | |||
| 29 | } else if (!this.isDisplayed) { | |||
| 30 | map.execScript("circle$title.addTo(myMap)") | |||
| 31 | this.isDisplayed = true | |||
| 32 | } | |||
| 33 | } | |||
| 34 | ||||
| 35 | fun modifyCircle(latLong: LatLong, radius: Double) { | |||
| 36 | this.center = latLong | |||
| 37 | this.radius = radius | |||
| 38 | this.radius = nauticalMilesToMeter(radius) | |||
| 39 | } | |||
| 40 | ||||
| 41 | fun uppdateMap() { | |||
| 42 | if (this.isAttached && !this.isDisplayed) { | |||
| 43 | map.execScript("myMap.removeLayer(circle$title);" + | |||
| 44 | "circle$title = L.circle([${center.latitude}, ${center.longitude}], $radius).addTo(myMap);") | |||
| 45 | this.isDisplayed = true | |||
| 46 | } | |||
| 47 | } | |||
| 48 | ||||
| 49 | internal fun removeCircle(map: LeafletMapView) { | |||
| 50 | if (this.isAttached && this.isDisplayed) { | |||
| 51 | map.execScript("myMap.removeLayer(circle$title);") | 
src/main/kotlin/map/ColorMarker.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | /** | |||
| 4 | * Enumeration for all marker colors of the leaflet-color-markers JavaScript library. | |||
| 5 | * | |||
| 6 | * @author Stefan Saring | |||
| 7 | */ | |||
| 8 | enum class ColorMarker(val iconName: String) { | |||
| 9 | ||||
| 10 | BLUE_MARKER("blueIcon"), | |||
| 11 | RED_MARKER("redIcon"), | |||
| 12 | GREEN_MARKER("greenIcon"), | |||
| 13 | ORANGE_MARKER("orangeIcon"), | |||
| 14 | YELLOW_MARKER("yellowIcon"), | |||
| 15 | VIOLET_MARKER("violetIcon"), | |||
| 16 | GREY_MARKER("greyIcon"), | 
src/main/kotlin/map/ControlPosition.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | /** | |||
| 4 | * Enumeration for all possible map control positions. | |||
| 5 | * | |||
| 6 | * @author Stefan Saring | |||
| 7 | */ | |||
| 8 | enum class ControlPosition(val positionName: String) { | |||
| 9 | ||||
| 10 | TOP_LEFT("topleft"), | |||
| 11 | TOP_RIGHT("topright"), | |||
| 12 | BOTTOM_LEFT("bottomleft"), | 
src/main/kotlin/map/LatLong.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | /** | |||
| 4 | * Immutable value class for defining a geo position. | |||
| 5 | * | |||
| 6 | * @author Stefan Saring | |||
| 7 | */ | 
src/main/kotlin/map/LeafletMapView.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | import fdit.leafletmap.events.* | |||
| 4 | import fdit.leafletmap.events.MapClickEventMaker | |||
| 5 | import fdit.leafletmap.events.MapMoveEventMaker | |||
| 6 | import fdit.leafletmap.events.MarkerClickEventMaker | |||
| 7 | import javafx.concurrent.Worker | |||
| 8 | import javafx.scene.layout.StackPane | |||
| 9 | import javafx.scene.paint.Color | |||
| 10 | import javafx.scene.shape.Polygon | |||
| 11 | import javafx.scene.web.WebEngine | |||
| 12 | import javafx.scene.web.WebView | |||
| 13 | import fdit.leafletmap.events.* | |||
| 14 | import netscape.javascript.JSObject | |||
| 15 | import java.io.ByteArrayOutputStream | |||
| 16 | import java.io.File | |||
| 17 | import java.io.IOException | |||
| 18 | import java.net.URL | |||
| 19 | import java.util.* | |||
| 20 | import java.util.concurrent.CompletableFuture | |||
| 21 | import javax.imageio.ImageIO | |||
| 22 | ||||
| 23 | ||||
| 24 | /** | |||
| 25 | * JavaFX component for displaying OpenStreetMap based maps by using the Leaflet.js JavaScript library inside a WebView | |||
| 26 | * browser component.<br/> | |||
| 27 | * This component can be embedded most easily by placing it inside a StackPane, the component uses then the size of the | |||
| 28 | * parent automatically. | |||
| 29 | * | |||
| 30 | * @author Stefan Saring | |||
| 31 | * @author Niklas Kellner | |||
| 32 | */ | |||
| 33 | class LeafletMapView : StackPane() { | |||
| 34 | ||||
| 35 | private val webView = WebView() | |||
| 36 | private val webEngine: WebEngine = webView.engine | |||
| 37 | ||||
| 38 | private var varNameSuffix: Int = 1 | |||
| 39 | private val mapClickEvent = MapClickEventMaker() | |||
| 40 | private val markerClickEvent = MarkerClickEventMaker() | |||
| 41 | private val mapMoveEvent = MapMoveEventMaker() | |||
| 42 | internal val zoomLimitSmallMarker = 8 | |||
| 43 | ||||
| 44 | /** | |||
| 45 | * Creates the LeafletMapView component, it does not show any map yet. | |||
| 46 | */ | |||
| 47 | init { | |||
| 48 | this.children.add(webView) | |||
| 49 | } | |||
| 50 | ||||
| 51 | /** | |||
| 52 | * Displays the initial map in the web view. Needs to be called and complete before adding any markers or tracks. | |||
| 53 | * The returned CompletableFuture will provide the final map load state, the map can be used when the load has | |||
| 54 | * completed with state SUCCEEDED (use CompletableFuture#whenComplete() for waiting to complete). | |||
| 55 | * | |||
| 56 | * @param mapConfig configuration of the map layers and controls | |||
| 57 | * @return the CompletableFuture which will provide the final map load state | |||
| 58 | */ | |||
| 59 | fun displayMap(mapConfig: MapConfig): CompletableFuture<Worker.State> { | |||
| 60 | val finalMapLoadState = CompletableFuture<Worker.State>() | |||
| 61 | ||||
| 62 | webEngine.loadWorker.stateProperty().addListener { _, _, newValue -> | |||
| 63 | ||||
| 64 | if (newValue == Worker.State.SUCCEEDED) { | |||
| 65 | executeMapSetupScripts(mapConfig) | |||
| 66 | } | |||
| 67 | ||||
| 68 | if (newValue == Worker.State.SUCCEEDED || newValue == Worker.State.FAILED) { | |||
| 69 | finalMapLoadState.complete(newValue) | |||
| 70 | } | |||
| 71 | } | |||
| 72 | ||||
| 73 | val localFileUrl: URL = LeafletMapView::class.java.getResource("/leafletmap/leafletmap.html") | |||
| 74 | webEngine.load(localFileUrl.toExternalForm()) | |||
| 75 | return finalMapLoadState | |||
| 76 | } | |||
| 77 | ||||
| 78 | private fun executeMapSetupScripts(mapConfig: MapConfig) { | |||
| 79 | ||||
| 80 | // execute scripts for layer definition | |||
| 81 | mapConfig.layers.forEachIndexed { i, layer -> | |||
| 82 | execScript("var layer${i + 1} = ${layer.javaScriptCode};") | |||
| 83 | } | |||
| 84 | ||||
| 85 | val jsLayers = mapConfig.layers | |||
| 86 | .mapIndexed { i, layer -> "'${layer.displayName}': layer${i + 1}" } | |||
| 87 | .joinToString(", ") | |||
| 88 | execScript("var baseMaps = { $jsLayers };") | |||
| 89 | ||||
| 90 | // execute script for map view creation (Leaflet attribution must not be a clickable link) | |||
| 91 | execScript(""" | |||
| 92 | |var myMap = L.map('map', { | |||
| 93 | | center: new L.LatLng(${mapConfig.initialCenter.latitude}, ${mapConfig.initialCenter.longitude}), | |||
| 94 | | zoom: 5, | |||
| 95 | | zoomControl: false, | |||
| 96 | | layers: [layer1] | |||
| 97 | |}); | |||
| 98 | | | |||
| 99 | |var markersGroup = L.featureGroup(); | |||
| 100 | |myMap.addLayer(markersGroup); | |||
| 101 | |var trackGroup = L.featureGroup(); | |||
| 102 | |myMap.addLayer(trackGroup); | |||
| 103 | | | |||
| 104 | |myMap.addEventListener("contextmenu", function(e){}); | |||
| 105 | |var attribution = myMap.attributionControl; | |||
| 106 | |attribution.setPrefix('Leaflet');""".trimMargin()) | |||
| 107 | ||||
| 108 | eventZoomChangeIcon() | |||
| 109 | ||||
| 110 | // execute script for layer control definition if there are multiple layers | |||
| 111 | if (mapConfig.layers.size > 1) { | |||
| 112 | execScript(""" | |||
| 113 | |var overlayMaps = {}; | |||
| 114 | |L.control.layers(baseMaps, overlayMaps).addTo(myMap);""".trimMargin()) | |||
| 115 | ||||
| 116 | } | |||
| 117 | ||||
| 118 | // execute script for scale control definition | |||
| 119 | if (mapConfig.scaleControlConfig.show) { | |||
| 120 | execScript("L.control.scale({position: '${mapConfig.scaleControlConfig.position.positionName}', " + | |||
| 121 | "metric: ${mapConfig.scaleControlConfig.metric}, " + | |||
| 122 | "imperial: ${!mapConfig.scaleControlConfig.metric}})" + | |||
| 123 | ".addTo(myMap);") | |||
| 124 | } | |||
| 125 | ||||
| 126 | // execute script for zoom control definition | |||
| 127 | if (mapConfig.zoomControlConfig.show) { | |||
| 128 | execScript("L.control.zoom({position: '${mapConfig.zoomControlConfig.position.positionName}'})" + | |||
| 129 | ".addTo(myMap);") | |||
| 130 | } | |||
| 131 | } | |||
| 132 | ||||
| 133 | /** | |||
| 134 | * Sets the view of the map to the specified geographical center position and zoom level. | |||
| 135 | * | |||
| 136 | * @param position map center position | |||
| 137 | * @param zoomLevel zoom level (0 - 19 for OpenStreetMap) | |||
| 138 | */ | |||
| 139 | fun setView(position: LatLong, zoomLevel: Int) = | |||
| 140 | execScript("myMap.setView([${position.latitude}, ${position.longitude}], $zoomLevel);") | |||
| 141 | ||||
| 142 | /** | |||
| 143 | * Pans the map to the specified geographical center position. | |||
| 144 | * | |||
| 145 | * @param position map center position | |||
| 146 | */ | |||
| 147 | fun panTo(position: LatLong) = | |||
| 148 | execScript("myMap.panTo([${position.latitude}, ${position.longitude}]);") | |||
| 149 | ||||
| 150 | /** | |||
| 151 | * Sets the zoom of the map to the specified level. | |||
| 152 | * | |||
| 153 | * @param zoomLevel zoom level (0 - 19 for OpenStreetMap) | |||
| 154 | */ | |||
| 155 | fun setZoom(zoomLevel: Int) = | |||
| 156 | execScript("myMap.setZoom([$zoomLevel]);") | |||
| 157 | ||||
| 158 | /** | |||
| 159 | * Adds a Marker Object to a map | |||
| 160 | * | |||
| 161 | * @param marker the Marker Object | |||
| 162 | */ | |||
| 163 | fun addMarker(marker: Marker) { | |||
| 164 | marker.addToMap(getNextMarkerName(), this) | |||
| 165 | } | |||
| 166 | ||||
| 167 | fun addCircle(circle: Circle) { | |||
| 168 | circle.addToMap(this) | |||
| 169 | } | |||
| 170 | ||||
| 171 | fun addZone(zone: Zone) { | |||
| 172 | zone.addToMap(this) | |||
| 173 | } | |||
| 174 | ||||
| 175 | /** | |||
| 176 | * Removes an existing marker from the map | |||
| 177 | * | |||
| 178 | * @param marker the Marker object | |||
| 179 | */ | |||
| 180 | fun removeMarker(marker: Marker) { | |||
| 181 | execScript("myMap.removeLayer(${marker.getName()});") | |||
| 182 | } | |||
| 183 | ||||
| 184 | fun removeCircle(circle: Circle) { | |||
| 185 | circle.removeCircle(this) | |||
| 186 | } | |||
| 187 | ||||
| 188 | fun removeZone(zone: Zone) { | |||
| 189 | zone.removeZone() | |||
| 190 | } | |||
| 191 | ||||
| 192 | fun removeZone(id: String) { | |||
| 193 | val idSanitized = id.replace("-", "") | |||
| 194 | execScript("myMap.removeLayer(polygon$idSanitized);") | |||
| 195 | } | |||
| 196 | ||||
| 197 | ||||
| 198 | fun uppdateCircle(circle: Circle, latLong: LatLong, radius: Double) { | |||
| 199 | circle.modifyCircle(latLong, radius) | |||
| 200 | circle.uppdateMap() | |||
| 201 | } | |||
| 202 | ||||
| 203 | fun setEventMousePosition() { | |||
| 204 | execScript("var lat=0.0, lng=0.0;\n" + | |||
| 205 | "myMap.addEventListener('mousemove', function(ev) {\n" + | |||
| 206 | " lat = ev.latlng.lat;\n" + | |||
| 207 | " lng = ev.latlng.lng;\n" + | |||
| 208 | "});" | |||
| 209 | ) | |||
| 210 | } | |||
| 211 | ||||
| 212 | fun getMousePosition(): LatLong { | |||
| 213 | val lat = execScript("lat;") as Double | |||
| 214 | val lng = execScript("lng;") as Double | |||
| 215 | return LatLong(lat, lng) | |||
| 216 | } | |||
| 217 | ||||
| 218 | /** | |||
| 219 | * Adds a custom marker type | |||
| 220 | * | |||
| 221 | * @param markerName the name of the marker type | |||
| 222 | * @param iconUrl the url if the marker icon | |||
| 223 | */ | |||
| 224 | fun addCustomMarker(markerName: String, iconUrl: String): String { | |||
| 225 | execScript("var $markerName = L.icon({\n" + | |||
| 226 | "iconUrl: '${createImage(iconUrl, "png")}',\n" + | |||
| 227 | "iconSize: [24, 24],\n" + | |||
| 228 | "iconAnchor: [12, 12],\n" + | |||
| 229 | "});") | |||
| 230 | return markerName | |||
| 231 | } | |||
| 232 | ||||
| 233 | private fun createImage(path: String, type: String): String { | |||
| 234 | val image = ImageIO.read(File(path)) | |||
| 235 | var imageString: String? = null | |||
| 236 | val bos = ByteArrayOutputStream() | |||
| 237 | ||||
| 238 | try { | |||
| 239 | ImageIO.write(image, type, bos) | |||
| 240 | val imageBytes = bos.toByteArray() | |||
| 241 | ||||
| 242 | val encoder = Base64.getEncoder() | |||
| 243 | imageString = encoder.encodeToString(imageBytes) | |||
| 244 | ||||
| 245 | bos.close() | |||
| 246 | } catch (e: IOException) { | |||
| 247 | e.printStackTrace() | |||
| 248 | } | |||
| 249 | return "data:image/$type;base64,$imageString" | |||
| 250 | } | |||
| 251 | ||||
| 252 | /** | |||
| 253 | * Sets the onMarkerClickListener | |||
| 254 | * | |||
| 255 | * @param listener the onMarerClickEventListener | |||
| 256 | */ | |||
| 257 | fun onMarkerClick(listener: MarkerClickEventListener) { | |||
| 258 | val win = execScript("document") as JSObject | |||
| 259 | win.setMember("java", this) | |||
| 260 | markerClickEvent.addListener(listener) | |||
| 261 | } | |||
| 262 | ||||
| 263 | /** | |||
| 264 | * Handles the callback from the markerClickEvent | |||
| 265 | */ | |||
| 266 | fun markerClick(title: String) { | |||
| 267 | markerClickEvent.MarkerClickEvent(title) | |||
| 268 | } | |||
| 269 | ||||
| 270 | /** | |||
| 271 | * Sets the onMapMoveListener | |||
| 272 | * | |||
| 273 | * @param listener the MapMoveEventListener | |||
| 274 | */ | |||
| 275 | fun onMapMove(listener: MapMoveEventListener) { | |||
| 276 | val win = execScript("document") as JSObject | |||
| 277 | win.setMember("java", this) | |||
| 278 | execScript("myMap.on('moveend', function(e){ document.java.mapMove(myMap.getCenter().lat, myMap.getCenter().lng);});") | |||
| 279 | mapMoveEvent.addListener(listener) | |||
| 280 | } | |||
| 281 | ||||
| 282 | /** | |||
| 283 | * Handles the callback from the mapMoveEvent | |||
| 284 | */ | |||
| 285 | fun mapMove(lat: Double, lng: Double) { | |||
| 286 | val latlng = LatLong(lat, lng) | |||
| 287 | mapMoveEvent.MapMoveEvent(latlng) | |||
| 288 | } | |||
| 289 | ||||
| 290 | /** | |||
| 291 | * Sets the onMapClickListener | |||
| 292 | * | |||
| 293 | * @param listener the onMapClickEventListener | |||
| 294 | */ | |||
| 295 | fun onMapClick(listener: MapClickEventListener) { | |||
| 296 | val win = execScript("document") as JSObject | |||
| 297 | win.setMember("java", this) | |||
| 298 | execScript("myMap.on('click', function(e){ document.java.mapClick(e.latlng.lat, e.latlng.lng);});") | |||
| 299 | mapClickEvent.addListener(listener) | |||
| 300 | } | |||
| 301 | ||||
| 302 | /** | |||
| 303 | * Handles the callback from the mapClickEvent | |||
| 304 | */ | |||
| 305 | fun mapClick(lat: Double, lng: Double) { | |||
| 306 | val latlng = LatLong(lat, lng) | |||
| 307 | mapClickEvent.MapClickEvent(latlng) | |||
| 308 | } | |||
| 309 | ||||
| 310 | /** | |||
| 311 | * Draws a track path along the specified positions. | |||
| 312 | * | |||
| 313 | * @param positions list of track positions | |||
| 314 | */ | |||
| 315 | fun addTrack(positions: List<LatLong>) { | |||
| 316 | ||||
| 317 | val jsPositions = positions | |||
| 318 | .map { " [${it.latitude}, ${it.longitude}]" } | |||
| 319 | .joinToString(", \n") | |||
| 320 | ||||
| 321 | execScript(""" | |||
| 322 | |var latLngs = [ | |||
| 323 | |$jsPositions | |||
| 324 | |]; | |||
| 325 | |var polyline = L.polyline(latLngs, {color: 'red', weight: 2}).addTo(myMap);""".trimMargin()) | |||
| 326 | } | |||
| 327 | ||||
| 328 | fun addTrack(positions: List<LatLong>, id: String, color: Color, tooltip: String) { | |||
| 329 | ||||
| 330 | val jsPositions = positions | |||
| 331 | .map { " [${it.latitude}, ${it.longitude}]" } | |||
| 332 | .joinToString(", \n") | |||
| 333 | ||||
| 334 | val cleanTooltip = tooltip.replace("'", "'") | |||
| 335 | execScript(""" | |||
| 336 | |var latLngs = [ | |||
| 337 | |$jsPositions | |||
| 338 | |]; | |||
| 339 | |var color = "rgb(${Math.floor(color.getRed() * 255).toInt()} ,${Math.floor(color.getGreen() * 255).toInt()},${Math.floor(color.getBlue() * 255).toInt()})"; | |||
| 340 | |var polyline$id = L.polyline(latLngs, {color: color, weight: 2, zIndexOffset: 200}).bindTooltip('$cleanTooltip', {sticky: true}).addTo(trackGroup)""".trimMargin()) | |||
| 341 | } | |||
| 342 | ||||
| 343 | fun makeVesselTrackTransparent(id: String) { | |||
| 344 | execScript("polyline$id.setStyle({opacity: 0.5});") | |||
| 345 | } | |||
| 346 | ||||
| 347 | fun highlightTrack(id: String) { | |||
| 348 | execScript("polyline$id.setStyle({weight: 4});") | 
src/main/kotlin/map/MapConfig.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | /** | |||
| 4 | * Class for defining the layers and controls in the map to be shown. | |||
| 5 | * | |||
| 6 | * @property layers List of layers to be shown in the map, the default layer is OpenStreetMap. If more than one layer is | |||
| 7 | * specified, then a layer selection control will be shown in the top right corner. | |||
| 8 | * @property zoomControlConfig Zoom control definition, by default it's shown in the top left corner. | |||
| 9 | * @property scaleControlConfig Scale control definition, by default it's not shown. | |||
| 10 | * @property initialCenter Initial center position of the map (default is London city). | |||
| 11 | * | |||
| 12 | * @author Stefan Saring | |||
| 13 | */ | |||
| 14 | class MapConfig @JvmOverloads constructor( | |||
| 15 | ||||
| 16 | val layers: List<MapLayer> = listOf(MapLayer.OPENSTREETMAP), | |||
| 17 | val zoomControlConfig: ZoomControlConfig = ZoomControlConfig(), | |||
| 18 | val scaleControlConfig: ScaleControlConfig = ScaleControlConfig(), | 
src/main/kotlin/map/MapLayer.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | /** | |||
| 4 | * Enumeration for all supported map layers. | |||
| 5 | * | |||
| 6 | * @author Stefan Saring | |||
| 7 | */ | |||
| 8 | enum class MapLayer(val displayName: String, val javaScriptCode: String) { | |||
| 9 | ||||
| 10 | /** OpenStreetMap layer. */ | |||
| 11 | OPENSTREETMAP("OpenStreetMap", """ | |||
| 12 | L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |||
| 13 | attribution: 'Map data © OpenStreetMap and contributors', noWrap: true | |||
| 14 | })"""), | |||
| 15 | ||||
| 16 | /** OpenCycleMap layer. */ | |||
| 17 | OPENCYCLEMAP("OpenCycleMap", """ | |||
| 18 | L.tileLayer('http://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png', { | |||
| 19 | attribution: '© OpenCycleMap, Map data © OpenStreetMap contributors', noWrap: true | |||
| 20 | })"""), | |||
| 21 | ||||
| 22 | /** Hike & bike maps layer (HikeBikeMap.org). */ | |||
| 23 | HIKE_BIKE_MAP("Hike & Bike Map", """ | |||
| 24 | L.tileLayer('http://{s}.tiles.wmflabs.org/hikebike/{z}/{x}/{y}.png', { | |||
| 25 | attribution: '© HikeBikeMap.org, Map data © OpenStreetMap and contributors', noWrap: true | |||
| 26 | })"""), | |||
| 27 | ||||
| 28 | /** MTB map (mtbmap.cz). */ | |||
| 29 | MTB_MAP("MTB Map", """ | |||
| 30 | L.tileLayer('http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png', { | |||
| 31 | attribution: '© OpenStreetMap and USGS', noWrap: true | |||
| 32 | })"""), | |||
| 33 | ||||
| 34 | /** MapBox layer in streets mode (consider: a project specific access token is required!). */ | 
src/main/kotlin/map/Marker.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | import fdit.gui.graphicalScenarioEditor.GraphicalScenarioEditorContext | |||
| 4 | import fdit.gui.utils.tooltip.VesselTooltipUtils.formatVesselSnapshotTooltip | |||
| 5 | import fdit.metamodel.vessel.Vessel | |||
| 6 | ||||
| 7 | /** | |||
| 8 | * Creates a marker at the specified geographical position. | |||
| 9 | * | |||
| 10 | * @author Niklas Kellner | |||
| 11 | * | |||
| 12 | * @param position marker position | |||
| 13 | * @param title marker title shown in tooltip (pass empty string when tooltip not needed) | |||
| 14 | * @param zIndexOffset zIndexOffset (higher number means on top) | |||
| 15 | * | |||
| 16 | */ | |||
| 17 | class Marker private constructor(private var position: LatLong, private var zIndexOffset: Int) { | |||
| 18 | private var marker = "aircraftIcon" | |||
| 19 | private var markerSmall = "aircraftSmallIcon" | |||
| 20 | private lateinit var map: LeafletMapView | |||
| 21 | private var attached = false | |||
| 22 | private var clickable = false | |||
| 23 | private var name = "" | |||
| 24 | private var tooltip = "" | |||
| 25 | private var rotation = 0 | |||
| 26 | private lateinit var aircraft: Vessel | |||
| 27 | private lateinit var context: GraphicalScenarioEditorContext | |||
| 28 | private var relativeDate: Double = 0.0 | |||
| 29 | ||||
| 30 | ||||
| 31 | constructor(position: LatLong, aircraft: Vessel, relativeDate: Double, context: GraphicalScenarioEditorContext, aircraftIcon: String, zIndexOffset: Int) : this(position, zIndexOffset){ | |||
| 32 | this.aircraft = aircraft | |||
| 33 | this.context = context | |||
| 34 | this.relativeDate = relativeDate | |||
| 35 | this.marker = aircraftIcon | |||
| 36 | } | |||
| 37 | ||||
| 38 | /** | |||
| 39 | * Adds the marker to a map, gets called from the mapAddMarker | |||
| 40 | * | |||
| 41 | * @param nextMarkerName the variable name of the marker | |||
| 42 | * @param map the LeafetMapView | |||
| 43 | */ | |||
| 44 | internal fun addToMap(nextMarkerName: String, map: LeafletMapView) { | |||
| 45 | this.name = nextMarkerName | |||
| 46 | this.map = map | |||
| 47 | this.attached = true | |||
| 48 | map.execScript(""" | |||
| 49 | |var currentZoom = myMap.getZoom(); | |||
| 50 | |var $name; | |||
| 51 | |if (currentZoom < ${map.zoomLimitSmallMarker}) { | |||
| 52 | |$name = L.marker([${position.latitude}, ${position.longitude}], {title: '', icon: $markerSmall, zIndexOffset: $zIndexOffset}).addTo(markersGroup); | |||
| 53 | |} else { | |||
| 54 | |$name = L.marker([${position.latitude}, ${position.longitude}], {title: '', icon: $marker, zIndexOffset: $zIndexOffset}).addTo(markersGroup); | |||
| 55 | |} | |||
| 56 | """.trimMargin()) | |||
| 57 | setTooltip() | |||
| 58 | if (clickable) { | |||
| 59 | setClickable() | |||
| 60 | } | |||
| 61 | } | |||
| 62 | ||||
| 63 | fun setTooltip() { | |||
| 64 | this.tooltip = formatVesselSnapshotTooltip(aircraft, | |||
| 65 | context.getGraphicalScenario().getRecording(), | |||
| 66 | relativeDate) | |||
| 67 | this.tooltip = tooltip.replace("\n", "<br>") | |||
| 68 | this.tooltip = tooltip.replace("'", "'") | |||
| 69 | map.execScript("$name.bindTooltip('<div id=\"html_c92f9552ec164f36978869550cb44ffe\" style=\"width: 100.0%; height: 100.0%;\">${this.tooltip}</div>');") | |||
| 70 | } | |||
| 71 | ||||
| 72 | ||||
| 73 | /** | |||
| 74 | * Changes the icon of the marker | |||
| 75 | * | |||
| 76 | * @param newIcon the name of the new icon | |||
| 77 | */ | |||
| 78 | fun changeIcon(newIcon: String) { | |||
| 79 | this.marker = newIcon | |||
| 80 | if (attached) { | |||
| 81 | map.execScript("$name.setIcon($marker);") | |||
| 82 | } | |||
| 83 | } | |||
| 84 | ||||
| 85 | /** | |||
| 86 | * Changes the icon of the marker | |||
| 87 | * | |||
| 88 | * @param newIcon the new ColorMarker | |||
| 89 | */ | |||
| 90 | fun changeIcon(newIcon: ColorMarker) { | |||
| 91 | this.marker = newIcon.iconName | |||
| 92 | if (attached) { | |||
| 93 | map.execScript("$name.setIcon(${newIcon.iconName});") | |||
| 94 | } | |||
| 95 | } | |||
| 96 | ||||
| 97 | /** | |||
| 98 | * Moves the existing marker specified by the variable name to the new geographical position. | |||
| 99 | * | |||
| 100 | * @param position new marker position | |||
| 101 | */ | |||
| 102 | fun move(position: LatLong) { | |||
| 103 | this.position = position | |||
| 104 | if (attached) { | |||
| 105 | map.execScript("$name.setLatLng([${this.position.latitude}, ${this.position.longitude}]);") | |||
| 106 | setTooltip() | |||
| 107 | } | |||
| 108 | } | |||
| 109 | ||||
| 110 | fun move(position: LatLong, aircraft: Vessel, relativeDate: Double) { | |||
| 111 | this.aircraft = aircraft | |||
| 112 | this.relativeDate = relativeDate | |||
| 113 | this.position = position | |||
| 114 | if (attached) { | |||
| 115 | map.execScript("$name.setLatLng([${this.position.latitude}, ${this.position.longitude}]);") | |||
| 116 | } | |||
| 117 | } | |||
| 118 | ||||
| 119 | fun setRotation(rotation: Int) { | |||
| 120 | if (rotation > 360 || rotation < 0) { | |||
| 121 | this.rotation = 0 | |||
| 122 | } else { | |||
| 123 | this.rotation = rotation | |||
| 124 | } | |||
| 125 | if (attached) { | |||
| 126 | map.execScript("$name.setRotationAngle(${this.rotation})") | |||
| 127 | } | 
src/main/kotlin/map/ScaleControlConfig.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | /** | |||
| 4 | * Class for defining the scale control of the map. The scale can show either metric or imperial units. | |||
| 5 | ||||
| 6 | * @author Stefan Saring | |||
| 7 | */ | |||
| 8 | class ScaleControlConfig @JvmOverloads constructor( | |||
| 9 | val show: Boolean = false, | 
src/main/kotlin/map/Zone.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | class Zone constructor(private var title: String) { | |||
| 4 | private lateinit var map: LeafletMapView | |||
| 5 | private var isAttached = false | |||
| 6 | private var isDisplayed = false | |||
| 7 | private var positions = listOf<LatLong>() | |||
| 8 | ||||
| 9 | ||||
| 10 | fun addToMap(map: LeafletMapView) { | |||
| 11 | this.map = map | |||
| 12 | ||||
| 13 | if (map.execScript("typeof zone$title == 'undefined';") as Boolean) { | |||
| 14 | map.execScript("var zone$title") | |||
| 15 | } | |||
| 16 | if (!this.isAttached) { | |||
| 17 | ||||
| 18 | map.execScript("var points$title = [];" + | |||
| 19 | "zone$title = L.polygon(points$title).addTo(myMap);") | |||
| 20 | this.isAttached = true | |||
| 21 | this.isDisplayed = true | |||
| 22 | } else if (!this.isDisplayed) { | |||
| 23 | map.execScript("zone$title.addTo(myMap);") | |||
| 24 | this.isDisplayed = true | |||
| 25 | } | |||
| 26 | } | |||
| 27 | ||||
| 28 | private fun addPoint(latLong: LatLong) { | |||
| 29 | map.execScript("points$title.push([${latLong.latitude}, ${latLong.longitude}]);") | |||
| 30 | } | |||
| 31 | ||||
| 32 | fun updatePoints(positions: List<LatLong>) { | |||
| 33 | this.positions = positions | |||
| 34 | if (map.execScript("typeof points$title == 'undefined'") as Boolean) { | |||
| 35 | map.execScript("var points$title = [];") | |||
| 36 | } else { | |||
| 37 | map.execScript("points$title = [];") | |||
| 38 | } | |||
| 39 | for (position in positions) { | |||
| 40 | addPoint(position) | |||
| 41 | } | |||
| 42 | } | |||
| 43 | ||||
| 44 | fun updateMap() { | |||
| 45 | if (this.isAttached) { | |||
| 46 | map.execScript("myMap.removeLayer(zone$title);" + | |||
| 47 | "zone$title = L.polygon(points$title).addTo(myMap);") | |||
| 48 | this.isDisplayed = true | |||
| 49 | } | 
src/main/kotlin/map/ZoomControlConfig.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap | ||
| 2 | ||||
| 3 | /** | |||
| 4 | * Class for defining the zoom control of the map. | |||
| 5 | ||||
| 6 | * @author Stefan Saring | |||
| 7 | */ | |||
| 8 | class ZoomControlConfig @JvmOverloads constructor( | 
src/main/kotlin/map/events/MapClickEvent.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap.events | ||
| 2 | ||||
| 3 | import fdit.leafletmap.LatLong | |||
| 4 | import java.util.* | |||
| 5 | ||||
| 6 | /** | |||
| 7 | * Handles the MapClickEvent | |||
| 8 | * @author Niklas Kellner | |||
| 9 | */ | |||
| 10 | interface MapClickEventListener { | |||
| 11 | fun onMapClick(latLong: LatLong) | |||
| 12 | } | |||
| 13 | ||||
| 14 | internal class MapClickEventMaker { | |||
| 15 | private val listeners = ArrayList<MapClickEventListener>() | |||
| 16 | ||||
| 17 | fun addListener(toAdd: MapClickEventListener) { | |||
| 18 | listeners.add(toAdd) | |||
| 19 | } | |||
| 20 | ||||
| 21 | fun MapClickEvent(latLong: LatLong) { | 
src/main/kotlin/map/events/MapMoveEvent.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap.events | ||
| 2 | ||||
| 3 | import fdit.leafletmap.LatLong | |||
| 4 | import java.util.* | |||
| 5 | ||||
| 6 | /** | |||
| 7 | * Handles the MapMoveEvent | |||
| 8 | * | |||
| 9 | * @author Niklas Kellner | |||
| 10 | */ | |||
| 11 | interface MapMoveEventListener { | |||
| 12 | fun onMapMove(center: LatLong) | |||
| 13 | } | |||
| 14 | ||||
| 15 | internal class MapMoveEventMaker { | |||
| 16 | private val listeners = ArrayList<MapMoveEventListener>() | |||
| 17 | ||||
| 18 | fun addListener(toAdd: MapMoveEventListener) { | |||
| 19 | listeners.add(toAdd) | |||
| 20 | } | |||
| 21 | ||||
| 22 | fun MapMoveEvent(latLong: LatLong) { | 
src/main/kotlin/map/events/MarkerClickEvent.kt
View file @
d06a68e
| File was created | 1 | package fdit.leafletmap.events | ||
| 2 | ||||
| 3 | import java.util.* | |||
| 4 | ||||
| 5 | /** | |||
| 6 | * Handles the MarkerClickEvent | |||
| 7 | * | |||
| 8 | * @author Niklas Kellner | |||
| 9 | */ | |||
| 10 | interface MarkerClickEventListener { | |||
| 11 | fun onMarkerClick(title: String) | |||
| 12 | } | |||
| 13 | ||||
| 14 | internal class MarkerClickEventMaker { | |||
| 15 | private val listeners = ArrayList<MarkerClickEventListener>() | |||
| 16 | private var listenerSet = false | |||
| 17 | ||||
| 18 | fun addListener(toAdd: MarkerClickEventListener) { | |||
| 19 | listeners.add(toAdd) | |||
| 20 | listenerSet = true | |||
| 21 | } | |||
| 22 | ||||
| 23 | fun MarkerClickEvent(title: String){ | |||
| 24 | // Notify everybody that may be interested. | |||
| 25 | for (hl in listeners) | |||
| 26 | hl.onMarkerClick(title) |