/** * Copyright 2010 Google Inc. * * 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 * 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. * */ package org.waveprotocol.wave.client.paging; import com.google.common.base.Preconditions; import org.waveprotocol.wave.client.common.util.LinkedSequence; import org.waveprotocol.wave.client.common.util.MathUtil; import org.waveprotocol.wave.client.paging.Traverser.BlockSide; import org.waveprotocol.wave.client.paging.Traverser.MoveablePoint; import org.waveprotocol.wave.client.paging.Traverser.Point; import org.waveprotocol.wave.client.paging.Traverser.SimplePoint; import org.waveprotocol.wave.model.util.CollectionUtils; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Queue; /** * Maintains a continuous active (paged-in) region within block tree. * */ public final class ActiveRegion { /** * A point defining the start of an active region. */ static final class StartPoint extends MoveablePoint { private StartPoint(BlockSide side, Block block) { super(side, block); } static StartPoint startOf(Block block) { return new StartPoint(BlockSide.START, block); } static StartPoint endOf(Block block) { return new StartPoint(BlockSide.END, block); } static StartPoint nowhere() { return new StartPoint(null, null); } static StartPoint at(Point point) { return new StartPoint(point.side, point.block); } /** * Moves to the next point, adding any blocks left behind into a * collection. * * @param toExclude collector of blocks left behind */ void trawlNext(Collection<? super Block> toExclude) { // If this point moves over a block end, that block becomes excluded. if (side == BlockSide.END) { toExclude.add(block); } next(); } /** * Moves to the previous point, adding any new blocks included into a * collection. * * @param toInclude collector of new blocks */ void trawlPrevious(Collection<? super Block> toInclude) { // If this point moves into a block end, that block becomes included. previous(); if (side == BlockSide.END) { toInclude.add(block); } } /** * Moves through next points until the last point before a given position, * adding any blocks left behind into a collection. * * @param position location to move to, but not beyond * @param toExclude collector of new blocks encountered */ void trawlNextUntil(double position, Collection<? super Block> toExclude) { while (nextIfBefore(position, toExclude)) { // Condition does the work. } } /** * Moves through previous points until the first point before a given position, * adding any new blocks included into a collection. * * @param position location to move to, but not beyond * @param toInclude collector of new blocks encountered */ void trawlPreviousUntil(double position, Collection<? super Block> toInclude) { while (previousIfAfter(position, toInclude)) { // Condition does the work. } } /** * Moves to the next point, if the next point is before a position. * * @param position boundary * @param toExclude * @return true if moved. */ private boolean nextIfBefore(double position, Collection<? super Block> toExclude) { if (!hasNext()) { return false; } Block oldBlock = block; BlockSide oldSide = side; next(); // Moved too far? if (absoluteLocation() >= position) { // Oops. Backtrack. previous(); return false; } else { // Moved beyond the end of a block, so that block is now excluded. if (oldSide == BlockSide.END) { toExclude.add(oldBlock); } return true; } } /** * Moves to the previous point, if this point is at, or after, a position. * * @param position boundary * @return true if moved. */ private boolean previousIfAfter(double position, Collection<? super Block> toInclude) { if (hasPrevious() && absoluteLocation() >= position) { trawlPrevious(toInclude); return true; } else { return false; } } } /** * A point defining the end of an active region. */ final static class EndPoint extends MoveablePoint { private EndPoint(BlockSide side, Block block) { super(side, block); } static EndPoint startOf(Block block) { return new EndPoint(BlockSide.START, block); } static EndPoint endOf(Block block) { return new EndPoint(BlockSide.END, block); } static EndPoint nowhere() { return new EndPoint(null, null); } static EndPoint at(Point point) { return new EndPoint(point.side, point.block); } /** * Moves to the previous point, adding any blocks left behind into a * collection. * * @param toExclude collector of blocks left behind */ void trawlPrevious(Collection<? super Block> toExclude) { // If this point moves over a block start, that block becomes excluded. if (side == BlockSide.START) { toExclude.add(block); } previous(); } /** * Moves to the next point, adding any new blocks included into a * collection. * * @param toInclude collector of new blocks */ void trawlNext(Collection<? super Block> toInclude) { // If this point moves into a block start, that block becomes included. next(); if (side == BlockSide.START) { toInclude.add(block); } } /** * Moves through previous points until the first point after a given * position, adding any blocks left behind into a collection. * * @param position location to move to, but not beyond * @param toExclude collector of blocks left behind */ void trawlPreviousUntil(double position, Collection<? super Block> toExclude) { while (previousIfAfter(position, toExclude)) { // Condition does the work. } } /** * Moves through next points until the first point after a given position, * adding any new blocks included into a collection. * * @param position location to move to, but not beyond * @param toInclude collector of new blocks encountered */ void trawlNextUntil(double position, Collection<? super Block> toInclude) { while (nextIfBefore(position, toInclude)) { // Condition does the work. } } /** * Moves to the previous point, if the previous point is after a position. * * @param position boundary * @param toExclude * @return true if moved. */ private boolean previousIfAfter(double position, Collection<? super Block> toExclude) { if (!hasPrevious()) { return false; } Block oldBlock = block; BlockSide oldSide = side; previous(); // Moved too far? if (absoluteLocation() <= position) { // Oops. Backtrack. next(); return false; } else { // Moved beyond the end of a block, so that block is now excluded. if (oldSide == BlockSide.START) { toExclude.add(oldBlock); } return true; } } /** * Moves to the next point, if this point is at, or before, a position. * * @param position boundary * @return true if moved. */ private boolean nextIfBefore(double position, Collection<? super Block> toInclude) { if (hasNext() && absoluteLocation() <= position) { trawlNext(toInclude); return true; } else { return false; } } } /** Start of the active region. */ private final StartPoint start; /** End of the active region. */ private final EndPoint end; /** Root block of pageable tree. */ private final Block root; /** Paging queues. */ private final Queue<Block> toPageIn = CollectionUtils.createQueue(); private final Queue<Block> toPageOut = CollectionUtils.createQueue(); private ActiveRegion(Block root, StartPoint start, EndPoint end) { this.root = root; this.start = start; this.end = end; } /** * Creates an active region. * * @param root root of the block tree to activate. * @return a new active region. */ public static ActiveRegion over(Block root) { return new ActiveRegion(root, StartPoint.nowhere(), EndPoint.nowhere()); } /** @return true if a region has been activated. */ private boolean isActive() { return start.isActive(); } /** * Invalidates any positions cached in the terminal points. This needs to be * called on all public entry points whose control flow queries positions. */ private void invalidatePositionCache() { start.invalidateCachedOrigin(); end.invalidateCachedOrigin(); } /** * @return the location of the activated part of the content. */ public Region getActiveRegion() { invalidatePositionCache(); return isActive() ? RegionImpl.at(start.absoluteLocation(), end.absoluteLocation()) : null; } /** * Grows the active region at the end. * * @param n number of blocks to page in */ public double growDown(int n) { assert toPageIn.isEmpty(); // Reminder that every public method flushes this. PagingDebugHelper.maybeCheckBlocks(root); Preconditions.checkState(isActive(), "No active region"); while (toPageIn.size() < n && end.hasNext()) { end.trawlNext(toPageIn); } page(); return 0; } /** * Grows the active region at the start. * * @param n number of blocks to page in * @return the change in */ public double growUp(int n) { assert toPageIn.isEmpty(); // Reminder that every public method flushes this. PagingDebugHelper.maybeCheckBlocks(root); Preconditions.checkState(isActive(), "No active region"); double oldEndLocation = end.absoluteLocation(); while (toPageIn.size() < n && start.hasPrevious()) { start.trawlPrevious(toPageIn); } page(); double newEndLocation = end.absoluteLocation(); return newEndLocation - oldEndLocation; } /** * Ensures that all blocks within a given region are paged in, and all those * outside the region are paged out. * * @param viewport viewport region * @return by how much all preserved points (points that are active both * before and after this method call) have shifted due to activation. * If there are no preserved points, returns {@code 0}. */ public double activate(Region viewport) { PagingDebugHelper.maybeCheckBlocks(root); PagingDebugHelper.enterActivate(); Point toPreserve; // A point whose location should be preserved through paging. Region active = getActiveRegion(); if (active == null) { // Initialize Point init = Traverser.locateStartWithin(root, viewport.getStart()); if (init == null) { init = Traverser.locateEndWithin(root, viewport.getStart()); } init(init); expandTo(viewport); // Preserve the start of the viewport. No particular reason. toPreserve = start; } else { // Adjust from previous state. switch (OverlapKind.compare(viewport, active)) { case FULLY_AFTER: Point newStart = Traverser.locateStartAfter(end, viewport.getStart()); shrinkToEnd(); moveActivePoint(newStart); expandTo(viewport); toPreserve = start; break; case FULLY_BEFORE: Point newEnd = Traverser.locateEndBefore(start, viewport.getEnd()); shrinkToStart(); moveActivePoint(newEnd); expandTo(viewport); toPreserve = end; break; case ENCLOSED: toPreserve = SimplePoint.at(start); shrinkStartUntil(viewport.getStart()); shrinkEndUntil(viewport.getEnd()); break; case ENCLOSES: growStartUntil(viewport.getStart()); growEndUntil(viewport.getEnd()); toPreserve = SimplePoint.at(start); break; case INTERSECTS_BEFORE: toPreserve = SimplePoint.at(start); growStartUntil(viewport.getStart()); shrinkEndUntil(viewport.getEnd()); break; case INTERSECTS_AFTER: shrinkStartUntil(viewport.getStart()); growEndUntil(viewport.getEnd()); toPreserve = SimplePoint.at(start); break; default: throw new RuntimeException("invalid enum value"); } } assert enclosesClipped(viewport); assert !new HashSet<Block>(toPageIn).removeAll(toPageOut); double shift = 0; if (!toPageIn.isEmpty() || !toPageOut.isEmpty()) { double oldLocation = toPreserve.absoluteLocation(); page(); double newLocation = toPreserve.absoluteLocation(); shift += newLocation - oldLocation; // Paging may have changed block sizes, so another round of activation is // necessary. shift += activate(RegionImpl.at(viewport).moveBy(shift)); } else { return 0; } PagingDebugHelper.leaveActivate(); return shift; } /** * Initializes the active region at a point. * * @param init initial point. */ private void init(Point init) { assert !isActive(); // Set active ends, page in from root to initialising point. end.set(init); start.set(init); collectAncestors(true, toPageIn, start.block, null); } /** * Resets the active region to be nothing. */ public void reset() { shrinkToStart(); collectAncestors(false, toPageOut, start.block, null); end.clear(); start.clear(); page(); } /** * Collects the ancestors of a block (inclusive). * * @param direction true if parents are to be collected before children; otherwise, false * @param list collector * @param block block whose ancestry is to be collected. * @param to terminal (null for all) */ private static void collectAncestors(boolean direction, Collection<? super Block> list, Block block, Block to) { if (block == to) { return; } else { if (direction) { collectAncestors(direction, list, block.getParent(), to); list.add(block); } else { list.add(block); collectAncestors(direction, list, block.getParent(), to); } } } /** @return true if the active region inclusively encloses a viewport. */ private boolean enclosesClipped(Region viewport) { Region clipped = RegionImpl.at( // \u2620 MathUtil.clip(root.getStart(), root.getEnd(), viewport.getStart()), // \u2620 MathUtil.clip(root.getStart(), root.getEnd(), viewport.getEnd())); // \u2620 Region active = getActiveRegion(); return OverlapKind.compare(active, clipped) == OverlapKind.ENCLOSES; } /** * Shrinks the active region to its start point. */ private void shrinkToStart() { // Shrink active region to a point. while (!end.equals(start)) { end.trawlPrevious(toPageOut); } } /** * Shrinks the active region to its end point. */ private void shrinkToEnd() { // Shrink active region to a point. while (!start.equals(end)) { start.trawlNext(toPageOut); } } /** * Moves the active region (assumed to be collapsed) to another point. Unique * ancestors of the current point are paged out, and unique ancestors of the * new point are paged in. * * @param point new active point */ private void moveActivePoint(Point point) { assert start.equals(end); // To find the unique ancestors of the old point, and the unique ancestors // of the new point, the LCA needs to be found. LinkedSequence<Block> oldAncestors = LinkedSequence.create(); for (Block block = start.block; block != null; block = block.getParent()) { oldAncestors.append(block); } List<Block> newAncestors = CollectionUtils.newArrayList(); Block lca = point.block; while (lca != null && !oldAncestors.contains(lca)) { newAncestors.add(lca); lca = lca.getParent(); } if (lca == null) { throw new IllegalArgumentException("Point not in block tree"); } // Page out unique old ancestors, in order from old point up to LCA. for (Block oldAncestor : oldAncestors) { if (oldAncestor == lca) { break; } else { toPageOut.add(oldAncestor); } } // Page in unique new ancestors, in order from LCA down to new point. while (!newAncestors.isEmpty()) { toPageIn.add(newAncestors.remove(newAncestors.size() - 1)); } // Then initiate active region. start.set(point); end.set(point); } /** * Expands a collapsed region. * * @param viewport */ private void expandTo(Region viewport) { assert isActive(); growStartUntil(viewport.getStart()); growEndUntil(viewport.getEnd()); } /** * Grows the active region from the start, until it includes a given position, * or the start of the entire tree is reached. * * @param position position to include in the active region */ private void growStartUntil(double position) { start.trawlPreviousUntil(position, toPageIn); } /** * Grows the active region from the end, until it includes a given position, * or the end of the entire tree is reached. * * @param position position to include in the active region */ private void growEndUntil(double position) { end.trawlNextUntil(position, toPageIn); } /** * Shrinks the active region from the start, until it includes a given * position, or the start of the entire tree is reached. * * @param position position to include in the active region */ private void shrinkStartUntil(double position) { start.trawlNextUntil(position, toPageOut); } /** * Shrinks the active region from the end, until it includes a given position, * or the end of the entire tree is reached. * * @param position position to include in the active region */ private void shrinkEndUntil(double position) { end.trawlPreviousUntil(position, toPageOut); } /** * Pages-in all the blocks in the page-in queue, and pages-out all the blocks * in the page-out queue. */ private void page() { // Do page-out then page-in, so that moving via reset() ; init() will work. while (!toPageOut.isEmpty()) { toPageOut.poll().pageOut(); } while (!toPageIn.isEmpty()) { toPageIn.poll().pageIn(); } invalidatePositionCache(); } // // Dynamic block handling. // /** * Notifies this region that a new subtree is now attached and ready to be * included as a pageable entity. */ public void onAfterBlockAdded(Block added) { if (!isActive()) { return; } if (Traverser.isBetween(start, end, SimplePoint.startOf(added))) { pageInCompletely(added); } } private void pageInCompletely(Block block) { block.pageIn(); for (Block child = block.getFirstChild(); child != null; child = child.getNextSibling()) { pageInCompletely(child); } } /** * Notifies this region that a subtree is about to be removed. All parts of * the accumulated block tree (i.e., the original tree, plus changes observed * through {@link #onAfterBlockAdded(Block)} and * {@link #onBeforeBlockRemoved(Block)}) are assumed to be still attached. * * @param block root of a subtree that is about to be removed */ public void onBeforeBlockRemoved(Block block) { if (!isActive()) { return; } if (block == root) { start.clear(); end.clear(); } else { // If the block is an ancestor of a terminal (start or end), then that // terminal needs to be moved. boolean startIsDescendant = Traverser.isDescendant(block, start.block); boolean endIsDescendant = Traverser.isDescendant(block, end.block); if (startIsDescendant && endIsDescendant) { start.set(SimplePoint.startOf(block.getParent())); end.set(start); } else if (startIsDescendant) { start.set(SimplePoint.endOf(block)); start.next(); } else if (endIsDescendant) { end.set(SimplePoint.startOf(block)); end.previous(); } } } //@VisibleForTesting(productionVisibility = Visibility.PRIVATE) public Point getStart() { return start; } // @VisibleForTesting(productionVisibility = Visibility.PRIVATE) public Point getEnd() { return end; } }