diff options
Diffstat (limited to 'android-app/app/src/main')
3 files changed, 103 insertions, 0 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ais/AisHubApiService.kt b/android-app/app/src/main/kotlin/org/terst/nav/ais/AisHubApiService.kt new file mode 100644 index 0000000..9946e5c --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ais/AisHubApiService.kt @@ -0,0 +1,31 @@ +package org.terst.nav.data.api + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import retrofit2.http.GET +import retrofit2.http.Query + +@JsonClass(generateAdapter = true) +data class AisHubVessel( + @Json(name = "MMSI") val mmsi: String, + @Json(name = "LATITUDE") val latitude: String, + @Json(name = "LONGITUDE") val longitude: String, + @Json(name = "SOG") val sog: String, + @Json(name = "COG") val cog: String, + @Json(name = "HEADING") val heading: String, + @Json(name = "NAME") val name: String, + @Json(name = "TYPE") val type: String +) + +interface AisHubApiService { + @GET("/0/down") + suspend fun getVessels( + @Query("username") username: String, + @Query("password") password: String, + @Query("latmin") latMin: Double, + @Query("latmax") latMax: Double, + @Query("lonmin") lonMin: Double, + @Query("lonmax") lonMax: Double, + @Query("output") output: String = "json" + ): List<AisHubVessel> +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ais/AisHubSource.kt b/android-app/app/src/main/kotlin/org/terst/nav/ais/AisHubSource.kt new file mode 100644 index 0000000..955f9d2 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ais/AisHubSource.kt @@ -0,0 +1,16 @@ +package org.terst.nav.ais + +import org.terst.nav.data.api.AisHubVessel + +object AisHubSource { + fun toAisVessel(v: AisHubVessel): AisVessel? { + val mmsi = v.mmsi.toIntOrNull() ?: return null + val lat = v.latitude.toDoubleOrNull() ?: return null + val lon = v.longitude.toDoubleOrNull() ?: return null + val sog = v.sog.toDoubleOrNull() ?: 0.0 + val cog = v.cog.toDoubleOrNull() ?: 0.0 + val heading = v.heading.toIntOrNull() ?: 511 + val vesselType = v.type.toIntOrNull() ?: 0 + return AisVessel(mmsi, v.name.trim(), "", lat, lon, sog, cog, heading, vesselType, System.currentTimeMillis()) + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ais/AisRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/ais/AisRepository.kt new file mode 100644 index 0000000..4b90c38 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ais/AisRepository.kt @@ -0,0 +1,56 @@ +package org.terst.nav.ais + +import kotlinx.coroutines.flow.Flow +import org.terst.nav.nmea.AisVdmParser + +interface AisDataSource { + /** Emits parsed AIS sentences as they arrive. Close the source to stop. */ + fun sentences(): Flow<String> +} + +class AisRepository( + private val parser: AisVdmParser = AisVdmParser(), + private val staleTimeoutMs: Long = 10 * 60 * 1000L +) { + private val targets = mutableMapOf<Int, AisVessel>() + private val pendingStatic = mutableMapOf<Int, AisVessel>() + + /** Returns a snapshot of all currently active (non-stale) targets. */ + fun getTargets(): List<AisVessel> = targets.values.toList() + + /** Process one raw NMEA sentence. Updates internal state. */ + fun processSentence(sentence: String) { + val vessel = parser.parse(sentence) ?: return + if (vessel.lat == 0.0 && vessel.lon == 0.0 && vessel.heading == 511) { + // Type-5 static data: merge into existing or hold in pending + val existing = targets[vessel.mmsi] + if (existing != null) { + targets[vessel.mmsi] = existing.copy( + name = vessel.name, + callsign = vessel.callsign, + vesselType = vessel.vesselType + ) + } else { + pendingStatic[vessel.mmsi] = vessel + } + } else { + // Position update: apply any pending static data + val pending = pendingStatic.remove(vessel.mmsi) + targets[vessel.mmsi] = if (pending != null) { + vessel.copy( + name = pending.name, + callsign = pending.callsign, + vesselType = pending.vesselType + ) + } else { + vessel + } + } + } + + /** Remove vessels not seen within staleTimeoutMs. */ + fun evictStale(nowMs: Long = System.currentTimeMillis()) { + val threshold = nowMs - staleTimeoutMs + targets.entries.removeIf { it.value.timestampMs < threshold } + } +} |
