Map Rotation #1247
-
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 2 replies
-
Beta Was this translation helpful? Give feedback.
-
|
i did want to use vtm but could not make it work. the documentation that it is very poor. maybe you could help with vtm. most of my app code related to map rendering is in file MapProcessor.kt here is the full file. how can i convert it to use vtm? package com.example.lysaksnavi.manager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.location.Location
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Button
import androidx.core.content.ContextCompat
import org.mapsforge.map.android.graphics.AndroidGraphicFactory
import org.mapsforge.map.android.rendertheme.AssetsRenderTheme
import org.osmdroid.api.IMapController
import org.osmdroid.config.Configuration
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
import org.osmdroid.events.ZoomEvent
import org.osmdroid.tileprovider.util.SimpleRegisterReceiver
import org.osmdroid.mapsforge.MapsForgeTileProvider
import org.osmdroid.mapsforge.MapsForgeTileSource
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.ScaleBarOverlay
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
import org.osmdroid.views.overlay.mylocation.IMyLocationConsumer
import org.osmdroid.views.overlay.mylocation.IMyLocationProvider
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import java.io.File
import android.content.SharedPreferences
import android.graphics.Color
import android.widget.TextView
import com.example.lysaksnavi.R
import org.osmdroid.views.overlay.TilesOverlay
private const val TAG = "mapProcessor"
typealias ZoomButtonUpdater = (Boolean, Boolean) -> Unit
typealias MapReadyCallback = (Boolean) -> Unit
var DEFAULT_LAT = 51.019695
var DEFAULT_LON = 2.192331
// dfds dunkerque
// 51.019695
// 2.192331
// A16 Fr.
// 51.03342763
// 2.43850405
class mapProcessor(
private val context: Context,
private val mapView: MapView,
private val btnZoomIn: Button,
private val btnZoomOut: Button,
private val sharedPreferences: SharedPreferences,
private val zoomButtonUpdater: ZoomButtonUpdater,
private val mapReadyCallback: MapReadyCallback,
val mapDataPath: File,
val useDummyLocation: Boolean = false
) {
internal lateinit var mapController: IMapController
private var isMapDisplayInitializedAndReady = false
val osmConfig = Configuration.getInstance()
var myLocationOverlay: MyLocationNewOverlay? = null
var myLocationProvider: IMyLocationProvider? = null
var currentZoomLevel = 0.0
var mapsforgeTheme = "lysaksnavi-bright.xml"
var scaleBarCurrentColor = Color.parseColor("#ffffff")
var mapBgColor = Color.BLACK
// private val DEFAULT_ZOOM_LEVEL = 14.0
private val KEY_MAP_ZOOM = "last-map-zoom-level"
private val KEY_LAST_LAT = "last-lat"
private val KEY_LAST_LON = "last-lon"
fun setupMapConfiguration() {
Configuration.getInstance().load(context, sharedPreferences)
Configuration.getInstance().osmdroidTileCache = File(context.cacheDir, "osmdroidTiles")
Configuration.getInstance().osmdroidBasePath = File(context.cacheDir, "osmdroidBase")
mapController = mapView.controller
osmConfig.cacheMapTileCount = 70
osmConfig.tileDownloadThreads = 1
mapView.setBuiltInZoomControls(false)
mapView.setMultiTouchControls(true)
btnZoomIn.setOnClickListener {
currentZoomLevel = mapView.zoomLevelDouble + 0.5
mapController.setZoom(currentZoomLevel)
saveMapZoomLevel(currentZoomLevel)
}
btnZoomOut.setOnClickListener {
currentZoomLevel = mapView.zoomLevelDouble - 0.5
mapController.setZoom(currentZoomLevel)
saveMapZoomLevel(currentZoomLevel)
}
mapView.addMapListener(object : MapListener {
override fun onScroll(event: ScrollEvent?): Boolean = false
override fun onZoom(event: ZoomEvent?): Boolean {
updateZoomButtonStates()
return false
}
})
}
fun initMapDisplay() {
if (isMapDisplayInitializedAndReady) {
return
}
if (!mapDataPath.exists() || !mapDataPath.isDirectory) {
Log.e(TAG, "Map data path is invalid: ${mapDataPath.absolutePath}")
mapReadyCallback(false)
return
}
val mapFilesToLoad = mapDataPath.listFiles { _, name -> name.endsWith(".map", ignoreCase = true) }?.sortedBy { it.name } ?: emptyList()
if (mapFilesToLoad.isEmpty()) {
Log.e(TAG, "No .map files found in ${mapDataPath.absolutePath} for map display.")
mapReadyCallback(false)
return
}
Log.i(TAG, "Found ${mapFilesToLoad.size} map files for display: ${mapFilesToLoad.joinToString {it.name}}")
try {
// This is your original, correct code for creating the tile source.
val themeName = mapsforgeTheme
val relativeThemePath = "renderthemes/mapsforge/"
val renderTheme = AssetsRenderTheme(context.assets, relativeThemePath, themeName)
val tileSource = MapsForgeTileSource.createFromFiles(mapFilesToLoad.toTypedArray(), renderTheme, "DynamicMultiMap")
if (tileSource == null) {
Log.e(TAG, "Failed to create tile source.")
mapReadyCallback(false)
return
}
val tileProvider = MapsForgeTileProvider(SimpleRegisterReceiver(context), tileSource, null)
if (tileProvider == null) {
Log.e(TAG, "Failed to create tile provider.")
mapReadyCallback(false)
return
}
mapView.setTileProvider(tileProvider)
val tileOverlay = TilesOverlay(tileProvider, context)
tileOverlay.setLoadingBackgroundColor(mapBgColor)
tileOverlay.setLoadingLineColor(Color.TRANSPARENT) // or Color.TRANSPARENT if you want no grid
mapView.overlays.add(tileOverlay)
val scaleBarOverlay = ScaleBarOverlay(mapView)
mapView.overlays.remove(scaleBarOverlay)
mapView.overlays.add(scaleBarOverlay)
scaleBarOverlay.unitsOfMeasure = ScaleBarOverlay.UnitsOfMeasure.valueOf("metric")
scaleBarOverlay.barPaint.setColor(scaleBarCurrentColor)
scaleBarOverlay.textPaint.setColor(scaleBarCurrentColor)
scaleBarOverlay.setScaleBarOffset(20,7)
scaleBarOverlay.drawLatitudeScale(false)
scaleBarOverlay.drawLongitudeScale(true)
scaleBarOverlay.setMaxLength(7.1f)
scaleBarOverlay.setTextSize(30.0f)
mapView.setBuiltInZoomControls(false)
mapView.setMultiTouchControls(true)
mapController.setZoom(loadMapZoomLevel())
currentZoomLevel = mapView.zoomLevelDouble
mapController.setCenter(loadMapLocation())
isMapDisplayInitializedAndReady = true
Log.i(TAG, "Map display setup complete with Mapsforge.")
mapReadyCallback(true)
mapView.post { updateZoomButtonStates() }
} catch (e: Exception) {
isMapDisplayInitializedAndReady = false
Log.e(TAG, "Error in map display setup with Mapsforge", e)
mapReadyCallback(false)
}
}
fun resumeMapDisplay() {
loadMapLocation()
mapView.onResume()
}
fun pauseMapDisplay() {
saveMapLocation()
mapView.onPause()
}
// This method just sets up the MyLocationNewOverlay with its visual parameters.
// It does not control how the map tracks or rotates; that's MainActivity's job.
fun setupLocationAndOrientationProviders(locationConsumerCallback: IMyLocationConsumer) {
if (myLocationOverlay == null) {
myLocationProvider = if (useDummyLocation) {
DummyLocationProvider(context, GeoPoint(DEFAULT_LAT, DEFAULT_LON))
} else {
GpsMyLocationProvider(context)
}
myLocationOverlay = MyLocationNewOverlay(myLocationProvider, mapView)
val navArrowBitmap = drawableToBitmap(R.drawable.ic_map_arrow)
navArrowBitmap?.let {
myLocationOverlay?.setDirectionIcon(navArrowBitmap)
myLocationOverlay?.setPersonIcon(navArrowBitmap)
myLocationOverlay?.setPersonAnchor(0.5f, 0.0f) // Keeps icon visually centered relative to its drawing point
myLocationOverlay?.setDirectionAnchor(0.5f, 0.0f)
} ?: run {
Log.e(TAG, "Failed to load custom navigation arrow bitmap. Using default.")
}
myLocationOverlay?.setDrawAccuracyEnabled(false)
} else {
}
mapView.invalidate()
}
// This just starts/stops the location provider. No map rotation or centering logic here.
fun resumeLocationAndOrientationUpdates(locationConsumerCallback: IMyLocationConsumer) {
myLocationOverlay?.enableMyLocation()
// enableFollowLocation() is crucial for the map to move with location. It's now called directly by MainActivity
// as applyCurrentMapMode needs it to activate.
myLocationProvider?.startLocationProvider(locationConsumerCallback)
}
// This just pauses the location provider. No map rotation or centering logic here.
fun pauseLocationAndOrientationUpdates() {
myLocationOverlay?.disableMyLocation()
myLocationOverlay?.disableFollowLocation() // Ensure it stops following map movement too.
myLocationProvider?.stopLocationProvider()
}
fun isMapDisplayLoaded(): Boolean = isMapDisplayInitializedAndReady
fun saveMapZoomLevel(zoomLevel: Double) {
with(sharedPreferences.edit()) {
putFloat(KEY_MAP_ZOOM, zoomLevel.toFloat())
apply()
}
}
fun loadMapZoomLevel(): Double {
val loadedZoom = sharedPreferences.getFloat(KEY_MAP_ZOOM, mapView.zoomLevelDouble.toFloat()).toDouble()
return loadedZoom
}
fun updateZoomButtonStates() {
if (mapView.isLayoutOccurred) {
val canZoomIn = mapView.canZoomIn()
val canZoomOut = mapView.canZoomOut()
zoomButtonUpdater(canZoomIn, canZoomOut)
} else {
}
}
fun drawableToBitmap(drawableId: Int): Bitmap? {
val drawable = ContextCompat.getDrawable(context, drawableId)
if (drawable == null) {
return null
}
val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: 64
val height = drawable.intrinsicHeight.takeIf { it > 0 } ?: 64
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawARGB(0, 0, 0, 0)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
fun cleanupMapDisplay() {
mapView.onDetach()
}
fun saveMapLocation() {
val center = mapView.mapCenter
var latitude = center.latitude
var longitude = center.longitude
with(sharedPreferences.edit()) {
putFloat(KEY_LAST_LAT, latitude.toFloat())
putFloat(KEY_LAST_LON, longitude.toFloat())
apply()
}
Log.e(TAG,"saved: ${latitude}....${longitude}")
}
fun loadMapLocation(): GeoPoint {
val loadedLat = sharedPreferences.getFloat(KEY_LAST_LAT, DEFAULT_LAT.toFloat()).toDouble()
val loadedLon = sharedPreferences.getFloat(KEY_LAST_LON, DEFAULT_LON.toFloat()).toDouble()
Log.e(TAG,"loaded: ${loadedLat}....${loadedLon}")
return GeoPoint(loadedLat, loadedLon)
}
class DummyLocationProvider(private val context: Context, private val fixedGeoPoint: GeoPoint) : IMyLocationProvider {
private var myLocationConsumer: IMyLocationConsumer? = null
private val handler = Handler(Looper.getMainLooper())
private var lastLocation: Location? = null
private var dummyBearing: Float = 0f
private val BEARING_CHANGE_RATE = 5f
private val SPEED_MS = 5.0f
private val updateRunnable = object : Runnable {
override fun run() {
val newLocation = Location("dummy_provider").apply {
latitude = fixedGeoPoint.latitude
longitude = fixedGeoPoint.longitude
altitude = 0.0
accuracy = 5.0f
time = System.currentTimeMillis()
dummyBearing = (dummyBearing + (BEARING_CHANGE_RATE * 0.1f)) % 360f
bearing = dummyBearing
speed = SPEED_MS
}
lastLocation = newLocation
myLocationConsumer?.onLocationChanged(newLocation, this@DummyLocationProvider)
handler.postDelayed(this, 100)
}
}
override fun startLocationProvider(myLocationConsumer: IMyLocationConsumer): Boolean {
this.myLocationConsumer = myLocationConsumer
handler.post(updateRunnable)
return true
}
override fun stopLocationProvider() {
myLocationConsumer = null
handler.removeCallbacks(updateRunnable)
}
override fun getLastKnownLocation(): Location? {
return lastLocation
}
override fun destroy() {
stopLocationProvider()
}
}
} |
Beta Was this translation helpful? Give feedback.
-
|
Build file '.../vtm-android-example/build.gradle' line: 1 A problem occurred evaluating root project 'vtm-android-example'.
|
Beta Was this translation helpful? Give feedback.
-
|
thank you. here is google ai studio response to my question if it's a good idea to switch to vtm from mapsforge. i would like to know what you think ..... xecutive Summary & RecommendationYes, you should absolutely pursue migrating to VTM. Your goal to keep text labels horizontal while the map rotates in a heading-up navigation mode is precisely the kind of advanced feature that a vector map library like VTM is designed for. The osmdroid/MapsForge stack, which renders map data into static bitmap tiles, simply cannot do this. Once a tile is rendered, the labels are "baked in" and will rotate with the rest of the map image. The migration is feasible, but it's not a simple drop-in replacement. You will need to change your map setup, data handling, and interaction logic. However, the result will be a much more fluid and professional-looking navigation experience. Analysis of Your MapProcessor.ktYour current MapProcessor.kt is well-structured and cleanly separates responsibilities. Here's a breakdown of its key functions and how they would translate to a VTM implementation:
Functionality in MapProcessor.kt | How it Works Now (osmdroid/MapsForge) | How it Would Work with VTM
-- | -- | --
Map Display | MapsForgeTileSource and MapsForgeTileProvider create bitmap tiles from .map files and a render theme. MapView displays these tiles. | VTM directly parses the vector data from the .map file and renders it on an OpenGL surface in real-time. You'll use a MapView class from the VTM library.
Styling/Theme | An XML render theme is loaded via AssetsRenderTheme to style the bitmap tiles. | VTM uses a similar XML theme file to define how vector features (roads, labels, etc.) are drawn on the map. The styling is applied dynamically during rendering.
User Location | MyLocationNewOverlay is used to display the user's position and bearing on top of the map tiles. | VTM will likely have its own "overlay" or "layer" system. You would add a location marker to a specific layer and update its position and rotation.
Map Rotation | mapView.mapOrientation = -bearing (likely in your MainActivity) rotates the entire MapView, including the rendered tiles and labels. | You will call a method like mapView.setMapRotation(-bearing). VTM's renderer will rotate the map geometry (roads, water) but intelligently redraw the text labels to remain horizontal on the screen.
Data Source | You are loading multiple .map files from a directory. | VTM is also compatible with the MapsForge .map file format, so you should be able to continue using your existing map data without conversion.
Zoom/Pan | Handled by IMapController and MapListener. | VTM's MapView will have its own controller and listeners for handling user interaction and map events.
Suggested Migration StrategyInstead of modifying your existing MapProcessor.kt line-by-line, I recommend creating a new VtmMapProcessor.kt that implements a similar interface. This allows you to develop and test the VTM implementation separately before swapping it out in your MainActivity. Here is a high-level plan: Step 1: Add VTM DependenciesFirst, you need to correctly set up your Gradle files to include the VTM libraries. As you discovered, this involves adding the project modules to your settings.gradle and implementing them in your app's build.gradle. Step 2: Create the Basic VTM Map ViewIn your layout XML, replace the org.osmdroid.views.MapView with VTM's equivalent. It will likely be something like org.oscim.android.MapView. Your new VtmMapProcessor.kt will take this new MapView as a parameter. Step 3: Initialize the Map and Load DataThis is the replacement for your initMapDisplay() method. The core steps will be:
Step 4: Implement Location DisplayThis replaces setupLocationAndOrientationProviders.
Step 5: Handle Map Rotation and Horizontal LabelsThis is the key part. Your MainActivity (or wherever you handle bearing updates) will call a method in your new processor. codeKotlin downloadcontent_copy expand_lessThe magic happens within the VTM library. When you call setMapRotation, it will handle the OpenGL rotation of the map geometry while ensuring the label rendering part of the pipeline always draws text aligned with the screen. You may need to ensure a setting like "text-orientation": "horizontal" is respected or enabled in your theme or layer properties, but this is often the default behavior in vector engines. Final Thoughts
This is a worthwhile endeavor. You've hit a clear limitation of a tile-based rendering system, and moving to a true vector renderer is the correct technical solution. Start small by getting a basic map with your data to display, then incrementally add the location marker, rotation, and other features. |
Beta Was this translation helpful? Give feedback.

Better use directly the Mapsforge library, see the rotation examples in samples.
Or use the VTM (OpenGL) library that is faster and can read Mapsforge maps.