@@ -88,6 +88,7 @@ import f.cking.software.dpToPx
8888import f.cking.software.frameRate
8989import f.cking.software.mapParallel
9090import f.cking.software.pxToDp
91+ import f.cking.software.splitToBatchesEqual
9192import f.cking.software.toLocation
9293import f.cking.software.topLeft
9394import f.cking.software.ui.AsyncBatchProcessor
@@ -132,7 +133,6 @@ import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlayOptions
132133import org.osmdroid.views.overlay.simplefastpoint.SimplePointTheme
133134import timber.log.Timber
134135import java.time.format.FormatStyle
135- import java.util.concurrent.ConcurrentHashMap
136136
137137@OptIn(ExperimentalMaterial3Api ::class )
138138object DeviceDetailsScreen {
@@ -905,12 +905,10 @@ object DeviceDetailsScreen {
905905
906906 fun updateViewport () {
907907 pendingViewport = getViewport()
908- Timber .tag(TAG ).d(" Update viewport" )
909908 }
910909
911910 fun commitViewPort () {
912911 committedViewport = pendingViewport
913- Timber .tag(TAG ).d(" Commit viewport" )
914912 }
915913
916914 MapView (
@@ -1013,7 +1011,7 @@ object DeviceDetailsScreen {
10131011 @Composable
10141012 private fun rememberTilesState () = remember { TilesState () }
10151013 private class TilesState {
1016- var tiles = ConcurrentHashMap <Tile , TilesData >()
1014+ var tiles = HashMap <Tile , TilesData >()
10171015 var lastLocationsState: List <LocationModel > = emptyList()
10181016 }
10191017
@@ -1029,12 +1027,26 @@ object DeviceDetailsScreen {
10291027 withContext(Dispatchers .Default ) {
10301028 val locations = mapUpdate.points.map { it.toLocation() }
10311029 Timber .tag(TAG ).d(" Heatmap points: ${locations.size} " )
1030+ val pointsChanged = tilesState.lastLocationsState.size != locations.size
10321031 val tiles = HeatMapBitmapFactory .buildTilesWithRenderPaddingStable(locations, TILE_SIZE_METERS , PADDING_METERS )
1033- .filter { viewport.intersects(it) }
1034- Timber .tag(TAG ).d(" Heatmap tiles: ${tiles.size} " )
1032+ .asSequence()
1033+ .filter {
1034+ val inViewport = viewport.intersects(it)
1035+
1036+ if (! pointsChanged) {
1037+ val existedTile = tilesState.tiles[it]
1038+ val isAdded = mapUpdate.map.overlays.contains(existedTile?.overlay)
1039+ inViewport && (! tilesState.tiles.containsKey(it) || ! isAdded)
1040+ } else {
1041+ inViewport
1042+ }
1043+ }
1044+ .sortedBy { tilesState.tiles.containsKey(it) }
1045+ .toList()
10351046
1036- if (tilesState.lastLocationsState != mapUpdate.points) {
1047+ Timber .tag( TAG ).d( " Heatmap tiles: ${tiles.size} " )
10371048
1049+ if (pointsChanged) {
10381050 val removedTiles = tilesState.tiles.values.filter { ! tiles.contains(it.tile) }
10391051 removedTiles.forEach { tileData ->
10401052 Timber .tag(TAG ).d(" Tile exists but should be removed" )
@@ -1045,62 +1057,69 @@ object DeviceDetailsScreen {
10451057
10461058 tilesState.lastLocationsState = mapUpdate.points
10471059
1048- val loadingJob = launch {
1060+ val loadingJob = launch( Dispatchers . Main ) {
10491061 delay(30 )
10501062 yield ()
10511063 viewModel.loadingHeatmap = true
10521064 }
1053- tiles.mapParallel { tile ->
1054- val positionsForTile = locations
1055- .mapNotNull {
1056- it.takeIf { tile.contains(it, PADDING_METERS ) }
1057- ?.let { HeatMapBitmapFactory .Position (it, PADDING_METERS .toFloat()) }
1058- }
1059- val existedTile = tilesState.tiles[tile]
1060-
1061- if (existedTile != null && positionsForTile == existedTile.locations) {
1062- // tile is already added and didn't change
1063- Timber .tag(TAG ).d(" Tile already rendered" )
1064- withContext(Dispatchers .IO ) {
1065- if (! mapUpdate.map.overlays.contains(existedTile.overlay)) {
1066- mapUpdate.map.overlays.add(0 , existedTile.overlay)
1065+
1066+ tiles.splitToBatchesEqual(10 ).mapParallel { batch ->
1067+ batch.map { tile ->
1068+ withContext(Dispatchers .Default ) {
1069+
1070+ val positionsForTile = locations
1071+ .mapNotNull {
1072+ it.takeIf { tile.contains(it, PADDING_METERS ) }
1073+ ?.let { HeatMapBitmapFactory .Position (it, PADDING_METERS .toFloat()) }
1074+ }
1075+ val existedTile = tilesState.tiles[tile]
1076+
1077+ if (existedTile != null && positionsForTile == existedTile.locations) {
1078+ // tile is already added and didn't change
1079+ Timber .tag(TAG ).d(" Tile already rendered" )
1080+ withContext(Dispatchers .IO ) {
1081+ if (! mapUpdate.map.overlays.contains(existedTile.overlay)) {
1082+ mapUpdate.map.overlays.add(0 , existedTile.overlay)
1083+ mapUpdate.map.invalidate()
1084+ }
1085+ }
1086+ return @withContext
1087+ } else if (existedTile != null ) {
1088+ // tile is rendered but changed (need to re-render)
1089+ Timber .tag(TAG ).d(" Tile exists but changed" )
1090+ mapUpdate.map.overlays.remove(existedTile.overlay)
1091+ tilesState.tiles.remove(tile)
1092+ }
1093+ Timber .tag(TAG ).d(" Rendering tile with ${positionsForTile.size} points" )
1094+ val bitmap = HeatMapBitmapFactory .generateTileGradientBitmapFastSeamless(
1095+ positionsAll = positionsForTile,
1096+ coreTile = tile,
1097+ widthPxCore = 300 ,
1098+ renderPaddingMeters = PADDING_METERS ,
1099+ debugBorderPx = 0 ,
1100+ )
1101+
1102+ yield ()
1103+ val heatmapOverlay = GroundOverlay ()
1104+ heatmapOverlay.setImage(bitmap)
1105+ heatmapOverlay.transparency = 0.3f
1106+ heatmapOverlay.setPosition(tile.topLeft.toGeoPoint(), tile.bottomRight.toGeoPoint())
1107+ withContext(Dispatchers .Main ) {
1108+ mapUpdate.map.overlays.add(0 , heatmapOverlay)
10671109 mapUpdate.map.invalidate()
10681110 }
1111+ tilesState.tiles[tile] = TilesData (tile, heatmapOverlay, positionsForTile)
10691112 }
1070- return @mapParallel
1071- } else if (existedTile != null ) {
1072- // tile is rendered but changed (need to re-render)
1073- Timber .tag(TAG ).d(" Tile exists but changed" )
1074- mapUpdate.map.overlays.remove(existedTile.overlay)
1075- tilesState.tiles.remove(tile)
1076- }
1077- Timber .tag(TAG ).d(" Rendering tile with ${positionsForTile.size} points" )
1078- val bitmap = HeatMapBitmapFactory .generateTileGradientBitmapFastSeamless(
1079- positionsAll = positionsForTile,
1080- coreTile = tile,
1081- widthPxCore = 300 ,
1082- renderPaddingMeters = PADDING_METERS ,
1083- debugBorderPx = 0 ,
1084- )
1085-
1086- yield ()
1087- val heatmapOverlay = GroundOverlay ()
1088- heatmapOverlay.setImage(bitmap)
1089- heatmapOverlay.setPosition(tile.topLeft.toGeoPoint(), tile.bottomRight.toGeoPoint())
1090- withContext(Dispatchers .Main ) {
1091- mapUpdate.map.overlays.add(0 , heatmapOverlay)
1092- mapUpdate.map.invalidate()
10931113 }
1094- tilesState.tiles[tile] = TilesData (tile, heatmapOverlay, positionsForTile)
10951114 }
10961115
10971116 Timber .tag(TAG ).d(" All tiles rendered" )
10981117 loadingJob.cancel()
1099- viewModel.loadingHeatmap = false
11001118 }
11011119 } else {
11021120 mapUpdate.map.overlays.removeAll { it is GroundOverlay }
11031121 }
1122+ viewModel.loadingHeatmap = false
11041123 mapUpdate.map.invalidate()
11051124 }
11061125
0 commit comments