diff options
Diffstat (limited to 'android-app')
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() - } - } -} |
