/**
* *****************************************************************************
* Copyright 2013 Johannes Mitlmeier
*
* 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 de.fub.agg2graph.agg.tiling;
import de.fub.agg2graph.structs.ILocation;
import java.awt.geom.Rectangle2D;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A tile in the tiled structure of the spatial data.
*
* @author Johannes Mitlmeier
*
* @param <T> Type of location data to hold
*/
public class Tile<T extends ILocation> {
public Rectangle2D.Double size;
public boolean isLeaf;
private List<Tile<T>> children;
private final Set<T> elements;
public Tile<T> parent;
private String ID = "";
public boolean isLoaded;
public TileManager tm;
private TileCache tc;
public Tile(TileManager tm, TileCache tc, Tile<T> parent,
Rectangle2D.Double size) {
this.tm = tm;
this.tc = tc;
this.parent = parent;
this.size = size;
isLeaf = true;
elements = new HashSet<T>(tm.getMaxElementsPerTile() / 10);
}
public Set<T> getElements() {
return elements;
}
public int getElemCount() {
if (isLeaf) {
return getElements().size();
}
int elemCount = 0;
for (Tile<T> child : getChildren()) {
elemCount += child.getElemCount();
}
return elemCount;
}
public String getID() {
return ID;
}
public void setID(String iD) {
ID = iD;
}
/**
* Split this tile. The number of sub-tiles created depends on
*
* @{link {@link TileManager#maxElementsPerTile}, e.g. a value of 3 will
* make 9 new tiles.
*/
public void split() {
if (!isLeaf) {
return;
}
// make new leafs
Rectangle2D.Double tileSize = getSize();
int splitFactor = tm.getSplitFactor();
double subTileWidth = tileSize.getWidth() / splitFactor;
double subTileHeight = tileSize.getHeight() / splitFactor;
double x, y;
for (int i = 0; i < splitFactor * splitFactor; i++) {
x = i % splitFactor;
y = i / splitFactor;
Tile<T> childTile = new Tile<T>(tm, tc, this,
new Rectangle2D.Double(tileSize.getMinX() + subTileWidth
* x, tileSize.getMinY() + subTileHeight * y,
subTileWidth, subTileHeight));
childTile.setID("".equals(getID()) ? String.valueOf(i) : MessageFormat.format("{0}-{1}", getID(), i));
getChildren().add(childTile);
}
// push points
for (T elem : getElements()) {
Tile<T> subTile = getSubTile(elem);
if (subTile == null) {
continue;
}
subTile.getElements().add(elem);
}
// turn old leaf to node
isLeaf = false;
getElements().clear();
// recurse!
for (Tile<T> subTile : getChildren()) {
subTile.setLoaded(isLoaded);
if (subTile.getElemCount() > tm.getMaxElementsPerTile()
&& subTile.size.getWidth() >= tm.getMinimumSplitSize().getWidth()
&& subTile.size.getHeight() >= tm.getMinimumSplitSize().getHeight()) {
subTile.split();
}
}
}
public List<Tile<T>> getChildren() {
if (this.children == null) {
this.children = new ArrayList<Tile<T>>(200);
}
return this.children;
}
/**
* Find the sub-tile an {@link ILocation} would belong in.
*
* @param loc
* @return
*/
public Tile<T> getSubTile(ILocation loc) {
Rectangle2D.Double tileSize = getSize();
int splitFactor = tm.getSplitFactor();
double relXPos = loc.getLon() - tileSize.getMinX();
double relYPos = loc.getLat() - tileSize.getMinY();
double subTileWidth = tileSize.getWidth() / splitFactor;
double subTileHeight = tileSize.getHeight() / splitFactor;
int xTile = (int) Math.floor(relXPos / subTileWidth);
int yTile = (int) Math.floor(relYPos / subTileHeight);
int index = yTile * splitFactor + xTile;
Tile<T> tile = null;
if (!getChildren().isEmpty()) {
if (index > -1 && index < getChildren().size()) {
tile = getChildren().get(index);
} else if (index > getElements().size() - 1) {
tile = getChildren().get(getChildren().size() - 1);
} else if (index < 0) {
tile = getChildren().get(0);
}
}
return tile;
}
/**
* Find out if a tile is loaded which means the data are in sync with any
* data previously saved to file and in sync with all data added in memory
* afterwards.
*
* @return
*/
public boolean isLoaded() {
return isLoaded;
}
/**
* Indicate if the data from disk have been loaded.
*
* @param isLoaded
*/
public void setLoaded(boolean isLoaded) {
this.isLoaded = isLoaded;
}
/**
* in meters
*
* @return
*/
public Rectangle2D.Double getSize() {
return size;
}
/**
* Recursively retrieve all nodes stored in this tile.
*
* @return
*/
public Set<T> getInnerNodes() {
if (isLeaf) {
return getElements();
}
Set<T> result = new HashSet<T>(tm.getMaxElementsPerTile() * 2);
for (Tile<T> subTile : getChildren()) {
result.addAll(subTile.getInnerNodes());
}
return result;
}
@Override
public String toString() {
return toStringHelper(false);
}
public String toDebugString() {
return toStringHelper(true);
}
public String toStringHelper(boolean debug) {
StringBuilder sb = new StringBuilder();
int count = getElemCount();
if (isLeaf) {
sb.append(String
.format("Tile %s [%.15f ~ %.15f ; %.15f ~ %.15f], %d elements, %d%% full",
getID(),
getSize().getMinX(),
getSize().getMaxX(),
getSize().getMinY(),
getSize().getMaxY(),
count,
Math.round(count / (double) tm.getMaxElementsPerTile()
* 100.0)));
if (debug) {
for (T elem : getElements()) {
sb.append("\n\t").append(elem.toDebugString());
}
}
} else {
sb.append(String.format("Tile %s [%.15f ~ %.15f ; %.15f ~ %.15f]",
getID(), getSize().getMinX(), getSize().getMaxX(),
getSize().getMinY(), getSize().getMaxY()));
for (Tile<T> subTile : getChildren()) {
sb.append("\n ").append(
subTile.toStringHelper(debug).replace("\n", "\n "));
}
}
return sb.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((ID == null) ? 0 : ID.hashCode());
result = prime * result + (isLeaf ? 1231 : 1237);
result = prime * result + ((size == null) ? 0 : size.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
@SuppressWarnings("rawtypes")
Tile other = (Tile) obj;
if (ID == null) {
if (other.ID != null) {
return false;
}
} else if (!ID.equals(other.ID)) {
return false;
}
if (isLeaf != other.isLeaf) {
return false;
}
if (size == null) {
if (other.size != null) {
return false;
}
} else if (!size.equals(other.size)) {
return false;
}
return true;
}
public Set<Tile<T>> getAllChildren() {
Set<Tile<T>> resultSet = new HashSet<Tile<T>>();
if (isLeaf) {
resultSet.add(this);
return resultSet;
}
for (Tile<T> child : getChildren()) {
resultSet.addAll(child.getLeafChildren());
}
return resultSet;
}
public Set<Tile<T>> getLeafChildren() {
Set<Tile<T>> resultSet = new HashSet<Tile<T>>();
if (isLeaf) {
return resultSet;
}
for (Tile<T> child : getChildren()) {
resultSet.addAll(child.getLeafChildren());
}
return resultSet;
}
public boolean isRoot() {
return "0".equals(getID());
}
}