summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/safety/AnchorWatchState.kt23
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/ui/anchorwatch/AnchorWatchHandler.kt58
-rw-r--r--android-app/app/src/main/res/layout/fragment_anchor_watch.xml79
-rwxr-xr-xandroid-app/app/src/main/res/values/strings.xml10
-rw-r--r--android-app/app/src/test/kotlin/com/example/androidapp/safety/AnchorWatchStateTest.kt32
5 files changed, 202 insertions, 0 deletions
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/safety/AnchorWatchState.kt b/android-app/app/src/main/kotlin/com/example/androidapp/safety/AnchorWatchState.kt
new file mode 100644
index 0000000..507736e
--- /dev/null
+++ b/android-app/app/src/main/kotlin/com/example/androidapp/safety/AnchorWatchState.kt
@@ -0,0 +1,23 @@
+package com.example.androidapp.safety
+
+/**
+ * Holds UI-facing state for the anchor watch setup screen and provides
+ * the suggested watch-circle radius derived from depth and rode out.
+ */
+class AnchorWatchState {
+
+ /**
+ * Returns the recommended watch-circle radius (metres) for the given depth
+ * and amount of rode deployed.
+ *
+ * Uses the Pythagorean formula via [ScopeCalculator.watchCircleRadius] when
+ * the geometry is valid (rode > depth + freeboard). Falls back to [rodeOutM]
+ * itself as the maximum possible swing radius when the rode is too short to
+ * form a catenary angle.
+ */
+ fun calculateRecommendedWatchCircleRadius(depthM: Double, rodeOutM: Double): Double {
+ val vertical = depthM + 2.0 // 2 m default freeboard
+ return if (rodeOutM > vertical) ScopeCalculator.watchCircleRadius(rodeOutM, depthM)
+ else rodeOutM
+ }
+}
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/anchorwatch/AnchorWatchHandler.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/anchorwatch/AnchorWatchHandler.kt
new file mode 100644
index 0000000..bc82795
--- /dev/null
+++ b/android-app/app/src/main/kotlin/com/example/androidapp/ui/anchorwatch/AnchorWatchHandler.kt
@@ -0,0 +1,58 @@
+package com.example.androidapp.ui.anchorwatch
+
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.example.androidapp.R
+import com.example.androidapp.databinding.FragmentAnchorWatchBinding
+import com.example.androidapp.safety.AnchorWatchState
+
+class AnchorWatchHandler : Fragment() {
+
+ private var _binding: FragmentAnchorWatchBinding? = null
+ private val binding get() = _binding!!
+
+ private val anchorWatchState = AnchorWatchState()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentAnchorWatchBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val watcher = object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
+ override fun afterTextChanged(s: Editable?) = updateSuggestedRadius()
+ }
+ binding.etDepth.addTextChangedListener(watcher)
+ binding.etRodeOut.addTextChangedListener(watcher)
+ }
+
+ private fun updateSuggestedRadius() {
+ val depth = binding.etDepth.text.toString().toDoubleOrNull()
+ val rode = binding.etRodeOut.text.toString().toDoubleOrNull()
+
+ if (depth != null && rode != null && depth >= 0.0 && rode > 0.0) {
+ val radius = anchorWatchState.calculateRecommendedWatchCircleRadius(depth, rode)
+ binding.tvSuggestedRadius.text =
+ getString(R.string.anchor_suggested_radius_fmt, radius)
+ } else {
+ binding.tvSuggestedRadius.text = getString(R.string.anchor_suggested_radius_empty)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/android-app/app/src/main/res/layout/fragment_anchor_watch.xml b/android-app/app/src/main/res/layout/fragment_anchor_watch.xml
new file mode 100644
index 0000000..96b9969
--- /dev/null
+++ b/android-app/app/src/main/res/layout/fragment_anchor_watch.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/anchor_watch_title"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ android:layout_marginBottom="24dp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/anchor_depth_label"
+ android:textSize="14sp"
+ android:layout_marginBottom="4dp" />
+
+ <EditText
+ android:id="@+id/etDepth"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/anchor_depth_hint"
+ android:inputType="numberDecimal"
+ android:importantForAutofill="no"
+ android:layout_marginBottom="16dp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/anchor_rode_label"
+ android:textSize="14sp"
+ android:layout_marginBottom="4dp" />
+
+ <EditText
+ android:id="@+id/etRodeOut"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/anchor_rode_hint"
+ android:inputType="numberDecimal"
+ android:importantForAutofill="no"
+ android:layout_marginBottom="24dp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/anchor_suggested_radius_label"
+ android:textSize="14sp"
+ android:layout_marginBottom="4dp" />
+
+ <TextView
+ android:id="@+id/tvSuggestedRadius"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/anchor_suggested_radius_empty"
+ android:textSize="18sp"
+ android:textStyle="bold"
+ android:layout_marginBottom="4dp" />
+
+ <TextView
+ android:id="@+id/tvSuggestedRadiusHint"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/anchor_suggested_radius_hint"
+ android:textSize="12sp"
+ android:alpha="0.6" />
+
+ </LinearLayout>
+
+</ScrollView>
diff --git a/android-app/app/src/main/res/values/strings.xml b/android-app/app/src/main/res/values/strings.xml
index 499ba8d..756f5e3 100755
--- a/android-app/app/src/main/res/values/strings.xml
+++ b/android-app/app/src/main/res/values/strings.xml
@@ -58,4 +58,14 @@
<string name="temp_fmt">%.0f °C</string>
<string name="precip_fmt">%d%%</string>
<string name="permission_rationale">Location is needed to show weather for your current position.</string>
+ <string name="nav_anchor_watch">Anchor</string>
+ <string name="anchor_watch_title">Anchor Watch</string>
+ <string name="anchor_depth_label">Depth (m)</string>
+ <string name="anchor_depth_hint">e.g. 5.0</string>
+ <string name="anchor_rode_label">Rode Out (m)</string>
+ <string name="anchor_rode_hint">e.g. 30.0</string>
+ <string name="anchor_suggested_radius_label">Suggested Watch Radius</string>
+ <string name="anchor_suggested_radius_empty">—</string>
+ <string name="anchor_suggested_radius_fmt">%.1f m</string>
+ <string name="anchor_suggested_radius_hint">Calculated from rode and depth using Pythagorean scope formula (2 m freeboard assumed)</string>
</resources>
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/safety/AnchorWatchStateTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/safety/AnchorWatchStateTest.kt
new file mode 100644
index 0000000..40f7df0
--- /dev/null
+++ b/android-app/app/src/test/kotlin/com/example/androidapp/safety/AnchorWatchStateTest.kt
@@ -0,0 +1,32 @@
+package com.example.androidapp.safety
+
+import org.junit.Assert.*
+import org.junit.Test
+import kotlin.math.sqrt
+
+class AnchorWatchStateTest {
+
+ private val state = AnchorWatchState()
+
+ @Test
+ fun calculateRecommendedWatchCircleRadius_validGeometry() {
+ // depth=6m, rode=50m → vertical=8m, radius=sqrt(50²-8²)=sqrt(2436)
+ val expected = sqrt(2436.0)
+ val actual = state.calculateRecommendedWatchCircleRadius(depthM = 6.0, rodeOutM = 50.0)
+ assertEquals(expected, actual, 0.001)
+ }
+
+ @Test
+ fun calculateRecommendedWatchCircleRadius_rodeShorterThanVertical_fallsBackToRode() {
+ // depth=10m, rode=5m → vertical=12m > rode, fallback returns rode
+ val actual = state.calculateRecommendedWatchCircleRadius(depthM = 10.0, rodeOutM = 5.0)
+ assertEquals(5.0, actual, 0.001)
+ }
+
+ @Test
+ fun calculateRecommendedWatchCircleRadius_rodeEqualsVertical_fallsBackToRode() {
+ // depth=8m, rode=10m → vertical=10m == rode, fallback returns rode
+ val actual = state.calculateRecommendedWatchCircleRadius(depthM = 8.0, rodeOutM = 10.0)
+ assertEquals(10.0, actual, 0.001)
+ }
+}