/*
* Copyright (c) 2014 Oculus Info Inc. http://www.oculusinfo.com/
*
* Released under the MIT License.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.oculusinfo.binning;
import java.awt.*;
import java.awt.geom.Point2D;
import java.io.Serializable;
import java.util.Comparator;
/**
* This class uses a Z-order curve (Morton curve) to sort points so that at
* every level, points in the same tile will be grouped together.
*
* Direct comparison code is taken from
* http://en.wikipedia.org/wiki/Z-order_curve
*
* Key-generation code is taken from one of Chris Bethune's projects.
*
* This class acts as a comparator, but does not implement the comparator
* interface, because it can handle both raw points and indices (and we can't
* simultaneously implement Comparator<Point> and Comparator<TileIndex>); if
* comparator implementation is needed, though - say, for instance, for sorting
* - it can provide one.
*
* @author nkronenfeld
*/
public class PyramidComparator implements Serializable {
private static final long serialVersionUID = 1L;
// Constants for bit interleaving.
public static final long BITS[] = {
0x5555555555555555L, 0x3333333333333333L, 0x0F0F0F0F0F0F0F0FL,
0x00FF00FF00FF00FFL, 0x0000FFFF0000FFFFL, 0x00000000FFFFFFFFL};
public static final long SHIFTS[] = {1, 2, 4, 8, 16};
public static final byte SW = 0;
public static final byte SE = 1;
public static final byte NW = 2;
public static final byte NE = 3;
/**
* Comparing points at level 20 has a granularity of 1M in each dimension in
* tiles alone
*
* This means, with 1E12 partitions - way more than hadoop or spark can
* handle - we would have roughly 1 tile/partition. So this should be
* fine-grained enough for anything we might care about.
*/
private static final int DEFAULT_COMPARISON_LEVEL = 20;
private int _comparisonLevel;
private TilePyramid _pyramid;
private Comparator<Point> _rawComparator;
private Comparator<TileIndex> _indexComparator;
/**
* Create a comparator that compares raw values at the default level (
* {@link #DEFAULT_COMPARISON_LEVEL} (or indices at any level)
*
* @param pyramid They pyramid scheme with which raw values are converted to
* tile values
*/
public PyramidComparator (TilePyramid pyramid) {
this(pyramid, DEFAULT_COMPARISON_LEVEL);
}
/**
* Creates a comparator that compares raw values at the given level (or
* indices at any level)
*
* @param pyramid They pyramid scheme with which raw values are converted to
* tile values
* @param comparisonLevel The level at which raw values are converted to
* tile indices for comparison. Comparing at level n gives 2^n
* bins in each dimension, or 4^n different discernable locations
* for comparison
*/
public PyramidComparator (TilePyramid pyramid, int comparisonLevel) {
_pyramid = pyramid;
_comparisonLevel = comparisonLevel;
_rawComparator = new RawCoordinateComparator();
_indexComparator = new TileIndexComparator();
}
/**
* Compare two raw input values according to our pyramid scheme
*
* @return Standard comparison values, as per {@link Comparator}
*/
public int compareRaw (Point2D pt1, Point2D pt2) {
// Mostly taken from http://en.wikipedia.org/wiki/Z-order_curve, but
// this is a very simple 2-dimensional case, so the results are rather
// simplified.
TileIndex tile1 = _pyramid.rootToTile(pt1, _comparisonLevel);
TileIndex tile2 = _pyramid.rootToTile(pt2, _comparisonLevel);
return compareIndexAtLevel(tile1, tile2);
}
/**
* Compare two raw input values according to our pyramid scheme
*
* @return Standard comparison values, as per {@link Comparator}
*/
public int compareRaw (double x1, double y1, double x2, double y2) {
TileIndex tile1 = _pyramid.rootToTile(x1, y1, _comparisonLevel);
TileIndex tile2 = _pyramid.rootToTile(x2, y2, _comparisonLevel);
return compareIndexAtLevel(tile1, tile2);
}
/**
* Returns a lookup key given a set of index values uniquely identifying a
* tile in the tree. The bits of the X and Y are interleaved to generate a
* Morton code, and then combined with an offset for level to get a final
* location code.
*
* @param rawX The x coordinate of the raw data
*
* @param rawY The y coordinate of the raw data
*
* @return A location key that should be in order according to the Morton
* curve ordering.
*/
public long getComparisonKey (double rawX, double rawY) {
TileIndex index = _pyramid.rootToTile(rawX, rawY, _comparisonLevel);
long x = index.getX();
long y = index.getY();
x = (x | (x << SHIFTS[4])) & BITS[4];
x = (x | (x << SHIFTS[3])) & BITS[3];
x = (x | (x << SHIFTS[2])) & BITS[2];
x = (x | (x << SHIFTS[1])) & BITS[1];
x = (x | (x << SHIFTS[0])) & BITS[0];
y = (y | (y << SHIFTS[4])) & BITS[4];
y = (y | (y << SHIFTS[3])) & BITS[3];
y = (y | (y << SHIFTS[2])) & BITS[2];
y = (y | (y << SHIFTS[1])) & BITS[1];
y = (y | (y << SHIFTS[0])) & BITS[0];
long z = x | (y << 1);
// Apply fence bit
return (0x01L << (2 * (_comparisonLevel + 1))) | z;
}
/**
* Compare two tile indices. The indices are compared at the level of the
* least zoomed-in of the two tiles.
*
* @return Standard comparison values, as per {@link Comparator}
*/
public int compareIndex (TileIndex t1, TileIndex t2) {
int l1 = t1.getLevel();
int l2 = t2.getLevel();
if (l1 < l2) {
// Bring t2 up to the level of t1
int p2 = 1 << (l2 - l1);
t2 = new TileIndex(l1, t2.getX() / p2, t2.getY() / p2,
t2.getXBins(), t2.getYBins());
} else if (l1 > l2) {
// Bring t1 up to the level of t2
int p2 = 1 << (l1 - l2);
t1 = new TileIndex(l2, t1.getX() / p2, t1.getY() / p2,
t1.getXBins(), t1.getYBins());
}
return compareIndexAtLevel(t1, t2);
}
/*
* Compare two tile indices, making the assumption that they are at the same
* level. This method is private so that we may guarantee that all calls to
* it take that assumption into account.
*/
private int compareIndexAtLevel (TileIndex t1, TileIndex t2) {
int x1 = t1.getX();
int y1 = t1.getY();
int x2 = t2.getX();
int y2 = t2.getY();
int x = x1 ^ x2;
int y = y1 ^ y2;
// if =, default to x
if (y < x && y < (x ^ y)) {
return x1 - x2;
} else {
return y1 - y2;
}
}
/**
* Retrieve a implementation of Comparator that can be used to compare raw
* points generically.
*/
public Comparator<Point> getRawCoordinateComparator () {
return _rawComparator;
}
/**
* Retreive an implementation of Comparator that can be used to compare tile
* indices generically.
*/
public Comparator<TileIndex> getTileIndexComparator () {
return _indexComparator;
}
private class RawCoordinateComparator implements Comparator<Point>,
Serializable {
private static final long serialVersionUID = 1L;
@Override
public int compare (Point p1, Point p2) {
return compareRaw(p1, p2);
}
}
private class TileIndexComparator implements Comparator<TileIndex>,
Serializable {
private static final long serialVersionUID = 1L;
@Override
public int compare (TileIndex t1, TileIndex t2) {
return compareIndex(t1, t2);
}
}
}