mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 19:53:08 +00:00
Merge pull request #3272 from vitorpamplona/claude/stoic-lovelace-qv7vy8
Add Roadstr road event support (kinds 1315/1316)
This commit is contained in:
@@ -75,6 +75,8 @@ import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
|
||||
import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
||||
@@ -3870,6 +3872,14 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
consume(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is RoadEventReportEvent -> {
|
||||
consumeRegularEvent(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is RoadEventConfirmationEvent -> {
|
||||
consumeRegularEvent(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is RelayDiscoveryEvent -> {
|
||||
consumeBaseReplaceable(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe
|
||||
|
||||
import com.vitorpamplona.amethyst.service.location.GeoHash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
|
||||
|
||||
fun compute50kmLine(geoHash: GeoHash): List<String> {
|
||||
val hashes = mutableListOf<String>()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.location
|
||||
|
||||
import android.location.Location
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
|
||||
|
||||
/**
|
||||
* Android `Location` glue for the platform-agnostic [GeoHash] that now lives in
|
||||
* Quartz. The encode/decode/neighbor logic is shared in `commonMain`; only these
|
||||
* conversions to/from the Android `Location` type stay here.
|
||||
*/
|
||||
fun Location.toGeoHash(charsCount: Int = GeoHash.MAX_CHAR_PRECISION): GeoHash = GeoHash.encode(latitude, longitude, charsCount)
|
||||
|
||||
fun GeoHash.toLocation(): Location =
|
||||
Location("").also {
|
||||
it.latitude = centerLat
|
||||
it.longitude = centerLon
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.location
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -174,6 +174,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderRelayLeaveRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRelayMembershipList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRelayRemoveMember
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderReport
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRoadEventConfirmation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRoadEventReport
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRootSiteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSoftwareApplication
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSoftwareAsset
|
||||
@@ -235,6 +237,8 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAss
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease
|
||||
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
@@ -1285,6 +1289,14 @@ private fun RenderNoteRow(
|
||||
RenderBirdex(baseNote)
|
||||
}
|
||||
|
||||
is RoadEventReportEvent -> {
|
||||
RenderRoadEventReport(baseNote)
|
||||
}
|
||||
|
||||
is RoadEventConfirmationEvent -> {
|
||||
RenderRoadEventConfirmation(baseNote)
|
||||
}
|
||||
|
||||
is HighlightEvent -> {
|
||||
RenderHighlight(
|
||||
baseNote,
|
||||
|
||||
+2
-1
@@ -30,7 +30,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations
|
||||
import com.vitorpamplona.amethyst.service.location.toGeoHash
|
||||
import com.vitorpamplona.amethyst.service.location.toLocation
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.toGeoHash
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatus
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventType
|
||||
|
||||
/** Emoji marker for a road event category — keeps the card icon-light and locale-independent. */
|
||||
private fun RoadEventType.emoji(): String =
|
||||
when (this) {
|
||||
RoadEventType.POLICE -> "👮"
|
||||
RoadEventType.SPEED_CAMERA -> "📷"
|
||||
RoadEventType.TRAFFIC_JAM -> "🚗"
|
||||
RoadEventType.ACCIDENT -> "💥"
|
||||
RoadEventType.ROAD_CLOSURE -> "⛔"
|
||||
RoadEventType.CONSTRUCTION -> "🚧"
|
||||
RoadEventType.HAZARD -> "⚠️"
|
||||
RoadEventType.ROAD_CONDITION -> "🛣️"
|
||||
RoadEventType.POTHOLE -> "🕳️"
|
||||
RoadEventType.FOG -> "🌫️"
|
||||
RoadEventType.ICE -> "🧊"
|
||||
RoadEventType.ANIMAL -> "🦌"
|
||||
RoadEventType.OTHER -> "📍"
|
||||
}
|
||||
|
||||
private fun RoadEventType.labelRes(): Int =
|
||||
when (this) {
|
||||
RoadEventType.POLICE -> R.string.road_event_police
|
||||
RoadEventType.SPEED_CAMERA -> R.string.road_event_speed_camera
|
||||
RoadEventType.TRAFFIC_JAM -> R.string.road_event_traffic_jam
|
||||
RoadEventType.ACCIDENT -> R.string.road_event_accident
|
||||
RoadEventType.ROAD_CLOSURE -> R.string.road_event_road_closure
|
||||
RoadEventType.CONSTRUCTION -> R.string.road_event_construction
|
||||
RoadEventType.HAZARD -> R.string.road_event_hazard
|
||||
RoadEventType.ROAD_CONDITION -> R.string.road_event_road_condition
|
||||
RoadEventType.POTHOLE -> R.string.road_event_pothole
|
||||
RoadEventType.FOG -> R.string.road_event_fog
|
||||
RoadEventType.ICE -> R.string.road_event_ice
|
||||
RoadEventType.ANIMAL -> R.string.road_event_animal
|
||||
RoadEventType.OTHER -> R.string.road_event_other
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-contained card for a Roadstr road event report (kind 1315).
|
||||
*
|
||||
* Shows the category (emoji + localized label), the optional free-text comment,
|
||||
* and the location resolved from the finest published geohash. Identical in the
|
||||
* feed and the opened thread view, so it takes no `makeItShort` flag — mirroring
|
||||
* [RenderBirdex].
|
||||
*/
|
||||
@Composable
|
||||
fun RenderRoadEventReport(baseNote: Note) {
|
||||
val noteEvent = baseNote.event as? RoadEventReportEvent ?: return
|
||||
|
||||
val type = remember(noteEvent) { noteEvent.roadEventType() }
|
||||
val comment = remember(noteEvent) { noteEvent.content.trim() }
|
||||
val geohash = remember(noteEvent) { noteEvent.geohashes().maxByOrNull { it.length } }
|
||||
|
||||
Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) {
|
||||
val title =
|
||||
if (type != null) {
|
||||
"${type.emoji()} ${stringResource(type.labelRes())}"
|
||||
} else {
|
||||
stringResource(R.string.road_event_unknown)
|
||||
}
|
||||
|
||||
Text(text = title, style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
if (comment.isNotEmpty()) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
text = comment,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
if (geohash != null) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
RoadEventLocation(geohash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-contained card for a Roadstr confirmation/denial (kind 1316).
|
||||
*
|
||||
* Shows whether the referenced report was confirmed (still there) or denied
|
||||
* (no longer there), plus a one-line description of the action.
|
||||
*/
|
||||
@Composable
|
||||
fun RenderRoadEventConfirmation(baseNote: Note) {
|
||||
val noteEvent = baseNote.event as? RoadEventConfirmationEvent ?: return
|
||||
|
||||
val status = remember(noteEvent) { noteEvent.status() }
|
||||
val geohash = remember(noteEvent) { noteEvent.geohashes().maxByOrNull { it.length } }
|
||||
|
||||
Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) {
|
||||
val emoji = if (status == RoadEventStatus.NO_LONGER_THERE) "❌" else "✅"
|
||||
val titleRes =
|
||||
if (status == RoadEventStatus.NO_LONGER_THERE) {
|
||||
R.string.road_event_denied
|
||||
} else {
|
||||
R.string.road_event_confirmed
|
||||
}
|
||||
|
||||
Text(text = "$emoji ${stringResource(titleRes)}", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
if (status == RoadEventStatus.NO_LONGER_THERE) {
|
||||
R.string.road_event_denies_report
|
||||
} else {
|
||||
R.string.road_event_confirms_report
|
||||
},
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
|
||||
if (geohash != null) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
RoadEventLocation(geohash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A single "📍 City" line that reverse-geocodes [geohash], falling back to the raw geohash. */
|
||||
@Composable
|
||||
private fun RoadEventLocation(geohash: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(text = "📍", style = MaterialTheme.typography.bodyMedium)
|
||||
LoadCityName(geohashStr = geohash) { cityName ->
|
||||
Text(
|
||||
text = cityName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -192,6 +192,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderRelayJoinRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRelayLeaveRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRelayMembershipList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRelayRemoveMember
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRoadEventConfirmation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRoadEventReport
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRootSiteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSoftwareApplication
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderSoftwareAsset
|
||||
@@ -250,6 +252,8 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.Softw
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease
|
||||
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
|
||||
@@ -814,6 +818,10 @@ private fun FullBleedNoteCompose(
|
||||
RenderFundraiser(baseNote, makeItShort = false, accountViewModel, nav)
|
||||
} else if (noteEvent is BirdexEvent) {
|
||||
RenderBirdex(baseNote)
|
||||
} else if (noteEvent is RoadEventReportEvent) {
|
||||
RenderRoadEventReport(baseNote)
|
||||
} else if (noteEvent is RoadEventConfirmationEvent) {
|
||||
RenderRoadEventConfirmation(baseNote)
|
||||
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
||||
RenderRepost(baseNote, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is RelayDiscoveryEvent) {
|
||||
|
||||
@@ -3441,6 +3441,25 @@
|
||||
<item quantity="one">%1$s +%2$d more</item>
|
||||
<item quantity="other">%1$s +%2$d more</item>
|
||||
</plurals>
|
||||
<!-- Roadstr road event reports (kinds 1315/1316). "Roadstr" is a proper noun, do not translate. -->
|
||||
<string name="road_event_police">Police</string>
|
||||
<string name="road_event_speed_camera">Speed camera</string>
|
||||
<string name="road_event_traffic_jam">Traffic jam</string>
|
||||
<string name="road_event_accident">Accident</string>
|
||||
<string name="road_event_road_closure">Road closure</string>
|
||||
<string name="road_event_construction">Construction</string>
|
||||
<string name="road_event_hazard">Hazard</string>
|
||||
<string name="road_event_road_condition">Road condition</string>
|
||||
<string name="road_event_pothole">Pothole</string>
|
||||
<string name="road_event_fog">Fog</string>
|
||||
<string name="road_event_ice">Ice</string>
|
||||
<string name="road_event_animal">Animal on the road</string>
|
||||
<string name="road_event_other">Road event</string>
|
||||
<string name="road_event_unknown">Road event</string>
|
||||
<string name="road_event_confirmed">Still there</string>
|
||||
<string name="road_event_denied">No longer there</string>
|
||||
<string name="road_event_confirms_report">Confirms a road report</string>
|
||||
<string name="road_event_denies_report">Reports a road event cleared</string>
|
||||
<string name="goal_amount_label">Goal amount (sats)</string>
|
||||
<string name="goal_amount_placeholder">100000</string>
|
||||
<string name="goal_description_label">Describe your goal</string>
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.confirmation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatus
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadReportTag
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.tags.coordinates
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.tags.latitude
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.tags.longitude
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.tags.roadGeohashes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* Roadstr road event confirmation/denial (kind 1316).
|
||||
*
|
||||
* Validates or refutes a previously published [RoadEventReportEvent]. See
|
||||
* <https://github.com/jooray/roadstr>.
|
||||
*
|
||||
* Tags (canonical order):
|
||||
* - `e` — the referenced report event id ([RoadReportTag]).
|
||||
* - `status` — [RoadEventStatus.STILL_THERE] (confirm) or [RoadEventStatus.NO_LONGER_THERE] (deny).
|
||||
* - `g` / `lat` / `lon` — optional copy of the report's location, so confirmations
|
||||
* are reachable through the same `#g` geohash queries as reports.
|
||||
* - `expiration` — NIP-40 relay garbage-collection window (`created_at + 14 days`).
|
||||
* - `alt` — NIP-31 human-readable fallback.
|
||||
*/
|
||||
@Immutable
|
||||
class RoadEventConfirmationEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider {
|
||||
override fun eventHints() = tags.mapNotNull(RoadReportTag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(RoadReportTag::parseId)
|
||||
|
||||
/** The referenced report event id from the first `e` tag. */
|
||||
fun reportId() = tags.roadReport()?.eventId
|
||||
|
||||
fun status() = tags.roadEventStatus()
|
||||
|
||||
fun isConfirmation() = status() == RoadEventStatus.STILL_THERE
|
||||
|
||||
fun isDenial() = status() == RoadEventStatus.NO_LONGER_THERE
|
||||
|
||||
fun geohashes() = tags.geohashes()
|
||||
|
||||
fun latitude() = tags.latitude()
|
||||
|
||||
fun longitude() = tags.longitude()
|
||||
|
||||
fun alt() = tags.firstNotNullOfOrNull(AltTag::parse)
|
||||
|
||||
fun relayExpiration() = tags.expiration()
|
||||
|
||||
companion object {
|
||||
const val KIND = 1316
|
||||
const val ALT_DESCRIPTION = "Roadstr: event confirmation"
|
||||
|
||||
fun build(
|
||||
reportId: HexKey,
|
||||
status: RoadEventStatus,
|
||||
latitude: Double? = null,
|
||||
longitude: Double? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<RoadEventConfirmationEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
report(reportId)
|
||||
status(status)
|
||||
if (latitude != null && longitude != null) {
|
||||
roadGeohashes(latitude, longitude)
|
||||
coordinates(latitude, longitude)
|
||||
}
|
||||
expiration(createdAt + RoadEventReportEvent.RELAY_TTL_SECONDS)
|
||||
alt(ALT_DESCRIPTION)
|
||||
initializer()
|
||||
}
|
||||
|
||||
fun build(
|
||||
report: EventHintBundle<RoadEventReportEvent>,
|
||||
status: RoadEventStatus,
|
||||
latitude: Double? = null,
|
||||
longitude: Double? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<RoadEventConfirmationEvent>.() -> Unit = {},
|
||||
) = build(report.event.id, status, latitude, longitude, createdAt) {
|
||||
report(report)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.confirmation
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatus
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatusTag
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadReportTag
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
|
||||
fun TagArrayBuilder<RoadEventConfirmationEvent>.report(reportId: HexKey) = addUnique(RoadReportTag.assemble(reportId, null, null))
|
||||
|
||||
fun TagArrayBuilder<RoadEventConfirmationEvent>.report(reportHint: EventHintBundle<RoadEventReportEvent>) = addUnique(RoadReportTag.assemble(reportHint))
|
||||
|
||||
fun TagArrayBuilder<RoadEventConfirmationEvent>.status(status: RoadEventStatus) = addUnique(RoadEventStatusTag.assemble(status))
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.confirmation
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatusTag
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadReportTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
|
||||
fun TagArray.roadReport() = firstNotNullOfOrNull(RoadReportTag::parse)
|
||||
|
||||
fun TagArray.roadEventStatus() = firstNotNullOfOrNull(RoadEventStatusTag::parse)
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/** Confirmation status carried in the `status` tag of a Roadstr confirmation (kind 1316). */
|
||||
enum class RoadEventStatus(
|
||||
val code: String,
|
||||
) {
|
||||
STILL_THERE("still_there"),
|
||||
NO_LONGER_THERE("no_longer_there"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String?): RoadEventStatus? = entries.firstOrNull { it.code == code }
|
||||
}
|
||||
}
|
||||
|
||||
/** The `status` tag of a Roadstr confirmation (kind 1316): `still_there` or `no_longer_there`. */
|
||||
class RoadEventStatusTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "status"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Array<String>): RoadEventStatus? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
return RoadEventStatus.fromCode(tag[1])
|
||||
}
|
||||
|
||||
fun assemble(status: RoadEventStatus) = arrayOf(TAG_NAME, status.code)
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/** The `e` tag of a Roadstr confirmation (kind 1316): the referenced report event. */
|
||||
@Immutable
|
||||
data class RoadReportTag(
|
||||
override val eventId: HexKey,
|
||||
) : GenericETag {
|
||||
override var relay: NormalizedRelayUrl? = null
|
||||
override var author: HexKey? = null
|
||||
|
||||
constructor(eventId: HexKey, relayHint: NormalizedRelayUrl? = null, authorPubKeyHex: HexKey? = null) : this(eventId) {
|
||||
this.relay = relayHint
|
||||
this.author = authorPubKeyHex
|
||||
}
|
||||
|
||||
fun toNEvent(): String = NEvent.create(eventId, author, RoadEventReportEvent.KIND, relay)
|
||||
|
||||
override fun toTagArray() = assemble(eventId, relay, author)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "e"
|
||||
|
||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
|
||||
|
||||
fun parse(tag: Array<String>): RoadReportTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
|
||||
return RoadReportTag(tag[1], hint, tag.getOrNull(3))
|
||||
}
|
||||
|
||||
fun parseId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun parseAsHint(tag: Array<String>): EventIdHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
|
||||
val hint = RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return EventIdHint(tag[1], hint)
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
eventId: HexKey,
|
||||
relay: NormalizedRelayUrl?,
|
||||
author: HexKey?,
|
||||
) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, author)
|
||||
|
||||
fun assemble(eventHint: EventHintBundle<RoadEventReportEvent>) = assemble(eventHint.event.id, eventHint.relay, eventHint.event.pubKey)
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.report
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventType
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.tags.coordinates
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.tags.latitude
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.tags.longitude
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.tags.roadGeohashes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* Roadstr road event report (kind 1315).
|
||||
*
|
||||
* A driver-published report of a notable road condition (police, accident,
|
||||
* hazard, …) at a specific location. See <https://github.com/jooray/roadstr>.
|
||||
*
|
||||
* Tags (canonical order):
|
||||
* - `t` — the [RoadEventType] code.
|
||||
* - `g` — geohash at precision 4, 5 and 6 (for multi-resolution spatial queries).
|
||||
* - `lat` / `lon` — coordinates as decimal strings with 7 fractional digits.
|
||||
* - `expiration` — NIP-40 relay garbage-collection window, always `created_at + 14 days`.
|
||||
* - `alt` — NIP-31 human-readable fallback.
|
||||
*
|
||||
* The relay-side `expiration` is a fixed 14-day window; clients should instead use
|
||||
* [effectiveExpirationAt] (derived from the event type's TTL) to decide when to
|
||||
* stop displaying a report. The `content` field holds an optional free-text comment.
|
||||
*/
|
||||
@Immutable
|
||||
class RoadEventReportEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
/** The parsed [RoadEventType], or null when the `t` tag is missing/unknown. */
|
||||
fun roadEventType() = tags.roadEventType()
|
||||
|
||||
/** The raw event-type code from the first `t` tag (may be outside [RoadEventType]). */
|
||||
fun roadEventTypeCode() = tags.roadEventTypeCode()
|
||||
|
||||
/** All `g` geohash tags, coarse-to-fine as published. */
|
||||
fun geohashes() = tags.geohashes()
|
||||
|
||||
fun latitude() = tags.latitude()
|
||||
|
||||
fun longitude() = tags.longitude()
|
||||
|
||||
fun alt() = tags.firstNotNullOfOrNull(AltTag::parse)
|
||||
|
||||
fun relayExpiration() = tags.expiration()
|
||||
|
||||
/** Client-side effective expiry (`created_at + ` the type's TTL), or null if the type is unknown. */
|
||||
fun effectiveExpirationAt(): Long? = roadEventType()?.let { createdAt + it.effectiveTtlSeconds }
|
||||
|
||||
/** Whether the report should no longer be displayed at [now], per its client-side TTL. */
|
||||
fun isEffectivelyExpired(now: Long = TimeUtils.now()): Boolean = effectiveExpirationAt()?.let { now > it } ?: false
|
||||
|
||||
companion object {
|
||||
const val KIND = 1315
|
||||
const val ALT_PREFIX = "Roadstr: "
|
||||
|
||||
/** Fixed relay-side NIP-40 window (14 days) for both report and confirmation events. */
|
||||
const val RELAY_TTL_SECONDS = 1_209_600L
|
||||
|
||||
fun altDescription(type: RoadEventType) = "$ALT_PREFIX${type.code} report"
|
||||
|
||||
fun build(
|
||||
type: RoadEventType,
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
comment: String = "",
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<RoadEventReportEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, comment, createdAt) {
|
||||
roadEventType(type)
|
||||
roadGeohashes(latitude, longitude)
|
||||
coordinates(latitude, longitude)
|
||||
expiration(createdAt + RELAY_TTL_SECONDS)
|
||||
alt(altDescription(type))
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.report
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventType
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventTypeTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
|
||||
fun TagArrayBuilder<RoadEventReportEvent>.roadEventType(type: RoadEventType) = add(RoadEventTypeTag.assemble(type))
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.report
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventTypeTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
|
||||
fun TagArray.roadEventType() = firstNotNullOfOrNull(RoadEventTypeTag::parse)
|
||||
|
||||
fun TagArray.roadEventTypeCode() = firstNotNullOfOrNull(RoadEventTypeTag::parseCode)
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.report.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* Road event categories carried in the `t` tag of a Roadstr report (kind 1315).
|
||||
*
|
||||
* Each type also defines a client-side *effective* TTL: the relay-side NIP-40
|
||||
* `expiration` tag is always a fixed 14-day garbage-collection window, but
|
||||
* clients should stop showing a report after [effectiveTtlSeconds] from its
|
||||
* `created_at` (subject to confirmations).
|
||||
*/
|
||||
enum class RoadEventType(
|
||||
val code: String,
|
||||
val effectiveTtlSeconds: Long,
|
||||
) {
|
||||
POLICE("police", 7_200L), // 2 hours
|
||||
SPEED_CAMERA("speed_camera", 2_592_000L), // 30 days
|
||||
TRAFFIC_JAM("traffic_jam", 3_600L), // 1 hour
|
||||
ACCIDENT("accident", 10_800L), // 3 hours
|
||||
ROAD_CLOSURE("road_closure", 604_800L), // 7 days
|
||||
CONSTRUCTION("construction", 604_800L), // 7 days
|
||||
HAZARD("hazard", 14_400L), // 4 hours
|
||||
ROAD_CONDITION("road_condition", 21_600L), // 6 hours
|
||||
POTHOLE("pothole", 604_800L), // 7 days
|
||||
FOG("fog", 10_800L), // 3 hours
|
||||
ICE("ice", 21_600L), // 6 hours
|
||||
ANIMAL("animal", 3_600L), // 1 hour
|
||||
OTHER("other", 7_200L), // 2 hours
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String?): RoadEventType? = entries.firstOrNull { it.code == code }
|
||||
}
|
||||
}
|
||||
|
||||
/** The `t` tag of a Roadstr report (kind 1315): the [RoadEventType] code. */
|
||||
class RoadEventTypeTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "t"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
/** Parses the tag into a known [RoadEventType], or null when missing/unknown. */
|
||||
fun parse(tag: Array<String>): RoadEventType? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
return RoadEventType.fromCode(tag[1])
|
||||
}
|
||||
|
||||
/** Returns the raw `t` value even when it is not a known [RoadEventType]. */
|
||||
fun parseCode(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(type: RoadEventType) = arrayOf(TAG_NAME, type.code)
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/** The `lat` tag of a Roadstr event: latitude as a decimal string with 7 fractional digits. */
|
||||
class LatitudeTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "lat"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Array<String>): Double? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
return tag[1].toDoubleOrNull()
|
||||
}
|
||||
|
||||
fun assemble(latitude: Double) = arrayOf(TAG_NAME, RoadCoordinate.format(latitude))
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/** The `lon` tag of a Roadstr event: longitude as a decimal string with 7 fractional digits. */
|
||||
class LongitudeTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "lon"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Array<String>): Double? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
return tag[1].toDoubleOrNull()
|
||||
}
|
||||
|
||||
fun assemble(longitude: Double) = arrayOf(TAG_NAME, RoadCoordinate.format(longitude))
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.tags
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.round
|
||||
|
||||
/**
|
||||
* Formatting for the `lat`/`lon` tag values of Roadstr events. The spec requires
|
||||
* a decimal string with exactly 7 fractional digits (e.g. `48.8566140`), which is
|
||||
* locale-independent here (unlike `String.format`, which is unavailable in
|
||||
* `commonMain` anyway).
|
||||
*/
|
||||
object RoadCoordinate {
|
||||
private const val SCALE = 10_000_000L // 7 decimal places
|
||||
|
||||
fun format(value: Double): String {
|
||||
val scaled = round(value * SCALE).toLong()
|
||||
val negative = scaled < 0L
|
||||
val absValue = abs(scaled)
|
||||
val intPart = absValue / SCALE
|
||||
val fracPart = (absValue % SCALE).toString().padStart(7, '0')
|
||||
return (if (negative) "-" else "") + intPart + "." + fracPart
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
|
||||
|
||||
/** Adds `lat`/`lon` tags (7-decimal strings) shared by Roadstr reports and confirmations. */
|
||||
fun <T : Event> TagArrayBuilder<T>.coordinates(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
) = apply {
|
||||
add(LatitudeTag.assemble(latitude))
|
||||
add(LongitudeTag.assemble(longitude))
|
||||
}
|
||||
|
||||
/** Adds the three `g` geohash tags (precision 4, 5 and 6) derived from [latitude]/[longitude]. */
|
||||
fun <T : Event> TagArrayBuilder<T>.roadGeohashes(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
) = apply {
|
||||
val geohash6 = GeoHash.encode(latitude, longitude, 6).toString()
|
||||
add(GeoHashTag.assembleSingle(geohash6.substring(0, 4)))
|
||||
add(GeoHashTag.assembleSingle(geohash6.substring(0, 5)))
|
||||
add(GeoHashTag.assembleSingle(geohash6))
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
|
||||
fun TagArray.latitude() = firstNotNullOfOrNull(LatitudeTag::parse)
|
||||
|
||||
fun TagArray.longitude() = firstNotNullOfOrNull(LongitudeTag::parse)
|
||||
+7
-11
@@ -18,13 +18,17 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.location
|
||||
|
||||
import android.location.Location
|
||||
package com.vitorpamplona.quartz.nip01Core.tags.geohash
|
||||
|
||||
/**
|
||||
* GeoHash encoding/decoding based on Gustavo Niemeyer's algorithm (2008).
|
||||
*
|
||||
* Pure-Kotlin and platform-agnostic (lives in `commonMain`), so any Nostr
|
||||
* protocol code can derive `g` tags from coordinates and walk neighboring cells
|
||||
* without depending on a platform location API. Callers that work with a
|
||||
* platform `Location` type add thin conversion helpers in their own module
|
||||
* (e.g. `amethyst`'s `Location.toGeoHash()` / `GeoHash.toLocation()`).
|
||||
*
|
||||
* Bits are interleaved: even-indexed bits (0, 2, 4, ...) = longitude,
|
||||
* odd-indexed bits (1, 3, 5, ...) = latitude.
|
||||
*/
|
||||
@@ -237,12 +241,6 @@ class GeoHash private constructor(
|
||||
val easternNeighbour: GeoHash get() = fromBitArrays(latBits, numLat, mask(lonBits + 1L, numLon), numLon)
|
||||
val westernNeighbour: GeoHash get() = fromBitArrays(latBits, numLat, mask(lonBits - 1L, numLon), numLon)
|
||||
|
||||
fun toLocation(): Location =
|
||||
Location("").also {
|
||||
it.latitude = centerLat
|
||||
it.longitude = centerLon
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val charCount = significantBits / BITS_PER_CHAR
|
||||
val sb = StringBuilder(charCount)
|
||||
@@ -263,6 +261,4 @@ class GeoHash private constructor(
|
||||
override fun hashCode(): Int = 31 * bits.hashCode() + significantBits
|
||||
}
|
||||
|
||||
fun Location.toGeoHash(charsCount: Int = GeoHash.MAX_CHAR_PRECISION): GeoHash = GeoHash.encode(latitude, longitude, charsCount)
|
||||
|
||||
fun String.toGeoHash(): GeoHash? = GeoHash.decode(this)
|
||||
@@ -53,6 +53,8 @@ import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
|
||||
import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
||||
@@ -579,6 +581,8 @@ class EventFactory {
|
||||
ReleaseArtifactSetEvent.KIND -> ReleaseArtifactSetEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelaySetEvent.KIND -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ReportEvent.KIND -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RoadEventConfirmationEvent.KIND -> RoadEventConfirmationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RoadEventReportEvent.KIND -> RoadEventReportEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RootSiteEvent.KIND -> RootSiteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RepostEvent.KIND -> RepostEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RequestToVanishEvent.KIND -> RequestToVanishEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.roadstr
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.tags.RoadEventStatus
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.tags.RoadEventType
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.tags.RoadCoordinate
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
|
||||
import com.vitorpamplona.quartz.utils.EventFactory
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RoadEventTest {
|
||||
private val reportId = "fcbfc866b657a2fe514b579bcf7ff7700a195731eb6f306efb01529a683f86db"
|
||||
|
||||
private fun sampleReport(): Event =
|
||||
EventFactory.create(
|
||||
id = "a099d4db563041bb289d3704f983fc148fc805860303a4f479a8264dc6a2d7cc",
|
||||
pubKey = "932614571afcbad4d17a191ee281e39eebbb41b93fac8fd87829622aeb112f4d",
|
||||
createdAt = 1_700_000_000L,
|
||||
kind = RoadEventReportEvent.KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("t", "police"),
|
||||
arrayOf("g", "u2ed"),
|
||||
arrayOf("g", "u2edc"),
|
||||
arrayOf("g", "u2edcg"),
|
||||
arrayOf("lat", "48.8566140"),
|
||||
arrayOf("lon", "2.3522219"),
|
||||
arrayOf("expiration", "1701209600"),
|
||||
arrayOf("alt", "Roadstr: police report"),
|
||||
),
|
||||
content = "",
|
||||
sig = "00".repeat(64),
|
||||
)
|
||||
|
||||
private fun sampleConfirmation(): Event =
|
||||
EventFactory.create(
|
||||
id = "b199d4db563041bb289d3704f983fc148fc805860303a4f479a8264dc6a2d7cc",
|
||||
pubKey = "932614571afcbad4d17a191ee281e39eebbb41b93fac8fd87829622aeb112f4d",
|
||||
createdAt = 1_700_003_600L,
|
||||
kind = RoadEventConfirmationEvent.KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("e", reportId),
|
||||
arrayOf("status", "no_longer_there"),
|
||||
arrayOf("expiration", "1701213200"),
|
||||
arrayOf("alt", "Roadstr: event confirmation"),
|
||||
),
|
||||
content = "",
|
||||
sig = "00".repeat(64),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun factoryParsesReportAndConfirmation() {
|
||||
assertIs<RoadEventReportEvent>(sampleReport())
|
||||
assertIs<RoadEventConfirmationEvent>(sampleConfirmation())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindsAreKnown() {
|
||||
assertTrue(EventFactory.isKnownKind(RoadEventReportEvent.KIND))
|
||||
assertTrue(EventFactory.isKnownKind(RoadEventConfirmationEvent.KIND))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsesReportFields() {
|
||||
val event = sampleReport()
|
||||
assertIs<RoadEventReportEvent>(event)
|
||||
|
||||
assertEquals(RoadEventType.POLICE, event.roadEventType())
|
||||
assertEquals(listOf("u2ed", "u2edc", "u2edcg"), event.geohashes())
|
||||
assertEquals(48.8566140, event.latitude())
|
||||
assertEquals(2.3522219, event.longitude())
|
||||
assertEquals("Roadstr: police report", event.alt())
|
||||
assertEquals(1701209600L, event.relayExpiration())
|
||||
// police TTL is 2h → created_at + 7200
|
||||
assertEquals(1_700_000_000L + 7_200L, event.effectiveExpirationAt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownTypeYieldsNullType() {
|
||||
val event =
|
||||
EventFactory.create<Event>(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "00".repeat(32),
|
||||
createdAt = 1_700_000_000L,
|
||||
kind = RoadEventReportEvent.KIND,
|
||||
tags = arrayOf(arrayOf("t", "spaceship")),
|
||||
content = "",
|
||||
sig = "00".repeat(64),
|
||||
)
|
||||
assertIs<RoadEventReportEvent>(event)
|
||||
assertEquals("spaceship", event.roadEventTypeCode())
|
||||
assertNull(event.roadEventType())
|
||||
assertNull(event.effectiveExpirationAt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsesConfirmationFields() {
|
||||
val event = sampleConfirmation()
|
||||
assertIs<RoadEventConfirmationEvent>(event)
|
||||
|
||||
assertEquals(reportId, event.reportId())
|
||||
assertEquals(RoadEventStatus.NO_LONGER_THERE, event.status())
|
||||
assertTrue(event.isDenial())
|
||||
assertTrue(!event.isConfirmation())
|
||||
assertEquals("Roadstr: event confirmation", event.alt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildReportProducesCanonicalTags() {
|
||||
val lat = 48.8566140
|
||||
val lon = 2.3522219
|
||||
val createdAt = 1_700_000_000L
|
||||
val template = RoadEventReportEvent.build(RoadEventType.SPEED_CAMERA, lat, lon, createdAt = createdAt)
|
||||
|
||||
assertEquals(RoadEventReportEvent.KIND, template.kind)
|
||||
|
||||
val expectedG6 = GeoHash.encode(lat, lon, 6).toString()
|
||||
val geohashes = template.tags.filter { it[0] == "g" }.map { it[1] }
|
||||
assertEquals(
|
||||
listOf(expectedG6.substring(0, 4), expectedG6.substring(0, 5), expectedG6),
|
||||
geohashes,
|
||||
)
|
||||
|
||||
assertEquals("speed_camera", template.tags.first { it[0] == "t" }[1])
|
||||
assertEquals("48.8566140", template.tags.first { it[0] == "lat" }[1])
|
||||
assertEquals("2.3522219", template.tags.first { it[0] == "lon" }[1])
|
||||
assertEquals((createdAt + RoadEventReportEvent.RELAY_TTL_SECONDS).toString(), template.tags.first { it[0] == "expiration" }[1])
|
||||
assertEquals("Roadstr: speed_camera report", template.tags.first { it[0] == "alt" }[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildConfirmationOmitsLocationWhenAbsent() {
|
||||
val template = RoadEventConfirmationEvent.build(reportId, RoadEventStatus.STILL_THERE, createdAt = 1_700_000_000L)
|
||||
|
||||
assertEquals(reportId, template.tags.first { it[0] == "e" }[1])
|
||||
assertEquals("still_there", template.tags.first { it[0] == "status" }[1])
|
||||
assertEquals("Roadstr: event confirmation", template.tags.first { it[0] == "alt" }[1])
|
||||
assertTrue(template.tags.none { it[0] == "g" || it[0] == "lat" || it[0] == "lon" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildConfirmationIncludesLocationWhenProvided() {
|
||||
val lat = 48.8566140
|
||||
val lon = 2.3522219
|
||||
val template = RoadEventConfirmationEvent.build(reportId, RoadEventStatus.STILL_THERE, lat, lon)
|
||||
|
||||
val expectedG6 = GeoHash.encode(lat, lon, 6).toString()
|
||||
assertEquals(expectedG6, template.tags.filter { it[0] == "g" }.last()[1])
|
||||
assertEquals("48.8566140", template.tags.first { it[0] == "lat" }[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roadEventTypeTtls() {
|
||||
assertEquals(7_200L, RoadEventType.POLICE.effectiveTtlSeconds)
|
||||
assertEquals(2_592_000L, RoadEventType.SPEED_CAMERA.effectiveTtlSeconds)
|
||||
assertEquals(3_600L, RoadEventType.TRAFFIC_JAM.effectiveTtlSeconds)
|
||||
assertEquals(604_800L, RoadEventType.POTHOLE.effectiveTtlSeconds)
|
||||
assertEquals(RoadEventType.TRAFFIC_JAM, RoadEventType.fromCode("traffic_jam"))
|
||||
assertNull(RoadEventType.fromCode("nope"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun coordinateFormattingHasSevenDecimals() {
|
||||
assertEquals("48.8566140", RoadCoordinate.format(48.856614))
|
||||
assertEquals("2.3522219", RoadCoordinate.format(2.3522219))
|
||||
assertEquals("-0.0000010", RoadCoordinate.format(-0.000001))
|
||||
assertEquals("0.0000000", RoadCoordinate.format(0.0))
|
||||
assertEquals("-33.8688000", RoadCoordinate.format(-33.8688))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun geohashEncodeProducesPrefixHierarchy() {
|
||||
val g6 = GeoHash.encode(48.8566140, 2.3522219, 6).toString()
|
||||
val g5 = GeoHash.encode(48.8566140, 2.3522219, 5).toString()
|
||||
assertEquals(6, g6.length)
|
||||
assertEquals(g6.substring(0, 5), g5)
|
||||
// decode round-trips back to a hash with the same prefix
|
||||
assertEquals(g6, GeoHash.decode(g6).toString())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user