diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 54850b5..7c683d0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -12,7 +12,7 @@ android {
defaultConfig {
applicationId = "com.example.trackpro"
minSdk = 26
- targetSdk = 34
+ targetSdk = 34 // intentionally behind compileSdk 35, not yet verified against Android 15 behavior changes
versionCode = 1
versionName = "1.1"
@@ -54,7 +54,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.0")
- implementation(platform("androidx.compose:compose-bom:2023.08.00"))
+ implementation(platform("androidx.compose:compose-bom:2024.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
@@ -68,7 +68,7 @@ dependencies {
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
- androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
+ androidTestImplementation(platform("androidx.compose:compose-bom:2024.03.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
@@ -101,8 +101,6 @@ dependencies {
implementation ("com.google.accompanist:accompanist-pager:0.31.1-alpha")
implementation ("com.google.accompanist:accompanist-pager-indicators:0.31.1-alpha")
- implementation(platform("androidx.compose:compose-bom:2024.03.00")) // Update to latest BOM
-
implementation ("com.google.code.gson:gson:2.10.1")
//MAP
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1509f18..e2d6409 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,11 +34,9 @@
-
+
-
-
\ No newline at end of file
diff --git a/app/src/main/java/com/example/trackpro/MainActivity.kt b/app/src/main/java/com/example/trackpro/MainActivity.kt
index 66eedb7..caba154 100644
--- a/app/src/main/java/com/example/trackpro/MainActivity.kt
+++ b/app/src/main/java/com/example/trackpro/MainActivity.kt
@@ -42,6 +42,8 @@ import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -112,6 +114,22 @@ class TrackProApp : Application() {
val useExternalGps = MutableStateFlow(true)
+ private val themePrefs by lazy { getSharedPreferences("theme_prefs", MODE_PRIVATE) }
+ val useDarkTheme by lazy { MutableStateFlow(themePrefs.getBoolean("dark_theme", true)) }
+
+ fun setDarkTheme(enabled: Boolean) {
+ themePrefs.edit().putBoolean("dark_theme", enabled).apply()
+ useDarkTheme.value = enabled
+ }
+
+ private val unitPrefs by lazy { getSharedPreferences("unit_prefs", MODE_PRIVATE) }
+ val useMetricUnits by lazy { MutableStateFlow(unitPrefs.getBoolean("metric_units", true)) }
+
+ fun setMetricUnits(enabled: Boolean) {
+ unitPrefs.edit().putBoolean("metric_units", enabled).apply()
+ useMetricUnits.value = enabled
+ }
+
val gpsManager: GpsManager by lazy {
GpsManager(
espProvider = espTcpClient,
@@ -168,7 +186,8 @@ class MainActivity : ComponentActivity() {
setContent {
- TrackProTheme {
+ val useDarkTheme by (application as TrackProApp).useDarkTheme.collectAsState()
+ TrackProTheme(darkTheme = useDarkTheme) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "main") {
composable("main") {
diff --git a/app/src/main/java/com/example/trackpro/dao/LapTimeDataDAO.kt b/app/src/main/java/com/example/trackpro/dao/LapTimeDataDAO.kt
index bee6385..63dfeff 100644
--- a/app/src/main/java/com/example/trackpro/dao/LapTimeDataDAO.kt
+++ b/app/src/main/java/com/example/trackpro/dao/LapTimeDataDAO.kt
@@ -32,6 +32,18 @@ interface LapTimeDataDAO {
@Query("SELECT * FROM lap_time_data WHERE sessionid = :sessionId ORDER BY laptime ASC LIMIT 1")
suspend fun getBestLapForSession(sessionId: Long): LapTimeData?
+ // Get best completed lap across all sessions recorded on a track
+ @Query("""
+ SELECT lap_time_data.* FROM lap_time_data
+ INNER JOIN session_data ON lap_time_data.sessionid = session_data.id
+ WHERE session_data.trackId = :trackId
+ AND lap_time_data.laptime != 'IN PROGRESS'
+ AND lap_time_data.laptime != 'INVALID'
+ ORDER BY lap_time_data.laptime ASC
+ LIMIT 1
+ """)
+ suspend fun getBestLapForTrack(trackId: Long): LapTimeData?
+
// Get total number of laps in a session
@Query("SELECT COUNT(*) FROM lap_time_data WHERE sessionid = :sessionId")
suspend fun getLapCountForSession(sessionId: Long): Int
diff --git a/app/src/main/java/com/example/trackpro/dao/RawGPSDataDao.kt b/app/src/main/java/com/example/trackpro/dao/RawGPSDataDao.kt
index b295b15..530ba36 100644
--- a/app/src/main/java/com/example/trackpro/dao/RawGPSDataDao.kt
+++ b/app/src/main/java/com/example/trackpro/dao/RawGPSDataDao.kt
@@ -18,5 +18,5 @@ interface RawGPSDataDao {
suspend fun getGPSDataBySession(sessionId: Long?): List
@Query("DELETE FROM raw_gps_data WHERE sessionId = :sessionId")
- suspend fun deleteGPSDataBySession(sessionId: Int)
+ suspend fun deleteGPSDataBySession(sessionId: Long)
}
diff --git a/app/src/main/java/com/example/trackpro/dao/TrackCoordinatesDataDAO.kt b/app/src/main/java/com/example/trackpro/dao/TrackCoordinatesDataDAO.kt
index 4de6151..55683f6 100644
--- a/app/src/main/java/com/example/trackpro/dao/TrackCoordinatesDataDAO.kt
+++ b/app/src/main/java/com/example/trackpro/dao/TrackCoordinatesDataDAO.kt
@@ -24,5 +24,5 @@ interface TrackCoordinatesDataDAO {
//OR
// IF the user filters the coordinates (if the full track is complete)
@Query("DELETE FROM track_coordinates_data WHERE trackId = :trackId")
- suspend fun deleteTrackCoordinates(trackId: Int)
+ suspend fun deleteTrackCoordinates(trackId: Long)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/trackpro/dataClasses/DerivedData.kt b/app/src/main/java/com/example/trackpro/dataClasses/DerivedData.kt
index f10804d..fc4b8a7 100644
--- a/app/src/main/java/com/example/trackpro/dataClasses/DerivedData.kt
+++ b/app/src/main/java/com/example/trackpro/dataClasses/DerivedData.kt
@@ -1,6 +1,7 @@
package com.example.trackpro.dataClasses
import androidx.room.Entity
+import androidx.room.Index
import androidx.room.PrimaryKey
@@ -10,7 +11,8 @@ import androidx.room.PrimaryKey
parentColumns = ["id"],
childColumns = ["sessionid"],
onDelete = androidx.room.ForeignKey.CASCADE
-)])
+)],
+ indices = [Index("sessionid")])
data class DerivedData(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
diff --git a/app/src/main/java/com/example/trackpro/dataClasses/LapInfoData.kt b/app/src/main/java/com/example/trackpro/dataClasses/LapInfoData.kt
index 17bfa35..37ea7a4 100644
--- a/app/src/main/java/com/example/trackpro/dataClasses/LapInfoData.kt
+++ b/app/src/main/java/com/example/trackpro/dataClasses/LapInfoData.kt
@@ -2,6 +2,7 @@ package com.example.trackpro.dataClasses
import androidx.room.Entity
import androidx.room.ForeignKey
+import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
@@ -11,7 +12,8 @@ import androidx.room.PrimaryKey
parentColumns = ["id"],
childColumns = ["lapid"],
onDelete = ForeignKey.CASCADE
- )]
+ )],
+ indices = [Index("lapid")]
)
data class LapInfoData(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
diff --git a/app/src/main/java/com/example/trackpro/dataClasses/LapTimeData.kt b/app/src/main/java/com/example/trackpro/dataClasses/LapTimeData.kt
index a1ce81e..29e9b8c 100644
--- a/app/src/main/java/com/example/trackpro/dataClasses/LapTimeData.kt
+++ b/app/src/main/java/com/example/trackpro/dataClasses/LapTimeData.kt
@@ -2,6 +2,7 @@ package com.example.trackpro.dataClasses
import androidx.room.Entity
import androidx.room.ForeignKey
+import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
@@ -11,7 +12,8 @@ import androidx.room.PrimaryKey
parentColumns = ["id"],
childColumns = ["sessionid"],
onDelete = ForeignKey.CASCADE
- )]
+ )],
+ indices = [Index("sessionid")]
)
data class LapTimeData (
diff --git a/app/src/main/java/com/example/trackpro/dataClasses/RawGPSData.kt b/app/src/main/java/com/example/trackpro/dataClasses/RawGPSData.kt
index d049991..1a11584 100644
--- a/app/src/main/java/com/example/trackpro/dataClasses/RawGPSData.kt
+++ b/app/src/main/java/com/example/trackpro/dataClasses/RawGPSData.kt
@@ -3,6 +3,7 @@ package com.example.trackpro.dataClasses
import androidx.room.Entity
import androidx.room.ForeignKey
+import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
@@ -12,7 +13,8 @@ import androidx.room.PrimaryKey
parentColumns = ["id"],
childColumns = ["sessionid"],
onDelete = ForeignKey.CASCADE
- )]
+ )],
+ indices = [Index("sessionid")]
)
data class RawGPSData(
diff --git a/app/src/main/java/com/example/trackpro/dataClasses/SessionData.kt b/app/src/main/java/com/example/trackpro/dataClasses/SessionData.kt
index 7bd747c..9306166 100644
--- a/app/src/main/java/com/example/trackpro/dataClasses/SessionData.kt
+++ b/app/src/main/java/com/example/trackpro/dataClasses/SessionData.kt
@@ -1,6 +1,7 @@
package com.example.trackpro.dataClasses
import androidx.room.Entity
+import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
@@ -10,7 +11,8 @@ import androidx.room.PrimaryKey
parentColumns = ["vehicleId"],
childColumns = ["vehicleId"],
onDelete = androidx.room.ForeignKey.CASCADE
- )]
+ )],
+ indices = [Index("vehicleId"), Index("trackId")]
)
data class SessionData(
diff --git a/app/src/main/java/com/example/trackpro/dataClasses/SmoothedGPSData.kt b/app/src/main/java/com/example/trackpro/dataClasses/SmoothedGPSData.kt
index 9b28a72..b015725 100644
--- a/app/src/main/java/com/example/trackpro/dataClasses/SmoothedGPSData.kt
+++ b/app/src/main/java/com/example/trackpro/dataClasses/SmoothedGPSData.kt
@@ -3,6 +3,7 @@ package com.example.trackpro.dataClasses
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.ForeignKey
+import androidx.room.Index
@Entity(
tableName = "smoothed_gps_data" ,
@@ -11,7 +12,8 @@ import androidx.room.ForeignKey
parentColumns = ["id"],
childColumns = ["sessionid"],
onDelete = ForeignKey.CASCADE
- )]
+ )],
+ indices = [Index("sessionid")]
)
data class SmoothedGPSData (
diff --git a/app/src/main/java/com/example/trackpro/dataClasses/TrackCoordinatesData.kt b/app/src/main/java/com/example/trackpro/dataClasses/TrackCoordinatesData.kt
index c8a6608..5a8ce7f 100644
--- a/app/src/main/java/com/example/trackpro/dataClasses/TrackCoordinatesData.kt
+++ b/app/src/main/java/com/example/trackpro/dataClasses/TrackCoordinatesData.kt
@@ -2,6 +2,7 @@ package com.example.trackpro.dataClasses
import androidx.room.Entity
import androidx.room.ForeignKey
+import androidx.room.Index
import androidx.room.PrimaryKey
@@ -12,7 +13,8 @@ import androidx.room.PrimaryKey
parentColumns = ["trackId"],
childColumns = ["trackId"],
onDelete = ForeignKey.CASCADE
- )]
+ )],
+ indices = [Index("trackId")]
)
data class TrackCoordinatesData(
@PrimaryKey(autoGenerate = true)
@@ -21,5 +23,7 @@ data class TrackCoordinatesData(
val latitude: Double,
val longitude: Double,
val altitude: Double?,
- val isStartPoint: Boolean = false
+ val isStartPoint: Boolean = false,
+ val isSectorPoint: Boolean = false,
+ val sectorIndex: Int? = null
)
diff --git a/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt b/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt
index 5daae37..7b4d733 100644
--- a/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt
+++ b/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt
@@ -5,7 +5,6 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.input.KeyboardType
@@ -20,17 +19,25 @@ fun CustomTextField(
OutlinedTextField(
value = value,
onValueChange = onValueChange,
- label = { Text(label, color = Color.White) },
+ label = { Text(label, color = TrackProTheme.colors.textMuted) },
keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default,
leadingIcon = if (leadingIcon != null) {
{
Icon(
imageVector = leadingIcon,
contentDescription = null,
- tint = Color.LightGray
+ tint = TrackProTheme.colors.textMuted
)
}
} else null,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedTextColor = TrackProTheme.colors.textPrimary,
+ unfocusedTextColor = TrackProTheme.colors.textPrimary,
+ focusedBorderColor = TrackProTheme.colors.accentCyan,
+ unfocusedBorderColor = TrackProTheme.colors.sectorLine,
+ focusedLabelColor = TrackProTheme.colors.accentCyan,
+ cursorColor = TrackProTheme.colors.accentCyan
+ ),
modifier = Modifier.fillMaxWidth()
)
}
diff --git a/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt b/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt
index 2ee3171..504cc5c 100644
--- a/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt
+++ b/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt
@@ -33,25 +33,33 @@ fun DropdownMenuFieldMulti(label: String, options: List, selectedOp
value = selectedText,
onValueChange = {},
readOnly = true,
- label = { Text(label) },
+ label = { Text(label, color = TrackProTheme.colors.textMuted) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = OutlinedTextFieldDefaults.colors(
- focusedBorderColor = Color(0xFF007BFF),
- unfocusedBorderColor = Color.Gray
+ focusedTextColor = TrackProTheme.colors.textPrimary,
+ unfocusedTextColor = TrackProTheme.colors.textPrimary,
+ focusedBorderColor = TrackProTheme.colors.accentCyan,
+ unfocusedBorderColor = TrackProTheme.colors.sectorLine
),
modifier = Modifier
.fillMaxWidth()
)
- ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
-
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ modifier = Modifier.background(TrackProTheme.colors.bgElevated)
+ ) {
options.forEach { option ->
Log.d("trackpro", "ID:" + option.vehicleId)
- DropdownMenuItem(text = { Text(option.manufacturerAndModel) }, onClick = {
- selectedText = option.manufacturerAndModel
- expanded = false
- onOptionSelected(option.vehicleId)
- })
+ DropdownMenuItem(
+ text = { Text(option.manufacturerAndModel, color = TrackProTheme.colors.textPrimary) },
+ onClick = {
+ selectedText = option.manufacturerAndModel
+ expanded = false
+ onOptionSelected(option.vehicleId)
+ }
+ )
}
}
}
@@ -73,25 +81,39 @@ fun TrackDropdownMenu(
value = selectedText,
onValueChange = {},
readOnly = true,
- label = { Text(label) },
+ label = { Text(label, color = TrackProTheme.colors.textMuted) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = OutlinedTextFieldDefaults.colors(
- focusedBorderColor = Color(0xFF007BFF),
- unfocusedBorderColor = Color.Gray
+ focusedTextColor = TrackProTheme.colors.textPrimary,
+ unfocusedTextColor = TrackProTheme.colors.textPrimary,
+ focusedBorderColor = TrackProTheme.colors.accentCyan,
+ unfocusedBorderColor = TrackProTheme.colors.sectorLine
),
modifier = Modifier.fillMaxWidth()
)
- ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
- tracks.forEach { track ->
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ modifier = Modifier.background(TrackProTheme.colors.bgElevated)
+ ) {
+ if (tracks.isEmpty()) {
DropdownMenuItem(
- text = { Text(track.trackName) },
- onClick = {
- selectedText = track.trackName
- expanded = false
- onTrackSelected(track.trackId)
- }
+ text = { Text("No tracks found", color = TrackProTheme.colors.textMuted) },
+ onClick = { expanded = false },
+ enabled = false
)
+ } else {
+ tracks.forEach { track ->
+ DropdownMenuItem(
+ text = { Text(track.trackName, color = TrackProTheme.colors.textPrimary) },
+ onClick = {
+ selectedText = track.trackName
+ expanded = false
+ onTrackSelected(track.trackId)
+ }
+ )
+ }
}
}
}
@@ -105,7 +127,7 @@ fun DropdownMenuField(
label: String,
options: List,
selectedOption: String,
- textColor: Color = Color.White,
+ textColor: Color = TrackProTheme.colors.textPrimary,
onOptionSelected: (String) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
@@ -115,10 +137,16 @@ fun DropdownMenuField(
value = selectedOption,
onValueChange = {},
readOnly = true,
- label = { Text(label, color = textColor) },
+ label = { Text(label, color = TrackProTheme.colors.textMuted) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
},
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedTextColor = textColor,
+ unfocusedTextColor = textColor,
+ focusedBorderColor = TrackProTheme.colors.accentCyan,
+ unfocusedBorderColor = TrackProTheme.colors.sectorLine
+ ),
modifier = Modifier
.fillMaxWidth()
)
@@ -126,11 +154,11 @@ fun DropdownMenuField(
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
- modifier = Modifier.background(Color(0xFF2C2C2C))
+ modifier = Modifier.background(TrackProTheme.colors.bgElevated)
) {
options.forEach { option ->
DropdownMenuItem(
- text = { Text(option, color = Color.White) },
+ text = { Text(option, color = TrackProTheme.colors.textPrimary) },
onClick = {
onOptionSelected(option)
expanded = false
diff --git a/app/src/main/java/com/example/trackpro/extrasForUI/TrackProTheme.kt b/app/src/main/java/com/example/trackpro/extrasForUI/TrackProTheme.kt
index 7375c5a..835197b 100644
--- a/app/src/main/java/com/example/trackpro/extrasForUI/TrackProTheme.kt
+++ b/app/src/main/java/com/example/trackpro/extrasForUI/TrackProTheme.kt
@@ -5,6 +5,10 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.ui.graphics.Color
import com.example.trackpro.theme.DarkTrackProColors
import com.example.trackpro.theme.LightTrackProColors
import com.example.trackpro.theme.TrackProColorScheme
@@ -19,6 +23,51 @@ object TrackProTheme {
get() = LocalTrackProColors.current
}
+/**
+ * Maps our racing-HUD palette onto Material3's ColorScheme so that stock Material3
+ * components (OutlinedTextField, DropdownMenu, AlertDialog, etc.) that don't explicitly
+ * override every color still fall back to something themed instead of Material3's
+ * default light/purple scheme.
+ */
+private fun TrackProColorScheme.toMaterialColorScheme(dark: Boolean): androidx.compose.material3.ColorScheme =
+ if (dark) {
+ darkColorScheme(
+ primary = accentCyan,
+ onPrimary = Color.Black,
+ secondary = accentBlue,
+ onSecondary = Color.Black,
+ tertiary = accentAmber,
+ onTertiary = Color.Black,
+ background = bgDeep,
+ onBackground = textPrimary,
+ surface = bgCard,
+ onSurface = textPrimary,
+ surfaceVariant = bgElevated,
+ onSurfaceVariant = textMuted,
+ outline = sectorLine,
+ error = deltaBad,
+ onError = Color.White
+ )
+ } else {
+ lightColorScheme(
+ primary = accentCyan,
+ onPrimary = Color.Black,
+ secondary = accentBlue,
+ onSecondary = Color.Black,
+ tertiary = accentAmber,
+ onTertiary = Color.Black,
+ background = bgDeep,
+ onBackground = textPrimary,
+ surface = bgCard,
+ onSurface = textPrimary,
+ surfaceVariant = bgElevated,
+ onSurfaceVariant = textMuted,
+ outline = sectorLine,
+ error = deltaBad,
+ onError = Color.White
+ )
+ }
+
@Composable
fun TrackProTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
@@ -27,7 +76,11 @@ fun TrackProTheme(
val colorScheme = if (darkTheme) DarkTrackProColors else LightTrackProColors
CompositionLocalProvider(
- LocalTrackProColors provides colorScheme,
- content = content
- )
+ LocalTrackProColors provides colorScheme
+ ) {
+ MaterialTheme(
+ colorScheme = colorScheme.toMaterialColorScheme(darkTheme),
+ content = content
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt b/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt
index 515e8f1..ebfb96b 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt
@@ -10,6 +10,7 @@ import com.example.trackpro.dao.DerivedDataDao
import com.example.trackpro.dao.LapInfoDataDAO
import com.example.trackpro.dao.LapTimeDataDAO
import com.example.trackpro.dao.RawGPSDataDao
+import com.example.trackpro.dao.SectorTimeDataDAO
import com.example.trackpro.dao.SessionDataDao
import com.example.trackpro.dao.SmoothedGPSDataDAO
import com.example.trackpro.dao.TrackCoordinatesDataDAO
@@ -19,6 +20,7 @@ import com.example.trackpro.dataClasses.DerivedData
import com.example.trackpro.dataClasses.LapInfoData
import com.example.trackpro.dataClasses.LapTimeData
import com.example.trackpro.dataClasses.RawGPSData
+import com.example.trackpro.dataClasses.SectorTimeData
import com.example.trackpro.dataClasses.SessionData
import com.example.trackpro.dataClasses.SmoothedGPSData
import com.example.trackpro.dataClasses.TrackCoordinatesData
@@ -42,8 +44,9 @@ import kotlinx.coroutines.launch
TrackCoordinatesData::class,
VehicleInformationData::class,
LapTimeData::class,
- LapInfoData::class
-], version = 1, exportSchema = false)
+ LapInfoData::class,
+ SectorTimeData::class
+], version = 3, exportSchema = false)
abstract class ESPDatabase : RoomDatabase() {
abstract fun sessionDataDao(): SessionDataDao
abstract fun rawGPSDataDao(): RawGPSDataDao
@@ -54,6 +57,7 @@ abstract class ESPDatabase : RoomDatabase() {
abstract fun vehicleInformationDAO(): VehicleInformationDAO
abstract fun lapTimeDataDAO(): LapTimeDataDAO
abstract fun lapInfoDataDAO(): LapInfoDataDAO
+ abstract fun sectorTimeDataDAO(): SectorTimeDataDAO
companion object {
@Volatile
@@ -114,6 +118,11 @@ abstract class ESPDatabase : RoomDatabase() {
}
})
+ // No migrations exist yet; without this, any future (or this) schema
+ // change throws IllegalStateException on every existing install instead
+ // of recovering. Replace with real Migration objects once the schema
+ // needs to be preserved across upgrades.
+ .fallbackToDestructiveMigration()
.build()
INSTANCE = instance
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/jsonReader.kt b/app/src/main/java/com/example/trackpro/managerClasses/JsonReader.kt
similarity index 100%
rename from app/src/main/java/com/example/trackpro/managerClasses/jsonReader.kt
rename to app/src/main/java/com/example/trackpro/managerClasses/JsonReader.kt
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/SessionManager.kt b/app/src/main/java/com/example/trackpro/managerClasses/SessionManager.kt
index e5c6725..2f0d9dc 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/SessionManager.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/SessionManager.kt
@@ -21,7 +21,7 @@ class SessionManager private constructor(
trackId = trackId,
)
currentSessionId = sessionDataDao.insertSession(session) // Insert session
- Log.e("SessionManager", "Inserted session with ID: $currentSessionId")
+ Log.d("SessionManager", "Inserted session with ID: $currentSessionId")
return currentSessionId ?: throw Exception("Failed to insert session")
}
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/calculationClasses/DragTimeCalculation.kt b/app/src/main/java/com/example/trackpro/managerClasses/calculationClasses/DragTimeCalculation.kt
index 9cc4bb9..525e6f4 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/calculationClasses/DragTimeCalculation.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/calculationClasses/DragTimeCalculation.kt
@@ -29,13 +29,19 @@ class DragTimeCalculation(
val session: Long? = null,
private val database: ESPDatabase
) {
+ companion object {
+ private const val QUARTER_MILE_METERS = 402.336f
+ private const val HALF_MILE_METERS = 804.672f
+ }
+
private val zeroThreshold = 2.0f // Speed threshold to consider as "zero" (km/h)
// Real-time tracking state
private var runStartTime: Long = 0L
private var hasStartedRun: Boolean = false
- private val gpsPoints = mutableListOf()
+ private var lastPoint: LatLonOffset? = null
private var totalDistanceMeters: Float = 0f
+ private var runStartDistanceMeters: Float = 0f
private var maxSpeedRecorded: Float = 0f
// Metric tracking
@@ -59,12 +65,12 @@ class DragTimeCalculation(
// --- 1. GLOBAL STATS ---
if (currentSpeed > maxSpeedRecorded) maxSpeedRecorded = currentSpeed
- gpsPoints.add(LatLonOffset(gpsData.latitude, gpsData.longitude))
- if (gpsPoints.size >= 2) {
- val last = gpsPoints[gpsPoints.size - 2]
- val dist = haversineDistance(last.lat, last.lon, gpsData.latitude, gpsData.longitude)
+ val currentPoint = LatLonOffset(gpsData.latitude, gpsData.longitude)
+ lastPoint?.let { last ->
+ val dist = haversineDistance(last.lat, last.lon, currentPoint.lat, currentPoint.lon)
totalDistanceMeters += dist.toFloat()
}
+ lastPoint = currentPoint
// --- 2. STANDING START LOGIC ---
// If we are below threshold, we are "Ready" to perform a 0-X run
@@ -76,6 +82,7 @@ class DragTimeCalculation(
if (!hasStartedRun && isReadyForRun && currentSpeed > zeroThreshold) {
hasStartedRun = true
runStartTime = currentTimeMillis
+ runStartDistanceMeters = totalDistanceMeters
}
// Calculate Standing Metrics (Only if a valid run started)
@@ -87,11 +94,16 @@ class DragTimeCalculation(
if (time0to160Result == null && currentSpeed >= 160f) time0to160Result = elapsed
if (time0to200Result == null && currentSpeed >= 200f) time0to200Result = elapsed
- // Quarter Mile (Standing only)
- if (quarterMileTimeResult == null && totalDistanceMeters >= 402.336f) {
+ // Quarter Mile (Standing only, measured from the run's start, not the whole recording)
+ if (quarterMileTimeResult == null && (totalDistanceMeters - runStartDistanceMeters) >= QUARTER_MILE_METERS) {
quarterMileTimeResult = elapsed
quarterMileSpeedResult = currentSpeed
}
+
+ // Half Mile (Standing only, measured from the run's start)
+ if (halfMileTimeResult == null && (totalDistanceMeters - runStartDistanceMeters) >= HALF_MILE_METERS) {
+ halfMileTimeResult = elapsed
+ }
}
// --- 3. ROLLING METRICS LOGIC (Always Active) ---
@@ -145,8 +157,10 @@ class DragTimeCalculation(
fun resetRealtimeTracking() {
runStartTime = 0L
hasStartedRun = false
- gpsPoints.clear()
+ isReadyForRun = false
+ lastPoint = null
totalDistanceMeters = 0f
+ runStartDistanceMeters = 0f
maxSpeedRecorded = 0f
time0to60Result = null
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/calculationClasses/PostProcessing.kt b/app/src/main/java/com/example/trackpro/managerClasses/calculationClasses/PostProcessing.kt
index 60cea01..ea0b883 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/calculationClasses/PostProcessing.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/calculationClasses/PostProcessing.kt
@@ -56,9 +56,6 @@ class PostProcessing(val database: ESPDatabase) {
val smoothedAlt = altWindow.filterNotNull().averageOrNull() ?: gpsData.altitude
val smoothedSpeed = speedWindow.filterNotNull().averageOrNull() ?: gpsData.speed
- // Debug log
- Log.d("PostProcessing", "Timestamp: ${gpsData.timestamp}, Smoothed Speed: $smoothedSpeed")
-
// Add smoothed result to the list
smoothed.add(
SmoothedGPSData(
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/PhoneGpsProvider.kt b/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/PhoneGpsProvider.kt
index 14eebfd..202cbb5 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/PhoneGpsProvider.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/PhoneGpsProvider.kt
@@ -1,8 +1,12 @@
package com.example.trackpro.managerClasses.gpsDataManagers
+import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
+import android.content.pm.PackageManager
import android.os.Looper
+import android.util.Log
+import androidx.core.content.ContextCompat
import com.example.trackpro.dataClasses.RawGPSData
import com.example.trackpro.models.GpsProvider
import com.google.android.gms.location.LocationCallback
@@ -14,7 +18,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class PhoneGpsProvider(
- context: Context,
+ private val context: Context,
) : GpsProvider {
private val client = LocationServices.getFusedLocationProviderClient(context)
@@ -41,6 +45,18 @@ class PhoneGpsProvider(
@SuppressLint("MissingPermission")
override fun start() {
+ val hasFineLocation = ContextCompat.checkSelfPermission(
+ context, Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ val hasCoarseLocation = ContextCompat.checkSelfPermission(
+ context, Manifest.permission.ACCESS_COARSE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ if (!hasFineLocation && !hasCoarseLocation) {
+ Log.w("PhoneGpsProvider", "Location permission not granted, cannot start phone GPS")
+ _isConnected.value = false
+ return
+ }
+
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 100).build()
client.requestLocationUpdates(request, callback, Looper.getMainLooper())
_isConnected.value = true
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/CircuitTiming.kt b/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/CircuitTiming.kt
index ebb19c0..2fe5b3f 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/CircuitTiming.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/CircuitTiming.kt
@@ -4,15 +4,30 @@ import android.os.SystemClock
import com.example.trackpro.dataClasses.TrackCoordinatesData
import kotlinx.coroutines.channels.Channel
import com.example.trackpro.dataClasses.RawGPSData
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** One completed sector split for the lap currently in progress. deltaMs is vs. this session's best for that sector, or null if this is the first time it's been recorded. */
+data class SectorSplit(val sectorIndex: Int, val splitMs: Long, val deltaMs: Long?)
class CircuitTimingManager(
- private val finishLine: List
+ private val finishLine: List,
+ private val sectorLines: List> = emptyList()
) : TimingManager() {
private var lapStartTime = SystemClock.elapsedRealtime()
private var lastCrossTime = 0L
+ private var lastSplitTime = lapStartTime
private var bestLapSeconds = Double.POSITIVE_INFINITY
private var hasStarted = false
+ private var currentSectorIndex = 0
+ private val bestSectorMs = mutableMapOf()
+
val lapCompletedChannel = Channel(Channel.UNLIMITED)
+ val sectorCompletedChannel = Channel(Channel.UNLIMITED)
+
+ private val _currentLapSplits = MutableStateFlow>(emptyList())
+ val currentLapSplits: StateFlow> = _currentLapSplits.asStateFlow()
override fun handleGpsUpdate(
prev: RawGPSData?,
@@ -20,23 +35,51 @@ class CircuitTimingManager(
) {
val now = SystemClock.elapsedRealtime()
prev?.let { prevData ->
- TrackGeometry.checkLineCrossing(prevData, current, finishLine)?.let { crossing ->
- if (crossing.isValid && now - lastCrossTime > 5000) {
- if (!hasStarted) {
- hasStarted = true
- lastCrossTime = now
- lapStartTime = now
- return
- }
+ val finishCrossing = TrackGeometry.checkLineCrossing(prevData, current, finishLine)
+ var finishHandled = false
+ if (finishCrossing != null && finishCrossing.isValid && now - lastCrossTime > 5000) {
+ finishHandled = true
+ if (!hasStarted) {
+ hasStarted = true
+ lastCrossTime = now
+ lapStartTime = now
+ lastSplitTime = now
+ currentSectorIndex = 0
+ _currentLapSplits.value = emptyList()
+ } else {
val lapMs = now - lapStartTime
updateTimes(lapMs)
lastCrossTime = now
lapStartTime = now
+ lastSplitTime = now
+ currentSectorIndex = 0
+ _currentLapSplits.value = emptyList()
_eventCount.value += 1
lapCompletedChannel.trySend(lapMs)
}
}
+
+ // Only look for the next expected sector gate while a lap is in progress, and
+ // only if the finish line didn't just fire on this same update.
+ if (!finishHandled && hasStarted && currentSectorIndex < sectorLines.size) {
+ val gate = sectorLines[currentSectorIndex]
+ val sectorCrossing = TrackGeometry.checkLineCrossing(prevData, current, gate)
+ if (sectorCrossing != null && sectorCrossing.isValid && now - lastCrossTime > 5000) {
+ val splitMs = now - lastSplitTime
+ val best = bestSectorMs[currentSectorIndex]
+ val deltaMs = best?.let { splitMs - it }
+ if (best == null || splitMs < best) bestSectorMs[currentSectorIndex] = splitMs
+
+ val split = SectorSplit(currentSectorIndex, splitMs, deltaMs)
+ _currentLapSplits.value = _currentLapSplits.value + split
+ sectorCompletedChannel.trySend(split)
+
+ lastCrossTime = now
+ lastSplitTime = now
+ currentSectorIndex += 1
+ }
+ }
}
_currentTime.value = formatTime(now - lapStartTime)
}
@@ -55,13 +98,20 @@ class CircuitTimingManager(
override fun reset() {
lapStartTime = SystemClock.elapsedRealtime()
lastCrossTime = 0L
+ lastSplitTime = lapStartTime
hasStarted = false
+ currentSectorIndex = 0
+ bestSectorMs.clear()
+ _currentLapSplits.value = emptyList()
_stintStart.value = lapStartTime
_eventCount.value = 0
}
override fun startNewEvent() {
lapStartTime = SystemClock.elapsedRealtime()
+ lastSplitTime = lapStartTime
+ currentSectorIndex = 0
+ _currentLapSplits.value = emptyList()
_currentTime.value = formatTime(0)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/TrackGeometry.kt b/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/TrackGeometry.kt
index d0b95d6..fbdae49 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/TrackGeometry.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/TrackGeometry.kt
@@ -7,6 +7,8 @@ import kotlin.math.sqrt
object TrackGeometry {
private const val TAG = "TrackGeometry"
+ private const val FINISH_LINE_WIDTH_METERS = 12.0
+ private const val METERS_PER_DEGREE_LATITUDE = 111320.0
data class Vector(val x: Double, val y: Double) {
operator fun plus(v: Vector) = Vector(x + v.x, y + v.y)
@@ -26,35 +28,64 @@ object TrackGeometry {
return if (track.size >= 2) listOf(track[0], track[1]) else track
}
+ return buildGateLine(track, startPoint, idBase = -1) ?: run {
+ Log.w(TAG, "Not enough points near start")
+ if (track.size >= 2) listOf(track[0], track[1]) else track
+ }
+ }
+
+ /**
+ * Computes a perpendicular gate line (2 points) through each point on the track that was
+ * marked as a sector split during track building, ordered by sectorIndex. Tracks with no
+ * marked sector points (e.g. the bundled seed tracks) simply return an empty list.
+ */
+ fun calculateSectorLines(track: List): List> {
+ val sectorPoints = track.filter { it.isSectorPoint }
+ .sortedBy { it.sectorIndex ?: Int.MAX_VALUE }
+
+ return sectorPoints.mapIndexedNotNull { index, point ->
+ buildGateLine(track, point, idBase = -100 - (index * 2L))
+ }
+ }
+
+ /**
+ * Builds a perpendicular gate line through [atPoint], oriented to the track's local
+ * direction of travel around it (same construction as the finish/sector lines), or null
+ * if there aren't enough nearby points to establish a direction.
+ */
+ private fun buildGateLine(
+ track: List,
+ atPoint: TrackCoordinatesData,
+ idBase: Long
+ ): List? {
val nearbyPoints = track.filter {
- it.id in (startPoint.id - 5)..(startPoint.id + 5) && it.id != startPoint.id
+ it.id in (atPoint.id - 5)..(atPoint.id + 5) && it.id != atPoint.id
}.take(10)
- if (nearbyPoints.isEmpty()) {
- Log.w(TAG, "Not enough points near start")
- return if (track.size >= 2) listOf(track[0], track[1]) else track
- }
+ if (nearbyPoints.isEmpty()) return null
val avgDirection = nearbyPoints.fold(Vector(0.0, 0.0)) { acc, point ->
- acc + Vector(point.longitude - startPoint.longitude, point.latitude - startPoint.latitude)
+ acc + Vector(point.longitude - atPoint.longitude, point.latitude - atPoint.latitude)
} * (1.0 / nearbyPoints.size)
val perpendicular = Vector(-avgDirection.y, avgDirection.x).normalized()
- val lineLength = 12.0 / 111320.0
+ val lineLength = FINISH_LINE_WIDTH_METERS / METERS_PER_DEGREE_LATITUDE
val scaledPerpendicular = perpendicular * lineLength
return listOf(
- startPoint.copy(
- id = -1,
- latitude = startPoint.latitude - scaledPerpendicular.y,
- longitude = startPoint.longitude - scaledPerpendicular.x,
- isStartPoint = false
+ atPoint.copy(
+ id = idBase,
+ latitude = atPoint.latitude - scaledPerpendicular.y,
+ longitude = atPoint.longitude - scaledPerpendicular.x,
+ isStartPoint = false,
+ isSectorPoint = false
),
- startPoint.copy(
- id = -2,
- latitude = startPoint.latitude + scaledPerpendicular.y,
- longitude = startPoint.longitude + scaledPerpendicular.x,
- isStartPoint = false
+ atPoint.copy(
+ id = idBase - 1,
+ latitude = atPoint.latitude + scaledPerpendicular.y,
+ longitude = atPoint.longitude + scaledPerpendicular.x,
+ isStartPoint = false,
+ isSectorPoint = false
)
)
}
@@ -69,7 +100,7 @@ object TrackGeometry {
val startPoint = track.first()
val startDirection = calculateDirection(startPoint, track[1])
val startPerpendicular = Vector(-startDirection.y, startDirection.x).normalized()
- val startLineLength = 12.0 / 111320.0
+ val startLineLength = FINISH_LINE_WIDTH_METERS / METERS_PER_DEGREE_LATITUDE
val startLine = listOf(
startPoint.copy(
id = -10,
@@ -87,7 +118,7 @@ object TrackGeometry {
val finishPoint = track.last()
val finishDirection = calculateDirection(track[track.size - 2], finishPoint)
val finishPerpendicular = Vector(-finishDirection.y, finishDirection.x).normalized()
- val finishLineLength = 12.0 / 111320.0
+ val finishLineLength = FINISH_LINE_WIDTH_METERS / METERS_PER_DEGREE_LATITUDE
val finishLine = listOf(
finishPoint.copy(
id = -20,
@@ -136,7 +167,13 @@ object TrackGeometry {
}
return intersection?.let {
- CrossingResult(true, determineDirection(prevPos, currPos, lineStart, lineEnd))
+ val direction = determineDirection(prevPos, currPos, lineStart, lineEnd)
+ // Lines are built with the perpendicular rotated from the track's forward
+ // direction of travel, which makes ENTERING correspond to a crossing in that
+ // forward direction. Only forward crossings should count, otherwise a car
+ // overshooting the line and rolling back across it would register a second
+ // (bogus) crossing.
+ CrossingResult(direction == CrossingDirection.ENTERING, direction)
}
}
diff --git a/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt b/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt
index 0361b05..0ab74dd 100644
--- a/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt
@@ -36,6 +36,8 @@ fun SettingsScreen(onBack: () -> Unit) {
val context = LocalContext.current
val app = context.applicationContext as TrackProApp
val useExternal by app.useExternalGps.collectAsState()
+ val useDarkTheme by app.useDarkTheme.collectAsState()
+ val useMetric by app.useMetricUnits.collectAsState()
Column(
modifier = Modifier
@@ -78,7 +80,7 @@ fun SettingsScreen(onBack: () -> Unit) {
Button(
onClick = { app.useExternalGps.value = !useExternal },
colors = ButtonDefaults.buttonColors(
- containerColor = if (useExternal) TrackProTheme.colors.accentCyan else Color(0xFF1E2530)
+ containerColor = if (useExternal) TrackProTheme.colors.accentCyan else TrackProTheme.colors.bgElevated
),
shape = RoundedCornerShape(4.dp),
modifier = Modifier.height(36.dp)
@@ -93,10 +95,84 @@ fun SettingsScreen(onBack: () -> Unit) {
}
}
- // --- Section: Measurement ---
+ // --- Section: Appearance ---
+ SettingsSectionHeader(title = "APPEARANCE")
+ SettingsCard {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ text = "THEME",
+ color = TrackProTheme.colors.textPrimary,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = if (useDarkTheme) "Dark" else "Light",
+ color = TrackProTheme.colors.accentCyan,
+ fontSize = 12.sp
+ )
+ }
+
+ Button(
+ onClick = { app.setDarkTheme(!useDarkTheme) },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = TrackProTheme.colors.accentCyan
+ ),
+ shape = RoundedCornerShape(4.dp),
+ modifier = Modifier.height(36.dp)
+ ) {
+ Text(
+ text = if (useDarkTheme) "USE LIGHT" else "USE DARK",
+ color = Color.Black,
+ fontSize = 10.sp,
+ fontWeight = FontWeight.ExtraBold
+ )
+ }
+ }
+ }
+
+ // --- Section: Units ---
SettingsSectionHeader(title = "UNITS")
SettingsCard {
- //SettingsToggleRow(label = "Use Metric Units (km/h)", isActive = true) {}
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ text = "SPEED & DISTANCE",
+ color = TrackProTheme.colors.textPrimary,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = if (useMetric) "Metric (km/h, km)" else "Imperial (mph, mi)",
+ color = TrackProTheme.colors.accentCyan,
+ fontSize = 12.sp
+ )
+ }
+
+ Button(
+ onClick = { app.setMetricUnits(!useMetric) },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = TrackProTheme.colors.accentCyan
+ ),
+ shape = RoundedCornerShape(4.dp),
+ modifier = Modifier.height(36.dp)
+ ) {
+ Text(
+ text = if (useMetric) "USE MPH" else "USE KM/H",
+ color = Color.Black,
+ fontSize = 10.sp,
+ fontWeight = FontWeight.ExtraBold
+ )
+ }
+ }
}
// --- Section: System ---
@@ -129,7 +205,7 @@ private fun SettingsCard(content: @Composable () -> Unit) {
modifier = Modifier
.fillMaxWidth()
.background(TrackProTheme.colors.bgCard, RoundedCornerShape(8.dp))
- .border(1.dp, Color(0xFF1E2530), RoundedCornerShape(8.dp))
+ .border(1.dp, TrackProTheme.colors.sectorLine, RoundedCornerShape(8.dp))
.padding(16.dp)
) {
content()
diff --git a/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt b/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt
index 9554709..981eea3 100644
--- a/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt
@@ -81,6 +81,14 @@ fun TrackBuilderScreen(
// Track Points
val gpsPointsList = remember { mutableStateListOf() }
+ var sectorCount by remember { mutableIntStateOf(0) }
+
+ fun markSector() {
+ if (gpsPointsList.isEmpty()) return
+ val lastIndex = gpsPointsList.size - 1
+ gpsPointsList[lastIndex] = gpsPointsList[lastIndex].copy(isSectorPoint = true, sectorIndex = sectorCount)
+ sectorCount += 1
+ }
// UI State
var showInfoDialog by remember { mutableStateOf(false) }
@@ -121,6 +129,7 @@ fun TrackBuilderScreen(
} else {
coroutineScope.launch {
gpsPointsList.clear() // Clear old preview
+ sectorCount = 0
trackID = startTrackBuilder(database, trackName, countryName, trackMode)
isLiveRecording = true
}
@@ -152,6 +161,13 @@ fun TrackBuilderScreen(
canSave = gpsPointsList.size > 1 && trackName.isNotEmpty()
)
}
+
+ Spacer(modifier = Modifier.height(12.dp))
+ MarkSectorButton(
+ count = sectorCount,
+ enabled = gpsPointsList.isNotEmpty() && (builderType == 1 || isLiveRecording),
+ onClick = { markSector() }
+ )
}
// Map/Preview Area
@@ -188,8 +204,9 @@ fun MapLibreBuilderView(
onMapTap: (LatLng) -> Unit
) {
var mapLibreMap by remember { mutableStateOf(null) }
+ val sectorMarkCount = points.count { it.isSectorPoint }
- LaunchedEffect(points.size) {
+ LaunchedEffect(points.size, sectorMarkCount) {
mapLibreMap?.let { map ->
map.clear()
@@ -221,6 +238,16 @@ fun MapLibreBuilderView(
.title(if (trackMode == "Circuit") "LAP COMPLETE" else "FINISH LINE")
)
}
+
+ // Sector markers
+ points.filter { it.isSectorPoint }
+ .sortedBy { it.sectorIndex ?: Int.MAX_VALUE }
+ .forEach { sectorPoint ->
+ map.addMarker(MarkerOptions()
+ .position(LatLng(sectorPoint.latitude, sectorPoint.longitude))
+ .title("S${(sectorPoint.sectorIndex ?: 0) + 1}")
+ )
+ }
}
}
}
@@ -268,6 +295,28 @@ private fun TrackInfoCard(name: String, country: String, mode: String, onClick:
}
}
+@Composable
+private fun MarkSectorButton(count: Int, enabled: Boolean, onClick: () -> Unit) {
+ Button(
+ onClick = onClick,
+ enabled = enabled,
+ modifier = Modifier.fillMaxWidth().height(48.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = TrackProTheme.colors.accentBlue,
+ disabledContainerColor = Color(0xFF1E2530)
+ ),
+ shape = RoundedCornerShape(8.dp)
+ ) {
+ Text(
+ "MARK SECTOR ${count + 1}",
+ color = if (enabled) Color.Black else TrackProTheme.colors.textMuted,
+ fontWeight = FontWeight.Bold,
+ fontSize = 12.sp,
+ letterSpacing = 1.sp
+ )
+ }
+}
+
@Composable
private fun ModeToggle(selected: Int, onSelect: (Int) -> Unit) {
Row(modifier = Modifier.fillMaxWidth().background(TrackProTheme.colors.bgCard, RoundedCornerShape(8.dp)).padding(4.dp)) {
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/CarListView.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/CarListView.kt
index fa00435..534090c 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/CarListView.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/CarListView.kt
@@ -1,9 +1,6 @@
package com.example.trackpro.screens.listViewScreens
import android.content.Context
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -52,16 +49,6 @@ import com.example.trackpro.viewModels.VehicleFULLViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-class CarListView : ComponentActivity()
-{
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent{
-
- }
- }
-}
-
@Composable
fun CarListScreen(navController: NavController, viewModel: VehicleFULLViewModel) {
val context = LocalContext.current
@@ -173,9 +160,9 @@ fun VehicleCard(
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
- containerColor = Color(0xFF0E1117),
- titleContentColor = Color(0xFFF0F2F5),
- textContentColor = Color(0xFF6B7280),
+ containerColor = TrackProTheme.colors.bgCard,
+ titleContentColor = TrackProTheme.colors.textPrimary,
+ textContentColor = TrackProTheme.colors.textMuted,
confirmButton = {
TextButton(onClick = {
onDelete(vehicle)
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt
index 065f3b9..d47efbf 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt
@@ -1,8 +1,5 @@
package com.example.trackpro.screens.listViewScreens
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -15,6 +12,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -35,30 +33,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
-import androidx.navigation.compose.rememberNavController
import com.example.trackpro.extrasForUI.TrackProTheme
import com.example.trackpro.managerClasses.utilities.DateFormatterUtil
import com.example.trackpro.models.DragSessionWithVehicle
import com.example.trackpro.viewModels.DragSessionViewModel
-import com.example.trackpro.viewModels.DragSessionViewModelFactory
import java.util.Date
-class DragTimesList : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- val navController = rememberNavController()
- val viewModel: DragSessionViewModel = viewModel(
- factory = DragSessionViewModelFactory(this)
- )
-
- DragTimesListView(viewModel = viewModel, navController = navController)
- }
- }
-}
-
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun DragTimesListView(
@@ -107,20 +88,40 @@ fun DragTimesListView(
}
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- groupedSessions.forEach { (groupKey, sessions) ->
- item(key = groupKey) {
- ExpandableSessionGroup(
- groupTitle = groupKey,
- sessions = sessions,
- navController = navController
+ if (dragSessions.isEmpty()) {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ "NO SESSIONS RECORDED",
+ color = TrackProTheme.colors.textMuted,
+ fontSize = 14.sp,
+ letterSpacing = 3.sp,
+ fontWeight = FontWeight.Black
+ )
+ Spacer(Modifier.height(8.dp))
+ Text(
+ "Run a drag session to see it here",
+ color = TrackProTheme.colors.textMuted.copy(alpha = 0.5f),
+ fontSize = 12.sp
)
}
}
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ groupedSessions.forEach { (groupKey, sessions) ->
+ item(key = groupKey) {
+ ExpandableSessionGroup(
+ groupTitle = groupKey,
+ sessions = sessions,
+ navController = navController
+ )
+ }
+ }
+ }
}
}
}
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/TimeAttackListView.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/TimeAttackListView.kt
index d2e08f0..309b607 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/TimeAttackListView.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/TimeAttackListView.kt
@@ -76,7 +76,23 @@ fun TimeAttackListViewScreen(
}
if (trackSessions.isEmpty()) {
- // ... Empty State ...
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ "NO SESSIONS RECORDED",
+ color = TrackProTheme.colors.textMuted,
+ fontSize = 14.sp,
+ letterSpacing = 3.sp,
+ fontWeight = FontWeight.Black
+ )
+ Spacer(Modifier.height(8.dp))
+ Text(
+ "Run a track session to see it here",
+ color = TrackProTheme.colors.textMuted.copy(alpha = 0.5f),
+ fontSize = 12.sp
+ )
+ }
+ }
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/TrackListView.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/TrackListView.kt
index b28305e..4cc8a63 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/TrackListView.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/TrackListView.kt
@@ -1,8 +1,5 @@
package com.example.trackpro.screens.listViewScreens
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -28,6 +25,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -49,14 +47,7 @@ import com.example.trackpro.managerClasses.ESPDatabase
import com.example.trackpro.viewModels.TrackViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-
-class TrackListView : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- }
- }
-}
+import kotlinx.coroutines.withContext
@Composable
fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) {
@@ -122,6 +113,7 @@ fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) {
TrackCard(
track = track,
navController = navController,
+ database = database,
bgCard = TrackProTheme.colors.bgCard,
bgElevated = TrackProTheme.colors.bgElevated,
accentAmber = TrackProTheme.colors.accentAmber,
@@ -146,6 +138,7 @@ fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) {
fun TrackCard(
track: TrackMainData,
navController: NavController,
+ database: ESPDatabase,
bgCard: Color,
bgElevated: Color,
accentAmber: Color,
@@ -156,13 +149,20 @@ fun TrackCard(
onDelete: (TrackMainData) -> Unit
) {
var showDeleteDialog by remember { mutableStateOf(false) }
+ var bestLapTime by remember(track.trackId) { mutableStateOf(null) }
+
+ LaunchedEffect(track.trackId) {
+ bestLapTime = withContext(Dispatchers.IO) {
+ database.lapTimeDataDAO().getBestLapForTrack(track.trackId)?.laptime
+ }
+ }
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
- containerColor = Color(0xFF0E1117),
- titleContentColor = Color(0xFFF0F2F5),
- textContentColor = Color(0xFF6B7280),
+ containerColor = TrackProTheme.colors.bgCard,
+ titleContentColor = TrackProTheme.colors.textPrimary,
+ textContentColor = TrackProTheme.colors.textMuted,
confirmButton = {
TextButton(onClick = { onDelete(track); showDeleteDialog = false }) {
Text("DELETE", color = accentRed, fontWeight = FontWeight.Black)
@@ -288,8 +288,7 @@ fun TrackCard(
Text("LAP RECORD", color = textMuted, fontSize = 9.sp,
letterSpacing = 1.sp, fontWeight = FontWeight.Bold)
Text(
- // TODO: wire up actual lap record from DB
- text = "—",
+ text = bestLapTime ?: "—",
color = textPrimary,
fontSize = 16.sp,
fontWeight = FontWeight.Black
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/DragScreenListItem.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/DragScreenListItem.kt
index 48d32c8..b417015 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/DragScreenListItem.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/DragScreenListItem.kt
@@ -11,10 +11,12 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableStateOf
@@ -35,6 +37,9 @@ import com.example.trackpro.dataClasses.RawGPSData
import com.example.trackpro.managerClasses.ESPDatabase
import com.example.trackpro.dataClasses.convertToLatLonOffsetList
import com.example.trackpro.extrasForUI.TrackProTheme
+import com.example.trackpro.TrackProApp
+import com.example.trackpro.managerClasses.utilities.SpeedColorUtils
+import com.example.trackpro.managerClasses.utilities.UnitFormatter
import com.example.trackpro.screens.telemetricScreens.DragMetricCard
import com.example.trackpro.screens.telemetricScreens.DragMetricDisplay
import com.github.mikephil.charting.charts.LineChart
@@ -45,6 +50,19 @@ import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.formatter.ValueFormatter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import org.maplibre.android.MapLibre
+import org.maplibre.android.camera.CameraUpdateFactory
+import org.maplibre.android.geometry.LatLng
+import org.maplibre.android.geometry.LatLngBounds
+import org.maplibre.android.maps.MapLibreMap
+import org.maplibre.android.maps.MapView
+import org.maplibre.android.maps.Style
+import org.maplibre.android.style.expressions.Expression
+import org.maplibre.android.style.layers.CircleLayer
+import org.maplibre.android.style.layers.LineLayer
+import org.maplibre.android.style.layers.Property
+import org.maplibre.android.style.layers.PropertyFactory
+import org.maplibre.android.style.sources.GeoJsonSource
import java.util.concurrent.TimeUnit
import kotlin.math.*
@@ -63,10 +81,13 @@ private fun haversineMeters(lat1: Double, lon1: Double, lat2: Double, lon2: Doub
@Composable
fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
val context = LocalContext.current
+ val app = context.applicationContext as TrackProApp
+ val useMetric by app.useMetricUnits.collectAsState()
val database = remember { ESPDatabase.getInstance(context) }
var coordinates by remember { mutableStateOf(emptyList()) }
val dragTimeClass = remember { DragTimeCalculation(sessionId, database) }
var totalDist by remember { mutableDoubleStateOf(-1.0) }
+ var showMap by remember { mutableStateOf(false) }
// X = cumulative meters or seconds, Y = speed (km/h)
val dataPointsMeters = remember { mutableListOf() }
@@ -202,11 +223,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
}
CompactStat(
label = "DISTANCE",
- value = when {
- totalDist <= 0 -> "—"
- totalDist >= 1000 -> String.format("%.2f km", totalDist / 1000.0)
- else -> String.format("%.0f m", totalDist)
- },
+ value = if (totalDist <= 0) "—" else UnitFormatter.formatDistance(totalDist, useMetric),
textMuted = TrackProTheme.colors.textMuted,
valueColor = TrackProTheme.colors.textPrimary
)
@@ -218,9 +235,9 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
- DragMetricCard(DragMetricDisplay("TOP SPEED", if (maxSpeed > 0) String.format("%.0f", maxSpeed) else "—", "KM/H", maxSpeed > 0), modifier = Modifier.weight(1f))
- DragMetricCard(DragMetricDisplay("AVG SPEED", if (avgSpeed > 0) String.format("%.0f", avgSpeed) else "—", "KM/H", avgSpeed > 0), modifier = Modifier.weight(1f))
- DragMetricCard(DragMetricDisplay("MAX ACCEL", if (maxAcceleration > 0) String.format("%.1f", maxAcceleration) else "—", "KM/H/S", maxAcceleration > 0), modifier = Modifier.weight(1f))
+ DragMetricCard(DragMetricDisplay("TOP SPEED", if (maxSpeed > 0) UnitFormatter.formatSpeed(maxSpeed, useMetric) else "—", UnitFormatter.speedUnitLabel(useMetric), maxSpeed > 0), modifier = Modifier.weight(1f))
+ DragMetricCard(DragMetricDisplay("AVG SPEED", if (avgSpeed > 0) UnitFormatter.formatSpeed(avgSpeed, useMetric) else "—", UnitFormatter.speedUnitLabel(useMetric), avgSpeed > 0), modifier = Modifier.weight(1f))
+ DragMetricCard(DragMetricDisplay("MAX ACCEL", if (maxAcceleration > 0) String.format("%.1f", UnitFormatter.convertSpeed(maxAcceleration, useMetric)) else "—", "${UnitFormatter.speedUnitLabel(useMetric)}/S", maxAcceleration > 0), modifier = Modifier.weight(1f))
}
Row(
@@ -244,7 +261,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
DragMetricCard(DragMetricDisplay("100-200", formatMetric(metrics.time100to200), "SEC", metrics.time100to200 != null), modifier = Modifier.weight(1f))
DragMetricCard(DragMetricDisplay("¼ MILE", formatMetric(metrics.quarterMileTime), "SEC", metrics.quarterMileTime != null), modifier = Modifier.weight(1f))
- DragMetricCard(DragMetricDisplay("TRAP SPD", metrics.quarterMileSpeed?.toInt()?.toString() ?: "—", "KM/H", metrics.quarterMileSpeed != null), modifier = Modifier.weight(1f))
+ DragMetricCard(DragMetricDisplay("TRAP SPD", metrics.quarterMileSpeed?.let { UnitFormatter.formatSpeed(it, useMetric) } ?: "—", UnitFormatter.speedUnitLabel(useMetric), metrics.quarterMileSpeed != null), modifier = Modifier.weight(1f))
}
}
@@ -260,7 +277,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = "SPEED TRACE",
+ text = if (showMap) "GPS TRACE" else "SPEED TRACE",
color = TrackProTheme.colors.textMuted,
fontSize = 9.sp,
letterSpacing = 2.sp,
@@ -270,15 +287,36 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
- listOf(true to "METER", false to "SEC").forEach { (isMeters, label) ->
- val active = xAxisInMeters == isMeters
+ if (!showMap) {
+ listOf(true to "METER", false to "SEC").forEach { (isMeters, label) ->
+ val active = xAxisInMeters == isMeters
+ Box(
+ modifier = Modifier
+ .background(
+ if (active) TrackProTheme.colors.accentCyan else TrackProTheme.colors.sectorLine,
+ RoundedCornerShape(3.dp)
+ )
+ .clickable { xAxisInMeters = isMeters }
+ .padding(horizontal = 10.dp, vertical = 3.dp)
+ ) {
+ Text(
+ text = label,
+ color = if (active) Color.Black else TrackProTheme.colors.textMuted,
+ fontSize = 9.sp,
+ fontWeight = FontWeight.Black,
+ )
+ }
+ }
+ }
+ listOf(false to "CHART", true to "MAP").forEach { (isMap, label) ->
+ val active = showMap == isMap
Box(
modifier = Modifier
.background(
- if (active) TrackProTheme.colors.accentCyan else TrackProTheme.colors.sectorLine,
- androidx.compose.foundation.shape.RoundedCornerShape(3.dp)
+ if (active) TrackProTheme.colors.accentAmber else TrackProTheme.colors.sectorLine,
+ RoundedCornerShape(3.dp)
)
- .clickable { xAxisInMeters = isMeters }
+ .clickable { showMap = isMap }
.padding(horizontal = 10.dp, vertical = 3.dp)
) {
Text(
@@ -299,43 +337,159 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
.fillMaxSize()
.background(TrackProTheme.colors.bgCard)
) {
- AndroidView(
- factory = { ctx ->
- LineChart(ctx).apply {
- layoutParams = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT
+ if (showMap) {
+ if (coordinates.isNotEmpty()) {
+ DragSessionMapView(gpsData = coordinates, modifier = Modifier.fillMaxSize())
+ } else {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Text(
+ "NO GPS DATA",
+ color = TrackProTheme.colors.textMuted,
+ fontSize = 12.sp,
+ letterSpacing = 2.sp
)
- setupChartStyle()
- }
- },
- update = { chart ->
- val dataSet = LineDataSet(dataPoints, "Speed").apply {
- setDrawValues(false)
- setDrawCircles(false)
- lineWidth = 2f
- color = android.graphics.Color.parseColor("#E8001C")
- setDrawFilled(true)
- fillColor = android.graphics.Color.parseColor("#E8001C")
- fillAlpha = 40
}
- chart.data = LineData(dataSet)
- chart.xAxis.valueFormatter = object : ValueFormatter() {
- override fun getFormattedValue(value: Float): String {
- return if (xAxisInMeters) "${value.toInt()}m" else "${value.toInt()}s"
+ }
+ } else {
+ AndroidView(
+ factory = { ctx ->
+ LineChart(ctx).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ setupChartStyle()
}
- }
- chart.notifyDataSetChanged()
- chart.invalidate()
- },
- modifier = Modifier
- .fillMaxSize()
- .padding(8.dp)
- )
+ },
+ update = { chart ->
+ val convertedPoints = dataPoints.map { Entry(it.x, UnitFormatter.convertSpeed(it.y, useMetric)) }
+ val dataSet = LineDataSet(convertedPoints, "Speed").apply {
+ setDrawValues(false)
+ setDrawCircles(false)
+ lineWidth = 2f
+ color = android.graphics.Color.parseColor("#E8001C")
+ setDrawFilled(true)
+ fillColor = android.graphics.Color.parseColor("#E8001C")
+ fillAlpha = 40
+ }
+ chart.data = LineData(dataSet)
+ chart.xAxis.valueFormatter = object : ValueFormatter() {
+ override fun getFormattedValue(value: Float): String {
+ return if (xAxisInMeters) "${value.toInt()}m" else "${value.toInt()}s"
+ }
+ }
+ chart.notifyDataSetChanged()
+ chart.invalidate()
+ },
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(8.dp)
+ )
+ }
}
}
}
+@Composable
+fun DragSessionMapView(
+ gpsData: List,
+ modifier: Modifier = Modifier
+) {
+ val mapRef = remember { mutableStateOf(null) }
+ val styleRef = remember { mutableStateOf