Rework MapRenderer abstraction

This commit is contained in:
2026-07-06 14:19:38 +02:00
parent a64788f023
commit 6d7a391e28
19 changed files with 338 additions and 200 deletions
+3 -2
View File
@@ -28,8 +28,8 @@ android {
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_15
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_15
} }
kotlin { kotlin {
@@ -46,6 +46,7 @@ dependencies {
implementation(libs.constraintlayout) implementation(libs.constraintlayout)
implementation(libs.viewpager2) implementation(libs.viewpager2)
implementation("org.osmdroid:osmdroid-android:6.1.20")
implementation("org.maplibre.gl:android-sdk:11.13.0") implementation("org.maplibre.gl:android-sdk:11.13.0")
implementation("com.github.mapsforge.vtm:vtm:0.27.0") implementation("com.github.mapsforge.vtm:vtm:0.27.0")
implementation("com.github.mapsforge.vtm:vtm-android:0.27.0") implementation("com.github.mapsforge.vtm:vtm-android:0.27.0")
@@ -1,5 +1,5 @@
# #
Builds included inside /dist correspond to MapLibre GL JS version 5.24.0 Builds included inside /dist correspond to MapLibre GL JS version 5.24.0 <br>
The source for these builds is available on https://github.com/maplibre/maplibre-gl-js The source for these builds is available on https://github.com/maplibre/maplibre-gl-js
# #
@@ -21,12 +21,8 @@
center: [13.388, 52.517], center: [13.388, 52.517],
zoom: 9.5, zoom: 9.5,
container: 'map', container: 'map',
attributionControl: false
}) })
window.setMapStyle = function(styleUrl) {
if (!map) return;
map.setStyle(styleUrl);
};
</script> </script>
</body> </body>
@@ -0,0 +1,93 @@
package eu.konggdev.strikemaps.app.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.stream.StreamSupport;
public final class JsonPatcher {
//Takes a json and a patch and applies it
public static JsonNode patch(JsonNode current, JsonNode patch) throws Exception {
if (current.isObject()) {
ObjectNode result = current.deepCopy();
patch.fields().forEachRemaining(entry -> { //For each
String key = entry.getKey();
JsonNode patchValue = entry.getValue();
if(!result.has(key)) { //Not at all in current
result.set(key, patchValue); //Just append it to result
} else if (result.has(key) && !patchValue.isEmpty()) { //Deeper depth possible
try {
result.set(key, patch(result.get(key), patchValue)); //Patch iteration with patch's value (traverse deeper)
} catch (Exception e) {
throw new RuntimeException(e);
}
} else if (result.has(key) && patchValue.isEmpty() && result.get(key).isEmpty()) { //Max depth match
result.set(key, patchValue); //Replace with patch's value
}
});
return result;
}
if (current.isArray()) {
ArrayNode result = current.deepCopy();
for (JsonNode entry : patch) {
if (StreamSupport.stream(result.spliterator(), false)
.noneMatch(node -> node.equals(entry))) { //If no exact entry, add entry (prevents duplicates)
result.add(entry);
}
}
return result;
}
throw new Exception();
}
//Takes an original json, a patched json and a patch and removes that specific patch's changes from it
public static JsonNode unpatch(JsonNode original, JsonNode current, JsonNode patch) throws Exception {
if (current.isObject()) {
ObjectNode result = current.deepCopy(); //Current contains the patch, deepcopy it and operate on the copy
current.fields().forEachRemaining(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();
if (!original.has(key) && patch.has(key)) { //Value is in patch and current but was not at all in the original
result.remove(key); //Just remove the value
} else if (!value.isEmpty() && patch.has(key)) { //Depper depth possible
try {
result.set(key, unpatch(original.get(key), result.get(key), patch.get(key))); //Replace with further unpatch of deeper level
} catch (Exception e) {
throw new RuntimeException(e);
}
} else if (value.isEmpty() && patch.has(key)) { //Max depth match
result.set(key, original.get(key)); //Replace with original unpatched value
}
});
return result;
}
if (current.isArray()) {
ArrayNode result = current.deepCopy();
for (int i = 0; i < result.size(); i++) {
JsonNode entry = result.get(i);
if (StreamSupport.stream(original.spliterator(), false)
.noneMatch(on -> on.equals(entry)) //If not in original
&&
StreamSupport.stream(patch.spliterator(), false)
.noneMatch(rn -> rn.equals(entry))) { //But also in patch
result.remove(i); //Remove from result
}
}
return result;
}
throw new Exception();
}
}
@@ -3,7 +3,6 @@ package eu.konggdev.strikemaps.map;
import java.util.*; import java.util.*;
import android.widget.Toast; import android.widget.Toast;
import android.widget.Toolbar;
import eu.konggdev.strikemaps.Component; import eu.konggdev.strikemaps.Component;
import eu.konggdev.strikemaps.map.renderer.implementation.MapLibreGLJSRenderer; import eu.konggdev.strikemaps.map.renderer.implementation.MapLibreGLJSRenderer;
import eu.konggdev.strikemaps.ui.factory.AlertDialogFactory; import eu.konggdev.strikemaps.ui.factory.AlertDialogFactory;
@@ -25,21 +24,22 @@ public class MapComponent implements Component {
public MapStyle style; public MapStyle style;
public Map<Class<? extends MapOverlay>, MapOverlay> overlays = new HashMap<>(); public Map<Class<? extends MapOverlay>, MapOverlay> overlays = new HashMap<>();
public MapComponent(AppController ref) { public MapComponent(AppController ref) {
this.app = ref; this.app = ref;
switch(UserPrefsHelper.mapRenderer(app.getPrefs())) { switch(UserPrefsHelper.mapRenderer(app.getPrefs())) {
case 0: case 0:
this.mapRenderer = new MapLibreNativeRenderer(app, this); this.mapRenderer = new MapLibreGLJSRenderer(app, this);
break; break;
case 1: case 1:
this.mapRenderer = new MapLibreGLJSRenderer(app, this); this.mapRenderer = new MapLibreNativeRenderer(app, this);
break; break;
case 2: case 2:
this.mapRenderer = new VtmRenderer(app, this); this.mapRenderer = new VtmRenderer(app, this);
break; break;
default: //This shouldn't happen default: //This shouldn't happen
Toast.makeText(app.getActivity(), "Invalid renderer value in preferences\nFalling back to MapLibre Native", Toast.LENGTH_SHORT).show(); Toast.makeText(app.getActivity(), "Invalid renderer value in preferences\nFalling back to MapLibre GL JS", Toast.LENGTH_SHORT).show();
this.mapRenderer = new MapLibreNativeRenderer(app, this); this.mapRenderer = new MapLibreGLJSRenderer(app, this);
break; break;
}; };
} }
@@ -50,13 +50,13 @@ public class MapComponent implements Component {
public void setStyle(MapStyle style) { public void setStyle(MapStyle style) {
this.style = style; this.style = style;
mapRenderer.reload(); mapRenderer.styleUpdate(style);
} }
public void switchOverlay(MapOverlay overlay) { public void switchOverlay(MapOverlay overlay) {
if (hasOverlay(overlay)) overlays.remove(overlay.getClass()); if (hasOverlay(overlay)) overlays.remove(overlay.getClass());
else overlays.put(overlay.getClass(), overlay); else overlays.put(overlay.getClass(), overlay);
update(); overlayUpdate(overlay);
} }
public boolean hasOverlay(MapOverlay overlay) { public boolean hasOverlay(MapOverlay overlay) {
@@ -71,12 +71,8 @@ public class MapComponent implements Component {
//FIXME: Put back FragmentPointPreviewPopup (private code atm) //FIXME: Put back FragmentPointPreviewPopup (private code atm)
} }
public void onOverlayUpdate() { public void overlayUpdate(MapOverlay in) {
update(); mapRenderer.overlayUpdate(in);
}
public void update() {
if(mapRenderer != null && style != null) mapRenderer.reload();
} }
public boolean onMapClick(LatLng point) { public boolean onMapClick(LatLng point) {
@@ -1,11 +0,0 @@
package eu.konggdev.strikemaps.map.layer;
import com.fasterxml.jackson.databind.JsonNode;
public class MapLayer {
public JsonNode layer;
public MapLayer(JsonNode layer) {
this.layer = layer;
}
}
@@ -1,16 +0,0 @@
package eu.konggdev.strikemaps.map.layer;
import com.fasterxml.jackson.databind.JsonNode;
import eu.konggdev.strikemaps.map.source.MapSource;
public class SourcedMapLayer {
public String key;
public MapSource source;
public JsonNode layer;
public SourcedMapLayer(String key, MapSource source, JsonNode layer) {
this.key = key;
this.source = source;
this.layer = layer;
}
}
@@ -1,8 +1,8 @@
package eu.konggdev.strikemaps.map.overlay; package eu.konggdev.strikemaps.map.overlay;
import eu.konggdev.strikemaps.map.layer.SourcedMapLayer; import com.fasterxml.jackson.databind.JsonNode;
/* More or less a data-driven layer factory */ /* More or less a data-driven layer factory */
public interface MapOverlay { public interface MapOverlay {
public SourcedMapLayer makeLayer(); public JsonNode makePatch();
} }
@@ -1,19 +1,15 @@
package eu.konggdev.strikemaps.map.overlay.implementation; package eu.konggdev.strikemaps.map.overlay.implementation;
import android.graphics.Color;
import android.location.Location; import android.location.Location;
import android.location.LocationListener; import android.location.LocationListener;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.fasterxml.jackson.databind.JsonNode;
import eu.konggdev.strikemaps.app.AppController; import eu.konggdev.strikemaps.app.AppController;
import eu.konggdev.strikemaps.map.MapComponent; import eu.konggdev.strikemaps.map.MapComponent;
import eu.konggdev.strikemaps.map.layer.SourcedMapLayer;
import eu.konggdev.strikemaps.map.overlay.MapOverlay; import eu.konggdev.strikemaps.map.overlay.MapOverlay;
import eu.konggdev.strikemaps.map.source.MapSource; import eu.konggdev.strikemaps.map.source.MapSource;
import eu.konggdev.strikemaps.data.provider.LocationDataProvider; import eu.konggdev.strikemaps.data.provider.LocationDataProvider;
import org.maplibre.geojson.Feature;
import org.maplibre.geojson.FeatureCollection;
import org.maplibre.geojson.Point;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -33,34 +29,38 @@ public class LocationOverlay implements MapOverlay, LocationListener {
} }
@Override @Override
public SourcedMapLayer makeLayer() { public JsonNode makePatch() {
MapSource source = new MapSource();
source.type = "geojson";
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
try {
ObjectNode data = mapper.createObjectNode();
data.put("type", "Feature");
if(currentLocation != null) { ObjectNode root = mapper.createObjectNode();
ObjectNode sources = mapper.createObjectNode();
ObjectNode location = mapper.createObjectNode();
ObjectNode data = mapper.createObjectNode();
ObjectNode geometry = mapper.createObjectNode(); ObjectNode geometry = mapper.createObjectNode();
geometry.put("type", "Point"); ObjectNode properties = mapper.createObjectNode();
ArrayNode coordinates = mapper.createArrayNode(); ArrayNode coordinates = mapper.createArrayNode();
coordinates.add(currentLocation.getLongitude()); coordinates.add(currentLocation.getLongitude());
coordinates.add(currentLocation.getLatitude()); coordinates.add(currentLocation.getLatitude());
geometry.put("type", "Point");
geometry.set("coordinates", coordinates); geometry.set("coordinates", coordinates);
data.set("geometry", geometry);
data.set("properties", mapper.createObjectNode());
}
source.data = data;
} catch (Exception e) {
e.printStackTrace();
}
data.put("type", "Feature");
data.set("geometry", geometry);
data.set("properties", properties);
location.put("type", "geojson");
location.set("data", data);
sources.set("location", location);
root.set("sources", sources);
// layers
ArrayNode layers = mapper.createArrayNode();
ObjectNode layer = mapper.createObjectNode(); ObjectNode layer = mapper.createObjectNode();
layer.put("id", "location"); layer.put("id", "location");
layer.put("type", "circle"); layer.put("type", "circle");
layer.put("source", "location"); layer.put("source", "location");
@@ -68,25 +68,21 @@ public class LocationOverlay implements MapOverlay, LocationListener {
ObjectNode paint = mapper.createObjectNode(); ObjectNode paint = mapper.createObjectNode();
paint.put("circle-radius", 5); paint.put("circle-radius", 5);
paint.put("circle-color", "#1E88E5"); paint.put("circle-color", "#1E88E5");
paint.put("circle-stroke-color", "#FFFFFF"); paint.put("circle-stroke-color", "#FFFFFF");
paint.put("circle-stroke-width", 1.5); paint.put("circle-stroke-width", 1.5);
layer.set("paint", paint); layer.set("paint", paint);
ObjectNode layout = mapper.createObjectNode();
layout.put("circle-pitch-alignment", "map");
layer.set("layout", layout);
ArrayNode layers = mapper.createArrayNode();
layers.add(layer); layers.add(layer);
root.set("layers", layers);
return new SourcedMapLayer("location", source, layers); return root;
} }
@Override @Override
public void onLocationChanged(@NonNull Location location) { public void onLocationChanged(@NonNull Location location) {
this.currentLocation = location; this.currentLocation = location;
map.onOverlayUpdate(); map.overlayUpdate(this);
} }
} }
@@ -1,12 +1,11 @@
package eu.konggdev.strikemaps.map.overlay.implementation; package eu.konggdev.strikemaps.map.overlay.implementation;
import eu.konggdev.strikemaps.map.layer.MapLayer; import com.fasterxml.jackson.databind.JsonNode;
import eu.konggdev.strikemaps.map.layer.SourcedMapLayer;
import eu.konggdev.strikemaps.map.overlay.MapOverlay; import eu.konggdev.strikemaps.map.overlay.MapOverlay;
public class PointSelectionOverlay implements MapOverlay { public class PointSelectionOverlay implements MapOverlay {
@Override @Override
public SourcedMapLayer makeLayer() { public JsonNode makePatch() {
return null; return null;
} }
} }
@@ -1,17 +1,18 @@
package eu.konggdev.strikemaps.map.renderer; package eu.konggdev.strikemaps.map.renderer;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import eu.konggdev.strikemaps.map.layer.MapLayer; import eu.konggdev.strikemaps.map.overlay.MapOverlay;
import org.maplibre.android.style.layers.Layer; import eu.konggdev.strikemaps.map.style.MapStyle;
import org.maplibre.android.geometry.LatLng; import org.maplibre.android.geometry.LatLng;
import org.maplibre.geojson.Feature; import org.maplibre.geojson.Feature;
import java.util.List; import java.util.List;
public interface MapRenderer { public interface MapRenderer {
void reload(); void styleUpdate(MapStyle style);
void overlayUpdate(MapOverlay overlay);
View getView(); View getView();
@@ -3,16 +3,19 @@ package eu.konggdev.strikemaps.map.renderer.implementation;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient; import android.webkit.WebChromeClient;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import androidx.annotation.NonNull;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import eu.konggdev.strikemaps.app.AppController; import eu.konggdev.strikemaps.app.AppController;
import eu.konggdev.strikemaps.app.util.JsonPatcher;
import eu.konggdev.strikemaps.map.MapComponent; import eu.konggdev.strikemaps.map.MapComponent;
import eu.konggdev.strikemaps.map.layer.SourcedMapLayer;
import eu.konggdev.strikemaps.map.overlay.MapOverlay; import eu.konggdev.strikemaps.map.overlay.MapOverlay;
import eu.konggdev.strikemaps.map.renderer.MapRenderer; import eu.konggdev.strikemaps.map.renderer.MapRenderer;
import eu.konggdev.strikemaps.map.style.MapStyle; import eu.konggdev.strikemaps.map.style.MapStyle;
@@ -25,64 +28,18 @@ import java.util.List;
//Stub for now //Stub for now
public class MapLibreGLJSRenderer implements MapRenderer { public class MapLibreGLJSRenderer implements MapRenderer {
@NonNull AppController app;
AppController app; @NonNull MapComponent controller;
MapComponent controller;
final WebView webView; final WebView webView;
@Override private JsonNode origin;
public void reload() {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
MapStyle style = controller.style;
try {
/* Take metadata from MapStyle
everything outside sources, layers */
ObjectNode root = style.metadata.deepCopy();
//Sources
ObjectNode sources = mapper.createObjectNode();
style.sources.forEach((k, v) -> sources.set(k, mapper.valueToTree(v)));
//Layers
ArrayNode layers = mapper.createArrayNode();
layers.addAll((ArrayNode) style.layerDefinitions);
//Overlays
for (MapOverlay overlay : controller.overlays.values()) {
SourcedMapLayer overlayLayer = overlay.makeLayer();
sources.set(overlayLayer.key, mapper.valueToTree(overlayLayer.source));
layers.addAll((ArrayNode) overlayLayer.layer);
}
//Set all to root
root.set("sources", sources);
root.set("layers", layers);
webView.evaluateJavascript(
"",
null
);
} catch (Exception e) {
app.logcat("Failed to reload Map");
e.printStackTrace();
}
}
@Override
public View getView() {
return webView;
}
@Override
public List<Feature> featuresAtPoint(LatLng point) {
return Collections.emptyList();
}
@SuppressLint({"JavascriptInterface", "SetJavaScriptEnabled"}) @SuppressLint({"JavascriptInterface", "SetJavaScriptEnabled"})
public MapLibreGLJSRenderer(AppController app, MapComponent controller) { public MapLibreGLJSRenderer(AppController app, MapComponent controller) {
this.app = app;
this.controller = controller;
webView = new WebView(app.getActivity()); webView = new WebView(app.getActivity());
webView.setLayoutParams( webView.setLayoutParams(
new ViewGroup.LayoutParams( new ViewGroup.LayoutParams(
@@ -100,7 +57,90 @@ public class MapLibreGLJSRenderer implements MapRenderer {
webView.addJavascriptInterface(new Bridge(), "AndroidBridge"); webView.addJavascriptInterface(new Bridge(), "AndroidBridge");
webView.loadUrl("file:///android_asset/maplibre/gl-js/index.html"); webView.loadUrl("file:///android_asset/library/maplibre/gl-js/index.html");
}
@Override
public void styleUpdate(MapStyle style) {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
//Thanks to this, styleUpdate(null) can be used as an origin reload
if (style != null) {
try {
ObjectNode root = style.metadata.deepCopy();
//Sources
ObjectNode sources = mapper.createObjectNode();
style.sources.forEach((k, v) -> sources.set(k, mapper.valueToTree(v)));
//Layers
ArrayNode layers = mapper.createArrayNode();
layers.addAll((ArrayNode) style.layerDefinitions);
//Set all to root
root.set("sources", sources);
root.set("layers", layers);
this.origin = root;
} catch (Exception e) {
app.logcat("Failed to parse style: " + style.name);
e.printStackTrace();
}
}
try {
final String mapped = mapper.writeValueAsString(origin);
webView.evaluateJavascript("map.setStyle(" + mapped + ", { diff: false });", null);
webView.evaluateJavascript("map.redraw()", null); //Force redraw to make the style change visible
} catch (Exception e) {
app.logcat("Failed to set style: " + style.name);
e.printStackTrace();
}
//Since we just annihilated all overlays from the face of the earth, lets repatch them
if (controller.overlays != null) {
for(MapOverlay overlay : controller.overlays.values())
overlayUpdate(overlay);
}
}
@Override
public void overlayUpdate(MapOverlay overlay) {
ObjectMapper mapper = new ObjectMapper();
webView.evaluateJavascript(
"JSON.stringify(map.getStyle())",
style -> {
try {
JsonNode current = mapper.readTree(style);
JsonNode merged;
if (controller.hasOverlay(overlay)) {
merged = JsonPatcher.patch(current, overlay.makePatch());
} else {
merged = JsonPatcher.unpatch(origin, current, overlay.makePatch());
}
final String mapped = mapper.writeValueAsString(merged);
webView.evaluateJavascript("map.setStyle(" + mapped + ", { diff: false });", null);
webView.evaluateJavascript("map.redraw()", null); //Force redraw to make the style change visible
} catch (Exception e) {
app.logcat("Failed to patch overlay: " + overlay.toString());
e.printStackTrace();
}
}
);
}
@Override
public View getView() {
return webView;
}
@Override
public List<Feature> featuresAtPoint(LatLng point) {
return Collections.emptyList();
} }
class Bridge { } class Bridge { }
@@ -5,12 +5,13 @@ import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import eu.konggdev.strikemaps.app.util.JsonPatcher;
import eu.konggdev.strikemaps.data.helper.UserPrefsHelper; import eu.konggdev.strikemaps.data.helper.UserPrefsHelper;
import eu.konggdev.strikemaps.map.overlay.MapOverlay; import eu.konggdev.strikemaps.map.overlay.MapOverlay;
import eu.konggdev.strikemaps.map.layer.SourcedMapLayer;
import eu.konggdev.strikemaps.map.renderer.MapRenderer; import eu.konggdev.strikemaps.map.renderer.MapRenderer;
import eu.konggdev.strikemaps.map.style.MapStyle; import eu.konggdev.strikemaps.map.style.MapStyle;
import org.maplibre.android.MapLibre; import org.maplibre.android.MapLibre;
@@ -27,11 +28,13 @@ import eu.konggdev.strikemaps.app.AppController;
import eu.konggdev.strikemaps.map.MapComponent; import eu.konggdev.strikemaps.map.MapComponent;
public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback { public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback {
AppController app; @NonNull AppController app;
MapComponent controller; @NonNull MapComponent controller;
MapLibreMap map; MapLibreMap map;
final MapView mapView; final MapView mapView;
private JsonNode origin;
public MapLibreNativeRenderer(AppController app, MapComponent controller) { public MapLibreNativeRenderer(AppController app, MapComponent controller) {
this.app = app; this.app = app;
this.controller = controller; this.controller = controller;
@@ -42,13 +45,13 @@ public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback {
} }
@Override @Override
public void reload() { public void styleUpdate(MapStyle style) {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
MapStyle style = controller.style;
//Thanks to this, styleUpdate(null) can be used as an origin reload
if (style != null) {
try { try {
/* Take metadata from MapStyle
everything outside sources, layers */
ObjectNode root = style.metadata.deepCopy(); ObjectNode root = style.metadata.deepCopy();
//Sources //Sources
@@ -59,20 +62,52 @@ public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback {
ArrayNode layers = mapper.createArrayNode(); ArrayNode layers = mapper.createArrayNode();
layers.addAll((ArrayNode) style.layerDefinitions); layers.addAll((ArrayNode) style.layerDefinitions);
//Overlays
for (MapOverlay overlay : controller.overlays.values()) {
SourcedMapLayer overlayLayer = overlay.makeLayer();
sources.set(overlayLayer.key, mapper.valueToTree(overlayLayer.source));
layers.addAll((ArrayNode) overlayLayer.layer);
}
//Set all to root //Set all to root
root.set("sources", sources); root.set("sources", sources);
root.set("layers", layers); root.set("layers", layers);
this.origin = root;
map.setStyle(new Style.Builder().fromJson(mapper.writeValueAsString(root)));
} catch (Exception e) { } catch (Exception e) {
app.logcat("Failed to reload Map"); app.logcat("Failed to parse style: " + style.name);
e.printStackTrace();
}
}
try {
map.setStyle(new Style.Builder().fromJson(mapper.writeValueAsString(origin)));
} catch (Exception e) {
app.logcat("Failed to set style: " + style.name);
e.printStackTrace();
}
//Since we just annihilated all overlays from the face of the earth, lets repatch them
if (controller.overlays != null) {
for(MapOverlay overlay : controller.overlays.values())
overlayUpdate(overlay);
}
}
@Override
public void overlayUpdate(MapOverlay overlay) {
if(map == null) return;
Style style = map.getStyle();
ObjectMapper mapper = new ObjectMapper();
if(style == null) return;
try {
JsonNode current = mapper.readTree(style.getJson());
JsonNode merged;
if (controller.hasOverlay(overlay)) {
merged = JsonPatcher.patch(current, overlay.makePatch());
} else {
merged = JsonPatcher.unpatch(origin, current, overlay.makePatch());
}
map.setStyle(new Style.Builder().fromJson(mapper.writeValueAsString(merged)));
} catch (Exception e) {
app.logcat("Failed to patch overlay: " + overlay.toString());
e.printStackTrace(); e.printStackTrace();
} }
} }
@@ -91,7 +126,7 @@ public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback {
public void onMapReady(@NonNull MapLibreMap maplibreMap) { public void onMapReady(@NonNull MapLibreMap maplibreMap) {
this.map = maplibreMap; this.map = maplibreMap;
controller.setStyle(MapStyle.fromMapLibreJsonFile(UserPrefsHelper.startupMapStyle(app.getPrefs()), app)); controller.setStyle(MapStyle.fromJsonFile(UserPrefsHelper.startupMapStyle(app.getPrefs()), app));
//I have my own implementation of attribution that credits MapLibre among others, it's not as bad as it looks :) //I have my own implementation of attribution that credits MapLibre among others, it's not as bad as it looks :)
map.getUiSettings().setLogoEnabled(false); map.getUiSettings().setLogoEnabled(false);
@@ -3,7 +3,9 @@ package eu.konggdev.strikemaps.map.renderer.implementation;
import android.view.View; import android.view.View;
import eu.konggdev.strikemaps.app.AppController; import eu.konggdev.strikemaps.app.AppController;
import eu.konggdev.strikemaps.map.MapComponent; import eu.konggdev.strikemaps.map.MapComponent;
import eu.konggdev.strikemaps.map.overlay.MapOverlay;
import eu.konggdev.strikemaps.map.renderer.MapRenderer; import eu.konggdev.strikemaps.map.renderer.MapRenderer;
import eu.konggdev.strikemaps.map.style.MapStyle;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import org.maplibre.android.geometry.LatLng; import org.maplibre.android.geometry.LatLng;
import org.maplibre.geojson.Feature; import org.maplibre.geojson.Feature;
@@ -16,6 +18,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
public class VtmRenderer implements MapRenderer { public class VtmRenderer implements MapRenderer {
AppController app; AppController app;
MapComponent controller; MapComponent controller;
@@ -30,7 +33,12 @@ public class VtmRenderer implements MapRenderer {
} }
@Override @Override
public void reload() { public void overlayUpdate(MapOverlay overlay) {
//TODO
}
@Override
public void styleUpdate(MapStyle style) {
//TODO //TODO
OkHttpClient.Builder builder = new OkHttpClient.Builder(); OkHttpClient.Builder builder = new OkHttpClient.Builder();
OSciMap4TileSource tileSource = OSciMap4TileSource.builder().httpFactory(new OkHttpEngine.OkHttpFactory(builder)).build(); OSciMap4TileSource tileSource = OSciMap4TileSource.builder().httpFactory(new OkHttpEngine.OkHttpFactory(builder)).build();
@@ -20,10 +20,10 @@ public class MapStyle {
public JsonNode metadata; // everything except layers + sources public JsonNode metadata; // everything except layers + sources
public Map<String, MapSource> sources; public Map<String, MapSource> sources;
public ArrayNode layerDefinitions; // the "layers" array public ArrayNode layerDefinitions; // "layers" array
//FIXME //FIXME
public static MapStyle fromMapLibreJsonFile(String filename, AppController app) { public static MapStyle fromJsonFile(String filename, AppController app) {
String styleContents; String styleContents;
if (filename.startsWith("/storage")) styleContents = FileHelper.loadStringFromUserFile(filename); if (filename.startsWith("/storage")) styleContents = FileHelper.loadStringFromUserFile(filename);
else styleContents = FileHelper.loadStringFromAssetFile(filename, app); else styleContents = FileHelper.loadStringFromAssetFile(filename, app);
@@ -56,6 +56,6 @@ public class FragmentMapChangePopup extends Fragment implements Popup {
stylePaths.addAll(Arrays.asList(FileHelper.getUserFiles("style", ".style.json", app))); stylePaths.addAll(Arrays.asList(FileHelper.getUserFiles("style", ".style.json", app)));
LinearLayout stylesLayout = view.findViewById(R.id.stylesLayout); LinearLayout stylesLayout = view.findViewById(R.id.stylesLayout);
for (String style : stylePaths) for (String style : stylePaths)
stylesLayout.addView(GenericItem.fromStyle(MapStyle.fromMapLibreJsonFile(style, app), map).makeView(ui)); stylesLayout.addView(GenericItem.fromStyle(MapStyle.fromJsonFile(style, app), map).makeView(ui));
} }
} }
+2 -2
View File
@@ -2,10 +2,10 @@
<string name="app_name">Strike Maps</string> <string name="app_name">Strike Maps</string>
<string name="settings_title">Settings</string> <string name="settings_title">Settings</string>
<string name="attribution_title"> Attribution </string> <string name="attribution_title"> Attribution </string>
<string name="shipped_attribution">Earth map data Included;\n - © OpenStreetMap Contributors\nMap Rendering libraries Included;\n - MapLibre developed by MapLibre Organization\n - Vtm developed by Mapsforge\nStrike Maps made with &lt;3 by konggdev</string> <string name="shipped_attribution">Map Rendering libraries Included;\n - MapLibre developed by MapLibre Organization\n - Vtm developed by Mapsforge\nStrike Maps made with &lt;3 by konggdev</string>
<string-array name="map_renderers"> <string-array name="map_renderers">
<item>MapLibre Native</item>
<item>MapLibre GL JS</item> <item>MapLibre GL JS</item>
<item>MapLibre Native</item>
<item>VTM</item> <item>VTM</item>
</string-array> </string-array>
</resources> </resources>