summaryrefslogtreecommitdiff
path: root/test-runner/src/test/kotlin/org/terst/nav/ais/CpaAlertTest.kt
blob: 3effb24962d336dedaa758b8946d164c4daf4447 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package org.terst.nav.ais

import org.junit.Assert.*
import org.junit.Test

class CpaAlertTest {

    // --- CpaCalculator ---

    @Test
    fun `head-on convergence produces small CPA and positive TCPA`() {
        // Own vessel heading north at 10 kt; target heading south at 10 kt, 1 nm ahead
        val (cpa, tcpa) = CpaCalculator.compute(
            ownLat = 0.0, ownLon = 0.0, ownSog = 10.0, ownCog = 0.0,
            tgtLat = 1.0 / 60.0, tgtLon = 0.0, tgtSog = 10.0, tgtCog = 180.0
        )
        assertTrue("CPA should be near zero, got $cpa", cpa < 0.1)
        assertTrue("TCPA should be positive, got $tcpa", tcpa > 0)
        assertTrue("TCPA should be < 5 min, got $tcpa", tcpa < 5.0)
    }

    @Test
    fun `diverging vessels produce negative TCPA`() {
        // Own heading north, target heading north ahead — both going same direction with target faster
        val (_, tcpa) = CpaCalculator.compute(
            ownLat = 0.0, ownLon = 0.0, ownSog = 5.0, ownCog = 0.0,
            tgtLat = 1.0 / 60.0, tgtLon = 0.0, tgtSog = 10.0, tgtCog = 0.0
        )
        assertTrue("TCPA should be negative for diverging vessels, got $tcpa", tcpa < 0)
    }

    @Test
    fun `zero relative velocity returns current distance`() {
        // Same velocity — parallel course, same speed
        val (cpa, tcpa) = CpaCalculator.compute(
            ownLat = 0.0, ownLon = 0.0, ownSog = 5.0, ownCog = 90.0,
            tgtLat = 1.0 / 60.0, tgtLon = 0.0, tgtSog = 5.0, tgtCog = 90.0
        )
        assertTrue("CPA should equal current distance, got $cpa", cpa > 0.9 && cpa < 1.1)
        assertEquals(0.0, tcpa, 1e-9)
    }

    @Test
    fun `well-separated crossing vessels produce large CPA`() {
        // Perpendicular courses but far apart — should not alert
        val (cpa, _) = CpaCalculator.compute(
            ownLat = 0.0, ownLon = 0.0, ownSog = 8.0, ownCog = 0.0,
            tgtLat = 0.0, tgtLon = 5.0, tgtSog = 8.0, tgtCog = 270.0
        )
        assertTrue("CPA should be > 2 nm for well-separated vessels, got $cpa", cpa > 2.0)
    }

    // --- Severity classification ---

    @Test
    fun `DANGER severity for CPA under 0_5 nm`() {
        val alert = CpaAlert(123, "TARGET", 0.3, 10.0, CpaSeverity.DANGER)
        assertEquals(CpaSeverity.DANGER, alert.severity)
    }

    @Test
    fun `label formats correctly with vessel name`() {
        val alert = CpaAlert(123456789, "STENA IMPERO", 0.8, 12.5, CpaSeverity.WARNING)
        val lbl = alert.label()
        assertTrue(lbl.contains("0.8"))
        assertTrue(lbl.contains("13") || lbl.contains("12"))  // rounded min
        assertTrue(lbl.contains("STENA"))
    }

    @Test
    fun `label uses MMSI prefix when name is blank`() {
        val alert = CpaAlert(987654321, "", 1.5, 20.0, CpaSeverity.CAUTION)
        // "MMSI 987654321" is 14 chars; label() takes the first 12 -> "MMSI 9876543"
        val lbl = alert.label()
        assertTrue("Expected MMSI prefix in label, got: $lbl", lbl.contains("MMSI"))
        assertTrue("Expected partial MMSI in label, got: $lbl", lbl.contains("987654"))
    }
}