Added DataParser.kt with tests, removed unused classes

This commit is contained in:
Arty Bishop committed 2021-10-27 21:47:02 +01:00
1 parent c8f7a29812
commit 5998a7e66d
19 files changed
+285 -346

No files matched your search

+1
View File
@@ -13,6 +13,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.json:json:$json_version"
testImplementation "junit:junit:$junit_version"
testImplementation "org.mockito:mockito-core:$mockito_version"
@@ -17,9 +17,9 @@
*/
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.DataParser
import com.rtbishop.look4sat.domain.DataRepository
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.predict.TLE
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -28,6 +28,7 @@ import java.io.InputStream
import java.util.zip.ZipInputStream
class DefaultRepository(
private val dataParser: DataParser,
private val localSource: LocalDataSource,
private val remoteSource: RemoteDataSource,
private val repoDispatcher: CoroutineDispatcher
@@ -66,20 +67,20 @@ class DefaultRepository(
val streams = mutableListOf<InputStream>()
val entries = mutableListOf<SatEntry>()
sources.forEach { source ->
remoteSource.fetchFileStream(source)?.let { stream ->
if (source.contains(".zip", true)) {
val zipStream = ZipInputStream(stream).apply { nextEntry }
streams.add(zipStream)
} else {
streams.add(stream)
}
val fileStream = remoteSource.fetchFileStream(source)
if (source.contains(".zip", true)) {
val zipStream = ZipInputStream(fileStream).apply { nextEntry }
streams.add(zipStream)
} else {
streams.add(fileStream)
}
}
streams.forEach { stream -> entries.addAll(importSatellites(stream)) }
localSource.updateEntries(entries)
}
launch(repoDispatcher) {
val transmitters = remoteSource.fetchTransmitters(transmittersSource)
val jsonStream = remoteSource.fetchFileStream(transmittersSource)
val transmitters = dataParser.parseJSONStream(jsonStream)
localSource.updateTransmitters(transmitters)
}
}
@@ -89,7 +90,7 @@ class DefaultRepository(
localSource.updateSelection(catnums, isSelected)
}
private fun importSatellites(stream: InputStream): List<SatEntry> {
return TLE.parseTleStream(stream).map { tle -> SatEntry(tle) }
private suspend fun importSatellites(stream: InputStream): List<SatEntry> {
return dataParser.parseTLEStream(stream).map { tle -> SatEntry(tle) }
}
}
@@ -17,12 +17,9 @@
*/
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.Transmitter
import java.io.InputStream
interface RemoteDataSource {
suspend fun fetchFileStream(url: String): InputStream?
suspend fun fetchTransmitters(url: String): List<Transmitter>
suspend fun fetchFileStream(fileUrl: String): InputStream
}
@@ -0,0 +1,150 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.model.Transmitter
import com.rtbishop.look4sat.domain.predict.TLE
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow
class DataParser(private val parserDispatcher: CoroutineDispatcher) {
private val calendar = Calendar.getInstance()
private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
suspend fun parseCSVStream(csvStream: InputStream): List<TLE> = withContext(parserDispatcher) {
val parsedItems = mutableListOf<TLE>()
csvStream.bufferedReader().useLines { lines ->
lines.forEachIndexed { index, line ->
if (index != 0) {
val values = line.split(",")
parseCSV(values)?.let { tle -> parsedItems.add(tle) }
}
}
}
return@withContext parsedItems
}
suspend fun parseTLEStream(tleStream: InputStream): List<TLE> = withContext(parserDispatcher) {
val tleStrings = mutableListOf(String(), String(), String())
val parsedItems = mutableListOf<TLE>()
var lineIndex = 0
tleStream.bufferedReader().forEachLine { line ->
tleStrings[lineIndex] = line
if (lineIndex < 2) {
lineIndex++
} else {
val isLineOneValid = tleStrings[1].substring(0, 1) == "1"
val isLineTwoValid = tleStrings[2].substring(0, 1) == "2"
if (!isLineOneValid && !isLineTwoValid) return@forEachLine
parseTLE(tleStrings)?.let { tle -> parsedItems.add(tle) }
lineIndex = 0
}
}
return@withContext parsedItems
}
suspend fun parseJSONStream(jsonStream: InputStream): List<Transmitter> {
return withContext(parserDispatcher) {
val parsedItems = mutableListOf<Transmitter>()
try {
val jsonArray = JSONArray(jsonStream.bufferedReader().readText())
for (index in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(index)
parseJSON(jsonObject)?.let { parsedItems.add(it) }
}
return@withContext parsedItems
} catch (exception: Exception) {
return@withContext parsedItems
}
}
}
private fun parseCSV(values: List<String>): TLE? {
try {
val name = values[0]
calendar.time = simpleDateFormat.parse(values[2]) ?: Date()
val year = calendar.get(Calendar.YEAR).toString().substring(2, 4)
val day = calendar.get(Calendar.DAY_OF_YEAR)
val hours = calendar.get(Calendar.HOUR_OF_DAY)
val minutes = calendar.get(Calendar.MINUTE)
val seconds = calendar.get(Calendar.SECOND)
val fraction = ((hours * 60 * 60 + minutes * 60 + seconds) / 86400.0).toString()
val epoch = "$year$day${fraction.substring(1, fraction.length)}".toDouble()
val meanmo = values[3].toDouble()
val eccn = values[4].toDouble()
val incl = values[5].toDouble()
val raan = values[6].toDouble()
val argper = values[7].toDouble()
val meanan = values[8].toDouble()
val catnum = values[11].toInt()
val bstar = values[14].toDouble()
return TLE(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
} catch (exception: Exception) {
return null
}
}
private fun parseTLE(tle: List<String>): TLE? {
if (tle[1].substring(0, 1) != "1" && tle[2].substring(0, 1) != "2") {
return null
}
try {
val name: String = tle[0].trim()
val epoch: Double = tle[1].substring(18, 32).toDouble()
val meanmo: Double = tle[2].substring(52, 63).toDouble()
val eccn: Double = 1.0e-07 * tle[2].substring(26, 33).toDouble()
val incl: Double = tle[2].substring(8, 16).toDouble()
val raan: Double = tle[2].substring(17, 25).toDouble()
val argper: Double = tle[2].substring(34, 42).toDouble()
val meanan: Double = tle[2].substring(43, 51).toDouble()
val catnum: Int = tle[1].substring(2, 7).trim().toInt()
val bstar: Double = 1.0e-5 * tle[1].substring(53, 59).toDouble() /
10.0.pow(tle[1].substring(60, 61).toDouble())
return TLE(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
} catch (exception: Exception) {
return null
}
}
private fun parseJSON(json: JSONObject): Transmitter? {
try {
val uuid = json.getString("uuid")
val info = json.getString("description")
val isAlive = json.getBoolean("alive")
val downlink = if (json.isNull("downlink_low")) null
else json.getLong("downlink_low")
val uplink = if (json.isNull("uplink_low")) null
else json.getLong("uplink_low")
val mode = if (json.isNull("mode")) null
else json.getString("mode")
val isInverted = json.getBoolean("invert")
val catnum = if (json.isNull("norad_cat_id")) null
else json.getInt("norad_cat_id")
return Transmitter(uuid, info, isAlive, downlink, uplink, mode, isInverted, catnum)
} catch (exception: Exception) {
return null
}
}
}
@@ -1,31 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
data class OMM(
val name: String,
val epochString: String,
val meanmo: Double,
val eccn: Double,
val incl: Double,
val raan: Double,
val argper: Double,
val meanan: Double,
val catnum: Int,
val bstar: Double,
)
@@ -17,9 +17,6 @@
*/
package com.rtbishop.look4sat.domain.predict
import java.io.InputStream
import kotlin.math.pow
data class TLE(
val name: String,
val epoch: Double,
@@ -45,48 +42,4 @@ data class TLE(
else -> NearEarthSat(this)
}
}
companion object {
fun parseTleStream(stream: InputStream): List<TLE> {
val tleStrings = mutableListOf(String(), String(), String())
val parsedItems = mutableListOf<TLE>()
var lineIndex = 0
stream.bufferedReader().forEachLine { line ->
tleStrings[lineIndex] = line
if (lineIndex < 2) {
lineIndex++
} else {
val isLineOneValid = tleStrings[1].substring(0, 1) == "1"
val isLineTwoValid = tleStrings[2].substring(0, 1) == "2"
if (!isLineOneValid && !isLineTwoValid) return@forEachLine
parseTleStrings(tleStrings)?.let { tle -> parsedItems.add(tle) }
lineIndex = 0
}
}
return parsedItems
}
private fun parseTleStrings(tleStrings: List<String>): TLE? {
if (tleStrings[1].substring(0, 1) != "1" && tleStrings[2].substring(0, 1) != "2") {
return null
}
try {
val name: String = tleStrings[0].trim()
val epoch: Double = tleStrings[1].substring(18, 32).toDouble()
val meanmo: Double = tleStrings[2].substring(52, 63).toDouble()
val eccn: Double = 1.0e-07 * tleStrings[2].substring(26, 33).toDouble()
val incl: Double = tleStrings[2].substring(8, 16).toDouble()
val raan: Double = tleStrings[2].substring(17, 25).toDouble()
val argper: Double = tleStrings[2].substring(34, 42).toDouble()
val meanan: Double = tleStrings[2].substring(43, 51).toDouble()
val catnum: Int = tleStrings[1].substring(2, 7).trim().toInt()
val bstar: Double = 1.0e-5 * tleStrings[1].substring(53, 59).toDouble() /
10.0.pow(tleStrings[1].substring(60, 61).toDouble())
return TLE(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
} catch (exception: Exception) {
return null
}
}
}
}
@@ -0,0 +1,89 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat
import com.rtbishop.look4sat.domain.DataParser
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Test
@ExperimentalCoroutinesApi
class DataParserTest {
private val dataParser = DataParser(TestCoroutineDispatcher())
private val validCSVStream = """
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
CALSPHERE 1,1964-063C,2021-10-23T03:34:39.713664,13.73605115,.0026891,90.1713,36.6548,20.0176,64.9795,0,U,900,999,83808,.30972E-3,.299E-5,0
""".trimIndent().byteInputStream()
private val invalidCSVStream = """
CALSPHERE 1,1964-063C,2021-10-23T03:34:39.713664,13.73605115,.0026891,90.1713,36.6548,20.0176,64.9795,0,U,900,999,83808,.30972E-3,.299E-5,0
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
""".trimIndent().byteInputStream()
private val validTLEStream = """
ISS (ZARYA)
1 25544U 98067A 21255.21005818 -.00120443 00000-0 -22592-2 0 9998
2 25544 51.6451 272.4173 0002526 27.1693 64.4213 15.48396490302085
""".trimIndent().byteInputStream()
private val invalidTLEStream = """
1 25544U 98067A 21255.21005818 -.00120443 00000-0 -22592-2 0 9998
2 25544 51.6451 272.4173 0002526 27.1693 64.4213 15.48396490302085
""".trimIndent().byteInputStream()
private val validJSONStream = """
[{"uuid":"UzPz4gcsNBPKPKAFPmer7g","description":"Upper side band (drifting)","alive":true,"type":"Transmitter","uplink_low":null,"uplink_high":null,"uplink_drift":null,"downlink_low":136658500,"downlink_high":null,"downlink_drift":null,"mode":"USB","mode_id":9,"uplink_mode":null,"invert":false,"baud":null,"sat_id":"SCHX-0895-2361-9925-0309","norad_cat_id":965,"status":"active","updated":"2019-04-18T05:39:53.343316Z","citation":"CITATION NEEDED - https://xkcd.com/285/","service":"Unknown","coordination":"","coordination_url":""}]
""".trimIndent().byteInputStream()
private val invalidJSONStream = """
[{"description":"Upper side band (drifting)","alive":true,"type":"Transmitter","uplink_low":null,"uplink_high":null,"uplink_drift":null,"downlink_low":136658500,"downlink_high":null,"downlink_drift":null,"mode":"USB","mode_id":9,"uplink_mode":null,"invert":false,"baud":null,"sat_id":"SCHX-0895-2361-9925-0309","norad_cat_id":965,"status":"active","updated":"2019-04-18T05:39:53.343316Z","citation":"CITATION NEEDED - https://xkcd.com/285/","service":"Unknown","coordination":"","coordination_url":""}]
""".trimIndent().byteInputStream()
@Test
fun `Given valid CSV stream returns valid data`() = runBlockingTest {
val parsedList = dataParser.parseCSVStream(validCSVStream)
assert(parsedList[0].catnum == 900)
}
@Test
fun `Given invalid CSV stream returns empty list`() = runBlockingTest {
val parsedList = dataParser.parseCSVStream(invalidCSVStream)
assert(parsedList.isEmpty())
}
@Test
fun `Given valid TLE stream returns valid data`() = runBlockingTest {
val parsedList = dataParser.parseTLEStream(validTLEStream)
assert(parsedList[0].catnum == 25544)
}
@Test
fun `Given invalid TLE stream returns empty list`() = runBlockingTest {
val parsedList = dataParser.parseTLEStream(invalidTLEStream)
assert(parsedList.isEmpty())
}
@Test
fun `Given valid JSON stream returns valid data`() = runBlockingTest {
val parsedList = dataParser.parseJSONStream(validJSONStream)
assert(parsedList[0].catnum == 965)
}
@Test
fun `Given invalid JSON stream returns empty list`() = runBlockingTest {
val parsedList = dataParser.parseJSONStream(invalidJSONStream)
assert(parsedList.isEmpty())
}
}
@@ -1,56 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat
import com.rtbishop.look4sat.domain.predict.TLE
import org.junit.Test
class TleParserTest {
private val validTleStream = """
ISS (ZARYA)
1 25544U 98067A 21255.21005818 -.00120443 00000-0 -22592-2 0 9998
2 25544 51.6451 272.4173 0002526 27.1693 64.4213 15.48396490302085
SHENZHOU-12
1 48852U 21053A 21254.65783594 .00009779 00000-0 11349-3 0 9992
2 48852 41.4714 115.4555 0001945 272.0324 187.3218 15.61782249 21227
PROGRESS-MS 17
1 48869U 21057A 21254.57379002 .00002620 00000-0 56399-4 0 9993
2 48869 51.6436 275.5624 0003404 23.4371 118.9508 15.48629868301984
""".trimIndent().byteInputStream()
private val invalidTleStream = """
1 25544U 98067A 21255.21005818 -.00120443 00000-0 -22592-2 0 9998
2 25544 51.6451 272.4173 0002526 27.1693 64.4213 15.48396490302085
1 48852U 21053A 21254.65783594 .00009779 00000-0 11349-3 0 9992
2 48852 41.4714 115.4555 0001945 272.0324 187.3218 15.61782249 21227
1 48869U 21057A 21254.57379002 .00002620 00000-0 56399-4 0 9993
2 48869 51.6436 275.5624 0003404 23.4371 118.9508 15.48629868301984
""".trimIndent().byteInputStream()
@Test
fun `Given valid TLE stream returns valid data`() {
val parsedList = TLE.parseTleStream(validTleStream)
assert(parsedList[2].catnum == 48869)
}
@Test
fun `Given invalid TLE stream returns empty list`() {
val parsedList = TLE.parseTleStream(invalidTleStream)
assert(parsedList.isEmpty())
}
}