Skip to content
Draft
114 changes: 114 additions & 0 deletions example/lib/example_building_explorer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +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.
//

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<ExampleBuildingExplorer> createState() =>
_ExampleBuildingExplorerState();
}

class _ExampleBuildingExplorerState extends State<ExampleBuildingExplorer> {
// 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.
BuildingSceneLayer? _buildingSceneLayer;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Building Explorer')),
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: _buildingSceneLayer != null
? showBuildingExplorerModal
: null,
child: const Text('Building Filter Settings'),
),
),
],
),
],
),
),
);
}

void showBuildingExplorerModal() {
showModalBottomSheet(
context: context,
builder: (context) {
return Container(
height: 400, // Define the height of the bottom sheet
color: Colors.white,
child: Center(
child: BuildingExplorer(
buildingSceneLayer: _buildingSceneLayer!,
onClose: () => Navigator.pop(context),
),
),
);
},
);
}

Future<void> 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.
final buildingSceneLayers = scene.operationalLayers
.whereType<BuildingSceneLayer>();
if (buildingSceneLayers.isNotEmpty) {
setState(() => _buildingSceneLayer = buildingSceneLayers.first);
}

// Apply the scene to the local scene view controller.
_localSceneViewController.arcGISScene = scene;
}
}
6 changes: 6 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions lib/arcgis_maps_toolkit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ 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_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';
// Compass Widget
part 'src/compass/compass.dart';
part 'src/compass/compass_needle_painter.dart';
Expand Down
51 changes: 51 additions & 0 deletions lib/src/building_explorer/building_category_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// 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,
});

final BuildingSceneLayer buildingSceneLayer;
final String fullModelSublayerName;

@override
Widget build(BuildContext context) {
final fullModelGroupSublayer = buildingSceneLayer.sublayers
.whereType<BuildingGroupSublayer>()
.where((sublayer) => sublayer.name == 'Full Model')
.firstOrNull;

final categoryGroupSublayers =
fullModelGroupSublayer?.sublayers
.whereType<BuildingGroupSublayer>()
.toList() ??
[];

return ListView.builder(
itemCount: categoryGroupSublayers.length,
itemBuilder: (context, index) {
final categoryGroupSublayer = categoryGroupSublayers[index];
return _BuildingCategorySelector(
buildingCategory: categoryGroupSublayer,
);
},
);
}
}
61 changes: 61 additions & 0 deletions lib/src/building_explorer/building_category_selector.dart
Original file line number Diff line number Diff line change
@@ -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});

final BuildingGroupSublayer buildingCategory;

@override
State<StatefulWidget> createState() => _BuildingCategorySelectorState();
}

class _BuildingCategorySelectorState extends State<_BuildingCategorySelector> {
@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(),
);
}
}
118 changes: 118 additions & 0 deletions lib/src/building_explorer/building_explorer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// 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';

/// 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,
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 at the top right of the widget.
final VoidCallback? onClose;

@override
Widget build(BuildContext context) {
return Column(
children: [
Stack(
alignment: Alignment.center,
children: [
// Building scene layer name centered
Text(
buildingSceneLayer.name,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
// Right-justified close 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),
const Divider(),
Text(
'Disciplines & Categories:',
style: Theme.of(context).textTheme.bodyLarge,
),
Expanded(
child: _BuildingCategoryList(
buildingSceneLayer: buildingSceneLayer,
fullModelSublayerName: fullModelSublayerName,
),
),
],
);
}
}
Loading