| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| import 'package:logging/logging.dart'; |
| import 'package:meta/meta.dart'; |
| import '../../surface/surface.dart'; |
| import '../../surface/surface_relation.dart'; |
| import './_surface_node.dart'; |
| |
| /// A logger for Surface Tree warnings |
| Logger log = Logger.root; |
| |
| /// A SurfaceTree of SurfaceNodes |
| class SurfaceTree extends Iterable<Surface> { |
| /// Construct [SurfaceTree] |
| |
| /// The root of the SurfaceTree. SurfaceNodes added to the Tree without |
| /// specifying parents are added to the root. |
| final SurfaceNode kRoot = SurfaceNode(surface: Surface(surfaceId: '_kRoot_')); |
| |
| /// Map from SurfaceIds to SurfaceNodes, for all the SurfaceNodes in the Forest |
| final Map<String, SurfaceNode> _nodeMap = {}; |
| |
| /// Add a SurfaceNode node to the SurfaceTree. The node will be added to a |
| /// parent matching optional parentId, or to the root. |
| void _addNode({ |
| @required SurfaceNode node, |
| String parentId, |
| }) { |
| // If the Surface is already in the Tree, return; |
| if (_nodeMap.containsKey(node.surface.surfaceId)) { |
| log.warning( |
| 'Surface with id "${node.surface.surfaceId}" already in tree. ' |
| 'Use update() to modify existing Surfaces. ' |
| 'Surface will not be not re-added.'); |
| return; |
| } |
| // If an invalid parent was specified throw an error |
| if (parentId != null && !_nodeMap.containsKey(parentId)) { |
| throw (ArgumentError.value( |
| parentId, 'parentId', 'not found in SurfaceTree')); |
| } |
| // If a valid parent was specified, add to the parent |
| if (parentId != null) { |
| // add node as child of parent if we can find the parent |
| _nodeMap[parentId].add(childNode: node); |
| _nodeMap[node.surface.surfaceId] = node; |
| // else add to the root |
| } else { |
| kRoot.add(childNode: node); |
| _nodeMap[node.surface.surfaceId] = node; |
| } |
| } |
| |
| /// Add a Surface to this Tree, optionally attach this Surface to the given |
| /// parent. |
| void add({ |
| @required Surface surface, |
| String parentId, |
| SurfaceRelation relationToParent, |
| }) { |
| SurfaceNode node = SurfaceNode(surface: surface); |
| if (relationToParent != null) { |
| node.relationToParent = relationToParent; |
| } |
| _addNode(node: node, parentId: parentId); |
| } |
| |
| /// Destructively removes the node from the SurfaceTree, and reparents any |
| /// children to the root |
| void remove({@required String surfaceId}) { |
| if (_nodeMap.containsKey(surfaceId)) { |
| SurfaceNode node = _nodeMap[surfaceId]; |
| _nodeMap.remove(surfaceId); |
| // detach the node from its parent |
| node.parentNode?.detach(childNode: node); |
| // reparent any children on the root of the tree |
| for (SurfaceNode child in node.childNodes) { |
| // make the children orphans |
| node.detach(childNode: child); |
| // remove them from the graph |
| _nodeMap.remove(child.surface.surfaceId); |
| // add them to the root |
| _addNode(node: child); |
| } |
| // remove the node from the nodeMap |
| } |
| } |
| |
| /// Update the [parentId] and the [relation] to the parent of a [Surface] |
| /// The Surface must exist in the [Tree] |
| /// NOTE! If the parentId is not specified, the child will be orphaned and |
| /// reparented on the root! |
| void update({ |
| @required Surface surface, |
| String parentId, |
| SurfaceRelation relation, |
| }) { |
| if (parentId == null && relation != null) { |
| throw (ArgumentError.value( |
| parentId, |
| 'parentId', |
| 'Relationship provided, but parent not specified. Relationships are' |
| 'between parents and children')); |
| } |
| SurfaceNode oldNode = _nodeMap[surface.surfaceId]; |
| SurfaceNode newNode = |
| SurfaceNode(surface: surface, relationToParent: relation); |
| |
| /// If the node had a parent, detach and reparent the updated node |
| if (parentId != null && _nodeMap[parentId] != null) { |
| _nodeMap[parentId] |
| ..detach(childNode: oldNode) |
| ..add(childNode: newNode); |
| } |
| _nodeMap[surface.surfaceId] = newNode; |
| } |
| |
| @override |
| Iterator<Surface> get iterator { |
| return flatten().iterator; |
| } |
| |
| /// Breadth first flattening of SurfaceNode |
| Iterable<Surface> flatten() { |
| return kRoot.flatten().map((f) => f.surface).toList() |
| ..remove(kRoot.surface); // The root is an implementation detail |
| } |
| |
| /// Find Surface by surfaceId |
| SurfaceNode findNode({String surfaceId}) => _nodeMap[surfaceId]; |
| |
| /// Reduces a Forest to a list of objects. |
| Iterable<V> reduceSurfaceTree<V>( |
| V f(Surface surface, Iterable<V> children)) => |
| kRoot.map((SurfaceNode t) => t.reduceSurfaceNode(f)); |
| |
| /// Get a flattened iterable of all of the values in the forest |
| Iterable get values => flatten(); |
| |
| @override |
| String toString() => 'SurfaceTree($kRoot)'; |
| |
| /// Creates a spanning tree with a given condition |
| SurfaceTree spanningTree( |
| {@required String startNodeId, @required bool condition(SurfaceNode s)}) { |
| SurfaceNode startNode; |
| if (_nodeMap.containsKey(startNodeId)) { |
| startNode = _nodeMap[startNodeId]; |
| } else { |
| log.warning('node $startNodeId not found in tree, ' |
| 'returning empty spanningTree'); |
| return SurfaceTree(); |
| } |
| // find the topmost point of the connected tree where the relationship holds |
| // (excluding the kRoot, which is not a valid Surface) |
| while (startNode?.parentNode != null && |
| condition(startNode) && |
| // the root not a valid surface |
| startNode?.parentNode != kRoot) { |
| startNode = startNode.parentNode; |
| } |
| // then return all the descendents who satisfy condition |
| SurfaceTree spanTree = SurfaceTree()..add(surface: startNode.surface); |
| __copyTreeWithConditional( |
| current: startNode, tree: spanTree, condition: condition); |
| return spanTree; |
| } |
| |
| // recursively search down surface nodes, adding children that match condition |
| // to tree; |
| void __copyTreeWithConditional( |
| {SurfaceNode current, SurfaceTree tree, bool condition(SurfaceNode s)}) { |
| for (SurfaceNode child in current.childNodes) { |
| if (condition(child)) { |
| tree.add(surface: child.surface, parentId: current.surface.surfaceId); |
| __copyTreeWithConditional( |
| current: child, tree: tree, condition: condition); |
| } |
| } |
| } |
| } |