Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Colinanderson/play kml tour compose #287

Merged
merged 32 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3ea40b5
Tour can now play
colinanderson Nov 25, 2024
6164dd2
Use flows for status and progress
colinanderson Nov 26, 2024
044dff0
Implement reset, renames etc.
colinanderson Nov 27, 2024
b2853bc
Layout and use the flow to detrmine play/pause button
colinanderson Nov 27, 2024
25ac699
Add icons
colinanderson Nov 27, 2024
5ce36d3
Disable/enable reset button
colinanderson Nov 27, 2024
9e473c5
Formatting and don't need to use emit
colinanderson Nov 28, 2024
84e6e9c
Downloade activity
colinanderson Dec 2, 2024
21328e7
Add downlaod activity to manifest
colinanderson Dec 2, 2024
51d5f0d
Use provisioning location
colinanderson Dec 2, 2024
36c0505
Add downloader to metadata
colinanderson Dec 2, 2024
120b781
Readme and metadata
colinanderson Dec 2, 2024
7c5eda4
Change basemap and add comments
colinanderson Dec 3, 2024
d4de4e8
Screenshot
colinanderson Dec 3, 2024
e9423e0
Clean up
colinanderson Dec 4, 2024
187c004
Replace old implementation with new compose implementation
colinanderson Dec 4, 2024
01c26d2
Add the new files
colinanderson Dec 4, 2024
05c43fd
Manifest
colinanderson Dec 4, 2024
2da0096
Merge branch 'v.next' into colinanderson/play_kml_tour_compose
colinanderson Dec 4, 2024
13b6dff
Update readme and metadata
colinanderson Dec 4, 2024
d49c3bf
Update the metadata to hopefully fix PR check
colinanderson Dec 4, 2024
11e52d9
Scene doesn't need to be mutable state
colinanderson Dec 5, 2024
c4cc7ef
Code review updates
colinanderson Dec 10, 2024
c7a27b3
Fix copyright
colinanderson Dec 10, 2024
779898d
Rename from KML to Kml
colinanderson Dec 11, 2024
6b67e9f
Renames
colinanderson Dec 11, 2024
f4b72f5
Disable play/pause button if the tour is not initialized
colinanderson Dec 11, 2024
6cfa664
Renames
colinanderson Dec 11, 2024
065290a
Don't need remember or initial values
colinanderson Dec 11, 2024
173f135
Add end of file
colinanderson Dec 11, 2024
8aacd89
Organize imports
colinanderson Dec 11, 2024
108e401
Be more Kotlin
colinanderson Dec 11, 2024
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
4 changes: 3 additions & 1 deletion samples/play-kml-tour/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ This sample uses a custom tour from [ArcGIS Online](https://arcgisruntime.maps.a

See [Touring in KML](https://developers.google.com/kml/documentation/touring) in *Keyhole Markup Language* for more information.

This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable SceneView.

## Tags

animation, interactive, KML, narration, pause, play, story, tour
animation, geoviewcompose, interactive, KML, narration, pause, play, story, toolkit, tour
8 changes: 6 additions & 2 deletions samples/play-kml-tour/README.metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
"keywords": [
"KML",
"animation",
"geoviewcompose",
"interactive",
"narration",
"pause",
"play",
"story",
"toolkit",
"tour",
"KmlTour",
"KmlTourController",
Expand All @@ -31,8 +33,10 @@
"KmlTourController.reset()"
],
"snippets": [
"src/main/java/com/esri/arcgismaps/sample/playkmltour/components/PlayKMLTourViewModel.kt",
"src/main/java/com/esri/arcgismaps/sample/playkmltour/DownloadActivity.kt",
"src/main/java/com/esri/arcgismaps/sample/playkmltour/MainActivity.kt",
"src/main/java/com/esri/arcgismaps/sample/playkmltour/DownloadActivity.kt"
"src/main/java/com/esri/arcgismaps/sample/playkmltour/screens/PlayKMLTourScreen.kt"
],
"title": "Play KML tour"
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd habitually add a newline to the end of the file, though I'm not sure it actually matters here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

3 changes: 1 addition & 2 deletions samples/play-kml-tour/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.arcgismaps.android.library)
alias(libs.plugins.arcgismaps.android.library.compose)
alias(libs.plugins.arcgismaps.kotlin.sample)
alias(libs.plugins.gradle.secrets)
}
Expand All @@ -11,9 +12,7 @@ secrets {

android {
namespace = "com.esri.arcgismaps.sample.playkmltour"
// For view based samples
buildFeatures {
dataBinding = true
buildConfig = true
}
}
Expand Down
Binary file modified samples/play-kml-tour/play-kml-tour.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 4 additions & 8 deletions samples/play-kml-tour/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@
<uses-permission android:name="android.permission.INTERNET" />

<application><activity
android:name=".DownloadActivity"
android:exported="true"
android:label="@string/play_kml_tour_app_name">
android:name=".DownloadActivity"
android:exported="true"
android:label="@string/play_kml_tour_app_name">


<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</activity>
<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/* Copyright 2024 Esri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.esri.arcgismaps.sample.playkmltour

import android.content.Intent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Copyright 2023 Esri
/* Copyright 2024 Esri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,233 +17,37 @@
package com.esri.arcgismaps.sample.playkmltour

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import com.arcgismaps.ApiKey
import com.arcgismaps.ArcGISEnvironment
import com.arcgismaps.mapping.ArcGISScene
import com.arcgismaps.mapping.ArcGISTiledElevationSource
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Surface
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.ViewpointType
import com.arcgismaps.mapping.kml.KmlContainer
import com.arcgismaps.mapping.kml.KmlDataset
import com.arcgismaps.mapping.kml.KmlNode
import com.arcgismaps.mapping.kml.KmlTour
import com.arcgismaps.mapping.kml.KmlTourController
import com.arcgismaps.mapping.kml.KmlTourStatus
import com.arcgismaps.mapping.layers.KmlLayer
import com.esri.arcgismaps.sample.playkmltour.databinding.PlayKmlTourActivityMainBinding
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import kotlin.math.roundToInt
import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
import com.esri.arcgismaps.sample.playkmltour.screens.PlayKMLTourScreen


class MainActivity : AppCompatActivity() {

private val provisionPath: String by lazy {
getExternalFilesDir(null)?.path.toString() + File.separator + getString(R.string.play_kml_tour_app_name)
}

// set up data binding for the activity
private val activityMainBinding: PlayKmlTourActivityMainBinding by lazy {
DataBindingUtil.setContentView(this, R.layout.play_kml_tour_activity_main)
}

private val sceneView by lazy {
activityMainBinding.sceneView
}

private val playPauseButton by lazy {
activityMainBinding.playPauseButton
}

private val resetTourButton by lazy {
activityMainBinding.resetTourButton
}

private val tourStatusTV by lazy {
activityMainBinding.tourStatusTV
}

private val tourProgressBar by lazy {
activityMainBinding.tourProgressBar
}

private var initialViewpoint: Viewpoint? = null

private val kmlTourController = KmlTourController()
class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// authentication with an API key or named user is
// required to access basemaps and other location services
ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
lifecycle.addObserver(sceneView)


// add elevation data
val surface = Surface().apply {
elevationSources.add(ArcGISTiledElevationSource(getString(R.string.world_terrain_service)))
}

// create a scene and set the surface
sceneView.scene = ArcGISScene(BasemapStyle.ArcGISImagery).apply {
baseSurface = surface
}

// add a KML layer from a KML dataset with a KML tour
val kmlDataset = KmlDataset(provisionPath + getString(R.string.kml_tour_path))
val kmlLayer = KmlLayer(kmlDataset)

// add the layer to the scene view's operational layers
sceneView.scene?.operationalLayers?.add(kmlLayer)

// load the KML layer
lifecycleScope.launch {
kmlLayer.load().onFailure {
showError(it.message.toString())
}.onSuccess {
// get the first loaded KML tour
val kmlTour = findFirstKMLTour(kmlDataset.rootNodes)
if (kmlTour == null) {
showError("Cannot find KML tour in dataset")
return@onSuccess
}

// collect changes in KML tour status
collectKmlTourStatus(kmlTour)

// set the KML tour to the controller
kmlTourController.tour = kmlTour
}
}

resetTourButton.setOnClickListener {
// set tour to the initial viewpoint
initialViewpoint?.let { sceneView.setViewpoint(it) }
// reset tour controller
kmlTourController.reset()
}

playPauseButton.setOnClickListener {
// button was clicked when tour was playing
if (kmlTourController.tour?.status?.value == KmlTourStatus.Playing)
// pause KML tour
kmlTourController.pause()
else
// play KML tour
kmlTourController.play()
}
}

/**
* Recursively searches for the first KML tour in a list of [kmlNodes].
* Returns the first [KmlTour], or null if there are no tours.
*/
private fun findFirstKMLTour(kmlNodes: List<KmlNode>): KmlTour? {
kmlNodes.forEach { node ->
if (node is KmlTour)
return node
else if (node is KmlContainer)
return findFirstKMLTour(node.childNodes)
}
return null
}

/**
* Collects KmlTourStatus events from the [kmlTour] and then calls
* showKmlTourStatus()
*/
private fun collectKmlTourStatus(kmlTour: KmlTour) = lifecycleScope.launch {
kmlTour.status.collect { kmlTourStatus ->
when (kmlTourStatus) {
KmlTourStatus.Completed -> {
showKmlTourStatus("Completed", isResetEnabled = false, isPlayingTour = false)
}
KmlTourStatus.Initialized -> {
showKmlTourStatus("Initialized", isResetEnabled = false, isPlayingTour = false)
}
KmlTourStatus.Paused -> {
showKmlTourStatus("Paused", isResetEnabled = true, isPlayingTour = false)
}
KmlTourStatus.Playing -> {
showKmlTourStatus("Playing", isResetEnabled = true, isPlayingTour = true)
// set the tour's initial viewpoint
if (initialViewpoint == null) {
initialViewpoint = sceneView.getCurrentViewpoint(
ViewpointType.BoundingGeometry
)
}
}
else -> {}
setContent {
SampleAppTheme {
PlayKMLTourApp()
}
}
}

/**
* Displays the KML tour status using the [kmlTourStatus], display [resetTourButton]
* if [isResetEnabled] and set [playPauseButton] based on [isPlayingTour].
*/
private fun showKmlTourStatus(
kmlTourStatus: String,
isResetEnabled: Boolean,
isPlayingTour: Boolean
) {
// set the KML tour status
tourStatusTV.text = String.format("Tour status: %s", kmlTourStatus)

// enable the buttons
resetTourButton.isEnabled = isResetEnabled
playPauseButton.isEnabled = true

// show pause button if true
if (isPlayingTour) {
playPauseButton.apply {
// set button icon
icon = AppCompatResources.getDrawable(
this@MainActivity,
R.drawable.ic_round_pause_24
)
// set button text
text = getText(R.string.pause)
}
} else { // show play button if false
playPauseButton.apply {
// set button icon
icon = AppCompatResources.getDrawable(
this@MainActivity,
R.drawable.ic_round_play_arrow_24
)
// set button text
text = getString(R.string.play)
}
}

// get progress of tour every second
lifecycleScope.launch {
// run as long as KML tour status is "Playing"
while (kmlTourStatus == "Playing") {
// get percentage of current position over total duration
val tourProgressInt = ((kmlTourController.currentPosition.value * 100.0)
/ (kmlTourController.totalDuration.value)).roundToInt()
tourProgressBar.progress = tourProgressInt
// set a second delay
delay(1000)
}
@Composable
private fun PlayKMLTourApp() {
Surface(color = MaterialTheme.colorScheme.background) {
PlayKMLTourScreen(
sampleName = getString(R.string.play_kml_tour_app_name)
)
}

}

private fun showError(message: String) {
Log.e(localClassName, message)
Snackbar.make(sceneView, message, Snackbar.LENGTH_SHORT).show()
}
}
Loading
Loading