Skip to content

Commit 60fa6b1

Browse files
committed
Add asteroid alerts
1 parent 1c13abe commit 60fa6b1

File tree

5 files changed

+232
-6
lines changed

5 files changed

+232
-6
lines changed

app/src/main/java/com/kylecorry/bell/infrastructure/alerts/AlertUpdater.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.kylecorry.bell.infrastructure.alerts.fire.InciwebWildfireAlertSource
1313
import com.kylecorry.bell.infrastructure.alerts.health.HealthAlertNetworkAlertSource
1414
import com.kylecorry.bell.infrastructure.alerts.health.USOutbreaksAlertSource
1515
import com.kylecorry.bell.infrastructure.alerts.space_weather.SWPCAlertSource
16+
import com.kylecorry.bell.infrastructure.alerts.space_weather.SentryAsteroidAlertSource
1617
import com.kylecorry.bell.infrastructure.alerts.travel.TravelAdvisoryAlertSource
1718
import com.kylecorry.bell.infrastructure.alerts.volcano.USGSVolcanoAlertSource
1819
import com.kylecorry.bell.infrastructure.alerts.water.NationalTsunamiAlertSource
@@ -141,7 +142,8 @@ class AlertUpdater(private val context: Context) {
141142
FuelPricesAlertSource(context),
142143
USOutbreaksAlertSource(context),
143144
IC3InternetCrimeAlertSource(context),
144-
NationalTerrorismAdvisoryAlertSource(context)
145+
NationalTerrorismAdvisoryAlertSource(context),
146+
SentryAsteroidAlertSource(context)
145147
)
146148
}
147149

app/src/main/java/com/kylecorry/bell/infrastructure/alerts/space_weather/SWPCAlertSource.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import android.content.Context
44
import com.kylecorry.bell.domain.Alert
55
import com.kylecorry.bell.domain.Category
66
import com.kylecorry.bell.domain.Certainty
7-
import com.kylecorry.bell.domain.Constants
87
import com.kylecorry.bell.domain.Severity
98
import com.kylecorry.bell.domain.Urgency
109
import com.kylecorry.bell.infrastructure.alerts.AlertLoader
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package com.kylecorry.bell.infrastructure.alerts.space_weather
2+
3+
import android.content.Context
4+
import com.kylecorry.bell.domain.Alert
5+
import com.kylecorry.bell.domain.Category
6+
import com.kylecorry.bell.domain.Certainty
7+
import com.kylecorry.bell.domain.Severity
8+
import com.kylecorry.bell.domain.Urgency
9+
import com.kylecorry.bell.infrastructure.alerts.AlertLoader
10+
import com.kylecorry.bell.infrastructure.alerts.AlertSource
11+
import com.kylecorry.bell.infrastructure.alerts.FileType
12+
import com.kylecorry.bell.infrastructure.parsers.DateTimeParser
13+
import com.kylecorry.bell.infrastructure.parsers.selectors.Selector.Companion.text
14+
import com.kylecorry.bell.infrastructure.parsers.selectors.select
15+
import com.kylecorry.bell.ui.FormatService
16+
import com.kylecorry.luna.text.toFloatCompat
17+
import com.kylecorry.sol.math.SolMath.roundPlaces
18+
import java.net.URLEncoder
19+
import java.time.Duration
20+
import java.time.Instant
21+
import java.time.ZoneId
22+
import java.time.ZonedDateTime
23+
24+
class SentryAsteroidAlertSource(context: Context) : AlertSource {
25+
26+
private val loader = AlertLoader(context)
27+
private val formatter = FormatService.getInstance(context)
28+
29+
private val torinoScaleDescription = mapOf(
30+
3 to "A close encounter, meriting attention by astronomers. Current calculations give a 1% or greater chance of collision capable of localized destruction. Most likely, new telescopic observations will lead to re-assignment to Level 0.",
31+
4 to "A close encounter, meriting attention by astronomers. Current calculations give a 1% or greater chance of collision capable of regional devastation. Most likely, new telescopic observations will lead to re-assignment to Level 0.",
32+
5 to "A close encounter posing a serious, but still uncertain threat of regional devastation. Critical attention by astronomers is needed to determine conclusively whether or not a collision will occur. Governmental contingency planning may be warranted.",
33+
6 to "A close encounter by a large object posing a serious but still uncertain threat of a global catastrophe. Critical attention by astronomers is needed to determine conclusively whether or not a collision will occur. Governmental contingency planning may be warranted.",
34+
7 to "A very close encounter by a large object, which if occurring over the next century, poses an unprecedented but still uncertain threat of a global catastrophe. For such a threat, international contingency planning is warranted, especially to determine urgently and conclusively whether or not a collision will occur.",
35+
8 to "A collision is certain, capable of causing localized destruction for an impact over land or possibly a tsunami if close offshore.",
36+
9 to "A collision is certain, capable of causing unprecedented regional devastation for a land impact or the threat of a major tsunami for an ocean impact.",
37+
20 to "A collision is certain, capable of causing global climatic catastrophe that may threaten the future of civilization as we know it, whether impacting land or ocean."
38+
)
39+
40+
private val ignoreLowLevels = true
41+
42+
override suspend fun load(): List<Alert> {
43+
val rawAlerts = loader.load(
44+
FileType.JSON,
45+
"https://ssd-api.jpl.nasa.gov/sentry.api?ps-min=-4&ip-min=1e-6",
46+
"$.data",
47+
mapOf(
48+
"id" to text("id"),
49+
"observed" to text("last_obs"),
50+
"name" to text("des"),
51+
"range" to text("range"),
52+
"level" to text("ts_max"),
53+
"velocity" to text("v_inf"),
54+
"diameter" to text("diameter"),
55+
"probability" to text("ip")
56+
),
57+
mitigate304 = false
58+
)
59+
60+
return rawAlerts.mapNotNull {
61+
val id = it["id"] ?: return@mapNotNull null
62+
val name = it["name"] ?: return@mapNotNull null
63+
val observed =
64+
DateTimeParser.parseInstant(
65+
it["observed"] ?: return@mapNotNull null,
66+
ZoneId.of("UTC")
67+
)
68+
?: return@mapNotNull null
69+
val range = it["range"]?.split("-") ?: return@mapNotNull null
70+
val level = it["level"]?.toIntOrNull() ?: return@mapNotNull null
71+
val velocity = it["velocity"]?.toFloatCompat()?.roundPlaces(2)
72+
val diameter = it["diameter"]?.toFloatCompat()?.roundPlaces(4)
73+
val probability = it["probability"]?.toFloatCompat()?.roundPlaces(4)?.times(100)
74+
75+
// Ignore very low level threats
76+
if (ignoreLowLevels && level <= 2) {
77+
return@mapNotNull null
78+
}
79+
80+
val effectiveYear = range.firstOrNull()?.toIntOrNull() ?: return@mapNotNull null
81+
val expirationYear = range.lastOrNull()?.toIntOrNull() ?: return@mapNotNull null
82+
83+
val timeUntilImpact = effectiveYear - ZonedDateTime.now(ZoneId.of("UTC")).year
84+
85+
// If over a decade away and under level 6, ignore
86+
if (ignoreLowLevels && timeUntilImpact > 10 && level < 6) {
87+
return@mapNotNull null
88+
}
89+
90+
// If over 3 decades away and level 6, ignore
91+
if (ignoreLowLevels && timeUntilImpact > 30 && level == 6) {
92+
return@mapNotNull null
93+
}
94+
95+
val severity = when (level) {
96+
in 3..4 -> Severity.Moderate
97+
in 5..8 -> Severity.Severe
98+
in 9..10 -> Severity.Extreme
99+
else -> Severity.Minor
100+
}
101+
102+
val certainty = when (level) {
103+
in 5..7 -> Certainty.Possible
104+
in 8..10 -> Certainty.Likely
105+
else -> Certainty.Unlikely
106+
}
107+
108+
Alert(
109+
id = 0,
110+
identifier = id,
111+
sender = "NASA",
112+
sent = observed,
113+
source = getUUID(),
114+
category = Category.Other,
115+
event = "Potential Asteroid Impact (between $effectiveYear and $expirationYear)",
116+
urgency = Urgency.Future,
117+
severity = severity,
118+
certainty = certainty,
119+
link = "https://ssd-api.jpl.nasa.gov/sentry.api?des=${
120+
URLEncoder.encode(
121+
name,
122+
"UTF-8"
123+
)
124+
}",
125+
description = torinoScaleDescription[level],
126+
effective = ZonedDateTime.of(effectiveYear, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC"))
127+
.toInstant(),
128+
expires = ZonedDateTime.of(
129+
expirationYear,
130+
12,
131+
31,
132+
23,
133+
59,
134+
59,
135+
9999,
136+
ZoneId.of("UTC")
137+
).toInstant(),
138+
parameters = mapOf(
139+
"Name" to name,
140+
"Diameter" to diameter.toString() + " km",
141+
"Velocity" to velocity.toString() + " km/s",
142+
"Probability" to probability.toString() + "%",
143+
"Torino scale" to level.toString(),
144+
"Impact range" to "$effectiveYear to $expirationYear"
145+
),
146+
isDownloadRequired = true,
147+
redownloadIntervalDays = if (timeUntilImpact < 1) 1 else 15
148+
)
149+
}
150+
}
151+
152+
override fun getUUID(): String {
153+
return "12f7c57c-a703-4c2a-aad3-617ee953f2af"
154+
}
155+
156+
override fun updateFromFullText(alert: Alert, fullText: String): Alert {
157+
val impacts = select<Any>(fullText, "data").mapNotNull {
158+
val date =
159+
select(it, text("date"))?.let { parseFractionalDate(it) } ?: return@mapNotNull null
160+
val probability = select(it, text("ip"))?.toFloatCompat()?.times(100)?.roundPlaces(4)
161+
?: return@mapNotNull null
162+
date to probability
163+
}.filter { it.first.isAfter(Instant.now()) }.sortedBy { it.first }
164+
165+
val pdate = select(fullText, text("summary.pdate"))?.let {
166+
DateTimeParser.parseInstant(
167+
it.replace(" ", "T") + "Z",
168+
ZoneId.of("UTC")
169+
)
170+
}
171+
172+
return alert.copy(
173+
sent = pdate ?: alert.sent,
174+
link = "https://cneos.jpl.nasa.gov/sentry/details.html#?des=${
175+
URLEncoder.encode(
176+
alert.parameters?.get(
177+
"Name"
178+
) ?: "", "UTF-8"
179+
)
180+
}",
181+
parameters = alert.parameters?.plus(
182+
"Potential impacts" to "\n" + impacts.joinToString("\n") {
183+
val pct = if (it.second < 1) {
184+
"< 1%"
185+
} else {
186+
"${it.second}%"
187+
}
188+
" - ${
189+
formatter.formatDateTime(
190+
it.first,
191+
includeWeekDay = false,
192+
abbreviateMonth = true
193+
)
194+
}: $pct"
195+
}
196+
),
197+
)
198+
}
199+
200+
private fun parseFractionalDate(date: String): Instant? {
201+
val parts = date.split(".")
202+
if (parts.size != 2) {
203+
return DateTimeParser.parseInstant(date, ZoneId.of("UTC"))
204+
}
205+
206+
val parsed = DateTimeParser.parseInstant(parts[0], ZoneId.of("UTC")) ?: return null
207+
val percent = ("0." + parts[1]).toFloatCompat() ?: return null
208+
return parsed.plus(percentOfDayToDuration(percent))
209+
}
210+
211+
private fun percentOfDayToDuration(percent: Float): Duration {
212+
val hours = (percent * 24).toInt()
213+
val minutes = ((percent * 24 - hours) * 60).toInt()
214+
val seconds = ((percent * 24 - hours - minutes / 60f) * 3600).toInt()
215+
return Duration.ofHours(hours.toLong()).plusMinutes(minutes.toLong())
216+
.plusSeconds(seconds.toLong())
217+
}
218+
}

app/src/main/java/com/kylecorry/bell/infrastructure/parsers/selectors/Selector.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,16 @@ private fun selectJson(node: Any, selector: Selector): String? {
213213
""
214214
}).trimEnd('.').replace(" ", ".")
215215

216-
val elements: Any = JsonPath.read(
217-
node, fullSelector
218-
)
216+
val elements = try {
217+
if (node is String){
218+
JsonPath.read(node.toString(), fullSelector)
219+
} else {
220+
JsonPath.read<Any>(node, fullSelector)
221+
}
222+
} catch (e: Exception) {
223+
e.printStackTrace()
224+
return null
225+
}
219226
val selectedElement = if (elements is List<*> && elements.isEmpty()) {
220227
null
221228
} else if (elements is List<*> && selector.index == null) {

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
androidApplication = "8.8.0"
2+
androidApplication = "8.8.1"
33
andromedaVersion = "13.1.1"
44
constraintlayout = "2.2.0"
55
coreKtx = "1.15.0"

0 commit comments

Comments
 (0)