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/MainActivity.kt4
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt14
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt43
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt102
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt66
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt51
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt5
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt55
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt7
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt1
-rw-r--r--android-app/app/src/main/res/layout/fragment_pretrip_report.xml144
-rw-r--r--android-app/app/src/main/res/layout/fragment_safety.xml9
12 files changed, 483 insertions, 18 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
index 66aa3e0..0f2eb91 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
@@ -30,6 +30,7 @@ import java.util.Locale
import org.maplibre.android.MapLibre
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.Style
+import org.maplibre.android.style.layers.PropertyFactory
import org.maplibre.android.style.layers.RasterLayer
import org.maplibre.android.style.sources.RasterSource
import org.maplibre.android.style.sources.TileSet
@@ -335,7 +336,8 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
lifecycleScope.launch {
loadedStyleFlow.filterNotNull()
.combine(viewModel.trackPoints) { style, points -> style to points }
- .collect { (style, points) -> mapHandler?.updateTrackLayer(style, points) }
+ .combine(viewModel.pastTracks) { (style, active), past -> Triple(style, active, past) }
+ .collect { (style, active, past) -> mapHandler?.updateTrackLayer(style, active, past) }
}
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt
index 7953822..85dd2dd 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt
@@ -5,22 +5,28 @@ class TrackRepository {
var isRecording: Boolean = false
private set
- private val points = mutableListOf<TrackPoint>()
+ private val activePoints = mutableListOf<TrackPoint>()
+ private val pastTracks = mutableListOf<List<TrackPoint>>()
fun startTrack() {
- points.clear()
+ activePoints.clear()
isRecording = true
}
fun stopTrack() {
+ if (isRecording && activePoints.isNotEmpty()) {
+ pastTracks.add(activePoints.toList())
+ }
isRecording = false
}
fun addPoint(point: TrackPoint): Boolean {
if (!isRecording) return false
- points.add(point)
+ activePoints.add(point)
return true
}
- fun getPoints(): List<TrackPoint> = points.toList()
+ fun getPoints(): List<TrackPoint> = activePoints.toList()
+
+ fun getPastTracks(): List<List<TrackPoint>> = pastTracks.toList()
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt
new file mode 100644
index 0000000..2362079
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt
@@ -0,0 +1,43 @@
+package org.terst.nav.tripreport
+
+enum class BoatType {
+ MONOHULL,
+ MULTIHULL
+}
+
+enum class RigType {
+ SLOOP,
+ CUTTER,
+ KETCH
+}
+
+data class BoatProfile(
+ val name: String,
+ val lengthFt: Double,
+ val type: BoatType,
+ val rig: RigType,
+ val hasSpinnaker: Boolean = false,
+ val hasGennaker: Boolean = false
+)
+
+data class PreTripSummary(
+ val timestampMs: Long,
+ val lat: Double,
+ val lon: Double,
+ val windSpeedKt: Double,
+ val windDirDeg: Double,
+ val waveHeightM: Double?,
+ val weatherDescription: String,
+ val boatProfile: BoatProfile
+)
+
+data class SailSuggestion(
+ val sailName: String,
+ val action: String // e.g., "Full Main", "1 Reef", "Furl"
+)
+
+data class PreTripReport(
+ val summary: PreTripSummary,
+ val routingSuggestion: String,
+ val sailPlan: List<SailSuggestion>
+)
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt
new file mode 100644
index 0000000..819485f
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt
@@ -0,0 +1,102 @@
+package org.terst.nav.tripreport
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ProgressBar
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.card.MaterialCardView
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import org.terst.nav.R
+import org.terst.nav.ui.MainViewModel
+import java.util.Locale
+
+class PreTripReportFragment : Fragment() {
+
+ private val viewModel: PreTripReportViewModel by activityViewModels()
+ private val mainViewModel: MainViewModel by activityViewModels()
+
+ private lateinit var tvWeatherSummary: TextView
+ private lateinit var tvRoutingContent: TextView
+ private lateinit var tvSailPlanContent: TextView
+ private lateinit var cardReport: MaterialCardView
+ private lateinit var btnGenerate: MaterialButton
+ private lateinit var progress: ProgressBar
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? = inflater.inflate(R.layout.fragment_pretrip_report, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ tvWeatherSummary = view.findViewById(R.id.tv_weather_summary)
+ tvRoutingContent = view.findViewById(R.id.tv_routing_content)
+ tvSailPlanContent = view.findViewById(R.id.tv_sail_plan_content)
+ cardReport = view.findViewById(R.id.card_report)
+ btnGenerate = view.findViewById(R.id.btn_generate_pretrip)
+ progress = view.findViewById(R.id.progress_pretrip)
+
+ btnGenerate.setOnClickListener {
+ generateReport()
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.state.collect { renderState(it) }
+ }
+ }
+
+ private fun generateReport() {
+ viewLifecycleOwner.lifecycleScope.launch {
+ val forecast = mainViewModel.forecast.value.firstOrNull()
+ val conditions = mainViewModel.marineConditions.value
+ // For now, use 0,0 if no location, but ideally we'd have last known
+ // In a real app, we'd get this from a LocationProvider
+ viewModel.generate(0.0, 0.0, forecast, conditions)
+ }
+ }
+
+ private fun renderState(state: PreTripState) {
+ when (state) {
+ is PreTripState.Loading -> {
+ progress.visibility = View.VISIBLE
+ btnGenerate.isEnabled = false
+ }
+ is PreTripState.Success -> {
+ progress.visibility = View.GONE
+ btnGenerate.isEnabled = true
+ cardReport.visibility = View.VISIBLE
+
+ val r = state.report
+ tvWeatherSummary.text = "Wind: %.1f kts from %.0f°\nWaves: %s\nSky: %s".format(
+ Locale.getDefault(),
+ r.summary.windSpeedKt,
+ r.summary.windDirDeg,
+ r.summary.waveHeightM?.let { "%.1fm".format(it) } ?: "N/A",
+ r.summary.weatherDescription
+ )
+
+ tvRoutingContent.text = r.routingSuggestion
+
+ val sailPlanText = r.sailPlan.joinToString("\n") {
+ "• ${it.sailName}: ${it.action}"
+ }
+ tvSailPlanContent.text = sailPlanText
+ }
+ is PreTripState.Error -> {
+ progress.visibility = View.GONE
+ btnGenerate.isEnabled = true
+ // Show toast or error message
+ }
+ else -> {}
+ }
+ }
+}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt
new file mode 100644
index 0000000..2ccabfb
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt
@@ -0,0 +1,66 @@
+package org.terst.nav.tripreport
+
+import org.terst.nav.data.model.ForecastItem
+import org.terst.nav.data.model.MarineConditions
+
+class PreTripReportGenerator {
+
+ fun generateReport(
+ lat: Double,
+ lon: Double,
+ forecast: ForecastItem?,
+ conditions: MarineConditions?,
+ boatProfile: BoatProfile
+ ): PreTripReport {
+ val summary = PreTripSummary(
+ timestampMs = System.currentTimeMillis(),
+ lat = lat,
+ lon = lon,
+ windSpeedKt = forecast?.windKt ?: 0.0,
+ windDirDeg = forecast?.windDirDeg ?: 0.0,
+ waveHeightM = conditions?.waveHeightM,
+ weatherDescription = forecast?.weatherDescription() ?: "Unknown",
+ boatProfile = boatProfile
+ )
+
+ val routing = suggestRouting(summary)
+ val sailPlan = suggestSailPlan(summary)
+
+ return PreTripReport(summary, routing, sailPlan)
+ }
+
+ private fun suggestRouting(summary: PreTripSummary): String {
+ val wind = summary.windSpeedKt
+ val waves = summary.waveHeightM ?: 0.0
+
+ return when {
+ wind > 35.0 -> "STORM WARNING: Winds exceed 35kts. Consider remaining in port or seeking shelter immediately."
+ wind > 25.0 && waves > 2.5 -> "HEAVY WEATHER: Expect challenging conditions. Coastal routing advised to minimize fetch."
+ wind < 5.0 -> "LIGHT WINDS: Motor-sailing likely required for efficient passage."
+ else -> "FAVORABLE CONDITIONS: Standard routing based on destination bearing should be effective."
+ }
+ }
+
+ private fun suggestSailPlan(summary: PreTripSummary): List<SailSuggestion> {
+ val wind = summary.windSpeedKt
+ val suggestions = mutableListOf<SailSuggestion>()
+
+ // Main sail
+ suggestions.add(when {
+ wind > 30.0 -> SailSuggestion("Main", "Deep Reef / Trysail")
+ wind > 22.0 -> SailSuggestion("Main", "2nd Reef")
+ wind > 16.0 -> SailSuggestion("Main", "1st Reef")
+ else -> SailSuggestion("Main", "Full Main")
+ })
+
+ // Headsail
+ suggestions.add(when {
+ wind > 25.0 -> SailSuggestion("Headsail", "Storm Jib / Furl 50%")
+ wind > 18.0 -> SailSuggestion("Headsail", "Working Jib / Furl 30%")
+ wind < 10.0 && summary.boatProfile.hasGennaker -> SailSuggestion("Gennaker", "Deploy for light air reach")
+ else -> SailSuggestion("Headsail", "Full Genoa")
+ })
+
+ return suggestions
+ }
+}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt
new file mode 100644
index 0000000..9fd32c7
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt
@@ -0,0 +1,51 @@
+package org.terst.nav.tripreport
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import org.terst.nav.data.model.ForecastItem
+import org.terst.nav.data.model.MarineConditions
+
+sealed class PreTripState {
+ object Idle : PreTripState()
+ object Loading : PreTripState()
+ data class Success(val report: PreTripReport) : PreTripState()
+ data class Error(val message: String) : PreTripState()
+}
+
+class PreTripReportViewModel(
+ private val generator: PreTripReportGenerator = PreTripReportGenerator()
+) : ViewModel() {
+
+ private val _state = MutableStateFlow<PreTripState>(PreTripState.Idle)
+ val state: StateFlow<PreTripState> = _state.asStateFlow()
+
+ private val _boatProfile = MutableStateFlow(
+ BoatProfile("Default Sloop", 35.0, BoatType.MONOHULL, RigType.SLOOP)
+ )
+ val boatProfile: StateFlow<BoatProfile> = _boatProfile.asStateFlow()
+
+ fun updateBoatProfile(profile: BoatProfile) {
+ _boatProfile.value = profile
+ }
+
+ fun generate(
+ lat: Double,
+ lon: Double,
+ forecast: ForecastItem?,
+ conditions: MarineConditions?
+ ) {
+ viewModelScope.launch {
+ _state.value = PreTripState.Loading
+ try {
+ val report = generator.generateReport(lat, lon, forecast, conditions, _boatProfile.value)
+ _state.value = PreTripState.Success(report)
+ } catch (e: Exception) {
+ _state.value = PreTripState.Error(e.message ?: "Failed to generate pre-trip report")
+ }
+ }
+ }
+}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
index 7caabe7..2c56b06 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
@@ -57,6 +57,9 @@ class MainViewModel(
private val _trackPoints = MutableStateFlow<List<TrackPoint>>(emptyList())
val trackPoints: StateFlow<List<TrackPoint>> = _trackPoints.asStateFlow()
+ private val _pastTracks = MutableStateFlow<List<List<TrackPoint>>>(emptyList())
+ val pastTracks: StateFlow<List<List<TrackPoint>>> = _pastTracks.asStateFlow()
+
fun startTrack() {
trackRepository.startTrack()
_trackPoints.value = emptyList()
@@ -65,6 +68,8 @@ class MainViewModel(
fun stopTrack() {
trackRepository.stopTrack()
+ _pastTracks.value = trackRepository.getPastTracks()
+ _trackPoints.value = emptyList()
_isRecording.value = false
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt
index f1feaed..4f08de7 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt
@@ -67,14 +67,17 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
private val USER_POS_LAYER_ID = "user-pos-layer"
private val USER_ICON_ID = "user-icon"
- private val TRACK_SOURCE_ID = "track-source"
- private val TRACK_LAYER_ID = "track-line"
+ private val TRACK_ACTIVE_SOURCE_ID = "track-active-source"
+ private val TRACK_ACTIVE_LAYER_ID = "track-line-active"
+ private val TRACK_PAST_SOURCE_ID = "track-past-source"
+ private val TRACK_PAST_LAYER_ID = "track-line-past"
private var anchorPointSource: GeoJsonSource? = null
private var anchorCircleSource: GeoJsonSource? = null
private var tidalCurrentSource: GeoJsonSource? = null
private var userPosSource: GeoJsonSource? = null
- private var trackSource: GeoJsonSource? = null
+ private var trackActiveSource: GeoJsonSource? = null
+ private var trackPastSource: GeoJsonSource? = null
/**
* Initializes map layers for anchor watch, tidal currents, and user position.
@@ -199,26 +202,52 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
}
/**
- * Updates the GPS track polyline on the map. Lazily initialises the layer on first call.
+ * Updates the GPS track polyline on the map. Lazily initialises the layers on first call.
*/
- fun updateTrackLayer(style: Style, points: List<TrackPoint>) {
- if (trackSource == null) {
- trackSource = GeoJsonSource(TRACK_SOURCE_ID)
- style.addSource(trackSource!!)
- style.addLayer(LineLayer(TRACK_LAYER_ID, TRACK_SOURCE_ID).apply {
+ fun updateTrackLayer(style: Style, activePoints: List<TrackPoint>, pastTracks: List<List<TrackPoint>>) {
+ // Active track layer (Solid)
+ if (trackActiveSource == null) {
+ trackActiveSource = GeoJsonSource(TRACK_ACTIVE_SOURCE_ID)
+ style.addSource(trackActiveSource!!)
+ style.addLayer(LineLayer(TRACK_ACTIVE_LAYER_ID, TRACK_ACTIVE_SOURCE_ID).apply {
setProperties(
PropertyFactory.lineColor("#E53935"),
PropertyFactory.lineWidth(4f),
+ PropertyFactory.lineCap("round")
+ )
+ })
+ }
+
+ // Past tracks layer (Dotted)
+ if (trackPastSource == null) {
+ trackPastSource = GeoJsonSource(TRACK_PAST_SOURCE_ID)
+ style.addSource(trackPastSource!!)
+ style.addLayer(LineLayer(TRACK_PAST_LAYER_ID, TRACK_PAST_SOURCE_ID).apply {
+ setProperties(
+ PropertyFactory.lineColor("#E53935"),
+ PropertyFactory.lineWidth(3f),
PropertyFactory.lineDasharray(arrayOf(1f, 2f)),
PropertyFactory.lineCap("round")
)
})
}
- if (points.size >= 2) {
- val coords = points.map { Point.fromLngLat(it.lon, it.lat) }
- trackSource?.setGeoJson(Feature.fromGeometry(LineString.fromLngLats(coords)))
+
+ // Update Active Track
+ if (activePoints.size >= 2) {
+ val coords = activePoints.map { Point.fromLngLat(it.lon, it.lat) }
+ trackActiveSource?.setGeoJson(Feature.fromGeometry(LineString.fromLngLats(coords)))
+ } else {
+ trackActiveSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList()))
+ }
+
+ // Update Past Tracks
+ if (pastTracks.isNotEmpty()) {
+ val features = pastTracks.map { track ->
+ Feature.fromGeometry(LineString.fromLngLats(track.map { Point.fromLngLat(it.lon, it.lat) }))
+ }
+ trackPastSource?.setGeoJson(FeatureCollection.fromFeatures(features))
} else {
- trackSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList()))
+ trackPastSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList()))
}
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt
index e950b5d..4bc0c7a 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt
@@ -48,6 +48,13 @@ class SafetyFragment : Fragment() {
view.findViewById<MaterialButton>(R.id.button_anchor_config).setOnClickListener {
listener?.onConfigureAnchor()
}
+
+ view.findViewById<MaterialButton>(R.id.button_plan_trip).setOnClickListener {
+ parentFragmentManager.beginTransaction()
+ .replace(R.id.fragment_container, org.terst.nav.tripreport.PreTripReportFragment())
+ .addToBackStack(null)
+ .commit()
+ }
}
fun updateAnchorStatus(statusText: String) {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
index 86fd67c..1c797d5 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
@@ -16,6 +16,7 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
+import com.google.android.material.button.MaterialButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.launch
import org.terst.nav.R
diff --git a/android-app/app/src/main/res/layout/fragment_pretrip_report.xml b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml
new file mode 100644
index 0000000..d7ede49
--- /dev/null
+++ b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="24dp">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Pre-Trip Planning"
+ android:textSize="24sp"
+ android:textStyle="bold"
+ android:layout_marginBottom="16dp" />
+
+ <!-- Boat Config (Simple for now) -->
+ <com.google.android.material.card.MaterialCardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="24dp"
+ app:cardCornerRadius="12dp"
+ app:strokeWidth="1dp"
+ app:strokeColor="?attr/colorOutlineVariant">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Vessel Profile"
+ android:textStyle="bold"
+ android:layout_marginBottom="8dp" />
+
+ <TextView
+ android:id="@+id/tv_vessel_info"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="35ft Sloop (Monohull)"
+ android:textSize="14sp" />
+
+ </LinearLayout>
+ </com.google.android.material.card.MaterialCardView>
+
+ <!-- Report Content -->
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/card_report"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:cardCornerRadius="16dp"
+ app:cardElevation="4dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="20dp">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Weather Summary"
+ android:textStyle="bold"
+ android:textSize="18sp"
+ android:layout_marginBottom="8dp" />
+
+ <TextView
+ android:id="@+id/tv_weather_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:layout_marginBottom="16dp" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?attr/colorOutlineVariant"
+ android:layout_marginBottom="16dp" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Routing Suggestion"
+ android:textStyle="bold"
+ android:textSize="18sp"
+ android:layout_marginBottom="8dp" />
+
+ <TextView
+ android:id="@+id/tv_routing_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:layout_marginBottom="16dp" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?attr/colorOutlineVariant"
+ android:layout_marginBottom="16dp" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Sail Plan"
+ android:textStyle="bold"
+ android:textSize="18sp"
+ android:layout_marginBottom="8dp" />
+
+ <TextView
+ android:id="@+id/tv_sail_plan_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="16sp" />
+
+ </LinearLayout>
+ </com.google.android.material.card.MaterialCardView>
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/btn_generate_pretrip"
+ android:layout_width="match_parent"
+ android:layout_height="60dp"
+ android:layout_marginTop="24dp"
+ android:text="GENERATE PRE-TRIP REPORT" />
+
+ <ProgressBar
+ android:id="@+id/progress_pretrip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="16dp"
+ android:visibility="gone" />
+
+ </LinearLayout>
+</ScrollView>
diff --git a/android-app/app/src/main/res/layout/fragment_safety.xml b/android-app/app/src/main/res/layout/fragment_safety.xml
index 5b2397e..f90420e 100644
--- a/android-app/app/src/main/res/layout/fragment_safety.xml
+++ b/android-app/app/src/main/res/layout/fragment_safety.xml
@@ -104,4 +104,13 @@
</com.google.android.material.card.MaterialCardView>
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button_plan_trip"
+ style="@style/Widget.Material3.Button.TonalButton"
+ android:layout_width="match_parent"
+ android:layout_height="60dp"
+ android:layout_marginTop="24dp"
+ android:text="PLAN TRIP (PRE-TRIP REPORT)"
+ app:layout_constraintTop_toBottomOf="@id/card_anchor" />
+
</androidx.constraintlayout.widget.ConstraintLayout>