diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 65d0dca..fa6c589 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,8 +28,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_15 + targetCompatibility = JavaVersion.VERSION_15 } kotlin { @@ -46,6 +46,7 @@ dependencies { implementation(libs.constraintlayout) implementation(libs.viewpager2) + implementation("org.osmdroid:osmdroid-android:6.1.20") implementation("org.maplibre.gl:android-sdk:11.13.0") implementation("com.github.mapsforge.vtm:vtm:0.27.0") implementation("com.github.mapsforge.vtm:vtm-android:0.27.0") diff --git a/app/src/main/assets/maplibre/gl-js/dist/THIRD_PARTY_NOTICES.md b/app/src/main/assets/library/maplibre/gl-js/dist/THIRD_PARTY_NOTICES.md similarity index 99% rename from app/src/main/assets/maplibre/gl-js/dist/THIRD_PARTY_NOTICES.md rename to app/src/main/assets/library/maplibre/gl-js/dist/THIRD_PARTY_NOTICES.md index ccb8763..0887f82 100644 --- a/app/src/main/assets/maplibre/gl-js/dist/THIRD_PARTY_NOTICES.md +++ b/app/src/main/assets/library/maplibre/gl-js/dist/THIRD_PARTY_NOTICES.md @@ -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
The source for these builds is available on https://github.com/maplibre/maplibre-gl-js # diff --git a/app/src/main/assets/maplibre/gl-js/dist/maplibre-gl.css b/app/src/main/assets/library/maplibre/gl-js/dist/maplibre-gl.css similarity index 100% rename from app/src/main/assets/maplibre/gl-js/dist/maplibre-gl.css rename to app/src/main/assets/library/maplibre/gl-js/dist/maplibre-gl.css diff --git a/app/src/main/assets/maplibre/gl-js/dist/maplibre-gl.js b/app/src/main/assets/library/maplibre/gl-js/dist/maplibre-gl.js similarity index 100% rename from app/src/main/assets/maplibre/gl-js/dist/maplibre-gl.js rename to app/src/main/assets/library/maplibre/gl-js/dist/maplibre-gl.js diff --git a/app/src/main/assets/maplibre/gl-js/index.html b/app/src/main/assets/library/maplibre/gl-js/index.html similarity index 82% rename from app/src/main/assets/maplibre/gl-js/index.html rename to app/src/main/assets/library/maplibre/gl-js/index.html index 383d135..9e3087d 100644 --- a/app/src/main/assets/maplibre/gl-js/index.html +++ b/app/src/main/assets/library/maplibre/gl-js/index.html @@ -21,12 +21,8 @@ center: [13.388, 52.517], zoom: 9.5, container: 'map', + attributionControl: false }) - - window.setMapStyle = function(styleUrl) { - if (!map) return; - map.setStyle(styleUrl); - }; diff --git a/app/src/main/java/eu/konggdev/strikemaps/app/util/JsonPatcher.java b/app/src/main/java/eu/konggdev/strikemaps/app/util/JsonPatcher.java new file mode 100644 index 0000000..c3b1472 --- /dev/null +++ b/app/src/main/java/eu/konggdev/strikemaps/app/util/JsonPatcher.java @@ -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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/MapComponent.java b/app/src/main/java/eu/konggdev/strikemaps/map/MapComponent.java index 1f62317..bd89b05 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/MapComponent.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/MapComponent.java @@ -3,7 +3,6 @@ package eu.konggdev.strikemaps.map; import java.util.*; import android.widget.Toast; -import android.widget.Toolbar; import eu.konggdev.strikemaps.Component; import eu.konggdev.strikemaps.map.renderer.implementation.MapLibreGLJSRenderer; import eu.konggdev.strikemaps.ui.factory.AlertDialogFactory; @@ -19,27 +18,28 @@ import eu.konggdev.strikemaps.map.renderer.implementation.MapLibreNativeRenderer import eu.konggdev.strikemaps.map.renderer.MapRenderer; import eu.konggdev.strikemaps.ui.fragment.layout.content.main.FragmentLayoutContentMap; -public class MapComponent implements Component { +public class MapComponent implements Component { MapRenderer mapRenderer; AppController app; public MapStyle style; public Map, MapOverlay> overlays = new HashMap<>(); + public MapComponent(AppController ref) { this.app = ref; switch(UserPrefsHelper.mapRenderer(app.getPrefs())) { case 0: - this.mapRenderer = new MapLibreNativeRenderer(app, this); + this.mapRenderer = new MapLibreGLJSRenderer(app, this); break; case 1: - this.mapRenderer = new MapLibreGLJSRenderer(app, this); + this.mapRenderer = new MapLibreNativeRenderer(app, this); break; case 2: this.mapRenderer = new VtmRenderer(app, this); break; - default: //This shouldn't happen - Toast.makeText(app.getActivity(), "Invalid renderer value in preferences\nFalling back to MapLibre Native", Toast.LENGTH_SHORT).show(); - this.mapRenderer = new MapLibreNativeRenderer(app, this); + default: //This shouldn't happen + Toast.makeText(app.getActivity(), "Invalid renderer value in preferences\nFalling back to MapLibre GL JS", Toast.LENGTH_SHORT).show(); + this.mapRenderer = new MapLibreGLJSRenderer(app, this); break; }; } @@ -50,13 +50,13 @@ public class MapComponent implements Component { public void setStyle(MapStyle style) { this.style = style; - mapRenderer.reload(); + mapRenderer.styleUpdate(style); } public void switchOverlay(MapOverlay overlay) { if (hasOverlay(overlay)) overlays.remove(overlay.getClass()); else overlays.put(overlay.getClass(), overlay); - update(); + overlayUpdate(overlay); } public boolean hasOverlay(MapOverlay overlay) { @@ -71,12 +71,8 @@ public class MapComponent implements Component { //FIXME: Put back FragmentPointPreviewPopup (private code atm) } - public void onOverlayUpdate() { - update(); - } - - public void update() { - if(mapRenderer != null && style != null) mapRenderer.reload(); + public void overlayUpdate(MapOverlay in) { + mapRenderer.overlayUpdate(in); } public boolean onMapClick(LatLng point) { diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/layer/MapLayer.java b/app/src/main/java/eu/konggdev/strikemaps/map/layer/MapLayer.java deleted file mode 100644 index a9d9970..0000000 --- a/app/src/main/java/eu/konggdev/strikemaps/map/layer/MapLayer.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/layer/SourcedMapLayer.java b/app/src/main/java/eu/konggdev/strikemaps/map/layer/SourcedMapLayer.java deleted file mode 100644 index 44a4237..0000000 --- a/app/src/main/java/eu/konggdev/strikemaps/map/layer/SourcedMapLayer.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/overlay/MapOverlay.java b/app/src/main/java/eu/konggdev/strikemaps/map/overlay/MapOverlay.java index 957d6c5..ab3507b 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/overlay/MapOverlay.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/overlay/MapOverlay.java @@ -1,8 +1,8 @@ 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 */ public interface MapOverlay { - public SourcedMapLayer makeLayer(); + public JsonNode makePatch(); } diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/LocationOverlay.java b/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/LocationOverlay.java index f45f643..ff19dde 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/LocationOverlay.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/LocationOverlay.java @@ -1,19 +1,15 @@ package eu.konggdev.strikemaps.map.overlay.implementation; -import android.graphics.Color; import android.location.Location; import android.location.LocationListener; import androidx.annotation.NonNull; +import com.fasterxml.jackson.databind.JsonNode; import eu.konggdev.strikemaps.app.AppController; 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.source.MapSource; 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.node.ObjectNode; @@ -32,61 +28,61 @@ public class LocationOverlay implements MapOverlay, LocationListener { this.locationDataProvider = new LocationDataProvider(app.getActivity(), this); } - @Override - public SourcedMapLayer makeLayer() { - MapSource source = new MapSource(); + @Override + public JsonNode makePatch() { + ObjectMapper mapper = new ObjectMapper(); - source.type = "geojson"; + ObjectNode root = mapper.createObjectNode(); - ObjectMapper mapper = new ObjectMapper(); - try { - ObjectNode data = mapper.createObjectNode(); - data.put("type", "Feature"); + ObjectNode sources = mapper.createObjectNode(); + ObjectNode location = mapper.createObjectNode(); + ObjectNode data = mapper.createObjectNode(); + ObjectNode geometry = mapper.createObjectNode(); + ObjectNode properties = mapper.createObjectNode(); - if(currentLocation != null) { - ObjectNode geometry = mapper.createObjectNode(); - geometry.put("type", "Point"); + ArrayNode coordinates = mapper.createArrayNode(); + coordinates.add(currentLocation.getLongitude()); + coordinates.add(currentLocation.getLatitude()); - ArrayNode coordinates = mapper.createArrayNode(); - coordinates.add(currentLocation.getLongitude()); - 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(); + + layer.put("id", "location"); + layer.put("type", "circle"); + layer.put("source", "location"); + + ObjectNode paint = mapper.createObjectNode(); + paint.put("circle-radius", 5); + paint.put("circle-color", "#1E88E5"); + + paint.put("circle-stroke-color", "#FFFFFF"); + paint.put("circle-stroke-width", 1.5); + + layer.set("paint", paint); + + layers.add(layer); + root.set("layers", layers); + + return root; } - ObjectNode layer = mapper.createObjectNode(); - layer.put("id", "location"); - layer.put("type", "circle"); - layer.put("source", "location"); - - ObjectNode paint = mapper.createObjectNode(); - paint.put("circle-radius", 5); - paint.put("circle-color", "#1E88E5"); - paint.put("circle-stroke-color", "#FFFFFF"); - paint.put("circle-stroke-width", 1.5); - - 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); - - return new SourcedMapLayer("location", source, layers); - } - @Override public void onLocationChanged(@NonNull Location location) { this.currentLocation = location; - map.onOverlayUpdate(); + map.overlayUpdate(this); } } diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/PointSelectionOverlay.java b/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/PointSelectionOverlay.java index 3fcd503..62ec2de 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/PointSelectionOverlay.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/overlay/implementation/PointSelectionOverlay.java @@ -1,12 +1,11 @@ package eu.konggdev.strikemaps.map.overlay.implementation; -import eu.konggdev.strikemaps.map.layer.MapLayer; -import eu.konggdev.strikemaps.map.layer.SourcedMapLayer; +import com.fasterxml.jackson.databind.JsonNode; import eu.konggdev.strikemaps.map.overlay.MapOverlay; public class PointSelectionOverlay implements MapOverlay { @Override - public SourcedMapLayer makeLayer() { + public JsonNode makePatch() { return null; } } diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/MapRenderer.java b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/MapRenderer.java index 55f086c..caad918 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/MapRenderer.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/MapRenderer.java @@ -1,17 +1,18 @@ package eu.konggdev.strikemaps.map.renderer; import android.view.View; -import android.view.ViewGroup; -import eu.konggdev.strikemaps.map.layer.MapLayer; -import org.maplibre.android.style.layers.Layer; +import eu.konggdev.strikemaps.map.overlay.MapOverlay; +import eu.konggdev.strikemaps.map.style.MapStyle; import org.maplibre.android.geometry.LatLng; import org.maplibre.geojson.Feature; import java.util.List; public interface MapRenderer { - void reload(); + void styleUpdate(MapStyle style); + + void overlayUpdate(MapOverlay overlay); View getView(); diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreGLJSRenderer.java b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreGLJSRenderer.java index 4d28734..b54d011 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreGLJSRenderer.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreGLJSRenderer.java @@ -3,16 +3,19 @@ package eu.konggdev.strikemaps.map.renderer.implementation; import android.annotation.SuppressLint; import android.view.View; import android.view.ViewGroup; +import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; +import androidx.annotation.NonNull; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import eu.konggdev.strikemaps.app.AppController; +import eu.konggdev.strikemaps.app.util.JsonPatcher; 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.renderer.MapRenderer; import eu.konggdev.strikemaps.map.style.MapStyle; @@ -25,64 +28,18 @@ import java.util.List; //Stub for now public class MapLibreGLJSRenderer implements MapRenderer { + @NonNull AppController app; - AppController app; - - MapComponent controller; + @NonNull MapComponent controller; final WebView webView; - @Override - 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 featuresAtPoint(LatLng point) { - return Collections.emptyList(); - } + private JsonNode origin; @SuppressLint({"JavascriptInterface", "SetJavaScriptEnabled"}) public MapLibreGLJSRenderer(AppController app, MapComponent controller) { + this.app = app; + this.controller = controller; webView = new WebView(app.getActivity()); webView.setLayoutParams( new ViewGroup.LayoutParams( @@ -100,7 +57,90 @@ public class MapLibreGLJSRenderer implements MapRenderer { 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 featuresAtPoint(LatLng point) { + return Collections.emptyList(); } class Bridge { } diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreNativeRenderer.java b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreNativeRenderer.java index e12445c..7d8c7ca 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreNativeRenderer.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/MapLibreNativeRenderer.java @@ -5,12 +5,13 @@ import android.view.View; import androidx.annotation.NonNull; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; 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.map.overlay.MapOverlay; -import eu.konggdev.strikemaps.map.layer.SourcedMapLayer; import eu.konggdev.strikemaps.map.renderer.MapRenderer; import eu.konggdev.strikemaps.map.style.MapStyle; import org.maplibre.android.MapLibre; @@ -27,11 +28,13 @@ import eu.konggdev.strikemaps.app.AppController; import eu.konggdev.strikemaps.map.MapComponent; public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback { - AppController app; - MapComponent controller; + @NonNull AppController app; + @NonNull MapComponent controller; MapLibreMap map; final MapView mapView; + private JsonNode origin; + public MapLibreNativeRenderer(AppController app, MapComponent controller) { this.app = app; this.controller = controller; @@ -42,37 +45,69 @@ public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback { } @Override - public void reload() { + public void styleUpdate(MapStyle style) { ObjectMapper mapper = new ObjectMapper(); 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 { + 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 { - /* 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); - - map.setStyle(new Style.Builder().fromJson(mapper.writeValueAsString(root))); + map.setStyle(new Style.Builder().fromJson(mapper.writeValueAsString(origin))); } catch (Exception e) { - app.logcat("Failed to reload Map"); + 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(); } } @@ -91,7 +126,7 @@ public class MapLibreNativeRenderer implements MapRenderer, OnMapReadyCallback { public void onMapReady(@NonNull MapLibreMap 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 :) map.getUiSettings().setLogoEnabled(false); diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/VtmRenderer.java b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/VtmRenderer.java index 463849e..9af0c9f 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/VtmRenderer.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/renderer/implementation/VtmRenderer.java @@ -3,7 +3,9 @@ package eu.konggdev.strikemaps.map.renderer.implementation; import android.view.View; import eu.konggdev.strikemaps.app.AppController; 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.style.MapStyle; import okhttp3.OkHttpClient; import org.maplibre.android.geometry.LatLng; import org.maplibre.geojson.Feature; @@ -16,6 +18,7 @@ import java.util.Collections; import java.util.List; public class VtmRenderer implements MapRenderer { + AppController app; MapComponent controller; @@ -30,7 +33,12 @@ public class VtmRenderer implements MapRenderer { } @Override - public void reload() { + public void overlayUpdate(MapOverlay overlay) { + //TODO + } + + @Override + public void styleUpdate(MapStyle style) { //TODO OkHttpClient.Builder builder = new OkHttpClient.Builder(); OSciMap4TileSource tileSource = OSciMap4TileSource.builder().httpFactory(new OkHttpEngine.OkHttpFactory(builder)).build(); diff --git a/app/src/main/java/eu/konggdev/strikemaps/map/style/MapStyle.java b/app/src/main/java/eu/konggdev/strikemaps/map/style/MapStyle.java index 3bd701a..8dad321 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/map/style/MapStyle.java +++ b/app/src/main/java/eu/konggdev/strikemaps/map/style/MapStyle.java @@ -20,10 +20,10 @@ public class MapStyle { public JsonNode metadata; // everything except layers + sources public Map sources; - public ArrayNode layerDefinitions; // the "layers" array + public ArrayNode layerDefinitions; // "layers" array //FIXME - public static MapStyle fromMapLibreJsonFile(String filename, AppController app) { + public static MapStyle fromJsonFile(String filename, AppController app) { String styleContents; if (filename.startsWith("/storage")) styleContents = FileHelper.loadStringFromUserFile(filename); else styleContents = FileHelper.loadStringFromAssetFile(filename, app); diff --git a/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/popup/FragmentMapChangePopup.java b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/popup/FragmentMapChangePopup.java index c532883..98d3d27 100644 --- a/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/popup/FragmentMapChangePopup.java +++ b/app/src/main/java/eu/konggdev/strikemaps/ui/fragment/popup/FragmentMapChangePopup.java @@ -56,6 +56,6 @@ public class FragmentMapChangePopup extends Fragment implements Popup { stylePaths.addAll(Arrays.asList(FileHelper.getUserFiles("style", ".style.json", app))); LinearLayout stylesLayout = view.findViewById(R.id.stylesLayout); 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)); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c661c57..04c32e9 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,10 +2,10 @@ Strike Maps Settings 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 <3 by konggdev + Map Rendering libraries Included;\n - MapLibre developed by MapLibre Organization\n - Vtm developed by Mapsforge\nStrike Maps made with <3 by konggdev - MapLibre Native MapLibre GL JS + MapLibre Native VTM \ No newline at end of file