Reintroduce circular headwind indicator data field with absolute wind speed (#160)
* fix #158: Add circular absolute wind direction datafield * Move unittest package * Rework circular indicator * Shorten wind direction and speed label * Update README
This commit is contained in:
parent
2378c944e6
commit
be7ca192b2
@ -23,7 +23,7 @@ After installing this app on your Karoo and opening it once from the main menu,
|
|||||||
|
|
||||||
- Headwind (graphical, 1x1 field): Shows the headwind direction and speed as a circle with a triangular direction indicator. The speed is shown at the center in your set unit of measurement (default is kilometers per hour if you have set up metric units in your Karoo, otherwise miles per hour). Both direction and speed are relative to the current riding direction by default, i. e., riding directly into a wind of 20 km/h will show a headwind speed of 20 km/h, while riding in the same direction will show -20 km/h.
|
- Headwind (graphical, 1x1 field): Shows the headwind direction and speed as a circle with a triangular direction indicator. The speed is shown at the center in your set unit of measurement (default is kilometers per hour if you have set up metric units in your Karoo, otherwise miles per hour). Both direction and speed are relative to the current riding direction by default, i. e., riding directly into a wind of 20 km/h will show a headwind speed of 20 km/h, while riding in the same direction will show -20 km/h.
|
||||||
- Tailwind with riding speed (graphical, 1x1 field): Shows an arrow indicating the current headwind direction next to a label reading your current speed and the speed of the tailwind. If you ride against a headwind of 5 mph, it will show "-5". If you ride in the same direction of a 5 mph wind, it will read "+5". Text and arrow are colored based on the tailwind speed, with red indicating a strong headwind and green indicating a strong tailwind.
|
- Tailwind with riding speed (graphical, 1x1 field): Shows an arrow indicating the current headwind direction next to a label reading your current speed and the speed of the tailwind. If you ride against a headwind of 5 mph, it will show "-5". If you ride in the same direction of a 5 mph wind, it will read "+5". Text and arrow are colored based on the tailwind speed, with red indicating a strong headwind and green indicating a strong tailwind.
|
||||||
- Wind direction and speed (graphical, 1x1 field): Similar to the tailwind data field, but shows the absolute wind speed and gust speed instead.
|
- Wind direction and speed (graphical, 1x1 field): Similar to the tailwind data field, but shows the absolute wind speed and gust speed instead. The "circular" variant uses the same circular graphics as the headwind indicator instead.
|
||||||
- Wind forecast / Temperature Forecast / Precipitation forecast (graphical, 2x1 field): Line graphs showing the forecasted wind speeds, temperature or precipitation for the next 12 hours if no route is loaded. If a route is loaded, forecasts along the route will be used instead of the current location.
|
- Wind forecast / Temperature Forecast / Precipitation forecast (graphical, 2x1 field): Line graphs showing the forecasted wind speeds, temperature or precipitation for the next 12 hours if no route is loaded. If a route is loaded, forecasts along the route will be used instead of the current location.
|
||||||
- Weather forecast (graphical, 2x1 field): Shows three columns indicating the current weather conditions (sunny, cloudy, ...), wind direction, precipitation and temperature forecasted for the next three hours. Tap on this widget to cycle through the 12 hour forecast. If you have a route loaded, the forecast widget will show the forecasted weather along points of the route, with an estimated traveled distance per hour of 20 km / 12 miles by default. If placed in a 1x1 datafield, only the current weather conditions are shown.
|
- Weather forecast (graphical, 2x1 field): Shows three columns indicating the current weather conditions (sunny, cloudy, ...), wind direction, precipitation and temperature forecasted for the next three hours. Tap on this widget to cycle through the 12 hour forecast. If you have a route loaded, the forecast widget will show the forecasted weather along points of the route, with an estimated traveled distance per hour of 20 km / 12 miles by default. If placed in a 1x1 datafield, only the current weather conditions are shown.
|
||||||
- Relative grade (numerical): Shows the relative grade. The relative grade is calculated by estimating the force of the headwind, and then calculating the gradient you would need to ride at to experience this resistance if there was no wind. Example: If you are riding on an actual gradient of 2 %, face a headwind of 18 km/h while riding at 29 km/h, the relative grade will be shown as 5.2 % (with 3.2 % added to the actual grade due to the headwind).
|
- Relative grade (numerical): Shows the relative grade. The relative grade is calculated by estimating the force of the headwind, and then calculating the gradient you would need to ride at to experience this resistance if there was no wind. Example: If you are riding on an actual gradient of 2 %, face a headwind of 18 km/h while riding at 29 km/h, the relative grade will be shown as 5.2 % (with 3.2 % added to the actual grade due to the headwind).
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
|||||||
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
|
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType
|
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType
|
||||||
|
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataTypeCircle
|
||||||
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindForecastDataType
|
import de.timklge.karooheadwind.datatypes.WindForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
||||||
@ -43,8 +44,6 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.retry
|
import kotlinx.coroutines.flow.retry
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.time.debounce
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
@ -66,6 +65,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
|||||||
listOf(
|
listOf(
|
||||||
HeadwindDirectionDataType(karooSystem, applicationContext),
|
HeadwindDirectionDataType(karooSystem, applicationContext),
|
||||||
TailwindAndRideSpeedDataType(karooSystem, applicationContext),
|
TailwindAndRideSpeedDataType(karooSystem, applicationContext),
|
||||||
|
WindDirectionAndSpeedDataTypeCircle(karooSystem, applicationContext),
|
||||||
WeatherForecastDataType(karooSystem),
|
WeatherForecastDataType(karooSystem),
|
||||||
HeadwindSpeedDataType(karooSystem, applicationContext),
|
HeadwindSpeedDataType(karooSystem, applicationContext),
|
||||||
RelativeHumidityDataType(karooSystem, applicationContext),
|
RelativeHumidityDataType(karooSystem, applicationContext),
|
||||||
|
|||||||
@ -67,24 +67,7 @@ class HeadwindDirectionDataType(
|
|||||||
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
||||||
|
|
||||||
var returnValue = 0.0
|
var returnValue = 0.0
|
||||||
if (value == null || streamData.absoluteWindDirection == null || streamData.windSpeed == null){
|
if (value != null && streamData.absoluteWindDirection != null && streamData.windSpeed != null) {
|
||||||
var errorCode = 1.0
|
|
||||||
var headingResponse = streamData.headingResponse
|
|
||||||
|
|
||||||
if (headingResponse is HeadingResponse.Value && (streamData.absoluteWindDirection == null || streamData.windSpeed == null)){
|
|
||||||
headingResponse = HeadingResponse.NoWeatherData
|
|
||||||
}
|
|
||||||
|
|
||||||
if (streamData.settings.welcomeDialogAccepted == false){
|
|
||||||
errorCode = ERROR_APP_NOT_SET_UP.toDouble()
|
|
||||||
} else if (headingResponse is HeadingResponse.NoGps){
|
|
||||||
errorCode = ERROR_NO_GPS.toDouble()
|
|
||||||
} else {
|
|
||||||
errorCode = ERROR_NO_WEATHER_DATA.toDouble()
|
|
||||||
}
|
|
||||||
|
|
||||||
returnValue = errorCode
|
|
||||||
} else {
|
|
||||||
var windDirection = value
|
var windDirection = value
|
||||||
|
|
||||||
if (windDirection < 0) windDirection += 360
|
if (windDirection < 0) windDirection += 360
|
||||||
@ -222,15 +205,4 @@ class HeadwindDirectionDataType(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun KarooSystemService.getRefreshRateInMilliseconds(context: Context): Long {
|
|
||||||
val refreshRate = context.streamSettings(this).first().refreshRate
|
|
||||||
val isK2 = hardwareType == HardwareType.K2
|
|
||||||
|
|
||||||
return if (isK2){
|
|
||||||
refreshRate.k2Ms
|
|
||||||
} else {
|
|
||||||
refreshRate.k3Ms
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -22,6 +22,10 @@ import androidx.glance.text.TextStyle
|
|||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
import de.timklge.karooheadwind.HeadwindSettings
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.HardwareType
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult {
|
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult {
|
||||||
@ -75,4 +79,15 @@ suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, errorCod
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun KarooSystemService.getRefreshRateInMilliseconds(context: Context): Long {
|
||||||
|
val refreshRate = context.streamSettings(this).first().refreshRate
|
||||||
|
val isK2 = hardwareType == HardwareType.K2
|
||||||
|
|
||||||
|
return if (isK2){
|
||||||
|
refreshRate.k2Ms
|
||||||
|
} else {
|
||||||
|
refreshRate.k3Ms
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,145 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||||
|
import androidx.glance.appwidget.GlanceRemoteViews
|
||||||
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
|
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType.StreamData
|
||||||
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
|
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
||||||
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
|
import de.timklge.karooheadwind.throttle
|
||||||
|
import de.timklge.karooheadwind.util.msInUserUnit
|
||||||
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
|
import io.hammerhead.karooext.internal.Emitter
|
||||||
|
import io.hammerhead.karooext.internal.ViewEmitter
|
||||||
|
import io.hammerhead.karooext.models.DataPoint
|
||||||
|
import io.hammerhead.karooext.models.DataType
|
||||||
|
import io.hammerhead.karooext.models.HardwareType
|
||||||
|
import io.hammerhead.karooext.models.StreamState
|
||||||
|
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
import io.hammerhead.karooext.models.ViewConfig
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
class WindDirectionAndSpeedDataTypeCircle(
|
||||||
|
private val karooSystem: KarooSystemService,
|
||||||
|
private val applicationContext: Context
|
||||||
|
) : DataTypeImpl("karoo-headwind", "windDirectionAndSpeedCircle") {
|
||||||
|
private val glance = GlanceRemoteViews()
|
||||||
|
|
||||||
|
data class StreamData(val headingResponse: HeadingResponse, val absoluteWindDirection: Double?, val windSpeed: Double?, val settings: HeadwindSettings)
|
||||||
|
|
||||||
|
private fun previewFlow(profileFlow: Flow<UserProfile>): Flow<de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType.StreamData> {
|
||||||
|
return flow {
|
||||||
|
val profile = profileFlow.first()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val bearing = (0..360).random().toDouble()
|
||||||
|
val windSpeed = (0..10).random()
|
||||||
|
val gustSpeed = windSpeed * ((10..20).random().toDouble() / 10)
|
||||||
|
val isImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
|
|
||||||
|
emit(StreamData(HeadingResponse.Value(bearing), bearing, windSpeed.toDouble(), HeadwindSettings(), gustSpeed = gustSpeed, isImperial = isImperial, isVisible = true))
|
||||||
|
|
||||||
|
delay(2_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Starting headwind direction view with $emitter")
|
||||||
|
|
||||||
|
val baseBitmap = BitmapFactory.decodeResource(
|
||||||
|
context.resources,
|
||||||
|
de.timklge.karooheadwind.R.drawable.circle
|
||||||
|
)
|
||||||
|
|
||||||
|
val configJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
emitter.onNext(UpdateGraphicConfig(showHeader = false))
|
||||||
|
awaitCancellation()
|
||||||
|
}
|
||||||
|
|
||||||
|
val flow = if (config.preview) {
|
||||||
|
previewFlow(karooSystem.streamUserProfile())
|
||||||
|
} else {
|
||||||
|
combine(karooSystem.getRelativeHeadingFlow(context),
|
||||||
|
context.streamCurrentWeatherData(karooSystem),
|
||||||
|
context.streamSettings(karooSystem),
|
||||||
|
karooSystem.streamUserProfile(),
|
||||||
|
karooSystem.streamDatatypeIsVisible(dataTypeId)
|
||||||
|
) { headingResponse, weatherData, settings, userProfile, isVisible ->
|
||||||
|
val isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
|
val absoluteWindDirection = weatherData?.windDirection
|
||||||
|
val windSpeed = weatherData?.windSpeed
|
||||||
|
val gustSpeed = weatherData?.windGusts
|
||||||
|
|
||||||
|
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val refreshRate = karooSystem.getRefreshRateInMilliseconds(context)
|
||||||
|
|
||||||
|
flow.filter { it.isVisible }.throttle(refreshRate).collect { streamData ->
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
|
||||||
|
|
||||||
|
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
||||||
|
if (value == null || streamData.absoluteWindDirection == null || streamData.windSpeed == null){
|
||||||
|
var headingResponse = streamData.headingResponse
|
||||||
|
|
||||||
|
if (headingResponse is HeadingResponse.Value && (streamData.absoluteWindDirection == null || streamData.windSpeed == null)){
|
||||||
|
headingResponse = HeadingResponse.NoWeatherData
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.updateView(getErrorWidget(glance, context, streamData.settings, headingResponse).remoteViews)
|
||||||
|
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
val windSpeed = streamData.windSpeed
|
||||||
|
val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
|
||||||
|
|
||||||
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
|
HeadwindDirection(
|
||||||
|
baseBitmap,
|
||||||
|
value.roundToInt(),
|
||||||
|
config.textSize,
|
||||||
|
windSpeedUserUnit.roundToInt().toString(),
|
||||||
|
preview = config.preview,
|
||||||
|
wideMode = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.updateView(result.remoteViews)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitter.setCancellable {
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Stopping headwind view with $emitter")
|
||||||
|
configJob.cancel()
|
||||||
|
viewJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,11 +10,11 @@
|
|||||||
<string name="cloudCover">Cloud cover</string>
|
<string name="cloudCover">Cloud cover</string>
|
||||||
<string name="cloudCover_description">Current cloud cover in percent</string>
|
<string name="cloudCover_description">Current cloud cover in percent</string>
|
||||||
<string name="windSpeed">Wind speed</string>
|
<string name="windSpeed">Wind speed</string>
|
||||||
<string name="windSpeed_description">Current wind speed in configured unit</string>
|
<string name="windSpeed_description">Current absolute wind speed</string>
|
||||||
<string name="windGusts">Wind gusts</string>
|
<string name="windGusts">Wind gusts</string>
|
||||||
<string name="windGusts_description">Current wind gust speed in configured unit</string>
|
<string name="windGusts_description">Current wind gust speed in configured unit</string>
|
||||||
<string name="windDirection">Absolute wind direction</string>
|
<string name="windDirection">Absolute wind direction</string>
|
||||||
<string name="windDirection_description">Current wind direction</string>
|
<string name="windDirection_description">Current absolute wind direction</string>
|
||||||
<string name="precipitation">Rainfall</string>
|
<string name="precipitation">Rainfall</string>
|
||||||
<string name="precipitation_description">Current precipitation (rainfall / snowfall)</string>
|
<string name="precipitation_description">Current precipitation (rainfall / snowfall)</string>
|
||||||
<string name="surfacePressure">Surface pressure</string>
|
<string name="surfacePressure">Surface pressure</string>
|
||||||
@ -39,6 +39,8 @@
|
|||||||
<string name="relativeGrade_description">Perceived grade in percent</string>
|
<string name="relativeGrade_description">Perceived grade in percent</string>
|
||||||
<string name="relativeElevationGain">Relative Elevation Gain</string>
|
<string name="relativeElevationGain">Relative Elevation Gain</string>
|
||||||
<string name="relativeElevationGain_description">Perceived elevation gain in meters</string>
|
<string name="relativeElevationGain_description">Perceived elevation gain in meters</string>
|
||||||
<string name="windDirectionAndSpeed">Wind direction and speed</string>
|
<string name="windDirectionAndSpeed">Wind direction, speed</string>
|
||||||
<string name="windDirectionAndSpeed_description">Current wind direction and wind speed</string>
|
<string name="windDirectionAndSpeed_description">Current wind direction and wind speed</string>
|
||||||
|
<string name="windDirectionAndSpeedCircle">Wind direction, speed (Circle)</string>
|
||||||
|
<string name="windDirectionAndSpeedCircle_description">Current wind direction and wind speed (Circle graphics)</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -22,10 +22,17 @@
|
|||||||
<DataType
|
<DataType
|
||||||
description="@string/windDirectionAndSpeed_description"
|
description="@string/windDirectionAndSpeed_description"
|
||||||
displayName="@string/windDirectionAndSpeed"
|
displayName="@string/windDirectionAndSpeed"
|
||||||
graphical="false"
|
graphical="true"
|
||||||
icon="@drawable/wind"
|
icon="@drawable/wind"
|
||||||
typeId="windDirectionAndSpeed" />
|
typeId="windDirectionAndSpeed" />
|
||||||
|
|
||||||
|
<DataType
|
||||||
|
description="@string/windDirectionAndSpeedCircle_description"
|
||||||
|
displayName="@string/windDirectionAndSpeedCircle"
|
||||||
|
graphical="true"
|
||||||
|
icon="@drawable/wind"
|
||||||
|
typeId="windDirectionAndSpeedCircle" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/weather_forecast_description"
|
description="@string/weather_forecast_description"
|
||||||
displayName="@string/weather_forecast"
|
displayName="@string/weather_forecast"
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import de.timklge.karooheadwind.datatypes.RelativeGradeDataType
|
import de.timklge.karooheadwind.datatypes.RelativeGradeDataType
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
Loading…
x
Reference in New Issue
Block a user