diff --git a/samples/trace-utility-network/src/main/java/com/esri/arcgismaps/sample/traceutilitynetwork/components/TraceUtilityNetworkViewModel.kt b/samples/trace-utility-network/src/main/java/com/esri/arcgismaps/sample/traceutilitynetwork/components/TraceUtilityNetworkViewModel.kt index fe6bba4c2..c2dc21d65 100644 --- a/samples/trace-utility-network/src/main/java/com/esri/arcgismaps/sample/traceutilitynetwork/components/TraceUtilityNetworkViewModel.kt +++ b/samples/trace-utility-network/src/main/java/com/esri/arcgismaps/sample/traceutilitynetwork/components/TraceUtilityNetworkViewModel.kt @@ -43,7 +43,6 @@ import com.arcgismaps.mapping.symbology.UniqueValueRenderer import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.mapping.view.IdentifyLayerResult -import com.arcgismaps.mapping.view.ScreenCoordinate import com.arcgismaps.mapping.view.SingleTapConfirmedEvent import com.arcgismaps.portal.Portal import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy @@ -56,37 +55,35 @@ import com.arcgismaps.utilitynetworks.UtilityTier import com.arcgismaps.utilitynetworks.UtilityTraceParameters import com.arcgismaps.utilitynetworks.UtilityTraceResult import com.arcgismaps.utilitynetworks.UtilityTraceType -import kotlinx.coroutines.flow.Flow +import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel(application) { + // Create a message dialog view model for handling error messages + val messageDialogVM = MessageDialogViewModel() + // The textual hint shown to the user private val _hint = MutableStateFlow(null) - val hint: Flow = _hint.asStateFlow() - - // The last element that was added (start/barrier) - private val _lastAddedElement = MutableStateFlow(null) - val lastAddedElement: Flow = _lastAddedElement.asStateFlow() - - // The last tap in screen coords + map coords - private val _lastSingleTap = MutableStateFlow?>(null) - val lastSingleTap: Flow?> = _lastSingleTap.asStateFlow() + val hint = _hint.asStateFlow() // The pending trace parameters private val _pendingTraceParameters = MutableStateFlow(null) - val pendingTraceParameters: Flow = - _pendingTraceParameters.asStateFlow() + val pendingTraceParameters = _pendingTraceParameters.asStateFlow() // Whether the terminal selector is open private val _terminalSelectorIsOpen = MutableStateFlow(false) - val terminalSelectorIsOpen: Flow = _terminalSelectorIsOpen.asStateFlow() + val terminalSelectorIsOpen = _terminalSelectorIsOpen.asStateFlow() + + // Current trace state + private val _traceState = MutableStateFlow(TraceState.None) + val traceState = _traceState - // Current trace “activity” state - private val _tracingActivity = MutableStateFlow(TracingActivity.None) - val tracingActivity: Flow = _tracingActivity + // Is trace utility network enabled + private val _canTrace = MutableStateFlow(false) + val canTrace = _canTrace.asStateFlow() // An ArcGISMap holding the UtilityNetwork and operational layers val arcGISMap = ArcGISMap( @@ -115,22 +112,24 @@ class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel( get() = electricDistribution?.getTier("Medium Voltage Radial") // Barrier / Start points overlay - val pointsOverlay: GraphicsOverlay by lazy { - GraphicsOverlay().apply { - val barrierUniqueValue = UniqueValue( - "Barrier", - "", // description - SimpleMarkerSymbol(SimpleMarkerSymbolStyle.X, Color.red, 20f), - listOf(PointType.Barrier.name) - ) + val pointsOverlay = GraphicsOverlay().apply { + val barrierUniqueValue = UniqueValue( + description = "Barrier", + label = "", + symbol = SimpleMarkerSymbol( + style = SimpleMarkerSymbolStyle.X, + color = Color.red, + size = 20f + ), + values = listOf(PointType.Barrier.name) + ) - // UniqueValueRenderer to differentiate barrier vs. start - renderer = UniqueValueRenderer().apply { - fieldNames.add("PointType") - uniqueValues.add(barrierUniqueValue) - defaultSymbol = SimpleMarkerSymbol(SimpleMarkerSymbolStyle.Cross, Color.green, 20f) - } - } + // UniqueValueRenderer to differentiate barrier vs. start + renderer = UniqueValueRenderer( + fieldNames = listOf("PointType"), + uniqueValues = listOf(barrierUniqueValue), + defaultSymbol = SimpleMarkerSymbol(SimpleMarkerSymbolStyle.Cross, Color.green, 20f) + ) } companion object { @@ -140,8 +139,10 @@ class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel( private const val SAMPLE_PORTAL_URL = "$SAMPLE_SERVER_7/portal/sharing/rest" private const val FEATURE_SERVICE_URL = SAMPLE_SERVER_7 + "/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer" + // The portal item ID for Naperville’s electrical network private const val NAPERVILLE_ELECTRICAL_NETWORK_ITEM_ID = "471eb0bf37074b1fbb972b1da70fb310" + // Feature layer IDs relevant to this sample private val FEATURE_LAYER_IDS = listOf("3", "0") @@ -162,57 +163,65 @@ class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel( password = PASSWORD ).onSuccess { tokenCredential -> ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(tokenCredential) - arcGISMap.load().onSuccess { - // Load the network - network.load().onSuccess { - - // Once loaded, remove all operational layers - arcGISMap.operationalLayers.clear() - - // Add relevant layers - FEATURE_LAYER_IDS.forEach { layerId -> - val table = ServiceFeatureTable("$FEATURE_SERVICE_URL/$layerId") - val layer = FeatureLayer.createWithFeatureTable(table) - if (layerId == "3") { - // Customize rendering for layer with ID 3 - layer.renderer = UniqueValueRenderer().apply { - fieldNames.add("ASSETGROUP") - uniqueValues.add( - UniqueValue( - description = "Low voltage", - label = "", - symbol = SimpleLineSymbol( - style = SimpleLineSymbolStyle.Dash, - color = Color.cyan, - width = 3f - ), - values = listOf(3) - ) - ) - uniqueValues.add( - UniqueValue( - description = "Medium voltage", - label = "", - symbol = SimpleLineSymbol( - style = SimpleLineSymbolStyle.Solid, - color = Color.cyan, - width = 3f - ), - values = listOf(5) - ) - ) - } - } - arcGISMap.operationalLayers.add(layer) - } - }.onFailure { - updateUserHint("An error occurred while loading the network: ${it.message}") + // load the NapervilleElectric web-map + arcGISMap.load().getOrElse { + messageDialogVM.showMessageDialog( + title = "Error loading the web-map: ${it.message}", + description = it.cause.toString() + ) + } + // load the utility network associated with the web-map + network.load().getOrElse { + messageDialogVM.showMessageDialog( + title = "Error loading the utility network: ${it.message}", + description = it.cause.toString() + ) + } + + // Once loaded, remove all operational layers + arcGISMap.operationalLayers.clear() + + // Add relevant layers + FEATURE_LAYER_IDS.forEach { layerId -> + val table = ServiceFeatureTable("$FEATURE_SERVICE_URL/$layerId") + val layer = FeatureLayer.createWithFeatureTable(table) + if (layerId == "3") { + // Customize rendering for layer with ID 3 + layer.renderer = UniqueValueRenderer( + fieldNames = listOf("ASSETGROUP"), + uniqueValues = listOf( + UniqueValue( + description = "Low voltage", + label = "", + symbol = SimpleLineSymbol( + style = SimpleLineSymbolStyle.Dash, + color = Color.cyan, + width = 3f + ), + values = listOf(3) + ), + UniqueValue( + description = "Medium voltage", + label = "", + symbol = SimpleLineSymbol( + style = SimpleLineSymbolStyle.Solid, + color = Color.cyan, + width = 3f + ), + values = listOf(5) + ) + + ) + ) } - }.onFailure { - updateUserHint("An error occurred while loading the network: ${it.message}") + // Add the two feature layers to the map's operational layers. + arcGISMap.operationalLayers.add(layer) } }.onFailure { - updateUserHint("An error occurred while loading the network: ${it.message}") + messageDialogVM.showMessageDialog( + title = "Error using TokenCredential: ${it.message}", + description = it.cause.toString() + ) } } } @@ -221,61 +230,72 @@ class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel( * Initialize the trace parameters and switch to “settingPoints” mode for .start points. */ fun setTraceParameters(traceType: UtilityTraceType) { - val params = UtilityTraceParameters(traceType, emptyList()).apply { + val params = UtilityTraceParameters( + traceType = traceType, + startingLocations = emptyList() + ).apply { // Attempt to set default config from the tier traceConfiguration = mediumVoltageRadial?.getDefaultTraceConfiguration() } _pendingTraceParameters.value = params - _tracingActivity.value = TracingActivity.SettingPoints(PointType.Start) + _traceState.value = TraceState.SettingPoints(PointType.Start) updateUserHint() // triggers default “Tap on the map to add a start location.” } /** - * Add a feature to the trace (barrier or start) based on the current tracing activity. + * Add a feature to the trace (barrier or start) based on the current tracing state. */ private fun addFeature( feature: ArcGISFeature, mapPoint: Point ) { val currentParams = _pendingTraceParameters.value - val activity = _tracingActivity.value - if (currentParams == null || activity !is TracingActivity.SettingPoints) return + val state = _traceState.value + if (currentParams == null || state !is TraceState.SettingPoints) return - // Convert to a UtilityElement. This is typically done via: - // network.createElement(feature) - // (ArcGIS Android differs slightly from iOS) + // Create a UtilityElement of the selected feature val element = try { network.createElementOrNull(feature) } catch (e: Exception) { - updateUserHint("Error adding element to the trace: ${e.message}") - return + return messageDialogVM.showMessageDialog( + title = "Error creating UtilityElement: ${e.message}", + description = e.cause.toString() + ) } // For a junction, add using its geometry // For an edge, compute fractionAlongEdge when (element?.networkSource?.sourceType) { UtilityNetworkSourceType.Junction -> { - addElementToPendingTrace(element, feature.geometry) val terminalCount = element.assetType.terminalConfiguration?.terminals?.size ?: 0 if (terminalCount > 1) { // Show terminal selector _terminalSelectorIsOpen.value = true } - + addElementToPendingTrace( + element = element, + pointGeometry = feature.geometry, + pointType = state.pointType + ) } UtilityNetworkSourceType.Edge -> { val line = feature.geometry as Polyline - val fraction = GeometryEngine.fractionAlong(line, mapPoint, 1.0) + val fraction = GeometryEngine.fractionAlong( + line = line, + point = mapPoint, + tolerance = 1.0 + ) element.fractionAlongEdge = fraction updateUserHint("fractionAlongEdge: %.3f".format(fraction)) - addElementToPendingTrace(element, mapPoint) - + addElementToPendingTrace( + element = element, + pointGeometry = mapPoint, + pointType = state.pointType + ) } - null -> { - - } + null -> {} } } @@ -283,31 +303,31 @@ class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel( * Actually add the UtilityElement to our UtilityTraceParameters (start or barrier) * and place a graphic on the map. */ - private fun addElementToPendingTrace(element: UtilityElement, pointGeom: Geometry?) { - val currentParams = _pendingTraceParameters.value - val activity = _tracingActivity.value - if (currentParams == null || activity !is TracingActivity.SettingPoints) return - - val graphic = Graphic(pointGeom).apply { - attributes["PointType"] = activity.pointType.name - } - - when (activity.pointType) { - PointType.Barrier -> currentParams.barriers.add(element) - PointType.Start -> currentParams.startingLocations.add(element) + private fun addElementToPendingTrace( + element: UtilityElement, + pointGeometry: Geometry?, + pointType: PointType + ) { + val graphic = Graphic(pointGeometry).apply { + attributes["PointType"] = pointType.name } - pointsOverlay.graphics.add(graphic) - _lastAddedElement.value = element + when (pointType) { + PointType.Barrier -> _pendingTraceParameters.value?.barriers?.add(element) + PointType.Start -> { + _pendingTraceParameters.value?.startingLocations?.add(element) + _canTrace.value = true + } + } } /** * Switch from adding .start points to adding .barrier, or vice versa. */ fun setPointType(pointType: PointType) { - val currentActivity = _tracingActivity.value - if (currentActivity is TracingActivity.SettingPoints) { - _tracingActivity.value = currentActivity.copy(pointType = pointType) + val currentTraceState = _traceState.value + if (currentTraceState is TraceState.SettingPoints) { + _traceState.value = currentTraceState.copy(pointType = pointType) } updateUserHint() } @@ -319,12 +339,15 @@ class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel( val params = _pendingTraceParameters.value ?: return viewModelScope.launch { try { - _tracingActivity.value = TracingActivity.TraceRunning + _traceState.value = TraceState.TraceRunning updateUserHint() // Perform the trace val traceResults: List = network.trace(params).getOrElse { - return@launch updateUserHint("An error occurred: ${it.message}") + return@launch messageDialogVM.showMessageDialog( + title = "Error performing trace: ${it.message}", + description = it.cause.toString() + ) } // Filter out element results @@ -351,12 +374,15 @@ class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel( } // Mark trace as completed - _tracingActivity.value = TracingActivity.TraceCompleted + _traceState.value = TraceState.TraceCompleted updateUserHint() } catch (e: Exception) { - _tracingActivity.value = TracingActivity.TraceFailed(e.message ?: "Unknown error") - updateUserHint() + _traceState.value = TraceState.TraceFailed(e.message ?: "Unknown error") + messageDialogVM.showMessageDialog( + title = "Error performing trace: ${e.message}", + description = e.cause.toString() + ) } } } @@ -370,32 +396,33 @@ class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel( .forEach { it.clearSelection() } pointsOverlay.graphics.clear() _pendingTraceParameters.value = null - _tracingActivity.value = TracingActivity.None + _traceState.value = TraceState.None + _canTrace.value = false updateUserHint(null) } /** - * Update the user hint text based on the current tracingActivity, or override with message. + * Update the user hint text based on the current TraceState, or override with message. */ private fun updateUserHint(message: String? = null) { _hint.value = message - ?: when (val activity = _tracingActivity.value) { - TracingActivity.None -> null - is TracingActivity.SettingPoints -> { - when (activity.pointType) { + ?: when (val state = _traceState.value) { + TraceState.None -> null + is TraceState.SettingPoints -> { + when (state.pointType) { PointType.Start -> "Tap on the map to add a start location." PointType.Barrier -> "Tap on the map to add a barrier." } } - TracingActivity.TraceCompleted -> "Trace completed." - is TracingActivity.TraceFailed -> "Trace failed.\n${activity.description}" - TracingActivity.TraceRunning -> null + TraceState.TraceCompleted -> "Trace completed." + is TraceState.TraceFailed -> "Trace failed.\n${state.description}" + TraceState.TraceRunning -> null } } fun identifyFeature(tapEvent: SingleTapConfirmedEvent) { - if (_tracingActivity.value is TracingActivity.SettingPoints) { + if (_traceState.value is TraceState.SettingPoints) { viewModelScope.launch { val identifyResults: List = mapViewProxy.identifyLayers( @@ -408,7 +435,10 @@ class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel( identifyResults.firstOrNull()?.geoElements?.firstOrNull() if (firstFeature != null) { (firstFeature as? ArcGISFeature)?.let { arcGISFeature -> - addFeature(arcGISFeature, tapEvent.mapPoint!!) + addFeature( + feature = arcGISFeature, + mapPoint = tapEvent.mapPoint!! + ) } } } @@ -421,10 +451,10 @@ enum class PointType { Barrier } -sealed class TracingActivity { - data object None : TracingActivity() - data class SettingPoints(val pointType: PointType) : TracingActivity() - data object TraceCompleted : TracingActivity() - data class TraceFailed(val description: String) : TracingActivity() - data object TraceRunning : TracingActivity() +sealed class TraceState { + data object None : TraceState() + data object TraceRunning : TraceState() + data object TraceCompleted : TraceState() + data class SettingPoints(val pointType: PointType) : TraceState() + data class TraceFailed(val description: String) : TraceState() } diff --git a/samples/trace-utility-network/src/main/java/com/esri/arcgismaps/sample/traceutilitynetwork/screens/TraceUtilityNetworkScreen.kt b/samples/trace-utility-network/src/main/java/com/esri/arcgismaps/sample/traceutilitynetwork/screens/TraceUtilityNetworkScreen.kt index 0c3f09e60..70c2ad220 100644 --- a/samples/trace-utility-network/src/main/java/com/esri/arcgismaps/sample/traceutilitynetwork/screens/TraceUtilityNetworkScreen.kt +++ b/samples/trace-utility-network/src/main/java/com/esri/arcgismaps/sample/traceutilitynetwork/screens/TraceUtilityNetworkScreen.kt @@ -27,15 +27,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold @@ -48,7 +47,6 @@ import androidx.compose.material3.TextField 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.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -56,7 +54,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties @@ -64,13 +64,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.arcgismaps.ArcGISEnvironment import com.arcgismaps.toolkit.geoviewcompose.MapView -import com.arcgismaps.utilitynetworks.UtilityTraceParameters import com.arcgismaps.utilitynetworks.UtilityTraceType +import com.esri.arcgismaps.sample.sampleslib.components.LoadingDialog +import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme import com.esri.arcgismaps.sample.traceutilitynetwork.components.PointType import com.esri.arcgismaps.sample.traceutilitynetwork.components.TraceUtilityNetworkViewModel -import com.esri.arcgismaps.sample.traceutilitynetwork.components.TracingActivity +import com.esri.arcgismaps.sample.traceutilitynetwork.components.TraceState @Composable fun TraceUtilityNetworkScreen(sampleName: String) { @@ -80,26 +81,14 @@ fun TraceUtilityNetworkScreen(sampleName: String) { // Observe relevant states val hintText by mapViewModel.hint .collectAsStateWithLifecycle(null) - val tracingActivity by mapViewModel.tracingActivity - .collectAsStateWithLifecycle(TracingActivity.None) + val traceState by mapViewModel.traceState + .collectAsStateWithLifecycle(TraceState.None) val pendingTraceParameters by mapViewModel.pendingTraceParameters .collectAsStateWithLifecycle(null) val terminalSelectorIsOpen by mapViewModel.terminalSelectorIsOpen .collectAsStateWithLifecycle(false) - val pendingTraceParams by mapViewModel.pendingTraceParameters - .collectAsState(null) - - // React to changes in tracingActivity – e.g., if we just switched to TraceRunning, call trace() - LaunchedEffect(tracingActivity) { - if (tracingActivity is TracingActivity.TraceRunning) { - try { - mapViewModel.trace() // runs the trace - // If successful, the VM sets TracingActivity to TraceCompleted - } catch (e: Exception) { - // If thrown, the VM presumably sets TracingActivity.TraceFailed - } - } - } + val canPerformTrace by mapViewModel.canTrace + .collectAsStateWithLifecycle(false) // remove credentials on screen dispose DisposableEffect(Unit) { @@ -111,11 +100,7 @@ fun TraceUtilityNetworkScreen(sampleName: String) { Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { padding -> - Column( - modifier = Modifier.padding(padding), - verticalArrangement = Arrangement.spacedBy(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { + Column(modifier = Modifier.padding(padding)) { MapView( modifier = Modifier .fillMaxSize() @@ -129,77 +114,62 @@ fun TraceUtilityNetworkScreen(sampleName: String) { ) // Bottom toolbar with trace options - Column( - modifier = Modifier - .wrapContentSize() - .animateContentSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (hintText != null) { - Text(text = hintText ?: "") - } - - TraceOptions( - pendingTraceParams = pendingTraceParams, - tracingActivity = tracingActivity, - onPointTypeChanged = mapViewModel::setPointType, - onTraceTypeSelected = mapViewModel::setTraceParameters, - onResetSelected = mapViewModel::reset, - onTraceSelected = mapViewModel::trace - ) - } + TraceOptions( + traceState = traceState, + hintText = hintText ?: "Trace Options", + isTraceButtonEnabled = canPerformTrace, + selectedTraceType = pendingTraceParameters?.traceType, + currentPointType = (traceState as? TraceState.SettingPoints)?.pointType, + onPointTypeChanged = mapViewModel::setPointType, + onTraceTypeSelected = mapViewModel::setTraceParameters, + onResetSelected = mapViewModel::reset, + onTraceSelected = mapViewModel::trace + ) } - if (terminalSelectorIsOpen) { - BasicAlertDialog( - onDismissRequest = {}, - properties = DialogProperties() - ) { - Surface { - Column { - Text( - modifier = Modifier.padding(24.dp), - text = "Select Terminal" - ) - Row(modifier = Modifier.fillMaxWidth()) { - OutlinedButton(onClick = {}) { Text("High") } - OutlinedButton(onClick = {}) { Text("Low") } - } - } - } - } - } + if (terminalSelectorIsOpen) { TerminalConfigurationDialog() } - if (tracingActivity is TracingActivity.TraceRunning && - pendingTraceParameters?.traceType != null - ) { + if (traceState is TraceState.TraceRunning && pendingTraceParameters?.traceType != null) { RunningTraceDialog( traceName = pendingTraceParameters?.traceType?.javaClass?.simpleName.toString() ) } + + mapViewModel.messageDialogVM.apply { + if (dialogStatus) { + MessageDialog( + title = messageTitle, + description = messageDescription, + onDismissRequest = ::dismissDialog + ) + } + } } ) } @Composable -fun RunningTraceDialog(traceName: String) { - BasicAlertDialog(onDismissRequest = {}, content = { +fun TerminalConfigurationDialog() { + BasicAlertDialog( + onDismissRequest = {}, + properties = DialogProperties() + ) { Surface { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp) - .clip(RoundedCornerShape(12.dp)), - verticalArrangement = Arrangement.spacedBy(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = "Running $traceName trace...") - CircularProgressIndicator() + Column { + Text( + modifier = Modifier.padding(24.dp), + text = "Select Terminal" + ) + Row(modifier = Modifier.fillMaxWidth()) { + OutlinedButton(onClick = {}) { Text("High") } + OutlinedButton(onClick = {}) { Text("Low") } + } } } - }) + } } + /** * - A trace-type picker * - A segmented button for Start vs. Barrier @@ -207,54 +177,42 @@ fun RunningTraceDialog(traceName: String) { */ @Composable fun TraceOptions( - pendingTraceParams: UtilityTraceParameters?, - tracingActivity: TracingActivity, + isTraceButtonEnabled: Boolean, + traceState: TraceState, + hintText: String, + selectedTraceType: UtilityTraceType?, + currentPointType: PointType?, onTraceTypeSelected: (UtilityTraceType) -> Unit, onPointTypeChanged: (PointType) -> Unit, onResetSelected: () -> Unit, onTraceSelected: () -> Unit ) { - // Observe state - val canTrace = (pendingTraceParams?.startingLocations?.isNotEmpty() == true) - // Example list of supported trace types - val traceOptions = listOf( - UtilityTraceType.Downstream, - UtilityTraceType.Upstream, - UtilityTraceType.Subnetwork, - UtilityTraceType.Connected - ) - Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .wrapContentSize() + .padding(12.dp) + .animateContentSize(), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // Show a menu to pick a new trace type + + Text(text = hintText, style = MaterialTheme.typography.labelLarge) + + // Show a dropdown menu to pick a new trace type ExposedDropdownMenuBoxWithTraceTypes( - traceOptions = traceOptions, + selectedTraceType = selectedTraceType, onTraceTypeSelected = onTraceTypeSelected ) - // Show segmented button for Start vs Barrier - var selectedIndex by remember { mutableIntStateOf(0) } - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - val options = listOf("Add starting location(s)", "Add barrier(s)") - options.forEachIndexed { index, label -> - SegmentedButton( - shape = SegmentedButtonDefaults.itemShape( - index = index, count = options.size - ), - onClick = { - selectedIndex = index - // Switch between Start/Barrier - onPointTypeChanged(if (index == 0) PointType.Start else PointType.Barrier) - }, - selected = (index == selectedIndex) - ) { Text(label) } - } - } + // Display segmented button for starting point type or barrier point type + SegmentedButtonTracePointTypes( + currentPointType = currentPointType, + onPointTypeChanged = onPointTypeChanged, + isPointTypesEnabled = selectedTraceType != null + ) + // Display a row with reset and trace controls Row( modifier = Modifier .fillMaxWidth() @@ -263,13 +221,13 @@ fun TraceOptions( ) { OutlinedButton( onClick = { onResetSelected() }, - enabled = (tracingActivity !is TracingActivity.TraceRunning) + enabled = (traceState !is TraceState.TraceRunning) ) { Text("Reset") } Button( onClick = { onTraceSelected() }, - enabled = canTrace + enabled = isTraceButtonEnabled ) { Text("Trace") } @@ -283,27 +241,43 @@ fun TraceOptions( @OptIn(ExperimentalMaterial3Api::class) @Composable fun ExposedDropdownMenuBoxWithTraceTypes( - traceOptions: List, + selectedTraceType: UtilityTraceType?, onTraceTypeSelected: (UtilityTraceType) -> Unit ) { + val traceOptions = listOf( + UtilityTraceType.Downstream, + UtilityTraceType.Upstream, + UtilityTraceType.Subnetwork, + UtilityTraceType.Connected + ) + + var selectedTraceName by remember { mutableStateOf("") } var expanded by remember { mutableStateOf(false) } - var selectedTraceName by remember { mutableStateOf("Select a trace type") } + + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + LaunchedEffect(selectedTraceType) { + if (selectedTraceType == null) { + focusManager.clearFocus() + selectedTraceName = "Select a trace type" + } else selectedTraceName = traceTypeDisplayName(selectedTraceType) + } ExposedDropdownMenuBox( + modifier = Modifier.focusRequester(focusRequester), expanded = expanded, onExpandedChange = { expanded = !expanded } ) { TextField( - value = selectedTraceName, - onValueChange = {}, - readOnly = true, - label = { Text("Trace Type") }, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) - }, modifier = Modifier .menuAnchor(MenuAnchorType.PrimaryNotEditable) - .fillMaxWidth() + .fillMaxWidth(), + label = { Text("Trace Type") }, + value = selectedTraceName, + readOnly = true, + onValueChange = { }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) } ) ExposedDropdownMenu( expanded = expanded, @@ -314,7 +288,6 @@ fun ExposedDropdownMenuBoxWithTraceTypes( DropdownMenuItem( text = { Text(displayName) }, onClick = { - selectedTraceName = displayName expanded = false onTraceTypeSelected(traceType) } @@ -327,6 +300,56 @@ fun ExposedDropdownMenuBoxWithTraceTypes( } } +@Composable +fun SegmentedButtonTracePointTypes( + currentPointType: PointType?, + onPointTypeChanged: (PointType) -> Unit, + isPointTypesEnabled: Boolean +) { + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + // Show segmented button for Start vs Barrier + var selectedIndex by remember { mutableIntStateOf(-1) } + + LaunchedEffect(currentPointType) { + if (currentPointType == null) { + focusManager.clearFocus() + selectedIndex = -1 + } else { + selectedIndex = if (currentPointType == PointType.Start) 0 else 1 + } + } + + SingleChoiceSegmentedButtonRow( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + ) { + val options = listOf("Add starting location(s)", "Add barrier(s)") + options.forEachIndexed { index, label -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, count = options.size + ), + onClick = { + selectedIndex = index + // Switch between Start/Barrier + onPointTypeChanged(if (index == 0) PointType.Start else PointType.Barrier) + }, + enabled = isPointTypesEnabled, + selected = (index == selectedIndex) + ) { Text(label) } + } + } +} + + +@Composable +fun RunningTraceDialog(traceName: String) { + LoadingDialog(loadingMessage = "Running $traceName trace...") +} + fun traceTypeDisplayName(type: UtilityTraceType): String = when (type) { UtilityTraceType.Connected -> "Connected" @@ -344,5 +367,18 @@ fun traceTypeDisplayName(type: UtilityTraceType): String = @Composable fun PreviewTraceUtilityNetworkScreen() { SampleAppTheme { + Surface { + TraceOptions( + isTraceButtonEnabled = true, + traceState = TraceState.None, + selectedTraceType = null, + currentPointType = null, + hintText = "Trace options", + onTraceTypeSelected = { }, + onPointTypeChanged = { }, + onResetSelected = { }, + onTraceSelected = { }, + ) + } } }