/**
Copyright 2015 Tim Engler, Rareventure LLC
This file is part of Tiny Travel Tracker.
Tiny Travel Tracker is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Tiny Travel Tracker is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>.
*/
package com.rareventure.gps2.database.cache;
import java.util.ArrayList;
import java.util.Arrays;
import android.util.Log;
import com.rareventure.android.Util;
import com.rareventure.android.database.Cache;
import com.rareventure.android.encryption.EncryptedRow;
import com.rareventure.gps2.CacheException;
import com.rareventure.gps2.GTG;
import com.rareventure.gps2.database.TAssert;
import com.rareventure.gps2.database.cache.TimeTree.IntersectsTimeStatus;
import com.rareventure.gps2.reviewer.map.Mercator;
/**
* An areapanel is a square of X Y coordinates shared in the same coordinates as
* mercator and open street maps. It differs in the X and Y coordinates which are
* always the same regardless of the depth (osm resets the coordinates per each depth
* level).
* <p>Each area panel contains a set of sub area panels which each contain a set of sub-sub
* area panels and so on to the deepest level defined by MAX_DEPTH.
* <p>
* X goes left to right and Y goes top to bottom
* <p>
* When displaying points to the screen, we find a depth of area panels appropriate
* for the zoom level. In this way, if the user is zoomed out so that a city is just
* a single point, we can use a large area panel that encompasses the points
* in the city. OTOH, if we are zoomed in very closely, we would use a smaller
* area panels to represent the points.
* </p>
*/
public class AreaPanel extends EncryptedRow {
//x and y are absolute coordinates of the earth
public static final Column X = new Column("X", Integer.class);
public static final Column Y = new Column("Y", Integer.class);
public static final Column DEPTH = new Column("DEPTH", Integer.class);
//number of child subpanels contained in one panel.
//TODO 3: WARNING: this value must be two for support of line views and
// the current implementation of autozoom
public static final int NUM_SUB_PANELS_PER_SIDE = 2;
public static final int NUM_SUB_PANELS = NUM_SUB_PANELS_PER_SIDE*NUM_SUB_PANELS_PER_SIDE;
/**
* This is the size of the area panel in MAX_AP_UNITS at each depth level (lower value
* means smaller panels), up to the root level with one tile.
*/
public static final int [] DEPTH_TO_WIDTH;
public static final double LATLON_TO_LATLONM = 1000000.;
/**
* The maximum layers of area panels
*/
public static int MAX_DEPTH = 24;
static {
//we use 26 because its the max depth of opernstreetmaps
//trying depth of 24
//here is 26 for 2 1/2 years
// -rw-rw-r-- root sdcard_rw 14013804 2012-11-20 15:07 AreaPanel.tt
// -rw-rw-r-- root sdcard_rw 54316 2012-11-20 15:07 MediaLocTime.tt
// -rw-rw-r-- root sdcard_rw 86251876 2012-11-20 15:07 TimeTree.tt
// -rw-rw-r-- root sdcard_rw 6208 2012-11-20 17:01 cache.td
// -rw-rw-r-- root sdcard_rw 13123584 2012-11-20 16:55 gps.db3
// -rw-rw-r-- root sdcard_rw 524288 2012-11-20 16:55 gps.db3-journal
// drwxrwxr-x root sdcard_rw 2012-11-08 15:36 tile_cache
int maxDepth = (int)Math.floor((MAX_DEPTH) * Math.log(2) / Math.log(NUM_SUB_PANELS_PER_SIDE));
DEPTH_TO_WIDTH = new int[maxDepth+1];
DEPTH_TO_WIDTH[0] = 1;
for(int i = 1; i < DEPTH_TO_WIDTH.length; i++)
DEPTH_TO_WIDTH[i] = DEPTH_TO_WIDTH[i-1] * NUM_SUB_PANELS_PER_SIDE;
}
/**
* Max ap units is the number of panels at the deepest level we
*/
public static final int MAX_AP_UNITS = DEPTH_TO_WIDTH[DEPTH_TO_WIDTH.length-1];
public static final Column TIME_TREE_FK = new Column("TIME_TREE_FK",Integer.class);
public static final Column SUB_AREA_PANELS = new Column("SUB_AREA_PANELS",(Integer.SIZE >> 3) * NUM_SUB_PANELS);
//PERF: do we really need weekly? it may be overkill
public static final int [] TIME_JUMP_SECS =
new int[] {
0,
(int) (1000 * 3600 * 25.5), //a little over a day for every day trips
1000 * 3600 * 8 // a little over a week for weekly trips
};
public static final Column[] COLUMNS = new Column[] { Y, X, DEPTH, TIME_TREE_FK, SUB_AREA_PANELS};
public static final Preferences prefs = new Preferences();
/**
* size of data
*/
public static final int DATA_LENGTH =
EncryptedRow.figurePosAndSizeForColumns(COLUMNS);
public AreaPanel() {
super();
}
public int getDataLength()
{
return DATA_LENGTH;
}
public static int convertXToLonm(int x) {
return (int) Math.round((Util.LONM_PER_WORLD*(double)x / MAX_AP_UNITS) + Util.MIN_LONM);
}
public static double convertXToLon(int x) {
return (Util.LON_PER_WORLD*(double)x / MAX_AP_UNITS) + Util.MIN_LON;
}
public static int convertYToLatm(int y) {
int result = (int) Math.round(Mercator.y2lat(Mercator.MAX_Y - y * Mercator.MAX_Y*2 / MAX_AP_UNITS)*LATLON_TO_LATLONM);
// Log.d("GTG","convert y to latm, y: "+y+" result: "+result);
return result;
}
public static double convertYToLat(int y) {
return Mercator.y2lat(Mercator.MAX_Y - y * Mercator.MAX_Y*2 / MAX_AP_UNITS );
}
public static int convertLonmToX(int lonm) {
return (int) Math.round((MAX_AP_UNITS*(double)(lonm - Util.MIN_LONM) / Util.LONM_PER_WORLD));
}
public static int convertLonToX(double lon) {
return (int) Math.round((MAX_AP_UNITS*(double)(lon * LATLON_TO_LATLONM - Util.MIN_LONM) / Util.LONM_PER_WORLD));
}
public static double convertLonToXDouble(double lon) {
return MAX_AP_UNITS*(double)(lon - Util.MIN_LON) / Util.LON_PER_WORLD;
}
/**
* Returns the y position based on latitude.
* Note that this is based of Mercator which will have infinite number of points approaching
* +/-90 degrees. So if we are out of range, (beyond the configured min/max latitude),
* we return -1 and MAX_AP_UNITS based on which direction we are out
*
* @param latm
* @return
*/
public static int convertLatmToY(int latm) {
long v = Math.round(
( Mercator.MAX_Y - Mercator.lat2y(latm/LATLON_TO_LATLONM) )
* MAX_AP_UNITS / (Mercator.MAX_Y * 2));
// Log.d("GTG","convert latm to y, latm: "+latm+" result: "+v);
if(v > MAX_AP_UNITS)
return MAX_AP_UNITS;
if(v < 0)
return -1;
return (int) v;
}
public static int convertLatToY(double lat) {
long v = Math.round((Mercator.MAX_Y - Mercator.lat2y(lat)) * MAX_AP_UNITS / (Mercator.MAX_Y * 2));
// Log.d("GTG","convert latm to y, latm: "+latm+" result: "+v);
if(v > MAX_AP_UNITS)
return MAX_AP_UNITS;
if(v < 0)
return -1;
return (int) v;
}
public static double convertLatToYDouble(double lat) {
double v = (Mercator.MAX_Y - Mercator.lat2y(lat)) * MAX_AP_UNITS / (Mercator.MAX_Y * 2);
// Log.d("GTG","convert latm to y, latm: "+latm+" result: "+v);
if(v > MAX_AP_UNITS)
return MAX_AP_UNITS;
if(v < 0)
return -1;
return v;
}
public int getX()
{
return getInt(X);
}
public int getY()
{
return getInt(Y);
}
public int getDepth()
{
return getInt(DEPTH);
}
public int getTimeTreeFk()
{
return getInt(TIME_TREE_FK);
}
public int getSubAreaPanelFk(int i) {
return Util.byteArrayToInt(data2, SUB_AREA_PANELS.pos + i * (Integer.SIZE >> 3));
}
public ArrayList<AreaPanel> getChildrenPanels()
{
ArrayList<AreaPanel> children = new ArrayList<AreaPanel>(NUM_SUB_PANELS);
for(int i = 0; i < NUM_SUB_PANELS; i++)
{
if (getSubAreaPanelFk(i) != Integer.MIN_VALUE)
children.add(getSubAreaPanel(i));
}
return children;
}
public void setChildAreaPanel(AreaPanel child, int i) {
setChildAreaPanelFk(child.id, i);
}
public void setChildAreaPanelFk(int childFk, int index) {
setInt(SUB_AREA_PANELS.pos+index*(Integer.SIZE >> 3), childFk);
}
public void setData(int x, int y, int depth) {
// public static final Column[] COLUMNS = new Column[] { MIN_LATM, MIN_LONM, TIME_INTERVAL_TREE_HEAD_FK, SUB_AREA_PANELS, POINT_COUNT, LINE_COUNT,
// POINTS, LINES};
if(data2 == null)
{
data2 = new byte [DATA_LENGTH];
}
setInt(X.pos,x);
setInt(Y.pos,y);
setInt(TIME_TREE_FK.pos,Integer.MIN_VALUE);
for(int i = 0; i < NUM_SUB_PANELS; i++)
setChildAreaPanelFk(Integer.MIN_VALUE, i);
setInt(DEPTH.pos, depth);
}
public boolean containsPoint(int x, int y) {
//if the point is out of range or the status isn't set
if(x < getX() || x >= getMaxX() || y < getY() || y >= getMaxY())
return false;
return true;
}
public String toStringFieldsOnly()
{
return
String.format("AreaPanel(id=%d,x=%d,y=%d,mx=%d,my=%d,depth=%d," +
"timeIntervalTreeHeadFk=%d,stSec=%d,etSec=%d,sp0=%d,sp1=%d,sp2=%d,sp3=%d)",
this.id, getX(), getY(), getMaxX(), getMaxY(), getDepth(),
getTimeTreeFk(),
getTimeTree() == null ? -1 : getStartTimeSec(),
getTimeTree() == null ? -1 : getEndTimeSec(),
getSubAreaPanelFk(0),
getSubAreaPanelFk(1),
getSubAreaPanelFk(2),
getSubAreaPanelFk(3)
);
}
public String toString()
{
return "#"+toStringFieldsOnly();
// +"\n"+Util.gnuPlot2DIt(
// minLonm, minLatm,
// maxLonm, minLatm,
// maxLonm, maxLatm,
// minLonm, minLatm,
// minLonm, maxLatm);
}
public static class Preferences
{
}
/**
* Adds a point to the panel and saves it
*/
public void addPoint(int id, AreaPanel prevAp, int prevX, int prevY, int x,
int y, int lastTimeSec, int timeSec, double dist) {
//handle child
if(getDepth() == 0)
//TODO 3: do we want to link to gps points?
//setPointFk(id);
{
}
else
{
int depth = getDepth();
int xIndex = (x-getX())/(DEPTH_TO_WIDTH[depth-1]);
int yIndex = (y-getY())/(DEPTH_TO_WIDTH[depth-1]);
int index = xIndex + yIndex * NUM_SUB_PANELS_PER_SIDE ;
// Log.d(GTG.TAG,"addPoint: subApFk = "+getSubAreaPanelFk(index)+" prevAp="+prevAp);
if(getSubAreaPanelFk(index) == Integer.MIN_VALUE)
{
AreaPanel childAreaPanel = GTG.apCache.newRow();
childAreaPanel.setData(getX() + xIndex * DEPTH_TO_WIDTH[depth-1],
getY() + yIndex * DEPTH_TO_WIDTH[depth-1], depth-1);
AreaPanel subPrevAp = null;
if(prevAp != null)
{
subPrevAp = prevAp.getSubAreaPanel(prevX, prevY);
if(subPrevAp == null)
throw new CacheException("What? why is there a prevAp but not a subPrevAp? "+prevAp+" prevX "+prevX+" prevY "+prevY);
}
childAreaPanel.addPoint(id, prevAp == null ? null : subPrevAp,
prevX, prevY,
x,y, lastTimeSec, timeSec, dist);
setSubAreaPanelFk(index, childAreaPanel.id);
}
else
{
getSubAreaPanel(index).addPoint(id,prevAp.getSubAreaPanel(prevX, prevY),
prevX, prevY, x, y, lastTimeSec, timeSec, dist);
}
}
int prevApIdToUse = (prevAp == null ? Integer.MIN_VALUE : prevAp.id);
if(getTimeTreeFk() == Integer.MIN_VALUE)
{
//the areapanel will extend in time all the way from the last time to the current time
//we add 1 to last time sec, so that we won't be on at the last gps point
//we add 1 to timeSec because aps must be at least one second long and this
//guarantees this
//note that line calculating depends
//on the time between the time trees to be at least one second
setTimeTreeFk(TimeTree.createTimeTreeForGpsPoint(lastTimeSec+1, timeSec+1,
prevApIdToUse, dist).id);
}
else
{
TimeTree tt = getTimeTree();
//if the previous point was in the area panel, we denote that the areapanel
//contains the whole timeperiod from that point to this point
//Example if the last point was at 10:01 and the current was at 10:05,
//the time range contained by the area panel would include 10:01 through
// 10:05
if(tt.getMaxTimeSecs() >= lastTimeSec)
setTimeTreeFk(tt.addSegmentForPoint(tt.getMaxTimeSecs(), timeSec+1,
prevApIdToUse, dist).id);
else
setTimeTreeFk(tt.addSegmentForPoint(lastTimeSec+1, timeSec+1,
prevApIdToUse, dist).id);
}
//hook up the prev ap's time tree to this ap, and extend its time to
//up to but not including the current ap
if(prevAp != null && prevAp != this)
{
TimeTree tt = prevAp.getTimeTree();
//extend the ap to the current ap's time
//note, we set distance to zero since it doesn't actually touch
//the next point. Also note that since we are extending from tt.getMaxTimeSecs()
// this will never result in the creation of a new tt, so we can
// use extendTimeTree rather than addSegmentForPoint
tt.extendTimeTree(timeSec-1, 0, false);
//setup the next ap id
tt.setNextApIdForThisAndAllChildren(this.id);
}
// if(getDepth() == 0)
// Log.d(GTG.TAG,"Added ap "+this);
}
private AreaPanel getSubAreaPanel(int x, int y) {
int depth = getDepth();
int xIndex = (x-getX())/(DEPTH_TO_WIDTH[depth-1]);
int yIndex = (y-getY())/(DEPTH_TO_WIDTH[depth-1]);
int index = xIndex + yIndex * NUM_SUB_PANELS_PER_SIDE ;
return getSubAreaPanel(index);
}
public TimeTree getTimeTree() {
int fk = getTimeTreeFk();
if(fk == Integer.MIN_VALUE)
return null;
return GTG.ttCache.getRow(fk);
}
private void setTimeTreeFk(int fk) {
setInt(TIME_TREE_FK.pos, fk);
}
private void setSubAreaPanelFk(int index, int fk) {
if(fk == -1)
TAssert.fail();
setInt(SUB_AREA_PANELS.pos + index * (Integer.SIZE >> 3), fk);
}
public int getCenterLonm() {
return convertXToLonm(getCenterX());
}
public int getCenterLatm() {
return convertYToLatm(getCenterY());
}
public int getCenterX() {
return (getX() + getMaxX()) >> 1;
}
public int getCenterY() {
return (getY() + getMaxY()) >> 1;
}
public int getEndTimeSec() {
return getTimeTree().getMaxTimeSecs();
}
public int getStartTimeSec() {
return getTimeTree().getMinTimeSecs();
}
public static int getDepthUnder(int units) {
int index = Arrays.binarySearch(DEPTH_TO_WIDTH, units);
if(index >= 0)
return index;
//return the point that's just below the number of units
return (-index)-1;
}
public int getOverlappingMap(int minX, int minY, int maxX,
int maxY) {
int result = 0;
int depth = getDepth();
if(maxX >= getX())
{
if(minX < getX()+DEPTH_TO_WIDTH[depth-1])
result |= 1;
if(minX < getX()+DEPTH_TO_WIDTH[depth])
result |= 2;
}
if(maxY >= getY())
{
if(minY < getY()+DEPTH_TO_WIDTH[depth-1])
result |= 4;
if(minY < getY()+DEPTH_TO_WIDTH[depth])
result |= 8;
}
return result;
}
public AreaPanel getFirstOverlappingSubPanel(int startingIndex, int minX, int minY, int maxX,
int maxY, int startTimeSecs, int endTimeSecs) {
for(int i = startingIndex; i < NUM_SUB_PANELS; i++)
{
if(getSubAreaPanelFk(i) != Integer.MIN_VALUE &&
isRectOverlapsSubArea(minX, minY, maxX, maxY, i))
{
AreaPanel aa = getSubAreaPanel(i);
IntersectsTimeStatus status = TimeTree.intersectsTime(aa.getTimeTree(),startTimeSecs, endTimeSecs);
if(status.overlaps)
{
if(status == IntersectsTimeStatus.RANGE_OVERLAPS_MIN_TIME)
{
TimeTree tt = aa.getTimeTree().getBottomLevelEncompassigTimeTree(startTimeSecs);
if(tt.calcTimeRangeCutEnd() < startTimeSecs)
continue;
}
if(status == IntersectsTimeStatus.RANGE_OVERLAPS_MAX_TIME)
{
TimeTree tt = aa.getTimeTree().getBottomLevelEncompassigTimeTree(endTimeSecs);
if(tt.calcTimeRangeCutStart() > endTimeSecs)
continue;
}
return aa;
}
}
}
//doesn't overlap at all
return null;
}
public AreaPanel getSubAreaPanel(int i) {
int fk = getSubAreaPanelFk(i);
if(fk != Integer.MIN_VALUE)
{
AreaPanel ap = GTG.apCache.getRow(fk);
return ap;
}
else return null;
}
public boolean isRectOverlapsSubArea(int minX, int minY, int maxX,
int maxY, int i) {
int depth = getDepth();
return
(maxX > getX() + DEPTH_TO_WIDTH[depth-1] * (i%NUM_SUB_PANELS_PER_SIDE))
&& maxY > getY() + DEPTH_TO_WIDTH[depth-1] * (i/NUM_SUB_PANELS_PER_SIDE)
&& minX < getX() + DEPTH_TO_WIDTH[depth-1] * (i%NUM_SUB_PANELS_PER_SIDE +1)
&& minY < getY() + DEPTH_TO_WIDTH[depth-1] * (i/NUM_SUB_PANELS_PER_SIDE +1);
}
public int getIndexOfSubAreaPanel(AreaPanel lastSubAP) {
for(int i = 0; i < NUM_SUB_PANELS; i++)
{
if(getSubAreaPanelFk(i) == lastSubAP.id)
return i;
}
throw new IllegalArgumentException(lastSubAP+" not in panel");
}
public int getIndexOfSubAreaPanelFk(int lastSubApFk) {
for(int i = 0; i < NUM_SUB_PANELS; i++)
{
if(getSubAreaPanelFk(i) == lastSubApFk)
return i;
}
throw new IllegalArgumentException(lastSubApFk+" not in panel");
}
public static class PointCoverageData
{
public int minX, maxX, minY, maxY;
public int modAmount;
public PointCoverageData(int modAmount)
{
minY = minX = MAX_AP_UNITS;
maxX = maxY = 0;
this.modAmount = modAmount;
}
public void adjustFor(int x, int y) {
//modamount is a way to get around the fact if
//the active points overlap the 0 lonm point.
//It's not perfect, since if the points overlap wherever modPoint is,
//we would split the screen at that point as well. But it will handle
//any case where the length of the points don't extend beyond half the
//world
x = (int) (((long)x + modAmount) % MAX_AP_UNITS);
y = (int) (((long)y + modAmount) % MAX_AP_UNITS);
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
}
public int shouldAdjustFor(AreaPanel ap)
{
int res = 0;
res = res | (ap.getX() < minX ? 1 : 0);
res = res | (ap.getX()+DEPTH_TO_WIDTH[ap.getDepth()] > maxX ? 2 : 0);
res = res | (ap.getY() < minY ? 4 : 0);
res = res | (ap.getY()+DEPTH_TO_WIDTH[ap.getDepth()] > maxY ? 8 : 0);
return res;
}
}
public void getPointCoverage(int startTimeSec, int endTimeSec,
PointCoverageData pcd) {
int res = pcd.shouldAdjustFor(this);
if(res != 0)
{
if(getDepth() == 0)
{
pcd.adjustFor(getX(), getY());
}
else {
//we start with the sub panels at the subpanels which are on the edges of
//the current panel, and the
//current panel rests beyond the edge of the pcd in the same direction
//then we do the others
for(int tryOpposite = 0; tryOpposite < 2; tryOpposite++)
{
for(int i = 0; i < AreaPanel.NUM_SUB_PANELS; i++)
{
AreaPanel subPanel = getSubAreaPanel(i);
if(subPanel != null && (tryOpposite == 0) == (
((res & 1) == 1 && i % AreaPanel.NUM_SUB_PANELS_PER_SIDE == 0 )
||
((res & 2) == 2 &&
(i % AreaPanel.NUM_SUB_PANELS_PER_SIDE == AreaPanel.NUM_SUB_PANELS_PER_SIDE-1))
||
((res & 4) == 4 && i < AreaPanel.NUM_SUB_PANELS_PER_SIDE)
||
((res & 8) == 8 && i >= AreaPanel.NUM_SUB_PANELS_PER_SIDE *
(AreaPanel.NUM_SUB_PANELS_PER_SIDE - 1))
)
&& TimeTree.intersectsTime(subPanel.getTimeTree(),startTimeSec, endTimeSec).
overlaps)
subPanel.getPointCoverage(startTimeSec, endTimeSec, pcd);
}
}
}
}
}
@Override
public Cache getCache() {
return (Cache)GTG.apCache;
}
public int getMaxX() {
if(getDepth() >= DEPTH_TO_WIDTH.length || getDepth() < 0)
throw new CacheException("bad depth "+getDepth()+" dtow is "+DEPTH_TO_WIDTH.length);
return getX() + DEPTH_TO_WIDTH[getDepth()];
}
public int getWidth() {
return DEPTH_TO_WIDTH[getDepth()];
}
public int getMaxY() {
return getY() + DEPTH_TO_WIDTH[getDepth()];
}
public static double convertApYToAbsPixelY2(double apY, long zoom8BitPrec) {
return apY * zoom8BitPrec / AreaPanel.MAX_AP_UNITS;
}
public static double convertApXToAbsPixelX2(double apX, long zoom8BitPrec) {
return apX * zoom8BitPrec / AreaPanel.MAX_AP_UNITS;
}
public boolean overlapsStbXY(AreaPanelSpaceTimeBox newLocalStBox) {
return !outsideOfXY(newLocalStBox);
// return getX() < newLocalStBox.maxX && getY() <
// newLocalStBox.maxY
// && getMaxX() >= newLocalStBox.minX && getMaxY() >=
// newLocalStBox.minY;
}
public boolean outsideOfXY(AreaPanelSpaceTimeBox newLocalStBox) {
// if we are wrapping 0 degrees longitude
if (newLocalStBox.maxX < newLocalStBox.minX) {
return (getMaxX() < newLocalStBox.minX && getX() > newLocalStBox.maxX)
|| getMaxY() < newLocalStBox.minY
|| getY() > newLocalStBox.maxY;
}
return getMaxX() < newLocalStBox.minX
|| getMaxY() < newLocalStBox.minY
|| getX() > newLocalStBox.maxX
|| getY() > newLocalStBox.maxY;
}
public static AreaPanel findAreaPanelForTime(int timeSec, boolean latestPrevOrEarliestNext)
{
ArrayList<AreaPanel> candidates = new ArrayList<AreaPanel>();
candidates.add(GTG.apCache.getTopRow());
int currDepth = candidates.get(0).getDepth();
//if the time is outside all the points
if(candidates.get(0).getStartTimeSec() > timeSec && latestPrevOrEarliestNext
|| candidates.get(0).getEndTimeSec() <= timeSec && !latestPrevOrEarliestNext)
return null;
while(true)
{
AreaPanel bestAp = null;
if(candidates.size() > 1)
{
//note, there are often "high traffic" spots that contain a lot of minimum depth points
//that are repeatedly visited (even though they're max depth)
//because so if we don't look within timetree, we will end up with a lot
//of candidate points
//search all the candidates for areapanels that overlap the time
//or, if none of them do, then the areapanel with the closest time
//without going over or without going under depending on latestPrevOrEarliestNext
int bestTime = latestPrevOrEarliestNext ? Integer.MIN_VALUE : Integer.MAX_VALUE;
for(AreaPanel ap : candidates)
{
int apStartTime = ap.getStartTimeSec();
int apEndTime = ap.getEndTimeSec();
if(latestPrevOrEarliestNext)
{
if(timeSec >= apEndTime
&& bestTime < apEndTime-1)
{
bestTime = apEndTime-1;
bestAp = ap;
}
//else we're in the middle of it
else if(timeSec >= apStartTime)
{
int apTime = ap.getTimeTree().getNearestTimePoint(timeSec, true);
if(apTime > bestTime)
{
bestTime = apTime;
bestAp = ap;
}
}
//else it's completely after the time (and were looking for
// latest previous)
}
else //we're looking for earliest next
{
if(timeSec < apStartTime
&& bestTime > apStartTime)
{
bestTime = apStartTime;
bestAp = ap;
}
//else we're in the middle of it
else if(timeSec < apEndTime)
{
int apTime = ap.getTimeTree().getNearestTimePoint(timeSec, false);
if(apTime < bestTime)
{
bestTime = apTime;
bestAp = ap;
}
//co: two areapanels can share the same time because prev and next areapanel times
//overlap for each gps point
// else if (apTime == bestTime)
// {
// throw new IllegalStateException("two area panels should never share the same time");
// }
}
}
} //for each candidate
if(bestAp == null)
throw new CacheException("no good candidates?");
} //if there is more than one candidate
else
{
bestAp = candidates.get(0);
}
if(currDepth == 0)
return bestAp;
candidates.clear();
int bestNonOverlappingApTime = latestPrevOrEarliestNext ? Integer.MIN_VALUE : Integer.MAX_VALUE;
AreaPanel bestNonOverlappingSubAp = null;
for(int i = 0; i < NUM_SUB_PANELS; i++)
{
AreaPanel subAp = bestAp.getSubAreaPanel(i);
//if the sub ap contains the same time as the parent
//(at least one sub ap should always)
if(subAp == null)
continue;
if(subAp.getStartTimeSec() > timeSec)
{
if(!latestPrevOrEarliestNext)
{
if(subAp.getStartTimeSec() < bestNonOverlappingApTime)
{
bestNonOverlappingSubAp = subAp;
bestNonOverlappingApTime = subAp.getStartTimeSec();
}
}
continue;
}
if(subAp.getEndTimeSec() <= timeSec)
{
if(latestPrevOrEarliestNext)
{
if(subAp.getEndTimeSec()-1 > bestNonOverlappingApTime)
{
bestNonOverlappingSubAp = subAp;
bestNonOverlappingApTime = subAp.getEndTimeSec() - 1;
}
}
continue;
}
candidates.add(subAp);
}
//this occurs we get far enough down where the user only spent an instant within
//each area panel (ie a single gps reading). For background, an area panel that
//contains two sequential gps reading is considered to contain the time between
//both
//note that we always add the best non overlapping ap because even though an
//ap overlaps the time, doesn't mean its internal gap is in a better position
//then an external one
if(bestNonOverlappingSubAp != null)
candidates.add(bestNonOverlappingSubAp);
currDepth--;
} // while loop
}
public int getCenterTimeSec() {
return (getStartTimeSec()>>1) + (getEndTimeSec()>>1);
}
public int getTimeSpanSecs() {
return getEndTimeSec()-getStartTimeSec();
}
public boolean overlapsArea(int x1, int y1, int x2, int y2) {
if(getMaxX() <= x1 || getMaxY() <= y1 || getX() >= x2 || getY() >= y2)
return false;
return true;
}
/**
* Given a value, rounds it to the nearest areapanel boundary
* at a depth.
*
* @return
*/
public static int alignToDepth(int val, int depth) {
return val / DEPTH_TO_WIDTH[depth] * DEPTH_TO_WIDTH[depth];
}
/**
*
* @param depth
* @param timeSec
* @return the child ap that is at depth and encompasses timeSec
*/
public AreaPanel getChildApAtDepthAndTime(int depth, int timeSec) {
AreaPanel ap = this;
if(ap.getTimeTree().getBottomLevelEncompassigTimeTree(timeSec) == null)
return null;
for(;;)
{
if(ap.getDepth() == depth)
return ap;
int i;
for(i = NUM_SUB_PANELS - 1; i >= 0; i--)
{
AreaPanel childAp = ap.getSubAreaPanel(i);
if(childAp == null) continue;
if(childAp.getTimeTree().getBottomLevelEncompassigTimeTree(timeSec) == null)
continue;
ap = childAp;
break;
}
if(i == -1)
throw new CacheException("Parent contains time, but child does not");
}
}
public boolean overlaps(AreaPanel o) {
if(getMaxX() <= o.getX() || getMaxY() <= o.getY() || getX() >= o.getMaxX() || getY() >= o.getMaxY())
return false;
return true;
}
}