Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package org.maplibre.navigation.core.models

import kotlinx.serialization.SerializationException
import org.maplibre.navigation.core.json
import org.maplibre.navigation.core.models.notification.Notification
import org.maplibre.navigation.core.models.notification.NotificationDetails
import org.maplibre.navigation.core.models.notification.NotificationRefreshType
import org.maplibre.navigation.core.models.notification.NotificationType
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull

class NotificationTest {

@Test
fun routeLeg_fromJson_deserializesNotifications() {
val routeLeg = json.decodeFromString<RouteLeg>(
"""
{
"distance": 100.0,
"duration": 10.0,
"steps": [],
"notifications": [
{
"type": "violation",
"subtype": "maxWidth",
"refresh_type": "static",
"geometry_index": 5,
"geometry_index_start": 4,
"geometry_index_end": 6,
"details": {
"requested_value": "3",
"actual_value": "2.5",
"unit": "m",
"message": "Width restriction"
}
}
]
}
""".trimIndent()
)

val notification = routeLeg.notifications.single()
assertEquals(NotificationType.Violation, notification.type)
assertEquals(NotificationType.Subtype.MaxWidth, notification.subtype)
assertEquals(NotificationRefreshType.Static, notification.refreshType)
assertEquals(5, notification.geometryIndex)
assertEquals(4, notification.startIndex)
assertEquals(6, notification.endIndex)
assertEquals("3", notification.details?.requestedValue)
assertEquals("2.5", notification.details?.actualValue)
assertEquals("m", notification.details?.unit)
assertEquals("Width restriction", notification.details?.message)
}

@Test
fun routeLeg_fromJson_deserializesPointAndRangeNotifications() {
val routeLeg = json.decodeFromString<RouteLeg>(
"""
{
"distance": 100.0,
"duration": 10.0,
"steps": [],
"notifications": [
{
"type": "alert",
"subtype": "countryBorderCrossing",
"refresh_type": "dynamic",
"geometry_index": 7
},
{
"type": "violation",
"subtype": "ferry",
"refresh_type": "static",
"geometry_index_start": 10,
"geometry_index_end": 15
}
]
}
""".trimIndent()
)

val pointNotification = routeLeg.notifications[0]
assertEquals(NotificationType.Alert, pointNotification.type)
assertEquals(NotificationType.Subtype.CountryBorderCrossing, pointNotification.subtype)
assertEquals(NotificationRefreshType.Dynamic, pointNotification.refreshType)
assertEquals(7, pointNotification.geometryIndex)
assertNull(pointNotification.startIndex)
assertNull(pointNotification.endIndex)
assertNull(pointNotification.details)

val rangeNotification = routeLeg.notifications[1]
assertEquals(NotificationType.Violation, rangeNotification.type)
assertEquals(NotificationType.Subtype.Ferry, rangeNotification.subtype)
assertEquals(NotificationRefreshType.Static, rangeNotification.refreshType)
assertNull(rangeNotification.geometryIndex)
assertEquals(10, rangeNotification.startIndex)
assertEquals(15, rangeNotification.endIndex)
}

@Test
fun notification_fromJson_preservesUnknownValuesForForwardCompatibility() {
val notification = json.decodeFromString<Notification>(
"""{"type":"newType","subtype":"newSubtype","refresh_type":"newRefreshType"}"""
)

assertEquals(NotificationType("newType"), notification.type)
assertEquals(NotificationType.Subtype("newSubtype"), notification.subtype)
assertEquals(NotificationRefreshType("newRefreshType"), notification.refreshType)
}

@Test
fun notification_fromJson_requiresTypeAndRefreshType() {
assertFailsWith<SerializationException> {
json.decodeFromString<Notification>("""{"refresh_type":"static"}""")
}
assertFailsWith<SerializationException> {
json.decodeFromString<Notification>("""{"type":"alert"}""")
}
}

@Test
fun notification_fromJson_allowsMessageOnlyDetailsAndNoSubtype() {
val notification = json.decodeFromString<Notification>(
"""{"type":"alert","refresh_type":"dynamic","details":{"message":"Ferry service unavailable"}}"""
)

assertNull(notification.subtype)
assertNull(notification.geometryIndex)
assertNull(notification.startIndex)
assertNull(notification.endIndex)
assertNull(notification.details?.requestedValue)
assertNull(notification.details?.actualValue)
assertNull(notification.details?.unit)
assertEquals("Ferry service unavailable", notification.details?.message)
}

@Test
fun routeLeg_fromJson_withoutNotifications_returnsEmptyList() {
val routeLeg = json.decodeFromString<RouteLeg>(
"""{"distance":100.0,"duration":10.0,"steps":[]}"""
)

assertEquals(emptyList(), routeLeg.notifications)
}

@Test
fun routeLeg_builderAndToBuilder_preserveNotifications() {
val notification = Notification(
type = NotificationType.Alert,
subtype = NotificationType.Subtype.Ferry,
refreshType = NotificationRefreshType.Static,
)
val routeLeg = RouteLeg.Builder(
distance = 100.0,
duration = 10.0,
steps = emptyList()
).withNotifications(listOf(notification)).build()

assertEquals(listOf(notification), routeLeg.notifications)
assertEquals(routeLeg, routeLeg.toBuilder().build())
}

@Test
fun notification_toJson_usesDirectionsWireNames() {
val notification = Notification(
type = NotificationType.Violation,
refreshType = NotificationRefreshType.Dynamic,
geometryIndex = 5,
startIndex = 4,
endIndex = 6,
details = NotificationDetails(
requestedValue = "3",
actualValue = "2.5",
)
)

val serialized = json.encodeToString(notification)

assertEquals(
"""{"type":"violation","refresh_type":"dynamic","geometry_index":5,"geometry_index_start":4,"geometry_index_end":6,"details":{"requested_value":"3","actual_value":"2.5"}}""",
serialized
)
assertNull(notification.subtype)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.maplibre.navigation.core.models

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.maplibre.navigation.core.models.notification.Notification

/**
* A route between only two [DirectionsWaypoint].
Expand Down Expand Up @@ -85,6 +86,11 @@ data class RouteLeg(
* @return a list of [Incident]
*/
val closures: List<Closure>? = null,

/**
* A list of notifications that are relevant to this leg
* */
val notifications: List<Notification> = emptyList(),
) {

/**
Expand All @@ -102,6 +108,7 @@ data class RouteLeg(
withIncidents(incidents)
withAnnotation(annotation)
withClosures(closures)
withNotifications(notifications)
}
}

Expand All @@ -122,14 +129,16 @@ data class RouteLeg(
private var incidents: List<Incident>? = null
private var annotation: LegAnnotation? = null
private var closures: List<Closure>? = null
private var notifications: List<Notification> = emptyList()

/**
* Sets the typical duration.
*
* @param durationTypical The typical duration.
* @return The builder instance.
*/
fun withDurationTypical(durationTypical: Double?) = apply { this.durationTypical = durationTypical }
fun withDurationTypical(durationTypical: Double?) =
apply { this.durationTypical = durationTypical }

/**
* Sets the summary.
Expand Down Expand Up @@ -171,6 +180,15 @@ data class RouteLeg(
*/
fun withClosures(closures: List<Closure>?) = apply { this.closures = closures }

/**
* Sets the notifications.
*
* @param notifications The notifications.
* @return The builder instance.
*/
fun withNotifications(notifications: List<Notification>) =
apply { this.notifications = notifications }

/**
* Builds a `RouteLeg` instance with the current builder values.
*
Expand All @@ -186,7 +204,8 @@ data class RouteLeg(
admins = admins,
incidents = incidents,
annotation = annotation,
closures = closures
closures = closures,
notifications = notifications
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.maplibre.navigation.core.models.notification

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* A notification that is relevant to a route leg.
* According to Mapbox Navigation Notifications API spec [Mapbox Docs](https://docs.mapbox.com/api/navigation/directions/#notification-object)
* */
@Serializable
data class Notification(
/**
* The type of notifications. Notification types supported are violation and alert.
* */
val type: NotificationType,

/**
* The optional subtype of notification.
* */
val subtype: NotificationType.Subtype? = null,

/**
* The refresh type of notification to distinguish between static and dynamic notifications.
* */
@SerialName("refresh_type")
val refreshType: NotificationRefreshType,

/**
* The optional position in the coordinate list where the notification occurred, relative to the start of the leg it's on.
* */
@SerialName("geometry_index")
val geometryIndex: Int? = null,

/**
* The optional position in the coordinate list where the notification began, relative to the start of the leg it's on.
* */
@SerialName("geometry_index_start")
val startIndex: Int? = null,

/**
* The optional position in the coordinate list where the notification ended, relative to the start of the leg it's on.
* */
@SerialName("geometry_index_end")
val endIndex: Int? = null,

/**
* The optional details specific to the notification type and subtype.
* */
val details: NotificationDetails? = null
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.maplibre.navigation.core.models.notification

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class NotificationDetails(
/**
* The optional requested value in the request. For example, it is 3 (meters) if we specify `max_width=3` in the request.
* */
@SerialName("requested_value")
val requestedValue: String? = null,
/**
* The optional actual value associated with the property of the road. For example, it is 2.5 (meters) if maximum road width is 2.5 meters.
* */
@SerialName("actual_value")
val actualValue: String? = null,
/**
* The optional unit of measure associated with `details.actual_value` and `details.requested_value`.
* */
val unit: String? = null,
/**
* The optional message associated with the notification.
* */
val message: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.maplibre.navigation.core.models.notification

import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline

/***
* The reason why notification was issued.
* */
@Serializable
@JvmInline
value class NotificationReason(val value: String) {
companion object {
val OutOfOrder = NotificationReason("outOfOrder")
val Occupied = NotificationReason("occupied")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.maplibre.navigation.core.models.notification

import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline

/**
* The refresh type of notification to distinguish between static and dynamic notifications.
* */
@Serializable
@JvmInline
value class NotificationRefreshType(val value: String) {
companion object {
val Static = NotificationRefreshType("static")
val Dynamic = NotificationRefreshType("dynamic")
}
}
Loading