From b4c12c61b237d1d0594c7b0b2f376a4a25011558 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Wed, 26 Nov 2025 14:18:03 -0600 Subject: [PATCH 01/14] In the beginning... --- lib/arcgis_maps_toolkit.dart | 2 + .../building_explorer/building_explorer.dart | 171 ++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 lib/src/building_explorer/building_explorer.dart diff --git a/lib/arcgis_maps_toolkit.dart b/lib/arcgis_maps_toolkit.dart index 38260dc..0860a58 100644 --- a/lib/arcgis_maps_toolkit.dart +++ b/lib/arcgis_maps_toolkit.dart @@ -46,6 +46,8 @@ part 'src/authenticator/authenticator_certificate_required.dart'; part 'src/authenticator/authenticator_certificate_password.dart'; part 'src/authenticator/authenticator_login.dart'; part 'src/authenticator/authenticator_trust.dart'; +// Building Explorer Widget +part 'src/building_explorer/building_explorer.dart'; // Compass Widget part 'src/compass/compass.dart'; part 'src/compass/compass_needle_painter.dart'; diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart new file mode 100644 index 0000000..ae7f0f9 --- /dev/null +++ b/lib/src/building_explorer/building_explorer.dart @@ -0,0 +1,171 @@ +// +// Copyright 2025 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 +// +// https://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. +// + +part of '../../arcgis_maps_toolkit.dart'; + +class BuildingExplorer extends StatefulWidget { + const BuildingExplorer({ + required this.buildingSceneLayer, + super.key, + this.overviewSublayerName = 'Overview', + this.fullModelSublayerName = 'Full Model', + }); + + final BuildingSceneLayer buildingSceneLayer; + final String overviewSublayerName; + final String fullModelSublayerName; + + @override + State createState() => _BuildingExplorerState(); +} + +class _BuildingExplorerState extends State { + // The currently selected floor. + var _selectedFloor = 'All'; + + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } + + // Utility function to update the building filters based on the selected floor. + void updateFloorFilters() { + if (_selectedFloor == 'All') { + // No filtering applied if 'All' floors are selected. + widget.buildingSceneLayer.activeFilter = null; + return; + } + + // Build a building filter to show the selected floor and an xray view of the floors below. + // Floors above the selected floor are not shown at all. + final buildingFilter = BuildingFilter( + name: 'Floor filter', + description: 'Show selected floor and xray filter for lower floors.', + blocks: [ + BuildingFilterBlock( + title: 'solid block', + whereClause: 'BldgLevel = $_selectedFloor', + mode: BuildingSolidFilterMode(), + ), + BuildingFilterBlock( + title: 'xray block', + whereClause: 'BldgLevel < $_selectedFloor', + mode: BuildingXrayFilterMode(), + ), + ], + ); + + // Apply the filter to the building scene layer. + widget.buildingSceneLayer.activeFilter = buildingFilter; + } +} + +// Widget to list and select building floor. +class _FloorLevelSelector extends StatelessWidget { + const _FloorLevelSelector({ + required this.floorList, + required this.selectedFloor, + required this.onChanged, + }); + + final List floorList; + final String selectedFloor; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final options = ['All', ...floorList]; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text('Floor:'), + DropdownButton( + value: selectedFloor, + items: options + .map( + (value) => DropdownMenuItem(value: value, child: Text(value)), + ) + .toList(), + onChanged: (value) { + if (value != null) onChanged(value); + }, + ), + ], + ); + } +} + +// Widget to show and select building sublayers. +class _SublayerSelector extends StatefulWidget { + const _SublayerSelector({ + required this.buildingSceneLayer, + required this.fullModelSublayerName, + }); + final BuildingSceneLayer buildingSceneLayer; + final String fullModelSublayerName; + + @override + State<_SublayerSelector> createState() => _SublayerSelectorState(); +} + +class _SublayerSelectorState extends State<_SublayerSelector> { + @override + Widget build(BuildContext context) { + final fullModelSublayer = + widget.buildingSceneLayer.sublayers.firstWhere( + (sublayer) => sublayer.name == 'Full Model', + ) + as BuildingGroupSublayer; + final categorySublayers = fullModelSublayer.sublayers; + return SizedBox( + height: 200, + child: ListView( + children: categorySublayers.map((categorySublayer) { + final componentSublayers = + (categorySublayer as BuildingGroupSublayer).sublayers; + return ExpansionTile( + title: Row( + children: [ + Text(categorySublayer.name), + const Spacer(), + Checkbox( + value: categorySublayer.isVisible, + onChanged: (val) { + setState(() { + categorySublayer.isVisible = val ?? false; + }); + }, + ), + ], + ), + children: componentSublayers.map((componentSublayer) { + return CheckboxListTile( + title: Text(componentSublayer.name), + value: componentSublayer.isVisible, + onChanged: (val) { + setState(() { + componentSublayer.isVisible = val ?? false; + }); + }, + ); + }).toList(), + ); + }).toList(), + ), + ); + } +} From 948fd28a2b87f8c3f25c9d8f735be0f34d052e0a Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Mon, 1 Dec 2025 15:53:23 -0600 Subject: [PATCH 02/14] Created example app - Added toolkit widget to example app - Refactored sub widgets to separate files - Still working building_explorer widget to act properly example app widgets --- example/lib/example_building_explorer.dart | 101 +++++++++++++ lib/arcgis_maps_toolkit.dart | 2 + .../building_explorer/building_explorer.dart | 141 ++++++------------ .../building_floor_level_selector.dart | 36 +++++ .../building_sublayer_selector.dart | 60 ++++++++ 5 files changed, 242 insertions(+), 98 deletions(-) create mode 100644 example/lib/example_building_explorer.dart create mode 100644 lib/src/building_explorer/building_floor_level_selector.dart create mode 100644 lib/src/building_explorer/building_sublayer_selector.dart diff --git a/example/lib/example_building_explorer.dart b/example/lib/example_building_explorer.dart new file mode 100644 index 0000000..3ab883f --- /dev/null +++ b/example/lib/example_building_explorer.dart @@ -0,0 +1,101 @@ +// +// Copyright 2025 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 +// +// https://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. +// + +import 'package:arcgis_maps/arcgis_maps.dart'; +import 'package:arcgis_maps_toolkit/arcgis_maps_toolkit.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MaterialApp(home: ExampleBuildingExplorer())); +} + +class ExampleBuildingExplorer extends StatefulWidget { + const ExampleBuildingExplorer({super.key}); + + @override + State createState() => + _ExampleBuildingExplorerState(); +} + +class _ExampleBuildingExplorerState extends State { + // Create a controller for the local scene view. + final _localSceneViewController = ArcGISLocalSceneView.createController(); + + // Building scene layer that will be filtered. Set after the WebScene is loaded. + late final BuildingSceneLayer _buildingSceneLayer; + + // Flag to show or hide the settings pane. + var _settingsVisible = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + top: false, + left: false, + right: false, + child: Stack( + children: [ + Column( + children: [ + Expanded( + // Add a local scene view to the widget tree and set a controller. + child: ArcGISLocalSceneView( + controllerProvider: () => _localSceneViewController, + onLocalSceneViewReady: onLocalSceneViewReady, + ), + ), + Center( + // Button to show the building filter settings sheet. + child: ElevatedButton( + onPressed: () => setState(() => _settingsVisible = true), + child: const Text('Building Filter Settings'), + ), + ), + ], + ), + ], + ), + ), + // Bottom sheet that displays the building filter settings. + bottomSheet: _settingsVisible + ? SizedBox( + height: 200, + child: BuildingExplorer(buildingSceneLayer: _buildingSceneLayer), + ) + : null, + ); + } + + Future onLocalSceneViewReady() async { + // Create the local scene from a ArcGISOnline web scene. + final sceneUri = Uri.parse( + 'https://arcgisruntime.maps.arcgis.com/home/item.html?id=b7c387d599a84a50aafaece5ca139d44', + ); + final scene = ArcGISScene.withUri(sceneUri)!; + + // Load the scene so the underlying layers can be accessed. + await scene.load(); + + // Get the BuildingSceneLayer from the webmap. + _buildingSceneLayer = scene.operationalLayers + .whereType() + .first; + + // Apply the scene to the local scene view controller. + _localSceneViewController.arcGISScene = scene; + } +} diff --git a/lib/arcgis_maps_toolkit.dart b/lib/arcgis_maps_toolkit.dart index 0860a58..cee548b 100644 --- a/lib/arcgis_maps_toolkit.dart +++ b/lib/arcgis_maps_toolkit.dart @@ -48,6 +48,8 @@ part 'src/authenticator/authenticator_login.dart'; part 'src/authenticator/authenticator_trust.dart'; // Building Explorer Widget part 'src/building_explorer/building_explorer.dart'; +part 'src/building_explorer/building_floor_level_selector.dart'; +part 'src/building_explorer/building_sublayer_selector.dart'; // Compass Widget part 'src/compass/compass.dart'; part 'src/compass/compass_needle_painter.dart'; diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index ae7f0f9..1d1e0ad 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -36,10 +36,51 @@ class _BuildingExplorerState extends State { // The currently selected floor. var _selectedFloor = 'All'; + // A listing of all floors in the building scene layer. + var _floorList = []; + + @override + void initState() { + super.initState(); + + _initFloorList(); + } + @override Widget build(BuildContext context) { - // TODO: implement build - throw UnimplementedError(); + return Column( + children: [ + _BuildingFloorLevelSelector( + floorList: _floorList, + selectedFloor: _selectedFloor, + onChanged: onFloorChanged, + ), + const Divider(), + const Text('Categories:'), + _BuildingSublayerSelector( + buildingSceneLayer: widget.buildingSceneLayer, + fullModelSublayerName: widget.fullModelSublayerName, + ), + ], + ); + } + + Future _initFloorList() async { + // Get the floor listing from the statistics. + final statistics = await widget.buildingSceneLayer.fetchStatistics(); + if (statistics['BldgLevel'] != null) { + final floorList = []; + floorList.addAll(statistics['BldgLevel']!.mostFrequentValues); + floorList.sort((a, b) => int.parse(b).compareTo(int.parse(a))); + setState(() { + _floorList = floorList; + }); + } + } + + void onFloorChanged(String floor) { + setState(() => _selectedFloor = floor); + updateFloorFilters(); } // Utility function to update the building filters based on the selected floor. @@ -73,99 +114,3 @@ class _BuildingExplorerState extends State { widget.buildingSceneLayer.activeFilter = buildingFilter; } } - -// Widget to list and select building floor. -class _FloorLevelSelector extends StatelessWidget { - const _FloorLevelSelector({ - required this.floorList, - required this.selectedFloor, - required this.onChanged, - }); - - final List floorList; - final String selectedFloor; - final ValueChanged onChanged; - - @override - Widget build(BuildContext context) { - final options = ['All', ...floorList]; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const Text('Floor:'), - DropdownButton( - value: selectedFloor, - items: options - .map( - (value) => DropdownMenuItem(value: value, child: Text(value)), - ) - .toList(), - onChanged: (value) { - if (value != null) onChanged(value); - }, - ), - ], - ); - } -} - -// Widget to show and select building sublayers. -class _SublayerSelector extends StatefulWidget { - const _SublayerSelector({ - required this.buildingSceneLayer, - required this.fullModelSublayerName, - }); - final BuildingSceneLayer buildingSceneLayer; - final String fullModelSublayerName; - - @override - State<_SublayerSelector> createState() => _SublayerSelectorState(); -} - -class _SublayerSelectorState extends State<_SublayerSelector> { - @override - Widget build(BuildContext context) { - final fullModelSublayer = - widget.buildingSceneLayer.sublayers.firstWhere( - (sublayer) => sublayer.name == 'Full Model', - ) - as BuildingGroupSublayer; - final categorySublayers = fullModelSublayer.sublayers; - return SizedBox( - height: 200, - child: ListView( - children: categorySublayers.map((categorySublayer) { - final componentSublayers = - (categorySublayer as BuildingGroupSublayer).sublayers; - return ExpansionTile( - title: Row( - children: [ - Text(categorySublayer.name), - const Spacer(), - Checkbox( - value: categorySublayer.isVisible, - onChanged: (val) { - setState(() { - categorySublayer.isVisible = val ?? false; - }); - }, - ), - ], - ), - children: componentSublayers.map((componentSublayer) { - return CheckboxListTile( - title: Text(componentSublayer.name), - value: componentSublayer.isVisible, - onChanged: (val) { - setState(() { - componentSublayer.isVisible = val ?? false; - }); - }, - ); - }).toList(), - ); - }).toList(), - ), - ); - } -} diff --git a/lib/src/building_explorer/building_floor_level_selector.dart b/lib/src/building_explorer/building_floor_level_selector.dart new file mode 100644 index 0000000..33c9e43 --- /dev/null +++ b/lib/src/building_explorer/building_floor_level_selector.dart @@ -0,0 +1,36 @@ +part of '../../arcgis_maps_toolkit.dart'; + +// Widget to list and select building floor. +class _BuildingFloorLevelSelector extends StatelessWidget { + const _BuildingFloorLevelSelector({ + required this.floorList, + required this.selectedFloor, + required this.onChanged, + }); + + final List floorList; + final String selectedFloor; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final options = ['All', ...floorList]; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text('Floor:'), + DropdownButton( + value: selectedFloor, + items: options + .map( + (value) => DropdownMenuItem(value: value, child: Text(value)), + ) + .toList(), + onChanged: (value) { + if (value != null) onChanged(value); + }, + ), + ], + ); + } +} diff --git a/lib/src/building_explorer/building_sublayer_selector.dart b/lib/src/building_explorer/building_sublayer_selector.dart new file mode 100644 index 0000000..deb81e2 --- /dev/null +++ b/lib/src/building_explorer/building_sublayer_selector.dart @@ -0,0 +1,60 @@ +part of '../../arcgis_maps_toolkit.dart'; + +// Widget to show and select building sublayers. +class _BuildingSublayerSelector extends StatefulWidget { + const _BuildingSublayerSelector({ + required this.buildingSceneLayer, + required this.fullModelSublayerName, + }); + final BuildingSceneLayer buildingSceneLayer; + final String fullModelSublayerName; + + @override + State<_BuildingSublayerSelector> createState() => + _BuildingSublayerSelectorState(); +} + +class _BuildingSublayerSelectorState extends State<_BuildingSublayerSelector> { + @override + Widget build(BuildContext context) { + final fullModelSublayer = + widget.buildingSceneLayer.sublayers.firstWhere( + (sublayer) => sublayer.name == 'Full Model', + ) + as BuildingGroupSublayer; + final categorySublayers = fullModelSublayer.sublayers; + return ListView( + children: categorySublayers.map((categorySublayer) { + final componentSublayers = + (categorySublayer as BuildingGroupSublayer).sublayers; + return ExpansionTile( + title: Row( + children: [ + Text(categorySublayer.name), + const Spacer(), + Checkbox( + value: categorySublayer.isVisible, + onChanged: (val) { + setState(() { + categorySublayer.isVisible = val ?? false; + }); + }, + ), + ], + ), + children: componentSublayers.map((componentSublayer) { + return CheckboxListTile( + title: Text(componentSublayer.name), + value: componentSublayer.isVisible, + onChanged: (val) { + setState(() { + componentSublayer.isVisible = val ?? false; + }); + }, + ); + }).toList(), + ); + }).toList(), + ); + } +} From ae6d7cc018a4e0739de599b76149cc8e0252c391 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Tue, 2 Dec 2025 10:00:24 -0600 Subject: [PATCH 03/14] Refactored FloorLevelSelector - Moved all floor logic into FloorLevelSelector - Added example app to main - Added copyright to files --- example/lib/example_building_explorer.dart | 2 +- example/lib/main.dart | 6 + .../building_explorer/building_explorer.dart | 74 +----------- .../building_floor_level_selector.dart | 106 +++++++++++++++--- .../building_sublayer_selector.dart | 16 +++ 5 files changed, 121 insertions(+), 83 deletions(-) diff --git a/example/lib/example_building_explorer.dart b/example/lib/example_building_explorer.dart index 3ab883f..741ef48 100644 --- a/example/lib/example_building_explorer.dart +++ b/example/lib/example_building_explorer.dart @@ -73,7 +73,7 @@ class _ExampleBuildingExplorerState extends State { // Bottom sheet that displays the building filter settings. bottomSheet: _settingsVisible ? SizedBox( - height: 200, + height: 300, child: BuildingExplorer(buildingSceneLayer: _buildingSceneLayer), ) : null, diff --git a/example/lib/main.dart b/example/lib/main.dart index 079311e..dead7f6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -16,6 +16,7 @@ import 'package:arcgis_maps/arcgis_maps.dart'; import 'package:arcgis_maps_toolkit_example/example_authenticator.dart'; +import 'package:arcgis_maps_toolkit_example/example_building_explorer.dart'; import 'package:arcgis_maps_toolkit_example/example_compass.dart'; import 'package:arcgis_maps_toolkit_example/example_overview_map.dart'; import 'package:arcgis_maps_toolkit_example/example_popup.dart'; @@ -64,6 +65,11 @@ enum ComponentExample { 'PopupView', 'Displays a popup for a feature, including fields, media, and attachments', PopupExample.new, + ), + buildingExplorer( + 'BuildingExplorer', + 'Filters a BuildingSceneLayer by floor level and category sublayers', + ExampleBuildingExplorer.new, ); const ComponentExample(this.title, this.subtitle, this.constructor); diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index 1d1e0ad..788f60c 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -33,84 +33,22 @@ class BuildingExplorer extends StatefulWidget { } class _BuildingExplorerState extends State { - // The currently selected floor. - var _selectedFloor = 'All'; - - // A listing of all floors in the building scene layer. - var _floorList = []; - - @override - void initState() { - super.initState(); - - _initFloorList(); - } - @override Widget build(BuildContext context) { return Column( children: [ _BuildingFloorLevelSelector( - floorList: _floorList, - selectedFloor: _selectedFloor, - onChanged: onFloorChanged, + buildingSceneLayer: widget.buildingSceneLayer, ), const Divider(), const Text('Categories:'), - _BuildingSublayerSelector( - buildingSceneLayer: widget.buildingSceneLayer, - fullModelSublayerName: widget.fullModelSublayerName, - ), - ], - ); - } - - Future _initFloorList() async { - // Get the floor listing from the statistics. - final statistics = await widget.buildingSceneLayer.fetchStatistics(); - if (statistics['BldgLevel'] != null) { - final floorList = []; - floorList.addAll(statistics['BldgLevel']!.mostFrequentValues); - floorList.sort((a, b) => int.parse(b).compareTo(int.parse(a))); - setState(() { - _floorList = floorList; - }); - } - } - - void onFloorChanged(String floor) { - setState(() => _selectedFloor = floor); - updateFloorFilters(); - } - - // Utility function to update the building filters based on the selected floor. - void updateFloorFilters() { - if (_selectedFloor == 'All') { - // No filtering applied if 'All' floors are selected. - widget.buildingSceneLayer.activeFilter = null; - return; - } - - // Build a building filter to show the selected floor and an xray view of the floors below. - // Floors above the selected floor are not shown at all. - final buildingFilter = BuildingFilter( - name: 'Floor filter', - description: 'Show selected floor and xray filter for lower floors.', - blocks: [ - BuildingFilterBlock( - title: 'solid block', - whereClause: 'BldgLevel = $_selectedFloor', - mode: BuildingSolidFilterMode(), - ), - BuildingFilterBlock( - title: 'xray block', - whereClause: 'BldgLevel < $_selectedFloor', - mode: BuildingXrayFilterMode(), + Expanded( + child: _BuildingSublayerSelector( + buildingSceneLayer: widget.buildingSceneLayer, + fullModelSublayerName: widget.fullModelSublayerName, + ), ), ], ); - - // Apply the filter to the building scene layer. - widget.buildingSceneLayer.activeFilter = buildingFilter; } } diff --git a/lib/src/building_explorer/building_floor_level_selector.dart b/lib/src/building_explorer/building_floor_level_selector.dart index 33c9e43..50ba3f3 100644 --- a/lib/src/building_explorer/building_floor_level_selector.dart +++ b/lib/src/building_explorer/building_floor_level_selector.dart @@ -1,36 +1,114 @@ +// +// Copyright 2025 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 +// +// https://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. +// + part of '../../arcgis_maps_toolkit.dart'; +class _BuildingFloorLevelSelector extends StatefulWidget { + const _BuildingFloorLevelSelector({required this.buildingSceneLayer}); + + final BuildingSceneLayer buildingSceneLayer; + + @override + State createState() => _BuildingFloorLevelSelectorState(); +} + // Widget to list and select building floor. -class _BuildingFloorLevelSelector extends StatelessWidget { - const _BuildingFloorLevelSelector({ - required this.floorList, - required this.selectedFloor, - required this.onChanged, - }); +class _BuildingFloorLevelSelectorState + extends State<_BuildingFloorLevelSelector> { + // The currently selected floor. + var _selectedFloor = 'All'; - final List floorList; - final String selectedFloor; - final ValueChanged onChanged; + // A listing of all floors in the building scene layer. + var _floorList = []; + + @override + void initState() { + super.initState(); + + _initFloorList(); + } @override Widget build(BuildContext context) { - final options = ['All', ...floorList]; + final options = ['All', ..._floorList]; return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ const Text('Floor:'), DropdownButton( - value: selectedFloor, + value: _selectedFloor, items: options .map( (value) => DropdownMenuItem(value: value, child: Text(value)), ) .toList(), - onChanged: (value) { - if (value != null) onChanged(value); - }, + onChanged: onFloorChanged, ), ], ); } + + Future _initFloorList() async { + // Get the floor listing from the statistics. + final statistics = await widget.buildingSceneLayer.fetchStatistics(); + if (statistics['BldgLevel'] != null) { + final floorList = []; + floorList.addAll(statistics['BldgLevel']!.mostFrequentValues); + floorList.sort((a, b) => int.parse(b).compareTo(int.parse(a))); + setState(() { + _floorList = floorList; + }); + } + } + + void onFloorChanged(String? floor) { + if (floor == null) return; + + setState(() => _selectedFloor = floor); + updateFloorFilters(); + } + + // Utility function to update the building filters based on the selected floor. + void updateFloorFilters() { + if (_selectedFloor == 'All') { + // No filtering applied if 'All' floors are selected. + widget.buildingSceneLayer.activeFilter = null; + return; + } + + // Build a building filter to show the selected floor and an xray view of the floors below. + // Floors above the selected floor are not shown at all. + final buildingFilter = BuildingFilter( + name: 'Floor filter', + description: 'Show selected floor and xray filter for lower floors.', + blocks: [ + BuildingFilterBlock( + title: 'solid block', + whereClause: 'BldgLevel = $_selectedFloor', + mode: BuildingSolidFilterMode(), + ), + BuildingFilterBlock( + title: 'xray block', + whereClause: 'BldgLevel < $_selectedFloor', + mode: BuildingXrayFilterMode(), + ), + ], + ); + + // Apply the filter to the building scene layer. + widget.buildingSceneLayer.activeFilter = buildingFilter; + } } diff --git a/lib/src/building_explorer/building_sublayer_selector.dart b/lib/src/building_explorer/building_sublayer_selector.dart index deb81e2..e210fb7 100644 --- a/lib/src/building_explorer/building_sublayer_selector.dart +++ b/lib/src/building_explorer/building_sublayer_selector.dart @@ -1,3 +1,19 @@ +// +// Copyright 2025 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 +// +// https://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. +// + part of '../../arcgis_maps_toolkit.dart'; // Widget to show and select building sublayers. From e5adc70d483ab1f874b7f244e22f36ed56be3b56 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Tue, 2 Dec 2025 10:30:46 -0600 Subject: [PATCH 04/14] Disable category sublayers when cat is hidden - Added "Disciplines" to title of sublayer selector --- lib/src/building_explorer/building_explorer.dart | 2 +- lib/src/building_explorer/building_sublayer_selector.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index 788f60c..0a0960a 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -41,7 +41,7 @@ class _BuildingExplorerState extends State { buildingSceneLayer: widget.buildingSceneLayer, ), const Divider(), - const Text('Categories:'), + const Text('Disciplines & Categories:'), Expanded( child: _BuildingSublayerSelector( buildingSceneLayer: widget.buildingSceneLayer, diff --git a/lib/src/building_explorer/building_sublayer_selector.dart b/lib/src/building_explorer/building_sublayer_selector.dart index e210fb7..9be50e5 100644 --- a/lib/src/building_explorer/building_sublayer_selector.dart +++ b/lib/src/building_explorer/building_sublayer_selector.dart @@ -62,6 +62,7 @@ class _BuildingSublayerSelectorState extends State<_BuildingSublayerSelector> { return CheckboxListTile( title: Text(componentSublayer.name), value: componentSublayer.isVisible, + enabled: categorySublayer.isVisible, onChanged: (val) { setState(() { componentSublayer.isVisible = val ?? false; From f2c353beea73ea8fba9eff07b8186c31ea8ea979 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Tue, 2 Dec 2025 14:25:30 -0600 Subject: [PATCH 05/14] Updated example app - Added close button to bottom sheet popup - Changed label text style and words --- example/lib/example_building_explorer.dart | 24 ++++++++++++++++--- .../building_explorer/building_explorer.dart | 10 +++++++- .../building_floor_level_selector.dart | 2 +- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/example/lib/example_building_explorer.dart b/example/lib/example_building_explorer.dart index 741ef48..7af50ff 100644 --- a/example/lib/example_building_explorer.dart +++ b/example/lib/example_building_explorer.dart @@ -43,6 +43,7 @@ class _ExampleBuildingExplorerState extends State { @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar(title: const Text('Building Explorer')), body: SafeArea( top: false, left: false, @@ -70,11 +71,28 @@ class _ExampleBuildingExplorerState extends State { ], ), ), - // Bottom sheet that displays the building filter settings. bottomSheet: _settingsVisible ? SizedBox( - height: 300, - child: BuildingExplorer(buildingSceneLayer: _buildingSceneLayer), + height: 400, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: () => + setState(() => _settingsVisible = false), + icon: const Icon(Icons.close), + ), + ], + ), + Expanded( + child: BuildingExplorer( + buildingSceneLayer: _buildingSceneLayer, + ), + ), + ], + ), ) : null, ); diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index 0a0960a..3c5feba 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -37,11 +37,19 @@ class _BuildingExplorerState extends State { Widget build(BuildContext context) { return Column( children: [ + Text( + widget.buildingSceneLayer.name, + style: Theme.of(context).textTheme.headlineSmall, + ), + const Divider(), _BuildingFloorLevelSelector( buildingSceneLayer: widget.buildingSceneLayer, ), const Divider(), - const Text('Disciplines & Categories:'), + Text( + 'Disciplines & Categories:', + style: Theme.of(context).textTheme.bodyLarge, + ), Expanded( child: _BuildingSublayerSelector( buildingSceneLayer: widget.buildingSceneLayer, diff --git a/lib/src/building_explorer/building_floor_level_selector.dart b/lib/src/building_explorer/building_floor_level_selector.dart index 50ba3f3..7e9e6a4 100644 --- a/lib/src/building_explorer/building_floor_level_selector.dart +++ b/lib/src/building_explorer/building_floor_level_selector.dart @@ -47,7 +47,7 @@ class _BuildingFloorLevelSelectorState return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - const Text('Floor:'), + const Text('Select Level:'), DropdownButton( value: _selectedFloor, items: options From 711e7e0ff472a84b9a3364f0ca3a463340401519 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Tue, 2 Dec 2025 14:57:24 -0600 Subject: [PATCH 06/14] Changed bottom sheet to modal --- example/lib/example_building_explorer.dart | 58 ++++++++++++---------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/example/lib/example_building_explorer.dart b/example/lib/example_building_explorer.dart index 7af50ff..a74d76b 100644 --- a/example/lib/example_building_explorer.dart +++ b/example/lib/example_building_explorer.dart @@ -37,9 +37,6 @@ class _ExampleBuildingExplorerState extends State { // Building scene layer that will be filtered. Set after the WebScene is loaded. late final BuildingSceneLayer _buildingSceneLayer; - // Flag to show or hide the settings pane. - var _settingsVisible = false; - @override Widget build(BuildContext context) { return Scaffold( @@ -62,7 +59,7 @@ class _ExampleBuildingExplorerState extends State { Center( // Button to show the building filter settings sheet. child: ElevatedButton( - onPressed: () => setState(() => _settingsVisible = true), + onPressed: showBuildingExplorerModal, child: const Text('Building Filter Settings'), ), ), @@ -71,30 +68,39 @@ class _ExampleBuildingExplorerState extends State { ], ), ), - bottomSheet: _settingsVisible - ? SizedBox( - height: 400, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () => - setState(() => _settingsVisible = false), - icon: const Icon(Icons.close), - ), - ], - ), - Expanded( - child: BuildingExplorer( - buildingSceneLayer: _buildingSceneLayer, + ); + } + + void showBuildingExplorerModal() { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Container( + height: 400, // Define the height of the bottom sheet + color: Colors.white, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close), ), + ], + ), + Expanded( + child: BuildingExplorer( + buildingSceneLayer: _buildingSceneLayer, ), - ], - ), - ) - : null, + ), + ], + ), + ), + ); + }, ); } From b7af5127e42c6abb1999ba83e82d70c4572e8c0a Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Tue, 2 Dec 2025 15:16:27 -0600 Subject: [PATCH 07/14] UI change to level selector widget - Added padding to the row to align better with sublayer listings below. --- .../building_floor_level_selector.dart | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/src/building_explorer/building_floor_level_selector.dart b/lib/src/building_explorer/building_floor_level_selector.dart index 7e9e6a4..25aafd2 100644 --- a/lib/src/building_explorer/building_floor_level_selector.dart +++ b/lib/src/building_explorer/building_floor_level_selector.dart @@ -44,20 +44,24 @@ class _BuildingFloorLevelSelectorState @override Widget build(BuildContext context) { final options = ['All', ..._floorList]; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const Text('Select Level:'), - DropdownButton( - value: _selectedFloor, - items: options - .map( - (value) => DropdownMenuItem(value: value, child: Text(value)), - ) - .toList(), - onChanged: onFloorChanged, - ), - ], + return Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 20, 0), + child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text('Select Level:', style: Theme.of(context).textTheme.bodyLarge), + const Spacer(), + DropdownButton( + value: _selectedFloor, + items: options + .map( + (value) => DropdownMenuItem(value: value, child: Text(value)), + ) + .toList(), + onChanged: onFloorChanged, + ), + ], + ), ); } From 5b8a8c7e87b0edd9ec264c4675c4eb6c1b66e721 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Tue, 2 Dec 2025 15:30:36 -0600 Subject: [PATCH 08/14] removed overview layer name --- lib/src/building_explorer/building_explorer.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index 3c5feba..7d097aa 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -20,12 +20,10 @@ class BuildingExplorer extends StatefulWidget { const BuildingExplorer({ required this.buildingSceneLayer, super.key, - this.overviewSublayerName = 'Overview', this.fullModelSublayerName = 'Full Model', }); final BuildingSceneLayer buildingSceneLayer; - final String overviewSublayerName; final String fullModelSublayerName; @override From 72ee4fa2244d9f4aa986dfc7d6e711a3ec8c211e Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Wed, 3 Dec 2025 16:21:10 -0600 Subject: [PATCH 09/14] PR updates - Refactored BuildingSublayerSelector into smaller widgets - changed BuildingExplorer into a StatelessWidget - Added safety in comparator for String values that should represent integers --- example/lib/example_building_explorer.dart | 18 +++-- lib/arcgis_maps_toolkit.dart | 3 +- .../building_category_list.dart | 50 ++++++++++++ .../building_category_selector.dart | 61 +++++++++++++++ .../building_explorer/building_explorer.dart | 21 ++--- .../building_floor_level_selector.dart | 9 ++- .../building_sublayer_selector.dart | 77 ------------------- 7 files changed, 137 insertions(+), 102 deletions(-) create mode 100644 lib/src/building_explorer/building_category_list.dart create mode 100644 lib/src/building_explorer/building_category_selector.dart delete mode 100644 lib/src/building_explorer/building_sublayer_selector.dart diff --git a/example/lib/example_building_explorer.dart b/example/lib/example_building_explorer.dart index a74d76b..8da29a3 100644 --- a/example/lib/example_building_explorer.dart +++ b/example/lib/example_building_explorer.dart @@ -35,7 +35,7 @@ class _ExampleBuildingExplorerState extends State { final _localSceneViewController = ArcGISLocalSceneView.createController(); // Building scene layer that will be filtered. Set after the WebScene is loaded. - late final BuildingSceneLayer _buildingSceneLayer; + BuildingSceneLayer? _buildingSceneLayer; @override Widget build(BuildContext context) { @@ -59,7 +59,9 @@ class _ExampleBuildingExplorerState extends State { Center( // Button to show the building filter settings sheet. child: ElevatedButton( - onPressed: showBuildingExplorerModal, + onPressed: _buildingSceneLayer != null + ? showBuildingExplorerModal + : null, child: const Text('Building Filter Settings'), ), ), @@ -74,7 +76,7 @@ class _ExampleBuildingExplorerState extends State { void showBuildingExplorerModal() { showModalBottomSheet( context: context, - builder: (BuildContext context) { + builder: (context) { return Container( height: 400, // Define the height of the bottom sheet color: Colors.white, @@ -93,7 +95,7 @@ class _ExampleBuildingExplorerState extends State { ), Expanded( child: BuildingExplorer( - buildingSceneLayer: _buildingSceneLayer, + buildingSceneLayer: _buildingSceneLayer!, ), ), ], @@ -115,9 +117,11 @@ class _ExampleBuildingExplorerState extends State { await scene.load(); // Get the BuildingSceneLayer from the webmap. - _buildingSceneLayer = scene.operationalLayers - .whereType() - .first; + final buildingSceneLayers = scene.operationalLayers + .whereType(); + if (buildingSceneLayers.isNotEmpty) { + setState(() => _buildingSceneLayer = buildingSceneLayers.first); + } // Apply the scene to the local scene view controller. _localSceneViewController.arcGISScene = scene; diff --git a/lib/arcgis_maps_toolkit.dart b/lib/arcgis_maps_toolkit.dart index cee548b..3fb3374 100644 --- a/lib/arcgis_maps_toolkit.dart +++ b/lib/arcgis_maps_toolkit.dart @@ -47,9 +47,10 @@ part 'src/authenticator/authenticator_certificate_password.dart'; part 'src/authenticator/authenticator_login.dart'; part 'src/authenticator/authenticator_trust.dart'; // Building Explorer Widget +part 'src/building_explorer/building_category_list.dart'; +part 'src/building_explorer/building_category_selector.dart'; part 'src/building_explorer/building_explorer.dart'; part 'src/building_explorer/building_floor_level_selector.dart'; -part 'src/building_explorer/building_sublayer_selector.dart'; // Compass Widget part 'src/compass/compass.dart'; part 'src/compass/compass_needle_painter.dart'; diff --git a/lib/src/building_explorer/building_category_list.dart b/lib/src/building_explorer/building_category_list.dart new file mode 100644 index 0000000..54a8137 --- /dev/null +++ b/lib/src/building_explorer/building_category_list.dart @@ -0,0 +1,50 @@ +// +// Copyright 2025 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 +// +// https://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. +// + +part of '../../arcgis_maps_toolkit.dart'; + +class BuildingCategoryList extends StatelessWidget { + const BuildingCategoryList({ + required this.buildingSceneLayer, + required this.fullModelSublayerName, + super.key, + }); + + final BuildingSceneLayer buildingSceneLayer; + final String fullModelSublayerName; + + @override + Widget build(BuildContext context) { + final fullModelGroupSublayer = buildingSceneLayer.sublayers + .whereType() + .where((sublayer) => sublayer.name == 'Full Model') + .firstOrNull; + + final categoryGroupSublayers = + fullModelGroupSublayer?.sublayers.whereType() ?? + []; + + return ListView.builder( + itemCount: categoryGroupSublayers.length, + itemBuilder: (context, index) { + final categoryGroupSublayer = categoryGroupSublayers.elementAt(index); + return BuildingCategorySelector( + buildingCategory: categoryGroupSublayer, + ); + }, + ); + } +} diff --git a/lib/src/building_explorer/building_category_selector.dart b/lib/src/building_explorer/building_category_selector.dart new file mode 100644 index 0000000..ecd9335 --- /dev/null +++ b/lib/src/building_explorer/building_category_selector.dart @@ -0,0 +1,61 @@ +// +// Copyright 2025 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 +// +// https://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. +// + +part of '../../arcgis_maps_toolkit.dart'; + +class BuildingCategorySelector extends StatefulWidget { + const BuildingCategorySelector({required this.buildingCategory, super.key}); + + final BuildingGroupSublayer buildingCategory; + + @override + State createState() => _BuildingCategorySelectorState(); +} + +class _BuildingCategorySelectorState extends State { + @override + Widget build(BuildContext context) { + final componentSublayers = widget.buildingCategory.sublayers; + return ExpansionTile( + title: Row( + children: [ + Text(widget.buildingCategory.name), + const Spacer(), + Checkbox( + value: widget.buildingCategory.isVisible, + onChanged: (val) { + setState(() { + widget.buildingCategory.isVisible = val ?? false; + }); + }, + ), + ], + ), + children: componentSublayers.map((componentSublayer) { + return CheckboxListTile( + title: Text(componentSublayer.name), + value: componentSublayer.isVisible, + enabled: widget.buildingCategory.isVisible, + onChanged: (val) { + setState(() { + componentSublayer.isVisible = val ?? false; + }); + }, + ); + }).toList(), + ); + } +} diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index 7d097aa..038e75e 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -16,42 +16,35 @@ part of '../../arcgis_maps_toolkit.dart'; -class BuildingExplorer extends StatefulWidget { +class BuildingExplorer extends StatelessWidget { const BuildingExplorer({ required this.buildingSceneLayer, - super.key, this.fullModelSublayerName = 'Full Model', + super.key, }); final BuildingSceneLayer buildingSceneLayer; final String fullModelSublayerName; - @override - State createState() => _BuildingExplorerState(); -} - -class _BuildingExplorerState extends State { @override Widget build(BuildContext context) { return Column( children: [ Text( - widget.buildingSceneLayer.name, + buildingSceneLayer.name, style: Theme.of(context).textTheme.headlineSmall, ), const Divider(), - _BuildingFloorLevelSelector( - buildingSceneLayer: widget.buildingSceneLayer, - ), + _BuildingFloorLevelSelector(buildingSceneLayer: buildingSceneLayer), const Divider(), Text( 'Disciplines & Categories:', style: Theme.of(context).textTheme.bodyLarge, ), Expanded( - child: _BuildingSublayerSelector( - buildingSceneLayer: widget.buildingSceneLayer, - fullModelSublayerName: widget.fullModelSublayerName, + child: BuildingCategoryList( + buildingSceneLayer: buildingSceneLayer, + fullModelSublayerName: fullModelSublayerName, ), ), ], diff --git a/lib/src/building_explorer/building_floor_level_selector.dart b/lib/src/building_explorer/building_floor_level_selector.dart index 25aafd2..e4a3f2d 100644 --- a/lib/src/building_explorer/building_floor_level_selector.dart +++ b/lib/src/building_explorer/building_floor_level_selector.dart @@ -47,11 +47,10 @@ class _BuildingFloorLevelSelectorState return Padding( padding: const EdgeInsets.fromLTRB(15, 0, 20, 0), child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Text('Select Level:', style: Theme.of(context).textTheme.bodyLarge), const Spacer(), - DropdownButton( + DropdownButton( value: _selectedFloor, items: options .map( @@ -71,7 +70,11 @@ class _BuildingFloorLevelSelectorState if (statistics['BldgLevel'] != null) { final floorList = []; floorList.addAll(statistics['BldgLevel']!.mostFrequentValues); - floorList.sort((a, b) => int.parse(b).compareTo(int.parse(a))); + floorList.sort((a, b) { + final intA = int.tryParse(a) ?? 0; + final intB = int.tryParse(b) ?? 0; + return intB.compareTo(intA); + }); setState(() { _floorList = floorList; }); diff --git a/lib/src/building_explorer/building_sublayer_selector.dart b/lib/src/building_explorer/building_sublayer_selector.dart deleted file mode 100644 index 9be50e5..0000000 --- a/lib/src/building_explorer/building_sublayer_selector.dart +++ /dev/null @@ -1,77 +0,0 @@ -// -// Copyright 2025 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 -// -// https://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. -// - -part of '../../arcgis_maps_toolkit.dart'; - -// Widget to show and select building sublayers. -class _BuildingSublayerSelector extends StatefulWidget { - const _BuildingSublayerSelector({ - required this.buildingSceneLayer, - required this.fullModelSublayerName, - }); - final BuildingSceneLayer buildingSceneLayer; - final String fullModelSublayerName; - - @override - State<_BuildingSublayerSelector> createState() => - _BuildingSublayerSelectorState(); -} - -class _BuildingSublayerSelectorState extends State<_BuildingSublayerSelector> { - @override - Widget build(BuildContext context) { - final fullModelSublayer = - widget.buildingSceneLayer.sublayers.firstWhere( - (sublayer) => sublayer.name == 'Full Model', - ) - as BuildingGroupSublayer; - final categorySublayers = fullModelSublayer.sublayers; - return ListView( - children: categorySublayers.map((categorySublayer) { - final componentSublayers = - (categorySublayer as BuildingGroupSublayer).sublayers; - return ExpansionTile( - title: Row( - children: [ - Text(categorySublayer.name), - const Spacer(), - Checkbox( - value: categorySublayer.isVisible, - onChanged: (val) { - setState(() { - categorySublayer.isVisible = val ?? false; - }); - }, - ), - ], - ), - children: componentSublayers.map((componentSublayer) { - return CheckboxListTile( - title: Text(componentSublayer.name), - value: componentSublayer.isVisible, - enabled: categorySublayer.isVisible, - onChanged: (val) { - setState(() { - componentSublayer.isVisible = val ?? false; - }); - }, - ); - }).toList(), - ); - }).toList(), - ); - } -} From a7146b0cbaf655efc013de82952f4c9da25628f7 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Thu, 4 Dec 2025 11:30:01 -0600 Subject: [PATCH 10/14] Added onClose callback to widget --- example/lib/example_building_explorer.dart | 21 ++----------- .../building_explorer/building_explorer.dart | 31 +++++++++++++++++-- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/example/lib/example_building_explorer.dart b/example/lib/example_building_explorer.dart index 8da29a3..1ea5eb5 100644 --- a/example/lib/example_building_explorer.dart +++ b/example/lib/example_building_explorer.dart @@ -81,24 +81,9 @@ class _ExampleBuildingExplorerState extends State { height: 400, // Define the height of the bottom sheet color: Colors.white, child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close), - ), - ], - ), - Expanded( - child: BuildingExplorer( - buildingSceneLayer: _buildingSceneLayer!, - ), - ), - ], + child: BuildingExplorer( + buildingSceneLayer: _buildingSceneLayer!, + onClose: () => Navigator.pop(context), ), ), ); diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index 038e75e..b9344ae 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -20,19 +20,44 @@ class BuildingExplorer extends StatelessWidget { const BuildingExplorer({ required this.buildingSceneLayer, this.fullModelSublayerName = 'Full Model', + this.onClose, super.key, }); + // BuildingSceneLayer that this widget explores final BuildingSceneLayer buildingSceneLayer; + + // Name of the full model group sublayer final String fullModelSublayerName; + // Optional onClose callback. If set, a close IconButton will appear to the + //right of the widget title. + final VoidCallback? onClose; + @override Widget build(BuildContext context) { return Column( children: [ - Text( - buildingSceneLayer.name, - style: Theme.of(context).textTheme.headlineSmall, + Stack( + alignment: Alignment.center, + children: [ + // Centered title + Text( + buildingSceneLayer.name, + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + // Right-justified icon button + if (onClose != null) + Align( + alignment: Alignment.centerRight, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: onClose, + tooltip: 'Close', + ), + ), + ], ), const Divider(), _BuildingFloorLevelSelector(buildingSceneLayer: buildingSceneLayer), From f03cedfee6fc88800b9f49974f87fc509babde13 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Fri, 5 Dec 2025 09:38:23 -0600 Subject: [PATCH 11/14] Currently selected floor now initialized - Fixed issue where currently selected floor was lost between instances of widget - Replaced hard-coded strings with final String members. --- .../building_floor_level_selector.dart | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/src/building_explorer/building_floor_level_selector.dart b/lib/src/building_explorer/building_floor_level_selector.dart index e4a3f2d..cf08f3e 100644 --- a/lib/src/building_explorer/building_floor_level_selector.dart +++ b/lib/src/building_explorer/building_floor_level_selector.dart @@ -34,11 +34,19 @@ class _BuildingFloorLevelSelectorState // A listing of all floors in the building scene layer. var _floorList = []; + // Name constants + final _filterName = 'Floor filter'; + final _floorBlockName = 'solid block'; + final _xrayBlockName = 'xray block'; + final _buildingLevelAttribute = 'BldgLevel'; + @override void initState() { super.initState(); - _initFloorList(); + // Get the floor listing from the layer statistics, then look for a + //currently selected floor level. + _initFloorList().then((value) => _initSelectedFloor()); } @override @@ -67,9 +75,9 @@ class _BuildingFloorLevelSelectorState Future _initFloorList() async { // Get the floor listing from the statistics. final statistics = await widget.buildingSceneLayer.fetchStatistics(); - if (statistics['BldgLevel'] != null) { + if (statistics[_buildingLevelAttribute] != null) { final floorList = []; - floorList.addAll(statistics['BldgLevel']!.mostFrequentValues); + floorList.addAll(statistics[_buildingLevelAttribute]!.mostFrequentValues); floorList.sort((a, b) { final intA = int.tryParse(a) ?? 0; final intB = int.tryParse(b) ?? 0; @@ -81,6 +89,23 @@ class _BuildingFloorLevelSelectorState } } + void _initSelectedFloor() { + final activeFilter = widget.buildingSceneLayer.activeFilter; + if (activeFilter != null) { + if (activeFilter.name == _filterName) { + // Get the selected floor from the where clause of the solid filter block. + final floorBlock = activeFilter.blocks + .where((block) => block.title == _floorBlockName) + .firstOrNull; + if (floorBlock != null) { + setState( + () => _selectedFloor = floorBlock.whereClause.split(' ').last, + ); + } + } + } + } + void onFloorChanged(String? floor) { if (floor == null) return; @@ -99,17 +124,17 @@ class _BuildingFloorLevelSelectorState // Build a building filter to show the selected floor and an xray view of the floors below. // Floors above the selected floor are not shown at all. final buildingFilter = BuildingFilter( - name: 'Floor filter', + name: _filterName, description: 'Show selected floor and xray filter for lower floors.', blocks: [ BuildingFilterBlock( - title: 'solid block', - whereClause: 'BldgLevel = $_selectedFloor', + title: _floorBlockName, + whereClause: '$_buildingLevelAttribute = $_selectedFloor', mode: BuildingSolidFilterMode(), ), BuildingFilterBlock( - title: 'xray block', - whereClause: 'BldgLevel < $_selectedFloor', + title: _xrayBlockName, + whereClause: '$_buildingLevelAttribute < $_selectedFloor', mode: BuildingXrayFilterMode(), ), ], From 01561edbe31cf7a5a27736892c42f4e796db6df2 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Fri, 5 Dec 2025 13:13:22 -0600 Subject: [PATCH 12/14] PR updates --- .../building_explorer/building_category_list.dart | 13 +++++++------ .../building_category_selector.dart | 6 +++--- lib/src/building_explorer/building_explorer.dart | 4 ++-- .../building_floor_level_selector.dart | 6 +++--- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/src/building_explorer/building_category_list.dart b/lib/src/building_explorer/building_category_list.dart index 54a8137..acf2dd7 100644 --- a/lib/src/building_explorer/building_category_list.dart +++ b/lib/src/building_explorer/building_category_list.dart @@ -16,11 +16,10 @@ part of '../../arcgis_maps_toolkit.dart'; -class BuildingCategoryList extends StatelessWidget { - const BuildingCategoryList({ +class _BuildingCategoryList extends StatelessWidget { + const _BuildingCategoryList({ required this.buildingSceneLayer, required this.fullModelSublayerName, - super.key, }); final BuildingSceneLayer buildingSceneLayer; @@ -34,14 +33,16 @@ class BuildingCategoryList extends StatelessWidget { .firstOrNull; final categoryGroupSublayers = - fullModelGroupSublayer?.sublayers.whereType() ?? + fullModelGroupSublayer?.sublayers + .whereType() + .toList() ?? []; return ListView.builder( itemCount: categoryGroupSublayers.length, itemBuilder: (context, index) { - final categoryGroupSublayer = categoryGroupSublayers.elementAt(index); - return BuildingCategorySelector( + final categoryGroupSublayer = categoryGroupSublayers[index]; + return _BuildingCategorySelector( buildingCategory: categoryGroupSublayer, ); }, diff --git a/lib/src/building_explorer/building_category_selector.dart b/lib/src/building_explorer/building_category_selector.dart index ecd9335..dc79dc3 100644 --- a/lib/src/building_explorer/building_category_selector.dart +++ b/lib/src/building_explorer/building_category_selector.dart @@ -16,8 +16,8 @@ part of '../../arcgis_maps_toolkit.dart'; -class BuildingCategorySelector extends StatefulWidget { - const BuildingCategorySelector({required this.buildingCategory, super.key}); +class _BuildingCategorySelector extends StatefulWidget { + const _BuildingCategorySelector({required this.buildingCategory}); final BuildingGroupSublayer buildingCategory; @@ -25,7 +25,7 @@ class BuildingCategorySelector extends StatefulWidget { State createState() => _BuildingCategorySelectorState(); } -class _BuildingCategorySelectorState extends State { +class _BuildingCategorySelectorState extends State<_BuildingCategorySelector> { @override Widget build(BuildContext context) { final componentSublayers = widget.buildingCategory.sublayers; diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index b9344ae..9bdb8a7 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -31,7 +31,7 @@ class BuildingExplorer extends StatelessWidget { final String fullModelSublayerName; // Optional onClose callback. If set, a close IconButton will appear to the - //right of the widget title. + // right of the widget title. final VoidCallback? onClose; @override @@ -67,7 +67,7 @@ class BuildingExplorer extends StatelessWidget { style: Theme.of(context).textTheme.bodyLarge, ), Expanded( - child: BuildingCategoryList( + child: _BuildingCategoryList( buildingSceneLayer: buildingSceneLayer, fullModelSublayerName: fullModelSublayerName, ), diff --git a/lib/src/building_explorer/building_floor_level_selector.dart b/lib/src/building_explorer/building_floor_level_selector.dart index cf08f3e..645c249 100644 --- a/lib/src/building_explorer/building_floor_level_selector.dart +++ b/lib/src/building_explorer/building_floor_level_selector.dart @@ -45,8 +45,8 @@ class _BuildingFloorLevelSelectorState super.initState(); // Get the floor listing from the layer statistics, then look for a - //currently selected floor level. - _initFloorList().then((value) => _initSelectedFloor()); + // currently selected floor level. + _initFloorList().then((_) => _initSelectedFloor()); } @override @@ -56,7 +56,7 @@ class _BuildingFloorLevelSelectorState padding: const EdgeInsets.fromLTRB(15, 0, 20, 0), child: Row( children: [ - Text('Select Level:', style: Theme.of(context).textTheme.bodyLarge), + Text('Level:', style: Theme.of(context).textTheme.bodyLarge), const Spacer(), DropdownButton( value: _selectedFloor, From 8207f73f9f38ec8717446077ee7fb2ded0f29c67 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Fri, 5 Dec 2025 15:29:35 -0600 Subject: [PATCH 13/14] Added class comment for main widget --- .../building_explorer/building_explorer.dart | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index 9bdb8a7..4776a56 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -16,6 +16,48 @@ part of '../../arcgis_maps_toolkit.dart'; +/// A widget that enables a user to explore a building model in a [BuildingSceneLayer]. +/// +/// # Overview +/// The Building Explorer widget provides a tool for users to browse the floors and sublayers of a building scene layer. The widget can highlight specified floors and show or hide building features of different categories and subcategories. +/// +/// ## Features +/// Features of the Building Explorer widget include: +/// * Showing the name of the layer as the title of the widget. +/// * Selecting a level of the building to highlight in the view. +/// * The selected level and all of the features of the level are rendered normally. +/// * Levels above are hidden. +/// * Levels below are given an Xray style. +/// * Visibility of building feature categories and subcategories can be toggled on and off. +/// * The widget can present a close button when provided with an onClose callback function. +/// +/// ## Usage +/// A [BuildingExplorer] widget is created with the following parameters: +/// * buildingSceneLayer: The [BuildingSceneLayer] that this widget will be exploring +/// * fullModelSublayerName: An optional [String] that is the name of the full model sublayer. Default is “Full Model”. +/// * onClose: An optional callback that is called when the close button of the widget is tapped. If a callback is not provided, the close button will be hidden. +/// +/// One use case is to wrap the [BuildingExplorer] in a [BottomSheet] +/// ```dart +/// void showBuildingExplorerModal() { +/// showModalBottomSheet( +/// context: context, +/// builder: (context) { +/// return Container( +/// height: 400, +/// color: Colors.white, +/// child: Center( +/// child: BuildingExplorer( +/// buildingSceneLayer: _buildingSceneLayer!, +/// onClose: () => Navigator.pop(context), +/// ), +/// ), +/// ); +/// }, +/// ); +/// } +/// ``` + class BuildingExplorer extends StatelessWidget { const BuildingExplorer({ required this.buildingSceneLayer, From f7cbd684a743ebe243889523a2c7187822fc8896 Mon Sep 17 00:00:00 2001 From: Kevin Mueller Date: Fri, 5 Dec 2025 15:42:28 -0600 Subject: [PATCH 14/14] doc comments added --- lib/src/building_explorer/building_explorer.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/src/building_explorer/building_explorer.dart b/lib/src/building_explorer/building_explorer.dart index 4776a56..7df5fe5 100644 --- a/lib/src/building_explorer/building_explorer.dart +++ b/lib/src/building_explorer/building_explorer.dart @@ -57,7 +57,6 @@ part of '../../arcgis_maps_toolkit.dart'; /// ); /// } /// ``` - class BuildingExplorer extends StatelessWidget { const BuildingExplorer({ required this.buildingSceneLayer, @@ -66,14 +65,13 @@ class BuildingExplorer extends StatelessWidget { super.key, }); - // BuildingSceneLayer that this widget explores + /// BuildingSceneLayer that this widget explores final BuildingSceneLayer buildingSceneLayer; - // Name of the full model group sublayer + /// Name of the full model group sublayer final String fullModelSublayerName; - // Optional onClose callback. If set, a close IconButton will appear to the - // right of the widget title. + /// Optional onClose callback. If set, a close [IconButton] will appear at the top right of the widget. final VoidCallback? onClose; @override @@ -83,13 +81,13 @@ class BuildingExplorer extends StatelessWidget { Stack( alignment: Alignment.center, children: [ - // Centered title + // Building scene layer name centered Text( buildingSceneLayer.name, style: Theme.of(context).textTheme.headlineSmall, textAlign: TextAlign.center, ), - // Right-justified icon button + // Right-justified close icon button if (onClose != null) Align( alignment: Alignment.centerRight,