Skip to content

Commit 1af4af5

Browse files
authored
Adds sample "Apply style to WMS layer" (#428)
1 parent c2fa48d commit 1af4af5

File tree

9 files changed

+378
-0
lines changed

9 files changed

+378
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Apply style to WMS layer
2+
3+
Change the style of a Web Map Service (WMS) layer.
4+
5+
![Image of apply style to WMS layer](apply-style-to-wms-layer.png)
6+
7+
## Use case
8+
9+
Layers hosted on WMS may have different pre-set styles available to apply to them. Swapping between these styles can help during visual examination of the data. For example, increasing the contrast of satellite images can help in identifying urban and agricultural areas within forested areas.
10+
11+
## How to use the sample
12+
13+
Once the layer loads, the toggle button will be enabled. Click it to toggle between the first and second styles of the WMS layer.
14+
15+
## How it works
16+
17+
1. Create a `WmsLayer` specifying the URL of the service and the layer names you want `WmsLayer(url, names)`.
18+
2. When the layer is done loading, get it's list of style strings using `wmsLayer.layerInfos.firstOrNull()?.styles`.
19+
3. Set one of the styles using `wmsSublayer.currentStyle = styleString`.
20+
21+
## Relevant API
22+
23+
* WmsLayer
24+
* WmsSublayer
25+
* WmsSublayerInfo
26+
27+
## About the data
28+
29+
This sample uses a public service managed by the State of Minnesota and provides composite imagery for the state and the surrounding areas.
30+
31+
## Tags
32+
33+
imagery, styles, visualization, WMS
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"category": "Visualization",
3+
"description": "Change the style of a Web Map Service (WMS) layer.",
4+
"formal_name": "ApplyStyleToWmsLayer",
5+
"ignore": false,
6+
"images": [
7+
"apply-style-to-wms-layer.png"
8+
],
9+
"keywords": [
10+
"WMS",
11+
"imagery",
12+
"styles",
13+
"visualization",
14+
"WmsLayer",
15+
"WmsSublayer",
16+
"WmsSublayerInfo"
17+
],
18+
"language": "kotlin",
19+
"redirect_from": "",
20+
"relevant_apis": [
21+
"WmsLayer",
22+
"WmsSublayer",
23+
"WmsSublayerInfo"
24+
],
25+
"snippets": [
26+
"src/main/java/com/esri/arcgismaps/sample/applystyletowmslayer/components/ApplyStyleToWMSLayerViewModel.kt",
27+
"src/main/java/com/esri/arcgismaps/sample/applystyletowmslayer/MainActivity.kt",
28+
"src/main/java/com/esri/arcgismaps/sample/applystyletowmslayer/screens/ApplyStyleToWMSLayerScreen.kt"
29+
],
30+
"title": "Apply style to WMS layer"
31+
}
299 KB
Loading
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.applystyletowmslayer"
15+
buildFeatures {
16+
buildConfig = true
17+
}
18+
}
19+
20+
dependencies {
21+
// Only module specific dependencies needed here
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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/apply_style_to_wms_layer_app_name">
11+
12+
</activity>
13+
</application>
14+
</manifest>
Lines changed: 53 additions & 0 deletions
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.applystyletowmslayer
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.applystyletowmslayer.screens.ApplyStyleToWmsLayerScreen
28+
import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
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+
ApplyStyleToWMSLayerApp()
41+
}
42+
}
43+
}
44+
45+
@Composable
46+
private fun ApplyStyleToWMSLayerApp() {
47+
Surface(color = MaterialTheme.colorScheme.background) {
48+
ApplyStyleToWmsLayerScreen(
49+
sampleName = getString(R.string.apply_style_to_wms_layer_app_name)
50+
)
51+
}
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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.applystyletowmslayer.components
18+
19+
import android.app.Application
20+
import androidx.lifecycle.AndroidViewModel
21+
import androidx.lifecycle.viewModelScope
22+
import com.arcgismaps.geometry.SpatialReference
23+
import com.arcgismaps.mapping.ArcGISMap
24+
import com.arcgismaps.mapping.Viewpoint
25+
import com.arcgismaps.mapping.layers.WmsLayer
26+
import com.arcgismaps.mapping.layers.WmsSublayer
27+
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
28+
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
29+
import kotlinx.coroutines.flow.MutableStateFlow
30+
import kotlinx.coroutines.flow.asStateFlow
31+
import kotlinx.coroutines.launch
32+
33+
class ApplyStyleToWmsLayerViewModel(app: Application) : AndroidViewModel(app) {
34+
// WMS layer displaying Minnesota's county boundaries with multiple styles.
35+
private val wmsLayer = WmsLayer(
36+
url = "https://imageserver.gisdata.mn.gov/cgi-bin/mncomp?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities",
37+
layerNames = listOf("mncomp")
38+
)
39+
40+
// Map used by the MapView.
41+
val arcGISMap = ArcGISMap(SpatialReference(wkid = 26915)).apply {
42+
// apply a min scale
43+
minScale = 7_000_000.0
44+
// Add the WMS layer to the map's operational layers
45+
operationalLayers.add(wmsLayer)
46+
}
47+
48+
// MapViewProxy used to perform viewpoint operations.
49+
val mapViewProxy = MapViewProxy()
50+
51+
// Message dialog ViewModel for error reporting.
52+
val messageDialogVM = MessageDialogViewModel()
53+
54+
// Flow of available WMS styles.
55+
private val _styles = MutableStateFlow<List<String>>(emptyList())
56+
val styles = _styles.asStateFlow()
57+
58+
// Selected style index.
59+
private val _selectedStyleIndex = MutableStateFlow(0)
60+
val selectedStyleIndex = _selectedStyleIndex.asStateFlow()
61+
62+
// The WMS sublayer for which we will change the style.
63+
private var wmsSublayer: WmsSublayer? = null
64+
65+
init {
66+
viewModelScope.launch {
67+
createAndLoadWmsLayer()
68+
}
69+
}
70+
71+
/**
72+
* Creates the WMS layer, loads it, zooms to its full extent, and exposes its styles.
73+
*/
74+
private suspend fun createAndLoadWmsLayer() {
75+
wmsLayer.load().onSuccess {
76+
// Zoom to the full extent of the WMS layer, if available
77+
wmsLayer.fullExtent?.let { fullExtent ->
78+
mapViewProxy.setViewpointAnimated(Viewpoint(boundingGeometry = fullExtent))
79+
}
80+
81+
// Get the first WMS sublayer
82+
wmsSublayer = wmsLayer.sublayers.value.firstOrNull() as? WmsSublayer
83+
84+
// Obtain available styles from the WMS layer info
85+
val styles = wmsLayer.layerInfos.firstOrNull()?.styles ?: emptyList()
86+
if (styles.isEmpty()) {
87+
messageDialogVM.showMessageDialog("No styles found for the WMS layer.")
88+
return@onSuccess
89+
}
90+
91+
_styles.value = styles
92+
// Set initial selection to the first available style
93+
_selectedStyleIndex.value = 0
94+
wmsSublayer?.currentStyle = styles[0]
95+
}.onFailure { error ->
96+
messageDialogVM.showMessageDialog(error)
97+
}
98+
}
99+
100+
/**
101+
* Updates the selected style by index and applies it to the WMS sublayer.
102+
*/
103+
fun updateSelectedStyle(index: Int) {
104+
val styles = _styles.value
105+
if (index in styles.indices) {
106+
_selectedStyleIndex.value = index
107+
wmsSublayer?.currentStyle = styles[index]
108+
}
109+
}
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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.applystyletowmslayer.screens
18+
19+
import androidx.compose.foundation.layout.Arrangement
20+
import androidx.compose.foundation.layout.Column
21+
import androidx.compose.foundation.layout.fillMaxSize
22+
import androidx.compose.foundation.layout.fillMaxWidth
23+
import androidx.compose.foundation.layout.padding
24+
import androidx.compose.material3.OutlinedCard
25+
import androidx.compose.material3.Scaffold
26+
import androidx.compose.material3.Text
27+
import androidx.compose.runtime.Composable
28+
import androidx.compose.runtime.getValue
29+
import androidx.compose.ui.Alignment
30+
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.unit.dp
32+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
33+
import androidx.lifecycle.viewmodel.compose.viewModel
34+
import com.arcgismaps.toolkit.geoviewcompose.MapView
35+
import com.esri.arcgismaps.sample.applystyletowmslayer.components.ApplyStyleToWmsLayerViewModel
36+
import com.esri.arcgismaps.sample.sampleslib.components.DropDownMenuBox
37+
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog
38+
import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
39+
40+
/**
41+
* Main screen layout for the Apply style to WMS layer sample.
42+
*/
43+
@Composable
44+
fun ApplyStyleToWmsLayerScreen(sampleName: String) {
45+
val mapViewModel: ApplyStyleToWmsLayerViewModel = viewModel()
46+
47+
// Observe WMS styles and selected index from the ViewModel
48+
val styles by mapViewModel.styles.collectAsStateWithLifecycle()
49+
val selectedStyleIndex by mapViewModel.selectedStyleIndex.collectAsStateWithLifecycle()
50+
51+
Scaffold(
52+
topBar = { SampleTopAppBar(title = sampleName) },
53+
content = { paddingValues ->
54+
Column(
55+
modifier = Modifier
56+
.fillMaxSize()
57+
.padding(paddingValues)
58+
) {
59+
MapView(
60+
modifier = Modifier
61+
.fillMaxSize()
62+
.weight(1f),
63+
arcGISMap = mapViewModel.arcGISMap,
64+
mapViewProxy = mapViewModel.mapViewProxy
65+
)
66+
67+
// Style Picker UI
68+
OutlinedCard(
69+
modifier = Modifier
70+
.fillMaxWidth()
71+
.padding(12.dp)
72+
) {
73+
Column(
74+
modifier = Modifier
75+
.fillMaxWidth()
76+
.padding(12.dp),
77+
verticalArrangement = Arrangement.spacedBy(8.dp),
78+
horizontalAlignment = Alignment.CenterHorizontally
79+
) {
80+
Text(text = "Style")
81+
82+
val styleTitles = styles.map { styleId ->
83+
when (styleId.lowercase()) {
84+
"default" -> "Default"
85+
"stretch" -> "Contrast Stretch"
86+
else -> styleId.ifBlank { "Unknown" }
87+
}
88+
}
89+
90+
DropDownMenuBox(
91+
textFieldValue = styleTitles.getOrNull(selectedStyleIndex) ?: "",
92+
textFieldLabel = "Choose WMS style",
93+
dropDownItemList = styleTitles,
94+
onIndexSelected = mapViewModel::updateSelectedStyle
95+
)
96+
}
97+
}
98+
}
99+
100+
// Display a dialog if the sample encounters an error
101+
mapViewModel.messageDialogVM.apply {
102+
if (dialogStatus) {
103+
MessageDialog(
104+
title = messageTitle,
105+
description = messageDescription,
106+
onDismissRequest = ::dismissDialog
107+
)
108+
}
109+
}
110+
}
111+
)
112+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<resources>
2+
<string name="apply_style_to_wms_layer_app_name">Apply style to WMS layer</string>
3+
</resources>

0 commit comments

Comments
 (0)