org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.java

Source

/*
 * 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.apache.lucene.spatial.prefix.tree;

import java.text.ParseException;

import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.SpatialContextFactory;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.SpatialRelation;
import org.locationtech.spatial4j.shape.impl.RectangleImpl;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.StringHelper;

/**
 * A SpatialPrefixTree for single-dimensional numbers and number ranges of fixed precision values (not floating point).
 * Despite its name, the indexed values (and queries) need not actually be ranges, they can be unit instance/values.
 * <p>
 * Why might you use this instead of Lucene's built-in integer/long support?  Here are some reasons with features based
 * on code in this class, <em>or are possible based on this class but require a subclass to fully realize it</em>.
 * <ul>
 *   <li>Index ranges, not just unit instances. This is especially useful when the requirement calls for a
 *   multi-valued range.</li>
 *   <li>Instead of a fixed "precisionStep", this prefixTree can have a customizable number of child values for any
 *   prefix (up to 32768). This allows exact alignment of the prefix-tree with typical/expected values, which
 *   results in better performance.  For example in a Date implementation, every month can get its own dedicated prefix,
 *   every day, etc., even though months vary in duration.</li>
 *   <li>Arbitrary precision, like {@link java.math.BigDecimal}.</li>
 *   <li>Standard Lucene integer/long indexing always indexes the full precision of those data types but this one
 *   is customizable.</li>
 * </ul>
 *
 * Unlike "normal" spatial components in this module, this special-purpose one only works with {@link Shape}s
 * created by the methods on this class, not from any {@link org.locationtech.spatial4j.context.SpatialContext}.
 *
 * @see org.apache.lucene.spatial.prefix.NumberRangePrefixTreeStrategy
 * @see <a href="https://issues.apache.org/jira/browse/LUCENE-5648">LUCENE-5648</a>
 * @lucene.experimental
 */
public abstract class NumberRangePrefixTree extends SpatialPrefixTree {

    //
    //    Dummy SpatialContext
    //

    private static final SpatialContext DUMMY_CTX;
    static {
        SpatialContextFactory factory = new SpatialContextFactory();
        factory.geo = false;
        factory.worldBounds = new RectangleImpl(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 0L, 0L, null);
        DUMMY_CTX = factory.newSpatialContext();
    }

    /** Base interface for {@link Shape}s this prefix tree supports. It extends {@link Shape} (Spatial4j) for compatibility
     * with the spatial API even though it doesn't intermix with conventional 2D shapes.
     * @lucene.experimental
     */
    public static interface NRShape extends Shape, Cloneable {
        /** The result should be parseable by {@link #parseShape(String)}. */
        abstract String toString();

        /** Returns this shape rounded to the target level. If we are already more course than the level then the shape is
         * simply returned.  The result may refer to internal state of the argument so you may want to clone it.
         */
        public NRShape roundToLevel(int targetLevel);
    }

    //
    //  Factory / Conversions / parsing relating to NRShapes
    //

    /** Converts the value to a unit shape. Doesn't parse strings; see {@link #parseShape(String)} for
     * that. This is the reverse of {@link #toObject(org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.UnitNRShape)}. */
    public abstract UnitNRShape toUnitShape(Object value);

    /** Returns a shape that represents the continuous range between {@code start} and {@code end}. It will
     * be normalized, and so sometimes a {@link org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.UnitNRShape}
     * will be returned, other times a
     * {@link org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.SpanUnitsNRShape} will be.
     *
     * @throws IllegalArgumentException if the arguments are in the wrong order, or if either contains the other (yet they
     * aren't equal).
     */
    public NRShape toRangeShape(UnitNRShape startUnit, UnitNRShape endUnit) {
        //note: this normalization/optimization process is actually REQUIRED based on assumptions elsewhere.
        //Normalize start & end
        startUnit = startUnit.getShapeAtLevel(truncateStartVals(startUnit, 0)); // chops off trailing min-vals (zeroes)
        endUnit = endUnit.getShapeAtLevel(truncateEndVals(endUnit, 0)); // chops off trailing max-vals
        //Optimize to just start or end if it's equivalent, e.g. April to April 1st is April 1st.
        int cmp = comparePrefix(startUnit, endUnit);
        if (cmp > 0) {
            throw new IllegalArgumentException("Wrong order: " + startUnit + " TO " + endUnit);
        }
        if (cmp == 0) {//one is a prefix of the other
            if (startUnit.getLevel() == endUnit.getLevel()) {
                //same
                return startUnit;
            } else if (endUnit.getLevel() > startUnit.getLevel()) {
                // e.g. April to April 1st
                if (truncateStartVals(endUnit, startUnit.getLevel()) == startUnit.getLevel()) {
                    return endUnit;
                }
            } else {//minLV level > maxLV level
                // e.g. April 30 to April
                if (truncateEndVals(startUnit, endUnit.getLevel()) == endUnit.getLevel()) {
                    return startUnit;
                }
            }
        }
        return new SpanUnitsNRShape(startUnit, endUnit);
    }

    /** From lv.getLevel on up, it returns the first Level seen with val != 0. It doesn't check past endLevel. */
    private int truncateStartVals(UnitNRShape lv, int endLevel) {
        for (int level = lv.getLevel(); level > endLevel; level--) {
            if (lv.getValAtLevel(level) != 0)
                return level;
        }
        return endLevel;
    }

    private int truncateEndVals(UnitNRShape lv, int endLevel) {
        for (int level = lv.getLevel(); level > endLevel; level--) {
            int max = getNumSubCells(lv.getShapeAtLevel(level - 1)) - 1;
            if (lv.getValAtLevel(level) != max)
                return level;
        }
        return endLevel;
    }

    /** Converts a UnitNRShape shape to the corresponding type supported by this class, such as a Calendar/BigDecimal.
     * This is the reverse of {@link #toUnitShape(Object)}.
     */
    public abstract Object toObject(UnitNRShape shape);

    /** A string representation of the UnitNRShape that is parse-able by {@link #parseUnitShape(String)}. */
    protected abstract String toString(UnitNRShape lv);

    protected static String toStringUnitRaw(UnitNRShape lv) {
        StringBuilder buf = new StringBuilder(100);
        buf.append('[');
        for (int level = 1; level <= lv.getLevel(); level++) {
            buf.append(lv.getValAtLevel(level)).append(',');
        }
        buf.setLength(buf.length() - 1);//chop off ','
        buf.append(']');
        return buf.toString();
    }

    /** Detects a range pattern and parses it, otherwise it's parsed as one shape via
     * {@link #parseUnitShape(String)}.  The range pattern looks like this BNF:
     * <pre>
     *   '[' + parseShapeLV + ' TO ' + parseShapeLV + ']'
     * </pre>
     * It's the same thing as the toString() of the range shape, notwithstanding range optimization.
     *
     * @param str not null or empty
     * @return not null
     * @throws java.text.ParseException If there is a problem
     */
    public NRShape parseShape(String str) throws ParseException {
        if (str == null || str.isEmpty())
            throw new IllegalArgumentException("str is null or blank");
        if (str.charAt(0) == '[') {
            if (str.charAt(str.length() - 1) != ']')
                throw new ParseException("If starts with [ must end with ]; got " + str, str.length() - 1);
            int middle = str.indexOf(" TO ");
            if (middle < 0)
                throw new ParseException("If starts with [ must contain ' TO '; got " + str, -1);
            String leftStr = str.substring(1, middle);
            String rightStr = str.substring(middle + " TO ".length(), str.length() - 1);
            return toRangeShape(parseUnitShape(leftStr), parseUnitShape(rightStr));
        } else if (str.charAt(0) == '{') {
            throw new ParseException("Exclusive ranges not supported; got " + str, 0);
        } else {
            return parseUnitShape(str);
        }
    }

    /** Parse a String to a UnitNRShape. "*" should be the full-range (level 0 shape). */
    protected abstract UnitNRShape parseUnitShape(String str) throws ParseException;

    //
    //    UnitNRShape
    //

    /**
     * A unit value Shape implemented as a stack of numbers, one for each level in the prefix tree. It directly
     * corresponds to a {@link Cell}.  Spatially speaking, it's analogous to a Point but 1D and has some precision width.
     * @lucene.experimental
     */
    public static interface UnitNRShape extends NRShape, Comparable<UnitNRShape> {
        //note: formerly known as LevelledValue; thus some variables still use 'lv'

        /** Get the prefix tree level, the higher the more precise. 0 means the world (universe). */
        int getLevel();

        /** Gets the value at the specified level of this unit. level must be &gt;= 0 and &lt;= getLevel(). */
        int getValAtLevel(int level);

        /** Gets an ancestor at the specified level. It shares state, so you may want to clone() it. */
        UnitNRShape getShapeAtLevel(int level);

        @Override
        UnitNRShape roundToLevel(int targetLevel);

        /** Deep clone */
        UnitNRShape clone();
    }

    /** Compares a to b, returning less than 0, 0, or greater than 0, if a is less than, equal to, or
     * greater than b, respectively, up to their common prefix (i.e. only min(a.levels,b.levels) are compared).
     * @lucene.internal */
    protected static int comparePrefix(UnitNRShape a, UnitNRShape b) {
        int minLevel = Math.min(a.getLevel(), b.getLevel());
        for (int level = 1; level <= minLevel; level++) {
            int diff = a.getValAtLevel(level) - b.getValAtLevel(level);
            if (diff != 0)
                return diff;
        }
        return 0;
    }

    //
    //    SpanUnitsNRShape
    //

    /** A range Shape; based on a pair of {@link org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.UnitNRShape}.
     * Spatially speaking, it's analogous to a Rectangle but 1D. It might have been named with Range in the name but it
     * may be confusing since even the {@link org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.UnitNRShape}
     * is in some sense a range.
     * @lucene.experimental */
    public class SpanUnitsNRShape implements NRShape {

        private final UnitNRShape minLV, maxLV;
        private final int lastLevelInCommon;//computed; not part of identity

        /** Don't call directly; see
         * {@link #toRangeShape(org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.UnitNRShape, org.apache.lucene.spatial.prefix.tree.NumberRangePrefixTree.UnitNRShape)}. */
        private SpanUnitsNRShape(UnitNRShape minLV, UnitNRShape maxLV) {
            this.minLV = minLV;
            this.maxLV = maxLV;

            //calc lastLevelInCommon
            int level = 1;
            for (; level <= minLV.getLevel() && level <= maxLV.getLevel(); level++) {
                if (minLV.getValAtLevel(level) != maxLV.getValAtLevel(level))
                    break;
            }
            lastLevelInCommon = level - 1;
        }

        @Override
        public SpatialContext getContext() {
            return DUMMY_CTX;
        }

        public UnitNRShape getMinUnit() {
            return minLV;
        }

        public UnitNRShape getMaxUnit() {
            return maxLV;
        }

        /** How many levels are in common between minUnit and maxUnit, not including level 0. */
        private int getLevelsInCommon() {
            return lastLevelInCommon;
        }

        @Override
        public NRShape roundToLevel(int targetLevel) {
            return toRangeShape(minLV.roundToLevel(targetLevel), maxLV.roundToLevel(targetLevel));
        }

        @Override
        public SpatialRelation relate(Shape shape) {
            //      if (shape instanceof UnitNRShape)
            //        return relate((UnitNRShape)shape);
            if (shape instanceof SpanUnitsNRShape)
                return relate((SpanUnitsNRShape) shape);
            return shape.relate(this).transpose();//probably a UnitNRShape
        }

        public SpatialRelation relate(SpanUnitsNRShape ext) {
            //This logic somewhat mirrors RectangleImpl.relate_range()
            int extMin_intMax = comparePrefix(ext.getMinUnit(), getMaxUnit());
            if (extMin_intMax > 0)
                return SpatialRelation.DISJOINT;
            int extMax_intMin = comparePrefix(ext.getMaxUnit(), getMinUnit());
            if (extMax_intMin < 0)
                return SpatialRelation.DISJOINT;
            int extMin_intMin = comparePrefix(ext.getMinUnit(), getMinUnit());
            int extMax_intMax = comparePrefix(ext.getMaxUnit(), getMaxUnit());
            if ((extMin_intMin > 0 || extMin_intMin == 0 && ext.getMinUnit().getLevel() >= getMinUnit().getLevel())
                    && (extMax_intMax < 0
                            || extMax_intMax == 0 && ext.getMaxUnit().getLevel() >= getMaxUnit().getLevel()))
                return SpatialRelation.CONTAINS;
            if ((extMin_intMin < 0 || extMin_intMin == 0 && ext.getMinUnit().getLevel() <= getMinUnit().getLevel())
                    && (extMax_intMax > 0
                            || extMax_intMax == 0 && ext.getMaxUnit().getLevel() <= getMaxUnit().getLevel()))
                return SpatialRelation.WITHIN;
            return SpatialRelation.INTERSECTS;
        }

        @Override
        public Rectangle getBoundingBox() {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean hasArea() {
            return true;
        }

        @Override
        public double getArea(SpatialContext spatialContext) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Point getCenter() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Shape getBuffered(double v, SpatialContext spatialContext) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        /** A deep clone. */
        @Override
        public SpanUnitsNRShape clone() {
            return new SpanUnitsNRShape(minLV.clone(), maxLV.clone());
        }

        @Override
        public String toString() {
            return "[" + NumberRangePrefixTree.this.toString(minLV) + " TO "
                    + NumberRangePrefixTree.this.toString(maxLV) + "]";
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            SpanUnitsNRShape spanShape = (SpanUnitsNRShape) o;

            if (!maxLV.equals(spanShape.maxLV))
                return false;
            if (!minLV.equals(spanShape.minLV))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = minLV.hashCode();
            result = 31 * result + maxLV.hashCode();
            return result;
        }
    }// class SpanUnitsNRShape

    //
    //    NumberRangePrefixTree
    //

    protected final int[] maxSubCellsByLevel;
    protected final int[] termLenByLevel;
    protected final int[] levelByTermLen;
    protected final int maxTermLen; // how long could cell.getToken... (that is a leaf) possibly be?

    protected NumberRangePrefixTree(int[] maxSubCellsByLevel) {
        super(DUMMY_CTX, maxSubCellsByLevel.length);
        this.maxSubCellsByLevel = maxSubCellsByLevel;

        // Fill termLenByLevel
        this.termLenByLevel = new int[maxLevels + 1];
        termLenByLevel[0] = 0;
        final int MAX_STATES = 1 << 15;//1 bit less than 2 bytes
        for (int level = 1; level <= maxLevels; level++) {
            final int states = maxSubCellsByLevel[level - 1];
            if (states >= MAX_STATES || states <= 1) {
                throw new IllegalArgumentException(
                        "Max states is " + MAX_STATES + ", given " + states + " at level " + level);
            }
            boolean twoBytes = states >= 256;
            termLenByLevel[level] = termLenByLevel[level - 1] + (twoBytes ? 2 : 1);
        }
        maxTermLen = termLenByLevel[maxLevels] + 1;// + 1 for leaf byte

        // Fill levelByTermLen
        levelByTermLen = new int[maxTermLen];
        levelByTermLen[0] = 0;
        for (int level = 1; level < termLenByLevel.length; level++) {
            int termLen = termLenByLevel[level];
            int prevTermLen = termLenByLevel[level - 1];
            if (termLen - prevTermLen == 2) {//2 byte delta
                //if the term doesn't completely cover this cell then it must be a leaf of the prior.
                levelByTermLen[termLen - 1] = -1;//won't be used; otherwise erroneous
                levelByTermLen[termLen] = level;
            } else {//1 byte delta
                assert termLen - prevTermLen == 1;
                levelByTermLen[termLen] = level;
            }
        }

    }

    @Override
    public String toString() {
        return getClass().getSimpleName();
    }

    @Override
    public int getLevelForDistance(double dist) {
        //note: it might be useful to compute which level has a raw width (counted in
        // bottom units, e.g. milliseconds), that covers the provided dist in those units?
        return maxLevels; // thus always use full precision. We don't do approximations in this tree/strategy.
        //throw new UnsupportedOperationException("Not applicable.");
    }

    @Override
    public double getDistanceForLevel(int level) {
        //note: we could compute this... should we?
        throw new UnsupportedOperationException("Not applicable.");
    }

    protected UnitNRShape toShape(int[] valStack, int len) {
        final NRCell[] cellStack = newCellStack(len);
        for (int i = 0; i < len; i++) {
            cellStack[i + 1].resetCellWithCellNum(valStack[i]);
        }
        return cellStack[len];
    }

    @Override
    public Cell getWorldCell() {
        return newCellStack(maxLevels)[0];
    }

    protected NRCell[] newCellStack(int levels) {
        final NRCell[] cellsByLevel = new NRCell[levels + 1];
        final BytesRef term = new BytesRef(maxTermLen);
        for (int level = 0; level <= levels; level++) {
            cellsByLevel[level] = new NRCell(cellsByLevel, term, level);
        }
        return cellsByLevel;
    }

    @Override
    public Cell readCell(BytesRef term, Cell scratch) {
        if (scratch == null)
            scratch = getWorldCell();

        //We decode level #, leaf boolean, and populate bytes by reference. We don't decode the stack.

        //reverse lookup term length to the level and hence the cell
        NRCell[] cellsByLevel = ((NRCell) scratch).cellsByLevel;
        boolean isLeaf = term.bytes[term.offset + term.length - 1] == 0;
        int lenNoLeaf = isLeaf ? term.length - 1 : term.length;

        NRCell result = cellsByLevel[levelByTermLen[lenNoLeaf]];
        if (cellsByLevel[0].termBuf == null)
            cellsByLevel[0].termBuf = result.term.bytes;//a kluge; see cell.ensureOwnTermBytes()
        result.term.bytes = term.bytes;
        result.term.offset = term.offset;
        result.term.length = lenNoLeaf;//technically this isn't used but may help debugging
        result.reset();
        if (isLeaf)
            result.setLeaf();

        result.cellNumber = -1;//lazy decode flag

        return result;
    }

    /** Returns the number of sub-cells beneath the given UnitNRShape. */
    public int getNumSubCells(UnitNRShape lv) {
        return maxSubCellsByLevel[lv.getLevel()];
    }

    //
    //    NRCell
    //

    /** Most of the PrefixTree implementation is in this one class, which is both
     * the Cell, the CellIterator, and the Shape to reduce object allocation. It's implemented as a re-used array/stack
     * of Cells at adjacent levels, that all have a reference back to the cell array to traverse. They also share a common
     * BytesRef for the term.
     * @lucene.internal */
    protected class NRCell extends CellIterator implements Cell, UnitNRShape {

        //Shared: (TODO put this in a new class)
        final NRCell[] cellsByLevel;
        final BytesRef term;//AKA the token
        byte[] termBuf;// see ensureOwnTermBytes(), only for cell0

        //Cell state...
        final int cellLevel; // assert levelStack[cellLevel] == this
        int cellNumber; //relative to parent cell. It's unused for level 0. Starts at 0.

        SpatialRelation cellShapeRel;
        boolean cellIsLeaf;

        //CellIterator state is defined further below

        NRCell(NRCell[] cellsByLevel, BytesRef term, int cellLevel) {
            this.cellsByLevel = cellsByLevel;
            this.term = term;
            this.cellLevel = cellLevel;
            this.cellNumber = cellLevel == 0 ? 0 : -1;
            this.cellIsLeaf = false;
            assert cellsByLevel[cellLevel] == null;
        }

        /** Ensure we own term.bytes so that it's safe to modify. We detect via a kluge in which cellsByLevel[0].termBuf
         * is non-null, which is a pre-allocated for use to replace term.bytes. */
        void ensureOwnTermBytes() {
            NRCell cell0 = cellsByLevel[0];
            if (cell0.termBuf == null)
                return;//we already own the bytes
            System.arraycopy(term.bytes, term.offset, cell0.termBuf, 0, term.length);
            term.bytes = cell0.termBuf;
            term.offset = 0;
            cell0.termBuf = null;
        }

        private void reset() {
            this.cellIsLeaf = false;
            this.cellShapeRel = null;
        }

        private void resetCellWithCellNum(int cellNumber) {
            reset();

            //update bytes
            //  note: see lazyInitCellNumsFromBytes() for the reverse
            if (cellNumber >= 0) {//valid
                ensureOwnTermBytes();
                int termLen = termLenByLevel[getLevel()];
                boolean twoBytes = (termLen - termLenByLevel[getLevel() - 1]) > 1;
                if (twoBytes) {
                    //right 7 bits, plus 1 (may overflow to 8th bit which is okay)
                    term.bytes[termLen - 2] = (byte) (cellNumber >> 7);
                    term.bytes[termLen - 1] = (byte) ((cellNumber & 0x7F) + 1);
                } else {
                    term.bytes[termLen - 1] = (byte) (cellNumber + 1);
                }
                assert term.bytes[termLen - 1] != 0;
                term.length = termLen;
            }
            this.cellNumber = cellNumber;
        }

        private void ensureDecoded() {
            if (cellNumber >= 0)
                return;
            //Decode cell numbers from bytes. This is the inverse of resetCellWithCellNum().
            for (int level = 1; level <= getLevel(); level++) {
                NRCell cell = cellsByLevel[level];
                int termLen = termLenByLevel[level];
                boolean twoBytes = (termLen - termLenByLevel[level - 1]) > 1;
                if (twoBytes) {
                    int byteH = (term.bytes[term.offset + termLen - 2] & 0xFF);
                    int byteL = (term.bytes[term.offset + termLen - 1] & 0xFF);
                    assert byteL - 1 < (1 << 7);
                    cell.cellNumber = (byteH << 7) + (byteL - 1);
                    assert cell.cellNumber < 1 << 15;
                } else {
                    cell.cellNumber = (term.bytes[term.offset + termLen - 1] & 0xFF) - 1;
                    assert cell.cellNumber < 255;
                }
                cell.assertDecoded();
            }
        }

        private void assertDecoded() {
            assert cellNumber >= 0 : "Illegal state; ensureDecoded() wasn't called";
        }

        @Override // for Cell & for UnitNRShape
        public int getLevel() {
            return cellLevel;
        }

        @Override
        public SpatialRelation getShapeRel() {
            return cellShapeRel;
        }

        @Override
        public void setShapeRel(SpatialRelation rel) {
            cellShapeRel = rel;
        }

        @Override
        public boolean isLeaf() {
            return cellIsLeaf;
        }

        @Override
        public void setLeaf() {
            cellIsLeaf = true;
        }

        @Override
        public UnitNRShape getShape() {
            ensureDecoded();
            return this;
        }

        @Override
        public BytesRef getTokenBytesNoLeaf(BytesRef result) {
            if (result == null)
                result = new BytesRef();
            result.bytes = term.bytes;
            result.offset = term.offset;
            result.length = termLenByLevel[cellLevel];
            assert result.length <= term.length;
            return result;
        }

        @Override
        public BytesRef getTokenBytesWithLeaf(BytesRef result) {
            ensureOwnTermBytes();//normally shouldn't do anything
            result = getTokenBytesNoLeaf(result);
            if (isLeaf()) {
                result.bytes[result.length++] = 0;
            }
            return result;
        }

        @Override
        public boolean isPrefixOf(Cell c) {
            NRCell otherCell = (NRCell) c;
            assert term != otherCell.term;
            //trick to re-use bytesref; provided that we re-instate it
            int myLastLen = term.length;
            term.length = termLenByLevel[getLevel()];
            int otherLastLen = otherCell.term.length;
            otherCell.term.length = termLenByLevel[otherCell.getLevel()];
            boolean answer = StringHelper.startsWith(otherCell.term, term);
            term.length = myLastLen;
            otherCell.term.length = otherLastLen;
            return answer;
        }

        @Override
        public int compareToNoLeaf(Cell fromCell) {
            final NRCell nrCell = (NRCell) fromCell;
            assert term != nrCell.term;
            //trick to re-use bytesref; provided that we re-instate it
            int myLastLen = term.length;
            int otherLastLen = nrCell.term.length;
            term.length = termLenByLevel[getLevel()];
            nrCell.term.length = termLenByLevel[nrCell.getLevel()];
            int answer = term.compareTo(nrCell.term);
            term.length = myLastLen;
            nrCell.term.length = otherLastLen;
            return answer;
        }

        @Override
        public CellIterator getNextLevelCells(Shape shapeFilter) {
            ensureDecoded();
            NRCell subCell = cellsByLevel[cellLevel + 1];
            subCell.initIter(shapeFilter);
            return subCell;
        }

        //----------- CellIterator

        Shape iterFilter;//UnitNRShape or NRShape
        boolean iterFirstIsIntersects;
        boolean iterLastIsIntersects;
        int iterFirstCellNumber;
        int iterLastCellNumber;

        private void initIter(Shape filter) {
            cellNumber = -1;
            if (filter instanceof UnitNRShape && ((UnitNRShape) filter).getLevel() == 0)
                filter = null;//world means everything -- no filter
            iterFilter = filter;

            NRCell parent = getShapeAtLevel(getLevel() - 1);

            // Initialize iter* members.

            //no filter means all subcells
            if (filter == null) {
                iterFirstCellNumber = 0;
                iterFirstIsIntersects = false;
                iterLastCellNumber = getNumSubCells(parent) - 1;
                iterLastIsIntersects = false;
                return;
            }

            final UnitNRShape minLV;
            final UnitNRShape maxLV;
            final int lastLevelInCommon;//between minLV & maxLV
            if (filter instanceof SpanUnitsNRShape) {
                SpanUnitsNRShape spanShape = (SpanUnitsNRShape) iterFilter;
                minLV = spanShape.getMinUnit();
                maxLV = spanShape.getMaxUnit();
                lastLevelInCommon = spanShape.getLevelsInCommon();
            } else {
                minLV = (UnitNRShape) iterFilter;
                maxLV = minLV;
                lastLevelInCommon = minLV.getLevel();
            }

            //fast path optimization that is usually true, but never first level
            if (iterFilter == parent.iterFilter && (getLevel() <= lastLevelInCommon
                    || parent.iterFirstCellNumber != parent.iterLastCellNumber)) {
                //TODO benchmark if this optimization pays off. We avoid two comparePrefixLV calls.
                if (parent.iterFirstIsIntersects && parent.cellNumber == parent.iterFirstCellNumber
                        && minLV.getLevel() >= getLevel()) {
                    iterFirstCellNumber = minLV.getValAtLevel(getLevel());
                    iterFirstIsIntersects = (minLV.getLevel() > getLevel());
                } else {
                    iterFirstCellNumber = 0;
                    iterFirstIsIntersects = false;
                }
                if (parent.iterLastIsIntersects && parent.cellNumber == parent.iterLastCellNumber
                        && maxLV.getLevel() >= getLevel()) {
                    iterLastCellNumber = maxLV.getValAtLevel(getLevel());
                    iterLastIsIntersects = (maxLV.getLevel() > getLevel());
                } else {
                    iterLastCellNumber = getNumSubCells(parent) - 1;
                    iterLastIsIntersects = false;
                }
                if (iterFirstCellNumber == iterLastCellNumber) {
                    if (iterLastIsIntersects)
                        iterFirstIsIntersects = true;
                    else if (iterFirstIsIntersects)
                        iterLastIsIntersects = true;
                }
                return;
            }

            //not common to get here, except for level 1 which always happens

            int startCmp = comparePrefix(minLV, parent);
            if (startCmp > 0) {//start comes after this cell
                iterFirstCellNumber = 0;
                iterFirstIsIntersects = false;
                iterLastCellNumber = -1;//so ends early (no cells)
                iterLastIsIntersects = false;
                return;
            }
            int endCmp = comparePrefix(maxLV, parent);//compare to end cell
            if (endCmp < 0) {//end comes before this cell
                iterFirstCellNumber = 0;
                iterFirstIsIntersects = false;
                iterLastCellNumber = -1;//so ends early (no cells)
                iterLastIsIntersects = false;
                return;
            }
            if (startCmp < 0 || minLV.getLevel() < getLevel()) {
                //start comes before...
                iterFirstCellNumber = 0;
                iterFirstIsIntersects = false;
            } else {
                iterFirstCellNumber = minLV.getValAtLevel(getLevel());
                iterFirstIsIntersects = (minLV.getLevel() > getLevel());
            }
            if (endCmp > 0 || maxLV.getLevel() < getLevel()) {
                //end comes after...
                iterLastCellNumber = getNumSubCells(parent) - 1;
                iterLastIsIntersects = false;
            } else {
                iterLastCellNumber = maxLV.getValAtLevel(getLevel());
                iterLastIsIntersects = (maxLV.getLevel() > getLevel());
            }
            if (iterFirstCellNumber == iterLastCellNumber) {
                if (iterLastIsIntersects)
                    iterFirstIsIntersects = true;
                else if (iterFirstIsIntersects)
                    iterLastIsIntersects = true;
            }
        }

        @Override
        public boolean hasNext() {
            thisCell = null;
            if (nextCell != null)//calling hasNext twice in a row
                return true;

            if (cellNumber >= iterLastCellNumber)
                return false;

            resetCellWithCellNum(cellNumber < iterFirstCellNumber ? iterFirstCellNumber : cellNumber + 1);

            boolean hasChildren = (cellNumber == iterFirstCellNumber && iterFirstIsIntersects)
                    || (cellNumber == iterLastCellNumber && iterLastIsIntersects);

            if (!hasChildren) {
                setLeaf();
                setShapeRel(SpatialRelation.WITHIN);
            } else if (iterFirstCellNumber == iterLastCellNumber) {
                setShapeRel(SpatialRelation.CONTAINS);
            } else {
                setShapeRel(SpatialRelation.INTERSECTS);
            }

            nextCell = this;
            return true;
        }

        //TODO override nextFrom to be more efficient

        //----------- UnitNRShape

        @Override
        public int getValAtLevel(int level) {
            final int result = cellsByLevel[level].cellNumber;
            assert result >= 0;//initialized (decoded)
            return result;
        }

        @Override
        public NRCell getShapeAtLevel(int level) {
            assert level <= cellLevel;
            return cellsByLevel[level];
        }

        @Override
        public UnitNRShape roundToLevel(int targetLevel) {
            if (getLevel() <= targetLevel) {
                return this;
            } else {
                return getShapeAtLevel(targetLevel);
            }
        }

        @Override
        public SpatialRelation relate(Shape shape) {
            assertDecoded();
            if (shape == iterFilter && cellShapeRel != null)
                return cellShapeRel;
            if (shape instanceof UnitNRShape)
                return relate((UnitNRShape) shape);
            if (shape instanceof SpanUnitsNRShape)
                return relate((SpanUnitsNRShape) shape);
            return shape.relate(this).transpose();
        }

        public SpatialRelation relate(UnitNRShape lv) {
            assertDecoded();
            int cmp = comparePrefix(this, lv);
            if (cmp != 0)
                return SpatialRelation.DISJOINT;
            if (getLevel() > lv.getLevel())
                return SpatialRelation.WITHIN;
            return SpatialRelation.CONTAINS;//or equals
            //no INTERSECTS; that won't happen.
        }

        public SpatialRelation relate(SpanUnitsNRShape spanShape) {
            assertDecoded();
            int startCmp = comparePrefix(spanShape.getMinUnit(), this);
            if (startCmp > 0) {//start comes after this cell
                return SpatialRelation.DISJOINT;
            }
            int endCmp = comparePrefix(spanShape.getMaxUnit(), this);
            if (endCmp < 0) {//end comes before this cell
                return SpatialRelation.DISJOINT;
            }
            int nrMinLevel = spanShape.getMinUnit().getLevel();
            int nrMaxLevel = spanShape.getMaxUnit().getLevel();
            if ((startCmp < 0 || startCmp == 0 && nrMinLevel <= getLevel())
                    && (endCmp > 0 || endCmp == 0 && nrMaxLevel <= getLevel()))
                return SpatialRelation.WITHIN;//or equals
            //At this point it's Contains or Within.
            if (startCmp != 0 || endCmp != 0)
                return SpatialRelation.INTERSECTS;
            //if min or max Level is less, it might be on the equivalent edge.
            for (; nrMinLevel < getLevel(); nrMinLevel++) {
                if (getValAtLevel(nrMinLevel + 1) != 0)
                    return SpatialRelation.INTERSECTS;
            }
            for (; nrMaxLevel < getLevel(); nrMaxLevel++) {
                if (getValAtLevel(nrMaxLevel + 1) != getNumSubCells(getShapeAtLevel(nrMaxLevel)) - 1)
                    return SpatialRelation.INTERSECTS;
            }
            return SpatialRelation.CONTAINS;
        }

        @Override
        public UnitNRShape clone() {
            //no leaf distinction; this is purely based on UnitNRShape
            NRCell cell = (NRCell) readCell(getTokenBytesNoLeaf(null), null);
            cell.ensureOwnTermBytes();
            return cell.getShape();
        }

        @Override
        public int compareTo(UnitNRShape o) {
            assertDecoded();
            //no leaf distinction; this is purely based on UnitNRShape
            int cmp = comparePrefix(this, o);
            if (cmp != 0) {
                return cmp;
            } else {
                return getLevel() - o.getLevel();
            }
        }

        @Override
        public Rectangle getBoundingBox() {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean hasArea() {
            return true;
        }

        @Override
        public double getArea(SpatialContext ctx) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Point getCenter() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Shape getBuffered(double distance, SpatialContext ctx) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        //------- Object

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof NRCell)) {
                return false;
            }
            if (this == obj)
                return true;
            NRCell nrCell = (NRCell) obj;
            assert term != nrCell.term;
            if (getLevel() != nrCell.getLevel())
                return false;
            //trick to re-use bytesref; provided that we re-instate it
            int myLastLen = term.length;
            int otherLastLen = nrCell.term.length;
            boolean answer = getTokenBytesNoLeaf(term).equals(nrCell.getTokenBytesNoLeaf(nrCell.term));
            term.length = myLastLen;
            nrCell.term.length = otherLastLen;
            return answer;
        }

        @Override
        public SpatialContext getContext() {
            return DUMMY_CTX;
        }

        @Override
        public int hashCode() {
            //trick to re-use bytesref; provided that we re-instate it
            int myLastLen = term.length;
            int result = getTokenBytesNoLeaf(term).hashCode();
            term.length = myLastLen;
            return result;
        }

        @Override
        public String toString() {
            return NumberRangePrefixTree.this.toString(getShape());
        }

        /** Configure your IDE to use this. */
        public String toStringDebug() {
            String pretty = toString();
            if (getLevel() == 0)
                return pretty;
            return toStringUnitRaw(this) + (isLeaf() ? "" : "") + " " + pretty;
        }

    } // END OF NRCell

}