Skip to content

Commit 08868c6

Browse files
authored
feat: Overhaul Fly-Along with Nav Waypoints and Supersonic Pacing (#28)
* Add Routes API sample with Map3D integration * feat: overhaul fly-along with navigation waypoints and high-velocity pacing * Fix unresolved MAPS_API_KEY reference in RouteSampleActivity * Address PR reviews: update copyrights and release Map3DView
1 parent a84fb7c commit 08868c6

12 files changed

Lines changed: 820 additions & 0 deletions

File tree

Maps3DSamples/advanced/app/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ plugins {
8080
alias(libs.plugins.ksp)
8181
alias(libs.plugins.hilt.android)
8282
alias(libs.plugins.secrets.gradle.plugin)
83+
alias(libs.plugins.kotlinx.serialization)
8384
}
8485

8586
android {
@@ -154,6 +155,12 @@ dependencies {
154155
// Google Maps Utils for the polyline decoder
155156
implementation(libs.maps.utils.ktx)
156157

158+
implementation(libs.ktor.client.core)
159+
implementation(libs.ktor.client.cio)
160+
implementation(libs.ktor.client.content.negotiation)
161+
implementation(libs.ktor.serialization.kotlinx.json)
162+
implementation(libs.kotlinx.serialization.json)
163+
157164
implementation(libs.androidx.material.icons.extended)
158165
}
159166

Maps3DSamples/advanced/app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
android:name=".scenarios.ScenariosActivity"
5151
android:exported="true"
5252
/>
53+
<activity
54+
android:name=".scenarios.RouteSampleActivity"
55+
android:exported="true"
56+
/>
5357
</application>
5458

5559
</manifest>

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/MainActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import androidx.compose.runtime.Composable
3939
import androidx.compose.ui.Modifier
4040
import androidx.compose.ui.res.stringResource
4141
import androidx.compose.ui.unit.dp
42+
import com.example.advancedmaps3dsamples.scenarios.RouteSampleActivity
4243
import com.example.advancedmaps3dsamples.scenarios.ScenariosActivity
4344
import com.example.advancedmaps3dsamples.ui.theme.AdvancedMaps3DSamplesTheme
4445
import dagger.hilt.android.AndroidEntryPoint
@@ -48,6 +49,7 @@ data class MapSample(@StringRes val label: Int, val clazz: Class<*>)
4849
private val samples =
4950
listOf(
5051
MapSample(R.string.map_sample_scenarios, ScenariosActivity::class.java),
52+
MapSample(R.string.map_sample_route, RouteSampleActivity::class.java),
5153
)
5254

5355
@OptIn(ExperimentalMaterial3Api::class)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.example.advancedmaps3dsamples.common
16+
17+
import android.util.Log
18+
import io.ktor.client.HttpClient
19+
import io.ktor.client.call.body
20+
import io.ktor.client.engine.cio.CIO
21+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
22+
import io.ktor.client.request.header
23+
import io.ktor.client.request.post
24+
import io.ktor.client.request.setBody
25+
import io.ktor.client.statement.HttpResponse
26+
import io.ktor.client.statement.bodyAsText
27+
import io.ktor.http.ContentType
28+
import io.ktor.http.contentType
29+
import io.ktor.http.isSuccess
30+
import io.ktor.serialization.kotlinx.json.json
31+
import kotlinx.serialization.json.Json
32+
33+
/**
34+
* Exception thrown when the Routes API returns an error, such as a 403 Forbidden
35+
* if the API is not enabled for the provided key.
36+
*/
37+
class DirectionsErrorException(message: String) : Exception(message)
38+
39+
/**
40+
* A simple network service to fetch routes from the Google Maps Routes API.
41+
*
42+
* Note: In a production application, making direct API calls to Google Maps Platform
43+
* services from a client device requires embedding the API key in the app, which
44+
* poses a security risk. Best practice is to proxy these requests through a secure
45+
* backend server. This client implementation is provided for demonstration purposes.
46+
*/
47+
object RoutesApiService {
48+
49+
private val client = HttpClient(CIO) {
50+
install(ContentNegotiation) {
51+
json(Json {
52+
ignoreUnknownKeys = true
53+
encodeDefaults = true
54+
})
55+
}
56+
}
57+
58+
/**
59+
* Fetches a route between the origin and destination coordinates.
60+
*
61+
* @param apiKey The Google Maps API key (requires Routes API enabled).
62+
* @param originLat The latitude of the starting point.
63+
* @param originLng The longitude of the starting point.
64+
* @param destLat The latitude of the destination point.
65+
* @param destLng The longitude of the destination point.
66+
* @return [RoutesResponse] containing the computed route.
67+
* @throws [DirectionsErrorException] if the API returns a non-success HTTP status.
68+
*/
69+
suspend fun fetchRoute(
70+
apiKey: String,
71+
originLat: Double,
72+
originLng: Double,
73+
destLat: Double,
74+
destLng: Double
75+
): RoutesResponse {
76+
val requestBody = RoutesRequest(
77+
origin = Waypoint(Location(RequestLatLng(originLat, originLng))),
78+
destination = Waypoint(Location(RequestLatLng(destLat, destLng)))
79+
)
80+
81+
val response: HttpResponse = client.post("https://routes.googleapis.com/directions/v2:computeRoutes") {
82+
contentType(ContentType.Application.Json)
83+
header("X-Goog-Api-Key", apiKey)
84+
// Requesting only the most relevant fields to optimize payload size
85+
header("X-Goog-FieldMask", "routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline,routes.legs.steps.startLocation")
86+
setBody(requestBody)
87+
}
88+
89+
if (response.status.isSuccess()) {
90+
return response.body()
91+
} else {
92+
val errorBody = response.bodyAsText()
93+
Log.e("RoutesApiService", "Failed to fetch route: ${response.status.value}\n$errorBody")
94+
95+
// Provide a localized, user-friendly message based on typical API errors
96+
val userMsg = if (response.status.value == 403) {
97+
"API Error (HTTP 403). Ensure the Routes API is enabled in the Google Cloud Console for the provided API key."
98+
} else {
99+
"Failed to fetch route (HTTP ${response.status.value})."
100+
}
101+
throw DirectionsErrorException(userMsg)
102+
}
103+
}
104+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.example.advancedmaps3dsamples.common
16+
17+
import kotlinx.serialization.Serializable
18+
19+
@Serializable
20+
data class RoutesRequest(
21+
val origin: Waypoint,
22+
val destination: Waypoint,
23+
val travelMode: String = "DRIVE",
24+
val routingPreference: String = "TRAFFIC_AWARE",
25+
val computeAlternativeRoutes: Boolean = false,
26+
val routeModifiers: RouteModifiers = RouteModifiers(),
27+
val languageCode: String = "en-US",
28+
val units: String = "METRIC"
29+
)
30+
31+
@Serializable
32+
data class Waypoint(
33+
val location: Location
34+
)
35+
36+
@Serializable
37+
data class Location(
38+
val latLng: RequestLatLng
39+
)
40+
41+
@Serializable
42+
data class RequestLatLng(
43+
val latitude: Double,
44+
val longitude: Double
45+
)
46+
47+
@Serializable
48+
data class RouteModifiers(
49+
val avoidTolls: Boolean = false,
50+
val avoidHighways: Boolean = false,
51+
val avoidFerries: Boolean = false
52+
)
53+
54+
@Serializable
55+
data class RoutesResponse(
56+
val routes: List<Route> = emptyList()
57+
)
58+
59+
@Serializable
60+
data class Route(
61+
val distanceMeters: Int? = null,
62+
val duration: String? = null,
63+
val polyline: Polyline? = null,
64+
val legs: List<RouteLeg> = emptyList()
65+
)
66+
67+
@Serializable
68+
data class RouteLeg(
69+
val steps: List<RouteStep> = emptyList()
70+
)
71+
72+
@Serializable
73+
data class RouteStep(
74+
val startLocation: Location? = null
75+
)
76+
77+
@Serializable
78+
data class Polyline(
79+
val encodedPolyline: String
80+
)

0 commit comments

Comments
 (0)