summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin/com/example
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main/kotlin/com/example')
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/data/api/ApiClient.kt35
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/data/api/MarineApiService.kt17
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/data/api/WeatherApiService.kt18
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/data/model/ForecastItem.kt30
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/data/model/MarineResponse.kt20
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/data/model/WeatherResponse.kt21
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/data/model/WindArrow.kt31
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/data/repository/WeatherRepository.kt57
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/ui/LocationPermissionHandler.kt43
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/ui/MainActivity.kt125
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/ui/MainViewModel.kt63
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/ui/forecast/ForecastAdapter.kt54
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/ui/forecast/ForecastFragment.kt74
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/ui/map/MapFragment.kt167
14 files changed, 0 insertions, 755 deletions
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/api/ApiClient.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/api/ApiClient.kt
deleted file mode 100644
index dd53f2e..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/data/api/ApiClient.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.example.androidapp.data.api
-
-import com.squareup.moshi.Moshi
-import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
-import okhttp3.OkHttpClient
-import okhttp3.logging.HttpLoggingInterceptor
-import retrofit2.Retrofit
-import retrofit2.converter.moshi.MoshiConverterFactory
-
-object ApiClient {
-
- private val moshi: Moshi = Moshi.Builder()
- .addLast(KotlinJsonAdapterFactory())
- .build()
-
- private val okHttpClient: OkHttpClient = OkHttpClient.Builder()
- .addInterceptor(
- HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }
- )
- .build()
-
- private fun retrofit(baseUrl: String): Retrofit = Retrofit.Builder()
- .baseUrl(baseUrl)
- .client(okHttpClient)
- .addConverterFactory(MoshiConverterFactory.create(moshi))
- .build()
-
- val weatherApi: WeatherApiService by lazy {
- retrofit("https://api.open-meteo.com/").create(WeatherApiService::class.java)
- }
-
- val marineApi: MarineApiService by lazy {
- retrofit("https://marine-api.open-meteo.com/").create(MarineApiService::class.java)
- }
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/api/MarineApiService.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/api/MarineApiService.kt
deleted file mode 100644
index 641cebc..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/data/api/MarineApiService.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.example.androidapp.data.api
-
-import com.example.androidapp.data.model.MarineResponse
-import retrofit2.http.GET
-import retrofit2.http.Query
-
-interface MarineApiService {
-
- @GET("v1/marine")
- suspend fun getMarineForecast(
- @Query("latitude") latitude: Double,
- @Query("longitude") longitude: Double,
- @Query("hourly") hourly: String =
- "wave_height,wave_direction,ocean_current_velocity,ocean_current_direction",
- @Query("forecast_days") forecastDays: Int = 7
- ): MarineResponse
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/api/WeatherApiService.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/api/WeatherApiService.kt
deleted file mode 100644
index 0d53ff9..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/data/api/WeatherApiService.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.example.androidapp.data.api
-
-import com.example.androidapp.data.model.WeatherResponse
-import retrofit2.http.GET
-import retrofit2.http.Query
-
-interface WeatherApiService {
-
- @GET("v1/forecast")
- suspend fun getWeatherForecast(
- @Query("latitude") latitude: Double,
- @Query("longitude") longitude: Double,
- @Query("hourly") hourly: String =
- "windspeed_10m,winddirection_10m,temperature_2m,precipitation_probability,weathercode",
- @Query("forecast_days") forecastDays: Int = 7,
- @Query("wind_speed_unit") windSpeedUnit: String = "kn"
- ): WeatherResponse
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/model/ForecastItem.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/model/ForecastItem.kt
deleted file mode 100644
index 3c3fc4d..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/data/model/ForecastItem.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.example.androidapp.data.model
-
-/** One hourly weather forecast slot shown in the forecast panel. */
-data class ForecastItem(
- val timeIso: String,
- val windKt: Double,
- val windDirDeg: Double,
- val tempC: Double,
- val precipProbabilityPct: Int,
- val weatherCode: Int
-) {
- /** Human-readable description based on WMO weather code. */
- fun weatherDescription(): String = when (weatherCode) {
- 0 -> "Clear sky"
- 1 -> "Mainly clear"
- 2 -> "Partly cloudy"
- 3 -> "Overcast"
- 45, 48 -> "Fog"
- 51, 53, 55 -> "Drizzle"
- 61, 63, 65 -> "Rain"
- 71, 73, 75 -> "Snow"
- 80, 81, 82 -> "Rain showers"
- 95 -> "Thunderstorm"
- 96, 99 -> "Thunderstorm with hail"
- else -> "Unknown ($weatherCode)"
- }
-
- /** WMO codes 51-67 and 80-82 are precipitation. */
- fun isRainy(): Boolean = weatherCode in 51..67 || weatherCode in 80..82
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/model/MarineResponse.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/model/MarineResponse.kt
deleted file mode 100644
index 8bbacb1..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/data/model/MarineResponse.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.example.androidapp.data.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class MarineResponse(
- @Json(name = "latitude") val latitude: Double,
- @Json(name = "longitude") val longitude: Double,
- @Json(name = "hourly") val hourly: MarineHourly
-)
-
-@JsonClass(generateAdapter = true)
-data class MarineHourly(
- @Json(name = "time") val time: List<String>,
- @Json(name = "wave_height") val waveHeight: List<Double?>,
- @Json(name = "wave_direction") val waveDirection: List<Double?>,
- @Json(name = "ocean_current_velocity") val oceanCurrentVelocity: List<Double?>,
- @Json(name = "ocean_current_direction") val oceanCurrentDirection: List<Double?>
-)
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/model/WeatherResponse.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/model/WeatherResponse.kt
deleted file mode 100644
index 89d8a11..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/data/model/WeatherResponse.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.example.androidapp.data.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class WeatherResponse(
- @Json(name = "latitude") val latitude: Double,
- @Json(name = "longitude") val longitude: Double,
- @Json(name = "hourly") val hourly: WeatherHourly
-)
-
-@JsonClass(generateAdapter = true)
-data class WeatherHourly(
- @Json(name = "time") val time: List<String>,
- @Json(name = "windspeed_10m") val windspeed10m: List<Double>,
- @Json(name = "winddirection_10m") val winddirection10m: List<Double>,
- @Json(name = "temperature_2m") val temperature2m: List<Double>,
- @Json(name = "precipitation_probability") val precipitationProbability: List<Int>,
- @Json(name = "weathercode") val weathercode: List<Int>
-)
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/model/WindArrow.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/model/WindArrow.kt
deleted file mode 100644
index 48699da..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/data/model/WindArrow.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.example.androidapp.data.model
-
-/** A wind vector at a geographic point, used for map overlay rendering. */
-data class WindArrow(
- val lat: Double,
- val lon: Double,
- val speedKt: Double,
- val directionDeg: Double
-) {
- fun isCalm(): Boolean = speedKt < 1.0
-
- /** Normalise 360° → 0°; all other values unchanged. */
- fun normalisedDirection(): Double = if (directionDeg >= 360.0) 0.0 else directionDeg
-
- /** Beaufort scale 0-12 from wind speed in knots. */
- fun beaufortScale(): Int = when {
- speedKt < 1 -> 0
- speedKt < 4 -> 1
- speedKt < 7 -> 2
- speedKt < 11 -> 3
- speedKt < 16 -> 4
- speedKt < 22 -> 5
- speedKt < 28 -> 6
- speedKt < 34 -> 7
- speedKt < 41 -> 8
- speedKt < 48 -> 9
- speedKt < 56 -> 10
- speedKt < 64 -> 11
- else -> 12
- }
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/data/repository/WeatherRepository.kt b/android-app/app/src/main/kotlin/com/example/androidapp/data/repository/WeatherRepository.kt
deleted file mode 100644
index 6affdbd..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/data/repository/WeatherRepository.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.example.androidapp.data.repository
-
-import com.example.androidapp.data.api.MarineApiService
-import com.example.androidapp.data.api.WeatherApiService
-import com.example.androidapp.data.model.ForecastItem
-import com.example.androidapp.data.model.WindArrow
-
-class WeatherRepository(
- private val weatherApi: WeatherApiService,
- private val marineApi: MarineApiService
-) {
-
- /**
- * Fetch 7-day hourly forecast items for the given position.
- * Both weather and marine data are requested; only weather fields are needed for ForecastItem,
- * but marine is fetched here to prime the cache for wind-arrow use.
- */
- suspend fun fetchForecastItems(lat: Double, lon: Double): Result<List<ForecastItem>> =
- runCatching {
- val weather = weatherApi.getWeatherForecast(lat, lon)
- val h = weather.hourly
- h.time.indices.map { i ->
- ForecastItem(
- timeIso = h.time[i],
- windKt = h.windspeed10m[i],
- windDirDeg = h.winddirection10m[i],
- tempC = h.temperature2m[i],
- precipProbabilityPct = h.precipitationProbability[i],
- weatherCode = h.weathercode[i]
- )
- }
- }
-
- /**
- * Fetch a single WindArrow representing the current conditions at [lat]/[lon].
- * Uses the first hourly slot as the current-hour proxy.
- */
- suspend fun fetchWindArrow(lat: Double, lon: Double): Result<WindArrow> =
- runCatching {
- val weather = weatherApi.getWeatherForecast(lat, lon, forecastDays = 1)
- val h = weather.hourly
- WindArrow(
- lat = lat,
- lon = lon,
- speedKt = h.windspeed10m.firstOrNull() ?: 0.0,
- directionDeg = h.winddirection10m.firstOrNull() ?: 0.0
- )
- }
-
- companion object {
- /** Factory using the shared ApiClient singletons. */
- fun create(): WeatherRepository {
- val client = com.example.androidapp.data.api.ApiClient
- return WeatherRepository(client.weatherApi, client.marineApi)
- }
- }
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/LocationPermissionHandler.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/LocationPermissionHandler.kt
deleted file mode 100644
index 664d5bb..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/ui/LocationPermissionHandler.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.example.androidapp.ui
-
-/**
- * Encapsulates location permission decision logic.
- *
- * Extracted for testability — no direct Android framework dependency in the core logic.
- *
- * Permissions handled: ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
- *
- * Usage:
- * - Call [start] on activity start to check existing permission or trigger a request.
- * - Call [onResult] from the ActivityResultLauncher callback with the permission grants map.
- */
-class LocationPermissionHandler(
- /** Returns true if location permission is already granted. */
- private val checkGranted: () -> Boolean,
- /** Called when location permission is available (already granted or just granted). */
- private val onGranted: () -> Unit,
- /** Called when location permission is denied or the user refuses (including "never ask again"). */
- private val onDenied: () -> Unit,
- /** Called when permission needs to be requested from the user via the system dialog. */
- private val requestPermissions: () -> Unit
-) {
- /**
- * Check current permission state and dispatch:
- * - If already granted, invoke [onGranted] immediately.
- * - Otherwise, invoke [requestPermissions] to trigger the system dialog.
- */
- fun start() {
- if (checkGranted()) onGranted() else requestPermissions()
- }
-
- /**
- * Process the result from the system permission dialog.
- *
- * @param grants Map of permission name → granted status from ActivityResultLauncher.
- * Invokes [onGranted] if any permission was granted, [onDenied] otherwise.
- * An empty map (e.g. "never ask again" scenario) also triggers [onDenied].
- */
- fun onResult(grants: Map<String, Boolean>) {
- if (grants.values.any { it }) onGranted() else onDenied()
- }
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainActivity.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainActivity.kt
deleted file mode 100644
index b29aefa..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainActivity.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-package org.terst.nav.ui
-
-import android.Manifest
-import android.content.pm.PackageManager
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.viewModels
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.ContextCompat
-import org.terst.nav.R
-import org.terst.nav.databinding.ActivityWeatherBinding
-import org.terst.nav.ui.forecast.ForecastFragment
-import org.terst.nav.ui.map.MapFragment
-import com.google.android.gms.location.LocationServices
-import com.google.android.gms.location.Priority
-
-class MainActivity : AppCompatActivity() {
-
- private lateinit var binding: ActivityWeatherBinding
- private val viewModel: MainViewModel by viewModels()
-
- // Default position (San Francisco Bay) used when location is unavailable
- private val defaultLat = 37.8
- private val defaultLon = -122.4
-
- private val permissionHandler: LocationPermissionHandler by lazy {
- LocationPermissionHandler(
- checkGranted = {
- ContextCompat.checkSelfPermission(
- this, Manifest.permission.ACCESS_FINE_LOCATION
- ) == PackageManager.PERMISSION_GRANTED
- },
- onGranted = { fetchLocationAndLoad() },
- onDenied = { loadWeatherAtDefault() },
- requestPermissions = {
- locationPermissionLauncher.launch(
- arrayOf(
- Manifest.permission.ACCESS_FINE_LOCATION,
- Manifest.permission.ACCESS_COARSE_LOCATION
- )
- )
- }
- )
- }
-
- private val locationPermissionLauncher = registerForActivityResult(
- ActivityResultContracts.RequestMultiplePermissions()
- ) { grants ->
- permissionHandler.onResult(grants)
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityWeatherBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- setupBottomNav()
-
- if (savedInstanceState == null) {
- showFragment(MapFragment(), TAG_MAP)
- requestLocationOrLoad()
- }
- }
-
- private fun setupBottomNav() {
- binding.bottomNav.setOnItemSelectedListener { item ->
- when (item.itemId) {
- R.id.nav_map -> {
- showFragment(MapFragment(), TAG_MAP)
- true
- }
- R.id.nav_forecast -> {
- showFragment(ForecastFragment(), TAG_FORECAST)
- true
- }
- else -> false
- }
- }
- }
-
- private fun showFragment(fragment: androidx.fragment.app.Fragment, tag: String) {
- val existing = supportFragmentManager.findFragmentByTag(tag)
- supportFragmentManager.beginTransaction()
- .apply {
- supportFragmentManager.fragments.forEach { hide(it) }
- if (existing == null) add(R.id.fragment_container, fragment, tag)
- else show(existing)
- }
- .commit()
- }
-
- private fun requestLocationOrLoad() {
- permissionHandler.start()
- }
-
- private fun fetchLocationAndLoad() {
- val client = LocationServices.getFusedLocationProviderClient(this)
- try {
- client.getCurrentLocation(Priority.PRIORITY_BALANCED_POWER_ACCURACY, null)
- .addOnSuccessListener { location ->
- if (location != null) {
- viewModel.loadWeather(location.latitude, location.longitude)
- } else {
- loadWeatherAtDefault()
- }
- }
- .addOnFailureListener {
- loadWeatherAtDefault()
- }
- } catch (e: SecurityException) {
- loadWeatherAtDefault()
- }
- }
-
- private fun loadWeatherAtDefault() {
- Toast.makeText(this, R.string.error_location, Toast.LENGTH_SHORT).show()
- viewModel.loadWeather(defaultLat, defaultLon)
- }
-
- companion object {
- private const val TAG_MAP = "map"
- private const val TAG_FORECAST = "forecast"
- }
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainViewModel.kt
deleted file mode 100644
index eabb594..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/ui/MainViewModel.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.example.androidapp.ui
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.example.androidapp.data.model.ForecastItem
-import com.example.androidapp.data.model.WindArrow
-import com.example.androidapp.data.repository.WeatherRepository
-import kotlinx.coroutines.async
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.launch
-
-sealed class UiState {
- object Loading : UiState()
- object Success : UiState()
- data class Error(val message: String) : UiState()
-}
-
-class MainViewModel(
- private val repository: WeatherRepository = WeatherRepository.create()
-) : ViewModel() {
-
- private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
- val uiState: StateFlow<UiState> = _uiState
-
- private val _windArrow = MutableStateFlow<WindArrow?>(null)
- val windArrow: StateFlow<WindArrow?> = _windArrow
-
- private val _forecast = MutableStateFlow<List<ForecastItem>>(emptyList())
- val forecast: StateFlow<List<ForecastItem>> = _forecast
-
- /**
- * Fetch weather and marine data for [lat]/[lon] in parallel.
- * Called once the device location is known.
- */
- fun loadWeather(lat: Double, lon: Double) {
- viewModelScope.launch {
- val arrowDeferred = async { repository.fetchWindArrow(lat, lon) }
- val forecastDeferred = async { repository.fetchForecastItems(lat, lon) }
-
- val arrowResult = arrowDeferred.await()
- val forecastResult = forecastDeferred.await()
-
- when {
- arrowResult.isFailure -> {
- _uiState.value = UiState.Error(
- arrowResult.exceptionOrNull()?.message ?: "Unknown error"
- )
- }
- forecastResult.isFailure -> {
- _uiState.value = UiState.Error(
- forecastResult.exceptionOrNull()?.message ?: "Unknown error"
- )
- }
- else -> {
- _windArrow.value = arrowResult.getOrNull()
- _forecast.value = forecastResult.getOrThrow()
- _uiState.value = UiState.Success
- }
- }
- }
- }
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/forecast/ForecastAdapter.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/forecast/ForecastAdapter.kt
deleted file mode 100644
index 06c5eed..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/ui/forecast/ForecastAdapter.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.example.androidapp.ui.forecast
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.example.androidapp.data.model.ForecastItem
-import com.example.androidapp.databinding.ItemForecastBinding
-
-class ForecastAdapter : ListAdapter<ForecastItem, ForecastAdapter.ViewHolder>(DIFF) {
-
- inner class ViewHolder(private val binding: ItemForecastBinding) :
- RecyclerView.ViewHolder(binding.root) {
-
- fun bind(item: ForecastItem) {
- // Display short time (e.g. "13:00" from "2026-03-13T13:00")
- binding.tvTime.text = item.timeIso.substringAfter("T", item.timeIso)
-
- binding.tvDescription.text = item.weatherDescription()
-
- binding.tvWind.text = String.format("%.0f kt %s",
- item.windKt, directionLabel(item.windDirDeg))
-
- binding.tvTemp.text = String.format("%.0f °C", item.tempC)
-
- binding.tvPrecip.text = "${item.precipProbabilityPct}%"
- }
-
- private fun directionLabel(deg: Double): String {
- val dirs = arrayOf("N","NE","E","SE","S","SW","W","NW")
- val idx = ((deg + 22.5) / 45.0).toInt() % 8
- return dirs[idx]
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- val binding = ItemForecastBinding.inflate(
- LayoutInflater.from(parent.context), parent, false
- )
- return ViewHolder(binding)
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bind(getItem(position))
- }
-
- companion object {
- private val DIFF = object : DiffUtil.ItemCallback<ForecastItem>() {
- override fun areItemsTheSame(a: ForecastItem, b: ForecastItem) = a.timeIso == b.timeIso
- override fun areContentsTheSame(a: ForecastItem, b: ForecastItem) = a == b
- }
- }
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/forecast/ForecastFragment.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/forecast/ForecastFragment.kt
deleted file mode 100644
index a8be8f6..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/ui/forecast/ForecastFragment.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.example.androidapp.ui.forecast
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.example.androidapp.databinding.FragmentForecastBinding
-import com.example.androidapp.ui.MainViewModel
-import com.example.androidapp.ui.UiState
-import kotlinx.coroutines.launch
-
-class ForecastFragment : Fragment() {
-
- private var _binding: FragmentForecastBinding? = null
- private val binding get() = _binding!!
-
- private val viewModel: MainViewModel by activityViewModels()
- private val adapter = ForecastAdapter()
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- _binding = FragmentForecastBinding.inflate(inflater, container, false)
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- binding.forecastList.layoutManager = LinearLayoutManager(requireContext())
- binding.forecastList.adapter = adapter
-
- viewLifecycleOwner.lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- launch {
- viewModel.uiState.collect { state ->
- when (state) {
- UiState.Loading -> {
- binding.progress.visibility = View.VISIBLE
- binding.errorText.visibility = View.GONE
- }
- UiState.Success -> {
- binding.progress.visibility = View.GONE
- binding.errorText.visibility = View.GONE
- }
- is UiState.Error -> {
- binding.progress.visibility = View.GONE
- binding.errorText.visibility = View.VISIBLE
- binding.errorText.text = state.message
- }
- }
- }
- }
- launch {
- viewModel.forecast.collect { items ->
- adapter.submitList(items)
- }
- }
- }
- }
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/map/MapFragment.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/map/MapFragment.kt
deleted file mode 100644
index 82dd999..0000000
--- a/android-app/app/src/main/kotlin/com/example/androidapp/ui/map/MapFragment.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-package com.example.androidapp.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 com.example.androidapp.R
-import com.example.androidapp.data.model.WindArrow
-import com.example.androidapp.databinding.FragmentMapBinding
-import com.example.androidapp.ui.MainViewModel
-import com.example.androidapp.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"
- }
-}