diff --git a/android/MapLibreUI/build.gradle b/android/MapLibreUI/build.gradle index fe4a739a..57881ded 100644 --- a/android/MapLibreUI/build.gradle +++ b/android/MapLibreUI/build.gradle @@ -45,6 +45,9 @@ dependencies { implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2' + + implementation 'org.ramani-maps:ramani-maplibre:0.2.0' implementation project(':core') diff --git a/android/MapLibreUI/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt b/android/MapLibreUI/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt new file mode 100644 index 00000000..0213d5b3 --- /dev/null +++ b/android/MapLibreUI/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt @@ -0,0 +1,32 @@ +package com.stadiamaps.ferrostar.maplibreui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import com.mapbox.mapboxsdk.geometry.LatLng +import com.stadiamaps.ferrostar.core.NavigationViewModel +import org.ramani.compose.Circle +import org.ramani.compose.MapLibre +import org.ramani.compose.Polyline + +@Composable +fun NavigationMapView( + viewModel: NavigationViewModel +) { + val uiState = viewModel.uiState.collectAsState() + + MapLibre(modifier = Modifier.fillMaxSize()) { + Circle( + center = LatLng( + uiState.value.snappedLocation.coordinates.lat, + uiState.value.snappedLocation.coordinates.lng + ), radius = 10f, color = "Blue" + ) + Polyline( + points = uiState.value.routeGeometry.map { LatLng(it.lat, it.lng) }, + color = "Red", + lineWidth = 5f + ) + } +} diff --git a/android/build.gradle b/android/build.gradle index f61c255b..86e3ecec 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.1.2' apply false - id 'com.android.library' version '8.1.2' apply false + id 'com.android.application' version '8.2.0' apply false + id 'com.android.library' version '8.2.0' apply false id 'org.jetbrains.kotlin.android' version '1.7.20' apply false id 'com.github.willir.rust.cargo-ndk-android' version '0.3.4' apply false } \ No newline at end of file diff --git a/android/compose-ui/build.gradle b/android/compose-ui/build.gradle new file mode 100644 index 00000000..fe4a739a --- /dev/null +++ b/android/compose-ui/build.gradle @@ -0,0 +1,59 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.stadiamaps.ferrostar.maplibreui' + compileSdk 33 + + defaultConfig { + minSdk 29 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.3.2' + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.9.0' + implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.activity:activity-compose:1.7.2' + implementation platform('androidx.compose:compose-bom:2022.10.00') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-graphics' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3' + + implementation project(':core') + + testImplementation 'junit:junit:4.13.2' + + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') + + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' +} \ No newline at end of file diff --git a/android/compose-ui/consumer-rules.pro b/android/compose-ui/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/android/compose-ui/proguard-rules.pro b/android/compose-ui/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/android/compose-ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/compose-ui/src/main/AndroidManifest.xml b/android/compose-ui/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/android/compose-ui/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/MapLibreUI/src/main/java/com/stadiamaps/ferrostar/maplibreui/BannerView.kt b/android/compose-ui/src/main/java/com/stadiamaps/ferrostar/compose/BannerView.kt similarity index 97% rename from android/MapLibreUI/src/main/java/com/stadiamaps/ferrostar/maplibreui/BannerView.kt rename to android/compose-ui/src/main/java/com/stadiamaps/ferrostar/compose/BannerView.kt index a06d92df..bc6a7294 100644 --- a/android/MapLibreUI/src/main/java/com/stadiamaps/ferrostar/maplibreui/BannerView.kt +++ b/android/compose-ui/src/main/java/com/stadiamaps/ferrostar/compose/BannerView.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.maplibreui +package com.stadiamaps.ferrostar.compose import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -83,7 +82,7 @@ fun BannerView(instructions: VisualInstruction, distanceToNextManeuver: Double?) } } -@Preview() +@Preview @Composable fun PreviewBannerView() { val instructions = VisualInstruction( diff --git a/android/compose-ui/src/test/java/com/stadiamaps/ferrostar/compose/ExampleUnitTest.kt b/android/compose-ui/src/test/java/com/stadiamaps/ferrostar/compose/ExampleUnitTest.kt new file mode 100644 index 00000000..4251dc78 --- /dev/null +++ b/android/compose-ui/src/test/java/com/stadiamaps/ferrostar/compose/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.stadiamaps.ferrostar.compose + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/android/core/build.gradle b/android/core/build.gradle index 28767fb4..9a8a39ca 100644 --- a/android/core/build.gradle +++ b/android/core/build.gradle @@ -8,12 +8,12 @@ plugins { android { namespace 'com.stadiamaps.ferrostar.core' - compileSdk 33 + compileSdk 34 ndkVersion "25.2.9519653" defaultConfig { minSdk 29 - targetSdk 33 + targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index 86bc8299..b6b5eb73 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import uniffi.ferrostar.Disposable +import uniffi.ferrostar.GeographicCoordinate import uniffi.ferrostar.NavigationControllerInterface import uniffi.ferrostar.TripState import uniffi.ferrostar.UserLocation @@ -13,7 +14,8 @@ import java.util.concurrent.Executors data class NavigationUiState( val snappedLocation: UserLocation, - val heading: Float? + val heading: Float?, + val routeGeometry: List ) /** @@ -29,12 +31,20 @@ class NavigationViewModel( private val navigationController: NavigationControllerInterface, private val locationProvider: LocationProvider, initialUserLocation: Location, + routeGeometry: List, ) : ViewModel(), LocationUpdateListener { // TODO: Is this the best executor? private val _executor = Executors.newSingleThreadExecutor() private var _state = navigationController.getInitialState(initialUserLocation.userLocation()) - // TODO: UI state flow? - private val _uiState = MutableStateFlow(NavigationUiState(snappedLocation = initialUserLocation.userLocation(), heading = null)) + + private val _uiState = MutableStateFlow( + NavigationUiState( + snappedLocation = initialUserLocation.userLocation(), + // TODO: Heading/course over ground + heading = null, + routeGeometry = routeGeometry + ) + ) val uiState: StateFlow = _uiState.asStateFlow() init { @@ -51,7 +61,12 @@ class NavigationViewModel( } override fun onLocationUpdated(location: Location) { - update(newState = navigationController.updateUserLocation(location = location.userLocation(), state = _state), location = location) + update( + newState = navigationController.updateUserLocation( + location = location.userLocation(), + state = _state + ), location = location + ) } override fun onHeadingUpdated(heading: Float) { diff --git a/android/demo-app/build.gradle b/android/demo-app/build.gradle index b70325fe..8c6d410e 100644 --- a/android/demo-app/build.gradle +++ b/android/demo-app/build.gradle @@ -57,6 +57,7 @@ dependencies { implementation 'androidx.compose.material3:material3' implementation project(':core') + implementation project(':MapLibreUI') implementation platform("com.squareup.okhttp3:okhttp-bom:4.10.0") implementation 'com.squareup.okhttp3:okhttp' diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt index 7a1d5be8..5866279c 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt @@ -6,30 +6,73 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import com.stadiamaps.ferrostar.core.FerrostarCore +import com.stadiamaps.ferrostar.core.NavigationViewModel +import com.stadiamaps.ferrostar.core.SimulatedLocation import com.stadiamaps.ferrostar.core.SimulatedLocationProvider +import com.stadiamaps.ferrostar.maplibreui.NavigationMapView import com.stadiamaps.ferrostar.ui.theme.FerrostarTheme import okhttp3.OkHttpClient +import uniffi.ferrostar.GeographicCoordinate +import uniffi.ferrostar.ManeuverModifier +import uniffi.ferrostar.ManeuverType +import uniffi.ferrostar.NavigationController +import uniffi.ferrostar.NavigationControllerConfig +import uniffi.ferrostar.Route +import uniffi.ferrostar.RouteStep +import uniffi.ferrostar.StepAdvanceMode +import uniffi.ferrostar.VisualInstruction +import uniffi.ferrostar.VisualInstructionContent import java.net.URL +import java.time.Instant class MainActivity : ComponentActivity() { - // TODO: Create a view model instead - val locationProvider = SimulatedLocationProvider() - val httpClient = OkHttpClient.Builder().build() + // TODO: Move to some sort of test fixtures + private val geom = listOf( + GeographicCoordinate(-122.41970699999999, 37.807770999999995), + GeographicCoordinate(-122.42041599999999, 37.807680999999995), + GeographicCoordinate(-122.42040399999999, 37.807623), + GeographicCoordinate(-122.420678, 37.807587), + ) + // Maybe the core should create the view model and expose it via a property... + private val simulatedLocation = + SimulatedLocation(geom.first(), 6.0, null, Instant.now()) + private val locationProvider = SimulatedLocationProvider() + private val httpClient = OkHttpClient.Builder().build() + // TODO: Something useful. This is just a placeholder that essentially checks our ability to load the Rust library val core = FerrostarCore( - valhallaEndpointURL = URL("https://api.stadiamaps.com/navigate/v1?api_key=YOUR-KEY-HERE"), + valhallaEndpointURL = URL("https://api.stadiamaps.com/route/v1?api_key=YOUR-KEY-HERE"), profile = "pedestrian", httpClient = httpClient ) + private val route = Route( + geometry = geom, + distance = 100.0, + steps = listOf( + RouteStep( + geom, 100.0, "Jefferson St.", "Walk west on Jefferson St.", listOf( + VisualInstruction( + VisualInstructionContent( + "Hyde Street", + ManeuverType.TURN, + ManeuverModifier.LEFT, + null + ), + null, + 42.0 + ) + ), + listOf() + ) + ), + waypoints = listOf(geom.first(), geom.last()) + ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContent { FerrostarTheme { // A surface container using the 'background' color from the theme @@ -37,26 +80,25 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - Greeting("Android") + NavigationMapView( + // TODO: This constructor pattern is pretty whack. It's also probably the wrong way to create the ViewModel. + viewModel = NavigationViewModel( + NavigationController( + route = route, + config = NavigationControllerConfig( + StepAdvanceMode.RelativeLineStringDistance( + 40U, + 15U + ) + ) + ), + locationProvider, + simulatedLocation, + geom + ) + ) } } } } } - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name! 🦀 says 2 + 2 = 4", - modifier = modifier, - textAlign = TextAlign.Center, - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - FerrostarTheme { - Greeting("Android") - } -} \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 8c63f280..f42067c0 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Apr 27 20:24:21 KST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/settings.gradle b/android/settings.gradle index c756bfe3..4d4fd94e 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -14,5 +14,6 @@ dependencyResolutionManagement { } rootProject.name = "Ferrostar" include ':core' +include ':compose-ui' include ':demo-app' include ':MapLibreUI'