mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
Add geohash globe render quality selector
This commit is contained in:
parent
5bde4e0b4a
commit
8602db88de
@ -15,6 +15,9 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
@ -34,6 +37,8 @@ import com.bitchat.android.geohash.Geohash
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.ui.globe.GlobeColors
|
||||
import com.bitchat.android.ui.globe.GlobeRenderQuality
|
||||
import com.bitchat.android.ui.globe.GlobeRenderQualityPreference
|
||||
import com.bitchat.android.ui.globe.GlobeState
|
||||
import com.bitchat.android.ui.globe.GlobeView
|
||||
import com.bitchat.android.ui.globe.LandData
|
||||
@ -152,6 +157,9 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
|
||||
val labelTypeface = remember { ResourcesCompat.getFont(context, R.font.geist_mono_medium) }
|
||||
val labelTypefaceBold = remember { ResourcesCompat.getFont(context, R.font.geist_mono_semibold) }
|
||||
var renderQuality by remember {
|
||||
mutableStateOf(GlobeRenderQualityPreference.load(context))
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
@ -165,6 +173,7 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
land = rings,
|
||||
borders = borders,
|
||||
cities = cities,
|
||||
renderQuality = renderQuality,
|
||||
labelTypeface = labelTypeface,
|
||||
labelTypefaceBold = labelTypefaceBold,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@ -183,15 +192,45 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
tonalElevation = 3.dp,
|
||||
shadowElevation = 6.dp
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.pan_zoom_instruction),
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.pan_zoom_instruction),
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
val qualities = GlobeRenderQuality.entries
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
qualities.forEachIndexed { index, quality ->
|
||||
SegmentedButton(
|
||||
selected = renderQuality == quality,
|
||||
onClick = {
|
||||
renderQuality = quality
|
||||
GlobeRenderQualityPreference.save(context, quality)
|
||||
},
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = qualities.size
|
||||
),
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(quality.labelResource),
|
||||
fontSize = 11.sp,
|
||||
fontFamily = BitchatFontFamily
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Floating bottom controls
|
||||
@ -290,6 +329,13 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val GlobeRenderQuality.labelResource: Int
|
||||
get() = when (this) {
|
||||
GlobeRenderQuality.FAST -> R.string.globe_render_quality_fast
|
||||
GlobeRenderQuality.MEDIUM -> R.string.globe_render_quality_medium
|
||||
GlobeRenderQuality.HIGH -> R.string.globe_render_quality_high
|
||||
}
|
||||
|
||||
private fun levelForLength(length: Int): GeohashChannelLevel {
|
||||
return when (length) {
|
||||
in 0..2 -> GeohashChannelLevel.REGION
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
package com.bitchat.android.ui.globe
|
||||
|
||||
import android.content.Context
|
||||
|
||||
enum class GlobeRenderQuality {
|
||||
FAST,
|
||||
MEDIUM,
|
||||
HIGH;
|
||||
|
||||
companion object {
|
||||
fun fromStoredValue(value: String?): GlobeRenderQuality =
|
||||
entries.firstOrNull { it.name == value } ?: MEDIUM
|
||||
}
|
||||
}
|
||||
|
||||
object GlobeRenderQualityPreference {
|
||||
private const val PREFERENCES_NAME = "bitchat_settings"
|
||||
private const val KEY_RENDER_QUALITY = "geohash_globe_render_quality"
|
||||
|
||||
fun load(context: Context): GlobeRenderQuality {
|
||||
val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
return GlobeRenderQuality.fromStoredValue(
|
||||
preferences.getString(KEY_RENDER_QUALITY, GlobeRenderQuality.MEDIUM.name)
|
||||
)
|
||||
}
|
||||
|
||||
fun save(context: Context, quality: GlobeRenderQuality) {
|
||||
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(KEY_RENDER_QUALITY, quality.name)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
@ -64,6 +64,54 @@ data class GlobeColors(
|
||||
|
||||
private class Star(val x: Float, val y: Float, val radius: Float, val alpha: Float)
|
||||
|
||||
internal data class GlobeFrameDetail(
|
||||
val graticuleStepDegrees: Double,
|
||||
val landPointStride: Int,
|
||||
val showBorders: Boolean,
|
||||
val cityMaxRank: Int?,
|
||||
val showCityLabels: Boolean,
|
||||
val showGeohashGrid: Boolean,
|
||||
val showNeighborCells: Boolean
|
||||
)
|
||||
|
||||
internal fun globeFrameDetail(
|
||||
quality: GlobeRenderQuality,
|
||||
isMoving: Boolean
|
||||
): GlobeFrameDetail {
|
||||
if (!isMoving || quality == GlobeRenderQuality.HIGH) {
|
||||
return GlobeFrameDetail(
|
||||
graticuleStepDegrees = 4.0,
|
||||
landPointStride = 1,
|
||||
showBorders = true,
|
||||
cityMaxRank = null,
|
||||
showCityLabels = true,
|
||||
showGeohashGrid = true,
|
||||
showNeighborCells = true
|
||||
)
|
||||
}
|
||||
return when (quality) {
|
||||
GlobeRenderQuality.FAST -> GlobeFrameDetail(
|
||||
graticuleStepDegrees = 10.0,
|
||||
landPointStride = 2,
|
||||
showBorders = false,
|
||||
cityMaxRank = -1,
|
||||
showCityLabels = false,
|
||||
showGeohashGrid = false,
|
||||
showNeighborCells = false
|
||||
)
|
||||
GlobeRenderQuality.MEDIUM -> GlobeFrameDetail(
|
||||
graticuleStepDegrees = 8.0,
|
||||
landPointStride = 2,
|
||||
showBorders = true,
|
||||
cityMaxRank = 1,
|
||||
showCityLabels = false,
|
||||
showGeohashGrid = true,
|
||||
showNeighborCells = false
|
||||
)
|
||||
GlobeRenderQuality.HIGH -> error("Handled above")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlobeView(
|
||||
state: GlobeState,
|
||||
@ -71,6 +119,7 @@ fun GlobeView(
|
||||
land: List<LandData.Ring>,
|
||||
borders: List<LandData.Ring>,
|
||||
cities: List<LandData.City>,
|
||||
renderQuality: GlobeRenderQuality,
|
||||
labelTypeface: Typeface?,
|
||||
labelTypefaceBold: Typeface?,
|
||||
modifier: Modifier = Modifier
|
||||
@ -275,12 +324,12 @@ fun GlobeView(
|
||||
|
||||
val clip = ClipRect(-size.width, -size.height, size.width * 2f, size.height * 2f)
|
||||
|
||||
val lowDetail = state.isInMotion
|
||||
val frameDetail = globeFrameDetail(renderQuality, state.isInMotion)
|
||||
|
||||
// Graticule
|
||||
drawGraticule(
|
||||
cx, cy, r, cLat, cLon, colors.graticule, clip,
|
||||
step = if (lowDetail) 10.0 else 4.0
|
||||
step = frameDetail.graticuleStepDegrees
|
||||
)
|
||||
|
||||
// Landmasses
|
||||
@ -294,12 +343,12 @@ fun GlobeView(
|
||||
r = r,
|
||||
colors = colors,
|
||||
clip = clip,
|
||||
pointStride = if (lowDetail && ring.size >= 64) 2 else 1
|
||||
pointStride = if (ring.size >= 64) frameDetail.landPointStride else 1
|
||||
)
|
||||
}
|
||||
|
||||
// Country borders are restored when interaction settles.
|
||||
if (!lowDetail) {
|
||||
if (frameDetail.showBorders) {
|
||||
for (line in borders) {
|
||||
drawBorderLine(line, borderScratch, preparedProjector, cx, cy, r, colors, clip)
|
||||
}
|
||||
@ -331,20 +380,27 @@ fun GlobeView(
|
||||
)
|
||||
|
||||
// Cities are detail-only; omitting them while moving keeps touch latency predictable.
|
||||
if (!lowDetail) {
|
||||
val cityMaxRank = frameDetail.cityMaxRank
|
||||
if (cityMaxRank == null || cityMaxRank >= 0) {
|
||||
drawCities(
|
||||
cities, state, preparedProjector, cx, cy, r, colors,
|
||||
labelPaint, haloPaint, labelTypeface, labelTextSizeSmall, density.density
|
||||
labelPaint, haloPaint, labelTypeface, labelTextSizeSmall, density.density,
|
||||
maxRankOverride = cityMaxRank,
|
||||
showLabels = frameDetail.showCityLabels
|
||||
)
|
||||
}
|
||||
|
||||
// Detailed cells and labels settle into place after the gesture ends.
|
||||
if (!lowDetail && state.selectedGeohash.isNotEmpty()) {
|
||||
drawGeohashGrid(state, cx, cy, r, cLat, cLon, colors, clip)
|
||||
if (frameDetail.showGeohashGrid && state.selectedGeohash.isNotEmpty()) {
|
||||
drawGeohashGrid(
|
||||
state, cx, cy, r, cLat, cLon, colors, clip,
|
||||
includeNeighbors = frameDetail.showNeighborCells
|
||||
)
|
||||
drawGeohashLabels(
|
||||
state, cx, cy, r, cLat, cLon, colors,
|
||||
labelPaint, haloPaint, labelTypeface, labelTypefaceBold,
|
||||
labelTextSize, labelTextSizeSmall
|
||||
labelTextSize, labelTextSizeSmall,
|
||||
includeNeighbors = frameDetail.showNeighborCells
|
||||
)
|
||||
}
|
||||
|
||||
@ -697,12 +753,14 @@ private fun DrawScope.drawCities(
|
||||
haloPaint: Paint,
|
||||
typeface: Typeface?,
|
||||
textSize: Float,
|
||||
density: Float
|
||||
density: Float,
|
||||
maxRankOverride: Int?,
|
||||
showLabels: Boolean
|
||||
) {
|
||||
if (cities.isEmpty()) return
|
||||
val projection = FloatArray(3)
|
||||
val zoom = state.zoom
|
||||
val maxRank = when {
|
||||
val maxRank = maxRankOverride ?: when {
|
||||
zoom < 2f -> 1
|
||||
zoom < 8f -> 3
|
||||
zoom < 40f -> 4
|
||||
@ -725,7 +783,7 @@ private fun DrawScope.drawCities(
|
||||
else colors.label.copy(alpha = alpha * 0.85f)
|
||||
drawCircle(dotColor, radius = dotRadius, center = Offset(sx, sy))
|
||||
|
||||
if (zoom >= 6f || (important && zoom >= 2.5f)) {
|
||||
if (showLabels && (zoom >= 6f || (important && zoom >= 2.5f))) {
|
||||
labelPaint.textSize = textSize
|
||||
labelPaint.typeface = typeface
|
||||
labelPaint.textAlign = Paint.Align.LEFT
|
||||
@ -789,11 +847,12 @@ private fun DrawScope.drawGeohashGrid(
|
||||
cx: Float, cy: Float, r: Float,
|
||||
cLat: Double, cLon: Double,
|
||||
colors: GlobeColors,
|
||||
clip: ClipRect
|
||||
clip: ClipRect,
|
||||
includeNeighbors: Boolean
|
||||
) {
|
||||
val selected = state.selectedGeohash
|
||||
val cells = linkedSetOf(selected)
|
||||
cells.addAll(Geohash.neighborsSamePrecision(selected))
|
||||
if (includeNeighbors) cells.addAll(Geohash.neighborsSamePrecision(selected))
|
||||
|
||||
for (cell in cells) {
|
||||
val isSelected = cell == selected
|
||||
@ -856,11 +915,12 @@ private fun DrawScope.drawGeohashLabels(
|
||||
labelTypeface: Typeface?,
|
||||
labelTypefaceBold: Typeface?,
|
||||
selectedSize: Float,
|
||||
neighborSize: Float
|
||||
neighborSize: Float,
|
||||
includeNeighbors: Boolean
|
||||
) {
|
||||
val selected = state.selectedGeohash
|
||||
val cells = linkedSetOf(selected)
|
||||
cells.addAll(Geohash.neighborsSamePrecision(selected))
|
||||
if (includeNeighbors) cells.addAll(Geohash.neighborsSamePrecision(selected))
|
||||
val canvas = drawContext.canvas.nativeCanvas
|
||||
|
||||
for (cell in cells) {
|
||||
|
||||
@ -540,6 +540,9 @@
|
||||
<string name="nobody_around">Nobody around…</string>
|
||||
<string name="you_suffix"> (you)</string>
|
||||
<string name="pan_zoom_instruction">Drag to spin · Pinch to zoom · Tap to focus</string>
|
||||
<string name="globe_render_quality_fast">Fast</string>
|
||||
<string name="globe_render_quality_medium">Medium</string>
|
||||
<string name="globe_render_quality_high">High</string>
|
||||
<string name="select">Select</string>
|
||||
<string name="type_a_message_placeholder">Type a message…</string>
|
||||
<string name="mention_suggestion_at">@%1$s</string>
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
package com.bitchat.android.ui.globe
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GlobeRenderQualityTest {
|
||||
|
||||
@Test
|
||||
fun invalidStoredValue_defaultsToMedium() {
|
||||
assertEquals(GlobeRenderQuality.MEDIUM, GlobeRenderQuality.fromStoredValue(null))
|
||||
assertEquals(GlobeRenderQuality.MEDIUM, GlobeRenderQuality.fromStoredValue("UNKNOWN"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stationaryFrame_alwaysUsesFullDetail() {
|
||||
GlobeRenderQuality.entries.forEach { quality ->
|
||||
val detail = globeFrameDetail(quality, isMoving = false)
|
||||
|
||||
assertEquals(1, detail.landPointStride)
|
||||
assertTrue(detail.showBorders)
|
||||
assertNull(detail.cityMaxRank)
|
||||
assertTrue(detail.showCityLabels)
|
||||
assertTrue(detail.showGeohashGrid)
|
||||
assertTrue(detail.showNeighborCells)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingFastFrame_usesMinimumDetail() {
|
||||
val detail = globeFrameDetail(GlobeRenderQuality.FAST, isMoving = true)
|
||||
|
||||
assertEquals(2, detail.landPointStride)
|
||||
assertFalse(detail.showBorders)
|
||||
assertEquals(-1, detail.cityMaxRank)
|
||||
assertFalse(detail.showGeohashGrid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingMediumFrame_preservesOrientationAndSelection() {
|
||||
val detail = globeFrameDetail(GlobeRenderQuality.MEDIUM, isMoving = true)
|
||||
|
||||
assertEquals(2, detail.landPointStride)
|
||||
assertTrue(detail.showBorders)
|
||||
assertEquals(1, detail.cityMaxRank)
|
||||
assertFalse(detail.showCityLabels)
|
||||
assertTrue(detail.showGeohashGrid)
|
||||
assertFalse(detail.showNeighborCells)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingHighFrame_usesFullDetail() {
|
||||
val detail = globeFrameDetail(GlobeRenderQuality.HIGH, isMoving = true)
|
||||
|
||||
assertEquals(1, detail.landPointStride)
|
||||
assertTrue(detail.showBorders)
|
||||
assertNull(detail.cityMaxRank)
|
||||
assertTrue(detail.showCityLabels)
|
||||
assertTrue(detail.showNeighborCells)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user