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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ No programming experience is required, translations can be done directly in the

## Notes

- For larger changes, please open an Issue in github first to discuss.
- For larger changes, please open an Issue in GitHub first to discuss.
- All derivative work must remain open-source under GPLv3.

Thanks for helping to improve MBCompass!
2 changes: 0 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,8 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)


// Dagger Hilt
implementation(libs.hilt.android)
implementation(libs.androidx.coordinatorlayout)
ksp(libs.hilt.android.compiler)

// Android UI ViewBinding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import androidx.core.content.ContextCompat
Expand Down Expand Up @@ -190,7 +189,7 @@ object LocationHelper {



// Calculate distance between two points (Haversine formula)
// Calculate distance between two points
fun calculateDistance(previousLocation: Location?, location: Location): Float {
var distance = 0f
if (previousLocation != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class PermissionHandler(
private val context: Context
get() = fragment.requireContext()


fun requestLocationPermission(
launcher: ActivityResultLauncher<String>,
onGranted: () -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import kotlinx.coroutines.runBlocking

object AppPreferences {

private const val TAG = "AppPreferences"

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_preferences")

private lateinit var prefDataStore: DataStore<Preferences>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
Expand All @@ -38,10 +37,8 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
Expand All @@ -68,6 +65,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.location.LocationManagerCompat
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
Expand All @@ -76,10 +74,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mubarak.mbcompass.MainViewModel
import com.mubarak.mbcompass.R
import com.mubarak.mbcompass.core.sensors.AndroidSensorEventListener
import com.mubarak.mbcompass.core.sensors.SensorViewModel
import com.mubarak.mbcompass.core.location.AndroidLocationManager
import com.mubarak.mbcompass.core.location.TAG
import com.mubarak.mbcompass.core.sensors.AndroidSensorEventListener
import com.mubarak.mbcompass.core.sensors.SensorViewModel
import com.mubarak.mbcompass.features.settings.SettingsViewModel
import com.mubarak.mbcompass.utils.Azimuth
import com.mubarak.mbcompass.utils.CardinalDirection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,6 @@ class MapFragment : Fragment() {
currentBestLocation = LocationHelper.getLastKnownLocation(requireContext())
trackingState = AppPreferences.loadTrackingState()
permissionHandler = PermissionHandler(this)

Log.d(TAG, "onCreate - NO permission request on launch")
}

override fun onCreateView(
Expand Down Expand Up @@ -310,6 +308,7 @@ class MapFragment : Fragment() {
// don't show start track button on TrackFragment
val isViewOnlyMode = trackUriToDisplay != null
btnStart.isVisible = !isViewOnlyMode
locationButton.isVisible = !isViewOnlyMode

btnStart.setOnClickListener {
handleStartButton()
Expand Down Expand Up @@ -342,7 +341,9 @@ class MapFragment : Fragment() {
permissionHandler.requestLocationPermission(
launcher = locationPermissionLauncher,
onGranted = { onLocationPermissionGranted() },
onDenied = { /* User declined */ }
onDenied = {
Toast.makeText(requireContext(), R.string.location_permission_required, Toast.LENGTH_SHORT).show()
}
)
}
}
Expand All @@ -364,7 +365,7 @@ class MapFragment : Fragment() {
permissionHandler.requestLocationPermission(
launcher = locationPermissionLauncher,
onGranted = { startTracking(resume) },
onDenied = { /* Cancelled */ }
onDenied = { Toast.makeText(requireContext(), R.string.location_permission_title, Toast.LENGTH_SHORT).show() }
)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,19 @@ import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mubarak.mbcompass.R
import com.mubarak.mbcompass.features.settings.SettingsViewModel
import com.mubarak.mbcompass.ui.theme.MBCompassTheme
import com.mubarak.mbcompass.ui.theme.MBShapeDefaults.bottomListItemShape
import com.mubarak.mbcompass.ui.theme.MBShapeDefaults.middleListItemShape
import com.mubarak.mbcompass.ui.theme.MBShapeDefaults.singleListItemShape
import com.mubarak.mbcompass.ui.theme.MBShapeDefaults.topListItemShape
import com.mubarak.mbcompass.ui.theme.ThemeConfig
import com.mubarak.mbcompass.ui.theme.iconDefaultSize
import com.mubarak.mbcompass.ui.theme.spacingMedium
import com.mubarak.mbcompass.ui.theme.spacingSmall
import com.mubarak.mbcompass.utils.Const.APP_PAGE
import com.mubarak.mbcompass.utils.Const.AUTHOR_EMAIL
import com.mubarak.mbcompass.utils.Const.LICENSE_PAGE
import com.mubarak.mbcompass.utils.Const.SUPPORT_PAGE
import com.mubarak.mbcompass.ui.theme.ThemeConfig

@Composable
fun SettingsScreen(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class SettingsViewModel @Inject constructor(
}.catch {
Log.d("SettingsViewModel", "Error getting user preference", it)
emit(SettingsUiState())
}.stateIn(viewModelScope, SharingStarted.Companion.WhileSubscribed(5_000), SettingsUiState())
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SettingsUiState())

fun setTheme(theme: String) {
viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package com.mubarak.mbcompass.features.track

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mubarak.mbcompass.features.tracks.model.WayPoint
import kotlin.math.*

@Composable
fun ElevationChart(
waypoints: List<WayPoint>,
modifier: Modifier = Modifier,
useMetric: Boolean = true
) {
val validPoints = remember(waypoints) {
waypoints.filter { it.altitude != 0.0 }
}

if (validPoints.size < 2) {
Box(modifier = modifier, contentAlignment = Alignment.Center) {
Text(
text = "No elevation data",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
return
}

val elevations = remember(validPoints) {
validPoints.map { it.altitude.toFloat() }
}

val distances = remember(validPoints) {
var cum = 0f
val list = mutableListOf(0f)
for (i in 1 until validPoints.size) {
cum += calcDistance(validPoints[i - 1], validPoints[i])
list.add(cum)
}
list
}

val rawMin = elevations.min()
val rawMax = elevations.max()
val rawRange = (rawMax - rawMin).coerceAtLeast(1f)
val pad = rawRange * 0.15f
val minE = rawMin - pad
val maxE = rawMax + pad
val eRange = maxE - minE

val totalDist = distances.last().coerceAtLeast(1f)

val primaryColor = MaterialTheme.colorScheme.primary
val labelColor = MaterialTheme.colorScheme.onSurfaceVariant

val leftPadDp = 44.dp
val bottomPadDp = 20.dp
val topPadDp = 8.dp

BoxWithConstraints(modifier = modifier) {
val density = LocalDensity.current

val lp = with(density) { leftPadDp.toPx() }
val bp = with(density) { bottomPadDp.toPx() }
val tp = with(density) { topPadDp.toPx() }

val totalW = with(density) { maxWidth.toPx() }
val totalH = with(density) { maxHeight.toPx() }

val chartW = totalW - lp
val chartH = totalH - bp - tp

fun x(d: Float) = lp + (d / totalDist) * chartW
fun y(e: Float) = tp + chartH - ((e - minE) / eRange) * chartH

Canvas(modifier = Modifier.fillMaxSize()) {

val pts = elevations.indices.map { i ->
Offset(x(distances[i]), y(elevations[i]))
}

val splinePath = catmullRomToBezier(pts)

// fill path
val fillPath = Path().apply {
addPath(splinePath)
lineTo(pts.last().x, tp + chartH)
lineTo(pts.first().x, tp + chartH)
close()
}

// gradient fil
drawPath(
path = fillPath,
brush = Brush.verticalGradient(
colorStops = arrayOf(
0.0f to primaryColor.copy(alpha = 0.25f),
0.6f to primaryColor.copy(alpha = 0.10f),
1.0f to primaryColor.copy(alpha = 0.0f)
),
startY = tp,
endY = tp + chartH
)
)

drawPath(
path = splinePath,
color = primaryColor,
style = Stroke(
width = 2.5.dp.toPx(),
cap = StrokeCap.Round,
join = StrokeJoin.Round
)
)

// baseline
drawLine(
color = primaryColor.copy(alpha = 0.15f),
start = Offset(lp, tp + chartH),
end = Offset(totalW, tp + chartH),
strokeWidth = 1.dp.toPx()
)

listOf(pts.first(), pts.last()).forEach { p ->
drawCircle(color = primaryColor, radius = 3.dp.toPx(), center = p)
drawCircle(
color = Color.White,
radius = 1.5.dp.toPx(),
center = p
)
}
}

// Yaxis labels
fun formatAlt(v: Float) = if (useMetric) "%.0f m".format(v)
else "%.0f ft".format(v * 3.28084f)

Text(
text = formatAlt(rawMax),
fontSize = 9.sp,
color = labelColor,
modifier = Modifier
.align(Alignment.TopStart)
.padding(top = topPadDp, start = 2.dp)
)
Text(
text = formatAlt(rawMin),
fontSize = 9.sp,
color = labelColor,
modifier = Modifier
.align(Alignment.BottomStart)
.padding(bottom = bottomPadDp + 4.dp, start = 2.dp)
)

// Xaxis labels
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomStart)
.padding(start = leftPadDp),
horizontalArrangement = Arrangement.SpaceBetween
) {
val steps = 3
repeat(steps + 1) { i ->
val d = totalDist * i / steps
val label = if (useMetric) {
if (d < 1000f) "%.0f m".format(d)
else "%.1f km".format(d / 1000f)
} else {
"%.2f mi".format(d / 1609.34f)
}
Text(text = label, fontSize = 9.sp, color = labelColor)
}
}
}
}


// convert it to bezier https://cubic-bezier.com/ for smooth spine
private fun catmullRomToBezier(pts: List<Offset>): Path {
val path = Path()
if (pts.size < 2) return path

path.moveTo(pts[0].x, pts[0].y)

for (i in 0 until pts.size - 1) {
val p0 = if (i == 0) pts[0] else pts[i - 1]
val p1 = pts[i]
val p2 = pts[i + 1]
val p3 = if (i + 2 < pts.size) pts[i + 2] else pts[i + 1]

val cp1x = p1.x + (p2.x - p0.x) / 6f
val cp1y = p1.y + (p2.y - p0.y) / 6f
val cp2x = p2.x - (p3.x - p1.x) / 6f
val cp2y = p2.y - (p3.y - p1.y) / 6f

path.cubicTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y)
}

return path
}

// calculate distance using Haversine formula
private fun calcDistance(p1: WayPoint, p2: WayPoint): Float {
val R = 6_371_000f
val lat1 = Math.toRadians(p1.latitude)
val lat2 = Math.toRadians(p2.latitude)
val dLat = Math.toRadians(p2.latitude - p1.latitude)
val dLon = Math.toRadians(p2.longitude - p1.longitude)

val a = sin(dLat / 2).pow(2) + cos(lat1) * cos(lat2) * sin(dLon / 2).pow(2)
return (R * 2 * atan2(sqrt(a), sqrt(1 - a))).toFloat()
}
Loading