From 2c9a5b376f0ac0d4738c0208843f7ed0885def90 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Mon, 7 Sep 2026 15:06:33 +0800 Subject: [PATCH] feat: release-notes body in update checker; grid mode centers on station grid on every entry; own grid square drawn with thick outline; low-zoom meridian vanishing while panning fixed --- .../core/data/repository/UpdateRepository.kt | 10 ++++- .../feature/map/MaidenheadGridOverlay.kt | 39 ++++++++++++++++++- .../look4sat/feature/map/MapScreen.kt | 20 ++++++++-- gradle/libs.versions.toml | 4 +- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/UpdateRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/UpdateRepository.kt index 18e6dfb1..bb0d969b 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/UpdateRepository.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/UpdateRepository.kt @@ -58,6 +58,14 @@ class UpdateRepository( val title = Regex("(.*?)", RegexOption.DOT_MATCHES_ALL) .find(html)?.groupValues?.get(1)?.trim() ?.substringBefore("ยท")?.removePrefix("Release")?.trim() ?: tag + // Release notes live in the page's markdown-body section (first occurrence + // is the release description). Strip HTML tags for plain-text display. + val raw = Regex("]*class=\"[^\"]*markdown-body[^\"]*\"[^>]*>(.*?)", RegexOption.DOT_MATCHES_ALL) + .find(html)?.groupValues?.get(1) ?: "" + val body = raw.replace(Regex("<[^>]+>"), "") + .replace("<", "<").replace(">", ">").replace("&", "&") + .replace(""", "\"").replace("'", "'") + .trim() // The release APK asset follows the fixed naming scheme used by the build: // Look4Sat--release.apk. When the page was fetched // through an accelerator mirror (https:///https://github.com/...), @@ -72,7 +80,7 @@ class UpdateRepository( return LatestRelease( versionTag = tag, title = title, - body = "", + body = body, apkUrl = apkUrl ) } diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt index d0616d61..9cf7eddf 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt @@ -56,6 +56,11 @@ class MaidenheadGridOverlay : Overlay() { style = android.graphics.Paint.Style.FILL color = Color.argb(90, 76, 217, 100) } + private val ownLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + strokeWidth = 4.5f + style = Paint.Style.STROKE + color = Color.argb(255, 90, 200, 255) + } /** Worked gridsquares (4-char, uppercase) to highlight, e.g. {"OL62", "PM95"}. */ var workedGrids: Set = emptySet() @@ -63,10 +68,14 @@ class MaidenheadGridOverlay : Overlay() { /** The station's own 4-char gridsquare, drawn with a distinct outline. */ var ownGrid: String? = null + /** Viewport width, refreshed each draw; used by projectionToX bounds. */ + private var canvasWidthPx = 1080f + override fun draw(canvas: Canvas, mapView: MapView, shadow: Boolean) { if (shadow || !isEnabled) return val projection = mapView.projection val zoom = mapView.zoomLevelDouble + canvasWidthPx = canvas.width.toFloat() val cellLat = if (zoom >= GRID_ZOOM_SUB) SUB_SQUARE_LAT else FIELD_LAT val cellLon = if (zoom >= GRID_ZOOM_SUB) SUB_SQUARE_LON else FIELD_LON @@ -86,6 +95,9 @@ class MaidenheadGridOverlay : Overlay() { val lastCol = ceil(rightLon / cellLon).toInt() val centerLon = (leftLon + rightLon) / 2.0 + // The station's own grid square (4-char, only meaningful at sub-square zoom) + val ownCell = ownGrid?.takeIf { zoom >= GRID_ZOOM_SUB } + // Worked-grid highlight fills (only meaningful at sub-square zoom) if (workedGrids.isNotEmpty() && zoom >= GRID_ZOOM_SUB) { for (row in firstRow..lastRow) { @@ -124,6 +136,27 @@ class MaidenheadGridOverlay : Overlay() { canvas.drawLine(0f, y, canvas.width.toFloat(), y, linePaint) } + // The station's own grid square: redraw its four borders thicker on top. + if (ownCell != null) { + val ownColIdx = firstCol..lastCol + for (row in firstRow..lastRow) { + val lat = row * cellLat + if (lat < -90.0 || lat >= 90.0) continue + for (col in ownColIdx) { + val lon = col * cellLon + if (cellLabel(lat, lon, zoom) != ownCell) continue + val yTop = projectionToY(projection, lat + cellLat) ?: continue + val yBottom = projectionToY(projection, lat) ?: continue + val xLeft = projectionToX(projection, lon, centerLon) ?: continue + val xRight = projectionToX(projection, lon + cellLon, centerLon) ?: continue + canvas.drawLine(xLeft, yTop, xRight, yTop, ownLinePaint) + canvas.drawLine(xLeft, yBottom, xRight, yBottom, ownLinePaint) + canvas.drawLine(xLeft, yTop, xLeft, yBottom, ownLinePaint) + canvas.drawLine(xRight, yTop, xRight, yBottom, ownLinePaint) + } + } + } + // Labels: centered in each cell, only when the cell is large enough on // screen to hold a label (avoid clutter at low zoom) val showLabels = when { @@ -175,7 +208,11 @@ class MaidenheadGridOverlay : Overlay() { while (normalized > centerLon + 180.0) normalized -= 360.0 val geo = org.osmdroid.util.GeoPoint(0.0, normalized) val p = projection.toPixels(geo, null) - return if (p.x in -MAX_OVERSHOOT_PX..MAX_OVERSHOOT_PX + 4096) p.x.toFloat() else null + // Accept generous horizontal overshoot: at low zoom the world repeats and + // a meridian just off-screen may still be projected; rejecting too early + // makes lines vanish while panning. 2x viewport width is plenty. + val limit = canvasWidthPx * 2f + MAX_OVERSHOOT_PX + return if (p.x >= -limit && p.x <= limit) p.x.toFloat() else null } /** Y pixel for a latitude, or null when outside the viewport. */ diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt index 18e0328f..8d86c6ed 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt @@ -161,9 +161,9 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView: } ElevatedCard(modifier = Modifier.weight(1f)) { Box(contentAlignment = Alignment.BottomCenter) { - // Track grid-mode transitions so the map only re-centers when the - // mode is switched ON, not on every recomposition. - var prevGridMode by remember { mutableStateOf(uiState.isGridMode) } + // Track grid-mode transitions; initial value false so that entering + // the map page with grid mode already ON also centers the map. + var prevGridMode by remember { mutableStateOf(false) } AndroidView({ mapView }) { view -> setGridMode( uiState.isGridMode, uiState.workedGrids, view, @@ -289,10 +289,12 @@ private fun setGridMode( if (gridOverlay is MaidenheadGridOverlay) { gridOverlay.isEnabled = gridMode gridOverlay.workedGrids = workedGrids + gridOverlay.ownGrid = ownGridOf(stationPosition) } else { mapView.overlays[OVERLAY_GRID] = MaidenheadGridOverlay().apply { isEnabled = gridMode this.workedGrids = workedGrids + this.ownGrid = ownGridOf(stationPosition) } } // Satellite-related layers are hidden in grid mode; the grid overlay @@ -317,6 +319,18 @@ private fun setGridMode( } } +/** The station's 4-char Maidenhead gridsquare, e.g. "OL62", or null without a position. */ +private fun ownGridOf(stationPosition: GeoPos?): String? { + val pos = stationPosition ?: return null + val lat = pos.latitude + val lon = pos.longitude + val fieldLat = ((lat + 90.0) / 10.0).toInt().coerceIn(0, 17) + val fieldLon = ((lon + 180.0) / 20.0).toInt().coerceIn(0, 17) + val subLat = ((lat + 90.0) % 10.0).toInt() + val subLon = ((lon + 180.0) % 20.0 / 2.0).toInt() + return "${'A' + fieldLon}${'A' + fieldLat}$subLon$subLat" +} + private fun setStationPosition(stationPos: GeoPos, mapView: MapView) { try { val overlay = mapView.overlays[OVERLAY_STATION] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 354ade80..e9643b29 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] #noinspection UnusedVersionCatalogEntry -appVersionCode = "455" +appVersionCode = "456" #noinspection UnusedVersionCatalogEntry -appVersionName = "4.4.6-ba7opf.9.3" +appVersionName = "4.4.6-ba7opf.9.4" #noinspection UnusedVersionCatalogEntry compileSdk = "37" #noinspection UnusedVersionCatalogEntry