diff --git a/app/src/main/java/edu/rpi/shuttletracker/data/models/Vehicle.kt b/app/src/main/java/edu/rpi/shuttletracker/data/models/Vehicle.kt index e9dc9d3..bd6d2c7 100644 --- a/app/src/main/java/edu/rpi/shuttletracker/data/models/Vehicle.kt +++ b/app/src/main/java/edu/rpi/shuttletracker/data/models/Vehicle.kt @@ -20,6 +20,7 @@ data class Vehicle( val longitude: Double, val speedMph: Double, val timestamp: String, + val headingDegrees: Int?, // from velocities val routeName: String?, val isAtStop: Boolean?, @@ -70,6 +71,7 @@ data class VehicleLocation( val longitude: Double, @SerializedName("speed_mph") val speedMph: Double, val timestamp: String, + @SerializedName("heading_degrees") val headingDegrees: Int?, ) data class VehicleStopEta( @@ -101,6 +103,7 @@ object VehicleMerger { longitude = location.longitude, speedMph = location.speedMph, timestamp = location.timestamp, + headingDegrees = location.headingDegrees, routeName = velocity?.routeName, isAtStop = velocity?.isAtStop, currentStop = velocity?.currentStop, diff --git a/app/src/main/java/edu/rpi/shuttletracker/data/repositories/UserPreferencesRepository.kt b/app/src/main/java/edu/rpi/shuttletracker/data/repositories/UserPreferencesRepository.kt index 092a770..671300a 100644 --- a/app/src/main/java/edu/rpi/shuttletracker/data/repositories/UserPreferencesRepository.kt +++ b/app/src/main/java/edu/rpi/shuttletracker/data/repositories/UserPreferencesRepository.kt @@ -45,6 +45,8 @@ class UserPreferencesRepository private val DEV_OPTIONS_ACTIVE = booleanPreferencesKey("dev_options_active") private val THEME_MODE = stringPreferencesKey("theme_mode") private val MAP_TYPE = stringPreferencesKey("map_type") + private val SHUTTLE_ANIMATIONS = booleanPreferencesKey("shuttle-animations") + private val SHUTTLE_ROTATION = booleanPreferencesKey("shuttle_rotation") } suspend fun getUserId(): String = @@ -198,4 +200,27 @@ class UserPreferencesRepository } } } + + fun getShuttleAnimations(): Flow = + dataStore.data.map { + it[SHUTTLE_ANIMATIONS] + ?: false + } + + suspend fun saveShuttleAnimations(animationsEnable: Boolean) { + dataStore.edit { + it[SHUTTLE_ANIMATIONS] = animationsEnable + } + } + + fun getShuttleRotation(): Flow = + dataStore.data.map { + it[SHUTTLE_ROTATION] ?: true + } + + suspend fun saveShuttleRotations(rotationsEnable: Boolean) { + dataStore.edit { + it[SHUTTLE_ROTATION] = rotationsEnable + } + } } diff --git a/app/src/main/java/edu/rpi/shuttletracker/ui/maps/MapsScreen.kt b/app/src/main/java/edu/rpi/shuttletracker/ui/maps/MapsScreen.kt index 6040d19..b8058a9 100644 --- a/app/src/main/java/edu/rpi/shuttletracker/ui/maps/MapsScreen.kt +++ b/app/src/main/java/edu/rpi/shuttletracker/ui/maps/MapsScreen.kt @@ -3,6 +3,8 @@ package edu.rpi.shuttletracker.ui.maps import android.Manifest import android.content.pm.PackageManager import android.location.Location +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -206,6 +208,8 @@ private fun ShuttleMap( mapsUiState.vehicles.forEach { vehicle -> VehicleMarker( vehicle = vehicle, + animationsEnabled = mapsUiState.shuttleAnimationsEnabled, + rotationEnabled = mapsUiState.shuttleRotationEnabled, ) } @@ -310,13 +314,47 @@ private fun StopMarker( } @Composable -private fun VehicleMarker(vehicle: Vehicle) { +private fun VehicleMarker( + vehicle: Vehicle, + animationsEnabled: Boolean, + rotationEnabled: Boolean, +) { val context = LocalContext.current - val markerState = rememberUpdatedMarkerState(position = vehicle.latLng()) + val target = vehicle.latLng() + val heading = vehicle.headingDegrees?.toFloat() ?: 0f + + val lat = remember { Animatable(target.latitude.toFloat()) } + val lng = remember { Animatable(target.longitude.toFloat()) } + + val markerState = + rememberUpdatedMarkerState( + position = LatLng(lat.value.toDouble(), lng.value.toDouble()), + ) + + // Animate movement + LaunchedEffect(target, animationsEnabled) { + if (animationsEnabled) { + launch { + lat.animateTo( + target.latitude.toFloat(), + animationSpec = tween(durationMillis = 2000), + ) + } + launch { + lng.animateTo( + target.longitude.toFloat(), + animationSpec = tween(durationMillis = 2000), + ) + } + } else { + lat.snapTo(target.latitude.toFloat()) + lng.snapTo(target.longitude.toFloat()) + } + } - // every time the vehicle changes, update the position of the marker - LaunchedEffect(vehicle) { - markerState.position = vehicle.latLng() + // Update marker position when animation values change + LaunchedEffect(lat.value, lng.value) { + markerState.position = LatLng(lat.value.toDouble(), lng.value.toDouble()) } val resolvedColor = @@ -346,7 +384,6 @@ private fun VehicleMarker(vehicle: Vehicle) { getVehicleMarkerDescriptor(context, 25f, finalColor.toArgb()) } - // gets vehicle speed and last time it updated val timeAgoFlow = remember(vehicle.timestamp) { vehicle.getTimeAgo() } val lastUpdatedAgoText = timeAgoFlow.collectAsStateWithLifecycle(initialValue = "").value @@ -367,6 +404,8 @@ private fun VehicleMarker(vehicle: Vehicle) { snippet = snippetText, anchor = Offset(0.5f, 0.5f), zIndex = 3f, + rotation = if (rotationEnabled) heading else 0f, + flat = rotationEnabled, onClick = { it.showInfoWindow() true diff --git a/app/src/main/java/edu/rpi/shuttletracker/ui/maps/MapsViewModel.kt b/app/src/main/java/edu/rpi/shuttletracker/ui/maps/MapsViewModel.kt index add1f15..cd9de82 100644 --- a/app/src/main/java/edu/rpi/shuttletracker/ui/maps/MapsViewModel.kt +++ b/app/src/main/java/edu/rpi/shuttletracker/ui/maps/MapsViewModel.kt @@ -30,7 +30,7 @@ import javax.inject.Inject @HiltViewModel class MapsViewModel - // represents the ui state of the view +// represents the ui state of the view @Inject constructor( private val apiRepository: ApiRepository, @@ -135,6 +135,24 @@ class MapsViewModel it.copy(mapType = mapType) } }.launchIn(viewModelScope) + + userPreferencesRepository + .getShuttleAnimations() + .flowOn(Dispatchers.Default) + .onEach { animationsEnable -> + _mapsUiState.update { + it.copy(shuttleAnimationsEnabled = animationsEnable) + } + }.launchIn(viewModelScope) + + userPreferencesRepository + .getShuttleRotation() + .flowOn(Dispatchers.Default) + .onEach { rotationEnable -> + _mapsUiState.update { + it.copy(shuttleRotationEnabled = rotationEnable) + } + }.launchIn(viewModelScope) } fun updateMapType(mapType: MapType) { @@ -157,6 +175,21 @@ class MapsViewModel updateMapType(next) } + fun setShuttleAnimations(animationsEnable: Boolean) { + viewModelScope.launch { + userPreferencesRepository.saveShuttleAnimations(animationsEnable) + } + } + + fun setShuttleRotation(rotationEnable: Boolean) { + viewModelScope.launch { + userPreferencesRepository.saveShuttleRotations(rotationEnable) + } + } + + /** + * Reads the network response and maps it to correct place + * */ private fun readApiResponse( response: NetworkResponse, success: (body: T) -> Unit, @@ -193,4 +226,6 @@ data class MapsUiState( val totalAnnouncements: Int = -1, val themeMode: ThemeMode = ThemeMode.System, val mapType: MapType = MapType.NORMAL, + val shuttleAnimationsEnabled: Boolean = false, + val shuttleRotationEnabled: Boolean = true, ) diff --git a/app/src/main/java/edu/rpi/shuttletracker/ui/settings/SettingsScreen.kt b/app/src/main/java/edu/rpi/shuttletracker/ui/settings/SettingsScreen.kt index 91d484b..2466d46 100644 --- a/app/src/main/java/edu/rpi/shuttletracker/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/edu/rpi/shuttletracker/ui/settings/SettingsScreen.kt @@ -12,6 +12,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Contrast +import androidx.compose.material.icons.outlined.DirectionsBus +import androidx.compose.material.icons.outlined.Explore import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.RestartAlt import androidx.compose.material.icons.outlined.Settings @@ -24,6 +26,7 @@ import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -101,6 +104,32 @@ fun SettingsScreen( ) } + item { + SettingsItem( + icon = Icons.Outlined.Explore, + title = stringResource(R.string.rotation), + description = stringResource(R.string.rotation_description), + ) { + Switch( + checked = settingsUiState.rotationEnabled, + onCheckedChange = { viewModel.updateShuttleRotation(it) }, + ) + } + } + + item { + SettingsItem( + icon = Icons.Outlined.DirectionsBus, + title = stringResource(R.string.animation), + description = stringResource(R.string.animation_description), + ) { + Switch( + checked = settingsUiState.animationsEnabled, + onCheckedChange = { viewModel.updateShuttleAnimations(it) }, + ) + } + } + item { SettingsItem( Icons.Outlined.RestartAlt, diff --git a/app/src/main/java/edu/rpi/shuttletracker/ui/settings/SettingsViewModel.kt b/app/src/main/java/edu/rpi/shuttletracker/ui/settings/SettingsViewModel.kt index 8ecdd12..972b1c4 100644 --- a/app/src/main/java/edu/rpi/shuttletracker/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/edu/rpi/shuttletracker/ui/settings/SettingsViewModel.kt @@ -23,11 +23,15 @@ class SettingsViewModel userPreferencesRepository.getColorBlindMode(), userPreferencesRepository.getDevOptions(), userPreferencesRepository.getThemeMode(), - ) { colorBindMode, devOptionState, themeMode -> + userPreferencesRepository.getShuttleAnimations(), + userPreferencesRepository.getShuttleRotation(), + ) { colorBindMode, devOptionState, themeMode, animationsEnabled, rotationEnabled -> return@combine SettingsUiState( colorBlindMode = colorBindMode, devOptionState = devOptionState, themeMode = themeMode, + animationsEnabled = animationsEnabled, + rotationEnabled = rotationEnabled, ) }.stateIn( scope = viewModelScope, @@ -47,6 +51,18 @@ class SettingsViewModel } } + fun updateShuttleAnimations(enabled: Boolean) { + viewModelScope.launch { + userPreferencesRepository.saveShuttleAnimations(enabled) + } + } + + fun updateShuttleRotation(enabled: Boolean) { + viewModelScope.launch { + userPreferencesRepository.saveShuttleRotations(enabled) + } + } + fun clearAllPreferences() = viewModelScope.launch { userPreferencesRepository.clearAllPreferences() @@ -58,4 +74,6 @@ data class SettingsUiState( val colorBlindMode: Boolean = false, val devOptionState: Boolean = false, val themeMode: ThemeMode = ThemeMode.System, + val animationsEnabled: Boolean = false, + val rotationEnabled: Boolean = true, ) diff --git a/app/src/main/java/edu/rpi/shuttletracker/ui/settings/developerMenu/DevMenuScreen.kt b/app/src/main/java/edu/rpi/shuttletracker/ui/settings/developerMenu/DevMenuScreen.kt index dcaf720..a3f29a6 100644 --- a/app/src/main/java/edu/rpi/shuttletracker/ui/settings/developerMenu/DevMenuScreen.kt +++ b/app/src/main/java/edu/rpi/shuttletracker/ui/settings/developerMenu/DevMenuScreen.kt @@ -87,11 +87,6 @@ fun DevMenuScreen( }) } -// MinStopDistItem( -// maxStopDist = devMenuUiState.maxStopDist, -// updateMaxStopDist = viewModel::updateMinStopDist, -// ) - BaseUrlSettingItem( currentUrl = devMenuUiState.baseUrl, updateBaseUrl = viewModel::updateBaseUrl, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e788175..6a8f0c9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -141,6 +141,10 @@ App Theme Redo Setup Open app settings + Shuttle Rotation + Show vehicle direction + Shuttle Animation + Smooth movement Analytics