-
Notifications
You must be signed in to change notification settings - Fork 10
Architecture
This document provides a comprehensive overview of the Java Leaflet architecture, explaining the multi-module design, layer system, and key architectural patterns.
- Multi-Module Architecture
- Layer System
- Transport Layer
- Event System Architecture
- Builder Pattern
- Context Menu Architecture
- Design Patterns
- Platform Abstraction
Java Leaflet v2.0.0 introduces a clean multi-module Maven architecture:
java_leaflet/
├── jlmap-parent/ # Parent POM
├── jlmap-api/ # Core API
├── jlmap-fx/ # JavaFX implementation
├── jlmap-vaadin/ # Vaadin implementation
└── jlmap-vaadin-demo/ # Demo application
jlmap-fx ──────┐
├──> jlmap-api
jlmap-vaadin ──┘
jlmap-vaadin-demo ──> jlmap-vaadin ──> jlmap-api
Purpose: Provides platform-independent abstractions and models.
Exports:
-
io.github.makbn.jlmap
- Main package -
io.github.makbn.jlmap.layer
- Layer management -
io.github.makbn.jlmap.layer.leaflet
- Leaflet layer interfaces -
io.github.makbn.jlmap.listener
- Event listeners -
io.github.makbn.jlmap.model
- Data models -
io.github.makbn.jlmap.exception
- Custom exceptions -
io.github.makbn.jlmap.geojson
- GeoJSON support -
io.github.makbn.jlmap.engine
- Web engine abstractions -
io.github.makbn.jlmap.element.menu
- Context menu system -
io.github.makbn.jlmap.journey
- Journey system
Key Dependencies:
- Gson & Jackson for JSON processing
- SLF4J for logging
- JetBrains annotations
- SnakeYAML for YAML support
Purpose: JavaFX-specific implementation using WebView.
Key Classes:
-
io.github.makbn.jlmap.fx.JLMapView
- Main JavaFX component -
io.github.makbn.jlmap.fx.JLEngine
- JavaFX WebView wrapper -
io.github.makbn.jlmap.fx.element.menu.JavaFXContextMenuMediator
- Context menu support
Dependencies:
- jlmap-api
- JavaFX (controls, web, graphics, base)
- JDK jsobject module
Purpose: Vaadin component for web applications.
Key Classes:
-
io.github.makbn.jlmap.vaadin.JLMapView
- Main Vaadin component -
io.github.makbn.jlmap.vaadin.JLEngine
- Vaadin web component wrapper -
io.github.makbn.jlmap.vaadin.element.menu.VaadinContextMenuMediator
- Context menu support -
io.github.makbn.jlmap.vaadin.journey.VaadinJourneyPlayer
- Journey player
Dependencies:
- jlmap-api
- Vaadin Core (24.8.6+)
- Vaadin Spring Boot Starter (optional)
Java Leaflet organizes map functionality into four specialized layers, each with distinct responsibilities:
Responsibilities: User interface elements that appear on top of the map.
public interface LeafletUILayerInt {
// Markers
JLMarker addMarker(JLLatLng latLng, String text, boolean draggable);
void removeMarker(String id);
// Popups
JLPopup addPopup(JLLatLng latLng, String text);
JLPopup addPopup(JLLatLng latLng, String text, JLOptions options);
void removePopup(String id);
// Image overlays
JLImageOverlay addImage(JLBounds bounds, String url, JLOptions options);
void removeImage(String id);
}
Use Cases:
- Adding markers at points of interest
- Displaying informational popups
- Overlaying images on specific geographic areas
Responsibilities: Geometric shapes and vector graphics.
public interface LeafletVectorLayerInt {
// Polylines
JLPolyline addPolyline(JLLatLng[] vertices, JLOptions options);
JLMultiPolyline addMultiPolyline(JLLatLng[][] vertices, JLOptions options);
// Polygons
JLPolygon addPolygon(JLLatLng[][][] vertices, JLOptions options);
// Circles
JLCircle addCircle(JLLatLng center, int radius, JLOptions options);
JLCircleMarker addCircleMarker(JLLatLng center, int radius, JLOptions options);
// Removal
void removePolyline(String id);
void removePolygon(String id);
void removeCircle(String id);
}
Key Differences:
- JLCircle: Radius in meters (geographic), scales with zoom
- JLCircleMarker: Radius in pixels, maintains size regardless of zoom
Responsibilities: Programmatic control of map navigation and viewport.
public interface LeafletControlLayerInt {
// Zoom operations
void setZoom(int zoom);
void zoomIn(int delta);
void zoomOut(int delta);
void setZoomAround(JLLatLng latLng, int zoom);
// Pan operations
void panTo(JLLatLng latLng);
void flyTo(JLLatLng latLng, int zoom);
// Bounds operations
void fitBounds(JLBounds bounds);
void flyToBounds(JLBounds bounds);
void setMaxBounds(JLBounds bounds);
// Constraints
void setMinZoom(int zoom);
void setMaxZoom(int zoom);
}
Use Cases:
- Smooth camera transitions
- Constraining viewable area
- Programmatic navigation
Responsibilities: Loading and styling GeoJSON geographic data.
public interface LeafletGeoJsonLayerInt {
JLGeoJson addFromFile(File file, JLGeoJsonOptions options);
JLGeoJson addFromUrl(String url, JLGeoJsonOptions options);
JLGeoJson addFromContent(String content, JLGeoJsonOptions options);
void removeGeoJson(String id);
}
Advanced Features:
- Style Functions: Dynamic styling based on feature properties
- Filter Functions: Selective feature rendering
- Point-to-Layer: Custom marker creation for point features
The transport layer bridges Java code with JavaScript execution in the web engine.
Architecture:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Java Code │───>│ Transport Layer │───>│ JavaScript Code │
│ (JLObject) │ │ (JLTransporter) │ │ (Leaflet) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
│ Convert Result
▼
┌──────────────────┐
│ Java Result │
│ (CompletableFuture)│
└──────────────────┘
Interface:
public interface JLServerToClientTransporter<T> {
BiFunction<String, Boolean, T> serverToClientTransport();
<M> M convertResult(T value, Class<M> target);
default <M> M execute(JLTransportRequest request) {
String jsCode = generateJavaScript(request);
T rawResult = serverToClientTransport().apply(jsCode, request.returnable());
return convertResult(rawResult, request.resultClass());
}
}
Purpose: Encapsulates method calls from Java to JavaScript.
public record JLTransportRequest(
JLObject<?> self,
String function,
Class<?> clazz,
Object... params
) {
public static JLTransportRequest voidCall(JLObject<?> self, String function, Object... params) {
return new JLTransportRequest(self, function, Void.class, params);
}
public static <M> JLTransportRequest returnableCall(JLObject<?> self, String function,
Class<M> clazz, Object... params) {
return new JLTransportRequest(self, function, clazz, params);
}
}
Example Flow:
// Java: Set circle radius
circle.setRadius(1000);
// Generates Transport Request:
JLTransportRequest.voidCall(circle, "setRadius", 1000);
// Generates JavaScript:
"window.jlmap_objects['circle-uuid-123'].setRadius(1000);"
// Executed in WebView
transport.execute(request);
Operations that return values use CompletableFuture
:
// Get circle bounds asynchronously
CompletableFuture<JLBounds> boundsF future = circle.getBounds();
boundsFuture.thenAccept(bounds -> {
System.out.println("Bounds: " + bounds);
});
┌─────────────────┐
│ User Action │ (click, drag, etc.)
└────────┬────────┘
│
▼
┌─────────────────┐
│ JavaScript │ (Leaflet event)
│ Event Handler │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Bridge to Java │ (JSObject callback)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Event Parser │ (JLInteractionEventHandler)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Typed Event │ (ClickEvent, DragEvent, etc.)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Listener │ (OnJLActionListener)
└─────────────────┘
interface Event {
JLAction action();
}
// Concrete event types
record ClickEvent(JLAction action, JLLatLng center) implements Event
record DragEvent(JLAction action, JLLatLng center, JLBounds bounds, int zoom) implements Event
record MoveEvent(JLAction action, JLLatLng center, JLBounds bounds, int zoom) implements Event
record ZoomEvent(JLAction action, int zoom) implements Event
record ContextMenuEvent(JLAction action, double x, double y, boolean open, boolean close) implements Event
Maps Java actions to Leaflet JavaScript events:
public enum JLAction {
CLICK("click"),
DOUBLE_CLICK("dblclick"),
DRAG("drag"),
DRAG_START("dragstart"),
DRAG_END("dragend"),
ZOOM("zoom"),
MOVE("move"),
CONTEXT_MENU("contextmenu"),
// ... more events
}
marker.setOnActionListener((source, event) -> {
switch (event.action()) {
case CLICK -> {
ClickEvent clickEvent = (ClickEvent) event;
System.out.println("Clicked at: " + clickEvent.center());
}
case DRAG_END -> {
DragEvent dragEvent = (DragEvent) event;
System.out.println("Dragged to: " + dragEvent.center());
}
}
});
Java Leaflet uses a sophisticated builder pattern that serves dual purposes: building Java objects and generating JavaScript code.
Type Parameters:
-
M
: Model type (e.g., JLMarker) -
T
: Builder type (for self-typing)
Dual Build Methods:
public abstract class JLObjectBuilder<M extends JLObject<M>, T extends JLObjectBuilder<M, T>> {
// Builds JavaScript code
protected abstract String buildJsElement();
// Builds Java object
protected abstract M buildJLObject();
// Public build method
public M build() {
String jsCode = buildJsElement();
M object = buildJLObject();
object.setJsCode(jsCode);
return object;
}
}
Example: JLMarkerBuilder:
JLMarker marker = JLMarkerBuilder.create()
.latLng(new JLLatLng(51.5, -0.09))
.withOptions(JLOptions.builder()
.draggable(true)
.build())
.build();
// Generates JavaScript:
// "L.marker([51.5, -0.09], {draggable: true})"
// And creates Java object:
// JLMarker instance with same properties
public class JLMarkerBuilder extends JLObjectBuilder<JLMarker, JLMarkerBuilder> {
private JLLatLng latLng;
public JLMarkerBuilder latLng(JLLatLng latLng) {
this.latLng = latLng;
return this; // Returns concrete type, not base
}
@Override
protected JLMarkerBuilder self() {
return this;
}
}
This allows method chaining without losing type information.
The context menu system uses the Mediator Pattern to abstract platform-specific implementations.
┌──────────────────┐
│ JLHasContextMenu │ (interface)
└────────┬─────────┘
│
├──> JLMarker
├──> JLPolygon
├──> JLPolyline
└──> JLMap
│
▼
┌──────────────────┐
│ JLContextMenu │ (manages menu items)
└────────┬─────────┘
│
▼
┌───────────────────────┐
│ JLContextMenuMediator │ (interface)
└────────┬──────────────┘
│
├──> JavaFXContextMenuMediator
└──> VaadinContextMenuMediator
Mediators are discovered at runtime using ServiceLoader
:
META-INF/services/io.github.makbn.jlmap.element.menu.JLContextMenuMediator
├── io.github.makbn.jlmap.fx.element.menu.JavaFXContextMenuMediator
└── io.github.makbn.jlmap.vaadin.element.menu.VaadinContextMenuMediator
Discovery Algorithm:
- Load all available mediators via ServiceLoader
- Filter by platform availability (
isAvailable()
) - Filter by object type support (
supportsObjectType()
) - Sort by priority (
getPriority()
) - Cache the selected mediator
Separate interfaces for different responsibilities:
-
LeafletUILayerInt
- UI elements -
LeafletVectorLayerInt
- Vector graphics -
LeafletControlLayerInt
- Map control -
LeafletGeoJsonLayerInt
- GeoJSON data
Pluggable transport implementations:
-
JLServerToClientTransporter<Object>
for JavaFX -
JLServerToClientTransporter<JsonValue>
for Vaadin
Event listeners for user interactions:
-
OnJLActionListener
for object events -
OnJLContextMenuItemListener
for menu selections
Fluent object construction with self-typing:
-
JLMarkerBuilder
,JLPolygonBuilder
,JLOptionsBuilder
Platform abstraction for context menus:
-
JLContextMenuMediator
interface - Platform-specific implementations
Runtime service discovery:
JLContextMenuMediatorLocator
- Java ServiceLoader integration
Non-blocking operations:
-
CompletableFuture
for returnable operations -
toGeoJSON()
,getBounds()
,getAttribution()
Web Engine: Uses javafx.scene.web.WebView
JavaScript Execution:
Object result = webEngine.executeScript("window.jlmap_objects['id'].method()");
Event Bridge:
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("javaEventHandler", javaEventHandler);
Web Component: Uses Vaadin's WebComponent
API
JavaScript Execution:
JsonValue result = element.executeJs("return window.jlmap_objects[$0].method()", id);
Event Bridge:
element.addEventListener("custom-event", event -> {
JsonValue detail = event.getEventData().get("detail");
// Handle event
});
- Creation: Builder creates Java object + JS code
-
Registration: Object registered in
window.jlmap_objects
- Usage: Transport layer facilitates communication
-
Disposal:
remove()
method cleans up both sides
public void remove() {
// Remove from JavaScript
transport.execute(JLTransportRequest.voidCall(this, "remove"));
// Unregister from global map
transport.execute(JLTransportRequest.voidCall(this, "delete", "window.jlmap_objects['" + jLId + "']"));
// Clean up Java references
this.popup = null;
this.contextMenu = null;
}
Context menus are created only when first accessed:
public JLContextMenu<T> getContextMenu() {
if (contextMenu == null) {
synchronized (this) {
if (contextMenu == null) {
contextMenu = new JLContextMenu<>(self());
registerContextMenuWithMediator();
}
}
}
return contextMenu;
}
Mediators are discovered once and cached:
private final Map<Class<?>, JLContextMenuMediator> mediatorCache = new ConcurrentHashMap<>();
Builders reuse instances where possible.
Non-blocking operations return CompletableFuture
.
private final Map<String, JLMenuItem> menuItems = new ConcurrentHashMap<>();
Used for lazy initialization of expensive objects.
- JavaFX: Operations must run on JavaFX Application Thread
- Vaadin: Operations synchronized via Vaadin's session lock
Implement JLContextMenuMediator
for new UI frameworks.
Implement Event
interface for new event types.
Create custom tile providers by extending JLMapProvider
.
Use JLGeoJsonOptions
with style and filter functions.
Previous: ← Getting Started | Next: API Reference →