diff options
Diffstat (limited to 'android-app/app/src/main')
5 files changed, 78 insertions, 49 deletions
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/storage/GribFileManager.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/storage/GribFileManager.kt new file mode 100644 index 0000000..b336818 --- /dev/null +++ b/android-app/app/src/main/kotlin/com/example/androidapp/data/storage/GribFileManager.kt @@ -0,0 +1,24 @@ +package com.example.androidapp.data.storage + +import com.example.androidapp.data.model.GribFile +import com.example.androidapp.data.model.GribRegion +import java.time.Instant + +interface GribFileManager { + fun saveMetadata(file: GribFile) + fun listFiles(region: GribRegion): List<GribFile> + fun latestFile(region: GribRegion): GribFile? + fun delete(file: GribFile): Boolean + fun purgeOlderThan(before: Instant): Int + fun totalSizeBytes(): Long +} + +class InMemoryGribFileManager : GribFileManager { + private val files = mutableListOf<GribFile>() + override fun saveMetadata(file: GribFile) { files.add(file) } + override fun listFiles(region: GribRegion): List<GribFile> = files.filter { it.region.name == region.name }.sortedByDescending { it.downloadedAt } + override fun latestFile(region: GribRegion): GribFile? = listFiles(region).firstOrNull() + override fun delete(file: GribFile): Boolean = files.remove(file) + override fun purgeOlderThan(before: Instant): Int { val toRemove = files.filter { it.downloadedAt.isBefore(before) }; files.removeAll(toRemove); return toRemove.size } + override fun totalSizeBytes(): Long = files.sumOf { it.sizeBytes } +} diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/weather/GribStalenessChecker.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/weather/GribStalenessChecker.kt new file mode 100644 index 0000000..63466b2 --- /dev/null +++ b/android-app/app/src/main/kotlin/com/example/androidapp/data/weather/GribStalenessChecker.kt @@ -0,0 +1,36 @@ +package com.example.androidapp.data.weather + +import com.example.androidapp.data.model.GribFile +import com.example.androidapp.data.storage.GribFileManager +import com.example.androidapp.data.model.GribRegion +import java.time.Instant + +/** Outcome of a freshness check. */ +sealed class FreshnessResult { + /** Data is current; no user action needed. */ + object Fresh : FreshnessResult() + /** Data is stale; user should re-download. [message] is shown in the UI badge. */ + data class Stale(val file: GribFile, val message: String) : FreshnessResult() + /** No local GRIB data exists for this region. */ + object NoData : FreshnessResult() +} + +/** + * Checks whether locally-stored GRIB data for a region is fresh or stale. + * Per design doc §6.3: GRIB weather valid until model run + forecast hour; stale after. + */ +class GribStalenessChecker(private val manager: GribFileManager) { + + /** + * Check freshness of the most-recent GRIB file for [region] relative to [now]. + */ + fun check(region: GribRegion, now: Instant = Instant.now()): FreshnessResult { + val latest = manager.latestFile(region) ?: return FreshnessResult.NoData + return if (latest.isStale(now)) { + val hoursAgo = (now.epochSecond - latest.validUntil().epochSecond) / 3600 + FreshnessResult.Stale(latest, "Weather data outdated by ${hoursAgo}h — tap to refresh") + } else { + FreshnessResult.Fresh + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/model/GribFile.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/model/GribFile.kt index 9d284b5..715c1db 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/data/model/GribFile.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/data/model/GribFile.kt @@ -2,39 +2,8 @@ package org.terst.nav.data.model import java.time.Instant -/** - * Metadata record for a locally-stored GRIB2 file. - * - * @param region The geographic region this file covers. - * @param modelRunTime When the NWP model run that produced this file started (UTC). - * @param forecastHours Number of forecast hours included in this file. - * @param downloadedAt Wall-clock time when the file was saved locally. - * @param filePath Absolute path to the GRIB2 file on the device filesystem. - * @param sizeBytes File size in bytes. - */ -data class GribFile( - val region: GribRegion, - val modelRunTime: Instant, - val forecastHours: Int, - val downloadedAt: Instant, - val filePath: String, - val sizeBytes: Long -) { - /** - * The wall-clock time at which this GRIB file's forecast data expires. - * Per design doc §6.3: valid until model run + forecast hours. - */ +data class GribFile(val region: GribRegion, val modelRunTime: Instant, val forecastHours: Int, val downloadedAt: Instant, val filePath: String, val sizeBytes: Long) { fun validUntil(): Instant = modelRunTime.plusSeconds(forecastHours.toLong() * 3600) - - /** - * Returns true if the data has expired relative to [now]. - * Per design doc §6.3: stale after model run + forecast hour. - */ fun isStale(now: Instant = Instant.now()): Boolean = now.isAfter(validUntil()) - - /** - * Age of the download in seconds. - */ - fun ageSeconds(now: Instant = Instant.now()): Long = - now.epochSecond - downloadedAt.epochSecond + fun ageSeconds(now: Instant = Instant.now()): Long = now.epochSecond - downloadedAt.epochSecond } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/model/GribRegion.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/model/GribRegion.kt index 5e32d6c..f960bc3 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/data/model/GribRegion.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/data/model/GribRegion.kt @@ -1,20 +1,6 @@ package org.terst.nav.data.model -/** - * Geographic bounding box used to identify a GRIB download region. - * @param name Human-readable region name (e.g. "North Atlantic", "English Channel") - */ -data class GribRegion( - val name: String, - val latMin: Double, - val latMax: Double, - val lonMin: Double, - val lonMax: Double -) { - /** True if [lat]/[lon] falls within this region's bounding box. */ - fun contains(lat: Double, lon: Double): Boolean = - lat in latMin..latMax && lon in lonMin..lonMax - - /** Area in square degrees (rough proxy for download size estimate). */ +data class GribRegion(val name: String, val latMin: Double, val latMax: Double, val lonMin: Double, val lonMax: Double) { + fun contains(lat: Double, lon: Double): Boolean = lat in latMin..latMax && lon in lonMin..lonMax fun areaDegrees(): Double = (latMax - latMin) * (lonMax - lonMin) } diff --git a/android-app/app/src/main/res/layout/fragment_map.xml b/android-app/app/src/main/res/layout/fragment_map.xml index e5b86b7..2b9b40d 100644 --- a/android-app/app/src/main/res/layout/fragment_map.xml +++ b/android-app/app/src/main/res/layout/fragment_map.xml @@ -27,4 +27,18 @@ android:textSize="14sp" android:visibility="gone" /> + <!-- Staleness banner --> + <TextView + android:id="@+id/tvStalenessWarning" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:background="#FFCC00" + android:textColor="#000000" + android:textStyle="bold" + android:padding="8dp" + android:gravity="center" + android:visibility="gone" + android:text="" /> + </FrameLayout> |
