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 Side-by-side 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
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);") | |
52 | + this.isDisplayed = false | |
53 | + } | |
54 | + } | |
55 | + | |
56 | + private fun nauticalMilesToMeter(nauticalMiles: Double): Double { | |
57 | + return nauticalMiles * 1.852 | |
58 | + } | |
59 | + | |
60 | +} |
src/main/kotlin/map/ColorMarker.kt
View file @
d06a68e
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"), | |
17 | + BLACK_MARKER("blackIcon") | |
18 | +} |
src/main/kotlin/map/ControlPosition.kt
View file @
d06a68e
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"), | |
13 | + BOTTOM_RIGHT("bottomright") | |
14 | +} |
src/main/kotlin/map/LatLong.kt
View file @
d06a68e
src/main/kotlin/map/LeafletMapView.kt
View file @
d06a68e
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});") | |
349 | + } | |
350 | + | |
351 | + fun normalizeVesselTrack(id: String) { | |
352 | + execScript("polyline$id.setStyle({opacity: 1,weight: 2});") | |
353 | + } | |
354 | + | |
355 | + fun eventZoomChangeIcon() { | |
356 | + execScript(""" | |
357 | + |myMap.on('zoomend', function() { | |
358 | + |var currentZoom = myMap.getZoom(); | |
359 | + |if (currentZoom < $zoomLimitSmallMarker) { | |
360 | + |markersGroup.eachLayer(function(layer) { | |
361 | + return layer.setIcon(aircraftSmallIcon) | |
362 | + |}); | |
363 | + |} else { | |
364 | + |markersGroup.eachLayer(function(layer) { | |
365 | + return layer.setIcon(aircraftIcon) | |
366 | + |}); | |
367 | + |} | |
368 | + |}); | |
369 | + """.trimMargin()) | |
370 | + } | |
371 | + | |
372 | + fun removeTrack(id: String) { | |
373 | + execScript("myMap.removeLayer(polyline$id);") | |
374 | + } | |
375 | + | |
376 | + fun fitBoundsMarkers() { | |
377 | + execScript("setTimeout(() => {myMap.fitBounds(markersGroup.getBounds().pad(0.05));}, 500);") | |
378 | + } | |
379 | + | |
380 | + fun addZone(polygon: Polygon, id: String, color: Color) { | |
381 | + val points = polygon.points | |
382 | + val latLongs = arrayListOf<LatLong>() | |
383 | + var lat: Double | |
384 | + var lon = 0.0 | |
385 | + | |
386 | + for (i in 0 until points.size) { | |
387 | + if (i % 2 == 0) { | |
388 | + lon = points[i] | |
389 | + } else { | |
390 | + lat = points[i] | |
391 | + latLongs.add(LatLong(lat, lon)) | |
392 | + } | |
393 | + } | |
394 | + | |
395 | + val jsPositions = latLongs | |
396 | + .map { " [${it.latitude}, ${it.longitude}]" } | |
397 | + .joinToString(", \n") | |
398 | + val idSanitized = id.replace("-", "") | |
399 | + execScript(""" | |
400 | + |var latLngs = [ | |
401 | + |$jsPositions | |
402 | + |]; | |
403 | + |var color = "rgb(${Math.floor(color.getRed() * 255).toInt()} ,${Math.floor(color.getGreen() * 255).toInt()},${Math.floor(color.getBlue() * 255).toInt()})"; | |
404 | + |var polygon$idSanitized = L.polygon(latLngs, {color: color}).addTo(myMap);""".trimMargin()) | |
405 | + | |
406 | + } | |
407 | + | |
408 | + internal fun execScript(script: String) = webEngine.executeScript(script) | |
409 | + | |
410 | + private fun getNextMarkerName(): String = "marker${varNameSuffix++}" | |
411 | +} |
src/main/kotlin/map/MapConfig.kt
View file @
d06a68e
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(), | |
19 | + val initialCenter: LatLong = LatLong(0.0, 0.0) | |
20 | +) |
src/main/kotlin/map/MapLayer.kt
View file @
d06a68e
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!). */ | |
35 | + MAPBOX("MapBox", """ | |
36 | + L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', { | |
37 | + id: 'mapbox.streets', | |
38 | + attribution: 'Map data © OpenStreetMap contributors, Imagery © Mapbox', noWrap: true | |
39 | + })""") | |
40 | +} |
src/main/kotlin/map/Marker.kt
View file @
d06a68e
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 | + } | |
128 | + } | |
129 | + | |
130 | + | |
131 | + /** | |
132 | + * Sets the marker clickable | |
133 | + */ | |
134 | + private fun setClickable() { | |
135 | + this.clickable = true | |
136 | + if (attached) { | |
137 | + map.execScript("$name.on('click', function(e){ document.java.markerClick($name.options.title)})") | |
138 | + } | |
139 | + } | |
140 | + | |
141 | + internal fun getName(): String = this.name | |
142 | +} |
src/main/kotlin/map/ScaleControlConfig.kt
View file @
d06a68e
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, | |
10 | + val position: ControlPosition = ControlPosition.BOTTOM_LEFT, | |
11 | + val metric: Boolean = true) |
src/main/kotlin/map/Zone.kt
View file @
d06a68e
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 | + } | |
50 | + } | |
51 | + | |
52 | + internal fun removeZone() { | |
53 | + if (this.isAttached && this.isDisplayed) { | |
54 | + map.execScript("myMap.removeLayer(zone$title);") | |
55 | + this.isDisplayed = false | |
56 | + } | |
57 | + } | |
58 | + | |
59 | +} |
src/main/kotlin/map/ZoomControlConfig.kt
View file @
d06a68e
src/main/kotlin/map/events/MapClickEvent.kt
View file @
d06a68e
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) { | |
22 | + // Notify everybody that may be interested. | |
23 | + for (hl in listeners) | |
24 | + hl.onMapClick(latLong) | |
25 | + } | |
26 | +} |
src/main/kotlin/map/events/MapMoveEvent.kt
View file @
d06a68e
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) { | |
23 | + // Notify everybody that may be interested. | |
24 | + for (hl in listeners) | |
25 | + hl.onMapMove(latLong) | |
26 | + } | |
27 | +} |
src/main/kotlin/map/events/MarkerClickEvent.kt
View file @
d06a68e
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) | |
27 | + } | |
28 | + | |
29 | + | |
30 | + fun isListenerSet(): Boolean{ | |
31 | + return listenerSet | |
32 | + } | |
33 | +} |