Skip to content

Commit

Permalink
Merge pull request #162 from mjaakko/map_improvements
Browse files Browse the repository at this point in the history
Map improvements
  • Loading branch information
mjaakko committed Jul 23, 2024
2 parents 75d6a56 + da33539 commit 9df6005
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import org.osmdroid.config.Configuration
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
import org.osmdroid.events.ZoomEvent
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
Expand All @@ -41,11 +44,8 @@ import xyz.malkki.neostumbler.extensions.checkMissingPermissions
import xyz.malkki.neostumbler.ui.viewmodel.MapViewModel
import kotlin.math.roundToInt

//Number of reports needed for max heat color
private const val MAX_HEAT = 15

private val HEAT_LOW = ColorUtils.setAlphaComponent(0xd278ff, 150)
private val HEAT_HIGH = ColorUtils.setAlphaComponent(0xaa00ff, 150)
private val HEAT_LOW = ColorUtils.setAlphaComponent(0xd278ff, 120)
private val HEAT_HIGH = ColorUtils.setAlphaComponent(0xaa00ff, 120)

/**
* Sets map to the specified location if the map has not been moved yet
Expand Down Expand Up @@ -124,6 +124,27 @@ fun ReportMap(mapViewModel: MapViewModel = viewModel()) {
false
}

map.addMapListener(object : MapListener {
override fun onScroll(event: ScrollEvent): Boolean {
mapViewModel.setMapCenter(event.source.mapCenter)

return false
}

override fun onZoom(event: ZoomEvent): Boolean {
mapViewModel.setZoom(event.source.zoomLevelDouble)

return false
}
})

mapViewModel.mapCenter.value?.let {
map.controller.setCenter(it)
}
mapViewModel.zoom.value?.let {
map.controller.setZoom(it)
}

if (latestPosition.value != null) {
map.setPositionIfNotMoved(latestPosition.value!!)
} else {
Expand Down Expand Up @@ -167,9 +188,7 @@ fun ReportMap(mapViewModel: MapViewModel = viewModel()) {

heatMapTiles.value
.map {
val heatPct = (it.heat.toFloat() / MAX_HEAT).coerceAtMost(1.0f)

val color = ColorUtils.blendARGB(HEAT_LOW, HEAT_HIGH, heatPct)
val color = ColorUtils.blendARGB(HEAT_LOW, HEAT_HIGH, it.heatPct)

val polygon = Polygon(view)
polygon.fillPaint.color = color
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,92 @@ package xyz.malkki.neostumbler.ui.viewmodel
import android.Manifest
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import org.geohex.geohex4j.GeoHex
import org.osmdroid.api.IGeoPoint
import org.osmdroid.util.GeoPoint
import xyz.malkki.neostumbler.StumblerApplication
import xyz.malkki.neostumbler.extensions.checkMissingPermissions
import xyz.malkki.neostumbler.extensions.parallelMap
import xyz.malkki.neostumbler.location.LocationSourceProvider
import kotlin.time.Duration.Companion.seconds

//The "size" of one report relative to the geohex size. The idea is that hexes with lower resolution need more reports to show the same color
private const val REPORT_SIZE = 4

private const val GEOHEX_RESOLUTION_HIGH = 9
private const val GEOHEX_RESOLUTION_MEDIUM = 8
private const val GEOHEX_RESOLUTION_LOW = 7

class MapViewModel(application: Application) : AndroidViewModel(application) {
private val locationSource = LocationSourceProvider(getApplication()).getLocationSource()

private val db = getApplication<StumblerApplication>().reportDb

private val showMyLocation = MutableLiveData(getApplication<StumblerApplication>().checkMissingPermissions(Manifest.permission.ACCESS_COARSE_LOCATION).isEmpty())

private val _mapCenter = MutableLiveData<IGeoPoint>(GeoPoint(0.0, 0.0))
val mapCenter: LiveData<IGeoPoint>
get() = _mapCenter

private val _zoom = MutableLiveData(5.0)
val zoom: LiveData<Double>
get() = _zoom

val latestReportPosition = liveData {
emit(db.positionDao().getLatestPosition())
}

val heatMapTiles = db.reportDao().getAllReportsWithLocation()
.distinctUntilChanged()
.map { reportsWithLocation ->
.combine(zoom.asFlow()
.map { zoom ->
if (zoom >= 13.5) {
GEOHEX_RESOLUTION_HIGH
} else if (zoom >= 11.5) {
GEOHEX_RESOLUTION_MEDIUM
} else {
GEOHEX_RESOLUTION_LOW
}
}
.distinctUntilChanged()
) { a, b -> a to b }
.map { (reportsWithLocation, resolution) ->
reportsWithLocation
.asFlow()
.parallelMap { reportWithLocation ->
GeoHex.encode(reportWithLocation.latitude, reportWithLocation.longitude, 9)
GeoHex.encode(reportWithLocation.latitude, reportWithLocation.longitude, resolution)
}
.toList()
.groupingBy { it }
.eachCount()
.map {
val zone = GeoHex.getZoneByCode(it.key)

HeatMapTile(
GeoHex.getZoneByCode(it.key).hexCoords.map { coord ->
zone.hexCoords.map { coord ->
GeoPoint(coord.lat, coord.lon)
},
it.value
((it.value * REPORT_SIZE) / zone.hexSize).coerceAtMost(1.0).toFloat()
)
}
}
.flowOn(Dispatchers.Default)
.asLiveData()

private val showMyLocation = MutableLiveData(getApplication<StumblerApplication>().checkMissingPermissions(Manifest.permission.ACCESS_COARSE_LOCATION).isEmpty())

val myLocation = showMyLocation
.asFlow()
.distinctUntilChanged()
Expand All @@ -72,5 +104,12 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
showMyLocation.postValue(value)
}

data class HeatMapTile(val outline: List<GeoPoint>, val heat: Int)
fun setMapCenter(mapCenter: IGeoPoint) = this._mapCenter.postValue(mapCenter)

fun setZoom(zoom: Double) = this._zoom.postValue(zoom)

/**
* @property heatPct From 0.0 to 1.0
*/
data class HeatMapTile(val outline: List<GeoPoint>, val heatPct: Float)
}

0 comments on commit 9df6005

Please sign in to comment.