diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 326c83466..21e5a4cbc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,6 +8,7 @@ on: branches: - main - v.next + - feature-branch/geo-compose # A workflow run is made up of one or more jobs that can run sequentially or # in parallel. diff --git a/add-dynamic-entity-layer/README.md b/add-dynamic-entity-layer/README.md index d2ae6259a..8cbfdc7f6 100644 --- a/add-dynamic-entity-layer/README.md +++ b/add-dynamic-entity-layer/README.md @@ -38,8 +38,9 @@ This sample uses a [stream service](https://realtimegis2016.esri.com:6443/arcgis ## Additional information +This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable MapView. More information about dynamic entities can be found in the [guide documentation](https://developers.arcgis.com/kotlin/real-time/work-with-dynamic-entities/). ## Tags -data, dynamic, entity, live, purge, real-time, service, stream, track +data, dynamic, entity, geoviewcompose, live, purge, real-time, service, stream, toolkit, track diff --git a/add-dynamic-entity-layer/README.metadata.json b/add-dynamic-entity-layer/README.metadata.json index fc7c41a10..286a0a468 100644 --- a/add-dynamic-entity-layer/README.metadata.json +++ b/add-dynamic-entity-layer/README.metadata.json @@ -10,11 +10,13 @@ "data", "dynamic", "entity", + "geoviewcompose", "live", "purge", "real-time", "service", "stream", + "toolkit", "track", "ArcGISStreamService", "DynamicEntity", @@ -36,7 +38,6 @@ "snippets": [ "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/MainActivity.kt", "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/BottomSheetContent.kt", - "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/ComposeMapView.kt", "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/MapViewModel.kt", "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/screens/MainScreen.kt" ], diff --git a/add-dynamic-entity-layer/build.gradle.kts b/add-dynamic-entity-layer/build.gradle.kts index 0da2047b5..edcdafaf3 100644 --- a/add-dynamic-entity-layer/build.gradle.kts +++ b/add-dynamic-entity-layer/build.gradle.kts @@ -48,4 +48,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) } diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/MainActivity.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/MainActivity.kt index b8444a445..602ca410f 100644 --- a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/MainActivity.kt +++ b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/MainActivity.kt @@ -47,8 +47,7 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { MainScreen( - sampleName = getString(R.string.app_name), - application = application + sampleName = getString(R.string.app_name) ) } } diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/BottomSheetContent.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/BottomSheetContent.kt index b5c32ac01..d61bcbf47 100644 --- a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/BottomSheetContent.kt +++ b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/BottomSheetContent.kt @@ -106,7 +106,6 @@ fun DynamicEntityLayerProperties( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Text( text = "Previous Observations", style = SampleTypography.bodyLarge diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/ComposeMapView.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/ComposeMapView.kt deleted file mode 100644 index 73efcabbd..000000000 --- a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/ComposeMapView.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* Copyright 2023 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.adddynamicentitylayer.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LifecycleOwner -import com.arcgismaps.mapping.view.MapView -import kotlinx.coroutines.launch - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun ComposeMapView( - modifier: Modifier = Modifier, - mapViewModel: MapViewModel -) { - // get an instance of the current lifecycle owner - val lifecycleOwner = LocalLifecycleOwner.current - // collect the latest state of the MapViewState - val mapViewState by mapViewModel.mapViewState.collectAsState() - // create and add MapView to the activity lifecycle - val mapView = createMapViewInstance(lifecycleOwner) - - // wrap the MapView as an AndroidView - AndroidView( - modifier = modifier, - factory = { mapView }, - // recomposes the MapView on changes in the MapViewState - update = { mapView -> - mapView.apply { - map = mapViewState.arcGISMap - setViewpoint(mapViewState.viewpoint) - } - } - ) - - // launch coroutine functions in the composition's CoroutineContext - LaunchedEffect(Unit) { - launch { - mapView.onSingleTapConfirmed.collect { - mapViewModel.dismissBottomSheet() - } - } - launch { - mapView.onPan.collect{ - mapViewModel.dismissBottomSheet() - } - } - } -} - -/** - * Create the MapView instance and add it to the Activity lifecycle - */ -@Composable -fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { - // create the MapView - val mapView = MapView(LocalContext.current) - // add the side effects for MapView composition - DisposableEffect(lifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(mapView) - onDispose { - lifecycleOwner.lifecycle.removeObserver(mapView) - } - } - return mapView -} diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/MapViewModel.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/MapViewModel.kt index f26268939..e37bcc7dc 100644 --- a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/MapViewModel.kt +++ b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/MapViewModel.kt @@ -17,6 +17,7 @@ package com.esri.arcgismaps.sample.adddynamicentitylayer.components import android.app.Application +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.AndroidViewModel import com.arcgismaps.mapping.ArcGISMap @@ -27,7 +28,6 @@ import com.arcgismaps.realtime.ArcGISStreamService import com.arcgismaps.realtime.ArcGISStreamServiceFilter import com.esri.arcgismaps.sample.adddynamicentitylayer.R import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch class MapViewModel( @@ -38,14 +38,11 @@ class MapViewModel( // set the state of the switches and slider val trackLineCheckedState = mutableStateOf(false) val prevObservationCheckedState = mutableStateOf(false) - val trackSliderValue = mutableStateOf(5f) + val trackSliderValue = mutableFloatStateOf(5f) // flag to show or dismiss the bottom sheet val isBottomSheetVisible = mutableStateOf(false) - // set the MapView mutable stateflow - val mapViewState = MutableStateFlow(MapViewState()) - // create ArcGIS Stream Service private val streamService = ArcGISStreamService(application.getString(R.string.stream_service_url)) @@ -56,6 +53,11 @@ class MapViewModel( // layer displaying the dynamic entities on the map private val dynamicEntityLayer: DynamicEntityLayer + // define ArcGIS map using Streets basemap + val map = ArcGISMap(BasemapStyle.ArcGISStreets).apply { + initialViewpoint = Viewpoint(40.559691, -111.869001, 150000.0) + } + /** * set the data source for the dynamic entity layer. */ @@ -70,7 +72,7 @@ class MapViewModel( dynamicEntityLayer = DynamicEntityLayer(streamService) // add the dynamic entity layer to the map's operational layers - mapViewState.value.arcGISMap.operationalLayers.add(dynamicEntityLayer) + map.operationalLayers.add(dynamicEntityLayer) } // disconnects the stream service @@ -125,10 +127,3 @@ class MapViewModel( } } -/** - * Data class that represents the MapView state - */ -data class MapViewState( - var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISStreets), - var viewpoint: Viewpoint = Viewpoint(40.559691, -111.869001, 150000.0) -) diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/screens/MainScreen.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/screens/MainScreen.kt index 307933ae3..e9046dd05 100644 --- a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/screens/MainScreen.kt +++ b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/screens/MainScreen.kt @@ -34,8 +34,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.esri.arcgismaps.sample.adddynamicentitylayer.components.ComposeMapView +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.adddynamicentitylayer.components.DynamicEntityLayerProperties import com.esri.arcgismaps.sample.adddynamicentitylayer.components.MapViewModel import com.esri.arcgismaps.sample.sampleslib.components.BottomSheet @@ -45,9 +46,11 @@ import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar * Main screen layout for the sample app */ @Composable -fun MainScreen(sampleName: String, application: Application) { +fun MainScreen(sampleName: String) { /// coroutineScope that will be cancelled when this call leaves the composition val sampleCoroutineScope = rememberCoroutineScope() + // get the application context + val application = LocalContext.current.applicationContext as Application // create a ViewModel to handle MapView interactions val mapViewModel = remember { MapViewModel(application, sampleCoroutineScope) } @@ -64,11 +67,13 @@ fun MainScreen(sampleName: String, application: Application) { .padding(it) ) { // composable function that wraps the MapView - ComposeMapView( + MapView( modifier = Modifier .fillMaxSize() .weight(1f), - mapViewModel = mapViewModel + arcGISMap = mapViewModel.map, + onSingleTapConfirmed = { mapViewModel.dismissBottomSheet() }, + onPan = { mapViewModel.dismissBottomSheet() } ) Row( modifier = Modifier diff --git a/analyze-hotspots/README.md b/analyze-hotspots/README.md index 316aeaafe..df7f298cc 100644 --- a/analyze-hotspots/README.md +++ b/analyze-hotspots/README.md @@ -28,6 +28,10 @@ Tap on Analyze, and select a date from the "FROM" DatePicker and "TO" DatePicker * GeoprocessingResult * GeoprocessingTask +## Additional information + +This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable MapView. + ## Tags -analysis, density, geoprocessing, hot spots, hotspots +analysis, density, geoprocessing, geoviewcompose, hot spots, hotspots, toolkit diff --git a/analyze-hotspots/README.metadata.json b/analyze-hotspots/README.metadata.json index 3fe5afb6d..8f2463927 100644 --- a/analyze-hotspots/README.metadata.json +++ b/analyze-hotspots/README.metadata.json @@ -10,8 +10,10 @@ "analysis", "density", "geoprocessing", + "geoviewcompose", "hot spots", "hotspots", + "toolkit", "GeoprocessingJob", "GeoprocessingParameters", "GeoprocessingResult", @@ -27,7 +29,6 @@ ], "snippets": [ "src/main/java/com/esri/arcgismaps/sample/analyzehotspots/MainActivity.kt", - "src/main/java/com/esri/arcgismaps/sample/analyzehotspots/components/ComposeMapView.kt", "src/main/java/com/esri/arcgismaps/sample/analyzehotspots/components/MapViewModel.kt", "src/main/java/com/esri/arcgismaps/sample/analyzehotspots/screens/BottomAppContent.kt", "src/main/java/com/esri/arcgismaps/sample/analyzehotspots/screens/BottomSheetScreen.kt", diff --git a/analyze-hotspots/build.gradle.kts b/analyze-hotspots/build.gradle.kts index 950db8994..6d6aa87c8 100644 --- a/analyze-hotspots/build.gradle.kts +++ b/analyze-hotspots/build.gradle.kts @@ -48,4 +48,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) } diff --git a/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/MainActivity.kt b/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/MainActivity.kt index 47ae750e1..b976e78e6 100644 --- a/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/MainActivity.kt +++ b/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/MainActivity.kt @@ -48,8 +48,7 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { MainScreen( - sampleName = getString(R.string.app_name), - application = application + sampleName = getString(R.string.app_name) ) } } diff --git a/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/components/ComposeMapView.kt b/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/components/ComposeMapView.kt deleted file mode 100644 index 029168867..000000000 --- a/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/components/ComposeMapView.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* Copyright 2023 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.analyzehotspots.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LifecycleOwner -import com.arcgismaps.mapping.view.MapView -import com.arcgismaps.mapping.view.SingleTapConfirmedEvent -import kotlinx.coroutines.launch - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun ComposeMapView( - modifier: Modifier = Modifier, - mapViewModel: MapViewModel, - onSingleTap: (SingleTapConfirmedEvent) -> Unit = {} -) { - // get an instance of the current lifecycle owner - val lifecycleOwner = LocalLifecycleOwner.current - // get an instance of the ViewModel's MapViewState - val mapViewState = mapViewModel.mapViewState - // create and add MapView to the activity lifecycle - val mapView = createMapViewInstance(lifecycleOwner) - - // wrap the MapView as an AndroidView - AndroidView( - modifier = modifier, - factory = { mapView }, - // recomposes the MapView on changes in the MapViewState - update = { mapView -> - mapView.apply { - map = mapViewState.arcGISMap - setViewpoint(mapViewState.viewpoint) - } - } - ) - - // launch coroutine functions in the composition's CoroutineContext - LaunchedEffect(Unit) { - launch { - mapView.onSingleTapConfirmed.collect { - onSingleTap(it) - } - } - } -} - -/** - * Create the MapView instance and add it to the Activity lifecycle - */ -@Composable -fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { - // create the MapView - val mapView = MapView(LocalContext.current) - // add the side effects for MapView composition - DisposableEffect(lifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(mapView) - onDispose { - lifecycleOwner.lifecycle.removeObserver(mapView) - } - } - return mapView -} diff --git a/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/components/MapViewModel.kt b/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/components/MapViewModel.kt index 87ce6afaa..1de877c3a 100644 --- a/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/components/MapViewModel.kt +++ b/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/components/MapViewModel.kt @@ -19,8 +19,8 @@ package com.esri.arcgismaps.sample.analyzehotspots.components import android.app.Application import android.util.Log import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.AndroidViewModel import com.arcgismaps.geometry.Point import com.arcgismaps.geometry.SpatialReference @@ -45,8 +45,8 @@ class MapViewModel( private val application: Application, private val sampleCoroutineScope: CoroutineScope, ) : AndroidViewModel(application) { - // set the MapView state - val mapViewState = MapViewState() + // create a map using the topographic basemap style + val map: ArcGISMap by mutableStateOf(ArcGISMap(BasemapStyle.ArcGISTopographic)) // create a ViewModel to handle dialog interactions val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel() @@ -55,11 +55,20 @@ class MapViewModel( val showJobProgressDialog = mutableStateOf(false) // determinate job progress percentage - val geoprocessingJobProgress = mutableStateOf(0) + val geoprocessingJobProgress = mutableIntStateOf(0) // job used to run the geoprocessing task on a service private var geoprocessingJob: GeoprocessingJob? = null + init { + map.apply { + // Set the map's initialViewpoint + initialViewpoint = Viewpoint( + center = Point(-13671170.0, 5693633.0, SpatialReference(wkid = 3857)), + scale = 1e5 + ) + } + } /** * Creates a [geoprocessingJob] with the default [GeoprocessingParameters] * and a custom query date range between [fromDate] & [toDate] @@ -69,7 +78,7 @@ class MapViewModel( toDate: String, ) { // a map image layer might be generated, clear previous results - mapViewState.arcGISMap.operationalLayers.clear() + map.operationalLayers.clear() // create and load geoprocessing task val geoprocessingTask = GeoprocessingTask(application.getString(R.string.service_url)) @@ -110,8 +119,8 @@ class MapViewModel( sampleCoroutineScope.launch { geoprocessingJob.progress.collect { progress -> // updates the job progress dialog - geoprocessingJobProgress.value = progress - Log.i("Progress", "geoprocessingJobProgress: ${geoprocessingJobProgress.value}") + geoprocessingJobProgress.intValue = progress + Log.i("Progress", "geoprocessingJobProgress: ${geoprocessingJobProgress.intValue}") } } // get the result of the job on completion @@ -128,7 +137,7 @@ class MapViewModel( } ?: return messageDialogVM.showMessageDialog("Result map image layer is null") // add new layer to map - mapViewState.arcGISMap.operationalLayers.add(hotspotMapImageLayer) + map.operationalLayers.add(hotspotMapImageLayer) }.onFailure { throwable -> messageDialogVM.showMessageDialog( title = throwable.message.toString(), @@ -155,14 +164,3 @@ class MapViewModel( return date.format(formatter) } } - -/** - * Class that represents the MapView's current state - */ -class MapViewState { - var arcGISMap: ArcGISMap by mutableStateOf(ArcGISMap(BasemapStyle.ArcGISTopographic)) - var viewpoint: Viewpoint = Viewpoint( - center = Point(-13671170.0, 5693633.0, SpatialReference(wkid = 3857)), - scale = 1e5 - ) -} diff --git a/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/screens/MainScreen.kt b/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/screens/MainScreen.kt index 0a4451bb5..77e09360e 100644 --- a/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/screens/MainScreen.kt +++ b/analyze-hotspots/src/main/java/com/esri/arcgismaps/sample/analyzehotspots/screens/MainScreen.kt @@ -25,7 +25,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import com.esri.arcgismaps.sample.analyzehotspots.components.ComposeMapView +import androidx.compose.ui.platform.LocalContext +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.analyzehotspots.components.MapViewModel import com.esri.arcgismaps.sample.sampleslib.components.JobLoadingDialog import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog @@ -36,21 +37,26 @@ import kotlinx.coroutines.launch * Main screen layout for the sample app */ @Composable -fun MainScreen(sampleName: String, application: Application) { +fun MainScreen(sampleName: String) { // coroutineScope that will be cancelled when this call leaves the composition val sampleCoroutineScope = rememberCoroutineScope() + // get the application property that will be used to construct MapViewModel + val sampleApplication = LocalContext.current.applicationContext as Application // create a ViewModel to handle MapView interactions - val mapViewModel = remember { MapViewModel(application, sampleCoroutineScope) } + val mapViewModel = remember { MapViewModel(sampleApplication, sampleCoroutineScope) } Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { // sample app content layout - Column(modifier = Modifier.fillMaxSize().padding(it)) { - // composable function that wraps the MapView - ComposeMapView( - modifier = Modifier.fillMaxSize().weight(1f), - mapViewModel = mapViewModel + Column(modifier = Modifier + .fillMaxSize() + .padding(it)) { + MapView( + modifier = Modifier + .fillMaxSize() + .weight(1f), + arcGISMap = mapViewModel.map ) // bottom layout with a button to display analyze hotspot options BottomAppContent( @@ -83,7 +89,7 @@ fun MainScreen(sampleName: String, application: Application) { if (mapViewModel.showJobProgressDialog.value) { JobLoadingDialog( title = "Analyzing hotspots...", - progress = mapViewModel.geoprocessingJobProgress.value, + progress = mapViewModel.geoprocessingJobProgress.intValue, cancelJobRequest = { mapViewModel.cancelGeoprocessingJob() } ) } diff --git a/display-composable-mapview/README.md b/display-composable-mapview/README.md index dfcebf4bf..650df0d44 100644 --- a/display-composable-mapview/README.md +++ b/display-composable-mapview/README.md @@ -19,7 +19,7 @@ Run the sample to view the map. Pan and zoom to navigate the map. 3. Set its `Modifier` to define the MapView layout parameters 4. Use its `factory` parameter to provide context and create `MapView(context)` 5. Add the `MapView` to the lifecycle observer -6. Add the composable content to the Activity using `setContent { }` +6. Add the composable content to the Activity using `setContent { }` ## Relevant API @@ -27,6 +27,10 @@ Run the sample to view the map. Pan and zoom to navigate the map. * BasemapStyle * MapView +## Additional information + +This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable MapView. + ## Tags -basemap, compose, jetpack, map +basemap, compose, geoviewcompose, jetpack, map, toolkit diff --git a/display-composable-mapview/README.metadata.json b/display-composable-mapview/README.metadata.json index 3082f0881..3c0815f18 100644 --- a/display-composable-mapview/README.metadata.json +++ b/display-composable-mapview/README.metadata.json @@ -9,8 +9,10 @@ "keywords": [ "basemap", "compose", + "geoviewcompose", "jetpack", "map", + "toolkit", "ArcGISMap", "BasemapStyle", "MapView" @@ -23,9 +25,7 @@ "MapView" ], "snippets": [ - "src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MainActivity.kt", - "src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MapViewWithCompose.kt", - "src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/theme/Theme.kt" + "src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MainActivity.kt" ], "title": "Display Composable MapView" } diff --git a/display-composable-mapview/build.gradle.kts b/display-composable-mapview/build.gradle.kts index b90f7f1b9..4a3921c15 100644 --- a/display-composable-mapview/build.gradle.kts +++ b/display-composable-mapview/build.gradle.kts @@ -48,4 +48,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) } diff --git a/display-composable-mapview/src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MainActivity.kt b/display-composable-mapview/src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MainActivity.kt index 4931af98c..26434a792 100644 --- a/display-composable-mapview/src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MainActivity.kt +++ b/display-composable-mapview/src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MainActivity.kt @@ -19,26 +19,20 @@ package com.esri.arcgismaps.sample.displaycomposablemapview import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.arcgismaps.ApiKey import com.arcgismaps.ArcGISEnvironment import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.BasemapStyle -import com.arcgismaps.mapping.Viewpoint +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme class MainActivity : ComponentActivity() { - private val viewpointAmerica = Viewpoint(39.8, -98.6, 10e7) - private val viewpointAsia = Viewpoint(39.8, 98.6, 10e7) - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is @@ -47,27 +41,12 @@ class MainActivity : ComponentActivity() { setContent { SampleAppTheme { - Column( + // create a map with a navigation night basemap style + val map = ArcGISMap(BasemapStyle.ArcGISNavigationNight) + MapView( modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // a mutable/immutable state is computed by remember to store its value during - // initial composition, and updates the composition on the state value change - var viewpoint by remember { mutableStateOf(viewpointAmerica) } - val map by remember { mutableStateOf(ArcGISMap(BasemapStyle.ArcGISNavigationNight)) } - - // Composable function that wraps the MapView - MapViewWithCompose( - arcGISMap = map, - viewpoint = viewpoint, - // lambda to retrieve the MapView's onSingleTapConfirmed - onSingleTap = { - // swap between America and Asia viewpoints - viewpoint = - if (viewpoint == viewpointAmerica) viewpointAsia else viewpointAmerica - } - ) - } + arcGISMap = map + ) } } } diff --git a/display-composable-mapview/src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MapViewWithCompose.kt b/display-composable-mapview/src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MapViewWithCompose.kt deleted file mode 100644 index af9ec0509..000000000 --- a/display-composable-mapview/src/main/java/com/esri/arcgismaps/sample/displaycomposablemapview/MapViewWithCompose.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.esri.arcgismaps.sample.displaycomposablemapview - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.lifecycleScope -import com.arcgismaps.geometry.Point -import com.arcgismaps.mapping.ArcGISMap -import com.arcgismaps.mapping.Viewpoint -import com.arcgismaps.mapping.view.MapView -import kotlinx.coroutines.launch - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun MapViewWithCompose( - arcGISMap: ArcGISMap, - viewpoint: Viewpoint, - onSingleTap: (mapPoint: Point?) -> Unit = {}, -) { - val lifecycleOwner = LocalLifecycleOwner.current - AndroidView( - // modifiers are used to set layout parameters - modifier = Modifier.fillMaxSize(), - // the factory parameter provides a context to create a classic Android view - // called when the composable is created, but not when it's recomposed - factory = { context -> - MapView(context).also { mapView -> - // add the MapView to the lifecycle observer - lifecycleOwner.lifecycle.addObserver(mapView) - // set the map - mapView.map = arcGISMap - // launch a coroutine to collect map taps - lifecycleOwner.lifecycleScope.launch { - mapView.onSingleTapConfirmed.collect { - onSingleTap(it.mapPoint) - } - } - } - }, - - // update block runs every time this view is recomposed which only occurs - // when a `State` or `MutableState` parameter is changed. - update = { view -> // view is automatically cast to a MapView - view.map = arcGISMap // called only if the arcGISMap parameter is changes - lifecycleOwner.lifecycleScope.launch { - view.setViewpointAnimated(viewpoint) // called only if the viewpoint parameter is changes - } - } - ) -} diff --git a/display-points-using-clustering-feature-reduction/README.md b/display-points-using-clustering-feature-reduction/README.md index e36314279..3613f01c7 100644 --- a/display-points-using-clustering-feature-reduction/README.md +++ b/display-points-using-clustering-feature-reduction/README.md @@ -3,7 +3,7 @@ Display a web map with a point feature layer that has feature reduction enabled to aggregate points into clusters. Map displaying the feature layer with feature reduction property enabled by default: -![Feature reduction map](display-points-using-clustering-feature-reduction.png) +![Feature reduction map](display-points-using-clustering-feature-reduction-main.png) Popup message displaying the cluster details: ![Cluster details popup](display-points-using-clustering-feature-reduction-popup.png) @@ -40,6 +40,10 @@ Pan and zoom the map to view how clustering is dynamically updated. Disable clus This sample uses a [web map](https://www.arcgis.com/home/item.html?id=8916d50c44c746c1aafae001552bad23) that displays the Esri [Global Power Plants](https://www.arcgis.com/home/item.html?id=eb54b44c65b846cca12914b87b315169) feature layer with feature reduction enabled. When enabled, the aggregate features symbology shows the color of the most common power plant type, and a size relative to the average plant capacity of the cluster. +## Additional information + +This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable MapView. + ## Tags -aggregate, bin, cluster, group, merge, normalize, reduce, summarize +aggregate, bin, cluster, geoviewcompose, group, merge, normalize, reduce, summarize, toolkit diff --git a/display-points-using-clustering-feature-reduction/README.metadata.json b/display-points-using-clustering-feature-reduction/README.metadata.json index 3a9cdd383..a86fcfe43 100644 --- a/display-points-using-clustering-feature-reduction/README.metadata.json +++ b/display-points-using-clustering-feature-reduction/README.metadata.json @@ -4,18 +4,20 @@ "formal_name": "DisplayPointsUsingClusteringFeatureReduction", "ignore": false, "images": [ - "display-points-using-clustering-feature-reduction.png", + "display-points-using-clustering-feature-reduction-main.png", "display-points-using-clustering-feature-reduction-popup.png" ], "keywords": [ "aggregate", "bin", "cluster", + "geoviewcompose", "group", "merge", "normalize", "reduce", "summarize", + "toolkit", "FeatureLayer", "FeatureReduction", "IdentifyLayerResult", @@ -40,7 +42,6 @@ "snippets": [ "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/MainActivity.kt", "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ClusterInfoContent.kt", - "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ComposeMapView.kt", "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/MapViewModel.kt", "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/screens/MainScreen.kt" ], diff --git a/display-points-using-clustering-feature-reduction/build.gradle.kts b/display-points-using-clustering-feature-reduction/build.gradle.kts index d64fa5339..f62df0bfe 100644 --- a/display-points-using-clustering-feature-reduction/build.gradle.kts +++ b/display-points-using-clustering-feature-reduction/build.gradle.kts @@ -48,4 +48,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) } diff --git a/display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction.png b/display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction-main.png similarity index 100% rename from display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction.png rename to display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction-main.png diff --git a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/MainActivity.kt b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/MainActivity.kt index 5e1d71dca..ba4e4b8f2 100644 --- a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/MainActivity.kt +++ b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/MainActivity.kt @@ -48,8 +48,7 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { MainScreen( - sampleName = getString(R.string.app_name), - application = application + sampleName = getString(R.string.app_name) ) } } diff --git a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ComposeMapView.kt b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ComposeMapView.kt deleted file mode 100644 index 759f38519..000000000 --- a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ComposeMapView.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* Copyright 2023 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.displaypointsusingclusteringfeaturereduction.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LifecycleOwner -import com.arcgismaps.mapping.view.MapView -import kotlinx.coroutines.launch - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun ComposeMapView( - modifier: Modifier = Modifier, - mapViewModel: MapViewModel -) { - // get an instance of the current lifecycle owner - val lifecycleOwner = LocalLifecycleOwner.current - // collect the latest state of the MapViewState - val mapViewState by mapViewModel.mapViewState.collectAsState() - // create and add MapView to the activity lifecycle - val mapView = createMapViewInstance(lifecycleOwner) - - // wrap the MapView as an AndroidView - AndroidView( - modifier = modifier, - factory = { mapView }, - // recomposes the MapView on changes in the MapViewState - update = { mapView -> - mapView.apply { - map = mapViewState.arcGISMap - setViewpoint(mapViewState.viewpoint) - } - } - ) - - // launch coroutine functions in the composition's CoroutineContext - LaunchedEffect(Unit) { - launch { - mapView.onSingleTapConfirmed.collect { - mapViewModel.dismissBottomSheet() - // call identifyLayers when a tap event occurs - val identifyResult = mapView.identifyLayers(it.screenCoordinate, 3.0, false) - mapViewModel.handleIdentifyResult(identifyResult) - } - } - launch { - mapView.onPan.collect{ - mapViewModel.dismissBottomSheet() - } - } - } -} - -/** - * Create the MapView instance and add it to the Activity lifecycle - */ -@Composable -fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { - // create the MapView - val mapView = MapView(LocalContext.current) - // add the side effects for MapView composition - DisposableEffect(lifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(mapView) - onDispose { - lifecycleOwner.lifecycle.removeObserver(mapView) - } - } - return mapView -} diff --git a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/MapViewModel.kt b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/MapViewModel.kt index 61bd5dfff..aedb6bb73 100644 --- a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/MapViewModel.kt +++ b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/MapViewModel.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp import androidx.core.text.HtmlCompat import androidx.lifecycle.AndroidViewModel import com.arcgismaps.LoadStatus @@ -42,19 +43,18 @@ import com.arcgismaps.mapping.layers.FeatureLayer import com.arcgismaps.mapping.popup.FieldsPopupElement import com.arcgismaps.mapping.popup.TextPopupElement import com.arcgismaps.mapping.view.IdentifyLayerResult +import com.arcgismaps.mapping.view.SingleTapConfirmedEvent import com.arcgismaps.portal.Portal +import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy import com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.R import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch class MapViewModel( application: Application, private val sampleCoroutineScope: CoroutineScope ) : AndroidViewModel(application) { - // set the MapView mutable stateflow - val mapViewState = MutableStateFlow(MapViewState()) // create a ViewModel to handle dialog interactions val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel() @@ -74,6 +74,13 @@ class MapViewModel( // the title of the popup result val popupTitle = mutableStateOf("") + // create a MapViewProxy that the view model will use to identify features in the MapView. + // this also needs to be passed to the composable MapView() function. + val mapViewProxy = MapViewProxy() + + // define ArcGIS map using Night Navigation basemap + var map = ArcGISMap(BasemapStyle.ArcGISNavigationNight) + init { // show loading dialog to indicate that the map is loading showLoadingDialog.value = true @@ -82,11 +89,13 @@ class MapViewModel( Portal(application.getString(R.string.portal_url)), "8916d50c44c746c1aafae001552bad23" ) - // set the map to be displayed in the layout's MapView - mapViewState.value.arcGISMap = ArcGISMap(portalItem) + // set the map to be displayed in the layout's MapView and set it's initialViewpoint + map = ArcGISMap(portalItem).apply { + initialViewpoint = Viewpoint(39.8, -98.6, 10e7) + } sampleCoroutineScope.launch { - mapViewState.value.arcGISMap.load().onSuccess { + map.load().onSuccess { showLoadingDialog.value = false } } @@ -96,7 +105,6 @@ class MapViewModel( `* Toggle the FeatureLayer's featureReduction property */ fun toggleFeatureReduction() { - val map = mapViewState.value.arcGISMap isFeatureReductionEnabled.value = !isFeatureReductionEnabled.value if (map.loadStatus.value == LoadStatus.Loaded) { map.operationalLayers.forEach { layer -> @@ -111,10 +119,22 @@ class MapViewModel( } } + /** + * Identifies the tapped screen coordinate in the provided [singleTapConfirmedEvent] + */ + fun identify(singleTapConfirmedEvent: SingleTapConfirmedEvent) { + sampleCoroutineScope.launch { + dismissBottomSheet() + // call identifyLayers when a tap event occurs + val identifyResult = mapViewProxy.identifyLayers(singleTapConfirmedEvent.screenCoordinate, 3.dp) + handleIdentifyResult(identifyResult) + } + } + /** * Identify the feature layer results and display the resulting popup element information */ - fun handleIdentifyResult(result: Result>) { + private fun handleIdentifyResult(result: Result>) { sampleCoroutineScope.launch { result.onSuccess { identifyResultList -> // initialize the string for each tap event resulting in a new identifyResultList @@ -229,10 +249,3 @@ class MapViewModel( } } -/** - * Class that represents the MapView state - */ -data class MapViewState( - var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight), - val viewpoint: Viewpoint = Viewpoint(39.8, -98.6, 10e7) -) diff --git a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/screens/MainScreen.kt b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/screens/MainScreen.kt index 35761a5a9..5b71a2f2a 100644 --- a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/screens/MainScreen.kt +++ b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/screens/MainScreen.kt @@ -31,9 +31,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.components.ClusterInfoContent -import com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.components.ComposeMapView import com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.components.MapViewModel import com.esri.arcgismaps.sample.sampleslib.components.BottomSheet import com.esri.arcgismaps.sample.sampleslib.components.LoadingDialog @@ -45,10 +46,12 @@ import com.esri.arcgismaps.sample.sampleslib.theme.SampleTypography * Main screen layout for the sample app */ @Composable -fun MainScreen(sampleName: String, application: Application) { +fun MainScreen(sampleName: String) { // coroutineScope that will be cancelled when this call leaves the composition val sampleCoroutineScope = rememberCoroutineScope() + // get the application context + val application = LocalContext.current.applicationContext as Application // create a ViewModel to handle MapView interactions val mapViewModel = remember { MapViewModel(application, sampleCoroutineScope) } @@ -62,12 +65,14 @@ fun MainScreen(sampleName: String, application: Application) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - // composable function that wraps the MapView - ComposeMapView( + MapView( modifier = Modifier .fillMaxSize() .weight(1f), - mapViewModel = mapViewModel + arcGISMap = mapViewModel.map, + mapViewProxy = mapViewModel.mapViewProxy, + onSingleTapConfirmed = mapViewModel::identify, + onPan = { mapViewModel.dismissBottomSheet() } ) // Button to enable/disable featureReduction property Row( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc55dc585..720d3b5d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] # ArcGIS Maps SDK for Kotlin version -arcgisMapsKotlinVersion = "200.4.0-4121" +arcgisMapsKotlinVersion = "200.4.0-4175" # ArcGIS Maps SDK for Kotlin Toolkit version -arcgisToolkitVersion = "200.4.0-4121" +arcgisToolkitVersion = "200.4.0-4183" # SDK versions compileSdk = "34" minSdk = "26" @@ -58,7 +58,8 @@ androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifec androidx-multidex = { group = "androidx.multidex", name = "multidex", version.ref = "multidexVersion"} androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workVersion" } arcgis-maps-kotlin = { group = "com.esri", name = "arcgis-maps-kotlin", version.ref = "arcgisMapsKotlinVersion"} -arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" } arcgis-maps-kotlin-toolkit-bom = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-bom", version.ref = "arcgisToolkitVersion"} +arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" } +arcgis-maps-kotlin-toolkit-geoview-compose = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-geoview-compose" } commons-io = { group = "commons-io", name = "commons-io", version.ref = "commonsIoVersion" } stdlib-jdk8 = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlinVersion"} diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/identify-layer-features/README.md b/identify-layer-features/README.md index 816e71b8d..1f1c9764f 100644 --- a/identify-layer-features/README.md +++ b/identify-layer-features/README.md @@ -26,8 +26,9 @@ Tap to identify features. A bottom text banner will show all layers with feature ## Additional information +This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable MapView. The GeoView supports two methods of identify: `identifyLayer`, which identifies features within a specific layer and `identifyLayers`, which identifies features for all layers in the current view. ## Tags -identify, recursion, recursive, sublayers +geoviewcompose, identify, recursion, recursive, sublayers, toolkit diff --git a/identify-layer-features/README.metadata.json b/identify-layer-features/README.metadata.json index 8a0b8d3a1..613b0dfb1 100644 --- a/identify-layer-features/README.metadata.json +++ b/identify-layer-features/README.metadata.json @@ -7,10 +7,12 @@ "identify-layer-features.png" ], "keywords": [ + "geoviewcompose", "identify", "recursion", "recursive", "sublayers", + "toolkit", "IdentifyLayerResult", "IdentifyLayerResult.sublayerResults", "LayerContent" @@ -24,7 +26,6 @@ ], "snippets": [ "src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/MainActivity.kt", - "src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/ComposeMapView.kt", "src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/MapViewModel.kt", "src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/screens/MainScreen.kt" ], diff --git a/identify-layer-features/build.gradle.kts b/identify-layer-features/build.gradle.kts index 03b1b5444..fc1bbbf5e 100644 --- a/identify-layer-features/build.gradle.kts +++ b/identify-layer-features/build.gradle.kts @@ -48,4 +48,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) } diff --git a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/MainActivity.kt b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/MainActivity.kt index 8b43d63dc..ac4844fa4 100644 --- a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/MainActivity.kt +++ b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/MainActivity.kt @@ -48,8 +48,7 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { MainScreen( - sampleName = getString(R.string.app_name), - application = application + sampleName = getString(R.string.app_name) ) } } diff --git a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/ComposeMapView.kt b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/ComposeMapView.kt deleted file mode 100644 index 7bec430bb..000000000 --- a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/ComposeMapView.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* Copyright 2023 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.identifylayerfeatures.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LifecycleOwner -import com.arcgismaps.mapping.view.MapView - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun ComposeMapView( - modifier: Modifier = Modifier, - mapViewModel: MapViewModel -) { - // get an instance of the current lifecycle owner - val lifecycleOwner = LocalLifecycleOwner.current - // collect the latest state of the MapViewState - val mapViewState by mapViewModel.mapViewState.collectAsState() - // create and add MapView to the activity lifecycle - val mapView = createMapViewInstance(lifecycleOwner) - - // wrap the MapView as an AndroidView - AndroidView( - modifier = modifier, - factory = { mapView }, - // recomposes the MapView on changes in the MapViewState - update = { mapView -> - mapView.apply { - map = mapViewState.arcGISMap - setViewpoint(mapViewState.viewpoint) - } - } - ) - - // launch coroutine functions in the composition's CoroutineContext - LaunchedEffect(Unit) { - mapView.onSingleTapConfirmed.collect { - // call identifyLayers when a tap event occurs - val identifyResult = mapView.identifyLayers( - screenCoordinate = it.screenCoordinate, - tolerance = 12.0, - returnPopupsOnly = false, - maximumResults = 10 - ) - mapViewModel.handleIdentifyResult(identifyResult) - } - } -} - -/** - * Create the MapView instance and add it to the Activity lifecycle - */ -@Composable -fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { - // create the MapView - val mapView = MapView(LocalContext.current) - // add the side effects for MapView composition - DisposableEffect(lifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(mapView) - onDispose { - lifecycleOwner.lifecycle.removeObserver(mapView) - } - } - return mapView -} diff --git a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/MapViewModel.kt b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/MapViewModel.kt index e1b216712..308afd984 100644 --- a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/MapViewModel.kt +++ b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/MapViewModel.kt @@ -18,28 +18,33 @@ package com.esri.arcgismaps.sample.identifylayerfeatures.components import android.app.Application import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.unit.dp import androidx.lifecycle.AndroidViewModel import com.arcgismaps.data.ServiceFeatureTable -import com.arcgismaps.geometry.Point -import com.arcgismaps.geometry.SpatialReference import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.BasemapStyle import com.arcgismaps.mapping.Viewpoint import com.arcgismaps.mapping.layers.ArcGISMapImageLayer import com.arcgismaps.mapping.layers.FeatureLayer.Companion.createWithFeatureTable import com.arcgismaps.mapping.view.IdentifyLayerResult +import com.arcgismaps.mapping.view.SingleTapConfirmedEvent +import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy import com.esri.arcgismaps.sample.identifylayerfeatures.R import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch class MapViewModel( application: Application, private val sampleCoroutineScope: CoroutineScope ) : AndroidViewModel(application) { - // set the MapView mutable stateflow - val mapViewState = MutableStateFlow(MapViewState()) + + // create a map using the topographic basemap style + val map: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic) + + // create a mapViewProxy that will be used to identify features in the MapView + // should also be passed to the composable MapView this mapViewProxy is associated with + val mapViewProxy = MapViewProxy() // create a ViewModel to handle dialog interactions val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel() @@ -66,21 +71,19 @@ class MapViewModel( } } - // create a topographic map - val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply { - // add world cities layer - operationalLayers.add(mapImageLayer) - // add damaged property data - operationalLayers.add(featureLayer) + // add the world cities layer with and the damaged properties feature layer + map.apply { + // set initial Viewpoint to North America + initialViewpoint = Viewpoint(39.8, -98.6, 5e7) + operationalLayers.addAll(listOf(mapImageLayer, featureLayer)) } - // assign the map to the map view - mapViewState.value.arcGISMap = map + } /** * Identify the feature layer results and display the resulting information */ - fun handleIdentifyResult(result: Result>) { + private fun handleIdentifyResult(result: Result>) { sampleCoroutineScope.launch { result.onSuccess { identifyResultList -> val message = StringBuilder() @@ -129,19 +132,20 @@ class MapViewModel( } return subLayerGeoElementCount + result.geoElements.size } -} -/** - * Data class that represents the MapView state - */ -data class MapViewState( - var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight), - var viewpoint: Viewpoint = Viewpoint( - center = Point( - x = -10977012.785807, - y = 4514257.550369, - spatialReference = SpatialReference(wkid = 3857) - ), - scale = 68015210.0 - ) -) + /** + * Identifies the tapped screen coordinate in the provided [singleTapConfirmedEvent] + */ + fun identify(singleTapConfirmedEvent: SingleTapConfirmedEvent) { + sampleCoroutineScope.launch { + // identify the layers on the tapped coordinate + val identifyResult = mapViewProxy.identifyLayers( + screenCoordinate = singleTapConfirmedEvent.screenCoordinate, + tolerance = 12.dp, + maximumResults = 10 + ) + // use the layer result to display feature information + handleIdentifyResult(identifyResult) + } + } +} diff --git a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/screens/MainScreen.kt b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/screens/MainScreen.kt index e81ad166d..1f3633a7b 100644 --- a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/screens/MainScreen.kt +++ b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/screens/MainScreen.kt @@ -29,8 +29,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.esri.arcgismaps.sample.identifylayerfeatures.components.ComposeMapView +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.identifylayerfeatures.components.MapViewModel import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar @@ -39,11 +40,15 @@ import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar * Main screen layout for the sample app */ @Composable -fun MainScreen(sampleName: String, application: Application) { +fun MainScreen(sampleName: String) { // coroutineScope that will be cancelled when this call leaves the composition val sampleCoroutineScope = rememberCoroutineScope() + // get the application property that will be used to construct MapViewModel + val sampleApplication = LocalContext.current.applicationContext as Application // create a ViewModel to handle MapView interactions - val mapViewModel = remember { MapViewModel(application, sampleCoroutineScope) } + val mapViewModel = remember { MapViewModel(sampleApplication, sampleCoroutineScope) } + // create a Viewpoint + Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, @@ -53,13 +58,14 @@ fun MainScreen(sampleName: String, application: Application) { .fillMaxSize() .padding(it) ) { - // composable function that wraps the MapView - ComposeMapView( + MapView( modifier = Modifier .fillMaxSize() .weight(1f) .animateContentSize(), - mapViewModel = mapViewModel + arcGISMap = mapViewModel.map, + mapViewProxy = mapViewModel.mapViewProxy, + onSingleTapConfirmed = mapViewModel::identify ) // Bottom text to display the identify results Row( diff --git a/manage-operational-layers/README.md b/manage-operational-layers/README.md index 65fa9f210..cc09bdbf9 100644 --- a/manage-operational-layers/README.md +++ b/manage-operational-layers/README.md @@ -27,8 +27,9 @@ When the app starts, the display lists of operational layers and any removed lay ## Additional information +This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable MapView. You cannot add the same layer to the map multiple times or add the same layer to multiple maps. Instead, clone the layer with `layer.clone()` before duplicating. ## Tags -add, delete, layer, map, remove +add, delete, geoviewcompose, layer, map, remove, toolkit diff --git a/manage-operational-layers/README.metadata.json b/manage-operational-layers/README.metadata.json index 7bebd151d..d130bd64f 100644 --- a/manage-operational-layers/README.metadata.json +++ b/manage-operational-layers/README.metadata.json @@ -9,9 +9,11 @@ "keywords": [ "add", "delete", + "geoviewcompose", "layer", "map", "remove", + "toolkit", "ArcGISMap", "ArcGISMapImageLayer", "LayerList" @@ -25,7 +27,6 @@ ], "snippets": [ "src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/MainActivity.kt", - "src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/components/ComposeMapView.kt", "src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/components/MapViewModel.kt", "src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/screens/LayersList.kt", "src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/screens/MainScreen.kt" diff --git a/manage-operational-layers/build.gradle.kts b/manage-operational-layers/build.gradle.kts index fc10b311f..0ca9e0a7f 100644 --- a/manage-operational-layers/build.gradle.kts +++ b/manage-operational-layers/build.gradle.kts @@ -48,4 +48,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) } diff --git a/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/MainActivity.kt b/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/MainActivity.kt index a6486fc76..d97c91028 100644 --- a/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/MainActivity.kt +++ b/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/MainActivity.kt @@ -48,8 +48,7 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { MainScreen( - sampleName = getString(R.string.app_name), - application = application + sampleName = getString(R.string.app_name) ) } } diff --git a/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/components/ComposeMapView.kt b/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/components/ComposeMapView.kt deleted file mode 100644 index 9c534c42d..000000000 --- a/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/components/ComposeMapView.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* Copyright 2023 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.manageoperationallayers.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LifecycleOwner -import com.arcgismaps.mapping.view.MapView - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun ComposeMapView( - modifier: Modifier = Modifier, - mapViewModel: MapViewModel -) { - // get an instance of the current lifecycle owner - val lifecycleOwner = LocalLifecycleOwner.current - // get the instance of the MapView state - val mapViewState = mapViewModel.mapViewState - // create and add MapView to the activity lifecycle - val mapView = createMapViewInstance(lifecycleOwner) - - // wrap the MapView as an AndroidView - AndroidView( - modifier = modifier, - factory = { mapView }, - // recomposes the MapView on changes in the MapViewState - update = { mapView -> - mapView.apply { - map = mapViewState.arcGISMap - setViewpoint(mapViewState.viewpoint) - } - } - ) -} - -/** - * Create the MapView instance and add it to the Activity lifecycle - */ -@Composable -fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { - // create the MapView - val mapView = MapView(LocalContext.current) - // add the side effects for MapView composition - DisposableEffect(lifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(mapView) - onDispose { - lifecycleOwner.lifecycle.removeObserver(mapView) - } - } - return mapView -} diff --git a/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/components/MapViewModel.kt b/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/components/MapViewModel.kt index 5520b74a5..b003eb943 100644 --- a/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/components/MapViewModel.kt +++ b/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/components/MapViewModel.kt @@ -17,10 +17,7 @@ package com.esri.arcgismaps.sample.manageoperationallayers.components import android.app.Application -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.AndroidViewModel import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.BasemapStyle @@ -33,8 +30,8 @@ class MapViewModel( application: Application ) : AndroidViewModel(application) { - // get an instance of the MapView state - val mapViewState = MapViewState() + // create an ArcGISMap + val arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic) // a list of the active map image layer names var activateLayerNames = mutableStateListOf() @@ -65,7 +62,8 @@ class MapViewModel( ) // add the layers to the map's operational layers - mapViewState.arcGISMap.apply { + arcGISMap.apply { + initialViewpoint = Viewpoint(39.8, -98.6, 5e7) operationalLayers.addAll( listOf( imageLayerElevation, @@ -81,7 +79,7 @@ class MapViewModel( */ fun moveLayerUp(layerName: String) { // get a copy of the operational layers - val operationalLayers = mapViewState.arcGISMap.operationalLayers.toMutableList() + val operationalLayers = arcGISMap.operationalLayers.toMutableList() // if move up on the first item is selected, then return if (operationalLayers.first().name == layerName) { return @@ -96,7 +94,7 @@ class MapViewModel( addAll(operationalLayers.map { layer -> layer.name }) } // update the operational layers - mapViewState.arcGISMap.operationalLayers.apply { + arcGISMap.operationalLayers.apply { clear() addAll(operationalLayers) } @@ -107,7 +105,7 @@ class MapViewModel( */ fun moveLayerDown(layerName: String) { // get a copy of the operational layers - val operationalLayers = mapViewState.arcGISMap.operationalLayers.toMutableList() + val operationalLayers = arcGISMap.operationalLayers.toMutableList() // if move down on the last item is selected, then return if (operationalLayers.last().name == layerName) { return @@ -122,7 +120,7 @@ class MapViewModel( addAll(operationalLayers.map { layer -> layer.name }) } // update the operational layers - mapViewState.arcGISMap.operationalLayers.apply { + arcGISMap.operationalLayers.apply { clear() addAll(operationalLayers) } @@ -132,7 +130,7 @@ class MapViewModel( * Removes [layerName] from map and adds it to the list of [inactiveLayers]. */ fun removeLayerFromMap(layerName: String) { - mapViewState.arcGISMap.operationalLayers.apply { + arcGISMap.operationalLayers.apply { val layerIndex = indexOf(find { it.name == layerName }) inactiveLayers.add(get(layerIndex)) removeAt(layerIndex) @@ -146,22 +144,13 @@ class MapViewModel( fun addLayerToMap(layerName: String) { inactiveLayers.apply { val layerIndex = indexOf(find { it.name == layerName }) - mapViewState.arcGISMap.operationalLayers.add(get(layerIndex)) + arcGISMap.operationalLayers.add(get(layerIndex)) activateLayerNames.add(get(layerIndex).name) removeAt(layerIndex) } } } - -/** - * Class that represents the MapView state - */ -class MapViewState { - val arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic) - val viewpoint: Viewpoint = Viewpoint(39.8, -98.6, 5e7) -} - /** * Extension function to swap two values of a mutable list. */ diff --git a/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/screens/MainScreen.kt b/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/screens/MainScreen.kt index 43f433bde..0d51f3467 100644 --- a/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/screens/MainScreen.kt +++ b/manage-operational-layers/src/main/java/com/esri/arcgismaps/sample/manageoperationallayers/screens/MainScreen.kt @@ -23,7 +23,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.esri.arcgismaps.sample.manageoperationallayers.components.ComposeMapView +import androidx.compose.ui.platform.LocalContext +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.manageoperationallayers.components.MapViewModel import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar @@ -31,9 +32,9 @@ import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar * Main screen layout for the sample app */ @Composable -fun MainScreen(sampleName: String, application: Application) { +fun MainScreen(sampleName: String) { // create a ViewModel to handle MapView interactions - val mapViewModel = MapViewModel(application) + val mapViewModel = MapViewModel(LocalContext.current.applicationContext as Application) Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, @@ -41,10 +42,9 @@ fun MainScreen(sampleName: String, application: Application) { Column( modifier = Modifier.fillMaxSize().padding(it) ) { - // composable function that wraps the MapView - ComposeMapView( + MapView( modifier = Modifier.fillMaxSize().weight(1f), - mapViewModel = mapViewModel, + arcGISMap = mapViewModel.arcGISMap ) LayersList( activateLayerNames = mapViewModel.activateLayerNames, diff --git a/query-feature-table/README.md b/query-feature-table/README.md index 6267c55f9..521394257 100644 --- a/query-feature-table/README.md +++ b/query-feature-table/README.md @@ -30,6 +30,10 @@ Input the name of a U.S. state into the text field. When you click the search ic This sample uses U.S. State polygon features from the [USA 2016 Daytime Population](https://www.arcgis.com/home/item.html?id=f01f0eda766344e29f42031e7bfb7d04) feature service. +## Additional information + +This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable MapView. + ## Tags -query, search +geoviewcompose, query, search, toolkit diff --git a/query-feature-table/README.metadata.json b/query-feature-table/README.metadata.json index 9dd8b7a4d..fb13a5153 100644 --- a/query-feature-table/README.metadata.json +++ b/query-feature-table/README.metadata.json @@ -7,8 +7,10 @@ "query-feature-table.png" ], "keywords": [ + "geoviewcompose", "query", "search", + "toolkit", "FeatureLayer", "FeatureQueryResult", "QueryParameters", @@ -24,7 +26,6 @@ ], "snippets": [ "src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/MainActivity.kt", - "src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/components/ComposeMapView.kt", "src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/components/MapViewModel.kt", "src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/screens/MainScreen.kt", "src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/screens/SearchBar.kt" diff --git a/query-feature-table/build.gradle.kts b/query-feature-table/build.gradle.kts index 33ca805ce..dcb034a7f 100644 --- a/query-feature-table/build.gradle.kts +++ b/query-feature-table/build.gradle.kts @@ -48,4 +48,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) } diff --git a/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/MainActivity.kt b/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/MainActivity.kt index f70086ffe..c1938f4b4 100644 --- a/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/MainActivity.kt +++ b/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/MainActivity.kt @@ -48,8 +48,7 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { MainScreen( - sampleName = getString(R.string.app_name), - application = application + sampleName = getString(R.string.app_name) ) } } diff --git a/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/components/ComposeMapView.kt b/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/components/ComposeMapView.kt deleted file mode 100644 index f4b8e8178..000000000 --- a/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/components/ComposeMapView.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* Copyright 2023 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.queryfeaturetable.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import com.arcgismaps.mapping.view.MapView -import kotlinx.coroutines.launch - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun ComposeMapView( - modifier: Modifier = Modifier, - mapViewModel: MapViewModel -) { - // get an instance of the current lifecycle owner - val lifecycleOwner = LocalLifecycleOwner.current - // get an instance of the MapView state - val mapViewState = mapViewModel.mapViewState - // create and add MapView to the activity lifecycle - val mapView = createMapViewInstance(lifecycleOwner) - - // wrap the MapView as an AndroidView - AndroidView( - modifier = modifier, - factory = { mapView }, - // recomposes the MapView on changes in the MapViewState - update = { mapView -> - mapView.apply { - map = mapViewState.arcGISMap - setViewpoint(mapViewState.initialViewpoint) - lifecycleOwner.lifecycleScope.launch { - mapViewState.stateGeometry?.let { setViewpointGeometry(it, 20.0) } - } - } - } - ) -} - -/** - * Create the MapView instance and add it to the Activity lifecycle - */ -@Composable -fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { - // create the MapView - val mapView = MapView(LocalContext.current) - // add the side effects for MapView composition - DisposableEffect(lifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(mapView) - onDispose { - lifecycleOwner.lifecycle.removeObserver(mapView) - } - } - return mapView -} diff --git a/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/components/MapViewModel.kt b/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/components/MapViewModel.kt index 02f653538..8051ff1c5 100644 --- a/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/components/MapViewModel.kt +++ b/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/components/MapViewModel.kt @@ -25,7 +25,6 @@ import com.arcgismaps.Color import com.arcgismaps.data.FeatureQueryResult import com.arcgismaps.data.QueryParameters import com.arcgismaps.data.ServiceFeatureTable -import com.arcgismaps.geometry.Geometry import com.arcgismaps.geometry.Point import com.arcgismaps.geometry.SpatialReference import com.arcgismaps.mapping.ArcGISMap @@ -37,6 +36,7 @@ import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle import com.arcgismaps.mapping.symbology.SimpleLineSymbol import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle import com.arcgismaps.mapping.symbology.SimpleRenderer +import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy import com.esri.arcgismaps.sample.queryfeaturetable.R import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel import kotlinx.coroutines.CoroutineScope @@ -44,13 +44,10 @@ import kotlinx.coroutines.launch import java.util.Locale class MapViewModel( - private val application: Application, + application: Application, private val sampleCoroutineScope: CoroutineScope ) : AndroidViewModel(application) { - // get an instance of the MapView state - val mapViewState = MapViewState() - // create a ViewModel to handle dialog interactions val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel() @@ -64,6 +61,17 @@ class MapViewModel( FeatureLayer.createWithFeatureTable(serviceFeatureTable) } + // map used to display the feature layer + val map = ArcGISMap(BasemapStyle.ArcGISTopographic) + + private var usaViewpoint = Viewpoint( + center = Point(-11e6, 5e6, SpatialReference.webMercator()), + scale = 1e8 + ) + + // create a MapViewProxy to handle MapView operations + var mapViewProxy by mutableStateOf(MapViewProxy()) + init { // use symbols to show U.S. states with a black outline and yellow fill val lineSymbol = SimpleLineSymbol( @@ -77,6 +85,7 @@ class MapViewModel( outline = lineSymbol ) + // set featurelayer properties featureLayer.apply { // set renderer for the feature layer renderer = SimpleRenderer(fillSymbol) @@ -84,7 +93,10 @@ class MapViewModel( maxScale = 10000.0 } // add the feature layer to the map's operational layers - mapViewState.arcGISMap.operationalLayers.add(featureLayer) + map.apply { + initialViewpoint = usaViewpoint + operationalLayers.add(featureLayer) + } } /** @@ -113,28 +125,11 @@ class MapViewModel( // get the extent of the first feature in the result to zoom to val envelope = feature.geometry?.extent ?: return@launch messageDialogVM.showMessageDialog("Error retrieving geometry extent") - // update the map view to set the viewpoint to the state geometry - mapViewState.stateGeometry = envelope + // update the map's viewpoint to the feature's geometry + mapViewProxy.setViewpointGeometry(envelope) } else { messageDialogVM.showMessageDialog("No states found with name: $searchQuery") } } } } - -/** - * Class that represents the MapView state - */ -class MapViewState { - // map used to display the feature layer - var arcGISMap: ArcGISMap by mutableStateOf(ArcGISMap(BasemapStyle.ArcGISTopographic)) - - // geometry of the queried state - var stateGeometry: Geometry? by mutableStateOf(null) - - // set an initial viewpoint over the USA - val initialViewpoint: Viewpoint = Viewpoint( - center = Point(-11e6, 5e6, SpatialReference.webMercator()), - scale = 1e8 - ) -} diff --git a/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/screens/MainScreen.kt b/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/screens/MainScreen.kt index 8ab3b3253..7d25fb53c 100644 --- a/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/screens/MainScreen.kt +++ b/query-feature-table/src/main/java/com/esri/arcgismaps/sample/queryfeaturetable/screens/MainScreen.kt @@ -26,7 +26,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import com.esri.arcgismaps.sample.queryfeaturetable.components.ComposeMapView +import androidx.compose.ui.platform.LocalContext +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.queryfeaturetable.components.MapViewModel import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar @@ -35,20 +36,27 @@ import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar * Main screen layout for the sample app */ @Composable -fun MainScreen(sampleName: String, application: Application) { +fun MainScreen(sampleName: String) { // coroutineScope that will be cancelled when this call leaves the composition val sampleCoroutineScope = rememberCoroutineScope() + // get the application context + val application = LocalContext.current.applicationContext as Application // create a ViewModel to handle MapView interactions val mapViewModel = remember { MapViewModel(application, sampleCoroutineScope) } Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { - Column(modifier = Modifier.fillMaxSize().padding(it)) { + Column(modifier = Modifier + .fillMaxSize() + .padding(it)) { // composable function that wraps the MapView - ComposeMapView( - modifier = Modifier.fillMaxWidth().weight(1f), - mapViewModel = mapViewModel + MapView( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + arcGISMap = mapViewModel.map, + mapViewProxy = mapViewModel.mapViewProxy ) SearchBar( modifier = Modifier.fillMaxWidth(), diff --git a/show-coordinates-in-multiple-formats/README.md b/show-coordinates-in-multiple-formats/README.md index ec4d846ab..8c5dea6f7 100644 --- a/show-coordinates-in-multiple-formats/README.md +++ b/show-coordinates-in-multiple-formats/README.md @@ -24,6 +24,10 @@ Tap on the map to see a marker with the tapped location's coordinate formatted i * CoordinateFormatter.LatitudeLongitudeFormat * CoordinateFormatter.UtmConversionMode +## Additional information + +This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable MapView. + ## Tags -convert, coordinate, decimal degrees, degree minutes seconds, format, latitude, longitude, USNG, UTM +convert, coordinate, decimal degrees, degree minutes seconds, format, geoviewcompose, latitude, longitude, toolkit, USNG, UTM diff --git a/show-coordinates-in-multiple-formats/README.metadata.json b/show-coordinates-in-multiple-formats/README.metadata.json index a7d84439b..e3af54d48 100644 --- a/show-coordinates-in-multiple-formats/README.metadata.json +++ b/show-coordinates-in-multiple-formats/README.metadata.json @@ -14,8 +14,10 @@ "decimal degrees", "degree minutes seconds", "format", + "geoviewcompose", "latitude", "longitude", + "toolkit", "CoordinateFormatter", "CoordinateFormatter.LatitudeLongitudeFormat", "CoordinateFormatter.UtmConversionMode" @@ -29,7 +31,6 @@ ], "snippets": [ "src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/MainActivity.kt", - "src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/components/ComposeMapView.kt", "src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/components/MapViewModel.kt", "src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/screens/CoordinatesLayout.kt", "src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/screens/MainScreen.kt" diff --git a/show-coordinates-in-multiple-formats/build.gradle.kts b/show-coordinates-in-multiple-formats/build.gradle.kts index d9701983c..3871724f4 100644 --- a/show-coordinates-in-multiple-formats/build.gradle.kts +++ b/show-coordinates-in-multiple-formats/build.gradle.kts @@ -48,4 +48,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) } diff --git a/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/components/ComposeMapView.kt b/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/components/ComposeMapView.kt deleted file mode 100644 index f91491bf4..000000000 --- a/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/components/ComposeMapView.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* Copyright 2023 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.showcoordinatesinmultipleformats.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LifecycleOwner -import com.arcgismaps.mapping.view.MapView -import com.arcgismaps.mapping.view.SingleTapConfirmedEvent -import kotlinx.coroutines.launch - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun ComposeMapView( - modifier: Modifier = Modifier, - mapViewModel: MapViewModel, - onSingleTap: (SingleTapConfirmedEvent) -> Unit = {} -) { - // get an instance of the current lifecycle owner - val lifecycleOwner = LocalLifecycleOwner.current - // get an instance of the ViewModel's MapViewState - val mapViewState = mapViewModel.mapViewState - // create and add MapView to the activity lifecycle - val mapView = createMapViewInstance(lifecycleOwner) - - // wrap the MapView as an AndroidView - AndroidView( - modifier = modifier, - factory = { mapView }, - // recomposes the MapView on changes in the MapViewState - update = { mapView -> - mapView.apply { - map = mapViewState.arcGISMap - mapView.graphicsOverlays.clear() - mapView.graphicsOverlays.add(mapViewState.graphicsOverlay) - } - } - ) - - // launch coroutine functions in the composition's CoroutineContext - LaunchedEffect(Unit) { - launch { - mapView.onSingleTapConfirmed.collect { - onSingleTap(it) - } - } - } -} - -/** - * Create the MapView instance and add it to the Activity lifecycle - */ -@Composable -fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { - // create the MapView - val mapView = MapView(LocalContext.current) - // add the side effects for MapView composition - DisposableEffect(lifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(mapView) - onDispose { - lifecycleOwner.lifecycle.removeObserver(mapView) - } - } - return mapView -} diff --git a/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/components/MapViewModel.kt b/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/components/MapViewModel.kt index f87589128..4869b23c1 100644 --- a/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/components/MapViewModel.kt +++ b/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/components/MapViewModel.kt @@ -27,20 +27,12 @@ import com.arcgismaps.geometry.LatitudeLongitudeFormat import com.arcgismaps.geometry.Point import com.arcgismaps.geometry.SpatialReference import com.arcgismaps.geometry.UtmConversionMode -import com.arcgismaps.mapping.ArcGISMap -import com.arcgismaps.mapping.Basemap -import com.arcgismaps.mapping.BasemapStyle -import com.arcgismaps.mapping.layers.ArcGISTiledLayer import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle import com.arcgismaps.mapping.view.Graphic -import com.arcgismaps.mapping.view.GraphicsOverlay import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel -import com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.R class MapViewModel(application: Application) : AndroidViewModel(application) { - // set the MapView state - val mapViewState = MapViewState() var decimalDegrees by mutableStateOf("") private set @@ -53,10 +45,13 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { var usng by mutableStateOf("") private set + // create a ViewModel to handle dialog interactions + val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel() + // set up a graphic to indicate where the coordinates relate to, with an initial location - private val initialPoint = Point(0.0, 0.0, SpatialReference.wgs84()) + val initialPoint = Point(0.0, 0.0, SpatialReference.wgs84()) - private val coordinateLocation = Graphic( + val coordinateLocationGraphic = Graphic( geometry = initialPoint, symbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.Cross, @@ -65,42 +60,12 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { ) ) - // create a ViewModel to handle dialog interactions - val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel() - - init { - // create a map that has the WGS 84 coordinate system and set this into the map - val basemapLayer = ArcGISTiledLayer(application.getString(R.string.basemap_url)) - val map = ArcGISMap(Basemap(basemapLayer)) - mapViewState.arcGISMap = map - mapViewState.graphicsOverlay.graphics.add(coordinateLocation) - - // update the coordinate notations using the initial point - toCoordinateNotationFromPoint(initialPoint) - } - - /** - * Updates the tapped graphic and coordinate notations using the [tappedPoint] - */ - fun onMapTapped(tappedPoint: Point?) { - if (tappedPoint != null) { - // update the tapped location graphic - coordinateLocation.geometry = tappedPoint - mapViewState.graphicsOverlay.graphics.apply { - clear() - add(coordinateLocation) - } - // update the coordinate notations using the tapped point - toCoordinateNotationFromPoint(tappedPoint) - } - } - /** * Uses CoordinateFormatter to update the UI with coordinate notation strings based on the * given [newLocation] point to convert to coordinate notations */ - private fun toCoordinateNotationFromPoint(newLocation: Point) { - coordinateLocation.geometry = newLocation + fun toCoordinateNotationFromPoint(newLocation: Point) { + coordinateLocationGraphic.geometry = newLocation // use CoordinateFormatter to convert to Latitude Longitude, formatted as Decimal Degrees decimalDegrees = CoordinateFormatter.toLatitudeLongitudeOrNull( point = newLocation, @@ -206,11 +171,3 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { usng = inputString } } - -/** - * Class that represents the MapView's current state - */ -class MapViewState { - var arcGISMap: ArcGISMap by mutableStateOf(ArcGISMap(BasemapStyle.ArcGISNavigationNight)) - var graphicsOverlay: GraphicsOverlay by mutableStateOf(GraphicsOverlay()) -} diff --git a/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/screens/MainScreen.kt b/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/screens/MainScreen.kt index 037cefe92..d2b9bbcb5 100644 --- a/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/screens/MainScreen.kt +++ b/show-coordinates-in-multiple-formats/src/main/java/com/esri/arcgismaps/sample/showcoordinatesinmultipleformats/screens/MainScreen.kt @@ -21,11 +21,18 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel +import com.arcgismaps.mapping.ArcGISMap +import com.arcgismaps.mapping.Basemap +import com.arcgismaps.mapping.layers.ArcGISTiledLayer +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar -import com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.components.ComposeMapView +import com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.R import com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.components.MapViewModel /** @@ -35,21 +42,45 @@ import com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.components.Ma fun MainScreen(sampleName: String) { // create a ViewModel to handle MapView interactions val mapViewModel: MapViewModel = viewModel() + // create a map that has the WGS 84 coordinate system and set this into the map + val basemapLayer = ArcGISTiledLayer(LocalContext.current.applicationContext.getString(R.string.basemap_url)) + val arcGISMap = ArcGISMap(Basemap(basemapLayer)) + // graphics overlay to display a graphics of the coordinate location + val graphicsOverlay = GraphicsOverlay().apply { + graphics.add(mapViewModel.coordinateLocationGraphic) + } + // the collection of graphics overlays used by the MapView + val graphicsOverlays = remember { listOf(graphicsOverlay) } + // update the coordinate notations using the initial point + mapViewModel.toCoordinateNotationFromPoint(mapViewModel.initialPoint) Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { Column( - modifier = Modifier.fillMaxSize().padding(it) + modifier = Modifier + .fillMaxSize() + .padding(it) ) { // layout to display the coordinate text fields. CoordinatesLayout(mapViewModel = mapViewModel) - // composable function that wraps the MapView - ComposeMapView( + MapView( modifier = Modifier.fillMaxSize(), - mapViewModel = mapViewModel, - onSingleTap = { singleTapConfirmedEvent -> - mapViewModel.onMapTapped(singleTapConfirmedEvent.mapPoint) + arcGISMap = arcGISMap, + graphicsOverlays = graphicsOverlays, + onSingleTapConfirmed = { singleTapConfirmedEvent -> + // retrieve the map point on MapView tapped + val tappedPoint = singleTapConfirmedEvent.mapPoint + if (tappedPoint != null) { + // update the tapped location graphic + mapViewModel.coordinateLocationGraphic.geometry = tappedPoint + graphicsOverlay.graphics.apply { + clear() + add(mapViewModel.coordinateLocationGraphic) + } + // update the coordinate notations using the tapped point + mapViewModel.toCoordinateNotationFromPoint(tappedPoint) + } } ) diff --git a/show-magnifier/README.md b/show-magnifier/README.md index ec7f16628..cf2cc102c 100644 --- a/show-magnifier/README.md +++ b/show-magnifier/README.md @@ -22,13 +22,13 @@ Tap and hold on the map to show a magnifier, then drag across the map to move th * ArcGISMap * MapView -* MapView.interactionOptions.allowMagnifierToPan * MapView.interactionOptions.isMagnifierEnabled ## Additional information -This sample only works on a device with a touch screen. The magnifier will not appear via a mouse click. +This sample uses the GeoViewCompose Toolkit module to be able to implement a Composable MapView. +It only works on a device with a touch screen. The magnifier will not appear via a mouse click. ## Tags -magnify, map, zoom +geoviewcompose, magnify, map, toolkit, zoom diff --git a/show-magnifier/README.metadata.json b/show-magnifier/README.metadata.json index d41c5fcea..f416c5829 100644 --- a/show-magnifier/README.metadata.json +++ b/show-magnifier/README.metadata.json @@ -7,12 +7,13 @@ "show-magnifier.png" ], "keywords": [ + "geoviewcompose", "magnify", "map", + "toolkit", "zoom", "ArcGISMap", "MapView", - "MapView.interactionOptions.allowMagnifierToPan", "MapView.interactionOptions.isMagnifierEnabled" ], "language": "kotlin", @@ -20,12 +21,10 @@ "relevant_apis": [ "ArcGISMap", "MapView", - "MapView.interactionOptions.allowMagnifierToPan", "MapView.interactionOptions.isMagnifierEnabled" ], "snippets": [ "src/main/java/com/esri/arcgismaps/sample/showmagnifier/MainActivity.kt", - "src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/ComposeMapView.kt", "src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/MapViewModel.kt", "src/main/java/com/esri/arcgismaps/sample/showmagnifier/screens/MainScreen.kt" ], diff --git a/show-magnifier/build.gradle.kts b/show-magnifier/build.gradle.kts index 708d281d7..bd2845d89 100644 --- a/show-magnifier/build.gradle.kts +++ b/show-magnifier/build.gradle.kts @@ -48,4 +48,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) } diff --git a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/MainActivity.kt b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/MainActivity.kt index a0b665732..2c8416c74 100644 --- a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/MainActivity.kt +++ b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/MainActivity.kt @@ -48,8 +48,7 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { MainScreen( - sampleName = getString(R.string.app_name), - application = application + sampleName = getString(R.string.app_name) ) } } diff --git a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/ComposeMapView.kt b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/ComposeMapView.kt deleted file mode 100644 index 66e95e015..000000000 --- a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/ComposeMapView.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* Copyright 2023 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.showmagnifier.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LifecycleOwner -import com.arcgismaps.mapping.view.MapView - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun ComposeMapView( - modifier: Modifier = Modifier, - mapViewModel: MapViewModel -) { - // get an instance of the current lifecycle owner - val lifecycleOwner = LocalLifecycleOwner.current - // get an instance of the MapView state - val mapViewState = mapViewModel.mapViewState - // create and add MapView to the activity lifecycle - val mapView = createMapViewInstance(lifecycleOwner) - - // wrap the MapView as an AndroidView - AndroidView( - modifier = modifier, - factory = { mapView }, - // recomposes the MapView on changes in the MapViewState - update = { mapView -> - mapView.apply { - map = mapViewState.arcGISMap - setViewpoint(mapViewState.viewpoint) - // set the MapView's interaction options using the MapViewState - mapViewState.interactionOptions.apply { - interactionOptions.isMagnifierEnabled = isMagnifierEnabled - interactionOptions.allowMagnifierToPan = allowMagnifierToPan - } - } - } - ) -} - -/** - * Create the MapView instance and add it to the Activity lifecycle - */ -@Composable -fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { - // create the MapView - val mapView = MapView(LocalContext.current) - // add the side effects for MapView composition - DisposableEffect(lifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(mapView) - onDispose { - lifecycleOwner.lifecycle.removeObserver(mapView) - } - } - return mapView -} diff --git a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/MapViewModel.kt b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/MapViewModel.kt index f92e11575..8b1378917 100644 --- a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/MapViewModel.kt +++ b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/MapViewModel.kt @@ -1,42 +1 @@ -/* Copyright 2023 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.showmagnifier.components - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import com.arcgismaps.mapping.ArcGISMap -import com.arcgismaps.mapping.BasemapStyle -import com.arcgismaps.mapping.Viewpoint -import com.arcgismaps.mapping.view.MapViewInteractionOptions - -class MapViewModel(application: Application) : AndroidViewModel(application) { - // get an instance of the MapView state - val mapViewState = MapViewState() -} - -/** - * Class that represents the MapView's current state - */ -class MapViewState { - val arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic) - val viewpoint: Viewpoint = Viewpoint(34.056295, -117.195800, 1000000.0) - // setting `isMagnifierEnabled` property to true. `allowMagnifierToPan` by default is true - val interactionOptions: MapViewInteractionOptions = MapViewInteractionOptions( - isMagnifierEnabled = true - ) -} diff --git a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/screens/MainScreen.kt b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/screens/MainScreen.kt index e60eb1b7f..a86edfd1d 100644 --- a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/screens/MainScreen.kt +++ b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/screens/MainScreen.kt @@ -16,35 +16,37 @@ package com.esri.arcgismaps.sample.showmagnifier.screens -import android.app.Application -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.arcgismaps.mapping.ArcGISMap +import com.arcgismaps.mapping.BasemapStyle +import com.arcgismaps.mapping.Viewpoint +import com.arcgismaps.mapping.view.MapViewInteractionOptions +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar -import com.esri.arcgismaps.sample.showmagnifier.components.ComposeMapView -import com.esri.arcgismaps.sample.showmagnifier.components.MapViewModel /** * Main screen layout for the sample app */ @Composable -fun MainScreen(sampleName: String, application: Application) { - // create a ViewModel to handle MapView interactions - var mapViewModel = MapViewModel(application) +fun MainScreen(sampleName: String) { + // Create an ArcGISMap and Viewpoint + val californiaViewpoint = Viewpoint(34.056295, -117.195800, 1000000.0) + val arcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic).apply { + initialViewpoint = californiaViewpoint + } Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { - Column(modifier = Modifier.fillMaxSize().padding(it)) { - // composable function that wraps the MapView - ComposeMapView( - modifier = Modifier.fillMaxSize().weight(1f), - mapViewModel = mapViewModel, - ) - } + MapView( + modifier = Modifier.fillMaxSize().padding(it), + arcGISMap = arcGISMap, + mapViewInteractionOptions = MapViewInteractionOptions(isMagnifierEnabled = true) + ) } ) } diff --git a/snap-to-features/.gitignore b/snap-to-features/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/snap-to-features/.gitignore @@ -0,0 +1 @@ +/build diff --git a/snap-to-features/README.md b/snap-to-features/README.md new file mode 100644 index 000000000..cb54dc598 --- /dev/null +++ b/snap-to-features/README.md @@ -0,0 +1 @@ +# Snap To Features diff --git a/snap-to-features/README.metadata.json b/snap-to-features/README.metadata.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/snap-to-features/README.metadata.json @@ -0,0 +1,2 @@ +{ +} diff --git a/snap-to-features/build.gradle.kts b/snap-to-features/build.gradle.kts new file mode 100644 index 000000000..d3da26d3c --- /dev/null +++ b/snap-to-features/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "com.esri.arcgismaps.sample.snaptofeatures" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = libs.versions.versionCode.get().toInt() + versionName = libs.versions.versionName.get() + buildConfigField("String", "API_KEY", project.properties["API_KEY"].toString()) + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + } + } + + buildFeatures { + compose = true + buildConfig = true + } + + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerExt.get() + } + + namespace = "com.esri.arcgismaps.sample.snaptofeatures" +} + +dependencies { + // lib dependencies from rootProject build.gradle.kts + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.activity.compose) + // Jetpack Compose Bill of Materials + implementation(platform(libs.androidx.compose.bom)) + // Jetpack Compose dependencies + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(project(":samples-lib")) + // Toolkit dependencies + implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) + implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) +} diff --git a/snap-to-features/proguard-rules.pro b/snap-to-features/proguard-rules.pro new file mode 100644 index 000000000..2f9dc5a47 --- /dev/null +++ b/snap-to-features/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.kts. +# +# 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 diff --git a/snap-to-features/src/main/AndroidManifest.xml b/snap-to-features/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c6647b46d --- /dev/null +++ b/snap-to-features/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/MainActivity.kt b/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/MainActivity.kt new file mode 100644 index 000000000..e4a81fe18 --- /dev/null +++ b/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/MainActivity.kt @@ -0,0 +1,51 @@ +/* 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.snaptofeatures + +import android.os.Bundle +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.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme +import com.esri.arcgismaps.sample.snaptofeatures.screens.MainScreen + +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.API_KEY) + + setContent { + SampleAppTheme { + SnapToFeaturesApp() + } + } + } + + @Composable + private fun SnapToFeaturesApp() { + Surface(color = MaterialTheme.colorScheme.background) { + MainScreen(sampleName = getString(R.string.app_name)) + } + } +} diff --git a/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/components/BottomSheetContents.kt b/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/components/BottomSheetContents.kt new file mode 100644 index 000000000..f5a60499d --- /dev/null +++ b/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/components/BottomSheetContents.kt @@ -0,0 +1,245 @@ +/* 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.snaptofeatures.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.arcgismaps.geometry.GeometryType +import com.arcgismaps.mapping.layers.FeatureLayer +import com.arcgismaps.mapping.view.geometryeditor.SnapSourceSettings +import com.esri.arcgismaps.sample.sampleslib.theme.SampleTypography + +/** + * Composable component to display the snapping configuration settings. + */ +@Composable +fun SnapSettings( + onSnappingChanged: (Boolean) -> Unit = { }, + isSnappingEnabled: Boolean, + snapSourceList: State>, + isSnapSourceEnabled: MutableList, + onSnapSourceChanged: (Boolean, Int) -> Unit = { _: Boolean, _: Int -> }, + onDismiss: () -> Unit = { } +) { + Surface( + Modifier + .background(MaterialTheme.colorScheme.background) + .verticalScroll(rememberScrollState()) + ) { + Column(Modifier.background(MaterialTheme.colorScheme.background)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp, 20.dp, 20.dp, 0.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + style = SampleTypography.titleMedium, + text = "Snapping", + color = MaterialTheme.colorScheme.primary + ) + TextButton( onClick = onDismiss ) + { Text(text = "Done") } + } + if (snapSourceList.value.isEmpty()) { + Surface( + modifier = Modifier.padding(20.dp), + tonalElevation = 1.dp, + shape = RoundedCornerShape(20.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) + ) { + Column( + modifier = Modifier.padding(14.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + style = SampleTypography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.weight(12f), + text = "No valid snap sources." + ) + } + } + } + } else { + Surface( + modifier = Modifier.padding(20.dp, 20.dp, 20.dp, 0.dp), + tonalElevation = 1.dp, + shape = RoundedCornerShape(20.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) + ) { + Column( + modifier = Modifier.padding(14.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "\t\tEnabled", + style = SampleTypography.bodyLarge, + ) + Switch( + checked = isSnappingEnabled, + onCheckedChange = { + onSnappingChanged(it) + } + ) + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp, 10.dp, 20.dp, 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + style = SampleTypography.titleMedium, + text = "Point Layers", + color = MaterialTheme.colorScheme.primary + ) + TextButton( + onClick = { + snapSourceList.value.forEachIndexed { index, snapSource -> + if ((snapSource.source as FeatureLayer).featureTable?.geometryType == GeometryType.Point) { + onSnapSourceChanged(true, index) + } + } + } + ) { + Text(text = "Enable All Sources") + } + } + Surface( + modifier = Modifier.padding(20.dp, 0.dp, 20.dp, 10.dp), + tonalElevation = 1.dp, + shape = RoundedCornerShape(20.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) + ) { + Column( + modifier = Modifier.padding(14.dp) + ) { + Column { + snapSourceList.value.forEachIndexed { index, snapSource -> + if ((snapSource.source as FeatureLayer).featureTable?.geometryType == GeometryType.Point) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.weight(0.5f), + text = "\t\t${(snapSource.source as FeatureLayer).name}" + ) + Switch( + checked = isSnapSourceEnabled[index], + onCheckedChange = { newValue -> + onSnapSourceChanged(newValue, index) + } + ) + } + } + } + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp, 0.dp, 20.dp, 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + style = SampleTypography.titleMedium, + text = "Polyline Layers", + color = MaterialTheme.colorScheme.primary + ) + TextButton( + onClick = { + snapSourceList.value.forEachIndexed { index, snapSource -> + if ((snapSource.source as FeatureLayer).featureTable?.geometryType == GeometryType.Polyline) { + onSnapSourceChanged(true, index) + } + } + } + ) { + Text(text = "Enable All Sources") + } + } + Surface( + modifier = Modifier.padding(20.dp, 0.dp, 20.dp, 10.dp), + tonalElevation = 1.dp, + shape = RoundedCornerShape(20.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) + ) { + Column( + modifier = Modifier.padding(14.dp) + ) { + Column { + snapSourceList.value.forEachIndexed { index, snapSource -> + if ((snapSource.source as FeatureLayer).featureTable?.geometryType == GeometryType.Polyline) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.weight(0.5f), + text = "\t\t${(snapSource.source as FeatureLayer).name}" + ) + Switch( + checked = isSnapSourceEnabled[index], + onCheckedChange = { newValue -> + onSnapSourceChanged(newValue, index) + } + ) + } + } + } + } + } + } + } + } + } +} diff --git a/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/components/MapViewModel.kt b/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/components/MapViewModel.kt new file mode 100644 index 000000000..fd2f339a1 --- /dev/null +++ b/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/components/MapViewModel.kt @@ -0,0 +1,235 @@ +/* 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.snaptofeatures.components + +import android.app.Application +import android.util.Log +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.unit.dp +import androidx.lifecycle.AndroidViewModel +import com.arcgismaps.LoadStatus +import com.arcgismaps.geometry.Envelope +import com.arcgismaps.geometry.GeometryType +import com.arcgismaps.geometry.Multipoint +import com.arcgismaps.geometry.Point +import com.arcgismaps.geometry.Polygon +import com.arcgismaps.geometry.Polyline +import com.arcgismaps.mapping.ArcGISMap +import com.arcgismaps.mapping.layers.FeatureTilingMode +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.arcgismaps.mapping.view.SingleTapConfirmedEvent +import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor +import com.arcgismaps.mapping.view.geometryeditor.GeometryEditorStyle +import com.arcgismaps.mapping.view.geometryeditor.SnapSourceSettings +import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy +import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class MapViewModel( + application: Application, + private val sampleCoroutineScope: CoroutineScope +) : AndroidViewModel(application) { + // create a map using a Uri + val map = ArcGISMap("https://www.arcgis.com/home/item.html?id=b95fe18073bc4f7788f0375af2bb445e") + + // create a graphic, graphic overlay, and geometry editor + private var identifiedGraphic = Graphic() + val graphicsOverlay = GraphicsOverlay() + val geometryEditor = GeometryEditor() + + // create a mapViewProxy that will be used to identify features in the MapView + // should also be passed to the composable MapView this mapViewProxy is associated with + val mapViewProxy = MapViewProxy() + + // create a messageDialogViewModel to handle dialog interactions + val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel() + + // create lists for displaying the snap sources in the bottom sheet and their symbology + private val _snapSourceSettingsList = MutableStateFlow(listOf()) + val snapSourceList: StateFlow> = _snapSourceSettingsList + + // boolean flags to track the state of the geometry editor, snap settings, and UI components + val isSnapSettingsButtonEnabled = mutableStateOf(false) + val isCreateButtonEnabled = mutableStateOf(false) + val isBottomSheetVisible = mutableStateOf(false) + val snappingCheckedState = mutableStateOf(false) + val snapSourceCheckedState = mutableStateListOf(false) + + /** + * Set the viewpoint and configure operational layers. + */ + init { + sampleCoroutineScope.launch { + // set the map's viewpoint to Naperville, Illinois + mapViewProxy.setViewpointCenter(Point(-9812798.0, 5126406.0), 2000.0) + // set the feature layer's feature tiling mode + map.loadSettings.featureTilingMode = FeatureTilingMode.EnabledWithFullResolutionWhenSupported + // load the map's operational layers + map.operationalLayers.forEach { layer -> + layer.load().onFailure { error -> + messageDialogVM.showMessageDialog( + error.message.toString(), + error.cause.toString() + ) + } + } + // enable the create and snap settings buttons + isCreateButtonEnabled.value = true + isSnapSettingsButtonEnabled.value = true + } + } + + /** + * Identifies the graphic at the tapped screen coordinate in the provided [singleTapConfirmedEvent] + * and starts the GeometryEditor using the graphic's geometry. Hide the BottomSheet on + * [singleTapConfirmedEvent]. + */ + fun identify(singleTapConfirmedEvent: SingleTapConfirmedEvent) { + sampleCoroutineScope.launch { + val graphicsResult = mapViewProxy.identifyGraphicsOverlays( + screenCoordinate = singleTapConfirmedEvent.screenCoordinate, + tolerance = 10.0.dp, + returnPopupsOnly = false + ).getOrNull() + + if (!geometryEditor.isStarted.value) { + if (graphicsResult != null) { + if (graphicsResult.isNotEmpty()) { + identifiedGraphic = graphicsResult[0].graphics[0] + identifiedGraphic.isSelected = true + identifiedGraphic.geometry?.let { geometryEditor.start(it) } + isCreateButtonEnabled.value = false + } + } + identifiedGraphic.geometry = null + } + } + dismissBottomSheet() + } + + /** + * Starts the GeometryEditor using the selected [GeometryType] from the DropDownMenu. + */ + fun startEditor(selectedGeometry: GeometryType) { + if (!geometryEditor.isStarted.value) { + geometryEditor.start(selectedGeometry) + isCreateButtonEnabled.value = false + } + } + + /** + * Stop the GeometryEditor and update the Graphic or GraphicsOverlay. + */ + fun stopEditor() { + if (identifiedGraphic.geometry != null) { + identifiedGraphic.geometry = geometryEditor.stop() + identifiedGraphic.isSelected = false + } else { + if (geometryEditor.isStarted.value) { + createNewGraphic() + } + } + isCreateButtonEnabled.value = true + } + + /** + * Create a Graphic from the GeometryEditor's geometry and add it to the GraphicsOverlay. + */ + private fun createNewGraphic() { + // stop the geometryEditor and store the geometry + val geometry = geometryEditor.stop() + val graphic = Graphic(geometry) + + // apply symbology to the graphic + when (geometry!!) { + is Point -> graphic.symbol = GeometryEditorStyle().vertexSymbol + is Multipoint -> graphic.symbol = GeometryEditorStyle().vertexSymbol + is Polyline -> graphic.symbol = GeometryEditorStyle().lineSymbol + is Polygon -> graphic.symbol = GeometryEditorStyle().fillSymbol + is Envelope -> graphic.symbol = GeometryEditorStyle().lineSymbol + } + // add the graphic to the graphicOverlay and unselect it + graphicsOverlay.graphics.add(graphic) + graphic.isSelected = false + } + + /** + * Undo the last event on the GeometryEditor. + */ + fun editorUndo() { + geometryEditor.undo() + } + + /** + * Delete the selected element and stop the geometry editor if there are no + * more elements. + */ + fun deleteSelection() { + if(geometryEditor.selectedElement.value != null) { + geometryEditor.deleteSelectedElement() + if(geometryEditor.geometry.value?.isEmpty == true) { + geometryEditor.stop() + isCreateButtonEnabled.value = true + } + } + } + + /** + * Update the snapSettings.isEnabled value using the [checkedValue] from the BottomSheet toggle. + */ + fun snappingEnabledStatus(checkedValue: Boolean) { + snappingCheckedState.value = checkedValue + geometryEditor.snapSettings.isEnabled = snappingCheckedState.value + } + + /** + * Update the sourceSettings at [index] enabled value to the [checkedValue] + * from the BottomSheet toggle. + */ + fun sourceEnabledStatus(checkedValue: Boolean, index: Int) { + snapSourceCheckedState[index] = checkedValue + geometryEditor.snapSettings.sourceSettings[index].isEnabled = snapSourceCheckedState[index] + } + + /** + * Dismiss the BottomSheet. + */ + fun dismissBottomSheet() { + isBottomSheetVisible.value = false + } + + /** + * Show the BottomSheet. + */ + fun showBottomSheet() { + if (geometryEditor.snapSettings.sourceSettings.isEmpty()) { + // synchronise the snap source collection with the Map's operational layers + geometryEditor.snapSettings.syncSourceSettings() + // update the lists used for the UI + _snapSourceSettingsList.value = geometryEditor.snapSettings.sourceSettings + geometryEditor.snapSettings.sourceSettings.forEach { + snapSourceCheckedState.add(it.isEnabled) + } + } + isBottomSheetVisible.value = true + } +} diff --git a/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/screens/MainScreen.kt b/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/screens/MainScreen.kt new file mode 100644 index 000000000..5575e751a --- /dev/null +++ b/snap-to-features/src/main/java/com/esri/arcgismaps/sample/snaptofeatures/screens/MainScreen.kt @@ -0,0 +1,183 @@ +/* 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.snaptofeatures.screens + +import android.app.Application +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Create +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.arcgismaps.mapping.view.MapViewInteractionOptions +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.arcgismaps.geometry.GeometryType +import com.arcgismaps.toolkit.geoviewcompose.MapView +import com.esri.arcgismaps.sample.sampleslib.components.BottomSheet +import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog +import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar +import com.esri.arcgismaps.sample.snaptofeatures.R +import com.esri.arcgismaps.sample.snaptofeatures.components.MapViewModel +import com.esri.arcgismaps.sample.snaptofeatures.components.SnapSettings + +/** + * Main screen layout for the sample app. + */ +@Composable +fun MainScreen (sampleName: String) { + // coroutineScope that will be cancelled when this call leaves the composition + val sampleCoroutineScope = rememberCoroutineScope() + // get the application property that will be used to construct MapViewModel + val sampleApplication = LocalContext.current.applicationContext as Application + // create a ViewModel to handle MapView interactions + val mapViewModel = remember { MapViewModel(sampleApplication, sampleCoroutineScope) } + // the collection of graphics overlays used by the MapView + val graphicsOverlayCollection = listOf(mapViewModel.graphicsOverlay) + + Scaffold( + content = { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it) + ) { + SampleTopAppBar(title = sampleName) + MapView( + modifier = Modifier + .fillMaxSize() + .weight(1f), + arcGISMap = mapViewModel.map, + geometryEditor = mapViewModel.geometryEditor, + graphicsOverlays = graphicsOverlayCollection, + mapViewProxy = mapViewModel.mapViewProxy, + mapViewInteractionOptions = MapViewInteractionOptions(isMagnifierEnabled = true), + onSingleTapConfirmed = mapViewModel::identify, + onPan = { mapViewModel.dismissBottomSheet() } + ) + Row( + modifier = Modifier + .padding(12.dp) + .fillMaxWidth(), + ) { + var expanded by remember { mutableStateOf(false) } + Box( + modifier = Modifier + ) { + IconButton( + enabled = mapViewModel.isCreateButtonEnabled.value, + onClick = { expanded = !expanded } + ) { + Icon(imageVector = Icons.Default.Create, contentDescription = "Start") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem( + text = { Text("Point") }, + onClick = { + mapViewModel.startEditor(GeometryType.Point) + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Multipoint") }, + onClick = { + mapViewModel.startEditor(GeometryType.Multipoint) + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Polyline") }, + onClick = { + mapViewModel.startEditor(GeometryType.Polyline) + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Polygon") }, + onClick = { + mapViewModel.startEditor(GeometryType.Polygon) + expanded = false + } + ) + } + } + val vector = ImageVector + IconButton(onClick = { mapViewModel.editorUndo() }) { + Icon(vector.vectorResource(R.drawable.undo), contentDescription = "Undo") + } + IconButton(onClick = { mapViewModel.stopEditor() }) { + Icon(vector.vectorResource(R.drawable.save), contentDescription = "Save") + } + IconButton(onClick = { mapViewModel.deleteSelection() }) { + Icon(Icons.Filled.Delete, contentDescription = "Delete") + } + Row ( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ){ + TextButton( + enabled = mapViewModel.isSnapSettingsButtonEnabled.value, + onClick = { mapViewModel.showBottomSheet() } + ) { Text(text = "Snap Settings") } + } + } + mapViewModel.messageDialogVM.apply { + if (dialogStatus) { + MessageDialog( + title = messageTitle, + description = messageDescription, + onDismissRequest = ::dismissDialog + ) + } + } + } + BottomSheet(isVisible = mapViewModel.isBottomSheetVisible.value) { + SnapSettings( + onSnappingChanged = mapViewModel::snappingEnabledStatus, + isSnappingEnabled = mapViewModel.snappingCheckedState.value, + snapSourceList = mapViewModel.snapSourceList.collectAsState(), + onSnapSourceChanged = mapViewModel::sourceEnabledStatus, + isSnapSourceEnabled = mapViewModel.snapSourceCheckedState + ) { mapViewModel.dismissBottomSheet() } + } + } + ) +} diff --git a/snap-to-features/src/main/res/drawable-v24/ic_launcher_foreground.xml b/snap-to-features/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..c7bd21dbd --- /dev/null +++ b/snap-to-features/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/snap-to-features/src/main/res/drawable/ic_launcher_background.xml b/snap-to-features/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..6d8cae103 --- /dev/null +++ b/snap-to-features/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snap-to-features/src/main/res/drawable/save.xml b/snap-to-features/src/main/res/drawable/save.xml new file mode 100644 index 000000000..b5d0e0b24 --- /dev/null +++ b/snap-to-features/src/main/res/drawable/save.xml @@ -0,0 +1,10 @@ + + + diff --git a/snap-to-features/src/main/res/drawable/undo.xml b/snap-to-features/src/main/res/drawable/undo.xml new file mode 100644 index 000000000..a3f745b76 --- /dev/null +++ b/snap-to-features/src/main/res/drawable/undo.xml @@ -0,0 +1,11 @@ + + + diff --git a/snap-to-features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/snap-to-features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6b78462d6 --- /dev/null +++ b/snap-to-features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/snap-to-features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/snap-to-features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6b78462d6 --- /dev/null +++ b/snap-to-features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/snap-to-features/src/main/res/mipmap-hdpi/ic_launcher.png b/snap-to-features/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..a2f590828 Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/snap-to-features/src/main/res/mipmap-hdpi/ic_launcher_round.png b/snap-to-features/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..1b5239980 Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/snap-to-features/src/main/res/mipmap-mdpi/ic_launcher.png b/snap-to-features/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..ff10afd6e Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/snap-to-features/src/main/res/mipmap-mdpi/ic_launcher_round.png b/snap-to-features/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..115a4c768 Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/snap-to-features/src/main/res/mipmap-xhdpi/ic_launcher.png b/snap-to-features/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..dcd3cd808 Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/snap-to-features/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/snap-to-features/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..459ca609d Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/snap-to-features/src/main/res/mipmap-xxhdpi/ic_launcher.png b/snap-to-features/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..8ca12fe02 Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/snap-to-features/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/snap-to-features/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..8e19b410a Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/snap-to-features/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/snap-to-features/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..b824ebdd4 Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/snap-to-features/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/snap-to-features/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..4c19a13c2 Binary files /dev/null and b/snap-to-features/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/snap-to-features/src/main/res/values/strings.xml b/snap-to-features/src/main/res/values/strings.xml new file mode 100644 index 000000000..aa21f1451 --- /dev/null +++ b/snap-to-features/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Snap geometry edits + diff --git a/tools/NewModuleScript.jar b/tools/NewModuleScript.jar index 2f1c4b460..d479fb9a5 100644 Binary files a/tools/NewModuleScript.jar and b/tools/NewModuleScript.jar differ diff --git a/tools/NewModuleScript/ComposeMapViewTemplate.kt b/tools/NewModuleScript/ComposeMapViewTemplate.kt deleted file mode 100644 index 61feabc16..000000000 --- a/tools/NewModuleScript/ComposeMapViewTemplate.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* Copyright 2023 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.displaycomposablemapview.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LifecycleOwner -import com.arcgismaps.mapping.view.MapView -import com.arcgismaps.mapping.view.SingleTapConfirmedEvent -import kotlinx.coroutines.launch - -/** - * Wraps the MapView in a Composable function. - */ -@Composable -fun ComposeMapView( - modifier: Modifier = Modifier, - mapViewModel: MapViewModel, - onSingleTap: (SingleTapConfirmedEvent) -> Unit = {} -) { - // get an instance of the current lifecycle owner - val lifecycleOwner = LocalLifecycleOwner.current - // collect the latest state of the MapViewState - val mapViewState by mapViewModel.mapViewState.collectAsState() - // create and add MapView to the activity lifecycle - val mapView = createMapViewInstance(lifecycleOwner) - - // wrap the MapView as an AndroidView - AndroidView( - modifier = modifier, - factory = { mapView }, - // recomposes the MapView on changes in the MapViewState - update = { mapView -> - mapView.apply { - map = mapViewState.arcGISMap - setViewpoint(mapViewState.viewpoint) - } - } - ) - - // launch coroutine functions in the composition's CoroutineContext - LaunchedEffect(Unit) { - launch { - mapView.onSingleTapConfirmed.collect { - onSingleTap(it) - } - } - } -} - -/** - * Create the MapView instance and add it to the Activity lifecycle - */ -@Composable -fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { - // create the MapView - val mapView = MapView(LocalContext.current) - // add the side effects for MapView composition - DisposableEffect(lifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(mapView) - onDispose { - lifecycleOwner.lifecycle.removeObserver(mapView) - } - } - return mapView -} diff --git a/tools/NewModuleScript/MainActivityTemplate.kt b/tools/NewModuleScript/MainActivityTemplate.kt index e7b6ae342..a6efcd0c3 100644 --- a/tools/NewModuleScript/MainActivityTemplate.kt +++ b/tools/NewModuleScript/MainActivityTemplate.kt @@ -48,8 +48,7 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background ) { MainScreen( - sampleName = getString(R.string.app_name), - application = application + sampleName = getString(R.string.app_name) ) } } diff --git a/tools/NewModuleScript/MainScreenTemplate.kt b/tools/NewModuleScript/MainScreenTemplate.kt index 8aeaadb2e..268cc9da1 100644 --- a/tools/NewModuleScript/MainScreenTemplate.kt +++ b/tools/NewModuleScript/MainScreenTemplate.kt @@ -17,13 +17,18 @@ package com.esri.arcgismaps.sample.displaycomposablemapview.screens import android.app.Application -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.esri.arcgismaps.sample.displaycomposablemapview.components.ComposeMapView +import androidx.compose.ui.platform.LocalContext +import com.arcgismaps.mapping.ArcGISMap +import com.arcgismaps.mapping.BasemapStyle +import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.displaycomposablemapview.components.MapViewModel import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar @@ -31,27 +36,26 @@ import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar * Main screen layout for the sample app */ @Composable -fun MainScreen(sampleName: String, application: Application) { +fun MainScreen(sampleName: String) { + val application = LocalContext.current.applicationContext as Application // create a ViewModel to handle MapView interactions val mapViewModel = MapViewModel(application) + val arcGISMap by remember { + mutableStateOf(ArcGISMap(BasemapStyle.ArcGISNavigationNight).apply { + initialViewpoint = mapViewModel.viewpoint.value + }) + } Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { - Column( - modifier = Modifier - .fillMaxSize() - .padding(it) - ) { - // composable function that wraps the MapView - ComposeMapView( - modifier = Modifier.fillMaxSize(), - mapViewModel = mapViewModel, - onSingleTap = { - mapViewModel.changeBasemap() - } - ) - } + MapView( + modifier = Modifier.fillMaxSize().padding(it), + arcGISMap = arcGISMap, + onSingleTapConfirmed = { + mapViewModel.changeBasemap() + } + ) } ) } diff --git a/tools/NewModuleScript/MapViewModelTemplate.kt b/tools/NewModuleScript/MapViewModelTemplate.kt index 36f3e9bd3..1f314f1f1 100644 --- a/tools/NewModuleScript/MapViewModelTemplate.kt +++ b/tools/NewModuleScript/MapViewModelTemplate.kt @@ -17,36 +17,19 @@ package com.esri.arcgismaps.sample.displaycomposablemapview.components import android.app.Application +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.AndroidViewModel -import com.arcgismaps.mapping.ArcGISMap -import com.arcgismaps.mapping.BasemapStyle import com.arcgismaps.mapping.Viewpoint -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update class MapViewModel(application: Application) : AndroidViewModel(application) { - // set the MapView mutable stateflow - val mapViewState = MutableStateFlow(MapViewState()) - + private val viewpointAmerica = Viewpoint(39.8, -98.6, 10e7) + private val viewpointAsia = Viewpoint(39.8, 98.6, 10e7) + var viewpoint = mutableStateOf(viewpointAmerica) /** * Switch between two basemaps */ fun changeBasemap() { - val newArcGISMap: ArcGISMap = - if (mapViewState.value.arcGISMap.basemap.value?.name.equals("ArcGIS:NavigationNight")) { - ArcGISMap(BasemapStyle.ArcGISStreets) - } else { - ArcGISMap(BasemapStyle.ArcGISNavigationNight) - } - mapViewState.update { it.copy(arcGISMap = newArcGISMap) } + viewpoint.value = + if (viewpoint.value == viewpointAmerica) viewpointAsia else viewpointAmerica } } - - -/** - * Data class that represents the MapView state - */ -data class MapViewState( // This would change based on each sample implementation - var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight), - var viewpoint: Viewpoint = Viewpoint(39.8, -98.6, 10e7) -) diff --git a/tools/NewModuleScript/src/main/java/ScriptMain.java b/tools/NewModuleScript/src/main/java/ScriptMain.java index 95192ada4..accdea380 100644 --- a/tools/NewModuleScript/src/main/java/ScriptMain.java +++ b/tools/NewModuleScript/src/main/java/ScriptMain.java @@ -99,7 +99,6 @@ private void createFilesAndFolders() { // Copy Kotlin template files to new sample File mainActivityTemplate = new File(samplesRepoPath + "/tools/NewModuleScript/MainActivityTemplate.kt"); - File composeMapViewTemplate = new File(samplesRepoPath + "/tools/NewModuleScript/ComposeMapViewTemplate.kt"); File mapViewModelTemplate = new File(samplesRepoPath + "/tools/NewModuleScript/MapViewModelTemplate.kt"); File mainScreenTemplate = new File(samplesRepoPath + "/tools/NewModuleScript/MainScreenTemplate.kt"); @@ -109,20 +108,17 @@ private void createFilesAndFolders() { Path source = Paths.get(packageDirectory+"/MainActivityTemplate.kt"); Files.move(source, source.resolveSibling("MainActivity.kt")); - File composeComponentsDir = new File(packageDirectory + "/components"); - composeComponentsDir.mkdirs(); - FileUtils.copyFileToDirectory(composeMapViewTemplate, composeComponentsDir); - source = Paths.get(composeComponentsDir+"/ComposeMapViewTemplate.kt"); - Files.move(source, source.resolveSibling("ComposeMapView.kt")); + File componentsDir = new File(packageDirectory + "/components"); + componentsDir.mkdirs(); - FileUtils.copyFileToDirectory(mapViewModelTemplate, composeComponentsDir); - source = Paths.get(composeComponentsDir+"/MapViewModelTemplate.kt"); + FileUtils.copyFileToDirectory(mapViewModelTemplate, componentsDir); + source = Paths.get(componentsDir+"/MapViewModelTemplate.kt"); Files.move(source, source.resolveSibling("MapViewModel.kt")); - composeComponentsDir = new File(packageDirectory + "/screens"); - composeComponentsDir.mkdirs(); - FileUtils.copyFileToDirectory(mainScreenTemplate, composeComponentsDir); - source = Paths.get(composeComponentsDir+"/MainScreenTemplate.kt"); + componentsDir = new File(packageDirectory + "/screens"); + componentsDir.mkdirs(); + FileUtils.copyFileToDirectory(mainScreenTemplate, componentsDir); + source = Paths.get(componentsDir+"/MainScreenTemplate.kt"); Files.move(source, source.resolveSibling("MainScreen.kt")); } catch (IOException e) { e.printStackTrace(); @@ -201,18 +197,6 @@ private void updateSampleContent() { exitProgram(e); } - //Update ComposeMapView.kt - file = new File(samplesRepoPath + "/" + sampleWithHyphen + "/src/main/java/com/esri/arcgismaps/sample/"+sampleWithoutSpaces+"/components/ComposeMapView.kt"); - try { - String fileContent = FileUtils.readFileToString(file, StandardCharsets.UTF_8); - fileContent = fileContent.replace("Copyright 2023", "Copyright " + Calendar.getInstance().get(Calendar.YEAR)); - fileContent = fileContent.replace("sample.displaycomposablemapview", "sample." + sampleWithoutSpaces); - FileUtils.write(file,fileContent, StandardCharsets.UTF_8); - } catch (IOException e) { - e.printStackTrace(); - exitProgram(e); - } - //Update MapViewModel.kt file = new File(samplesRepoPath + "/" + sampleWithHyphen + "/src/main/java/com/esri/arcgismaps/sample/"+sampleWithoutSpaces+"/components/MapViewModel.kt"); try {