summaryrefslogtreecommitdiff
path: root/test-runner/src/main/kotlin/org/terst/nav/nmea/AisVdmParser.kt
blob: 9709d21474d6102b222de023cf6b35e2e35ff314 (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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package org.terst.nav.nmea

import org.terst.nav.ais.AisVessel

class AisVdmParser {
    // Keyed by seqId -> list of (seq, payload)
    private val fragments = mutableMapOf<String, MutableList<Pair<Int, String>>>()

    fun parse(sentence: String): AisVessel? {
        if (!sentence.startsWith("!AIVDM") && !sentence.startsWith("!AIVDO")) return null

        val withoutBang = sentence.drop(1)
        val starIdx = withoutBang.indexOf('*')
        val body = if (starIdx >= 0) withoutBang.substring(0, starIdx) else withoutBang

        val fields = body.split(",")
        if (fields.size < 7) return null

        val count = fields[1].toIntOrNull() ?: return null
        val seq = fields[2].toIntOrNull() ?: return null
        val seqId = fields[3]
        val payload = fields[5]
        val padding = fields[6].toIntOrNull() ?: 0

        val combinedPayload: String
        if (count <= 1) {
            combinedPayload = payload
        } else {
            val list = fragments.getOrPut(seqId) { mutableListOf() }
            list.add(seq to payload)
            if (list.size < count) return null
            fragments.remove(seqId)
            combinedPayload = list.sortedBy { it.first }.joinToString("") { it.second }
        }

        val bits = decodeToBits(combinedPayload, padding)
        if (bits.size < 6) return null

        return when (extractUInt(bits, 0, 6)) {
            1, 2, 3 -> parseType123(bits)
            5 -> parseType5(bits)
            else -> null
        }
    }

    private fun decodeToBits(payload: String, padding: Int): IntArray {
        val allBits = ArrayList<Int>(payload.length * 6)
        for (c in payload) {
            var v = c.code - 48
            if (v > 39) v -= 8
            for (b in 5 downTo 0) allBits.add((v ushr b) and 1)
        }
        return if (padding > 0 && padding < allBits.size)
            allBits.dropLast(padding).toIntArray()
        else
            allBits.toIntArray()
    }

    private fun extractUInt(bits: IntArray, start: Int, len: Int): Int {
        var v = 0
        for (i in 0 until len) {
            val bit = if (start + i < bits.size) bits[start + i] else 0
            v = (v shl 1) or bit
        }
        return v
    }

    private fun extractInt(bits: IntArray, start: Int, len: Int): Int {
        val unsigned = extractUInt(bits, start, len)
        return if (len > 0 && (unsigned and (1 shl (len - 1))) != 0)
            unsigned - (1 shl len)
        else
            unsigned
    }

    private fun parseType123(bits: IntArray): AisVessel? {
        if (bits.size < 137) return null
        val mmsi = extractUInt(bits, 8, 30)
        val sogRaw = extractUInt(bits, 50, 10)
        val sog = if (sogRaw == 1023) 0.0 else sogRaw / 10.0
        val lonRaw = extractInt(bits, 61, 28)
        val lon = if (lonRaw == 0x6791AC0) 0.0 else lonRaw / 600000.0
        val latRaw = extractInt(bits, 89, 27)
        val lat = if (latRaw == 0x3412140) 0.0 else latRaw / 600000.0
        val cogRaw = extractUInt(bits, 116, 12)
        val cog = if (cogRaw == 3600) 0.0 else cogRaw / 10.0
        val heading = extractUInt(bits, 128, 9)
        return AisVessel(
            mmsi = mmsi,
            name = "",
            callsign = "",
            lat = lat,
            lon = lon,
            sog = sog,
            cog = cog,
            heading = heading,
            vesselType = 0,
            timestampMs = System.currentTimeMillis()
        )
    }

    private fun parseType5(bits: IntArray): AisVessel? {
        if (bits.size < 240) return null
        val mmsi = extractUInt(bits, 8, 30)
        val callsign = decodeText(bits, 70, 7).trimEnd('@', ' ')
        val name = decodeText(bits, 112, 20).trimEnd('@', ' ')
        val vesselType = extractUInt(bits, 232, 8)
        return AisVessel(
            mmsi = mmsi,
            name = name,
            callsign = callsign,
            lat = 0.0, lon = 0.0, sog = 0.0, cog = 0.0,
            heading = 511,
            vesselType = vesselType,
            timestampMs = System.currentTimeMillis()
        )
    }

    // AIS 6-bit text: bits<32 -> bits+64 (maps to @,A-Z,...), else bits (maps to space,digits,symbols)
    private fun decodeText(bits: IntArray, start: Int, nChars: Int): String {
        val sb = StringBuilder(nChars)
        for (i in 0 until nChars) {
            val v = extractUInt(bits, start + i * 6, 6)
            val c = if (v < 32) v + 64 else v
            sb.append(c.toChar())
        }
        return sb.toString()
    }
}