Compare commits
No commits in common. "1.6.3-beta-2" and "master" have entirely different histories.
1.6.3-beta
...
master
54
.github/workflows/release_comment.yml
vendored
Normal file
54
.github/workflows/release_comment.yml
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
name: Comment on Fixed Issues/PRs on Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to run the workflow for'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
comment-on-fixed:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for all tags and branches
|
||||
- name: Find closed issues/PRs and comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Use the input tag if provided, otherwise use the tag from the push event
|
||||
if [ -n "${{ github.event.inputs.tag }}" ]; then
|
||||
RELEASE_TAG="${{ github.event.inputs.tag }}"
|
||||
else
|
||||
RELEASE_TAG="${{ github.ref }}"
|
||||
# Remove the 'refs/tags/' part to get the tag name
|
||||
RELEASE_TAG="${RELEASE_TAG#refs/tags/}"
|
||||
fi
|
||||
|
||||
# Get the previous tag. If there is no previous tag, this will be empty.
|
||||
PREVIOUS_TAG=$(git tag --sort=-v:refname | grep -v "$RELEASE_TAG" | head -n 1)
|
||||
|
||||
# Get the commit range
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
# If there is no previous tag, get all commits up to the current tag
|
||||
COMMIT_RANGE="$RELEASE_TAG"
|
||||
else
|
||||
COMMIT_RANGE="$PREVIOUS_TAG..$RELEASE_TAG"
|
||||
fi
|
||||
|
||||
# Find the commits in this release
|
||||
COMMITS=$(git log "$COMMIT_RANGE" --pretty=format:"%B")
|
||||
|
||||
# Extract issues/PRs closed (simple regex, can be improved)
|
||||
echo "$COMMITS" | grep -oE "#[0-9]+" | sort -u | while read ISSUE; do
|
||||
ISSUE_NUMBER="${ISSUE//#/}"
|
||||
COMMENT="This issue/pr has been fixed in release ${RELEASE_TAG} :tada:"
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||
done
|
||||
shell: bash
|
||||
@ -30,13 +30,12 @@ After installing this app on your Karoo and opening it once from the main menu,
|
||||
- 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 elevation gain (numerical): Shows the relative elegation gain. The relative elevation gain is calculated using the relative grade and is an estimation of how much climbing would have been equivalent to the headwind you faced during the ride.
|
||||
- Resistance forces (graphical, 2x1 field): Shows a graphical representation of the different forces you have to overcome while riding, including gravity (actual gradient), rolling resistance (based on speed and weight), aerodynamic drag (based on speed) and wind resistance (based on headwind speed). The app reads your weight from your karoo user profile and uses rough estimates for CdA and Crr.
|
||||
- Compass (graphical, 1x1 field): Shows a compass needle that uses the Karoo's magnetometer (red points north).
|
||||
- Additionally, data fields that only show the current data value for headwind speed, humidity, cloud cover, absolute wind speed, absolute wind gust speed, absolute wind direction, rainfall and surface pressure can be added if desired.
|
||||
|
||||
The app can use OpenMeteo or OpenWeatherMap as providers for live weather data.
|
||||
|
||||
- OpenMeteo is the default provider and does not require any configuration. Wind speed will be reported in km/h if your karoo is set to metric units or mph if set to imperial.
|
||||
- OpenWeatherMap can provide more accurate data for some locations. OpenWeatherMap is free for personal use, but you need to register at https://openweathermap.org/home/sign_in and obtain a one call API key (e. g. by subscribing to "One Call API 3.0" from the [pricing page](https://openweathermap.org/price)). You can enter your API key in the app settings. Please note that it can take a few hours before OpenWeatherMap enables the key. You can check if your key is enabled by entering it in the app settings and pressing "Test API Key". Wind speed will be reported in km/h if your Karoo is set to metric units and miles per hour if set to imperial.
|
||||
- OpenWeatherMap can provide more accurate data for some locations. Forecasts along the loaded route are not available using OpenWeatherMap. OpenWeatherMap is free for personal use, but you need to register at https://openweathermap.org/home/sign_in and obtain a one call API key (e. g. by subscribing to "One Call API 3.0" from the [pricing page](https://openweathermap.org/price)). You can enter your API key in the app settings. Please note that it can take a few hours before OpenWeatherMap enables the key. You can check if your key is enabled by entering it in the app settings and pressing "Test API Key". Wind speed will be reported in km/h if your Karoo is set to metric units and miles per hour if set to imperial.
|
||||
|
||||
The app will automatically attempt to download weather data from the selected data provider once your device has acquired a GPS fix. Your location is rounded to approximately three kilometers to maintain privacy.
|
||||
New weather data is downloaded when you ride more than three kilometers from the location where the weather data was downloaded for or after one hour at the latest.
|
||||
|
||||
@ -72,7 +72,7 @@ tasks.register("generateManifest") {
|
||||
"latestVersionCode" to android.defaultConfig.versionCode,
|
||||
"developer" to "github.com/timklge",
|
||||
"description" to "Open-source extension that provides headwind direction, wind speed, forecast and other weather data fields.",
|
||||
"releaseNotes" to "* Add compass data field\n* Use magnetometer to determine heading",
|
||||
"releaseNotes" to "* Open main extension menu when clicking on graphical headwind / tailwind datafield\n* Only update position if the estimated accuracy is within 500 meters\n* Add force distribution datafield\n* Only increase relative elevation gain when relative grade is positive",
|
||||
"screenshotUrls" to listOf(
|
||||
"$baseUrl/preview1.png",
|
||||
"$baseUrl/preview3.png",
|
||||
|
||||
@ -1,20 +1,14 @@
|
||||
package de.timklge.karooheadwind
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.util.Log
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.util.signedAngleDifference
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.models.DataType
|
||||
import io.hammerhead.karooext.models.StreamState
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
@ -23,7 +17,6 @@ import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import java.time.Instant
|
||||
|
||||
sealed class HeadingResponse {
|
||||
data object NoGps: HeadingResponse()
|
||||
@ -54,10 +47,12 @@ fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingRes
|
||||
}
|
||||
|
||||
fun KarooSystemService.getHeadingFlow(context: Context): Flow<HeadingResponse> {
|
||||
// Use magnetometer heading instead of GPS bearing
|
||||
return getMagnetometerHeadingFlow(context)
|
||||
.map { heading ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Updated magnetometer heading: $heading")
|
||||
// return flowOf(HeadingResponse.Value(20.0))
|
||||
|
||||
return getGpsCoordinateFlow(context)
|
||||
.map { coords ->
|
||||
val heading = coords?.bearing
|
||||
Log.d(KarooHeadwindExtension.TAG, "Updated gps bearing: $heading")
|
||||
val headingValue = heading?.let { HeadingResponse.Value(it) }
|
||||
|
||||
headingValue ?: HeadingResponse.NoGps
|
||||
@ -161,85 +156,3 @@ fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinat
|
||||
}
|
||||
.dropNullsIfNullEncountered()
|
||||
}
|
||||
|
||||
fun KarooSystemService.getMagnetometerHeadingFlow(context: Context): Flow<Double?> = callbackFlow {
|
||||
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
val rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
|
||||
|
||||
Log.d(KarooHeadwindExtension.TAG, "Using magnetometer for heading")
|
||||
|
||||
if (rotationVectorSensor == null) {
|
||||
Log.w(KarooHeadwindExtension.TAG, "Rotation vector sensor not available, falling back to GPS bearing")
|
||||
|
||||
// Fall back to GPS bearing
|
||||
getGpsCoordinateFlow(context)
|
||||
.collect { coords ->
|
||||
val bearing = coords?.bearing
|
||||
if (bearing != null) {
|
||||
trySend(bearing)
|
||||
} else {
|
||||
trySend(null)
|
||||
}
|
||||
}
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
var lastEventReceived: Instant? = null
|
||||
|
||||
val listener = object : SensorEventListener {
|
||||
override fun onSensorChanged(event: SensorEvent?) {
|
||||
if (event == null){
|
||||
Log.w(KarooHeadwindExtension.TAG, "Received null sensor event")
|
||||
return
|
||||
}
|
||||
|
||||
if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
|
||||
if (lastEventReceived != null){
|
||||
val now = Instant.now()
|
||||
val duration = java.time.Duration.between(lastEventReceived, now).toMillis()
|
||||
if (duration < 500){
|
||||
// Throttle to max 2 updates per second
|
||||
return
|
||||
}
|
||||
lastEventReceived = now
|
||||
} else {
|
||||
lastEventReceived = Instant.now()
|
||||
}
|
||||
|
||||
val rotationMatrix = FloatArray(9)
|
||||
val orientationAngles = FloatArray(3)
|
||||
|
||||
// Convert rotation vector to rotation matrix
|
||||
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
|
||||
|
||||
// Get orientation angles from rotation matrix
|
||||
SensorManager.getOrientation(rotationMatrix, orientationAngles)
|
||||
|
||||
// Azimuth in radians, convert to degrees
|
||||
val azimuthRad = orientationAngles[0]
|
||||
var azimuthDeg = Math.toDegrees(azimuthRad.toDouble())
|
||||
|
||||
// Normalize to 0-360 range
|
||||
if (azimuthDeg < 0) {
|
||||
azimuthDeg += 360.0
|
||||
}
|
||||
|
||||
// Log.d(KarooHeadwindExtension.TAG, "Rotation vector heading: $azimuthDeg degrees")
|
||||
|
||||
trySend(azimuthDeg)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Sensor accuracy changed: ${sensor?.name}, accuracy: $accuracy")
|
||||
}
|
||||
}
|
||||
|
||||
// Register listener for rotation vector sensor
|
||||
sensorManager.registerListener(listener, rotationVectorSensor, SensorManager.SENSOR_DELAY_UI)
|
||||
|
||||
awaitClose {
|
||||
sensorManager.unregisterListener(listener)
|
||||
Log.d(KarooHeadwindExtension.TAG, "Rotation vector listener unregistered")
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import com.mapbox.geojson.LineString
|
||||
import com.mapbox.turf.TurfConstants
|
||||
import com.mapbox.turf.TurfMeasurement
|
||||
import de.timklge.karooheadwind.datatypes.CloudCoverDataType
|
||||
import de.timklge.karooheadwind.datatypes.CompassDataType
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
|
||||
import de.timklge.karooheadwind.datatypes.HeadwindForecastDataType
|
||||
@ -90,8 +89,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
||||
RelativeElevationGainDataType(karooSystem, applicationContext),
|
||||
TemperatureDataType(karooSystem, applicationContext),
|
||||
UviDataType(karooSystem, applicationContext),
|
||||
ResistanceForcesDataType(karooSystem, applicationContext),
|
||||
CompassDataType(karooSystem, applicationContext)
|
||||
ResistanceForcesDataType(karooSystem, applicationContext)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,183 +0,0 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.action.actionStartActivity
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||
import androidx.glance.appwidget.GlanceRemoteViews
|
||||
import androidx.glance.appwidget.background
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.ContentScale
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.padding
|
||||
import de.timklge.karooheadwind.HeadingResponse
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.MainActivity
|
||||
import de.timklge.karooheadwind.getHeadingFlow
|
||||
import de.timklge.karooheadwind.screens.isNightMode
|
||||
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
||||
import de.timklge.karooheadwind.throttle
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||
import io.hammerhead.karooext.internal.ViewEmitter
|
||||
import io.hammerhead.karooext.models.ShowCustomStreamState
|
||||
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
||||
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.filter
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CompassDataType(
|
||||
private val karooSystem: KarooSystemService,
|
||||
private val applicationContext: Context
|
||||
) : DataTypeImpl("karoo-headwind", "compass") {
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
private val glance = GlanceRemoteViews()
|
||||
|
||||
data class StreamData(val bearing: Float, val isVisible: Boolean)
|
||||
|
||||
private fun drawCompassNeedle(bitmap: Bitmap, baseColor: Int) {
|
||||
val canvas = Canvas(bitmap)
|
||||
val centerX = bitmap.width / 2f
|
||||
val centerY = bitmap.height / 2f
|
||||
|
||||
// Draw black needle pointing south (downward)
|
||||
val blackPaint = Paint().apply {
|
||||
color = baseColor
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
val southNeedle = Path().apply {
|
||||
moveTo(centerX, centerY * 1.9f) // Bottom point
|
||||
lineTo(centerX - 12f, centerY) // Left base
|
||||
lineTo(centerX + 12f, centerY) // Right base
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(southNeedle, blackPaint)
|
||||
|
||||
// Draw red needle pointing north (upward) SECOND so it's on top
|
||||
val redPaint = Paint().apply {
|
||||
color = android.graphics.Color.RED
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
val northNeedle = Path().apply {
|
||||
moveTo(centerX, centerY * 0.1f) // Top point
|
||||
lineTo(centerX - 12f, centerY) // Left base
|
||||
lineTo(centerX + 12f, centerY) // Right base
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(northNeedle, redPaint)
|
||||
|
||||
// Draw text labels
|
||||
val textPaint = Paint().apply {
|
||||
color = baseColor
|
||||
textSize = 24f
|
||||
textAlign = Paint.Align.CENTER
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = true
|
||||
}
|
||||
|
||||
// Draw "N" at the top
|
||||
// canvas.drawText("N", centerX, centerY * 0.3f, textPaint)
|
||||
|
||||
// Draw "S" at the bottom
|
||||
// canvas.drawText("S", centerX, centerY * 1.7f, textPaint)
|
||||
|
||||
// Draw center circle
|
||||
val centerPaint = Paint().apply {
|
||||
color = android.graphics.Color.DKGRAY
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
canvas.drawCircle(centerX, centerY, 8f, centerPaint)
|
||||
}
|
||||
|
||||
private fun previewFlow(): Flow<StreamData> {
|
||||
return flow {
|
||||
while (true) {
|
||||
emit(StreamData(isVisible = true, bearing = 360f * Math.random().toFloat()))
|
||||
delay(5_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Starting compass view with $emitter")
|
||||
|
||||
val baseModifier = GlanceModifier.fillMaxSize().padding(2.dp).background(Color.White, Color.Black).cornerRadius(10.dp)
|
||||
|
||||
val configJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
emitter.onNext(UpdateGraphicConfig(showHeader = false))
|
||||
awaitCancellation()
|
||||
}
|
||||
|
||||
val isNight = isNightMode(context)
|
||||
|
||||
val baseBitmap = createBitmap(128, 128).also {
|
||||
drawCompassNeedle(it, if (isNight) android.graphics.Color.WHITE else android.graphics.Color.BLACK)
|
||||
}
|
||||
|
||||
val flow = if (config.preview) {
|
||||
previewFlow()
|
||||
} else {
|
||||
combine(karooSystem.getHeadingFlow(context), karooSystem.streamDatatypeIsVisible(dataTypeId)){ bearingResponse, isVisible ->
|
||||
val bearing = when(bearingResponse) {
|
||||
is HeadingResponse.Value -> ((bearingResponse.diff + 360) % 360).toFloat()
|
||||
else -> null
|
||||
}
|
||||
|
||||
StreamData(bearing = bearing ?: 0f, isVisible = isVisible)
|
||||
}
|
||||
}
|
||||
|
||||
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
emitter.onNext(ShowCustomStreamState("", null))
|
||||
|
||||
val refreshRate = karooSystem.getRefreshRateInMilliseconds(context)
|
||||
flow.filter { it.isVisible }.throttle(refreshRate).collect { streamData ->
|
||||
val result = glance.compose(context, androidx.compose.ui.unit.DpSize.Unspecified) {
|
||||
Box(
|
||||
modifier = if (!config.preview) baseModifier.clickable(actionStartActivity<MainActivity>()) else baseModifier,
|
||||
) {
|
||||
Image(
|
||||
modifier = GlanceModifier.fillMaxSize(),
|
||||
provider = ImageProvider(getArrowBitmapByBearing(baseBitmap, 0-streamData.bearing.toInt())),
|
||||
contentDescription = "Relative wind direction indicator",
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
emitter.updateView(result.remoteViews)
|
||||
}
|
||||
}
|
||||
|
||||
emitter.setCancellable {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Stopping compass view with $emitter")
|
||||
configJob.cancel()
|
||||
viewJob.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -72,7 +72,7 @@ class WindDirectionAndSpeedDataType(
|
||||
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Starting wind direction and speed view with $emitter")
|
||||
Log.d(KarooHeadwindExtension.TAG, "Starting headwind direction view with $emitter")
|
||||
|
||||
val baseBitmap = BitmapFactory.decodeResource(
|
||||
context.resources,
|
||||
|
||||
@ -49,6 +49,4 @@
|
||||
<string name="windDirectionAndSpeedCircle_description">Current wind direction and wind speed (Circle graphics)</string>
|
||||
<string name="forces">Resistance forces</string>
|
||||
<string name="forces_description">Current resistance forces (air, rolling, gradient)</string>
|
||||
<string name="compass">Compass</string>
|
||||
<string name="compass_description">Current compass direction</string>
|
||||
</resources>
|
||||
@ -165,11 +165,4 @@
|
||||
graphical="true"
|
||||
icon="@drawable/wind"
|
||||
typeId="forces" />
|
||||
|
||||
<DataType
|
||||
description="@string/compass_description"
|
||||
displayName="@string/compass"
|
||||
graphical="true"
|
||||
icon="@drawable/arrow"
|
||||
typeId="compass" />
|
||||
</ExtensionInfo>
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
package de.timklge.karooheadwind
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.Assert.assertEquals
|
||||
|
||||
class DataStoreTest {
|
||||
|
||||
@Test
|
||||
fun testLerpAngle_normalCase() {
|
||||
// Normal case (no wrap-around)
|
||||
val result = lerpAngle(90.0, 180.0, 0.5)
|
||||
assertEquals(135.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLerpAngle_wrapAround0To360() {
|
||||
// Wrap around 0/360 degrees
|
||||
val result = lerpAngle(350.0, 10.0, 0.5)
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLerpAngle_wrapAround360To0() {
|
||||
// Wrap around 0/360 degrees (reverse)
|
||||
val result = lerpAngle(10.0, 350.0, 0.5)
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLerpAngle_largeAngleDifference() {
|
||||
// Large angle difference (should take shorter path)
|
||||
val result = lerpAngle(45.0, 315.0, 0.5)
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLerpAngle_exactly180DegreesApart() {
|
||||
// Exactly 180 degrees apart
|
||||
assertEquals(90.0, lerpAngle(0.0, 180.0, 0.5), 0.001)
|
||||
assertEquals(90.0, lerpAngle(180.0, 0.0, 0.5), 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLerpAngle_factorBoundaries() {
|
||||
// Factor = 0 and 1
|
||||
assertEquals(350.0, lerpAngle(350.0, 10.0, 0.0), 0.001)
|
||||
assertEquals(0.0, lerpAngle(350.0, 10.0, 0.5), 0.001)
|
||||
assertEquals(10.0, lerpAngle(350.0, 10.0, 1.0), 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLerpAngle_negativeAngles() {
|
||||
// Negative angles
|
||||
val result = lerpAngle(-10.0, 10.0, 0.5)
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLerpAngle_anglesGreaterThan360() {
|
||||
// Angles > 360
|
||||
val result = lerpAngle(370.0, 380.0, 0.5)
|
||||
assertEquals(15.0, result, 0.001)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user