/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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; /** * Defines strategies for traversing {@link Point points} in a block tree. * */ public final class Traverser { /** A side of a block. */ public enum BlockSide { /** The start of a block. */ START { @Override double of(Block b) { return b.getStart(); } }, /** The end of a block. */ END { @Override double of(Block b) { return b.getEnd(); } }; /** @return the location of this side of a block. */ abstract double of(Block b); } /** * A side of a particular block. */ public static abstract class Point implements Comparable<Point> { /** A number to represent an unset value for {@link #cachedOrigin}. */ protected static final double UNSET = Double.NEGATIVE_INFINITY; // NaN does not work. /** The side of this point. */ BlockSide side; /** The block of this point. */ Block block; /** Cached value of {@link #origin()}. */ double cachedOrigin = UNSET; public Point(BlockSide side, Block block) { this.side = side; this.block = block; } /** @return the location of this point, relative to its block's parent. */ final double location() { return side.of(block); } /** @return the absolute location of this point. */ final double absoluteLocation() { double ret = origin() + location(); return ret; } /** * @return the absolute location of the origin, relative to which * {@link #location()} is expressed. */ private double origin() { if (cachedOrigin == UNSET) { double origin = 0.0; Block parent = block.getParent(); while (parent != null) { origin += parent.getChildrenOrigin(); parent = parent.getParent(); } cachedOrigin = origin; } return cachedOrigin; } /** * Invalidates any cached origin on this point. This method needs to be * called if any part of the block tree may have changed since call to * {@link #absoluteLocation()}. */ void invalidateCachedOrigin() { cachedOrigin = UNSET; } /** * {@inheritDoc} * * The algorithm used to compare points is based on the sibling ordering of * children in the lowest common ancestor. It is not constant time. For thin * narrow trees, or wide short trees, comparing widely separated points may * approach linear complexity. */ @Override public int compareTo(Point other) { if (this.block == other.block) { return this.side == other.side ? 0 : this.side == BlockSide.START ? -1 : 1; } else { // Build the LCA triple: the lowest common ancestor (lca) of this and // other, the child of the LCA that is an ancestor of this (thisColca), // and the child of the LCA that is an ancestor of other (otherColca). Block lca = null; Block thisColca = null; Block otherColca = null; // The lca is found by collecting all ancestors of this, then finding // the first ancestor of other that is in that collection. The two // colcas are found by backtracking one in the ancestor chain of each. LinkedSequence<Block> thisAncestors = LinkedSequence.create(); for (Block block = this.block; block != null; block = block.getParent()) { thisAncestors.append(block); } for (Block block = other.block; block != null; block = block.getParent()) { if (thisAncestors.contains(block)) { lca = block; break; } otherColca = block; } if (lca == null) { throw new IllegalArgumentException("no common ancestor of " + this + " and " + other); } else { thisColca = thisAncestors.getPrevious(lca); } // thisColca and otherColca both null implies that this.block = // other.block, which is in a separate branch. assert !(thisColca == null && otherColca == null); if (thisColca == null) { // Other point's block is a descendant of this point's block. return this.side == BlockSide.START ? -1 : 1; } else if (otherColca == null) { // This point's block is a descendant of other point's block. return other.side == BlockSide.START ? 1 : -1; } else { assert thisColca.getParent() == otherColca.getParent() && thisColca != otherColca; // Sibling ordering of thisColca and otherColca defines the ordering of these two points. Block sibling = thisColca.getNextSibling(); while (sibling != null) { if (sibling == otherColca) { // This is before Other. return -1; } sibling = sibling.getNextSibling(); } // Other is before This. return 1; } } } public BlockSide getSide() { return side; } public Block getBlock() { return block; } @Override public final boolean equals(Object obj) { if (this == obj) { return true; } else if (!(obj instanceof Point)) { return false; } else { Point other = (Point) obj; return this.block == other.block && this.side == other.side; } } @Override public final int hashCode() { return 37 * block.hashCode() + side.hashCode(); } @Override public final String toString() { return side + " of " + block; } } /** * A point that is intended to be immutable, since this class exposes no * mutators. */ public static final class SimplePoint extends Point { protected SimplePoint(BlockSide side, Block block) { super(side, block); } static SimplePoint startOf(Block block) { return new SimplePoint(BlockSide.START, block); } static SimplePoint endOf(Block block) { return new SimplePoint(BlockSide.END, block); } public static SimplePoint at(Point point) { return new SimplePoint(point.side, point.block); } } /** * A mutable point that can be {@link #set(Point) placed} at a point, or at * {@link #clear() no point}, and {@link #next() moved} forwards and * backwards. */ static abstract class MoveablePoint extends Point { MoveablePoint(BlockSide side, Block block) { super(side, block); } /** @return true if this point is non-vacuous. */ boolean isActive() { return block != null; } /** * Places this point. */ void set(Point point) { this.side = point.side; this.block = point.block; invalidateCachedOrigin(); } /** * Unplaces this point. */ void clear() { this.side = null; this.block = null; invalidateCachedOrigin(); } /** * Shifts the cached origin, if there is one. */ private void shiftOrigin(double shift) { if (cachedOrigin != UNSET) { cachedOrigin += shift; } } /** @return true if there is a {@link #next()} point. */ protected final boolean hasNext() { return !(side == BlockSide.END && isRoot(block)); } /** @return true if there is a {@link #previous()} point. */ protected final boolean hasPrevious() { return !(side == BlockSide.START && isRoot(block)); } /** * Moves to the next point. */ protected void next() { Block next; switch (side) { case START: if ((next = block.getFirstChild()) != null) { // ______________ // this --> | _____ | // next ------>| | ... | // shiftOrigin(block.getChildrenOrigin()); block = next; } else { // _____ // this --> | | <-- next // side = BlockSide.END; } break; case END: if ((next = block.getNextSibling()) != null) { // _____ _____ // | | <-- this next --> | | // side = BlockSide.START; block = next; } else if ((next = block.getParent()) != null) { // ______________ // | _____ | <-- next // | ... | |<------ this // block = next; shiftOrigin(-block.getChildrenOrigin()); } else { // _____ // | | <-- this // // Can not go any further. Was not prefixed with hasNext(). throw new IllegalStateException("next() called without hasNext()"); } break; default: // Unreachable (switch handles every value). throw new RuntimeException(); } } /** * Moves to the previous point. * * @throws IllegalStateException if there is no previous point. */ protected void previous() { Block prev; switch (side) { case END: if ((prev = block.getLastChild()) != null) { // ______________ // | _____ <| this // | ... | <| | prev // shiftOrigin(block.getChildrenOrigin()); block = prev; } else { // _____ // prev <| <| this // side = BlockSide.START; } break; case START: if ((prev = block.getPreviousSibling()) != null) { // _____ _____ // prev | <| <| | this // block = prev; side = BlockSide.END; } else if ((prev = block.getParent()) != null) { // ______________ // prev <| _____ | // this | <| | ... | // block = prev; shiftOrigin(-block.getChildrenOrigin()); } else { // _____ // this <| | // // Can not go any further. Was not prefixed with hasPrevious(). throw new IllegalStateException("previous() called without hasPrevious()"); } break; default: // Unreachable (switch handles every value). throw new RuntimeException(); } } } private Traverser() { } /** * Finds the rightmost point in a tree that is strictly before a position. * * @param root root of a block tree * @param position location (in absolute space) * @return what the method says. * @throws IllegalArgumentException if {@code root} is not a root block. */ public static Point locateStartWithin(Block root, double position) { Preconditions.checkArgument(isRoot(root), "Not a root block"); return locateStart(root, root, position); } /** * Finds the leftmost point in a tree that is strictly after a position. * * @param root root of a block tree * @param position location (in absolute space) * @return what the method says. * @throws IllegalArgumentException if {@code root} is not a root block. */ public static Point locateEndWithin(Block root, double position) { Preconditions.checkArgument(isRoot(root), "Not a root block"); return locateEnd(root, root, position); } /** * Finds the last point, at or after a given point, located strictly before a * position. * * @param point * @param position position (in absolute space) * @return what the method says (never null). * @throws IllegalArgumentException if {@code point} is not strictly before * {@code position}. */ public static Point locateStartAfter(Point point, double position) { // // The gist of this method is: // // do { // point.next(); // } while (point.location() < position); // point.previous(); // // but optimized to skip subtrees. // // Rebase position. position -= point.origin(); Preconditions.checkArgument(point.location() < position); Block block = point.block; if (block.getEnd() >= position) { // [ block ] // |<--->| // The answer is a point between start and end of this block. return locateStart(block, block, position); } else { // Walk up the tree, so that entire subtrees of next-siblings can be skipped. // Invariant: block.getEnd() < position Block parent; double parentPosition; while ((parent = block.getParent()) != null && parent.getEnd() < (parentPosition = parent.getChildrenOrigin() + position)) { // _______________ // | _____ <| next // | | <| ... | this // |<--- position block = parent; position = parentPosition; } if (parent == null) { // No parent means block == root, and there is nothing after its end. return SimplePoint.endOf(block); } else { // End of block is before position, but there may be a subsequent point // that is also before position. If so, it must be before end of parent, // which is known to be after position. Block next = block.getNextSibling(); Point answer = next != null ? locateStart(next, parent.getLastChild(), position) : null; return answer != null ? answer : SimplePoint.endOf(block); } } } /** * Finds the first point, at or before a given point, located strictly after a * position. * * @param point * @param position (in absolute space) * @return what the method says (never null). * @throws IllegalArgumentException if {@code point} is not strictly after * {@code position}. */ public static Point locateEndBefore(Point point, double position) { // // The gist of this method is: // // do { // point.previous(); // } while (point.location() > position); // point.next(); // // but optimized to skip subtrees. // // Rebase position. position -= point.origin(); Preconditions.checkArgument(point.location() > position); Block block = point.block; if (block.getStart() <= position) { // [ block ] // |<--->| // The answer is a point between start and end of this block. return locateEnd(block, block, position); } else { // Walk up the tree, so that entire subtrees of next-siblings can be skipped. // Invariant: block.getStart() > position Block parent; double parentPosition; while ((parent = block.getParent()) != null && parent.getStart() > (parentPosition = parent.getChildrenOrigin() + position)) { // _______________ // next |> _____ | // this | ... |> | | this // |<--- position block = parent; position = parentPosition; } if (parent == null) { // No parent means block == root, and there is nothing before its start. return SimplePoint.startOf(block); } else { // Start of block is after position, but there may be a previous point // that is also after position. If so, it must be after start of parent, // which is known to be before position. Block prev = block.getPreviousSibling(); Point answer = prev != null ? locateEnd(parent.getFirstChild(), prev, position) : null; return answer != null ? answer : SimplePoint.startOf(block); } } } /** * Finds the rightmost point in a list of siblings that is strictly before a * position. * * @param first first child in a sibling list * @param last last child in a sibling list * @param position search position * @return what the method says. */ private static Point locateStart(Block first, Block last, double position) { if (first == null) { assert last == null; return null; } double fromFirst = position - first.getStart(); double toLast = last.getEnd() - position; // Check boundaries. if (fromFirst <= 0) { // [first] ... [last] // <--| // No point is strictly before position. return null; } else if (toLast < 0) { // [first] ... [last] // |--> // Very last point is the rightmost point before position. return SimplePoint.endOf(last); } // // [first] ... [last] // |<-------------->| // Find the oldest sibling with a start before position. That sibling is // the best candidate of all siblings, so the answer must be in the subtree // of that sibling. // Block oldest; if (first == last) { oldest = first; } else if (fromFirst <= toLast) { // Search, forwards from first, for the first block that starts at, or // after, position, then backtrack one. oldest = first; Block next = oldest.getNextSibling(); while (next != null && next.getStart() < position) { oldest = next; next = oldest.getNextSibling(); } } else { // Search, backwards from last, for the first block that starts before // position. Given that first starts before position, then it is known // that this will not terminate with null. oldest = last; while (!(oldest.getStart() < position)) { oldest = oldest.getPreviousSibling(); } } // All three branches above maintain the invariant: assert oldest.getStart() < position; // ...] [oldest] [... // |<----->| // The answer is within the subtree of oldest. if (oldest.getEnd() < position) { return SimplePoint.endOf(oldest); } else { // [oldest] // |<-->| // (Note: this method is tail-recursive, so could be optimized into a loop // if desired). double childPosition = position - oldest.getChildrenOrigin(); Point answer = locateStart(oldest.getFirstChild(), oldest.getLastChild(), childPosition); return answer != null ? answer : SimplePoint.startOf(oldest); } } /** * Finds the leftmost point in a list of siblings that is strictly after a * position. * * @param first first child in a sibling list * @param last last child in a sibling list * @param position search position * @return what the method says. */ private static Point locateEnd(Block first, Block last, double position) { if (first == null) { assert last == null; return null; } double fromFirst = position - first.getStart(); double toLast = last.getEnd() - position; // Check boundaries. if (toLast <= 0) { // [first] ... [last] // |--> // No point is strictly after position. return null; } else if (fromFirst < 0) { // [first] ... [last] // <--| // Very first point is the leftmost point after position. return SimplePoint.startOf(first); } // // [first] ... [last] // |<-------------->| // Find the youngest sibling with an end after position. That sibling is // the best candidate of all siblings, so the answer must be in the subtree // of that sibling. // Block youngest; if (first == last) { youngest = first; } else if (toLast <= fromFirst) { // Search, backwards from last, for the first block that ends at, or // before, position, then backtrack one. youngest = last; Block previous = youngest.getPreviousSibling(); while (previous != null && previous.getEnd() > position) { youngest = previous; previous = youngest.getPreviousSibling(); } } else { // Search, forwards from first, for the first block that ends after // position. Given that last ends after position, then it is known // that this will not terminate with null. youngest = first; while (!(youngest.getEnd() > position)) { youngest = youngest.getNextSibling(); } } // All three branches above maintain the invariant: assert youngest.getEnd() > position; // ...] [youngest] [... // |<----->| // The answer is within the subtree of youngest if (youngest.getStart() > position) { return SimplePoint.startOf(youngest); } else { // [youngest] // |<-->| // (Note: this method is tail-recursive, so could be optimized into a loop // if desired). double childPosition = position - youngest.getChildrenOrigin(); Point answer = locateEnd(youngest.getFirstChild(), youngest.getLastChild(), childPosition); return answer != null ? answer : SimplePoint.endOf(youngest); } } /** @return true if and only if {@code block} is a root block. */ private static boolean isRoot(Block block) { return block.getParent() == null && block.getPreviousSibling() == null && block.getNextSibling() == null; } /** * Tests if a point is between two others. This method is not constant time, * and may be linear in the worst case. * * @return true if and only if {@code point} is strictly between {@code start} * and {@code end}. */ public static boolean isBetween(Point start, Point end, Point point) { return start.compareTo(point) < 0 && point.compareTo(end) < 0; } /** * @return true if and only if {@code a} is a descendant of {@code b}. */ public static boolean isDescendant(Block b, Block a) { for (Block block = a; block != null; block = block.getParent()) { if (block == b) { return true; } } return false; } }