Improve map interaction, shelter selection, and offline caching

- Rework selection model so any shelter marker can be tapped to select,
  not just the nearest three in the bottom sheet list
- Highlight selected shelter with a distinct amber marker icon
- Track user map interaction (pan/zoom) to prevent auto-recentering
- Add reset navigation FAB to re-fit map to user + selected shelter
- Add offline map cache prompt (OK/Skip) with warning banner and retry
- Rewrite MapCacheManager to use passive tile caching via map panning
- Respect system status bar with fitsSystemWindows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-08 17:41:15 +01:00
commit 92531ee971
9 changed files with 373 additions and 112 deletions

View file

@ -20,6 +20,7 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import no.naiv.tilfluktsrom.data.MapCacheManager
@ -56,11 +57,20 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
private var currentLocation: Location? = null
private var allShelters: List<Shelter> = emptyList()
private var nearestShelters: List<ShelterWithDistance> = emptyList()
private var selectedShelterIndex = 0
private var deviceHeading = 0f
private var isCompassMode = false
private var locationJob: Job? = null
private var shelterMarkers: MutableList<Marker> = mutableListOf()
private var cachingJob: Job? = null
// Map from shelter lokalId to its map marker, for icon swapping on selection
private var shelterMarkerMap: MutableMap<String, Marker> = mutableMapOf()
private var highlightedMarkerId: String? = null
// The currently selected shelter — can be any shelter, not just one from nearestShelters
private var selectedShelter: ShelterWithDistance? = null
// When true, location updates won't auto-select the nearest shelter
private var userSelectedShelter = false
// When true, location updates won't auto-zoom the map
private var userHasInteractedWithMap = false
private val locationPermissionRequest = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
@ -99,6 +109,16 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
// Default center: roughly central Norway
controller.setCenter(GeoPoint(59.9, 10.7))
// Detect user touch interaction (pan/zoom) to suppress auto-zoom
setOnTouchListener { v, _ ->
if (!userHasInteractedWithMap) {
userHasInteractedWithMap = true
binding.resetNavigationFab.visibility = View.VISIBLE
}
v.performClick()
false // Don't consume the event
}
// Add user location overlay
myLocationOverlay = MyLocationNewOverlay(
GpsMyLocationProvider(this@MainActivity), this
@ -110,12 +130,10 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
}
private fun setupShelterList() {
shelterAdapter = ShelterListAdapter { selected ->
val idx = nearestShelters.indexOf(selected)
if (idx >= 0) {
selectedShelterIndex = idx
updateSelectedShelter()
}
shelterAdapter = ShelterListAdapter { swd ->
userSelectedShelter = true
userHasInteractedWithMap = false
selectShelter(swd)
}
binding.shelterList.apply {
@ -138,9 +156,26 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
}
}
// Reset to navigation: re-fit map to show user + selected shelter
binding.resetNavigationFab.setOnClickListener {
userHasInteractedWithMap = false
binding.resetNavigationFab.visibility = View.GONE
selectedShelter?.let { highlightShelterOnMap(it) }
}
binding.refreshButton.setOnClickListener {
forceRefresh()
}
binding.cacheRetryButton.setOnClickListener {
currentLocation?.let { loc ->
if (isNetworkAvailable()) {
startCaching(loc.latitude, loc.longitude)
} else {
Toast.makeText(this, R.string.error_download_failed, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun loadData() {
@ -231,16 +266,87 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
allShelters, location.latitude, location.longitude, NEAREST_COUNT
)
// Highlight which nearest-list item matches the current selection
val selectedIdx = if (selectedShelter != null) {
nearestShelters.indexOfFirst { it.shelter.lokalId == selectedShelter!!.shelter.lokalId }
} else -1
shelterAdapter.submitList(nearestShelters)
selectedShelterIndex = 0
shelterAdapter.selectPosition(0)
updateSelectedShelter()
shelterAdapter.selectPosition(selectedIdx)
if (userSelectedShelter && selectedShelter != null) {
// Recalculate distance/bearing for the user's picked shelter
refreshSelectedShelterDistance(location)
} else {
// Auto-select nearest
if (nearestShelters.isNotEmpty()) {
selectShelter(nearestShelters[0])
}
}
updateSelectedShelterUI()
}
private fun updateSelectedShelter() {
if (nearestShelters.isEmpty()) return
/**
* Select a specific shelter (from list tap, marker tap, or auto-select).
* Recalculates distance/bearing from current location.
*/
private fun selectShelter(swd: ShelterWithDistance) {
selectedShelter = swd
currentLocation?.let { refreshSelectedShelterDistance(it) }
val selected = nearestShelters[selectedShelterIndex]
// Update list highlight
val idx = nearestShelters.indexOfFirst { it.shelter.lokalId == swd.shelter.lokalId }
shelterAdapter.selectPosition(idx)
updateSelectedShelterUI()
}
/**
* Select a shelter by its data object (e.g. from a marker tap).
* Computes a fresh ShelterWithDistance from the current location.
*/
private fun selectShelterByData(shelter: Shelter) {
val loc = currentLocation
val swd = if (loc != null) {
ShelterWithDistance(
shelter = shelter,
distanceMeters = DistanceUtils.distanceMeters(
loc.latitude, loc.longitude, shelter.latitude, shelter.longitude
),
bearingDegrees = DistanceUtils.bearingDegrees(
loc.latitude, loc.longitude, shelter.latitude, shelter.longitude
)
)
} else {
ShelterWithDistance(shelter = shelter, distanceMeters = 0.0, bearingDegrees = 0.0)
}
userSelectedShelter = true
userHasInteractedWithMap = false
binding.resetNavigationFab.visibility = View.GONE
selectShelter(swd)
}
/** Recalculate distance/bearing for the currently selected shelter. */
private fun refreshSelectedShelterDistance(location: Location) {
val current = selectedShelter ?: return
selectedShelter = ShelterWithDistance(
shelter = current.shelter,
distanceMeters = DistanceUtils.distanceMeters(
location.latitude, location.longitude,
current.shelter.latitude, current.shelter.longitude
),
bearingDegrees = DistanceUtils.bearingDegrees(
location.latitude, location.longitude,
current.shelter.latitude, current.shelter.longitude
)
)
}
/** Update all UI elements for the currently selected shelter. */
private fun updateSelectedShelterUI() {
val selected = selectedShelter ?: return
val distanceText = DistanceUtils.formatDistance(selected.distanceMeters)
// Update bottom sheet
@ -260,28 +366,61 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
binding.compassAddressText.text = selected.shelter.adresse
binding.directionArrow.setDirection(bearing - deviceHeading)
// Center map on shelter if in map mode
if (!isCompassMode) {
// Emphasize the selected marker on the map
highlightSelectedMarker(selected.shelter.lokalId)
// Only auto-zoom the map if the user hasn't manually panned/zoomed
if (!isCompassMode && !userHasInteractedWithMap) {
highlightShelterOnMap(selected)
}
}
/** Swap marker icons so the selected shelter stands out. */
private fun highlightSelectedMarker(lokalId: String) {
if (lokalId == highlightedMarkerId) return
val normalIcon = ContextCompat.getDrawable(this, R.drawable.ic_shelter)
val selectedIcon = ContextCompat.getDrawable(this, R.drawable.ic_shelter_selected)
// Reset previous
highlightedMarkerId?.let { prevId ->
shelterMarkerMap[prevId]?.icon = normalIcon
}
// Highlight new
shelterMarkerMap[lokalId]?.icon = selectedIcon
highlightedMarkerId = lokalId
binding.mapView.invalidate()
}
private fun updateShelterMarkers() {
// Remove old markers
shelterMarkers.forEach { binding.mapView.overlays.remove(it) }
shelterMarkers.clear()
shelterMarkerMap.values.forEach { binding.mapView.overlays.remove(it) }
shelterMarkerMap.clear()
highlightedMarkerId = null
// Add markers for all shelters
val normalIcon = ContextCompat.getDrawable(this, R.drawable.ic_shelter)
val selectedIcon = ContextCompat.getDrawable(this, R.drawable.ic_shelter_selected)
val currentSelectedId = selectedShelter?.shelter?.lokalId
// Add markers for all shelters — tapping any marker selects it
allShelters.forEach { shelter ->
val isSelected = shelter.lokalId == currentSelectedId
val marker = Marker(binding.mapView).apply {
position = GeoPoint(shelter.latitude, shelter.longitude)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
title = shelter.adresse
snippet = getString(R.string.shelter_capacity, shelter.plasser) +
" - " + getString(R.string.shelter_room_nr, shelter.romnr)
icon = ContextCompat.getDrawable(this@MainActivity, R.drawable.ic_shelter)
icon = if (isSelected) selectedIcon else normalIcon
setOnMarkerClickListener { _, _ ->
selectShelterByData(shelter)
true
}
}
shelterMarkers.add(marker)
if (isSelected) highlightedMarkerId = shelter.lokalId
shelterMarkerMap[shelter.lokalId] = marker
binding.mapView.overlays.add(marker)
}
@ -305,18 +444,42 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
}
private fun cacheMapTiles(latitude: Double, longitude: Double) {
lifecycleScope.launch {
binding.statusText.text = getString(R.string.status_caching_map)
// Show prompt with OK / Skip choice
showLoading(getString(R.string.loading_map_explanation))
binding.loadingProgress.visibility = View.GONE
binding.loadingButtonRow.visibility = View.VISIBLE
binding.loadingOkButton.setOnClickListener {
startCaching(latitude, longitude)
}
binding.loadingSkipButton.setOnClickListener {
hideLoading()
showNoCacheBanner()
}
}
private fun startCaching(latitude: Double, longitude: Double) {
binding.noCacheBanner.visibility = View.GONE
showLoading(getString(R.string.loading_map))
binding.loadingButtonRow.visibility = View.GONE
cachingJob = lifecycleScope.launch {
mapCacheManager.cacheMapArea(
binding.mapView, latitude, longitude
) { progress ->
binding.statusText.text = getString(R.string.status_caching_map) +
binding.loadingText.text = getString(R.string.loading_map) +
" (${(progress * 100).toInt()}%)"
}
hideLoading()
binding.statusText.text = getString(R.string.status_shelters_loaded, allShelters.size)
}
}
private fun showNoCacheBanner() {
binding.noCacheBanner.visibility = View.VISIBLE
}
private fun forceRefresh() {
if (!isNetworkAvailable()) {
Toast.makeText(this, R.string.error_download_failed, Toast.LENGTH_SHORT).show()
@ -337,6 +500,8 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
private fun showLoading(message: String) {
binding.loadingOverlay.visibility = View.VISIBLE
binding.loadingText.text = message
binding.loadingProgress.visibility = View.VISIBLE
binding.loadingButtonRow.visibility = View.GONE
}
private fun hideLoading() {
@ -413,8 +578,7 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
}
private fun updateDirectionArrows() {
if (nearestShelters.isEmpty()) return
val selected = nearestShelters[selectedShelterIndex]
val selected = selectedShelter ?: return
val bearing = selected.bearingDegrees.toFloat()
val arrowAngle = bearing - deviceHeading

View file

@ -3,18 +3,20 @@ package no.naiv.tilfluktsrom.data
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.osmdroid.tileprovider.cachemanager.CacheManager
import org.osmdroid.tileprovider.modules.SqlTileWriter
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
/**
* Manages offline map tile caching for the surrounding area.
*
* On first launch, downloads map tiles for a region around the user's location.
* On subsequent launches, checks if the current location is within the cached area.
* OSMDroid's SqlTileWriter automatically caches every tile the MapView loads.
* We exploit this by programmatically panning the map across the surrounding area
* at multiple zoom levels, which causes tiles to be fetched and cached passively.
*
* This approach respects OSM's tile usage policy (no bulk download) while still
* building up an offline cache for the user's area.
*/
class MapCacheManager(private val context: Context) {
@ -26,10 +28,13 @@ class MapCacheManager(private val context: Context) {
private const val KEY_CACHE_RADIUS = "cache_radius_km"
private const val KEY_CACHE_COMPLETE = "cache_complete"
// Cache tiles for ~15km radius at useful zoom levels
private const val CACHE_RADIUS_DEGREES = 0.15 // ~15km
private const val MIN_ZOOM = 10
private const val MAX_ZOOM = 16
// Zoom levels to cache: overview down to street level
private val CACHE_ZOOM_LEVELS = intArrayOf(10, 12, 14, 16)
// Grid points per axis per zoom level for panning
private const val GRID_SIZE = 3
}
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
@ -46,14 +51,15 @@ class MapCacheManager(private val context: Context) {
if (radius == 0.0) return false
// Check if current location is within the cached region (with some margin)
val margin = radius * 0.3
return Math.abs(latitude - cachedLat) < (radius - margin) &&
Math.abs(longitude - cachedLon) < (radius - margin)
}
/**
* Download map tiles for the area around the given location.
* Seed the tile cache by panning the map across the area around the location.
* OSMDroid's built-in SqlTileWriter caches each tile as it's loaded.
*
* Reports progress via callback (0.0 to 1.0).
*/
suspend fun cacheMapArea(
@ -63,93 +69,51 @@ class MapCacheManager(private val context: Context) {
onProgress: (Float) -> Unit = {}
): Boolean = withContext(Dispatchers.Main) {
try {
Log.i(TAG, "Starting map tile cache for area around $latitude, $longitude")
Log.i(TAG, "Seeding tile cache for area around $latitude, $longitude")
val boundingBox = BoundingBox(
latitude + CACHE_RADIUS_DEGREES,
longitude + CACHE_RADIUS_DEGREES,
latitude - CACHE_RADIUS_DEGREES,
longitude - CACHE_RADIUS_DEGREES
)
val totalSteps = CACHE_ZOOM_LEVELS.size * GRID_SIZE * GRID_SIZE
var step = 0
val cacheManager = CacheManager(mapView)
var complete = false
var success = false
for (zoom in CACHE_ZOOM_LEVELS) {
mapView.controller.setZoom(zoom.toDouble())
cacheManager.downloadAreaAsync(
context,
boundingBox,
MIN_ZOOM,
MAX_ZOOM,
object : CacheManager.CacheManagerCallback {
override fun onTaskComplete() {
Log.i(TAG, "Map cache download complete")
success = true
complete = true
// Pan across a grid of points covering the area
for (row in 0 until GRID_SIZE) {
for (col in 0 until GRID_SIZE) {
val lat = latitude - CACHE_RADIUS_DEGREES +
(2 * CACHE_RADIUS_DEGREES * row) / (GRID_SIZE - 1)
val lon = longitude - CACHE_RADIUS_DEGREES +
(2 * CACHE_RADIUS_DEGREES * col) / (GRID_SIZE - 1)
mapView.controller.setCenter(GeoPoint(lat, lon))
// Force a layout pass so tiles are requested
mapView.invalidate()
step++
onProgress(step.toFloat() / totalSteps)
// Brief delay to allow tile loading to start
delay(300)
}
override fun onTaskFailed(errors: Int) {
Log.w(TAG, "Map cache download completed with $errors errors")
// Consider partial success if most tiles downloaded
success = errors < 50
complete = true
}
override fun updateProgress(
progress: Int,
currentZoomLevel: Int,
zoomMin: Int,
zoomMax: Int
) {
val totalZoomLevels = zoomMax - zoomMin + 1
val zoomProgress = (currentZoomLevel - zoomMin).toFloat() / totalZoomLevels
onProgress(zoomProgress + (progress / 100f) / totalZoomLevels)
}
override fun downloadStarted() {
Log.i(TAG, "Map cache download started")
}
override fun setPossibleTilesInArea(total: Int) {
Log.i(TAG, "Total tiles to download: $total")
}
}
)
// Wait for completion (the async callback runs on main thread)
withContext(Dispatchers.IO) {
while (!complete) {
Thread.sleep(500)
}
}
if (success) {
prefs.edit()
.putLong(KEY_CACHED_LAT, latitude.toBits())
.putLong(KEY_CACHED_LON, longitude.toBits())
.putFloat(KEY_CACHE_RADIUS, CACHE_RADIUS_DEGREES.toFloat())
.putBoolean(KEY_CACHE_COMPLETE, true)
.apply()
}
// Restore to user's location
mapView.controller.setZoom(14.0)
mapView.controller.setCenter(GeoPoint(latitude, longitude))
success
prefs.edit()
.putLong(KEY_CACHED_LAT, latitude.toBits())
.putLong(KEY_CACHED_LON, longitude.toBits())
.putFloat(KEY_CACHE_RADIUS, CACHE_RADIUS_DEGREES.toFloat())
.putBoolean(KEY_CACHE_COMPLETE, true)
.apply()
Log.i(TAG, "Tile cache seeding complete")
true
} catch (e: Exception) {
Log.e(TAG, "Failed to cache map tiles", e)
Log.e(TAG, "Failed to seed tile cache", e)
false
}
}
/**
* Get the approximate number of cached tiles.
*/
fun getCachedTileCount(): Long {
return try {
val writer = SqlTileWriter()
val count = writer.getRowCount(null)
writer.onDetach()
count
} catch (e: Exception) {
0L
}
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Crosshair / recenter icon for "reset to navigation" -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94V1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11H1v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94V23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94H23v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z" />
</vector>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Selected shelter marker: larger, amber fill with white outline for emphasis -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- White outline (slightly larger path) -->
<path
android:fillColor="#FFFFFF"
android:pathData="M12,1 L2,6.5 L2,12 C2,18 6.2,23.5 12,25 C17.8,23.5 22,18 22,12 L22,6.5 Z" />
<!-- Shield body (amber) -->
<path
android:fillColor="#FFC107"
android:pathData="M12,2 L3,7 L3,12 C3,17.5 6.8,22.7 12,24 C17.2,22.7 21,17.5 21,12 L21,7 Z" />
<!-- Inner cross/plus symbol -->
<path
android:fillColor="#1A1A2E"
android:pathData="M11,8 L13,8 L13,11 L16,11 L16,13 L13,13 L13,16 L11,16 L11,13 L8,13 L8,11 L11,11 Z" />
</vector>

View file

@ -5,6 +5,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="@color/background"
tools:context=".MainActivity">
@ -84,6 +85,22 @@
tools:text="Storgata 1" />
</FrameLayout>
<!-- Reset to navigation mode (re-fit map to user + selected shelter) -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/resetNavigationFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/action_reset_navigation"
android:src="@drawable/ic_my_location"
android:visibility="gone"
app:fabSize="mini"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottomSheet"
app:backgroundTint="@color/status_bar_bg"
app:tint="@color/white" />
<!-- Toggle button: map <-> compass -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/toggleViewFab"
@ -97,6 +114,38 @@
app:backgroundTint="@color/shelter_primary"
app:tint="@color/white" />
<!-- Warning banner: no offline map cache -->
<LinearLayout
android:id="@+id/noCacheBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/warning_bg"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/bottomSheet">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/warning_no_map_cache"
android:textColor="@color/warning_text"
android:textSize="12sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/cacheRetryButton"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="@string/action_cache_now"
android:textColor="@color/warning_text"
android:textSize="12sp"
android:minHeight="0dp" />
</LinearLayout>
<!-- Bottom sheet with shelter info -->
<LinearLayout
android:id="@+id/bottomSheet"
@ -183,9 +232,38 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:paddingHorizontal="32dp"
android:textAlignment="center"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="@string/loading_shelters" />
<LinearLayout
android:id="@+id/loadingButtonRow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal"
android:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/loadingSkipButton"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:text="@string/action_skip"
android:textColor="@color/white"
app:strokeColor="@color/white" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loadingOkButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_cache_ok"
android:textColor="@color/white"
app:backgroundTint="@color/shelter_primary" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View file

@ -14,6 +14,7 @@
<!-- Lasteskjerm -->
<string name="loading_shelters">Laster ned tilfluktsromdata&#8230;</string>
<string name="loading_map">Lagrer kartfliser&#8230;</string>
<string name="loading_map_explanation">Forbereder frakoblet kart.\nKartet vil rulle kort for å lagre omgivelsene dine.</string>
<string name="loading_first_time">Gjør klar for første gangs bruk&#8230;</string>
<!-- Tilfluktsrominfo -->
@ -25,6 +26,11 @@
<!-- Handlinger -->
<string name="action_refresh">Oppdater data</string>
<string name="action_toggle_view">Bytt mellom kart og kompassvisning</string>
<string name="action_skip">Hopp over</string>
<string name="action_cache_ok">Lagre kart</string>
<string name="action_cache_now">Lagre nå</string>
<string name="action_reset_navigation">Tilbakestill navigasjonsvisning</string>
<string name="warning_no_map_cache">Ingen frakoblet kart lagret. Kartet krever internett.</string>
<!-- Tillatelser -->
<string name="permission_location_title">Posisjonstillatelse kreves</string>

View file

@ -14,6 +14,7 @@
<!-- Lasteskjerm -->
<string name="loading_shelters">Lastar ned tilfluktsromdata&#8230;</string>
<string name="loading_map">Lagrar kartfliser&#8230;</string>
<string name="loading_map_explanation">Førebur fråkopla kart.\nKartet vil rulle kort for å lagre omgjevnadene dine.</string>
<string name="loading_first_time">Gjer klar for fyrste gongs bruk&#8230;</string>
<!-- Tilfluktsrominfo -->
@ -25,6 +26,11 @@
<!-- Handlingar -->
<string name="action_refresh">Oppdater data</string>
<string name="action_toggle_view">Byt mellom kart og kompassvising</string>
<string name="action_skip">Hopp over</string>
<string name="action_cache_ok">Lagre kart</string>
<string name="action_cache_now">Lagre no</string>
<string name="action_reset_navigation">Tilbakestill navigasjonsvising</string>
<string name="warning_no_map_cache">Ingen fråkopla kart lagra. Kartet krev internett.</string>
<!-- Løyve -->
<string name="permission_location_title">Posisjonsløyve krevst</string>

View file

@ -15,6 +15,9 @@
<color name="text_primary">#ECEFF1</color>
<color name="text_secondary">#90A4AE</color>
<color name="warning_bg">#E65100</color>
<color name="warning_text">#FFFFFF</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
</resources>

View file

@ -14,6 +14,7 @@
<!-- Loading screen -->
<string name="loading_shelters">Downloading shelter data&#8230;</string>
<string name="loading_map">Caching map tiles&#8230;</string>
<string name="loading_map_explanation">Preparing offline map.\nThe map will scroll briefly to cache your surroundings.</string>
<string name="loading_first_time">Setting up for first use&#8230;</string>
<!-- Shelter info -->
@ -25,6 +26,11 @@
<!-- Actions -->
<string name="action_refresh">Refresh data</string>
<string name="action_toggle_view">Toggle map/compass view</string>
<string name="action_skip">Skip</string>
<string name="action_cache_ok">Cache map</string>
<string name="action_cache_now">Cache now</string>
<string name="action_reset_navigation">Reset navigation view</string>
<string name="warning_no_map_cache">No offline map cached. Map requires internet.</string>
<!-- Permissions -->
<string name="permission_location_title">Location permission required</string>