Skip to content

Commit ae94e4a

Browse files
authored
Style point with distance composite scene symbol (#426)
1 parent ab3ed8b commit ae94e4a

File tree

11 files changed

+474
-1
lines changed

11 files changed

+474
-1
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[versions]
22

33
# ArcGIS Maps SDK for Kotlin version
4-
arcgisMapsKotlinVersion = "300.0.0-4719"
4+
arcgisMapsKotlinVersion = "300.0.0-4738"
55

66
### Android versions
77
androidGradlePlugin = "8.12.1"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Style point with distance composite scene symbol
2+
3+
Change a graphic's symbol based on the camera's proximity to it.
4+
5+
![Image of style point with distance composite scene symbol](style-point-with-distance-composite-scene-symbol.png)
6+
7+
## Use case
8+
9+
When showing dense datasets, it is beneficial to reduce the detail of individual points when zooming out to avoid visual clutter and to avoid data points overlapping and obscuring each other.
10+
11+
## How to use the sample
12+
13+
The sample starts looking at a plane. Zoom out from the plane to see it turn into a cone. Keeping zooming out and it will turn into a point.
14+
15+
## How it works
16+
17+
1. Create a `GraphicsOverlay` object and add it to a `SceneView`.
18+
2. Create a `DistanceCompositeSceneSymbol` object.
19+
3. Create `DistanceSymbolRange` objects specifying a `Symbol` and the min and max distance within which the symbol should be visible.
20+
4. Add the ranges to the range collection of the distance composite scene symbol.
21+
5. Create a `Graphic` object with the distance composite scene symbol at a location and add it to the graphics overlay.
22+
23+
## Relevant API
24+
25+
* DistanceCompositeSceneSymbol
26+
* DistanceSymbolRange
27+
* OrbitGeoElementCameraController
28+
29+
## Tags
30+
31+
3D, data, graphic
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"category": "Scenes",
3+
"description": "Change a graphic's symbol based on the camera's proximity to it.",
4+
"formal_name": "StylePointWithDistanceCompositeSceneSymbol",
5+
"ignore": false,
6+
"images": [
7+
"style-point-with-distance-composite-scene-symbol.png"
8+
],
9+
"keywords": [
10+
"3D",
11+
"data",
12+
"graphic",
13+
"DistanceCompositeSceneSymbol",
14+
"DistanceSymbolRange",
15+
"OrbitGeoElementCameraController"
16+
],
17+
"language": "kotlin",
18+
"redirect_from": "",
19+
"relevant_apis": [
20+
"DistanceCompositeSceneSymbol",
21+
"DistanceSymbolRange",
22+
"OrbitGeoElementCameraController"
23+
],
24+
"snippets": [
25+
"src/main/java/com/esri/arcgismaps/sample/stylepointwithdistancecompositescenesymbol/components/StylePointWithDistanceCompositeSceneSymbolViewModel.kt",
26+
"src/main/java/com/esri/arcgismaps/sample/stylepointwithdistancecompositescenesymbol/MainActivity.kt",
27+
"src/main/java/com/esri/arcgismaps/sample/stylepointwithdistancecompositescenesymbol/DownloadActivity.kt",
28+
"src/main/java/com/esri/arcgismaps/sample/stylepointwithdistancecompositescenesymbol/screens/StylePointWithDistanceCompositeSceneSymbolScreen.kt"
29+
],
30+
"title": "Style point with distance composite scene symbol"
31+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins {
2+
alias(libs.plugins.arcgismaps.android.library)
3+
alias(libs.plugins.arcgismaps.android.library.compose)
4+
alias(libs.plugins.arcgismaps.kotlin.sample)
5+
alias(libs.plugins.gradle.secrets)
6+
}
7+
8+
secrets {
9+
// this file doesn't contain secrets, it just provides defaults which can be committed into git.
10+
defaultPropertiesFileName = "secrets.defaults.properties"
11+
}
12+
13+
android {
14+
namespace = "com.esri.arcgismaps.sample.stylepointwithdistancecompositescenesymbol"
15+
buildFeatures {
16+
buildConfig = true
17+
}
18+
}
19+
20+
dependencies {
21+
// Only module specific dependencies needed here
22+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.INTERNET" />
5+
6+
<application>
7+
<activity
8+
android:name=".MainActivity"
9+
android:exported="true"
10+
android:label="@string/style_point_with_distance_composite_scene_symbol_app_name">
11+
12+
</activity>
13+
<activity
14+
android:name=".DownloadActivity"
15+
android:exported="true"
16+
android:label="@string/style_point_with_distance_composite_scene_symbol_app_name" />
17+
</application>
18+
19+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* Copyright 2025 Esri
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
package com.esri.arcgismaps.sample.stylepointwithdistancecompositescenesymbol
18+
19+
import android.content.Intent
20+
import android.os.Bundle
21+
import com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
22+
23+
class DownloadActivity : DownloaderActivity() {
24+
override fun onCreate(savedInstanceState: Bundle?) {
25+
super.onCreate(savedInstanceState)
26+
downloadAndStartSample(
27+
Intent(this, MainActivity::class.java),
28+
getString(R.string.style_point_with_distance_composite_scene_symbol_app_name),
29+
listOf(
30+
"https://www.arcgis.com/home/item.html?id=681d6f7694644709a7c830ec57a2d72b"
31+
)
32+
)
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/* Copyright 2025 Esri
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
package com.esri.arcgismaps.sample.stylepointwithdistancecompositescenesymbol
18+
19+
import android.os.Bundle
20+
import androidx.activity.ComponentActivity
21+
import androidx.activity.compose.setContent
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.Surface
24+
import androidx.compose.runtime.Composable
25+
import com.arcgismaps.ApiKey
26+
import com.arcgismaps.ArcGISEnvironment
27+
import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
28+
import com.esri.arcgismaps.sample.stylepointwithdistancecompositescenesymbol.screens.StylePointWithDistanceCompositeSceneSymbolScreen
29+
30+
class MainActivity : ComponentActivity() {
31+
32+
override fun onCreate(savedInstanceState: Bundle?) {
33+
super.onCreate(savedInstanceState)
34+
// authentication with an API key or named user is
35+
// required to access basemaps and other location services
36+
ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
37+
38+
setContent {
39+
SampleAppTheme {
40+
StylePointWithDistanceCompositeSceneSymbolApp()
41+
}
42+
}
43+
}
44+
45+
@Composable
46+
private fun StylePointWithDistanceCompositeSceneSymbolApp() {
47+
Surface(color = MaterialTheme.colorScheme.background) {
48+
StylePointWithDistanceCompositeSceneSymbolScreen(
49+
sampleName = getString(R.string.style_point_with_distance_composite_scene_symbol_app_name)
50+
)
51+
}
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/* Copyright 2025 Esri
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
package com.esri.arcgismaps.sample.stylepointwithdistancecompositescenesymbol.components
18+
19+
import android.app.Application
20+
import androidx.lifecycle.AndroidViewModel
21+
import androidx.lifecycle.viewModelScope
22+
import com.arcgismaps.Color
23+
import com.arcgismaps.geometry.Point
24+
import com.arcgismaps.geometry.SpatialReference
25+
import com.arcgismaps.mapping.ArcGISScene
26+
import com.arcgismaps.mapping.ArcGISTiledElevationSource
27+
import com.arcgismaps.mapping.BasemapStyle
28+
import com.arcgismaps.mapping.Viewpoint
29+
import com.arcgismaps.mapping.view.GraphicsOverlay
30+
import com.arcgismaps.mapping.view.Graphic
31+
import com.arcgismaps.mapping.symbology.DistanceCompositeSceneSymbol
32+
import com.arcgismaps.mapping.symbology.DistanceSymbolRange
33+
import com.arcgismaps.mapping.symbology.ModelSceneSymbol
34+
import com.arcgismaps.mapping.symbology.SimpleMarkerSceneSymbol
35+
import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol
36+
import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle
37+
import com.arcgismaps.mapping.view.Camera
38+
import com.arcgismaps.mapping.view.SurfacePlacement
39+
import com.arcgismaps.mapping.view.OrbitGeoElementCameraController
40+
import com.arcgismaps.toolkit.geoviewcompose.SceneViewProxy
41+
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
42+
import com.esri.arcgismaps.sample.stylepointwithdistancecompositescenesymbol.R
43+
import kotlinx.coroutines.launch
44+
import kotlinx.coroutines.flow.MutableStateFlow
45+
import kotlinx.coroutines.flow.asStateFlow
46+
import java.io.File
47+
48+
/**
49+
* ViewModel for the sample. Builds an ArcGISScene, a distance composite scene symbol and an orbit
50+
* camera controller.
51+
*/
52+
class StylePointWithDistanceCompositeSceneSymbolViewModel(app: Application) :
53+
AndroidViewModel(app) {
54+
55+
// Lazy provision path for reference offline resources.
56+
private val provisionPath: String by lazy {
57+
app.getExternalFilesDir(null)?.path.toString() +
58+
File.separator +
59+
app.getString(R.string.style_point_with_distance_composite_scene_symbol_app_name)
60+
}
61+
62+
// Construct the model file URI from the provision path.
63+
private val bristolModelUri
64+
get() = "$provisionPath${File.separator}Bristol.dae"
65+
66+
// The model (3D) graphic target.
67+
private val planePosition = Point(
68+
x = -2.708, y = 56.096, z = 5000.0,
69+
spatialReference = SpatialReference.wgs84()
70+
)
71+
72+
// Distance composite symbol with three ranges (detailed model, simplified model, and a simple circle).
73+
private val distanceCompositeSymbol: DistanceCompositeSceneSymbol by lazy {
74+
// Close-up: Detailed 3D model.
75+
val planeModel = ModelSceneSymbol(
76+
uri = bristolModelUri,
77+
scale = 100.0F
78+
)
79+
80+
// Mid-distance: Simple cone symbol.
81+
val coneSymbol = SimpleMarkerSceneSymbol.cone(
82+
color = Color.red,
83+
diameter = 200.0,
84+
height = 600.0
85+
)
86+
87+
// Far-distance: Simple 2D symbol.
88+
val circleSymbol = SimpleMarkerSymbol(
89+
style = SimpleMarkerSymbolStyle.Circle,
90+
color = Color.red,
91+
size = 10f
92+
)
93+
94+
DistanceCompositeSceneSymbol().apply {
95+
// Close-up: Detailed 3D model.
96+
ranges.add(
97+
DistanceSymbolRange(
98+
symbol = planeModel,
99+
minDistance = null,
100+
maxDistance = 10000.0
101+
)
102+
)
103+
// Mid-distance: Simple cone symbol.
104+
ranges.add(
105+
DistanceSymbolRange(
106+
symbol = coneSymbol,
107+
minDistance = 10001.0,
108+
maxDistance = 30000.0
109+
)
110+
)
111+
// Far-distance: Simple 2D symbol.
112+
ranges.add(
113+
DistanceSymbolRange(
114+
symbol = circleSymbol,
115+
minDistance = 30001.0,
116+
maxDistance = null
117+
)
118+
)
119+
}
120+
}
121+
122+
// Graphic for the plane using the distance composite symbol.
123+
private val planeGraphic by lazy {
124+
Graphic(
125+
geometry = planePosition,
126+
symbol = distanceCompositeSymbol
127+
)
128+
}
129+
130+
// Orbit camera controller that targets the plane graphic.
131+
val orbitCameraController: OrbitGeoElementCameraController by lazy {
132+
OrbitGeoElementCameraController(planeGraphic, 4000.0).apply {
133+
setCameraPitchOffset(80.0)
134+
setCameraHeadingOffset(-30.0)
135+
}
136+
}
137+
138+
// Scene using imagery basemap and a world elevation service.
139+
val arcGISScene = ArcGISScene(BasemapStyle.ArcGISImagery).apply {
140+
// Set the tiled elevation source
141+
baseSurface.elevationSources += ArcGISTiledElevationSource(
142+
uri = "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer"
143+
)
144+
// Set the initial viewpoint
145+
val camera = Camera(
146+
latitude = 56.096,
147+
longitude = -2.708,
148+
altitude = 5000.0,
149+
heading = -30.0,
150+
pitch = 80.0,
151+
roll = 0.0
152+
)
153+
initialViewpoint = Viewpoint(boundingGeometry = camera.location, camera = camera)
154+
}
155+
156+
// Graphics overlay to display the plane graphic using a distance composite symbol.
157+
private val graphicsOverlay by lazy {
158+
GraphicsOverlay(graphics = listOf(planeGraphic)).apply {
159+
sceneProperties.surfacePlacement = SurfacePlacement.Relative
160+
}
161+
}
162+
163+
// Expose the graphics overlay list for the SceneView
164+
val graphicsOverlays = listOf(graphicsOverlay)
165+
166+
// SceneView proxy to hand to the composable SceneView
167+
val sceneViewProxy = SceneViewProxy()
168+
169+
// Flow exposing the distance between camera and target (meters).
170+
private val _cameraDistanceMeters = MutableStateFlow(0.0)
171+
val cameraDistanceMeters = _cameraDistanceMeters.asStateFlow()
172+
173+
// Message dialog VM to surface errors.
174+
val messageDialogVM = MessageDialogViewModel()
175+
176+
init {
177+
// Load the scene and surface errors via the message dialog VM.
178+
viewModelScope.launch {
179+
arcGISScene.load().onFailure { messageDialogVM.showMessageDialog(it) }
180+
}
181+
182+
// Collect the cameraDistance flow exposed by the controller
183+
viewModelScope.launch {
184+
// Update flow to keep UI reactive.
185+
orbitCameraController.cameraDistance.collect { distance ->
186+
_cameraDistanceMeters.value = distance
187+
}
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)