From b9ddb9acf0c52a28d8e3f76ea1a8e4936fda0017 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 5 May 2024 01:17:07 +0200 Subject: [PATCH] Rewrite MapTiles component to allow non-square aspect ratios --- .../java/de/mm20/launcher2/ui/ktx/IntRange.kt | 9 + .../search/common/SearchableItemVM.kt | 2 +- .../launcher/search/location/LocationItem.kt | 58 +- .../ui/launcher/search/location/MapTiles.kt | 532 +++++++----------- 4 files changed, 259 insertions(+), 342 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/ktx/IntRange.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/IntRange.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/IntRange.kt new file mode 100644 index 00000000..1e2cd70e --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/IntRange.kt @@ -0,0 +1,9 @@ +package de.mm20.launcher2.ui.ktx + +inline operator fun IntRange.inc(): IntRange { + return start + 1..endInclusive + 1 +} + +inline operator fun IntRange.dec(): IntRange { + return start - 1..endInclusive - 1 +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt index 8ca044b4..54c31176 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt @@ -223,7 +223,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { permissionsManager.requestPermission(activity, PermissionGroup.AppShortcuts) } - val useInsaneUnits = locationSearchSettings.imperialUnits + val imperialUnits = locationSearchSettings.imperialUnits .stateIn(viewModelScope, SharingStarted.Lazily, false) val showMap = locationSearchSettings.showMap diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt index 6c46c2ed..65e5b7d3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt @@ -33,20 +33,14 @@ import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.TravelExplore import androidx.compose.material.icons.twotone.CloudOff -import androidx.compose.material3.AlertDialog import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -59,6 +53,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.times @@ -105,7 +100,24 @@ fun LocationItem( val userLocation by remember { viewModel.devicePoseProvider.getLocation() }.collectAsStateWithLifecycle(viewModel.devicePoseProvider.lastLocation) - val insaneUnits by viewModel.useInsaneUnits.collectAsState() + + val targetHeading by remember(userLocation, location) { + if (userLocation != null) { + viewModel.devicePoseProvider.getHeadingToDegrees( + userLocation!!.bearingTo( + location.toAndroidLocation() + ) + ) + } else emptyFlow() + }.collectAsStateWithLifecycle(null) + + val userHeading by remember { + if (userLocation != null) { + viewModel.devicePoseProvider.getAzimuthDegrees() + } else emptyFlow() + }.collectAsStateWithLifecycle(null) + + val imperialUnits by viewModel.imperialUnits.collectAsState() val isUpToDate by viewModel.isUpToDate.collectAsState() @@ -209,16 +221,6 @@ fun LocationItem( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceAround, ) { - val targetHeading by remember(userLocation, location) { - if (userLocation != null) - viewModel.devicePoseProvider.getHeadingToDegrees( - userLocation!!.bearingTo( - location.toAndroidLocation() - ) - ) - else - emptyFlow() - }.collectAsStateWithLifecycle(null) if (targetHeading != null) { val directionArrowAngle by animateValueAsState( @@ -234,7 +236,7 @@ fun LocationItem( if (distance != null) { Text( text = distance.metersToLocalizedString( - context, insaneUnits + context, imperialUnits ), style = MaterialTheme.typography.labelSmall ) } @@ -299,7 +301,6 @@ fun LocationItem( .padding(top = 16.dp, bottom = 8.dp) .align(Alignment.CenterHorizontally) .fillMaxWidth(.9125f) - .aspectRatio(1f) .border(1.dp, MaterialTheme.colorScheme.outline, shape) .clip(shape) .clickable { @@ -307,15 +308,18 @@ fun LocationItem( }, tileServerUrl = tileServerUrl, location = location, - initialZoomLevel = zoomLevel, - numberOfTiles = nTiles, + maxZoomLevel = zoomLevel, + tiles = IntSize(3, 2), applyTheming = applyTheming, - userLocation = if (showPositionOnMap) userLocation?.let { - UserLocation( - it.latitude, - it.longitude - ) - } else null, + userLocation = { + if (showPositionOnMap) userLocation?.let { + UserLocation( + it.latitude, + it.longitude, + heading = userHeading, + ) + } else null + }, ) val address = buildAddress(location.street, location.houseNumber) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/MapTiles.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/MapTiles.kt index d5abaadc..2af703d5 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/MapTiles.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/MapTiles.kt @@ -4,53 +4,55 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle -import android.util.Log import androidx.compose.animation.core.EaseInOutCirc import androidx.compose.animation.core.EaseInOutSine import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.animateOffsetAsState +import androidx.compose.animation.core.animateValueAsState import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Navigation +import androidx.compose.material.icons.rounded.Place +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.minus +import androidx.compose.ui.unit.times import coil.ImageLoader import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter import coil.disk.DiskCache import coil.memory.MemoryCache import coil.request.CachePolicy @@ -63,6 +65,7 @@ import de.mm20.launcher2.search.OpeningHours import de.mm20.launcher2.search.OpeningSchedule import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchableSerializer +import de.mm20.launcher2.ui.ktx.DegreesConverter import de.mm20.launcher2.ui.ktx.contrast import de.mm20.launcher2.ui.ktx.hue import de.mm20.launcher2.ui.ktx.hueRotate @@ -74,54 +77,39 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koin.core.context.GlobalContext import org.koin.core.context.startKoin -import kotlin.math.asinh +import kotlin.math.PI import kotlin.math.ceil +import kotlin.math.cos import kotlin.math.floor -import kotlin.math.max -import kotlin.math.min -import kotlin.math.sqrt +import kotlin.math.ln +import kotlin.math.pow import kotlin.math.tan -data class UserLocation(val lat: Double, val lon: Double) +data class UserLocation(val lat: Double, val lon: Double, val heading: Float? = null) @Composable fun MapTiles( tileServerUrl: String, location: Location, - initialZoomLevel: Int, - numberOfTiles: Int, - userLocation: UserLocation?, + userLocation: () -> UserLocation?, + maxZoomLevel: Int, + tiles: IntSize, applyTheming: Boolean, modifier: Modifier = Modifier, // https://wiki.openstreetmap.org/wiki/Attribution_guidelines/2021-06-04_draft#Attribution_text osmAttribution: String? = "© OpenStreetMap", ) { - val context = LocalContext.current val tintColor = MaterialTheme.colorScheme.surface val darkMode = LocalDarkTheme.current - val previousZoomLevel = remember { mutableIntStateOf(-1) } - val (start, stop, zoom) = remember(userLocation) { - userLocation - ?.runCatching { - getEnclosingTiles( - location, - numberOfTiles, - this, - previousZoomLevel - ) - } - ?.onFailure { - Log.e("MapTiles", "Enclosing calculation failed", it) - } - ?.getOrNull() - ?: getTilesAround(location, initialZoomLevel, numberOfTiles) + val userLocation = userLocation() + + val (start, stop, zoom) = remember(location, userLocation) { + getTileRange(location, userLocation, tiles, maxZoomLevel) } val sideLength = stop.x - start.x + 1 - val imageStates = remember { (0 until numberOfTiles).map { false }.toMutableStateList() } - val colorMatrix = remember(applyTheming, darkMode, tintColor) { // darkreader css for openstreetmap tiles // invert(93.7%) hue-rotate(180deg) contrast(90.6%) @@ -138,344 +126,259 @@ fun MapTiles( } else null } - Box( + BoxWithConstraints( modifier = modifier, contentAlignment = Alignment.Center, ) { - Column(modifier = Modifier.matchParentSize()) { + Column(modifier = Modifier.fillMaxWidth()) { for (y in start.y..stop.y) { Row( modifier = Modifier - .weight(1f / sideLength) - .fillMaxSize() + .fillMaxWidth() ) { for (x in start.x..stop.x) { AsyncImage( modifier = Modifier .weight(1f / sideLength) - .fillMaxSize(), + .aspectRatio(1f) + .background(MaterialTheme.colorScheme.secondaryContainer), imageLoader = MapTileLoader.loader, model = MapTileLoader.getTileRequest(tileServerUrl, x, y, zoom), contentDescription = null, colorFilter = colorMatrix?.let { ColorFilter.colorMatrix(it) }, filterQuality = FilterQuality.High, - onState = { - val stateIndex = - (y - start.y) * (stop.y - start.y + 1) + (x - start.x) - when (it) { - is AsyncImagePainter.State.Loading -> imageStates[stateIndex] = - false - - is AsyncImagePainter.State.Success -> imageStates[stateIndex] = - true - - is AsyncImagePainter.State.Error -> { - imageStates[stateIndex] = false - Log.e( - "MapTiles", - "Error loading tile: $x, $y @$zoom", - it.result.throwable - ) - } - - else -> {} - } - } ) } } } } - if (imageStates.all { it }) { - val locationBorderColor = - if (applyTheming) MaterialTheme.colorScheme.error else Color(0xFFEFA521) // orange-ish - val userLocationColor = - if (applyTheming) MaterialTheme.colorScheme.onErrorContainer else Color(0xFF35A82C) // darkish green - val userLocationBorderColor = - if (applyTheming) { - MaterialTheme.colorScheme.errorContainer - } else if (darkMode) { - Color(0xFF777777) - } else { - Color(0xFFE5E5E5) - } + val locationBorderColor = + if (applyTheming) MaterialTheme.colorScheme.error else Color(0xFFEFA521) // orange-ish + val userLocationColor = + if (applyTheming) MaterialTheme.colorScheme.onErrorContainer else Color(0xFF35A82C) // darkish green + val userLocationBorderColor = + if (applyTheming) { + MaterialTheme.colorScheme.errorContainer + } else if (darkMode) { + Color(0xFF777777) + } else { + Color(0xFFE5E5E5) + } - val infiniteTransition = rememberInfiniteTransition("infiniteTransition") - val userLocAnimation by infiniteTransition.animateFloat( - initialValue = 0.8f, - targetValue = 1f, - animationSpec = infiniteRepeatable( - animation = tween(2000, easing = EaseInOutSine), - repeatMode = RepeatMode.Reverse - ), - label = "userLocAnimation" + val infiniteTransition = rememberInfiniteTransition("infiniteTransition") + val userLocAnimation by infiniteTransition.animateFloat( + initialValue = 0.8f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(2000, easing = EaseInOutSine), + repeatMode = RepeatMode.Reverse + ), + label = "userLocAnimation" + ) + val poiLocAnimation by infiniteTransition.animateFloat( + initialValue = 30f, + targetValue = 20f, + animationSpec = infiniteRepeatable( + animation = tween(750, easing = EaseInOutCirc), + repeatMode = RepeatMode.Reverse + ), + label = "poiLocAnimation" + ) + + val textMeasurer = rememberTextMeasurer() + val osmAttributionTextStyle = MaterialTheme.typography.labelSmall + val osmAttributionTextColor = MaterialTheme.colorScheme.onSurface + val osmAttributionSurface = + MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = .5f) + + val (yLocation, xLocation) = remember(location, zoom) { + getTileCoordinates( + latitude = location.latitude, + longitude = location.longitude, + zoom ) - val poiLocAnimation by infiniteTransition.animateFloat( - initialValue = 30f, - targetValue = 20f, - animationSpec = infiniteRepeatable( - animation = tween(750, easing = EaseInOutCirc), - repeatMode = RepeatMode.Reverse - ), - label = "poiLocAnimation" - ) - - val textMeasurer = rememberTextMeasurer() - val osmAttributionTextStyle = MaterialTheme.typography.labelSmall - val osmAttributionTextColor = MaterialTheme.colorScheme.onSurface - val osmAttributionSurface = - MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = .5f) - - val (yLocation, xLocation) = remember(location, zoom) { - getDoubleTileCoordinates( - latitude = location.latitude, - longitude = location.longitude, + } + val userOffset = remember(userLocation, zoom) { + if (userLocation != null) { + getTileCoordinates( + latitude = userLocation.lat, + longitude = userLocation.lon, zoom ) + } else { + Offset.Unspecified } - val (yUser, xUser) = remember(userLocation, zoom) { - if (userLocation != null) { - getDoubleTileCoordinates( - latitude = userLocation.lat, - longitude = userLocation.lon, - zoom - ) - } else { - -1.0 to -1.0 - } - } - val animatedUserIndicatorOffset by animateOffsetAsState( - targetValue = (Offset( - xUser.toFloat(), - yUser.toFloat() - ) - start) / sideLength.toFloat(), - animationSpec = tween( - 1000, - 250 + } + val tileSize = minWidth / tiles.width.toFloat() + val locationTileCoordinates = + getTileCoordinates(location.latitude, location.longitude, zoom) + + Box( + modifier = Modifier + .align(Alignment.TopStart) + .width(12.dp) + .height(4.dp) + .absoluteOffset( + x = (locationTileCoordinates.x - start.x) * tileSize - 6.dp, + y = (locationTileCoordinates.y - start.y) * tileSize - 2.dp, ) - ) + .shadow(1.dp, CircleShape) + ) - - Canvas(modifier = Modifier.matchParentSize()) { - assert(size.width == size.height) - - if (userLocation != null) { - if (start.y < yUser && yUser < stop.y + 1 && - start.x < xUser && xUser < stop.x + 1 - ) { - val userIndicatorOffset = animatedUserIndicatorOffset * size.width - drawCircle( - color = userLocationBorderColor, - radius = 18.5f * userLocAnimation, - center = userIndicatorOffset, - alpha = (userLocAnimation - 0.8f) * 5f - ) - drawCircle( - color = userLocationColor, - radius = 13.5f * userLocAnimation, - center = userIndicatorOffset, - ) - } - } - - val locationIndicatorOffset = - (Offset( - xLocation.toFloat(), - yLocation.toFloat() - ) - start) / sideLength.toFloat() * size.width - drawCircle( - color = locationBorderColor, - radius = poiLocAnimation, - center = locationIndicatorOffset, - style = Stroke(width = 4f) + Icon( + imageVector = Icons.Rounded.Place, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier + .align(Alignment.TopStart) + .size(24.dp) + .absoluteOffset( + x = (locationTileCoordinates.x - start.x) * tileSize - 12.dp, + y = (locationTileCoordinates.y - start.y) * tileSize - 22.dp, + ) + ) + if (userLocation != null) { + val userTileCoordinates = getTileCoordinates(userLocation.lat, userLocation.lon, zoom) + + if (userLocation.heading != null) { + val headingAnim by animateValueAsState( + targetValue = userLocation.heading, + typeConverter = Float.DegreesConverter + ) + + Icon( + imageVector = Icons.Rounded.Navigation, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier + .align(Alignment.TopStart) + .size(16.dp) + .absoluteOffset( + x = (userTileCoordinates.x - start.x) * tileSize - 8.dp, + y = (userTileCoordinates.y - start.y) * tileSize - 8.dp, + ) + .rotate(headingAnim) + .absoluteOffset(y = -8.dp) ) - if (osmAttribution != null) { - val measureResult = textMeasurer.measure( - osmAttribution, - maxLines = 1, - style = osmAttributionTextStyle - ) - val osmLabelPadding = 6f - val textOffset = Offset( - x = size.width - measureResult.size.width - osmLabelPadding, - y = size.height - measureResult.size.height - osmLabelPadding - ) - drawRoundRect( - color = osmAttributionSurface, - topLeft = textOffset - Offset(osmLabelPadding, 0f), - size = Size( - width = measureResult.size.width + 2 * osmLabelPadding, - height = measureResult.size.height + osmLabelPadding - ), - cornerRadius = CornerRadius(8f, 8f) - ) - drawText( - measureResult, - color = osmAttributionTextColor, - topLeft = textOffset - ) - } } - } else { - val loadingColor = MaterialTheme.colorScheme.secondary - CircularProgressIndicator( - modifier = Modifier.fillMaxSize(.15f), - color = loadingColor, - strokeCap = StrokeCap.Round, + + Box( + modifier = Modifier + .align(Alignment.TopStart) + .size(16.dp) + .absoluteOffset( + x = (userTileCoordinates.x - start.x) * tileSize - 8.dp, + y = (userTileCoordinates.y - start.y) * tileSize - 8.dp, + ) + .background(MaterialTheme.colorScheme.tertiary, CircleShape) + .border(2.dp, MaterialTheme.colorScheme.onTertiary, CircleShape) + .shadow(1.dp, CircleShape) ) } } } -private fun getDoubleTileCoordinates( +/** + * Returns the tile coordinates for a given location at a given zoom level (not rounded). + */ +private fun getTileCoordinates( latitude: Double, longitude: Double, zoomLevel: Int -): Pair { +): Offset { // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Mathematics - val latRadians = Math.toRadians(latitude) - val xCoordinate = (longitude + 180.0) / 360.0 * (1 shl zoomLevel) - val yCoordinate = (1.0 - asinh(tan(latRadians)) / Math.PI) * (1 shl (zoomLevel - 1)) + val x = ((longitude + 180) / 360) * 2.0.pow(zoomLevel) + val latRad = Math.toRadians(latitude) + val y = (1.0 - ((ln(tan(latRad) + (1.0 / cos(latRad)))) / PI)) * (2.0.pow(zoomLevel - 1)) - return yCoordinate to xCoordinate + return Offset(x.toFloat(), y.toFloat()) } data class TileCoordinateRange(val start: IntOffset, val stop: IntOffset, val zoomLevel: Int) -private fun getTilesAround( - location: Location, - zoomLevel: Int, - nTiles: Int -): TileCoordinateRange { - if (sqrt(nTiles.toDouble()) % 1.0 != 0.0) - throw IllegalArgumentException("nTiles must be a square number") - - val sideLen = sqrt(nTiles.toDouble()).toInt() - val sideLenHalf = sideLen / 2 - - val (yCoordinate, xCoordinate) = getDoubleTileCoordinates( - location.latitude, - location.longitude, - zoomLevel - ) - val xTile = xCoordinate.toInt() - val yTile = yCoordinate.toInt() - - val yStart: Int - val yStop: Int - val xStart: Int - val xStop: Int - - if (sideLen % 2 == 1) { - // center tile is defined - yStart = yTile - sideLenHalf - yStop = yTile + sideLenHalf - xStart = xTile - sideLenHalf - xStop = xTile + sideLenHalf +private fun getTileRange( + center: Float, + tiles: Int, +): IntRange { + val centerRounded = center.toInt() + return if (tiles % 2 == 0) { + if (center % 1 >= 0.5f) { + (centerRounded - (tiles / 2) + 1)..(centerRounded + (tiles / 2)) + } else { + (centerRounded - (tiles / 2))..= 0.5f) { + (minX - diffX / 2)..<(maxX + diffX / 2 + 1) + } else { + (minX - diffX / 2 - 1)..<(maxX + diffX / 2) - if (topOfCenter) { - yStart -= ceil - yStop += floor - } else { - yStart -= floor - yStop += ceil - } } - previousZoomLevel.intValue = zoomLevel + val yRange = if (diffY % 2 == 0) { + (minY - diffY / 2)..<(maxY + diffY / 2) + } else if (centerY % 1 >= 0.5f) { + (minY - diffY / 2)..<(maxY + diffY / 2 + 1) + } else { + (minY - diffY / 2 - 1)..<(maxY + diffY / 2) + } return TileCoordinateRange( - IntOffset(xStart, yStart), - IntOffset(xStop, yStop), - zoomLevel + IntOffset(xRange.first, yRange.first), + IntOffset(xRange.last, yRange.last), + z ) } } - throw IllegalStateException("Unreachable (right?) | lat: ${location.latitude} | lon: ${location.longitude} | user: $userLocation | nTiles: $nTiles") + return TileCoordinateRange(IntOffset(0, 0), IntOffset(0, 0), 0) } +const val ZOOM_MAX = 19 +const val ZOOM_MIN = 0 + private object MapTileLoader : KoinComponent { private val context: Context by inject() @@ -485,6 +388,7 @@ private object MapTileLoader : KoinComponent { 0 )?.versionName ?: "dev" }" + fun getTileRequest(tileServerUrl: String, x: Int, y: Int, zoom: Int): ImageRequest { return ImageRequest.Builder(context) .data("$tileServerUrl/$zoom/$x/$y.png") @@ -538,10 +442,10 @@ private fun MapTilesPreview() { .clip(borderShape), tileServerUrl = "http://tile.openstreetmap.org", location = MockLocation, - initialZoomLevel = 19, - numberOfTiles = 9, + maxZoomLevel = 19, + tiles = IntSize(3, 3), applyTheming = false, - userLocation = UserLocation(52.51623, 13.4048) + userLocation = { UserLocation(52.51623, 13.4048) } ) }