From 3bb8ce00c56e84376a56745883b0829716e0232e Mon Sep 17 00:00:00 2001 From: Emux Date: Thu, 21 Jan 2021 15:01:19 +0200 Subject: [PATCH] Render themes: Android scoped storage, zip render theme, custom resource providers (#804) --- docs/Changelog.md | 5 +- vtm-android-example/res/menu/theme_menu.xml | 3 + vtm-android-example/res/values/strings.xml | 4 +- .../oscim/android/test/MapsforgeActivity.java | 183 ++++++++++++------ .../src/org/oscim/android/test/Samples.java | 2 +- .../oscim/android/canvas/AndroidGraphics.java | 5 +- .../android/theme/AssetsRenderTheme.java | 13 ++ .../android/theme/ContentRenderTheme.java | 39 ++-- .../ContentResolverResourceProvider.java | 146 ++++++++++++++ .../src/org/oscim/awt/AwtGraphics.java | 10 +- .../org/oscim/ios/test/IOSPathLayerTest.java | 3 +- .../org/oscim/ios/backend/IosGraphics.java | 5 +- vtm-tests/build.gradle | 1 + vtm-tests/resources/xmlthemetest.zip | Bin 0 -> 3531 bytes .../ZipXmlThemeResourceProviderTest.java | 86 ++++++++ vtm-themes/src/org/oscim/theme/VtmThemes.java | 10 + .../org/oscim/gdx/client/GwtGdxGraphics.java | 4 +- vtm/src/org/oscim/backend/CanvasAdapter.java | 47 +++-- .../org/oscim/theme/ExternalRenderTheme.java | 12 ++ .../org/oscim/theme/StreamRenderTheme.java | 18 +- vtm/src/org/oscim/theme/ThemeFile.java | 11 ++ vtm/src/org/oscim/theme/XmlThemeBuilder.java | 9 +- .../oscim/theme/XmlThemeResourceProvider.java | 32 +++ vtm/src/org/oscim/theme/ZipRenderTheme.java | 120 ++++++++++++ .../theme/ZipXmlThemeResourceProvider.java | 124 ++++++++++++ vtm/src/org/oscim/utils/Utils.java | 5 +- 26 files changed, 778 insertions(+), 119 deletions(-) create mode 100644 vtm-android/src/org/oscim/android/theme/ContentResolverResourceProvider.java create mode 100644 vtm-tests/resources/xmlthemetest.zip create mode 100644 vtm-tests/test/org/oscim/theme/ZipXmlThemeResourceProviderTest.java create mode 100644 vtm/src/org/oscim/theme/XmlThemeResourceProvider.java create mode 100644 vtm/src/org/oscim/theme/ZipRenderTheme.java create mode 100644 vtm/src/org/oscim/theme/ZipXmlThemeResourceProvider.java diff --git a/docs/Changelog.md b/docs/Changelog.md index 76975796..2afb4e30 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -2,13 +2,16 @@ ## New since 0.15.0 +- Android: scoped storage map / theme example [#804](https://github.com/mapsforge/vtm/pull/804) +- Render theme from zip archive [#804](https://github.com/mapsforge/vtm/pull/804) +- Render themes: custom resource providers [#804](https://github.com/mapsforge/vtm/pull/804) - Nautical unit adapter with feet [#803](https://github.com/mapsforge/vtm/pull/803) - Many other minor improvements and bug fixes - [Solved issues](https://github.com/mapsforge/vtm/issues?q=is%3Aclosed+milestone%3A0.16.0) ## Version 0.15.0 (2021-01-01) -- Android: scoped storage example [#785](https://github.com/mapsforge/vtm/pull/785) +- Android: scoped storage map example [#785](https://github.com/mapsforge/vtm/pull/785) - Mapsforge: map stream support [#784](https://github.com/mapsforge/vtm/pull/784) - Render theme from Android content providers [#783](https://github.com/mapsforge/vtm/pull/783) - Render theme xml pull parser [#786](https://github.com/mapsforge/vtm/pull/786) diff --git a/vtm-android-example/res/menu/theme_menu.xml b/vtm-android-example/res/menu/theme_menu.xml index 7a93959d..c32ba88d 100644 --- a/vtm-android-example/res/menu/theme_menu.xml +++ b/vtm-android-example/res/menu/theme_menu.xml @@ -20,6 +20,9 @@ + diff --git a/vtm-android-example/res/values/strings.xml b/vtm-android-example/res/values/strings.xml index d4757468..fe1ea541 100644 --- a/vtm-android-example/res/values/strings.xml +++ b/vtm-android-example/res/values/strings.xml @@ -6,7 +6,8 @@ Osmagray Tubes NewTron - External theme + External theme (Android 5) + External theme archive Line Area Outline @@ -15,6 +16,7 @@ Hide nature Grid Reverse Geocoding + Select a theme Add Cancel Error diff --git a/vtm-android-example/src/org/oscim/android/test/MapsforgeActivity.java b/vtm-android-example/src/org/oscim/android/test/MapsforgeActivity.java index 059706a3..f0adacbf 100644 --- a/vtm-android-example/src/org/oscim/android/test/MapsforgeActivity.java +++ b/vtm-android-example/src/org/oscim/android/test/MapsforgeActivity.java @@ -1,8 +1,9 @@ /* * Copyright 2014 Hannes Janetzek - * Copyright 2016-2020 devemux86 + * Copyright 2016-2021 devemux86 * Copyright 2017 Longri * Copyright 2018 Gustl22 + * Copyright 2021 eddiemuc * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -20,13 +21,17 @@ package org.oscim.android.test; import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.provider.DocumentsContract; import android.view.Menu; import android.view.MenuItem; import org.oscim.android.theme.ContentRenderTheme; +import org.oscim.android.theme.ContentResolverResourceProvider; import org.oscim.backend.CanvasAdapter; import org.oscim.core.MapElement; import org.oscim.core.MapPosition; @@ -42,9 +47,7 @@ import org.oscim.renderer.BitmapRenderer; import org.oscim.renderer.GLViewport; import org.oscim.renderer.bucket.RenderBuckets; import org.oscim.scalebar.*; -import org.oscim.theme.IRenderTheme; -import org.oscim.theme.ThemeFile; -import org.oscim.theme.VtmThemes; +import org.oscim.theme.*; import org.oscim.theme.styles.AreaStyle; import org.oscim.theme.styles.RenderStyle; import org.oscim.tiling.source.mapfile.MapFileTileSource; @@ -52,15 +55,20 @@ import org.oscim.tiling.source.mapfile.MapInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; +import java.util.List; +import java.util.zip.ZipInputStream; public class MapsforgeActivity extends MapActivity { private static final Logger log = LoggerFactory.getLogger(MapsforgeActivity.class); static final int SELECT_MAP_FILE = 0; - static final int SELECT_THEME_FILE = 1; + private static final int SELECT_THEME_ARCHIVE = 1; + private static final int SELECT_THEME_DIR = 2; + static final int SELECT_THEME_FILE = 3; private static final Tag ISSEA_TAG = new Tag("natural", "issea"); private static final Tag NOSEA_TAG = new Tag("natural", "nosea"); @@ -71,6 +79,7 @@ public class MapsforgeActivity extends MapActivity { private final boolean mS3db; IRenderTheme mTheme; VectorTileLayer mTileLayer; + private Uri mThemeDirUri; public MapsforgeActivity() { this(false); @@ -142,11 +151,19 @@ public class MapsforgeActivity extends MapActivity { item.setChecked(true); return true; - case R.id.theme_external: + case R.id.theme_external_archive: Intent intent = new Intent(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); - startActivityForResult(intent, SELECT_THEME_FILE); + startActivityForResult(intent, SELECT_THEME_ARCHIVE); + return true; + + case R.id.theme_external: + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return false; + intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivityForResult(intent, SELECT_THEME_DIR); return true; case R.id.gridlayer: @@ -176,75 +193,100 @@ public class MapsforgeActivity extends MapActivity { return; } - MapFileTileSource tileSource = new MapFileTileSource(); - //tileSource.setPreferredLanguage("en"); + try { + Uri uri = data.getData(); + + MapFileTileSource tileSource = new MapFileTileSource(); + //tileSource.setPreferredLanguage("en"); + FileInputStream fis = (FileInputStream) getContentResolver().openInputStream(uri); + tileSource.setMapFileInputStream(fis); + + mTileLayer = mMap.setBaseMap(tileSource); + loadTheme(null); + + if (mS3db) + mMap.layers().add(new S3DBLayer(mMap, mTileLayer)); + else + mMap.layers().add(new BuildingLayer(mMap, mTileLayer)); + mMap.layers().add(new LabelLayer(mMap, mTileLayer)); + + DefaultMapScaleBar mapScaleBar = new DefaultMapScaleBar(mMap); + mapScaleBar.setScaleBarMode(DefaultMapScaleBar.ScaleBarMode.BOTH); + mapScaleBar.setDistanceUnitAdapter(MetricUnitAdapter.INSTANCE); + mapScaleBar.setSecondaryDistanceUnitAdapter(ImperialUnitAdapter.INSTANCE); + mapScaleBar.setScaleBarPosition(MapScaleBar.ScaleBarPosition.BOTTOM_LEFT); + + MapScaleBarLayer mapScaleBarLayer = new MapScaleBarLayer(mMap, mapScaleBar); + BitmapRenderer renderer = mapScaleBarLayer.getRenderer(); + renderer.setPosition(GLViewport.Position.BOTTOM_LEFT); + renderer.setOffset(5 * CanvasAdapter.getScale(), 0); + mMap.layers().add(mapScaleBarLayer); + + MapInfo info = tileSource.getMapInfo(); + if (!info.boundingBox.contains(mMap.getMapPosition().getGeoPoint())) { + MapPosition pos = new MapPosition(); + pos.setByBoundingBox(info.boundingBox, Tile.SIZE * 4, Tile.SIZE * 4); + mMap.setMapPosition(pos); + mPrefs.clear(); + } + } catch (Exception e) { + log.error(e.getMessage()); + finish(); + } + } else if (requestCode == SELECT_THEME_ARCHIVE) { + if (resultCode != Activity.RESULT_OK || data == null) + return; try { Uri uri = data.getData(); - FileInputStream fis = (FileInputStream) getContentResolver().openInputStream(uri); - tileSource.setMapFileInputStream(fis); + + final ZipXmlThemeResourceProvider resourceProvider = new ZipXmlThemeResourceProvider(new ZipInputStream(new BufferedInputStream(getContentResolver().openInputStream(uri)))); + final List xmlThemes = resourceProvider.getXmlThemes(); + if (xmlThemes.isEmpty()) + return; + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.dialog_theme_title); + builder.setSingleChoiceItems(xmlThemes.toArray(new String[0]), -1, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + ThemeFile theme = new ZipRenderTheme(xmlThemes.get(which), resourceProvider); + if (mTheme != null) + mTheme.dispose(); + mTheme = mMap.setTheme(theme); + mapsforgeTheme(mTheme); + mMenu.findItem(R.id.theme_external_archive).setChecked(true); + } + }); + builder.show(); } catch (IOException e) { - log.error(e.getMessage()); - finish(); + e.printStackTrace(); + } + } else if (requestCode == SELECT_THEME_DIR) { + if (resultCode != Activity.RESULT_OK || data == null) return; - } - mTileLayer = mMap.setBaseMap(tileSource); - loadTheme(null); + mThemeDirUri = data.getData(); - if (mS3db) - mMap.layers().add(new S3DBLayer(mMap, mTileLayer)); - else - mMap.layers().add(new BuildingLayer(mMap, mTileLayer)); - mMap.layers().add(new LabelLayer(mMap, mTileLayer)); - - DefaultMapScaleBar mapScaleBar = new DefaultMapScaleBar(mMap); - mapScaleBar.setScaleBarMode(DefaultMapScaleBar.ScaleBarMode.BOTH); - mapScaleBar.setDistanceUnitAdapter(MetricUnitAdapter.INSTANCE); - mapScaleBar.setSecondaryDistanceUnitAdapter(ImperialUnitAdapter.INSTANCE); - mapScaleBar.setScaleBarPosition(MapScaleBar.ScaleBarPosition.BOTTOM_LEFT); - - MapScaleBarLayer mapScaleBarLayer = new MapScaleBarLayer(mMap, mapScaleBar); - BitmapRenderer renderer = mapScaleBarLayer.getRenderer(); - renderer.setPosition(GLViewport.Position.BOTTOM_LEFT); - renderer.setOffset(5 * CanvasAdapter.getScale(), 0); - mMap.layers().add(mapScaleBarLayer); - - MapInfo info = tileSource.getMapInfo(); - if (!info.boundingBox.contains(mMap.getMapPosition().getGeoPoint())) { - MapPosition pos = new MapPosition(); - pos.setByBoundingBox(info.boundingBox, Tile.SIZE * 4, Tile.SIZE * 4); - mMap.setMapPosition(pos); - mPrefs.clear(); - } + // Now we have the directory for resources, but we need to let the user also select a theme file + Intent intent = new Intent(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mThemeDirUri); + startActivityForResult(intent, SELECT_THEME_FILE); } else if (requestCode == SELECT_THEME_FILE) { if (resultCode != Activity.RESULT_OK || data == null) return; Uri uri = data.getData(); - ThemeFile theme = new ContentRenderTheme(getContentResolver(), "", uri); - - // Use tessellation with sea and land for Mapsforge themes - if (theme.isMapsforgeTheme()) { - mTileLayer.addHook(new VectorTileLayer.TileLoaderThemeHook() { - @Override - public boolean process(MapTile tile, RenderBuckets buckets, MapElement element, RenderStyle style, int level) { - if (element.tags.contains(ISSEA_TAG) || element.tags.contains(SEA_TAG) || element.tags.contains(NOSEA_TAG)) { - if (style instanceof AreaStyle) - ((AreaStyle) style).mesh = true; - } - return false; - } - - @Override - public void complete(MapTile tile, boolean success) { - } - }); - } + ThemeFile theme = new ContentRenderTheme(getContentResolver(), uri); + theme.setResourceProvider(new ContentResolverResourceProvider(getContentResolver(), mThemeDirUri)); if (mTheme != null) mTheme.dispose(); mTheme = mMap.setTheme(theme); + mapsforgeTheme(mTheme); mMenu.findItem(R.id.theme_external).setChecked(true); } } @@ -254,4 +296,25 @@ public class MapsforgeActivity extends MapActivity { mTheme.dispose(); mTheme = mMap.setTheme(VtmThemes.DEFAULT); } + + private void mapsforgeTheme(IRenderTheme theme) { + if (!theme.isMapsforgeTheme()) + return; + + // Use tessellation with sea and land for Mapsforge themes + mTileLayer.addHook(new VectorTileLayer.TileLoaderThemeHook() { + @Override + public boolean process(MapTile tile, RenderBuckets buckets, MapElement element, RenderStyle style, int level) { + if (element.tags.contains(ISSEA_TAG) || element.tags.contains(SEA_TAG) || element.tags.contains(NOSEA_TAG)) { + if (style instanceof AreaStyle) + ((AreaStyle) style).mesh = true; + } + return false; + } + + @Override + public void complete(MapTile tile, boolean success) { + } + }); + } } diff --git a/vtm-android-example/src/org/oscim/android/test/Samples.java b/vtm-android-example/src/org/oscim/android/test/Samples.java index 83c153cf..23ae7c7c 100644 --- a/vtm-android-example/src/org/oscim/android/test/Samples.java +++ b/vtm-android-example/src/org/oscim/android/test/Samples.java @@ -86,8 +86,8 @@ public class Samples extends Activity { LinearLayout linearLayout = findViewById(R.id.samples); linearLayout.addView(createButton(GettingStarted.class)); linearLayout.addView(createLabel(null)); - linearLayout.addView(createButton(SimpleMapActivity.class)); linearLayout.addView(createButton(MapsforgeActivity.class)); + linearLayout.addView(createButton(SimpleMapActivity.class)); linearLayout.addView(createButton(MBTilesMvtActivity.class)); linearLayout.addView(createButton(MapilionMvtActivity.class)); /*linearLayout.addView(createButton(MapzenMvtActivity.class)); diff --git a/vtm-android/src/org/oscim/android/canvas/AndroidGraphics.java b/vtm-android/src/org/oscim/android/canvas/AndroidGraphics.java index b608d592..81964b08 100644 --- a/vtm-android/src/org/oscim/android/canvas/AndroidGraphics.java +++ b/vtm-android/src/org/oscim/android/canvas/AndroidGraphics.java @@ -30,6 +30,7 @@ import org.oscim.backend.canvas.Canvas; import org.oscim.backend.canvas.Paint; import org.oscim.layers.marker.MarkerSymbol; import org.oscim.layers.marker.MarkerSymbol.HotspotPlace; +import org.oscim.theme.XmlThemeResourceProvider; import java.io.IOException; import java.io.InputStream; @@ -69,8 +70,8 @@ public final class AndroidGraphics extends CanvasAdapter { } @Override - public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) throws IOException { - return createBitmap(relativePathPrefix, src, width, height, percent); + public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException { + return createBitmap(relativePathPrefix, src, resourceProvider, width, height, percent); } @Override diff --git a/vtm-android/src/org/oscim/android/theme/AssetsRenderTheme.java b/vtm-android/src/org/oscim/android/theme/AssetsRenderTheme.java index ff969d49..9be12478 100644 --- a/vtm-android/src/org/oscim/android/theme/AssetsRenderTheme.java +++ b/vtm-android/src/org/oscim/android/theme/AssetsRenderTheme.java @@ -2,6 +2,7 @@ * Copyright 2010, 2011, 2012 mapsforge.org * Copyright 2016-2021 devemux86 * Copyright 2017 Andrey Novikov + * Copyright 2021 eddiemuc * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -21,6 +22,7 @@ import android.text.TextUtils; import org.oscim.theme.IRenderTheme.ThemeException; import org.oscim.theme.ThemeFile; import org.oscim.theme.XmlRenderThemeMenuCallback; +import org.oscim.theme.XmlThemeResourceProvider; import org.oscim.utils.Utils; import java.io.IOException; @@ -38,6 +40,7 @@ public class AssetsRenderTheme implements ThemeFile { private boolean mMapsforgeTheme; private XmlRenderThemeMenuCallback mMenuCallback; private final String mRelativePathPrefix; + private XmlThemeResourceProvider mResourceProvider; /** * @param assetManager the Android asset manager. @@ -99,6 +102,11 @@ public class AssetsRenderTheme implements ThemeFile { } } + @Override + public XmlThemeResourceProvider getResourceProvider() { + return mResourceProvider; + } + @Override public boolean isMapsforgeTheme() { return mMapsforgeTheme; @@ -113,4 +121,9 @@ public class AssetsRenderTheme implements ThemeFile { public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) { mMenuCallback = menuCallback; } + + @Override + public void setResourceProvider(XmlThemeResourceProvider resourceProvider) { + mResourceProvider = resourceProvider; + } } diff --git a/vtm-android/src/org/oscim/android/theme/ContentRenderTheme.java b/vtm-android/src/org/oscim/android/theme/ContentRenderTheme.java index d90e06a5..bf11eece 100644 --- a/vtm-android/src/org/oscim/android/theme/ContentRenderTheme.java +++ b/vtm-android/src/org/oscim/android/theme/ContentRenderTheme.java @@ -1,5 +1,6 @@ /* * Copyright 2020-2021 devemux86 + * Copyright 2021 eddiemuc * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -19,7 +20,7 @@ import android.net.Uri; import org.oscim.theme.IRenderTheme.ThemeException; import org.oscim.theme.ThemeFile; import org.oscim.theme.XmlRenderThemeMenuCallback; -import org.oscim.utils.Utils; +import org.oscim.theme.XmlThemeResourceProvider; import java.io.IOException; import java.io.InputStream; @@ -34,29 +35,26 @@ public class ContentRenderTheme implements ThemeFile { private final ContentResolver mContentResolver; private boolean mMapsforgeTheme; private XmlRenderThemeMenuCallback mMenuCallback; - private final String mRelativePathPrefix; + private XmlThemeResourceProvider mResourceProvider; private final Uri mUri; /** - * @param contentResolver the Android content resolver. - * @param relativePathPrefix the prefix for all relative resource paths. - * @param uri the XML render theme URI. + * @param contentResolver the Android content resolver. + * @param uri the XML render theme URI. * @throws ThemeException if an error occurs while reading the render theme XML. */ - public ContentRenderTheme(ContentResolver contentResolver, String relativePathPrefix, Uri uri) throws ThemeException { - this(contentResolver, relativePathPrefix, uri, null); + public ContentRenderTheme(ContentResolver contentResolver, Uri uri) throws ThemeException { + this(contentResolver, uri, null); } /** - * @param contentResolver the Android content resolver. - * @param relativePathPrefix the prefix for all relative resource paths. - * @param uri the XML render theme URI. - * @param menuCallback the interface callback to create a settings menu on the fly. + * @param contentResolver the Android content resolver. + * @param uri the XML render theme URI. + * @param menuCallback the interface callback to create a settings menu on the fly. * @throws ThemeException if an error occurs while reading the render theme XML. */ - public ContentRenderTheme(ContentResolver contentResolver, String relativePathPrefix, Uri uri, XmlRenderThemeMenuCallback menuCallback) throws ThemeException { + public ContentRenderTheme(ContentResolver contentResolver, Uri uri, XmlRenderThemeMenuCallback menuCallback) throws ThemeException { mContentResolver = contentResolver; - mRelativePathPrefix = relativePathPrefix; mUri = uri; mMenuCallback = menuCallback; } @@ -72,9 +70,6 @@ public class ContentRenderTheme implements ThemeFile { if (getRenderThemeAsStream() != other.getRenderThemeAsStream()) { return false; } - if (!Utils.equals(mRelativePathPrefix, other.mRelativePathPrefix)) { - return false; - } return true; } @@ -85,7 +80,7 @@ public class ContentRenderTheme implements ThemeFile { @Override public String getRelativePathPrefix() { - return mRelativePathPrefix; + return ""; } @Override @@ -97,6 +92,11 @@ public class ContentRenderTheme implements ThemeFile { } } + @Override + public XmlThemeResourceProvider getResourceProvider() { + return mResourceProvider; + } + @Override public boolean isMapsforgeTheme() { return mMapsforgeTheme; @@ -111,4 +111,9 @@ public class ContentRenderTheme implements ThemeFile { public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) { mMenuCallback = menuCallback; } + + @Override + public void setResourceProvider(XmlThemeResourceProvider resourceProvider) { + mResourceProvider = resourceProvider; + } } diff --git a/vtm-android/src/org/oscim/android/theme/ContentResolverResourceProvider.java b/vtm-android/src/org/oscim/android/theme/ContentResolverResourceProvider.java new file mode 100644 index 00000000..d0bc3b20 --- /dev/null +++ b/vtm-android/src/org/oscim/android/theme/ContentResolverResourceProvider.java @@ -0,0 +1,146 @@ +/* + * Copyright 2021 eddiemuc + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.android.theme; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import org.oscim.backend.CanvasAdapter; +import org.oscim.theme.XmlThemeResourceProvider; +import org.oscim.utils.IOUtils; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.*; + +/** + * An xml theme resource provider resolving resources using Android scoped storage (document framework). + *

+ * Implementation note: these methods do not use DocumentFile internally, + * but query directly for document info due to vastly better performance. + * Also for better performance, this implementation caches resource uris. + *

+ * Note: this implementation requires minimum Android 5.0 (API 21) + */ +public class ContentResolverResourceProvider implements XmlThemeResourceProvider { + + private final ContentResolver contentResolver; + private final Uri relativeRootUri; + + private final Map resourceUriCache = new HashMap<>(); + + private static class DocumentInfo { + private final String name; + private final Uri uri; + private final boolean isDirectory; + + private DocumentInfo(String name, Uri uri, boolean isDirectory) { + this.name = name; + this.uri = uri; + this.isDirectory = isDirectory; + } + } + + public ContentResolverResourceProvider(ContentResolver contentResolver, Uri treeUri) { + this.contentResolver = contentResolver; + this.relativeRootUri = treeUri; + + refreshCache(); + } + + /** + * Build uri cache for one dir level (recursive function). + */ + private void buildCacheLevel(String prefix, Uri dirUri) { + List docs = queryDir(dirUri); + for (DocumentInfo doc : docs) { + if (doc.isDirectory) { + buildCacheLevel(prefix + doc.name + "/", doc.uri); + } else { + resourceUriCache.put(prefix + doc.name, doc.uri); + } + } + } + + @Override + public InputStream createInputStream(String relativePath, String source) throws FileNotFoundException { + Uri docUri = resourceUriCache.get(source); + if (docUri != null) { + return contentResolver.openInputStream(docUri); + } + return null; + } + + /** + * Query the content of a directory using scoped storage. + * + * @return a list of arrays with info [0: name (String), 1: uri (Uri), 2: isDir (boolean)] + */ + private List queryDir(Uri dirUri) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return Collections.emptyList(); + } + if (dirUri == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(dirUri, DocumentsContract.getDocumentId(dirUri)); + + String[] columns = new String[]{ + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE + }; + + Cursor c = null; + try { + c = contentResolver.query(childrenUri, columns, null, null, null); + + while (c.moveToNext()) { + String documentId = c.getString(0); + String name = c.getString(1); + String mimeType = c.getString(2); + + Uri uri = DocumentsContract.buildDocumentUriUsingTree(dirUri, documentId); + boolean isDir = DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType); + result.add(new DocumentInfo(name, uri, isDir)); + } + + return result; + } finally { + IOUtils.closeQuietly(c); + } + } + + /** + * Refresh the uri cache by recreating it. + */ + private void refreshCache() { + resourceUriCache.clear(); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + if (relativeRootUri == null) { + return; + } + + Uri dirUri = DocumentsContract.buildDocumentUriUsingTree(relativeRootUri, DocumentsContract.getTreeDocumentId(relativeRootUri)); + buildCacheLevel(CanvasAdapter.PREFIX_FILE, dirUri); + } +} diff --git a/vtm-desktop/src/org/oscim/awt/AwtGraphics.java b/vtm-desktop/src/org/oscim/awt/AwtGraphics.java index 54aa6da5..e25a4d1f 100644 --- a/vtm-desktop/src/org/oscim/awt/AwtGraphics.java +++ b/vtm-desktop/src/org/oscim/awt/AwtGraphics.java @@ -23,11 +23,9 @@ import org.oscim.backend.Platform; import org.oscim.backend.canvas.Bitmap; import org.oscim.backend.canvas.Canvas; import org.oscim.backend.canvas.Paint; +import org.oscim.theme.XmlThemeResourceProvider; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; @@ -119,7 +117,7 @@ public class AwtGraphics extends CanvasAdapter { } @Override - public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) throws IOException { - return createBitmap(relativePathPrefix, src, width, height, percent); + public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException { + return createBitmap(relativePathPrefix, src, resourceProvider, width, height, percent); } } diff --git a/vtm-ios-example/src/org/oscim/ios/test/IOSPathLayerTest.java b/vtm-ios-example/src/org/oscim/ios/test/IOSPathLayerTest.java index bae128f2..cfd7f4a5 100644 --- a/vtm-ios-example/src/org/oscim/ios/test/IOSPathLayerTest.java +++ b/vtm-ios-example/src/org/oscim/ios/test/IOSPathLayerTest.java @@ -19,7 +19,6 @@ package org.oscim.ios.test; import com.badlogic.gdx.graphics.glutils.GLVersion; - import org.oscim.backend.GLAdapter; import org.oscim.backend.canvas.Color; import org.oscim.core.GeoPoint; @@ -74,7 +73,7 @@ public class IOSPathLayerTest extends GdxMap { mMap.setMapPosition(0, 0, 1 << 2); - tex = Utils.loadTexture("", "patterns/pike.png", 0, 0, 100); + tex = Utils.loadTexture("", "patterns/pike.png", null, 0, 0, 100); // tex = new TextureItem(CanvasAdapter.getBitmapAsset("", "patterns/pike.png")); tex.mipmap = true; diff --git a/vtm-ios/src/org/oscim/ios/backend/IosGraphics.java b/vtm-ios/src/org/oscim/ios/backend/IosGraphics.java index 253fe649..39a57492 100644 --- a/vtm-ios/src/org/oscim/ios/backend/IosGraphics.java +++ b/vtm-ios/src/org/oscim/ios/backend/IosGraphics.java @@ -21,6 +21,7 @@ import org.oscim.backend.Platform; import org.oscim.backend.canvas.Bitmap; import org.oscim.backend.canvas.Canvas; import org.oscim.backend.canvas.Paint; +import org.oscim.theme.XmlThemeResourceProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,7 +71,7 @@ public class IosGraphics extends CanvasAdapter { } @Override - protected Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) throws IOException { - return createBitmap(relativePathPrefix, src, width, height, percent); + protected Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException { + return createBitmap(relativePathPrefix, src, resourceProvider, width, height, percent); } } diff --git a/vtm-tests/build.gradle b/vtm-tests/build.gradle index cee19e71..a2bc7ddd 100644 --- a/vtm-tests/build.gradle +++ b/vtm-tests/build.gradle @@ -13,4 +13,5 @@ dependencies { sourceSets { main.java.srcDirs = ['src'] test.java.srcDirs = ['test'] + test.resources.srcDirs = ['resources'] } diff --git a/vtm-tests/resources/xmlthemetest.zip b/vtm-tests/resources/xmlthemetest.zip new file mode 100644 index 0000000000000000000000000000000000000000..e3921de975f4555c41fbec6f83f7f127a306e87a GIT binary patch literal 3531 zcmWIWW@Zs#U|`^2DCy7)+PAG^%6%ZOnvsEl9Z2WrrRr7W=Cn@O=y%vapsifx-a(_{ zHzF+|uAbHxSNe!_UftS%r0K@g_dTC~!~}JUiPdy3{k<=K`To0pkC*>wYO!ncTNk3s z$|60Nzg9h-e|MJd_Sv^*n>iLt$Y>B`(Po^@S*y;zD0p$Hrt4I8(cty(Uso-?KkvK# zw8-F}4Jl2mmD}oiF3$Ndw=!Z$@vlZ+Hp?9ckKS|7?%@A@U&y$@M#4M~UK14c8}TIjQSkwedO_S8DQYvA-ev^^?2rR?XYrt(E5VzM*mX zQ%}8ud13Kecl=}vfQA7BLsFM!5YXFjzyf3yr55YMRB$j{chC&_cZsucA~1v!nHd;_ zp(>JcN>k&DOA?FX4fTr4(&q+8PnvBY@aKMTz$yI~hA$d}9tc0-i;j|J-WpYq_*Uro z7s=BKbJmBft#erW?&UneE7#ZSMYjdNkY1$aw`rg3F1t0e@5|TiFmdHvIbcbyYnq4YZTkHvA<8~@u_`x7PmmMtin;IMV;)GtoiS%%Y( zT${S!R79>p&>5Yc7{kf=C)NoRMl4+v!=+e!u#ss~$dr%<(aCBb8NA))?w!;-_DXt3 z=hfi0xgCeS%v-$l!p+U~y>@o_Ll39mF{ovxX8t%9CmNr9~s8s^S8An}Hz(SXCL#uR6d7WP>md z5QD0!lGNf7y^@NOkc`Y?1t3aP0ExhgKg>Ynh3U&EN*zg+J!VLN8ayTC`6D6~F*4aR z;A-}%0K)+U6u>0B5y%A1vys@YAT|J2DiqLElK<5|2Gt9Xaq+y zAIR{k3yokJ+3A=~1ehftr@vr=I33hrKsNb)$}1g^i7?u4#!^W_&X-1UJ|D0_2?`XD z$p|mN8yU!!!MtHOSy~Qci9-&AMtB1=tWYhx!Gg;&a6O34GLH-IR6&-7tc1|WmZ5ha z1W. + */ +package org.oscim.theme; + +import org.junit.Assert; +import org.junit.Test; +import org.oscim.utils.IOUtils; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.zip.ZipInputStream; + +public class ZipXmlThemeResourceProviderTest { + + @Test + public void openZip() throws IOException { + ZipInputStream zis = new ZipInputStream(new BufferedInputStream(ZipXmlThemeResourceProviderTest.class.getResourceAsStream("/xmlthemetest.zip"))); + Assert.assertNotNull(zis); + + ZipXmlThemeResourceProvider zts = new ZipXmlThemeResourceProvider(zis); + + // All files contained + Assert.assertNotNull(zts.createInputStream(null, "file:one.xml")); + Assert.assertNotNull(zts.createInputStream(null, "file:two.xml")); + Assert.assertNotNull(zts.createInputStream(null, "file:res/three.xml")); + Assert.assertNotNull(zts.createInputStream(null, "file:res/blue_star_1.svg")); + Assert.assertNotNull(zts.createInputStream(null, "file:res/test.txt")); + Assert.assertNotNull(zts.createInputStream(null, "file:res/sub/four.xml")); + Assert.assertNotNull(zts.createInputStream(null, "file:res/sub/blue_star_sub_1.svg")); + Assert.assertNotNull(zts.createInputStream(null, "file:res/sub/blue_star_sub_2.svg")); + + //Relative Reference ok + Assert.assertNotNull(zts.createInputStream("", "file:res/sub/blue_star_sub_2.svg")); + Assert.assertNotNull(zts.createInputStream("res", "file:sub/blue_star_sub_2.svg")); + Assert.assertNotNull(zts.createInputStream("/", "file:res/sub/blue_star_sub_2.svg")); + Assert.assertNotNull(zts.createInputStream("/res", "file:sub/blue_star_sub_2.svg")); + Assert.assertNotNull(zts.createInputStream("res/", "file:/sub/blue_star_sub_2.svg")); + + // Can get same files using various other formats + Assert.assertNotNull(zts.createInputStream(null, "res/sub/blue_star_sub_2.svg")); + Assert.assertNotNull(zts.createInputStream(null, "/res/sub/blue_star_sub_2.svg")); + Assert.assertNotNull(zts.createInputStream(null, "file:/res/sub/blue_star_sub_2.svg")); + + // Dirs NOT contained! + Assert.assertNull(zts.createInputStream(null, "file:res/")); + + Assert.assertEquals(8, zts.getCount()); + + List xmlThemes = zts.getXmlThemes(); + Assert.assertEquals(4, xmlThemes.size()); + Assert.assertTrue(xmlThemes.contains("one.xml")); + Assert.assertTrue(xmlThemes.contains("two.xml")); + Assert.assertTrue(xmlThemes.contains("res/three.xml")); + Assert.assertTrue(xmlThemes.contains("res/sub/four.xml")); + + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(zts.createInputStream(null, "file:res/test.txt"))); + String line = reader.readLine(); + Assert.assertEquals(line, "This is a test"); + } finally { + IOUtils.closeQuietly(reader); + } + } + + @Test + public void openEmpty() throws IOException { + Assert.assertTrue(new ZipXmlThemeResourceProvider(null).getXmlThemes().isEmpty()); + } +} diff --git a/vtm-themes/src/org/oscim/theme/VtmThemes.java b/vtm-themes/src/org/oscim/theme/VtmThemes.java index c5ce9f45..ccca4449 100644 --- a/vtm-themes/src/org/oscim/theme/VtmThemes.java +++ b/vtm-themes/src/org/oscim/theme/VtmThemes.java @@ -4,6 +4,7 @@ * Copyright 2016-2021 devemux86 * Copyright 2017 nebular * Copyright 2017 Andrey Novikov + * Copyright 2021 eddiemuc * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -59,6 +60,11 @@ public enum VtmThemes implements ThemeFile { return AssetAdapter.readFileAsStream(mPath); } + @Override + public XmlThemeResourceProvider getResourceProvider() { + return null; + } + @Override public boolean isMapsforgeTheme() { return false; @@ -71,4 +77,8 @@ public enum VtmThemes implements ThemeFile { @Override public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) { } + + @Override + public void setResourceProvider(XmlThemeResourceProvider resourceProvider) { + } } diff --git a/vtm-web/src/org/oscim/gdx/client/GwtGdxGraphics.java b/vtm-web/src/org/oscim/gdx/client/GwtGdxGraphics.java index 270b3419..917f2fa9 100644 --- a/vtm-web/src/org/oscim/gdx/client/GwtGdxGraphics.java +++ b/vtm-web/src/org/oscim/gdx/client/GwtGdxGraphics.java @@ -21,11 +21,11 @@ package org.oscim.gdx.client; import com.google.gwt.canvas.client.Canvas; import com.google.gwt.canvas.dom.client.Context2d; import com.google.gwt.canvas.dom.client.TextMetrics; - import org.oscim.backend.CanvasAdapter; import org.oscim.backend.Platform; import org.oscim.backend.canvas.Bitmap; import org.oscim.backend.canvas.Paint; +import org.oscim.theme.XmlThemeResourceProvider; import java.io.File; import java.io.InputStream; @@ -68,7 +68,7 @@ public class GwtGdxGraphics extends CanvasAdapter { } @Override - public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) { + public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) { String pathName = (relativePathPrefix == null || relativePathPrefix.length() == 0 ? "" : relativePathPrefix + File.separatorChar) + src; return new GwtBitmap(pathName); } diff --git a/vtm/src/org/oscim/backend/CanvasAdapter.java b/vtm/src/org/oscim/backend/CanvasAdapter.java index 04a0c273..0eae14c9 100644 --- a/vtm/src/org/oscim/backend/CanvasAdapter.java +++ b/vtm/src/org/oscim/backend/CanvasAdapter.java @@ -1,7 +1,8 @@ /* * Copyright 2013 Hannes Janetzek - * Copyright 2016-2020 devemux86 + * Copyright 2016-2021 devemux86 * Copyright 2017 Longri + * Copyright 2021 eddiemuc * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -21,6 +22,7 @@ package org.oscim.backend; import org.oscim.backend.canvas.Bitmap; import org.oscim.backend.canvas.Canvas; import org.oscim.backend.canvas.Paint; +import org.oscim.theme.XmlThemeResourceProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,7 +39,7 @@ public abstract class CanvasAdapter { private static final Logger log = LoggerFactory.getLogger(CanvasAdapter.class); private static final String PREFIX_ASSETS = "assets:"; - private static final String PREFIX_FILE = "file:"; + public static final String PREFIX_FILE = "file:"; /** * The instance provided by backend @@ -156,34 +158,45 @@ public abstract class CanvasAdapter { * @param src the resource * @return the bitmap */ - protected abstract Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) throws IOException; + protected abstract Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException; public static Bitmap getBitmapAsset(String relativePathPrefix, String src) throws IOException { - return getBitmapAsset(relativePathPrefix, src, 0, 0, 100); + return getBitmapAsset(relativePathPrefix, src, null, 0, 0, 100); } - public static Bitmap getBitmapAsset(String relativePathPrefix, String src, int width, int height, int percent) throws IOException { - return g.loadBitmapAssetImpl(relativePathPrefix, src, width, height, percent); + public static Bitmap getBitmapAsset(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException { + return g.loadBitmapAssetImpl(relativePathPrefix, src, resourceProvider, width, height, percent); } - protected static Bitmap createBitmap(String relativePathPrefix, String src, int width, int height, int percent) throws IOException { + protected static Bitmap createBitmap(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException { if (src == null || src.length() == 0) { // no image source defined return null; } - InputStream inputStream; - if (src.startsWith(PREFIX_ASSETS)) { - src = src.substring(PREFIX_ASSETS.length()); - inputStream = inputStreamFromAssets(relativePathPrefix, src); - } else if (src.startsWith(PREFIX_FILE)) { - src = src.substring(PREFIX_FILE.length()); - inputStream = inputStreamFromFile(relativePathPrefix, src); - } else { - inputStream = inputStreamFromFile(relativePathPrefix, src); + InputStream inputStream = null; + if (resourceProvider != null) { + try { + inputStream = resourceProvider.createInputStream(relativePathPrefix, src); + } catch (IOException ioe) { + log.debug("Exception trying to access resource: " + src + " using custom provider: " + ioe); + // Ignore and try to resolve input stream using the standard process + } + } - if (inputStream == null) + if (inputStream == null) { + if (src.startsWith(PREFIX_ASSETS)) { + src = src.substring(PREFIX_ASSETS.length()); inputStream = inputStreamFromAssets(relativePathPrefix, src); + } else if (src.startsWith(PREFIX_FILE)) { + src = src.substring(PREFIX_FILE.length()); + inputStream = inputStreamFromFile(relativePathPrefix, src); + } else { + inputStream = inputStreamFromFile(relativePathPrefix, src); + + if (inputStream == null) + inputStream = inputStreamFromAssets(relativePathPrefix, src); + } } // Fallback to internal resources diff --git a/vtm/src/org/oscim/theme/ExternalRenderTheme.java b/vtm/src/org/oscim/theme/ExternalRenderTheme.java index 58e741b3..e1efca28 100644 --- a/vtm/src/org/oscim/theme/ExternalRenderTheme.java +++ b/vtm/src/org/oscim/theme/ExternalRenderTheme.java @@ -3,6 +3,7 @@ * Copyright 2013 Hannes Janetzek * Copyright 2016-2021 devemux86 * Copyright 2017 Andrey Novikov + * Copyright 2021 eddiemuc * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -37,6 +38,7 @@ public class ExternalRenderTheme implements ThemeFile { private boolean mMapsforgeTheme; private XmlRenderThemeMenuCallback mMenuCallback; private final String mPath; + private XmlThemeResourceProvider mResourceProvider; /** * @param fileName the path to the XML render theme file. @@ -109,6 +111,11 @@ public class ExternalRenderTheme implements ThemeFile { return is; } + @Override + public XmlThemeResourceProvider getResourceProvider() { + return mResourceProvider; + } + @Override public boolean isMapsforgeTheme() { return mMapsforgeTheme; @@ -123,4 +130,9 @@ public class ExternalRenderTheme implements ThemeFile { public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) { mMenuCallback = menuCallback; } + + @Override + public void setResourceProvider(XmlThemeResourceProvider resourceProvider) { + mResourceProvider = resourceProvider; + } } diff --git a/vtm/src/org/oscim/theme/StreamRenderTheme.java b/vtm/src/org/oscim/theme/StreamRenderTheme.java index 00ad4522..17a88a20 100644 --- a/vtm/src/org/oscim/theme/StreamRenderTheme.java +++ b/vtm/src/org/oscim/theme/StreamRenderTheme.java @@ -1,6 +1,7 @@ /* * Copyright 2016-2021 devemux86 * Copyright 2017 Andrey Novikov + * Copyright 2021 eddiemuc * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -31,12 +32,14 @@ public class StreamRenderTheme implements ThemeFile { private boolean mMapsforgeTheme; private XmlRenderThemeMenuCallback mMenuCallback; private final String mRelativePathPrefix; + private XmlThemeResourceProvider mResourceProvider; /** * @param relativePathPrefix the prefix for all relative resource paths. * @param inputStream an input stream containing valid render theme XML data. + * @throws ThemeException if an error occurs while reading the render theme XML. */ - public StreamRenderTheme(String relativePathPrefix, InputStream inputStream) { + public StreamRenderTheme(String relativePathPrefix, InputStream inputStream) throws ThemeException { this(relativePathPrefix, inputStream, null); } @@ -44,8 +47,9 @@ public class StreamRenderTheme implements ThemeFile { * @param relativePathPrefix the prefix for all relative resource paths. * @param inputStream an input stream containing valid render theme XML data. * @param menuCallback the interface callback to create a settings menu on the fly. + * @throws ThemeException if an error occurs while reading the render theme XML. */ - public StreamRenderTheme(String relativePathPrefix, InputStream inputStream, XmlRenderThemeMenuCallback menuCallback) { + public StreamRenderTheme(String relativePathPrefix, InputStream inputStream, XmlRenderThemeMenuCallback menuCallback) throws ThemeException { mRelativePathPrefix = relativePathPrefix; mInputStream = inputStream; mMenuCallback = menuCallback; @@ -83,6 +87,11 @@ public class StreamRenderTheme implements ThemeFile { return mInputStream; } + @Override + public XmlThemeResourceProvider getResourceProvider() { + return mResourceProvider; + } + @Override public boolean isMapsforgeTheme() { return mMapsforgeTheme; @@ -97,4 +106,9 @@ public class StreamRenderTheme implements ThemeFile { public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) { mMenuCallback = menuCallback; } + + @Override + public void setResourceProvider(XmlThemeResourceProvider resourceProvider) { + mResourceProvider = resourceProvider; + } } diff --git a/vtm/src/org/oscim/theme/ThemeFile.java b/vtm/src/org/oscim/theme/ThemeFile.java index 077fc1d5..dc47b2de 100644 --- a/vtm/src/org/oscim/theme/ThemeFile.java +++ b/vtm/src/org/oscim/theme/ThemeFile.java @@ -3,6 +3,7 @@ * Copyright 2013 Hannes Janetzek * Copyright 2016-2021 devemux86 * Copyright 2017 Andrey Novikov + * Copyright 2021 eddiemuc * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -44,6 +45,11 @@ public interface ThemeFile extends Serializable { */ InputStream getRenderThemeAsStream() throws ThemeException; + /** + * @return a custom provider to retrieve resources internally referenced by "src" attribute (e.g. images, icons). + */ + XmlThemeResourceProvider getResourceProvider(); + /** * Tells ThemeLoader if theme file is in Mapsforge format * @@ -60,4 +66,9 @@ public interface ThemeFile extends Serializable { * @param menuCallback the interface callback to create a settings menu on the fly. */ void setMenuCallback(XmlRenderThemeMenuCallback menuCallback); + + /** + * @param resourceProvider a custom provider to retrieve resources internally referenced by "src" attribute (e.g. images, icons). + */ + void setResourceProvider(XmlThemeResourceProvider resourceProvider); } diff --git a/vtm/src/org/oscim/theme/XmlThemeBuilder.java b/vtm/src/org/oscim/theme/XmlThemeBuilder.java index 9e2743b2..ee6712c3 100644 --- a/vtm/src/org/oscim/theme/XmlThemeBuilder.java +++ b/vtm/src/org/oscim/theme/XmlThemeBuilder.java @@ -8,6 +8,7 @@ * Copyright 2018-2019 Gustl22 * Copyright 2018 Izumi Kawashima * Copyright 2019 Murray Hughes + * Copyright 2021 eddiemuc * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -664,7 +665,7 @@ public class XmlThemeBuilder { } else { if (src != null) { float symbolScale = Parameters.SYMBOL_SCALING == Parameters.SymbolScaling.ALL ? CanvasAdapter.symbolScale : 1; - b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * symbolScale)); + b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, mTheme.getResourceProvider(), b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * symbolScale)); } if (b.texture != null && hasSymbol) { @@ -777,7 +778,7 @@ public class XmlThemeBuilder { } if (src != null) - b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, b.symbolWidth, b.symbolHeight, b.symbolPercent); + b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, mTheme.getResourceProvider(), b.symbolWidth, b.symbolHeight, b.symbolPercent); return b.build(); } @@ -1103,7 +1104,7 @@ public class XmlThemeBuilder { String lowValue = symbol.toLowerCase(Locale.ENGLISH); if (lowValue.endsWith(".png") || lowValue.endsWith(".svg")) { try { - b.bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), symbol, b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * CanvasAdapter.symbolScale)); + b.bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), symbol, mTheme.getResourceProvider(), b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * CanvasAdapter.symbolScale)); } catch (Exception e) { log.error("{}: {}", symbol, e.getMessage()); } @@ -1257,7 +1258,7 @@ public class XmlThemeBuilder { symbolScale = CanvasAdapter.symbolScale; break; } - Bitmap bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), b.src, b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * symbolScale)); + Bitmap bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), b.src, mTheme.getResourceProvider(), b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * symbolScale)); if (bitmap != null) return buildSymbol(b, b.src, bitmap); } catch (Exception e) { diff --git a/vtm/src/org/oscim/theme/XmlThemeResourceProvider.java b/vtm/src/org/oscim/theme/XmlThemeResourceProvider.java new file mode 100644 index 00000000..7596e772 --- /dev/null +++ b/vtm/src/org/oscim/theme/XmlThemeResourceProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 eddiemuc + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.theme; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Interface for a provider of resources referenced inside XML themes. + */ +public interface XmlThemeResourceProvider { + + /** + * @param relativePath a relative path to use as a base for search in the resource provuider + * @param source a source string parsed out of an XML render theme "src" attribute. + * @return an InputStream to read the resource data from. + * @throws IOException if the resource cannot be found or an access error occurred. + */ + InputStream createInputStream(String relativePath, String source) throws IOException; +} diff --git a/vtm/src/org/oscim/theme/ZipRenderTheme.java b/vtm/src/org/oscim/theme/ZipRenderTheme.java new file mode 100644 index 00000000..ed582e64 --- /dev/null +++ b/vtm/src/org/oscim/theme/ZipRenderTheme.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 devemux86 + * Copyright 2021 eddiemuc + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.theme; + +import org.oscim.theme.IRenderTheme.ThemeException; +import org.oscim.utils.Utils; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A ZipRenderTheme allows for customizing the rendering style of the map + * via an XML from an archive. + */ +public class ZipRenderTheme implements ThemeFile { + private static final long serialVersionUID = 1L; + + private boolean mMapsforgeTheme; + private XmlRenderThemeMenuCallback mMenuCallback; + private final String mRelativePathPrefix; + private XmlThemeResourceProvider mResourceProvider; + protected final String mXmlTheme; + + /** + * @param xmlTheme the XML theme path in the archive. + * @param resourceProvider the custom provider to retrieve resources internally referenced by "src" attribute (e.g. images, icons). + * @throws ThemeException if an error occurs while reading the render theme XML. + */ + public ZipRenderTheme(String xmlTheme, XmlThemeResourceProvider resourceProvider) throws ThemeException { + this(xmlTheme, resourceProvider, null); + } + + /** + * @param xmlTheme the XML theme path in the archive. + * @param resourceProvider the custom provider to retrieve resources internally referenced by "src" attribute (e.g. images, icons). + * @param menuCallback the interface callback to create a settings menu on the fly. + * @throws ThemeException if an error occurs while reading the render theme XML. + */ + public ZipRenderTheme(String xmlTheme, XmlThemeResourceProvider resourceProvider, XmlRenderThemeMenuCallback menuCallback) throws ThemeException { + mXmlTheme = xmlTheme; + mResourceProvider = resourceProvider; + mMenuCallback = menuCallback; + + mRelativePathPrefix = xmlTheme.substring(0, xmlTheme.lastIndexOf("/") + 1); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof ZipRenderTheme)) { + return false; + } + ZipRenderTheme other = (ZipRenderTheme) obj; + if (getRenderThemeAsStream() != other.getRenderThemeAsStream()) { + return false; + } + if (!Utils.equals(mRelativePathPrefix, other.mRelativePathPrefix)) { + return false; + } + return true; + } + + @Override + public XmlRenderThemeMenuCallback getMenuCallback() { + return mMenuCallback; + } + + @Override + public String getRelativePathPrefix() { + return mRelativePathPrefix; + } + + @Override + public InputStream getRenderThemeAsStream() throws ThemeException { + try { + return mResourceProvider.createInputStream(mRelativePathPrefix, mXmlTheme.substring(mXmlTheme.lastIndexOf("/") + 1)); + } catch (IOException e) { + throw new ThemeException(e.getMessage()); + } + } + + @Override + public XmlThemeResourceProvider getResourceProvider() { + return mResourceProvider; + } + + @Override + public boolean isMapsforgeTheme() { + return mMapsforgeTheme; + } + + @Override + public void setMapsforgeTheme(boolean mapsforgeTheme) { + mMapsforgeTheme = mapsforgeTheme; + } + + @Override + public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) { + mMenuCallback = menuCallback; + } + + @Override + public void setResourceProvider(XmlThemeResourceProvider resourceProvider) { + mResourceProvider = resourceProvider; + } +} diff --git a/vtm/src/org/oscim/theme/ZipXmlThemeResourceProvider.java b/vtm/src/org/oscim/theme/ZipXmlThemeResourceProvider.java new file mode 100644 index 00000000..94dddbda --- /dev/null +++ b/vtm/src/org/oscim/theme/ZipXmlThemeResourceProvider.java @@ -0,0 +1,124 @@ +/* + * Copyright 2021 eddiemuc + * Copyright 2021 devemux86 + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.theme; + +import org.oscim.backend.CanvasAdapter; +import org.oscim.utils.IOUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Resource provider reading resource files out of a zip input stream. + *

+ * Resources are cached. + */ +public class ZipXmlThemeResourceProvider implements XmlThemeResourceProvider { + + private final Map files = new HashMap<>(); + private final List xmlThemes = new ArrayList<>(); + + /** + * @param zipInputStream zip stream to read resources from + * @throws IOException if a problem occurs reading the stream + */ + public ZipXmlThemeResourceProvider(ZipInputStream zipInputStream) throws IOException { + this(zipInputStream, Integer.MAX_VALUE); + } + + /** + * @param zipInputStream zip stream to read resources from + * @param maxResourceSizeToCache only resources in the zip stream with a maximum size of this parameter (in bytes) are cached and provided + * @throws IOException if a problem occurs reading the stream + */ + public ZipXmlThemeResourceProvider(ZipInputStream zipInputStream, int maxResourceSizeToCache) throws IOException { + if (zipInputStream == null) { + return; + } + + try { + ZipEntry zipEntry; + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + if (zipEntry.isDirectory() || zipEntry.getSize() > maxResourceSizeToCache) { + continue; + } + byte[] entry = streamToBytes(zipInputStream, (int) zipEntry.getSize()); + String fileName = zipEntry.getName(); + if (fileName.startsWith("/")) { + fileName = fileName.substring(1); + } + files.put(fileName, entry); + if (fileName.toLowerCase(Locale.ROOT).endsWith(".xml")) { + xmlThemes.add(fileName); + } + } + } finally { + IOUtils.closeQuietly(zipInputStream); + } + } + + @Override + public InputStream createInputStream(String relativePath, String source) { + String sourceKey = source; + if (sourceKey.startsWith(CanvasAdapter.PREFIX_FILE)) { + sourceKey = sourceKey.substring(CanvasAdapter.PREFIX_FILE.length()); + } + if (sourceKey.startsWith("/")) { + sourceKey = sourceKey.substring(1); + } + if (relativePath != null) { + if (relativePath.startsWith("/")) { + relativePath = relativePath.substring(1); + } + if (relativePath.endsWith("/")) { + relativePath = relativePath.substring(0, relativePath.length() - 1); + } + sourceKey = relativePath.isEmpty() ? sourceKey : relativePath + "/" + sourceKey; + } + if (files.containsKey(sourceKey)) { + return new ByteArrayInputStream(files.get(sourceKey)); + } + return null; + } + + /** + * @return the number of files in the archive. + */ + public int getCount() { + return files.size(); + } + + /** + * @return the XML theme paths in the archive. + */ + public List getXmlThemes() { + return xmlThemes; + } + + private static byte[] streamToBytes(InputStream in, int size) throws IOException { + byte[] bytes = new byte[size]; + int count, offset = 0; + while ((count = in.read(bytes, offset, size)) > 0) { + size -= count; + offset += count; + } + return bytes; + } +} diff --git a/vtm/src/org/oscim/utils/Utils.java b/vtm/src/org/oscim/utils/Utils.java index 62c6991e..b8344591 100644 --- a/vtm/src/org/oscim/utils/Utils.java +++ b/vtm/src/org/oscim/utils/Utils.java @@ -19,6 +19,7 @@ import org.oscim.backend.CanvasAdapter; import org.oscim.backend.canvas.Bitmap; import org.oscim.backend.canvas.Canvas; import org.oscim.renderer.bucket.TextureItem; +import org.oscim.theme.XmlThemeResourceProvider; import org.oscim.utils.math.MathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,12 +38,12 @@ public final class Utils { /** * Load a texture from a specified location and optional dimensions. */ - public static TextureItem loadTexture(String relativePathPrefix, String src, int width, int height, int percent) { + public static TextureItem loadTexture(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) { if (src == null || src.length() == 0) return null; try { - Bitmap bitmap = CanvasAdapter.getBitmapAsset(relativePathPrefix, src, width, height, percent); + Bitmap bitmap = CanvasAdapter.getBitmapAsset(relativePathPrefix, src, resourceProvider, width, height, percent); if (bitmap != null) { log.debug("loading {}", src); return new TextureItem(potBitmap(bitmap), true);