package org.apache.lucene.spatial.prefix.tree;
/*
* 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.
*/
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.Shape;
import com.spatial4j.core.shape.SpatialRelation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Represents a grid cell. These are not necessarily thread-safe, although new
* Cell("") (world cell) must be.
*
* @lucene.experimental
*/
public abstract class Cell implements Comparable<Cell> {
public static final byte LEAF_BYTE = '+';//NOTE: must sort before letters & numbers
/*
Holds a byte[] and/or String representation of the cell. Both are lazy constructed from the other.
Neither contains the trailing leaf byte.
*/
private byte[] bytes;
private int b_off;
private int b_len;
private String token;//this is the only part of equality
/**
* When set via getSubCells(filter), it is the relationship between this cell
* and the given shape filter.
*/
protected SpatialRelation shapeRel;
/**
* Always false for points. Otherwise, indicate no further sub-cells are going
* to be provided because shapeRel is WITHIN or maxLevels or a detailLevel is
* hit.
*/
protected boolean leaf;
protected Cell(String token) {
this.token = token;
if (token.length() > 0 && token.charAt(token.length() - 1) == (char) LEAF_BYTE) {
this.token = token.substring(0, token.length() - 1);
setLeaf();
}
if (getLevel() == 0)
getShape();//ensure any lazy instantiation completes to make this threadsafe
}
protected Cell(byte[] bytes, int off, int len) {
this.bytes = bytes;
this.b_off = off;
this.b_len = len;
b_fixLeaf();
}
public void reset(byte[] bytes, int off, int len) {
assert getLevel() != 0;
token = null;
shapeRel = null;
this.bytes = bytes;
this.b_off = off;
this.b_len = len;
b_fixLeaf();
}
private void b_fixLeaf() {
//note that non-point shapes always have the maxLevels cell set with setLeaf
if (bytes[b_off + b_len - 1] == LEAF_BYTE) {
b_len--;
setLeaf();
} else {
leaf = false;
}
}
public SpatialRelation getShapeRel() {
return shapeRel;
}
/**
* For points, this is always false. Otherwise this is true if there are no
* further cells with this prefix for the shape (always true at maxLevels).
*/
public boolean isLeaf() {
return leaf;
}
/** Note: not supported at level 0. */
public void setLeaf() {
assert getLevel() != 0;
leaf = true;
}
/**
* Note: doesn't contain a trailing leaf byte.
*/
public String getTokenString() {
if (token == null) {
token = new String(bytes, b_off, b_len, SpatialPrefixTree.UTF8);
}
return token;
}
/**
* Note: doesn't contain a trailing leaf byte.
*/
public byte[] getTokenBytes() {
if (bytes != null) {
if (b_off != 0 || b_len != bytes.length) {
throw new IllegalStateException("Not supported if byte[] needs to be recreated.");
}
} else {
bytes = token.getBytes(SpatialPrefixTree.UTF8);
b_off = 0;
b_len = bytes.length;
}
return bytes;
}
public int getLevel() {
return token != null ? token.length() : b_len;
}
//TODO add getParent() and update some algorithms to use this?
//public Cell getParent();
/**
* Like {@link #getSubCells()} but with the results filtered by a shape. If
* that shape is a {@link com.spatial4j.core.shape.Point} then it must call
* {@link #getSubCell(com.spatial4j.core.shape.Point)}. The returned cells
* should have {@link Cell#getShapeRel()} set to their relation with {@code
* shapeFilter}. In addition, {@link Cell#isLeaf()}
* must be true when that relation is WITHIN.
* <p/>
* Precondition: Never called when getLevel() == maxLevel.
*
* @param shapeFilter an optional filter for the returned cells.
* @return A set of cells (no dups), sorted. Not Modifiable.
*/
public Collection<Cell> getSubCells(Shape shapeFilter) {
//Note: Higher-performing subclasses might override to consider the shape filter to generate fewer cells.
if (shapeFilter instanceof Point) {
Cell subCell = getSubCell((Point) shapeFilter);
subCell.shapeRel = SpatialRelation.CONTAINS;
return Collections.singletonList(subCell);
}
Collection<Cell> cells = getSubCells();
if (shapeFilter == null) {
return cells;
}
//TODO change API to return a filtering iterator
List<Cell> copy = new ArrayList<>(cells.size());
for (Cell cell : cells) {
SpatialRelation rel = cell.getShape().relate(shapeFilter);
if (rel == SpatialRelation.DISJOINT)
continue;
cell.shapeRel = rel;
if (rel == SpatialRelation.WITHIN)
cell.setLeaf();
copy.add(cell);
}
return copy;
}
/**
* Performant implementations are expected to implement this efficiently by
* considering the current cell's boundary. Precondition: Never called when
* getLevel() == maxLevel.
* <p/>
* Precondition: this.getShape().relate(p) != DISJOINT.
*/
public abstract Cell getSubCell(Point p);
//TODO Cell getSubCell(byte b)
/**
* Gets the cells at the next grid cell level that cover this cell.
* Precondition: Never called when getLevel() == maxLevel.
*
* @return A set of cells (no dups), sorted, modifiable, not empty, not null.
*/
protected abstract Collection<Cell> getSubCells();
/**
* {@link #getSubCells()}.size() -- usually a constant. Should be >=2
*/
public abstract int getSubCellsSize();
public abstract Shape getShape();
public Point getCenter() {
return getShape().getCenter();
}
@Override
public int compareTo(Cell o) {
return getTokenString().compareTo(o.getTokenString());
}
@Override
public boolean equals(Object obj) {
return !(obj == null || !(obj instanceof Cell)) && getTokenString().equals(((Cell) obj).getTokenString());
}
@Override
public int hashCode() {
return getTokenString().hashCode();
}
@Override
public String toString() {
return getTokenString() + (isLeaf() ? (char) LEAF_BYTE : "");
}
}