Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
829a61e
feat: Adds an AI Navigator demo
dkhawk May 29, 2025
baafc70
feat: Enhance AI Navigator with messages and prompt examples
dkhawk May 29, 2025
f407271
feat: Improved AiNavigator UI and controls
dkhawk May 29, 2025
b6efac9
feat: enable prompt generation in AI Navigator
dkhawk May 29, 2025
c1913cf
feat: Update AiNavigator example
dkhawk May 29, 2025
39f5497
feat: Updated app launcher icon
dkhawk May 29, 2025
6024611
feat: Implement "What am I looking at?" feature
dkhawk May 29, 2025
3da6763
feat: update AiNavigator sample to use Gemini API for multimodal prompts
dkhawk May 30, 2025
99f8b7e
feat: add compass to AI Navigator
dkhawk May 30, 2025
e6aacd9
feat: Improve AI Navigator functionality and UI
dkhawk May 30, 2025
b60c479
feat: enhance and integrate WhiskeyCompass
dkhawk May 30, 2025
987f5de
feat: enhance and integrate WhiskeyCompass
dkhawk May 30, 2025
4a8002d
feat: ensure WhiskeyCompass respects safe drawing insets
dkhawk May 30, 2025
b7b69ff
feat: added fade animation for compass in AiNavigator
dkhawk May 30, 2025
09e2b22
Refactor: Improve coroutine scope management in AiNavigatorActivity
dkhawk May 30, 2025
c466e92
feat(ainavigator): Add marker and polyline support
dkhawk May 30, 2025
fc47ad3
Refactor: Improve AI navigator polyline handling
dkhawk May 30, 2025
4640c52
feat: Add `addPolygon` command for AI Navigator
dkhawk May 30, 2025
9047a0c
feat: enhance AI Navigator prompts and guidelines
dkhawk May 30, 2025
7756930
feat: Add Boston scenario to advanced maps sample
dkhawk Jun 2, 2025
4344498
feat: enhance ScenariosActivity with immersive mode and compass
dkhawk Jun 3, 2025
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
5 changes: 4 additions & 1 deletion Maps3DSamples/advanced/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android)
alias(libs.plugins.secrets.gradle.plugin)
id("com.google.gms.google-services")
}

android {
Expand Down Expand Up @@ -48,7 +49,6 @@ android {
}

dependencies {

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
Expand Down Expand Up @@ -78,6 +78,9 @@ dependencies {
implementation(libs.maps.utils.ktx)

implementation(libs.androidx.material.icons.extended)

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.ai)
}

secrets {
Expand Down
6 changes: 6 additions & 0 deletions Maps3DSamples/advanced/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
android:name=".scenarios.ScenariosActivity"
android:exported="true"
/>

<activity
android:name=".ainavigator.AiNavigatorActivity"
android:exported="true"
/>

</application>

</manifest>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.advancedmaps3dsamples.ainavigator.AiNavigatorActivity
import com.example.advancedmaps3dsamples.scenarios.ScenariosActivity
import com.example.advancedmaps3dsamples.ui.theme.AdvancedMaps3DSamplesTheme
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -48,6 +49,7 @@ data class MapSample(@StringRes val label: Int, val clazz: Class<*>)
private val samples =
listOf(
MapSample(R.string.map_sample_scenarios, ScenariosActivity::class.java),
MapSample(R.string.map_sample_ai_navigator, AiNavigatorActivity::class.java),
)

@OptIn(ExperimentalMaterial3Api::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import android.app.Application
import android.content.pm.PackageManager
import android.util.Log
import android.widget.Toast
import com.google.firebase.FirebaseApp
import dagger.hilt.android.HiltAndroidApp
import java.util.Objects

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package com.example.advancedmaps3dsamples.ainavigator

import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import android.util.Log
import androidx.compose.ui.graphics.ImageBitmap
import androidx.lifecycle.viewModelScope
import com.example.advancedmaps3dsamples.ainavigator.data.NavigatorService
import com.example.advancedmaps3dsamples.ainavigator.data.examplePrompts
import com.example.advancedmaps3dsamples.common.Map3dViewModel
import com.example.advancedmaps3dsamples.scenarios.AnimationStep
import com.example.advancedmaps3dsamples.scenarios.ScenarioBaseViewModel
import com.example.advancedmaps3dsamples.scenarios.toAnimation
import com.example.advancedmaps3dsamples.utils.toCameraString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.resume

@HiltViewModel
class AiNavigatorViewModel @Inject constructor(
private val navigatorService: NavigatorService
) : Map3dViewModel(), ScenarioBaseViewModel {
override val TAG = this::class.java.simpleName
private var animationJob: Job? = null
private var currentAnimation: List<AnimationStep> = emptyList()
private var isAnimating = false

private val _isRequestInflight = MutableStateFlow(false)
val isRequestInflight: StateFlow<Boolean> = _isRequestInflight

private val _userMessage = Channel<String>()
val userMessage = _userMessage.receiveAsFlow()

val allPrompts = examplePrompts.toMutableList()

fun processUserRequest(userInput: String, cameraString: String) {
viewModelScope.launch {
_isRequestInflight.value = true
try {
val animationString = navigatorService.getAnimationString(userInput, cameraString)
Log.w(TAG, "Got animationString: $animationString")

currentAnimation = animationString.toAnimation()
playAnimation()
} catch (e: Exception) {
Log.e(TAG, "Error processing user request", e)
_userMessage.send("Error processing request: ${e.localizedMessage}")
} finally {
_isRequestInflight.value = false
}
}
}

fun playAnimation() {
animationJob?.cancel()
isAnimating = true

animationJob = viewModelScope.launch {
for (step in currentAnimation) {
step(this@AiNavigatorViewModel)
}
isAnimating = false
}
}

fun stopAnimation() {
if (isAnimating && animationJob?.isActive == true) {
animationJob?.cancel()
isAnimating = false
}
}

fun restartAnimation() {
stopAnimation()
if (currentAnimation.isNotEmpty()) {
playAnimation()
}
}

fun cancelRequest() {
stopAnimation()
_isRequestInflight.value = false
}

override suspend fun showMessage(message: String) {
_userMessage.send(message)
}

fun generateNewPrompts() {
viewModelScope.launch {
_isRequestInflight.value = true
try {
val newPrompts = navigatorService.getNewPrompts()
Log.w(TAG, "Got new prompts: $newPrompts")
if (newPrompts.isNotEmpty()) {
allPrompts.clear()
allPrompts.addAll(newPrompts)
}
} catch (e: Exception) {
Log.e(TAG, "Error generating new prompts", e)
_userMessage.send("Error generating new prompts: ${e.localizedMessage}")
}
_isRequestInflight.value = false
}
}

fun getRandomPrompt(): String = allPrompts.random()

fun whatAmILookingAt(bitmap: ImageBitmap) {
val cameraString = currentCamera.value.toCameraString()
Log.w(TAG, "What am I looking at? cameraString: $cameraString")

viewModelScope.launch {
_isRequestInflight.value = true
try {
val whatAmILookingAt = navigatorService.whatAmILookingAt(cameraString, bitmap)
Log.w(TAG, "Got whatAmILookingAt: $whatAmILookingAt")
if (whatAmILookingAt.isNotEmpty()) {
_userMessage.send(whatAmILookingAt)
}
} catch (e: Exception) {
Log.e(TAG, "Error getting whatAmILookingAt", e)
_userMessage.send("Error getting whatAmILookingAt: ${e.localizedMessage}")
}
_isRequestInflight.value = false
}
}

fun clearMapObjects() {
viewModelScope.launch {
// Stop any ongoing animation that might be adding objects
stopAnimation()
// Call the clearObjects method from the parent Map3dViewModel
// This removes objects from the map and clears internal tracking lists.
super.clearObjects()
// Optionally, send a message to the user
_userMessage.send("Map objects cleared.")
}
}
}

suspend fun Bitmap.saveToDisk(context: Context): Uri {
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"screenshot-${System.currentTimeMillis()}.png"
)

file.writeBitmap(this, Bitmap.CompressFormat.PNG, 100)

return scanFilePath(context, file.path) ?: throw Exception("File could not be saved")
}

private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
outputStream().use { out ->
bitmap.compress(format, quality, out)
out.flush()
}
}

private suspend fun scanFilePath(context: Context, filePath: String): Uri? {
return suspendCancellableCoroutine { continuation ->
MediaScannerConnection.scanFile(
context,
arrayOf(filePath),
arrayOf("image/png")
) { _, scannedUri ->
if (scannedUri == null) {
continuation.cancel(Exception("File $filePath could not be scanned"))
} else {
continuation.resume(scannedUri)
}
}
}
}

private fun shareImage(uri: Uri, context: Context) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "image/jpeg"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(intent, "Share Image"))
}
Loading
Loading