Skip to content

Commit

Permalink
Merge pull request #40 from MohamedRejeb/0.2.x
Browse files Browse the repository at this point in the history
Add geo support
  • Loading branch information
MohamedRejeb authored Dec 2, 2023
2 parents ea83c0f + 2492c2a commit 92f71bf
Show file tree
Hide file tree
Showing 17 changed files with 359 additions and 17 deletions.
4 changes: 2 additions & 2 deletions calf-file-picker/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.androidLibrary)
// id("module.publication")
id("module.publication")
}

kotlin {
Expand Down Expand Up @@ -47,7 +47,7 @@ kotlin {
}

android {
namespace = "com.mohamedrejeb.calf.sf.symbols"
namespace = "com.mohamedrejeb.calf.picker"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
Expand Down
49 changes: 49 additions & 0 deletions calf-geo/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.androidLibrary)
// id("module.publication")
}

kotlin {
kotlin.applyDefaultHierarchyTemplate()
androidTarget {
publishLibraryVariants("release")
compilations.all {
kotlinOptions {
jvmTarget = "11"
}
}
}
jvm("desktop") {
jvmToolchain(11)
}
js(IR) {
browser()
}
iosX64()
iosArm64()
iosSimulatorArm64()

sourceSets.commonMain.get().dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
}
sourceSets.androidMain.get().dependencies {
implementation(libs.appcompat)
implementation(libs.lifecycle.extensions)
implementation(libs.play.services.location)
}
}

android {
namespace = "com.mohamedrejeb.calf.geo"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.mohamedrejeb.calf.geo

import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

actual class LocationTracker(
interval: Long = 1000,
priority: Int = LocationRequest.PRIORITY_HIGH_ACCURACY
) : LocationCallback() {
private var locationProviderClient: FusedLocationProviderClient? = null
private var isStarted: Boolean = false
private val locationRequest = LocationRequest().also {
it.interval = interval
it.priority = priority
}
private val locationsChannel = Channel<LatLng>(Channel.CONFLATED)
private val extendedLocationsChannel = Channel<ExtendedLocation>(Channel.CONFLATED)
private val trackerScope = CoroutineScope(Dispatchers.Main)

fun bind(lifecycle: Lifecycle, context: Context, fragmentManager: FragmentManager) {
locationProviderClient = LocationServices.getFusedLocationProviderClient(context)

@SuppressLint("MissingPermission")
if (isStarted) {
locationProviderClient?.requestLocationUpdates(locationRequest, this, null)
}

lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
locationProviderClient?.removeLocationUpdates(this@LocationTracker)
locationProviderClient = null
}
})
}

override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult)

val lastLocation = locationResult.lastLocation ?: return

val speedAccuracy = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) null
else lastLocation.speedAccuracyMetersPerSecond.toDouble()

val bearingAccuracy = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) null
else lastLocation.bearingAccuracyDegrees.toDouble()

val verticalAccuracy = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) null
else lastLocation.verticalAccuracyMeters.toDouble()

val latLng = LatLng(
lastLocation.latitude,
lastLocation.longitude
)

val locationPoint = Location(
coordinates = latLng,
coordinatesAccuracyMeters = lastLocation.accuracy.toDouble()
)

val speed = Speed(
speedMps = lastLocation.speed.toDouble(),
speedAccuracyMps = speedAccuracy
)

val azimuth = Azimuth(
azimuthDegrees = lastLocation.bearing.toDouble(),
azimuthAccuracyDegrees = bearingAccuracy
)

val altitude = Altitude(
altitudeMeters = lastLocation.altitude,
altitudeAccuracyMeters = verticalAccuracy
)

val extendedLocation = ExtendedLocation(
location = locationPoint,
azimuth = azimuth,
speed = speed,
altitude = altitude,
timestampMs = lastLocation.time
)

trackerScope.launch {
extendedLocationsChannel.send(extendedLocation)
locationsChannel.send(latLng)
}
}

@SuppressLint("MissingPermission")
actual suspend fun startTracking() {
// if permissions request failed - execution stops here

isStarted = true
locationProviderClient?.requestLocationUpdates(locationRequest, this, null)
}

actual fun stopTracking() {
isStarted = false
locationProviderClient?.removeLocationUpdates(this)
}

private val _locationState: MutableState<LatLng> = mutableStateOf(LatLng(0.0, 0.0))
actual val locationState: State<LatLng> = _locationState

private val _extendedLocationState = mutableStateOf(
ExtendedLocation(
location = Location(
coordinates = LatLng(0.0, 0.0),
coordinatesAccuracyMeters = 0.0
),
azimuth = Azimuth(
azimuthDegrees = 0.0,
azimuthAccuracyDegrees = null
),
speed = Speed(
speedMps = 0.0,
speedAccuracyMps = null
),
altitude = Altitude(
altitudeMeters = 0.0,
altitudeAccuracyMeters = null
),
timestampMs = 0L
)
)
actual val extendedLocationState: State<ExtendedLocation> = _extendedLocationState

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mohamedrejeb.calf.geo

data class Altitude(
val altitudeMeters: Double,
val altitudeAccuracyMeters: Double?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mohamedrejeb.calf.geo

data class Azimuth(
val azimuthDegrees: Double,
val azimuthAccuracyDegrees: Double?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.mohamedrejeb.calf.geo

data class ExtendedLocation(
val location: Location,
val azimuth: Azimuth,
val speed: Speed,
val altitude: Altitude,
val timestampMs: Long
)
81 changes: 81 additions & 0 deletions calf-geo/src/commonMain/kotlin/com.mohamedrejeb.calf/geo/LatLng.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.mohamedrejeb.calf.geo

import kotlin.math.*

/**
* A class representing a pair of latitude and longitude coordinates, stored as degrees.
*
* @property latitude The latitude in degrees.
* @property longitude The longitude in degrees.
*/
data class LatLng(
val latitude: Double,
val longitude: Double
) {

/**
* Returns the distance to the given [LatLng] in kilometers.
*
* @param latLng The [LatLng] to calculate the distance to.
* @return The distance to the given [LatLng] in kilometers.
*/
fun distanceTo(latLng: LatLng): Double {
val lat1 = latitude
val lon1 = longitude
val lat2 = latLng.latitude
val lon2 = latLng.longitude

val r = EarthRadius
val dLat = toRadians(lat2 - lat1)
val dLon = toRadians(lon2 - lon1)
val a = sin(dLat / 2) * sin(dLat / 2) +
cos(toRadians(lat1)) * cos(toRadians(lat2)) * sin(dLon / 2) * sin(dLon / 2)
val c = 2 * asin(sqrt(a))
return r * c
}

/**
* Returns the angle to the given [LatLng] in degrees.
*
* @param latLng The [LatLng] to calculate the angle to.
* @param rounded Whether the angle should be rounded to the nearest 10 degrees.
* @return The angle to the given [LatLng] in degrees.
*/
fun getAngleTo(latLng: LatLng, rounded: Boolean = true): Double {
val lat1 = toRadians(this.latitude)
val lat2 = toRadians(latLng.latitude)
val lon1 = toRadians(this.longitude)
val lon2 = toRadians(latLng.longitude)

val dLon = lon2 - lon1

val y = sin(dLon) * cos(lat2)
val x = cos(lat1) * sin(lat2) -
(sin(lat1) * cos(lat2) * cos(dLon))

var brng = atan2(y, x)

brng = toDegree(brng)
brng = (brng + 360) % 360

var angle = brng
if (rounded) {
angle = (round(brng / 10) * 10)
}
if (angle == 360.0) angle = 0.0

return angle
}

private fun toRadians(angle: Double): Double {
return angle * PI / 180.0
}

private fun toDegree(angle: Double): Double {
return angle * 180.0 / PI
}

private companion object {
const val EarthRadius = 6371.00 // in Kilometers
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mohamedrejeb.calf.geo

/**
* A class representing the coordinates of a location, stored as a [LatLng] and an accuracy in meters.
*
* @property coordinates The coordinates of the location.
* @property coordinatesAccuracyMeters The accuracy of the coordinates in meters.
*/
data class Location(
val coordinates: LatLng,
val coordinatesAccuracyMeters: Double
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.mohamedrejeb.calf.geo

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State

expect class LocationTracker {
// val permissionsController: PermissionsController

suspend fun startTracking() // can be suspended for request permission
fun stopTracking()

val locationState: State<LatLng>

val extendedLocationState: State<ExtendedLocation>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mohamedrejeb.calf.geo

data class Speed(
val speedMps: Double,
val speedAccuracyMps: Double?
)
2 changes: 1 addition & 1 deletion calf-io/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
// id("module.publication")
id("module.publication")
}

kotlin {
Expand Down
8 changes: 4 additions & 4 deletions calf-maps/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ kotlin {
jvm("desktop") {
jvmToolchain(11)
}
// js(IR) {
// browser()
// }
js(IR) {
browser()
}
iosX64()
iosArm64()
iosSimulatorArm64()
Expand All @@ -36,7 +36,7 @@ kotlin {
}

android {
namespace = "com.mohamedrejeb.calf.sf.symbols"
namespace = "com.mohamedrejeb.calf.maps"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
Expand Down
Loading

0 comments on commit 92f71bf

Please sign in to comment.