summaryrefslogtreecommitdiff
path: root/android-app/app
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/map_orig/MapFragment.kt167
-rw-r--r--android-app/app/src/test/kotlin/org/terst/nav/ui_orig/LocationPermissionHandlerTest.kt110
-rw-r--r--android-app/app/src/test/kotlin/org/terst/nav/ui_orig/MainViewModelTest.kt105
3 files changed, 0 insertions, 382 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/map_orig/MapFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/map_orig/MapFragment.kt
deleted file mode 100644
index ea7b596..0000000
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/map_orig/MapFragment.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-package org.terst.nav.ui.map
-
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.content.ContextCompat
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import org.terst.nav.R
-import org.terst.nav.data.model.WindArrow
-import org.terst.nav.databinding.FragmentMapBinding
-import org.terst.nav.ui.MainViewModel
-import org.terst.nav.ui.UiState
-import kotlinx.coroutines.launch
-import org.maplibre.android.MapLibre
-import org.maplibre.android.camera.CameraPosition
-import org.maplibre.android.geometry.LatLng
-import org.maplibre.android.maps.MapLibreMap
-import org.maplibre.android.maps.Style
-import org.maplibre.android.style.expressions.Expression
-import org.maplibre.android.style.layers.PropertyFactory
-import org.maplibre.android.style.layers.SymbolLayer
-import org.maplibre.android.style.sources.GeoJsonSource
-import org.maplibre.geojson.Feature
-import org.maplibre.geojson.FeatureCollection
-import org.maplibre.geojson.Point
-
-class MapFragment : Fragment() {
-
- private var _binding: FragmentMapBinding? = null
- private val binding get() = _binding!!
-
- private val viewModel: MainViewModel by activityViewModels()
-
- private var mapLibreMap: MapLibreMap? = null
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- MapLibre.getInstance(requireContext())
- _binding = FragmentMapBinding.inflate(inflater, container, false)
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- binding.mapView.onCreate(savedInstanceState)
- binding.mapView.getMapAsync { map ->
- mapLibreMap = map
- map.setStyle(Style.Builder().fromUri(MAP_STYLE_URL)) { style ->
- addWindArrowImage(style)
- observeViewModel(style)
- }
- }
- }
-
- private fun observeViewModel(style: Style) {
- viewLifecycleOwner.lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- launch {
- viewModel.uiState.collect { state ->
- binding.statusText.visibility = when (state) {
- UiState.Loading -> View.VISIBLE.also { binding.statusText.text = getString(R.string.loading_weather) }
- UiState.Success -> View.GONE
- is UiState.Error -> View.VISIBLE.also { binding.statusText.text = state.message }
- }
- }
- }
- launch {
- viewModel.windArrow.collect { arrow ->
- if (arrow != null) {
- updateWindLayer(style, arrow)
- centerMapOn(arrow.lat, arrow.lon)
- }
- }
- }
- }
- }
- }
-
- private fun addWindArrowImage(style: Style) {
- val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_wind_arrow)
- ?: return
- val bitmap = Bitmap.createBitmap(
- drawable.intrinsicWidth.coerceAtLeast(24),
- drawable.intrinsicHeight.coerceAtLeast(24),
- Bitmap.Config.ARGB_8888
- )
- val canvas = Canvas(bitmap)
- drawable.setBounds(0, 0, canvas.width, canvas.height)
- drawable.draw(canvas)
- style.addImage(WIND_ARROW_ICON, bitmap)
- }
-
- private fun updateWindLayer(style: Style, arrow: WindArrow) {
- val feature = Feature.fromGeometry(
- Point.fromLngLat(arrow.lon, arrow.lat)
- ).also { f ->
- f.addNumberProperty("direction", arrow.directionDeg)
- f.addNumberProperty("speed_kt", arrow.speedKt)
- }
- val collection = FeatureCollection.fromFeature(feature)
-
- if (style.getSource(WIND_SOURCE_ID) == null) {
- style.addSource(GeoJsonSource(WIND_SOURCE_ID, collection))
- } else {
- (style.getSource(WIND_SOURCE_ID) as? GeoJsonSource)?.setGeoJson(collection)
- }
-
- if (style.getLayer(WIND_LAYER_ID) == null) {
- val layer = SymbolLayer(WIND_LAYER_ID, WIND_SOURCE_ID).withProperties(
- PropertyFactory.iconImage(WIND_ARROW_ICON),
- PropertyFactory.iconRotate(Expression.get("direction")),
- PropertyFactory.iconRotationAlignment("map"),
- PropertyFactory.iconAllowOverlap(true),
- PropertyFactory.iconSize(
- Expression.interpolate(
- Expression.linear(),
- Expression.get("speed_kt"),
- Expression.stop(0, 0.6f),
- Expression.stop(30, 1.4f)
- )
- )
- )
- style.addLayer(layer)
- }
- }
-
- private fun centerMapOn(lat: Double, lon: Double) {
- mapLibreMap?.cameraPosition = CameraPosition.Builder()
- .target(LatLng(lat, lon))
- .zoom(7.0)
- .build()
- }
-
- // Lifecycle delegation to MapView
- override fun onStart() { super.onStart(); binding.mapView.onStart() }
- override fun onResume() { super.onResume(); binding.mapView.onResume() }
- override fun onPause() { super.onPause(); binding.mapView.onPause() }
- override fun onStop() { super.onStop(); binding.mapView.onStop() }
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- binding.mapView.onSaveInstanceState(outState)
- }
- override fun onLowMemory() { super.onLowMemory(); binding.mapView.onLowMemory() }
-
- override fun onDestroyView() {
- binding.mapView.onDestroy()
- super.onDestroyView()
- _binding = null
- }
-
- companion object {
- private const val MAP_STYLE_URL = "https://demotiles.maplibre.org/style.json"
- private const val WIND_SOURCE_ID = "wind-source"
- private const val WIND_LAYER_ID = "wind-arrows"
- private const val WIND_ARROW_ICON = "wind-arrow"
- }
-}
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/LocationPermissionHandlerTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/LocationPermissionHandlerTest.kt
deleted file mode 100644
index 9caa5a0..0000000
--- a/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/LocationPermissionHandlerTest.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-package org.terst.nav.ui
-
-import org.junit.Assert.*
-import org.junit.Test
-
-class LocationPermissionHandlerTest {
-
- // Convenience factory — callers override only the lambdas they care about.
- private fun makeHandler(
- checkGranted: () -> Boolean = { false },
- onGranted: () -> Unit = {},
- onDenied: () -> Unit = {},
- requestPermissions: () -> Unit = {}
- ) = LocationPermissionHandler(checkGranted, onGranted, onDenied, requestPermissions)
-
- // ── start() ──────────────────────────────────────────────────────────────
-
- @Test
- fun `start - permission already granted - calls onGranted without requesting`() {
- var onGrantedCalled = false
- var requestCalled = false
- makeHandler(
- checkGranted = { true },
- onGranted = { onGrantedCalled = true },
- requestPermissions = { requestCalled = true }
- ).start()
-
- assertTrue("onGranted should be called", onGrantedCalled)
- assertFalse("requestPermissions should NOT be called", requestCalled)
- }
-
- @Test
- fun `start - permission not granted - calls requestPermissions without calling onGranted`() {
- var onGrantedCalled = false
- var requestCalled = false
- makeHandler(
- checkGranted = { false },
- onGranted = { onGrantedCalled = true },
- requestPermissions = { requestCalled = true }
- ).start()
-
- assertFalse("onGranted should NOT be called", onGrantedCalled)
- assertTrue("requestPermissions should be called", requestCalled)
- }
-
- // ── onResult() ───────────────────────────────────────────────────────────
-
- @Test
- fun `onResult - fine location granted - calls onGranted`() {
- var onGrantedCalled = false
- makeHandler(onGranted = { onGrantedCalled = true }).onResult(
- mapOf(
- "android.permission.ACCESS_FINE_LOCATION" to true,
- "android.permission.ACCESS_COARSE_LOCATION" to false
- )
- )
- assertTrue("onGranted should be called when fine location is granted", onGrantedCalled)
- }
-
- @Test
- fun `onResult - coarse location granted - calls onGranted`() {
- var onGrantedCalled = false
- makeHandler(onGranted = { onGrantedCalled = true }).onResult(
- mapOf(
- "android.permission.ACCESS_FINE_LOCATION" to false,
- "android.permission.ACCESS_COARSE_LOCATION" to true
- )
- )
- assertTrue("onGranted should be called when coarse location is granted", onGrantedCalled)
- }
-
- @Test
- fun `onResult - both permissions granted - calls onGranted`() {
- var onGrantedCalled = false
- makeHandler(onGranted = { onGrantedCalled = true }).onResult(
- mapOf(
- "android.permission.ACCESS_FINE_LOCATION" to true,
- "android.permission.ACCESS_COARSE_LOCATION" to true
- )
- )
- assertTrue(onGrantedCalled)
- }
-
- @Test
- fun `onResult - all permissions denied - calls onDenied not onGranted`() {
- var onGrantedCalled = false
- var onDeniedCalled = false
- makeHandler(
- onGranted = { onGrantedCalled = true },
- onDenied = { onDeniedCalled = true }
- ).onResult(
- mapOf(
- "android.permission.ACCESS_FINE_LOCATION" to false,
- "android.permission.ACCESS_COARSE_LOCATION" to false
- )
- )
- assertFalse("onGranted should NOT be called", onGrantedCalled)
- assertTrue("onDenied should be called", onDeniedCalled)
- }
-
- @Test
- fun `onResult - empty grants (never ask again scenario) - calls onDenied`() {
- var onDeniedCalled = false
- makeHandler(onDenied = { onDeniedCalled = true }).onResult(emptyMap())
- assertTrue(
- "onDenied should be called for empty grants (never-ask-again)",
- onDeniedCalled
- )
- }
-}
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/MainViewModelTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/MainViewModelTest.kt
deleted file mode 100644
index edecdd5..0000000
--- a/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/MainViewModelTest.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-package org.terst.nav.ui
-
-import app.cash.turbine.test
-import org.terst.nav.data.model.ForecastItem
-import org.terst.nav.data.model.WindArrow
-import org.terst.nav.data.repository.WeatherRepository
-import io.mockk.coEvery
-import io.mockk.mockk
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.*
-import org.junit.After
-import org.junit.Assert.*
-import org.junit.Before
-import org.junit.Test
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class MainViewModelTest {
-
- private val testDispatcher = UnconfinedTestDispatcher()
- private val repo = mockk<WeatherRepository>()
- private lateinit var vm: MainViewModel
-
- private val sampleArrow = WindArrow(37.5, -122.3, 15.0, 270.0)
- private val sampleForecast = listOf(
- ForecastItem("2026-03-13T00:00", 15.0, 270.0, 18.5, 20, 1)
- )
-
- @Before
- fun setUp() {
- Dispatchers.setMain(testDispatcher)
- }
-
- @After
- fun tearDown() {
- Dispatchers.resetMain()
- }
-
- private fun makeVm() = MainViewModel(repo)
-
- @Test
- fun `initial uiState is Loading`() {
- coEvery { repo.fetchWindArrow(any(), any()) } coAnswers { Result.success(sampleArrow) }
- coEvery { repo.fetchForecastItems(any(), any()) } coAnswers { Result.success(sampleForecast) }
- vm = makeVm()
- // Before loadWeather() is called the state is Loading
- assertEquals(UiState.Loading, vm.uiState.value)
- }
-
- @Test
- fun `loadWeather success transitions to Success state`() = runTest {
- coEvery { repo.fetchWindArrow(any(), any()) } returns Result.success(sampleArrow)
- coEvery { repo.fetchForecastItems(any(), any()) } returns Result.success(sampleForecast)
- vm = makeVm()
-
- vm.uiState.test {
- assertEquals(UiState.Loading, awaitItem())
- vm.loadWeather(37.5, -122.3)
- assertEquals(UiState.Success, awaitItem())
- cancelAndIgnoreRemainingEvents()
- }
- }
-
- @Test
- fun `loadWeather populates windArrow and forecast`() = runTest {
- coEvery { repo.fetchWindArrow(any(), any()) } returns Result.success(sampleArrow)
- coEvery { repo.fetchForecastItems(any(), any()) } returns Result.success(sampleForecast)
- vm = makeVm()
- vm.loadWeather(37.5, -122.3)
-
- assertEquals(sampleArrow, vm.windArrow.value)
- assertEquals(sampleForecast, vm.forecast.value)
- }
-
- @Test
- fun `loadWeather arrow failure transitions to Error state`() = runTest {
- coEvery { repo.fetchWindArrow(any(), any()) } returns Result.failure(RuntimeException("Net error"))
- coEvery { repo.fetchForecastItems(any(), any()) } returns Result.success(sampleForecast)
- vm = makeVm()
-
- vm.uiState.test {
- awaitItem() // Loading
- vm.loadWeather(37.5, -122.3)
- val state = awaitItem()
- assertTrue(state is UiState.Error)
- assertTrue((state as UiState.Error).message.contains("Net error"))
- cancelAndIgnoreRemainingEvents()
- }
- }
-
- @Test
- fun `loadWeather forecast failure transitions to Error state`() = runTest {
- coEvery { repo.fetchWindArrow(any(), any()) } returns Result.success(sampleArrow)
- coEvery { repo.fetchForecastItems(any(), any()) } returns Result.failure(RuntimeException("Timeout"))
- vm = makeVm()
-
- vm.uiState.test {
- awaitItem() // Loading
- vm.loadWeather(37.5, -122.3)
- val state = awaitItem()
- assertTrue(state is UiState.Error)
- cancelAndIgnoreRemainingEvents()
- }
- }
-}