diff --git a/README.md b/README.md index 3e6cf5bed..3bb928b43 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,14 @@ If you have any questions or problems, don't hesitate to ask the [forum](https:/ - Java map library - OpenGL vector-tile rendering - Themeable vector layers ([render themes](docs/Rendertheme.md)) +- Hillshading from HGT digital elevation model data - Support for multiple tile sources: - - OpenScienceMap vector tiles - Mapsforge vector maps - - MBTiles vector & raster maps - - Mapbox vector tiles (e.g. Mapilion, Mapzen, Nextzen, OpenMapTiles) - - GeoJSON vector tiles (e.g. Mapzen, Nextzen) - - Raster tiles: any quadtree-scheme tiles as texture + - MBTiles vector & raster + - Mapbox vector tiles + - GeoJSON vector tiles + - OpenScienceMap vector tiles + - Raster tiles - Backends: - Android ([example](vtm-android-example)) - iOS (libGDX/RoboVM, [instructions](docs/ios.md)) @@ -34,13 +35,14 @@ If you have any questions or problems, don't hesitate to ask the [forum](https:/ ### Projects - **vtm** core library +- **vtm-hillshading** hillshading +- **vtm-jts** overlays +- **vtm-http** online tiles +- **vtm-mvt** MBTiles - **vtm-android** Android backend - **vtm-android-example** Android examples - **vtm-gdx** common libGDX backend -- **vtm-android-gdx** Android libGDX backend - **vtm-desktop** Desktop libGDX backend -- **vtm-desktop-lwjgl** Desktop LWJGL backend -- **vtm-desktop-lwjgl3** Desktop LWJGL 3 backend - **vtm-playground** Desktop examples - **vtm-ios** iOS libGDX backend - **vtm-ios-example** iOS examples diff --git a/build.gradle b/build.gradle index 1b38a71a3..2b65be2a7 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ allprojects { group = 'org.mapsforge' version = 'master-SNAPSHOT' + ext.mapsforgeVersion = "0.24.1" ext.gdxVersion = "1.11.0" ext.gwtVersion = "2.8.2" diff --git a/docs/Changelog.md b/docs/Changelog.md index fa59d8d9a..b81e531b9 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -2,6 +2,7 @@ ## Next version +- Hillshading from HGT digital elevation model data [#1189](https://github.com/mapsforge/vtm/pull/1189) - Motorider map theme improvements [#1183](https://github.com/mapsforge/vtm/issues/1183) - Rename `MapDatabase` to `MapFile` [#1184](https://github.com/mapsforge/vtm/pull/1184) - Rename `MultiMapDatabase` to `MultiMapFile` diff --git a/docs/Integration.md b/docs/Integration.md index bd9db9fdc..d501a662f 100644 --- a/docs/Integration.md +++ b/docs/Integration.md @@ -26,41 +26,18 @@ implementation '[PACKAGE]:vtm:[CURRENT-VERSION]' implementation '[PACKAGE]:vtm-themes:[CURRENT-VERSION]' ``` -### Android +## Android ```groovy -runtimeOnly '[PACKAGE]:vtm-android:[CURRENT-VERSION]:natives-armeabi-v7a' -runtimeOnly '[PACKAGE]:vtm-android:[CURRENT-VERSION]:natives-arm64-v8a' -runtimeOnly '[PACKAGE]:vtm-android:[CURRENT-VERSION]:natives-x86' -runtimeOnly '[PACKAGE]:vtm-android:[CURRENT-VERSION]:natives-x86_64' implementation '[PACKAGE]:vtm-android:[CURRENT-VERSION]' -implementation 'com.caverock:androidsvg:1.4' -``` - -### Android (libGDX) - -```groovy runtimeOnly '[PACKAGE]:vtm-android:[CURRENT-VERSION]:natives-armeabi-v7a' runtimeOnly '[PACKAGE]:vtm-android:[CURRENT-VERSION]:natives-arm64-v8a' runtimeOnly '[PACKAGE]:vtm-android:[CURRENT-VERSION]:natives-x86' runtimeOnly '[PACKAGE]:vtm-android:[CURRENT-VERSION]:natives-x86_64' -implementation '[PACKAGE]:vtm-android:[CURRENT-VERSION]' -implementation '[PACKAGE]:vtm-gdx:[CURRENT-VERSION]' -runtimeOnly '[PACKAGE]:vtm-android-gdx:[CURRENT-VERSION]:natives-armeabi-v7a' -runtimeOnly '[PACKAGE]:vtm-android-gdx:[CURRENT-VERSION]:natives-arm64-v8a' -runtimeOnly '[PACKAGE]:vtm-android-gdx:[CURRENT-VERSION]:natives-x86' -runtimeOnly '[PACKAGE]:vtm-android-gdx:[CURRENT-VERSION]:natives-x86_64' -implementation '[PACKAGE]:vtm-android-gdx:[CURRENT-VERSION]' -implementation 'com.badlogicgames.gdx:gdx:1.11.0' -implementation 'com.badlogicgames.gdx:gdx-backend-android:1.11.0' implementation 'com.caverock:androidsvg:1.4' ``` -### iOS - -Detailed iOS instructions can be found [here](ios.md). - -### Desktop +## Desktop ```groovy implementation '[PACKAGE]:vtm-gdx:[CURRENT-VERSION]' @@ -74,7 +51,7 @@ implementation 'guru.nidi.com.kitfox:svgSalamander:1.1.3' implementation 'net.sf.kxml:kxml2:2.3.0' ``` -#### Desktop (LWJGL 2) +### LWJGL 2 ```groovy implementation '[PACKAGE]:vtm-desktop-lwjgl:[CURRENT-VERSION]' @@ -85,7 +62,7 @@ runtimeOnly 'org.lwjgl.lwjgl:lwjgl-platform:2.9.3:natives-osx' runtimeOnly 'org.lwjgl.lwjgl:lwjgl-platform:2.9.3:natives-windows' ``` -#### Desktop (LWJGL 3) +### LWJGL 3 ```groovy implementation '[PACKAGE]:vtm-desktop-lwjgl3:[CURRENT-VERSION]' @@ -96,7 +73,18 @@ runtimeOnly 'org.lwjgl:lwjgl:3.3.1:natives-macos' runtimeOnly 'org.lwjgl:lwjgl:3.3.1:natives-windows' ``` -### JTS overlays +## Features + +### Hillshading + +```groovy +implementation '[PACKAGE]:vtm-hillshading:[CURRENT-VERSION]' +implementation 'org.mapsforge:mapsforge-core:0.24.1' +implementation 'org.mapsforge:mapsforge-map:0.24.1' +implementation 'org.mapsforge:mapsforge-map-android:0.24.1' +``` + +### Overlays ```groovy implementation '[PACKAGE]:vtm-jts:[CURRENT-VERSION]' diff --git a/settings.gradle b/settings.gradle index c238e2d47..ad9032f89 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,6 +26,7 @@ include ':vtm-desktop-lwjgl3' include ':vtm-extras' include ':vtm-gdx' include ':vtm-gdx-poi3d' +include ':vtm-hillshading' include ':vtm-http' //include ':vtm-ios' //include ':vtm-ios-example' diff --git a/vtm-android-example/AndroidManifest.xml b/vtm-android-example/AndroidManifest.xml index f978d216f..0c884f5e1 100644 --- a/vtm-android-example/AndroidManifest.xml +++ b/vtm-android-example/AndroidManifest.xml @@ -51,6 +51,9 @@ + diff --git a/vtm-android-example/build.gradle b/vtm-android-example/build.gradle index cd96d3d91..9053cc34c 100644 --- a/vtm-android-example/build.gradle +++ b/vtm-android-example/build.gradle @@ -4,6 +4,7 @@ dependencies { implementation project(':vtm-android') implementation project(':vtm-android-mvt') implementation project(':vtm-extras') + implementation project(':vtm-hillshading') implementation project(':vtm-http') //implementation project(':vtm-jeo') implementation project(':vtm-json') @@ -15,7 +16,8 @@ dependencies { implementation project(':vtm-gdx') implementation project(':vtm-gdx-poi3d') - implementation 'org.mapsforge:mapsforge-poi-android:0.24.0' + implementation "org.mapsforge:mapsforge-map-android:$mapsforgeVersion" + implementation "org.mapsforge:mapsforge-poi-android:$mapsforgeVersion" } android { diff --git a/vtm-android-example/src/org/oscim/android/test/HillshadingActivity.java b/vtm-android-example/src/org/oscim/android/test/HillshadingActivity.java new file mode 100644 index 000000000..55d02100d --- /dev/null +++ b/vtm-android-example/src/org/oscim/android/test/HillshadingActivity.java @@ -0,0 +1,149 @@ +/* + * Copyright 2025 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.android.test; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Toast; +import org.mapsforge.map.android.graphics.AndroidGraphicFactory; +import org.mapsforge.map.android.hills.DemFolderAndroidContent; +import org.mapsforge.map.layer.hills.AdaptiveClasyHillShading; +import org.mapsforge.map.layer.hills.DemFolder; +import org.oscim.android.cache.TileCache; +import org.oscim.backend.CanvasAdapter; +import org.oscim.core.MapPosition; +import org.oscim.core.Tile; +import org.oscim.layers.tile.bitmap.BitmapTileLayer; +import org.oscim.layers.tile.buildings.BuildingLayer; +import org.oscim.layers.tile.vector.VectorTileLayer; +import org.oscim.layers.tile.vector.labeling.LabelLayer; +import org.oscim.map.Viewport; +import org.oscim.renderer.BitmapRenderer; +import org.oscim.renderer.GLViewport; +import org.oscim.scalebar.*; +import org.oscim.theme.internal.VtmThemes; +import org.oscim.tiling.ITileCache; +import org.oscim.tiling.source.hills.HillshadingTileSource; +import org.oscim.tiling.source.mapfile.MapFileTileSource; +import org.oscim.tiling.source.mapfile.MapInfo; + +import java.io.FileInputStream; +import java.util.logging.Logger; + +/** + * Standard map view with hill shading. + */ +public class HillshadingActivity extends MapActivity { + + private static final Logger log = Logger.getLogger(HillshadingActivity.class.getName()); + + private static final boolean USE_CACHE = false; + + private static final int SELECT_MAP_FILE = 0; + private static final int SELECT_DEM_DIR = 1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Select map file + Toast.makeText(this, "Select map file", Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + startActivityForResult(intent, SELECT_MAP_FILE); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + + if (requestCode == SELECT_MAP_FILE) { + if (resultCode != Activity.RESULT_OK || data == null) { + finish(); + return; + } + + try { + Uri uri = data.getData(); + + MapFileTileSource tileSource = new MapFileTileSource(); + //tileSource.setPreferredLanguage("en"); + FileInputStream fis = (FileInputStream) getContentResolver().openInputStream(uri); + tileSource.setMapFileInputStream(fis); + + VectorTileLayer tileLayer = mMap.setBaseMap(tileSource); + mMap.setTheme(VtmThemes.MOTORIDER); + + mMap.layers().add(new BuildingLayer(mMap, tileLayer)); + mMap.layers().add(new LabelLayer(mMap, tileLayer)); + + 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(); + } + + // Select DEM folder + Toast.makeText(this, "Select DEM folder", Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + ); + startActivityForResult(intent, SELECT_DEM_DIR); + } catch (Exception e) { + log.severe(e.toString()); + finish(); + } + } else if (requestCode == SELECT_DEM_DIR) { + if (resultCode != Activity.RESULT_OK || data == null) + return; + + Uri uri = data.getData(); + + DemFolder demFolder = new DemFolderAndroidContent(uri, this, getContentResolver()); + final AdaptiveClasyHillShading algorithm = new AdaptiveClasyHillShading() + // You can make additional behavior adjustments + .setAdaptiveZoomEnabled(true) + // .setZoomMinOverride(0) + // .setZoomMaxOverride(17) + .setCustomQualityScale(1); + HillshadingTileSource hillshadingTileSource = new HillshadingTileSource(Viewport.MIN_ZOOM_LEVEL, Viewport.MAX_ZOOM_LEVEL, demFolder, algorithm, 128, Color.BLACK, AndroidGraphicFactory.INSTANCE); + if (USE_CACHE) { + ITileCache tileCache = new TileCache(this, getExternalCacheDir().getAbsolutePath(), "hillshading"); + hillshadingTileSource.setCache(tileCache); + } + mMap.layers().add(new BitmapTileLayer(mMap, hillshadingTileSource, 150)); + mMap.clearMap(); + } + } +} 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 1d06f488b..f973cbd81 100644 --- a/vtm-android-example/src/org/oscim/android/test/Samples.java +++ b/vtm-android-example/src/org/oscim/android/test/Samples.java @@ -88,6 +88,7 @@ protected void onCreate(Bundle savedInstanceState) { linearLayout.addView(createLabel(null)); linearLayout.addView(createButton(LocationTextureActivity.class)); + linearLayout.addView(createButton(HillshadingActivity.class)); linearLayout.addView(createButton(PoiSearchActivity.class)); linearLayout.addView(createButton(MapsforgeStyleActivity.class)); linearLayout.addView(createButton(MapsforgeS3DBActivity.class)); diff --git a/vtm-hillshading/build.gradle b/vtm-hillshading/build.gradle new file mode 100644 index 000000000..f90293933 --- /dev/null +++ b/vtm-hillshading/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'java-library' +apply plugin: 'maven-publish' + +dependencies { + api project(':vtm') + api "org.mapsforge:mapsforge-map:$mapsforgeVersion" +} + +sourceSets { + main.java.srcDirs = ['src'] +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + } + } +} + +if (project.hasProperty("SONATYPE_USERNAME")) { + afterEvaluate { + project.apply from: "${rootProject.projectDir}/deploy.gradle" + } +} diff --git a/vtm-hillshading/src/org/oscim/tiling/source/hills/HillshadingTileDataSource.java b/vtm-hillshading/src/org/oscim/tiling/source/hills/HillshadingTileDataSource.java new file mode 100644 index 000000000..ed66652b9 --- /dev/null +++ b/vtm-hillshading/src/org/oscim/tiling/source/hills/HillshadingTileDataSource.java @@ -0,0 +1,428 @@ +/* + * Copyright 2017 usrusr + * Copyright 2017 oruxman + * Copyright 2024 Sublimis + * Copyright 2024 jhotadhari + * Copyright 2025 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.tiling.source.hills; + +import org.mapsforge.core.graphics.HillshadingBitmap; +import org.mapsforge.core.model.Rectangle; +import org.mapsforge.core.util.MercatorProjection; +import org.mapsforge.map.layer.hills.*; +import org.mapsforge.map.layer.renderer.HillshadingContainer; +import org.oscim.backend.CanvasAdapter; +import org.oscim.backend.canvas.Bitmap; +import org.oscim.backend.canvas.Canvas; +import org.oscim.core.Point; +import org.oscim.core.Tile; +import org.oscim.layers.tile.MapTile; +import org.oscim.tiling.ITileCache; +import org.oscim.tiling.ITileDataSink; +import org.oscim.tiling.ITileDataSource; +import org.oscim.tiling.QueryResult; +import org.oscim.tiling.source.ITileDecoder; +import org.oscim.utils.IOUtils; + +import java.io.*; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; + +/** + * Contains code from {@link org.mapsforge.map.rendertheme.renderinstruction.Hillshading}. + */ +public class HillshadingTileDataSource implements ITileDataSource { + + private static final Logger log = Logger.getLogger(HillshadingTileDataSource.class.getName()); + + /** + * Default name prefix for additional reading threads created and used by hill shading. A numbered suffix will be appended. + */ + private static final String ThreadPoolName = "MapsforgeHillShading"; + + private static final int ShadingLatStep = 1; + private static final int ShadingLonStep = 1; + + private final HillshadingTileSource mTileSource; + private final ITileDecoder mTileDecoder; + private final HillsRenderConfig mHillsRenderConfig; + + /** + * Static thread pool shared by all tasks. + */ + private static final AtomicReference ThreadPool = new AtomicReference<>(null); + + public HillshadingTileDataSource(HillshadingTileSource tileSource, ITileDecoder tileDecoder) { + mTileSource = tileSource; + mTileDecoder = tileDecoder; + + MemoryCachingHgtReaderTileSource shadeTileSource = new MemoryCachingHgtReaderTileSource(tileSource.mDemFolder, tileSource.mAlgorithm, tileSource.mGraphicFactory); + mHillsRenderConfig = new HillsRenderConfig(shadeTileSource); + mHillsRenderConfig.indexOnThread(); + } + + @Override + public void query(MapTile tile, ITileDataSink sink) { + // Out of zoom bounds, load nothing + byte zoomLevel = tile.zoomLevel; + if (zoomLevel > mTileSource.getZoomLevelMax() || zoomLevel < mTileSource.getZoomLevelMin()) { + sink.completed(QueryResult.SUCCESS); + return; + } + + ITileCache cache = mTileSource.tileCache; + + // Try to load from cache + if (cache != null) { + ITileCache.TileReader c = cache.getTile(tile); + if (c != null) { + InputStream is = c.getInputStream(); + try { + if (mTileDecoder.decode(tile, sink, is)) { + sink.completed(QueryResult.SUCCESS); + return; + } + } catch (IOException e) { + log.fine(tile + " Cache read: " + e); + } finally { + IOUtils.closeQuietly(is); + } + } + } + + // Create a new hillshading tile and set the sink + createTile(tile, sink, cache); + } + + @Override + public void dispose() { + } + + @Override + public void cancel() { + } + + /** + * Create new hillshading tile from hgt files and set the sink. + */ + private void createTile(MapTile tile, ITileDataSink sink, ITileCache cache) { + QueryResult res = QueryResult.FAILED; + ITileCache.TileWriter cacheWriter = null; + try { + final byte zoomLevel = tile.zoomLevel >= 0 ? tile.zoomLevel : 0; + + if (checkZoomLevelCoarse(zoomLevel, mHillsRenderConfig)) { + // Init tile bitmap to hold all the shaded parts + Bitmap tileBitmap = CanvasAdapter.newBitmap(Tile.SIZE, Tile.SIZE, 0); + Canvas canvas = CanvasAdapter.newCanvas(); + canvas.setBitmap(tileBitmap); + + final Point origin = tile.getOrigin(); + + final double maptileLeftLon = MercatorProjection.pixelXToLongitude(origin.x, tile.mapSize); + double maptileRightLon = MercatorProjection.pixelXToLongitude(origin.x + Tile.SIZE, tile.mapSize); + if (maptileRightLon < maptileLeftLon) + maptileRightLon += tile.mapSize; + + final double maptileTopLat = MercatorProjection.pixelYToLatitude(origin.y, tile.mapSize); + final double maptileBottomLat = MercatorProjection.pixelYToLatitude(origin.y + Tile.SIZE, tile.mapSize); + + final float effectiveMagnitude = Math.min(Math.max(0f, mTileSource.mMagnitude * mHillsRenderConfig.getMagnitudeScaleFactor()), 255f) / 255f; + final int effectiveColor = getEffectiveColor(mHillsRenderConfig); + + createThreadPoolsMaybe(); + + final Deque deque = new ArrayDeque<>(); + + for (int shadingLeftLon = (int) Math.floor(maptileLeftLon); shadingLeftLon <= maptileRightLon; shadingLeftLon += ShadingLonStep) { + final HillShadingUtils.SilentFutureTask code = renderLatStrip(shadingLeftLon, zoomLevel, tile, maptileBottomLat, maptileTopLat, maptileLeftLon, maptileRightLon, effectiveMagnitude, effectiveColor, canvas); + deque.addLast(code); + } + + while (!deque.isEmpty()) { + deque.pollFirst().get(); + } + + if (!tileBitmap.isValid()) { + log.fine(tile + " invalid bitmap"); + return; + } + + // Set tile bitmap to sink + sink.setTileImage(tileBitmap); + + // Write to cache + if (cache != null) { + cacheWriter = cache.writeTile(tile); + OutputStream outputStream = cacheWriter.getOutputStream(); + try { + byte[] pngBytes = tileBitmap.getPngEncodedData(); + outputStream.write(pngBytes); + } catch (IOException e) { + log.severe(e.toString()); + } finally { + IOUtils.closeQuietly(outputStream); + } + } + + res = QueryResult.SUCCESS; + } + } catch (Throwable t) { + log.severe(t.toString()); + } finally { + boolean ok = (res == QueryResult.SUCCESS); + + if (cacheWriter != null) + cacheWriter.complete(ok); + + sink.completed(res); + } + } + + private int getEffectiveColor(HillsRenderConfig hillsRenderConfig) { + int retVal = hillsRenderConfig.getColor(); + + if (retVal == 0) { + retVal = mTileSource.mColor; + } + + return retVal; + } + + private HillShadingUtils.SilentFutureTask renderLatStrip(final int shadingLeftLon, final byte zoomLevel, final MapTile tile, final double maptileBottomLat, final double maptileTopLat, final double maptileLeftLon, final double maptileRightLon, final float effectiveMagnitude, final int effectiveColor, final Canvas canvas) { + Callable runnable = new Callable() { + public Boolean call() { + try { + final int shadingRightLon = shadingLeftLon + ShadingLonStep; + final double leftX = MercatorProjection.longitudeToPixelX(shadingLeftLon, tile.mapSize); + final double rightX = MercatorProjection.longitudeToPixelX(shadingRightLon, tile.mapSize); + final double pxPerLon = (rightX - leftX) / ShadingLonStep; + + for (int shadingBottomLat = (int) Math.floor(maptileBottomLat); shadingBottomLat <= maptileTopLat; shadingBottomLat += ShadingLatStep) { + final int shadingTopLat = shadingBottomLat + ShadingLatStep; + + final double topY = MercatorProjection.latitudeToPixelY(shadingTopLat, tile.mapSize); + final double bottomY = MercatorProjection.latitudeToPixelY(shadingBottomLat, tile.mapSize); + final double pxPerLat = (bottomY - topY) / ShadingLatStep; + + HillshadingBitmap shadingTile = null; + + if (checkZoomLevelFine(zoomLevel, mHillsRenderConfig, shadingBottomLat, shadingLeftLon)) { + try { + shadingTile = mHillsRenderConfig.getShadingTile(shadingBottomLat, shadingLeftLon, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + } catch (Exception e) { + log.severe(e.toString()); + } + } + + if (shadingTile == null) { + continue; + } + + int padding = shadingTile.getPadding(); + int shadingInnerWidth = shadingTile.getWidth() - 2 * padding; + int shadingInnerHeight = shadingTile.getHeight() - 2 * padding; + + // shading tile subset if it fully fits inside map tile + double shadingSubrectTop = padding; + double shadingSubrectLeft = padding; + double shadingSubrectRight = shadingSubrectLeft + shadingInnerWidth; + double shadingSubrectBottom = shadingSubrectTop + shadingInnerHeight; + + // map tile subset if it fully fits inside shading tile + double maptileSubrectLeft = 0; + double maptileSubrectTop = 0; + double maptileSubrectRight = Tile.SIZE; + double maptileSubrectBottom = Tile.SIZE; + + final Point origin = tile.getOrigin(); + + // find the intersection between map tile and shading tile in earth coordinates and determine the pixel + + if (shadingBottomLat > maptileBottomLat) { + // Shading tile ends in map tile + maptileSubrectBottom = Math.round(MercatorProjection.latitudeToPixelY(shadingBottomLat, tile.mapSize)) - origin.y; + mergeNeighbor(shadingTile, padding, mHillsRenderConfig, HillshadingBitmap.Border.SOUTH, shadingBottomLat, shadingLeftLon, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + } else if (shadingBottomLat < maptileBottomLat) { + // Map tile ends in shading tile + shadingSubrectBottom -= shadingInnerHeight * ((maptileBottomLat - shadingBottomLat) / ShadingLatStep); + } + + if (shadingTopLat < maptileTopLat) { + // Shading tile ends in map tile + maptileSubrectTop = Math.round(MercatorProjection.latitudeToPixelY(shadingTopLat, tile.mapSize)) - origin.y; + mergeNeighbor(shadingTile, padding, mHillsRenderConfig, HillshadingBitmap.Border.NORTH, shadingBottomLat, shadingLeftLon, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + } else if (shadingTopLat > maptileTopLat) { + // Map tile ends in shading tile + shadingSubrectTop += shadingInnerHeight * ((shadingTopLat - maptileTopLat) / ShadingLatStep); + } + + if (shadingLeftLon > maptileLeftLon) { + // Shading tile ends in map tile + maptileSubrectLeft = Math.round(MercatorProjection.longitudeToPixelX(shadingLeftLon, tile.mapSize)) - origin.x; + mergeNeighbor(shadingTile, padding, mHillsRenderConfig, HillshadingBitmap.Border.WEST, shadingBottomLat, shadingLeftLon, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + } else if (shadingLeftLon < maptileLeftLon) { + // Map tile ends in shading tile + shadingSubrectLeft += shadingInnerWidth * ((maptileLeftLon - shadingLeftLon) / ShadingLonStep); + } + + if (shadingRightLon < maptileRightLon) { + // Shading tile ends in map tile + maptileSubrectRight = Math.round(MercatorProjection.longitudeToPixelX(shadingRightLon, tile.mapSize)) - origin.x; + mergeNeighbor(shadingTile, padding, mHillsRenderConfig, HillshadingBitmap.Border.EAST, shadingBottomLat, shadingLeftLon, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + } else if (shadingRightLon > maptileRightLon) { + // Map tile ends in shading tile + shadingSubrectRight -= shadingInnerWidth * ((shadingRightLon - maptileRightLon) / ShadingLonStep); + } + + final Rectangle hillsRect = new Rectangle(shadingSubrectLeft, shadingSubrectTop, shadingSubrectRight, shadingSubrectBottom); + final Rectangle maptileRect = new Rectangle(maptileSubrectLeft, maptileSubrectTop, maptileSubrectRight, maptileSubrectBottom); + final HillshadingContainer hillShape = new HillshadingContainer(shadingTile, effectiveMagnitude, effectiveColor, hillsRect, maptileRect); + + // Render ShapeContainer to a Mapsforge bitmap + org.mapsforge.core.graphics.Bitmap mapsforgeBitmap = mTileSource.mGraphicFactory.createBitmap(Tile.SIZE, Tile.SIZE, true); + org.mapsforge.core.graphics.Canvas mapsforgeCanvas = mTileSource.mGraphicFactory.createCanvas(); + mapsforgeCanvas.setBitmap(mapsforgeBitmap); + mapsforgeCanvas.shadeBitmap(hillShape.bitmap, hillShape.hillsRect, hillShape.tileRect, hillShape.magnitude, hillShape.color, true); + + // Convert Mapsforge bitmap to VTM bitmap + Bitmap bitmap = bitmapMapsforgeToVtm(mapsforgeBitmap); + + // Draw shaded bitmap on the tile bitmap + canvas.drawBitmap(bitmap, 0, 0); + } + } catch (Throwable t) { + log.severe(t.toString()); + } + return true; + } + }; + + final HillShadingUtils.SilentFutureTask code = new HillShadingUtils.SilentFutureTask(runnable); + + postToThreadPoolOrRun(code); + + return code; + } + + private static void postToThreadPoolOrRun(final Runnable code) { + final HillShadingUtils.HillShadingThreadPool threadPool = ThreadPool.get(); + + if (threadPool != null) { + threadPool.executeOrRun(code); + } + } + + private static void createThreadPoolsMaybe() { + final AtomicReference threadPoolReference = ThreadPool; + + if (threadPoolReference.get() == null) { + synchronized (threadPoolReference) { + if (threadPoolReference.get() == null) { + threadPoolReference.set(createThreadPool()); + } + } + } + } + + private static HillShadingUtils.HillShadingThreadPool createThreadPool() { + final int threadCount = AThreadedHillShading.ReadingThreadsCountDefault; + final int queueSize = Integer.MAX_VALUE; + return new HillShadingUtils.HillShadingThreadPool(threadCount, threadCount, queueSize, 5, ThreadPoolName).start(); + } + + private boolean checkZoomLevelCoarse(int zoomLevel, HillsRenderConfig hillsRenderConfig) { + boolean retVal = true; + + if (hillsRenderConfig.isAdaptiveZoomEnabled()) { + // Pass, wide zoom range algorithms will later use finer zoom level support check + } else { + if (zoomLevel > mTileSource.getZoomLevelMax()) { + retVal = false; + } else if (zoomLevel < mTileSource.getZoomLevelMin()) { + retVal = false; + } + } + + return retVal; + } + + private boolean checkZoomLevelFine(int zoomLevel, HillsRenderConfig hillsRenderConfig, int shadingBottomLat, int shadingLeftLon) { + boolean retVal = true; + + if (hillsRenderConfig.isAdaptiveZoomEnabled()) { + retVal = hillsRenderConfig.isZoomLevelSupported(zoomLevel, shadingBottomLat, shadingLeftLon); + } + + return retVal; + } + + private HillshadingBitmap getNeighbor(HillsRenderConfig hillsRenderConfig, HillshadingBitmap.Border border, int shadingBottomLat, int shadingLeftLon, int zoomLevel, double pxPerLat, double pxPerLon, int effectiveColor) { + + HillshadingBitmap neighbor = null; + try { + switch (border) { + case NORTH: + neighbor = hillsRenderConfig.getShadingTile(shadingBottomLat + ShadingLatStep, shadingLeftLon, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + break; + case SOUTH: + neighbor = hillsRenderConfig.getShadingTile(shadingBottomLat - ShadingLatStep, shadingLeftLon, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + break; + case EAST: + neighbor = hillsRenderConfig.getShadingTile(shadingBottomLat, shadingLeftLon + ShadingLonStep, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + break; + case WEST: + neighbor = hillsRenderConfig.getShadingTile(shadingBottomLat, shadingLeftLon - ShadingLonStep, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + break; + } + } catch (Exception e) { + log.severe(e.toString()); + } + + return neighbor; + } + + private void mergePaddingOnBitmap(HillshadingBitmap center, HillshadingBitmap neighbor, HillshadingBitmap.Border border, int padding) { + if (neighbor != null && padding > 0) { + final org.mapsforge.core.graphics.Canvas copyCanvas = mTileSource.mGraphicFactory.createCanvas(); + + HgtCache.mergeSameSized(center, neighbor, border, padding, copyCanvas); + } + } + + private void mergeNeighbor(HillshadingBitmap monoBitmap, int padding, HillsRenderConfig hillsRenderConfig, HillshadingBitmap.Border border, int shadingBottomLat, int shadingLeftLon, int zoomLevel, double pxPerLat, double pxPerLon, int effectiveColor) { + if (monoBitmap != null && padding > 0) { + final HillshadingBitmap neighbor = getNeighbor(hillsRenderConfig, border, shadingBottomLat, shadingLeftLon, zoomLevel, pxPerLat, pxPerLon, effectiveColor); + mergePaddingOnBitmap(monoBitmap, neighbor, border, padding); + } + } + + /** + * Converts a Mapsforge bitmap to a VTM bitmap. + */ + private static Bitmap bitmapMapsforgeToVtm(org.mapsforge.core.graphics.Bitmap mapsforgeBitmap) throws IOException { + ByteArrayOutputStream outputStream = null; + try { + outputStream = new ByteArrayOutputStream(); + mapsforgeBitmap.compress(outputStream); + return CanvasAdapter.decodeBitmap(new ByteArrayInputStream(outputStream.toByteArray())); + } finally { + IOUtils.closeQuietly(outputStream); + } + } +} diff --git a/vtm-hillshading/src/org/oscim/tiling/source/hills/HillshadingTileSource.java b/vtm-hillshading/src/org/oscim/tiling/source/hills/HillshadingTileSource.java new file mode 100644 index 000000000..5479d42ba --- /dev/null +++ b/vtm-hillshading/src/org/oscim/tiling/source/hills/HillshadingTileSource.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 jhotadhari + * Copyright 2025 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.tiling.source.hills; + +import org.mapsforge.core.graphics.GraphicFactory; +import org.mapsforge.map.layer.hills.AdaptiveClasyHillShading; +import org.mapsforge.map.layer.hills.DemFolder; +import org.mapsforge.map.layer.hills.ShadingAlgorithm; +import org.oscim.backend.CanvasAdapter; +import org.oscim.backend.canvas.Bitmap; +import org.oscim.backend.canvas.Color; +import org.oscim.core.Tile; +import org.oscim.map.Viewport; +import org.oscim.tiling.ITileDataSink; +import org.oscim.tiling.ITileDataSource; +import org.oscim.tiling.TileSource; +import org.oscim.tiling.source.ITileDecoder; + +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Logger; + +public class HillshadingTileSource extends TileSource { + + private static final Logger log = Logger.getLogger(HillshadingTileSource.class.getName()); + + final DemFolder mDemFolder; + final ShadingAlgorithm mAlgorithm; + final int mMagnitude; + final int mColor; + final GraphicFactory mGraphicFactory; + + public HillshadingTileSource(DemFolder demFolder, GraphicFactory graphicFactory) { + this(Viewport.MIN_ZOOM_LEVEL, Viewport.MAX_ZOOM_LEVEL, demFolder, new AdaptiveClasyHillShading(), 128, Color.BLACK, graphicFactory); + } + + public HillshadingTileSource(int zoomMin, int zoomMax, DemFolder demFolder, ShadingAlgorithm algorithm, int magnitude, int color, GraphicFactory graphicFactory) { + super(zoomMin, zoomMax); + mDemFolder = demFolder; + mAlgorithm = algorithm; + mMagnitude = magnitude; + mColor = color; + mGraphicFactory = graphicFactory; + } + + @Override + public ITileDataSource getDataSource() { + return new HillshadingTileDataSource(this, new TileDecoder()); + } + + @Override + public OpenResult open() { + return OpenResult.SUCCESS; + } + + @Override + public void close() { + getDataSource().dispose(); + } + + public static class TileDecoder implements ITileDecoder { + + @Override + public boolean decode(Tile tile, ITileDataSink sink, InputStream is) + throws IOException { + + Bitmap bitmap = CanvasAdapter.decodeBitmap(is); + if (!bitmap.isValid()) { + log.fine(tile + " invalid bitmap"); + return false; + } + sink.setTileImage(bitmap); + + return true; + } + } +} diff --git a/vtm-playground/build.gradle b/vtm-playground/build.gradle index 884704e8d..183cbf1fd 100644 --- a/vtm-playground/build.gradle +++ b/vtm-playground/build.gradle @@ -7,6 +7,7 @@ dependencies { } implementation project(':vtm-extras') implementation project(':vtm-gdx-poi3d') + implementation project(':vtm-hillshading') implementation project(':vtm-http') //implementation project(':vtm-jeo') implementation project(':vtm-json') @@ -14,6 +15,7 @@ dependencies { implementation project(':vtm-models') implementation project(':vtm-mvt') implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" + implementation "org.mapsforge:mapsforge-map-awt:$mapsforgeVersion" } sourceSets { diff --git a/vtm-playground/src/org/oscim/test/MapsforgePoi3DTest.java b/vtm-playground/src/org/oscim/test/MapsforgePoi3DTest.java index 9b8bc9927..0b52591ee 100644 --- a/vtm-playground/src/org/oscim/test/MapsforgePoi3DTest.java +++ b/vtm-playground/src/org/oscim/test/MapsforgePoi3DTest.java @@ -18,16 +18,24 @@ import org.oscim.gdx.GdxMapApp; import java.io.File; +import java.util.Arrays; import java.util.List; public class MapsforgePoi3DTest extends MapsforgeTest { - private MapsforgePoi3DTest(List mapFiles) { - super(mapFiles, false, true); + private MapsforgePoi3DTest(File demFolder, List mapFiles) { + super(demFolder, mapFiles, false, true); } + /** + * @param args command line args: expects the map files as multiple parameters + * with possible SRTM hgt folder as 1st argument. + */ public static void main(String[] args) { GdxMapApp.init(); - GdxMapApp.run(new MapsforgePoi3DTest(getMapFiles(args))); + File demFolder = getDemFolder(args); + if (demFolder != null) + args = Arrays.copyOfRange(args, 1, args.length); + GdxMapApp.run(new MapsforgePoi3DTest(demFolder, getMapFiles(args))); } } diff --git a/vtm-playground/src/org/oscim/test/MapsforgeS3DBTest.java b/vtm-playground/src/org/oscim/test/MapsforgeS3DBTest.java index 685c89a44..03fd15171 100644 --- a/vtm-playground/src/org/oscim/test/MapsforgeS3DBTest.java +++ b/vtm-playground/src/org/oscim/test/MapsforgeS3DBTest.java @@ -17,16 +17,24 @@ import org.oscim.gdx.GdxMapApp; import java.io.File; +import java.util.Arrays; import java.util.List; public class MapsforgeS3DBTest extends MapsforgeTest { - private MapsforgeS3DBTest(List mapFiles) { - super(mapFiles, true, false); + private MapsforgeS3DBTest(File demFolder, List mapFiles) { + super(demFolder, mapFiles, true, false); } + /** + * @param args command line args: expects the map files as multiple parameters + * with possible SRTM hgt folder as 1st argument. + */ public static void main(String[] args) { GdxMapApp.init(); - GdxMapApp.run(new MapsforgeS3DBTest(getMapFiles(args))); + File demFolder = getDemFolder(args); + if (demFolder != null) + args = Arrays.copyOfRange(args, 1, args.length); + GdxMapApp.run(new MapsforgeS3DBTest(demFolder, getMapFiles(args))); } } diff --git a/vtm-playground/src/org/oscim/test/MapsforgeStyleTest.java b/vtm-playground/src/org/oscim/test/MapsforgeStyleTest.java index af7f72e30..9b3413aa2 100644 --- a/vtm-playground/src/org/oscim/test/MapsforgeStyleTest.java +++ b/vtm-playground/src/org/oscim/test/MapsforgeStyleTest.java @@ -22,13 +22,14 @@ import org.oscim.theme.XmlRenderThemeStyleMenu; import java.io.File; +import java.util.Arrays; import java.util.List; import java.util.Set; public class MapsforgeStyleTest extends MapsforgeTest { - private MapsforgeStyleTest(List mapFiles) { - super(mapFiles); + private MapsforgeStyleTest(File demFolder, List mapFiles) { + super(demFolder, mapFiles); } @Override @@ -78,8 +79,15 @@ protected boolean onKeyDown(int keycode) { return super.onKeyDown(keycode); } + /** + * @param args command line args: expects the map files as multiple parameters + * with possible SRTM hgt folder as 1st argument. + */ public static void main(String[] args) { GdxMapApp.init(); - GdxMapApp.run(new MapsforgeStyleTest(getMapFiles(args))); + File demFolder = getDemFolder(args); + if (demFolder != null) + args = Arrays.copyOfRange(args, 1, args.length); + GdxMapApp.run(new MapsforgeStyleTest(demFolder, getMapFiles(args))); } } diff --git a/vtm-playground/src/org/oscim/test/MapsforgeTest.java b/vtm-playground/src/org/oscim/test/MapsforgeTest.java index 77fbf30f2..0812a6447 100644 --- a/vtm-playground/src/org/oscim/test/MapsforgeTest.java +++ b/vtm-playground/src/org/oscim/test/MapsforgeTest.java @@ -17,27 +17,35 @@ */ package org.oscim.test; +import org.mapsforge.map.awt.graphics.AwtGraphicFactory; +import org.mapsforge.map.layer.hills.AdaptiveClasyHillShading; +import org.mapsforge.map.layer.hills.DemFolderFS; +import org.oscim.backend.canvas.Color; import org.oscim.core.BoundingBox; import org.oscim.core.MapPosition; import org.oscim.core.Tile; import org.oscim.event.Event; import org.oscim.gdx.GdxMapApp; import org.oscim.gdx.poi3d.Poi3DLayer; +import org.oscim.layers.tile.bitmap.BitmapTileLayer; import org.oscim.layers.tile.buildings.BuildingLayer; import org.oscim.layers.tile.buildings.S3DBLayer; import org.oscim.layers.tile.vector.VectorTileLayer; import org.oscim.layers.tile.vector.labeling.LabelLayer; import org.oscim.map.Map; +import org.oscim.map.Viewport; import org.oscim.renderer.BitmapRenderer; import org.oscim.renderer.ExtrusionRenderer; import org.oscim.renderer.GLViewport; import org.oscim.scalebar.*; import org.oscim.theme.internal.VtmThemes; +import org.oscim.tiling.source.hills.HillshadingTileSource; import org.oscim.tiling.source.mapfile.MapFileTileSource; import org.oscim.tiling.source.mapfile.MultiMapFileTileSource; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.List; @@ -45,15 +53,17 @@ public class MapsforgeTest extends GdxMapApp { private static final boolean SHADOWS = false; + private final File demFolder; private final List mapFiles; private final boolean poi3d; private final boolean s3db; - MapsforgeTest(List mapFiles) { - this(mapFiles, false, false); + MapsforgeTest(File demFolder, List mapFiles) { + this(demFolder, mapFiles, false, false); } - MapsforgeTest(List mapFiles, boolean s3db, boolean poi3d) { + MapsforgeTest(File demFolder, List mapFiles, boolean s3db, boolean poi3d) { + this.demFolder = demFolder; this.mapFiles = mapFiles; this.s3db = s3db; this.poi3d = poi3d; @@ -61,20 +71,31 @@ public class MapsforgeTest extends GdxMapApp { @Override public void createLayers() { - MultiMapFileTileSource tileSource = new MultiMapFileTileSource(); + MultiMapFileTileSource multiMapFileTileSource = new MultiMapFileTileSource(); for (File mapFile : mapFiles) { MapFileTileSource mapFileTileSource = new MapFileTileSource(); mapFileTileSource.setMapFile(mapFile.getAbsolutePath()); if ("world.map".equalsIgnoreCase(mapFile.getName())) mapFileTileSource.setPriority(-1); - tileSource.add(mapFileTileSource); + multiMapFileTileSource.add(mapFileTileSource); } - //tileSource.setDeduplicate(true); - //tileSource.setPreferredLanguage("en"); + //multiMapFileTileSource.setDeduplicate(true); + //multiMapFileTileSource.setPreferredLanguage("en"); - VectorTileLayer l = mMap.setBaseMap(tileSource); + VectorTileLayer l = mMap.setBaseMap(multiMapFileTileSource); loadTheme(null); + if (demFolder != null) { + final AdaptiveClasyHillShading algorithm = new AdaptiveClasyHillShading() + // You can make additional behavior adjustments + .setAdaptiveZoomEnabled(true) + // .setZoomMinOverride(0) + // .setZoomMaxOverride(17) + .setCustomQualityScale(1); + final HillshadingTileSource hillshadingTileSource = new HillshadingTileSource(Viewport.MIN_ZOOM_LEVEL, Viewport.MAX_ZOOM_LEVEL, new DemFolderFS(demFolder), algorithm, 128, Color.BLACK, AwtGraphicFactory.INSTANCE); + mMap.layers().add(new BitmapTileLayer(mMap, hillshadingTileSource, 150)); + } + BuildingLayer buildingLayer = s3db ? new S3DBLayer(mMap, l, SHADOWS) : new BuildingLayer(mMap, l, false, SHADOWS); mMap.layers().add(buildingLayer); @@ -96,7 +117,7 @@ public void createLayers() { mMap.layers().add(mapScaleBarLayer); MapPosition pos = MapPreferences.getMapPosition(); - BoundingBox bbox = tileSource.getBoundingBox(); + BoundingBox bbox = multiMapFileTileSource.getBoundingBox(); if (pos == null || !bbox.contains(pos.getGeoPoint())) { pos = new MapPosition(); pos.setByBoundingBox(bbox, Tile.SIZE * 4, Tile.SIZE * 4); @@ -133,6 +154,18 @@ public void dispose() { super.dispose(); } + static File getDemFolder(String[] args) { + if (args.length == 0) { + throw new IllegalArgumentException("missing argument: "); + } + + File demFolder = new File(args[0]); + if (demFolder.exists() && demFolder.isDirectory() && demFolder.canRead()) { + return demFolder; + } + return null; + } + static List getMapFiles(String[] args) { if (args.length == 0) { throw new IllegalArgumentException("missing argument: "); @@ -157,8 +190,15 @@ void loadTheme(final String styleId) { mMap.setTheme(VtmThemes.MOTORIDER); } + /** + * @param args command line args: expects the map files as multiple parameters + * with possible SRTM hgt folder as 1st argument. + */ public static void main(String[] args) { GdxMapApp.init(); - GdxMapApp.run(new MapsforgeTest(getMapFiles(args))); + File demFolder = getDemFolder(args); + if (demFolder != null) + args = Arrays.copyOfRange(args, 1, args.length); + GdxMapApp.run(new MapsforgeTest(demFolder, getMapFiles(args))); } }