// Copyright 2015 The Project Buendia Authors // // 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: http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software distrib- // uted 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 // specific language governing permissions and limitations under the License. package org.projectbuendia.client.models; import android.database.ContentObserver; import android.support.annotation.Nullable; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.ImmutableSortedSet; import org.projectbuendia.client.utils.Logger; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A tree containing a hierarchy of {@link Location} objects, where the root is assumed to be a * single medical center. */ public class LocationTree implements Observable { public static final int ABSOLUTE_DEPTH_ROOT = 0; public static final int ABSOLUTE_DEPTH_ZONE = 1; public static final int ABSOLUTE_DEPTH_TENT = 2; public static final int ABSOLUTE_DEPTH_BED = 3; private static final Logger LOG = Logger.create(); private final TypedCursor<Location> mCursor; private final Location mRoot; private final Map<String, Location> mUuidsToLocations; private final Map<String, Location> mUuidsToParents; private final ImmutableSetMultimap<String, Location> mUuidsToChildren; /** * Creates a {@link LocationTree} from a {@link TypedCursor} of {@link Location}s. * If there are no locations in the local database, the location tree will have a null * root node (i.e. getRoot() == null). * <p/> * <p>Callers must call {@link #close} when done with an instance of this class. * @throws IllegalArgumentException if the location tree contains multiple root nodes or if the * the location tree has no root node or if the location tree * contains any nodes whose parents are missing */ public static LocationTree forTypedCursor(TypedCursor<Location> cursor) { Location root = null; Map<String, Location> uuidsToLocations = new HashMap<>(); Map<String, Location> uuidsToParents = new HashMap<>(); ImmutableSetMultimap.Builder<String, Location> uuidsToChildrenBuilder = ImmutableSetMultimap.builder(); // First, create mappings from location UUIDs to the locations themselves and to their // children. for (Location location : cursor) { if (location.parentUuid == null) { if (root != null) { LOG.w( "Creating location tree with multiple root nodes. Both location '" + root.name + "' (UUID '" + root.uuid + "') and location '" + location.name + "' (UUID '" + location.uuid + "') have " + "no parent nodes. The first location will be considered " + "the root node."); } root = location; } else { uuidsToChildrenBuilder.put(location.parentUuid, location); } uuidsToLocations.put(location.uuid, location); } if (root == null) { LOG.w("Creating a location tree with no root node. This tree has no data."); return new LocationTree( cursor, null, uuidsToLocations, uuidsToParents, uuidsToChildrenBuilder.build()); } // Then, create a mapping from location UUIDs to their parents. for (Location location : uuidsToLocations.values()) { if (location.parentUuid == null) continue; Location parent = uuidsToLocations.get(location.parentUuid); if (parent == null) { // TODO: Consider making this a warning rather than an exception. throw new IllegalArgumentException( "Unable to create tree because a location's parent does not exist. " + "Location '" + location.name + "' (UUID '" + location.uuid + "' points to parent location with UUID '" + location.parentUuid + "', which does not exist."); } uuidsToParents.put(location.uuid, parent); } return new LocationTree( cursor, root, uuidsToLocations, uuidsToParents, uuidsToChildrenBuilder.build()); } @Nullable public Location getRoot() { return mRoot; } /** * Returns all immediate children of a given {@link Location}, or an empty set if the * {@link Location} is null or has no children. */ public ImmutableSet<Location> getChildren(@Nullable Location location) { if (location == null) { return ImmutableSet.of(); } ImmutableSet<Location> children = mUuidsToChildren.get(location.uuid); return children == null ? ImmutableSet.<Location> of() : children; } /** * Returns the sorted descendants of the root location at the specified absolute depth. * <p/> * <p>The named values {@link #ABSOLUTE_DEPTH_ROOT}, {@link #ABSOLUTE_DEPTH_ZONE}, * {@link #ABSOLUTE_DEPTH_TENT}, and {@link #ABSOLUTE_DEPTH_BED} can be used for the * {@code level} parameter. */ public ImmutableSortedSet<Location> getDescendantsAtDepth(int absoluteDepth) { return getDescendantsAtDepth(mRoot, absoluteDepth); } /** * Returns the sorted descendants of the specified location at the specified depth relative to * that location. */ public ImmutableSortedSet<Location> getDescendantsAtDepth( Location location, int relativeDepth) { if (location == null) { return ImmutableSortedSet.of(); } if (relativeDepth == 0) { ImmutableSortedSet.Builder<Location> thisLocationSet = ImmutableSortedSet.orderedBy(new LocationComparator(this)); thisLocationSet.add(location); return thisLocationSet.build(); } ImmutableSortedSet.Builder<Location> descendants = ImmutableSortedSet.orderedBy(new LocationComparator(this)); for (Location child : getChildren(location)) { descendants.addAll(getDescendantsAtDepth(child, relativeDepth - 1)); } return descendants.build(); } /** * Returns a {@link List} representing a branch of {@link Location}s starting at the root * of the location tree and terminating at the given {@link Location}. */ public List<Location> getAncestorsStartingFromRoot(Location node) { List<Location> result = new ArrayList<>(); Location current = node; while (current != null) { result.add(current); current = getParent(current); } Collections.reverse(result); return result; } /** Returns the parent of a given {@link Location}. */ @Nullable public Location getParent(@Nullable Location location) { if (location == null) { return null; } return mUuidsToParents.get(location.uuid); } @Nullable public Location findByUuid(String uuid) { return mUuidsToLocations.get(uuid); } /** * Returns a list of all AppLocations within a subtree rooted at the given {@link Location}. * @param subroot the Location that will form the root of the subtree * @return a List of AppLocations in a subtree with the given root */ public List<Location> locationsInSubtree(Location subroot) { List<Location> result = new ArrayList<>(); result.add(subroot); addChildrenToCollection(result, subroot); return result; } /** Returns the total number of patients in this location and its descendant locations. */ public long getTotalPatientCount(Location location) { if (location == null) { return 0; } long count = location.patientCount; for (Location child : getChildren(location)) { count += getTotalPatientCount(child); } return count; } @Override public void registerContentObserver(ContentObserver observer) { mCursor.registerContentObserver(observer); } @Override public void unregisterContentObserver(ContentObserver observer) { mCursor.unregisterContentObserver(observer); } @Override public void close() { mCursor.close(); } private LocationTree( TypedCursor<Location> cursor, Location root, Map<String, Location> uuidsToLocations, Map<String, Location> uuidsToParents, ImmutableSetMultimap<String, Location> uuidsToChildren) { mCursor = cursor; mRoot = root; mUuidsToLocations = uuidsToLocations; mUuidsToParents = uuidsToParents; mUuidsToChildren = uuidsToChildren; } private void addChildrenToCollection(Collection<Location> collection, Location root) { for (Location child : getChildren(root)) { collection.add(child); addChildrenToCollection(collection, child); } } }