/**
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.reviewer.map.sas;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.TimeZone;
import java.util.TreeSet;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Looper;
import com.rareventure.android.Util;
import com.rareventure.gps2.CacheException;
import com.rareventure.gps2.GTG;
import com.rareventure.gps2.database.TimeZoneTimeRow;
import com.rareventure.gps2.database.cache.AreaPanel;
import com.rareventure.gps2.database.cache.AreaPanelSpaceTimeBox;
import com.rareventure.gps2.database.cache.TimeTree;
import com.rareventure.gps2.database.cachecreator.GpsTrailerCacheCreator;
import com.rareventure.gps2.reviewer.map.OsmMapGpsTrailerReviewerMapActivity;
import com.rareventure.gps2.reviewer.map.sas.Area.AreaPanelInfo;
import com.rareventure.util.ReadWriteThreadManager;
/**
* This finds when a user entered and left a given set of areas. It is meant to be updated dynamically
* and notify registered observers when the data changes .When finished, it will call
* activity.notifyPathsChanged() when the paths through the data set changes
*
* Note for path formation we use an AreaPanel structures rather than time ranges. The reason is
* that there can only be so many area panels (tens of them) per area, but there can be 1000 or more
* time ranges depending on how often the user came and left the place of interest.
* @author tim
*
*/
public class SelectedAreaSet extends Thread {
private static final float MIN_PATH_SPEED_M_PER_S_SQUARED = .5f * .5f;
private boolean isRunning = true;
private boolean isShutdown;
/**
* All access to this object must be synchronized through calls
* to this rwtm. If input variables are changed, this is considered
* a "write". The caller can have a reasonable expectation of timeliness
* when changing an input variable (ie. the result calculator will quit
* early if it detects the input variables are attempting to be changed)
*/
public ReadWriteThreadManager rwtm = new ReadWriteThreadManager();
private OsmMapGpsTrailerReviewerMapActivity activity;
private ArrayList<Area> requestedAreas = new ArrayList<Area>();
private int requestedStartTimeSec, requestedEndTimeSec;
/**
* Access to this variable can only occur after registering with rwtm
*/
private ArrayList<Path> resultPaths = new ArrayList<Path>();
private TreeSet<AreaTimeRange> resultTimeRangeTree = new TreeSet<AreaTimeRange>();
/**
* The areas for the current resultPaths and resultTimeRangeTree results
*/
private ArrayList<Area> resultAreas = new ArrayList<Area>();
/**
* The start and end times for the current resultPaths and resultTimeRangeTree results
*/
private int resultEndTime;
private int resultStartTime;
/**
* True if the time range tree and paths are up to date with the input data.
* This field is only set to false by the ui thread, and synchronization of
* it occurs outside of the rwtm. This is because while the run() method
* is updating the next valid results, we can't wait for it to finish
* before being able to read upToDate
*/
private boolean upToDate;
private boolean distCalculated;
public TimeZone timeZone = Util.getCurrTimeZone();
public SelectedAreaSet(OsmMapGpsTrailerReviewerMapActivity activity) {
this.activity = activity;
this.start();
}
public synchronized void shutdown() {
isRunning = false;
notify();
while (!isShutdown) {
try {
wait();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
// private void addPath(AreaPanelInfo stApi,
// AreaPanelInfo endApi) {
// int startTime = calcBestPathStart(stApi);
// int endTime = calcBestPathEnd(endApi);
//
// if(startTime <
// }
/**
* Note that for speed, it is up to the caller to
* create areas that are multiples of area panels
* whenever possible. ie. if we are selecting
* a huge swath of area, it would be best to be
* aligned at an area panel boundary, so that
* we aren't messing with a bunch of small area panels
*
* @param x1
* @param y1
* @param x2
* @param y2
*/
public void addArea(Area area) {
rwtm.registerWritingThread();
area.setIndex(requestedAreas.size());
requestedAreas.add(area);
upToDate = false;
notifyObserver();
synchronized (this) {
this.notify();
}
rwtm.unregisterWritingThread();
}
public void setRequestedTime(int startTime, int endTime) {
if (requestedStartTimeSec != startTime
|| requestedEndTimeSec != endTime) {
rwtm.registerWritingThread();
this.requestedStartTimeSec = startTime;
this.requestedEndTimeSec = endTime;
upToDate = false;
notifyObserver();
synchronized (this) {
this.notify();
}
rwtm.unregisterWritingThread();
}
}
private void notifyObserver() {
for (DataSetObserver observer : observers) {
if (Looper.getMainLooper().getThread() == Thread.currentThread())
observer.onChanged();
else {
final DataSetObserver localObserver = observer;
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
localObserver.onChanged();
}
});
//co: this version may cause deadlocks
//if were not on the main thread, we need to notify the observer, but this can
//only be done on the ui thread. So we tell the ui thread to do it, and wait
//for it to finish
// final boolean [] done = new boolean [1];
// Runnable r = new Runnable() {
//
// @Override
// public void run() {
// observer.onChanged();
//
// synchronized(this)
// {
// done[0] = true;
// notify();
// }
// }
// };
//
// activity.runOnUiThread(r);
// synchronized(r)
// {
// while(!done[0])
// try {
// r.wait();
// } catch (InterruptedException e) {
// throw new IllegalStateException(e);
// }
// }
}
}
}
public void run() {
while (isRunning) {
GTG.cacheCreatorLock.registerReadingThread();
rwtm.registerWritingThread();
boolean notifyPathsChanged = true;
try {
for (Area a : requestedAreas) {
a.calcAreaPanelInfos();
//if we were interrupted by a change to the input variables
//restart
if (rwtm.isWritingHoldingUpWritingThreads()) {
continue;
}
}
if (requestedAreas.size() > 1) {
if (runBackgroundTaskForPathCalculator()) {
distCalculated = false;
notifyPathsChanged = true;
}
} else if (requestedAreas.size() == 1) {
if (runBackgroundTaskForTimeRange()) {
distCalculated = false;
notifyPathsChanged = true;
calcTimeZone();
}
} else {
resultTimeRangeTree = new TreeSet<AreaTimeRange>();
copyRequestedInputToResultInput(true);
notifyPathsChanged = true;
upToDate = true;
}
} finally {
rwtm.unregisterWritingThread();
GTG.cacheCreatorLock.unregisterReadingThread();
}
//note we assume that we started not up to date, or we wouldn't be running anyway
if (upToDate) {
//notify the observer because we are now ready to display the results
notifyObserver();
rwtm.registerWritingThread();
calcDistForTimeRanges();
rwtm.unregisterWritingThread();
if (distCalculated) {
notifyObserver();
}
}
if(notifyPathsChanged)
activity.notifyPathsChanged();
synchronized (this) {
while (upToDate && isRunning)
try {
wait();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
isShutdown = true;
synchronized (this) {
notify();
}
}
private void calcTimeZone() {
timeZone = null;
if(!resultTimeRangeTree.isEmpty())
{
TimeRange tr = this.resultTimeRangeTree.first();
TimeZoneTimeRow tztr = GTG.tztSet.getTimeZoneCovering(tr.endTimeSec/2 + tr.startTimeSec/2);
if(tztr != null)
timeZone = tztr.getTimeZone();
}
if(timeZone == null)
timeZone = Util.getCurrTimeZone();
}
private void calcDistForTimeRanges() {
for (TimeRange tr : getTimeRangesAsCollection()) {
rwtm.pauseForReadingThreads();
if (rwtm.isWritingHoldingUpWritingThreads()) {
return;
}
tr.dist = GpsTrailerCacheCreator.calcTrDist(Math.max(tr.fullRangeStartSec, resultStartTime),
Math.min(tr.fullRangeEndSec, resultEndTime), rwtm);
//if we were interrupted
if(tr.dist == -1)
{
tr.dist = 0;
return;
}
}
distCalculated = true;
}
private Collection<? extends TimeRange> getTimeRangesAsCollection() {
if (resultAreas.isEmpty()) {
return new ArrayList<TimeRange>();
}
if (resultAreas.size() == 1)
return resultTimeRangeTree;
return resultPaths;
}
private Point p1 = new Point();
private Point p2 = new Point();
private AreaTimeRange lastTimeRange;
private int lastIndex;
private ArrayList<DataSetObserver> observers = new ArrayList<DataSetObserver>();
public void clearAreas() {
rwtm.registerWritingThread();
requestedAreas.clear();
upToDate = false;
notifyObserver();
synchronized (this) {
this.notify();
}
rwtm.unregisterWritingThread();
}
public void registerDataSetObserver(DataSetObserver observer) {
if (!observers.contains(observer))
observers.add(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
observers.remove(observer);
}
private AreaTimeRange getTimeRangeTr = new AreaTimeRange();
/**
* True if the worker thread was interrupted by a request to change
* the input variables
*/
private boolean workerInterrupted;
/**
* True if the current result was partially calculated last
* and not correct
*/
private boolean resultIncomplete;
private Object upToDateLock = new Object();
/**
* Note that this does a sequential search for the given index
* It saves the last position however, so as long as the requests
* are clustered, it should be pretty fast
*
* @param position
* @return
*/
public TimeRange getTimeRange(int position) {
synchronized (upToDateLock) {
if (!upToDate)
return null;
rwtm.registerReadingThread();
try {
if (isSingleAreaCalc()) {
if (lastTimeRange == null) {
lastTimeRange = resultTimeRangeTree.first();
lastIndex = 0;
}
while (lastIndex < position) {
//we compute the index on the fly,
lastTimeRange = resultTimeRangeTree
.higher(lastTimeRange);
lastIndex = lastIndex + 1;
}
while (lastIndex > position) {
lastTimeRange = resultTimeRangeTree
.lower(lastTimeRange);
lastIndex = lastIndex - 1;
}
//note technically we don't need to set the start and
//end times, (just the cut ones) but for consistancy...
getTimeRangeTr.fullRangeStartSec = lastTimeRange.fullRangeStartSec;
getTimeRangeTr.fullRangeEndSec = lastTimeRange.fullRangeEndSec;
getTimeRangeTr.startTimeSec = lastTimeRange.startTimeSec;
getTimeRangeTr.endTimeSec = lastTimeRange.endTimeSec;
getTimeRangeTr.dist = lastTimeRange.dist;
if (getTimeRangeTr.startTimeSec < requestedStartTimeSec) {
getTimeRangeTr.startTimeSec = requestedStartTimeSec;
}
if (getTimeRangeTr.endTimeSec > requestedEndTimeSec) {
getTimeRangeTr.endTimeSec = requestedEndTimeSec;
}
return getTimeRangeTr;
} else {
return resultPaths.get(position);
}
} finally {
rwtm.unregisterReadingThread();
}
}
}
private boolean isSingleAreaCalc() {
return resultAreas.size() <= 1;
}
public int getTimeRangeCount() {
synchronized (upToDateLock) {
if (!upToDate)
return 0;
rwtm.registerReadingThread();
try {
return isSingleAreaCalc() ? resultTimeRangeTree.size()
: resultPaths.size();
} finally {
rwtm.unregisterReadingThread();
}
}
}
public boolean isEmpty() {
synchronized (upToDateLock) {
if (upToDate) {
rwtm.registerReadingThread();
try {
return (isSingleAreaCalc() ? resultTimeRangeTree.isEmpty()
: resultPaths.isEmpty());
} finally {
rwtm.unregisterReadingThread();
}
}
return false;
}
}
private void deletePathsOutsideOfRange(ArrayList<Path> paths,
int startTime, int endTime) {
for (ListIterator<Path> i = paths.listIterator(); i.hasNext();) {
Path p = i.next();
if (p.startTimeSec < startTime || p.endTimeSec > endTime)
i.remove();
}
}
private ArrayList<Path> findApiPaths(ArrayList<Area> areas, int startTime,
int endTime) {
TreeSet<AreaPanelInfo> sortedApiTree = new TreeSet<AreaPanelInfo>();
sortedApiTree.clear();
for (Area a : areas) {
a.addResetApisToTree(sortedApiTree, startTime, endTime);
if (rwtm.isWritingHoldingUpWritingThreads()) {
workerInterrupted = true;
return null;
}
}
//we do this as follows:
//1. find the first api which corresponds to each area in path so
// that api for 1st area < api for 2nd area < api for 3rd area
//2. find the highest api for the second to last area in the path
// that is less than the api of the higher level of the path,
// moving the api's to the next time tree if necessary
//3. go to step 2, now using the api of the previous area to the last one
// handled. Continue this loop until we are at the start of the path
//4. now the complete path is found, push all apis after the time of
// the end of the path (including the last api of the path itself)
//5. loop back to one until we have exhausted all paths
//
// For instance:
// A1 B2 C1 D1 E3
// A1 B2 C1 D1 E3, path : null null E3
// A1 B2 C1 D1 E3, path : null B2 E3
// try to push B2 higher but lower than E3
// A1 C1 D1 B2 E3, path : null B2 E3
// now deal with 1
// A1 C1 D1 B2 E3, path : D1 B2 E3
// now push all items A,B,C,D,E past E and try again
ArrayList<Path> myPaths = new ArrayList<Path>();
//for each path to find in the tree
for (;;) {
AreaPanelInfo[] apiPath = new AreaPanelInfo[areas.size()];
//find the first api of the first area, then the first api
// of the second area that is greater than the first, etc
for (int currApiIndex = 0; currApiIndex < apiPath.length; currApiIndex++) {
//note that we have to start over each time, because we skip api's
//that are at a higher area than the currApiIndex (since we don't know
//the earliest apiIndex of the previous level, we can't find the earliest
//one that is greater than the last)
AreaPanelInfo api = sortedApiTree.isEmpty() ? null
: sortedApiTree.first();
while (api != null) {
if (api.areaIndex == currApiIndex) {
//if we found the earliest one that matches
if (currApiIndex == 0
|| api.currTtStartTime > apiPath[currApiIndex - 1].currTtStartTime) {
apiPath[currApiIndex] = api;
//go to the next area in the path
break;
} else {
AreaPanelInfo prevApi = sortedApiTree.lower(api);
//push it to the next tt that would work for the current path, or beyond if there
//isn't one
advanceApiToAtLeast(sortedApiTree, api,
apiPath[currApiIndex - 1].currTtStartTime,
endTime);
if (prevApi == null)
api = sortedApiTree.first();
else
api = sortedApiTree.higher(prevApi);
}
} else //not the area level that were interested in
{
//go to the next api
api = sortedApiTree.higher(api);
}
if (rwtm.isWritingHoldingUpWritingThreads()) {
workerInterrupted = true;
return null;
}
}
//if we went through the entire list without finding a valid api for the path at the current level
if (api == null)
//there are no more api paths
{
return myPaths;
}
}//while looking for the first valid path
//now that we found a valid path, we need to get the latest exit points for all waypoints (aka areas)
//in the path. This is so we don't get paths like 1 -> 2 -> 1 --> 2 --> 3
AreaPanelInfo endPathApi = apiPath[apiPath.length - 1];
//apiPathIndex is the index of the next higher area in the path
for (int apiPathIndex = areas.size() - 1; apiPathIndex >= 1; apiPathIndex--) {
//now find the highest api that does not exceed the api of the next level in the path
AreaPanelInfo currApi = sortedApiTree.first();
while (currApi != endPathApi) {
if (rwtm.isWritingHoldingUpWritingThreads()) {
workerInterrupted = true;
return null;
}
if (currApi.areaIndex != apiPathIndex - 1) {
currApi = sortedApiTree.higher(currApi);
continue;
}
//if we are currently the best api in the path for the current waypoint aka area
if (currApi == apiPath[apiPathIndex - 1]) {
//push as close as we can to the next level, but do not go past it
if (advanceApiUpTo(sortedApiTree, currApi,
apiPath[apiPathIndex].currTtStartTime)) {
//no-op, but note that apiPath now contains the same currApi
//but with a later start time
} else {
//if we were not able to move it at all then we keep going to
//the next item
currApi = sortedApiTree.higher(currApi);
}
} else {
AreaPanelInfo nextApi = sortedApiTree.higher(currApi);
//if we could advance the api beyond the current best without over shooting it
if (advanceApiBetweenOrLater(sortedApiTree, currApi,
apiPath[apiPathIndex - 1].currTtStartTime,
apiPath[apiPathIndex].currTtStartTime, endTime)) {
apiPath[apiPathIndex - 1] = currApi;
}
currApi = nextApi;
}
}//while looking at apis for the current waypoint aka area
}//for each waypoint aka area
//now apiPath should contain the first path, where for all waypoints besides the last one,
// the api is the last exit of the waypoint for the path, and for the last one, it is the
//first entry of the last waypoint
TimeTree startTree = calcActualStartOrEnd(areas.get(0), apiPath[0],
startTime, endTime, true);
TimeTree endTree = calcActualStartOrEnd(
areas.get(apiPath.length - 1), apiPath[apiPath.length - 1],
startTime, endTime, false);
Path p = new Path(apiPath, startTree, endTree);
myPaths.add(p);
//now move all the apis beyond the end point of the path to clear it out and go on to the
//next one
//now find the highest api that does not exceed the api of the next level in the path
AreaPanelInfo currApi = sortedApiTree.first();
while (currApi != endPathApi) {
AreaPanelInfo nextApi = sortedApiTree.higher(currApi);
advanceApiToAtLeast(sortedApiTree, currApi,
apiPath[apiPath.length - 1].currTtStartTime + 1,
endTime);
currApi = nextApi;
if (rwtm.isWritingHoldingUpWritingThreads()) {
workerInterrupted = true;
return null;
}
}
}//for each path in the tree
} //find api paths
private TimeTree calcActualStartOrEnd(Area a, AreaPanelInfo api,
int minStartTime, int maxEndTime, boolean calcStart) {
//we estimate the actual end of the path by tracing it
//through the area until:
// a) it leaves the area,
// b) it moves further away from the center of the area,
// c) the speed of movement as referenced from the distance
// between the area and the last area is less than a certain value
// Note that we don't use the distance traveled within the ap to
// make the speed calculation, because we don't want to include a path
// that went back and forth a lot
// We use the minDepth of the area as the depth level for the aps
int areaCenterX = a.getCenterX();
int areaCenterY = a.getCenterY();
TimeTree apiTt = api.tt();
AreaPanel lastAp = api.ap().getChildApAtDepthAndTime(a.minDepth,
apiTt.getMinTimeSecs());
TimeTree lastTt = lastAp.getTimeTree()
.getBottomLevelEncompassigTimeTree(apiTt.getMinTimeSecs());
long lastDistFromCenterSqr = ((long) lastAp.getX() - areaCenterX)
* (lastAp.getX() - areaCenterX)
+ ((long) lastAp.getY() - areaCenterY)
* (lastAp.getY() - areaCenterY);
for (;;) {
AreaPanel currAp = calcStart ? lastTt.getPrevAp() : lastTt
.getNextAp();
if (currAp == null)
break;
long currDistFromCenterSqr = ((long) currAp.getX() - areaCenterX)
* ((long) currAp.getX() - areaCenterX)
+ ((long) currAp.getY() - areaCenterY)
* ((long) currAp.getY() - areaCenterY);
if (lastDistFromCenterSqr < currDistFromCenterSqr)
break;
long currDistFromLastSqr = ((long) currAp.getX() - lastAp.getX())
* ((long) currAp.getX() - lastAp.getX())
+ ((long) currAp.getY() - lastAp.getY())
* ((long) currAp.getY() - lastAp.getY());
TimeTree currTt = currAp.getTimeTree()
.getBottomLevelEncompassigTimeTree(
calcStart ? lastTt.getMinTimeSecs() : lastTt
.getMaxTimeSecs());
//make sure the time tree isn't out of bounds of the stb
if (calcStart ? (currTt.getMinTimeSecs() < minStartTime) : (currTt
.getMaxTimeSecs() > maxEndTime))
break;
if (currDistFromLastSqr / (float) currTt.getTimeSec()
/ currTt.getTimeSec() < MIN_PATH_SPEED_M_PER_S_SQUARED)
break;
lastAp = currAp;
lastDistFromCenterSqr = currDistFromCenterSqr;
lastTt = currTt;
}
return lastTt;
}
/**
* Moves api to the latest value before time.
* @param sortedApiTree
*
* @return false if it couldn't be moved at all
*/
private boolean advanceApiUpTo(TreeSet<AreaPanelInfo> sortedApiTree,
AreaPanelInfo api, int time) {
AreaPanel ap = api.ap();
TimeTree rootTt = ap.getTimeTree();
TimeTree tt = rootTt.getEncompassigTimeTreeOrMaxTimeTreeBeforeTime(
time, true);
if (tt == null || tt.id == api.currTtId) //if we weren't able to move it at all
return false;
sortedApiTree.remove(api);
//add it back it in (it's guaranteed that we won't pass endtime so we pass Integer.MAX_VALUE for end time)
if (api.setTt(tt, Integer.MAX_VALUE))
sortedApiTree.add(api);
return true;
}
/**
* Moves api to the minimum value past start time, or deletes it completely if there isn't
* a time after start time
* @param sortedApiTree
* @param endTime if api extends past this time it is killed
*/
private void advanceApiToAtLeast(TreeSet<AreaPanelInfo> sortedApiTree,
AreaPanelInfo api, int startTime, int endTime) {
sortedApiTree.remove(api);
AreaPanel ap = api.ap();
TimeTree rootTt = ap.getTimeTree();
if (api.setTt(rootTt.getMinTimeTreeAfterTime(startTime), endTime))
//add it back it in
sortedApiTree.add(api);
}
/**
* Advances the api to the latest time tree that is >= minTime and <= maxTime. If this is
* not possible, will advance even further to the earliest time after maxTime
* @param sortedApiTree
* @param endTime the absolute end time for the tt. If it goes beyond this point the
* api is marked exhausted
*
* @return true if was able to advance within minTime and maxTime
*/
private boolean advanceApiBetweenOrLater(
TreeSet<AreaPanelInfo> sortedApiTree, AreaPanelInfo api,
int minTime, int maxTime, int endTime) {
sortedApiTree.remove(api);
AreaPanel ap = api.ap();
TimeTree rootTt = ap.getTimeTree();
TimeTree tt = rootTt.getEncompassigTimeTreeOrMaxTimeTreeBeforeTime(
maxTime, true);
//if we can't find a time tree between min and than max time
if (tt == null || tt.getMinTimeSecs() < minTime) {
//choose the earliest one after max time
tt = rootTt.getEncompassigTimeTreeOrMinTimeTreeAfterTime(maxTime,
true);
//if there is one
if (api.setTt(tt, endTime)) {
//add it back it in
sortedApiTree.add(api);
}
//otherwise leave it removed
return false;
} else {
//we found one less than amx time and greater than min time
if (api.setTt(tt, endTime))
sortedApiTree.add(api);
return true;
}
}
/**
* @return true if a change to the result has been made. Note that
* the gui may still have to update itself if the process has been interrupted
* (in which case the result does not match the request)
*/
private boolean runBackgroundTaskForPathCalculator() {
if (resultAreas.size() != requestedAreas.size()
|| requestedEndTimeSec <= resultStartTime
|| requestedStartTimeSec >= resultEndTime || resultIncomplete
|| resultPaths.size() == 0) {
ArrayList<Path> paths = findApiPaths(requestedAreas,
requestedStartTimeSec, requestedEndTimeSec);
if (workerInterrupted) {
workerInterrupted = false;
return false;
}
resultPaths = paths;
copyRequestedInputToResultInput(false);
upToDate = true;
return true;
}
//since we have to keep the old results paths valid
// (in case there are still references to it somewhere)
// we construct a new array to hold our changed results
ArrayList<Path> newResultPaths = null;
if (requestedEndTimeSec < resultEndTime
|| requestedStartTimeSec > resultStartTime) {
newResultPaths = new ArrayList<Path>(resultPaths);
deletePathsOutsideOfRange(newResultPaths, requestedStartTimeSec,
requestedEndTimeSec);
//this is set here incase we are interrupted later, so that we
// still have a valid result
resultStartTime = Math.max(requestedStartTimeSec, resultStartTime);
resultEndTime = Math.min(requestedEndTimeSec, resultEndTime);
}
if (requestedStartTimeSec < resultStartTime)
//we have to find paths up to the start of the currently known paths
//or a partial path that we ignored last time wouldn't be found this time
{
ArrayList<Path> newPaths = findApiPaths(requestedAreas,
requestedStartTimeSec, resultPaths.get(0).startTimeSec);
if (workerInterrupted) {
workerInterrupted = false;
return false; //if we we're up to date before and we're not null
//it doesn't really affect the viewer
}
if (!newPaths.isEmpty()) {
if (newResultPaths == null)
newResultPaths = new ArrayList<Path>(resultPaths);
newResultPaths.addAll(0, newPaths);
}
}
if (requestedEndTimeSec > resultEndTime) {
//we have to find paths up to the start of the currently known paths
//or a partial path that we ignored last time wouldn't be found this time
ArrayList<Path> newPaths = findApiPaths(requestedAreas,
resultPaths.get(resultPaths.size() - 1).endTimeSec,
requestedEndTimeSec);
if (workerInterrupted) {
workerInterrupted = false;
return false; //if we we're up to date before and we're not null
//it doesn't really affect the viewer
}
if (!newPaths.isEmpty()) {
if (newResultPaths == null)
newResultPaths = new ArrayList<Path>(resultPaths);
newResultPaths.addAll(newPaths);
}
}
copyRequestedInputToResultInput(false);
if (newResultPaths != null) {
resultPaths = newResultPaths;
upToDate = true;
return true;
}
upToDate = true;
return false;
}
/**
* Adds to the internal time range tree for the given area
*
* @param requestedStartTimeSec
* @param requestedEndTimeSec
*/
public void updateTimeRangeTree(int incStartTimeSec, int incEndTimeSec) {
// path from root of time trees for current area panel
ArrayList<Integer> timeTreeParentPath = new ArrayList<Integer>();
for (AreaPanelInfo api : requestedAreas.get(0).apiList) {
GTG.cacheCreatorLock.registerReadingThread();
try {
AreaPanel ap = api.ap();
timeTreeParentPath.clear();
timeTreeParentPath.add(ap.getTimeTree().id);
int lastSiblingTtFk = -1;
//loop for searching through the ap's tt's
while (!timeTreeParentPath.isEmpty()) {
if (rwtm.isWritingHoldingUpWritingThreads()) {
workerInterrupted = true;
resultIncomplete = true;
return;
}
TimeTree tt = GTG.ttCache.getRow(timeTreeParentPath
.get(timeTreeParentPath.size() - 1));
if (tt.getSubNodeFk(0) == Integer.MIN_VALUE)
{
if( tt.getMinTimeSecs() < incEndTimeSec
&& tt.getMaxTimeSecs() > incStartTimeSec)
// if at the bottom and within the area we're
// interested in
{
int cutStartTimeSec = tt.calcTimeRangeCutStart();
int cutEndTimeSec = tt.calcTimeRangeCutEnd();
if(cutStartTimeSec < incEndTimeSec && cutEndTimeSec > incStartTimeSec)
addTimeRange(tt, cutStartTimeSec, cutEndTimeSec);
}
// pop the tt off the stack so we can check out its
// siblings
lastSiblingTtFk = timeTreeParentPath
.remove(timeTreeParentPath.size() - 1);
} else {
// check the children
int nextTTChildIndex = 0;
// if we already handled a prior child from the current
// branch
if (lastSiblingTtFk != -1) {
for (; nextTTChildIndex < TimeTree.NUM_NODES; nextTTChildIndex++) {
if (tt.getSubNodeFk(nextTTChildIndex) == lastSiblingTtFk) {
break;
}
}
if (nextTTChildIndex == TimeTree.NUM_NODES) {
throw new CacheException(
"Couldn't find last tt sibling? "
+ lastSiblingTtFk + ": "
+ tt.getSubNodeFk(0) + ", "
+ tt.getSubNodeFk(1) + ", "
+ tt.getSubNodeFk(2) + ", "
+ tt.getSubNodeFk(3));
}
lastSiblingTtFk = -1;
// increase nextTTChildIndex so that we skip the
// last sibling (which we already went down)
nextTTChildIndex++;
}
// find a sub tt that matches
// note that there may not be one, since we might be
// starting half way through the
// sub nodes because lastSiblingTtFk was set
TimeTree subTT = null;
for (; nextTTChildIndex < TimeTree.NUM_NODES; nextTTChildIndex++) {
subTT = tt.getSubNode(nextTTChildIndex);
if (subTT == null)
break;
// if we found it
if (subTT.getMinTimeSecs() < incEndTimeSec
&& subTT.getMaxTimeSecs() > incStartTimeSec)
break;
// if we are completely past the time, no need to
// check any further items
else if (subTT.getMinTimeSecs() >= incEndTimeSec) {
subTT = null;
break;
} else
// we continue to check. We set subTT to null so
// that if we exhaust the children,
// we will know that we didn't find any
subTT = null;
}
// if we overlap
if (subTT != null)
timeTreeParentPath.add(subTT.id);
// else we must go back up
else {
// pop the tt off the stack so we can check out its
// siblings
lastSiblingTtFk = timeTreeParentPath
.remove(timeTreeParentPath.size() - 1);
}
}// else not at the bottom for tt processing
}// if we are doing tt processing
} finally {
GTG.cacheCreatorLock.unregisterReadingThread();
}
}
}
// these are scrap time ranges that can be used whenever SAS is synchronized
// (and only then)
private AreaTimeRange t1TimeRange = new AreaTimeRange();
private AreaTimeRange t2TimeRange = new AreaTimeRange();
/**
* An ever increasing counter that is updated every time a new result is calculated
*/
public int resultId = 1;
public boolean isResultTimeTree;
/**
*
* @param tt
* @param cutStartTimeSec must be within the stb or bugs will occur
* @param cutEndTimeSec same as above
*/
private void addTimeRange(TimeTree tt, int cutStartTimeSec, int cutEndTimeSec) {
int fullRangeMinTimeSecs = tt.getMinTimeSecs();
int fullRangeMaxTimeSecs = tt.getMaxTimeSecs();
t1TimeRange.startTimeSec = fullRangeMinTimeSecs;
t2TimeRange.startTimeSec = Integer.MAX_VALUE;
// look for overlaps
//find the tr to start with, that would be within
// the time range being added
AreaTimeRange tr = resultTimeRangeTree.floor(t1TimeRange);
if (tr != null)
{
if(tr.fullRangeEndSec <= fullRangeMinTimeSecs)
tr = resultTimeRangeTree.higher(tr);
else
// if the previous time range encompasses the range we are going to
// add
if (tr.fullRangeEndSec > fullRangeMaxTimeSecs)
return;
}
else if (!resultTimeRangeTree.isEmpty())
tr = resultTimeRangeTree.first();
AreaTimeRange ltr = null;
//while still within the new area to be added
while(tr != null && tr.fullRangeStartSec < fullRangeMaxTimeSecs)
{
if(tr.fullRangeStartSec < fullRangeMinTimeSecs)
fullRangeMinTimeSecs = tr.fullRangeStartSec;
if(tr.fullRangeEndSec > fullRangeMaxTimeSecs)
fullRangeMaxTimeSecs = tr.fullRangeEndSec;
if(tr.startTimeSec < cutStartTimeSec)
cutStartTimeSec = tr.startTimeSec;
if(tr.endTimeSec > cutEndTimeSec)
cutEndTimeSec = tr.endTimeSec;
ltr = tr;
tr = resultTimeRangeTree.higher(tr);
resultTimeRangeTree.remove(ltr);
}
//resuse the last tr that we removed if possible
if(ltr == null)
ltr = new AreaTimeRange(fullRangeMinTimeSecs,
fullRangeMaxTimeSecs, cutStartTimeSec,
cutEndTimeSec);
else
{
ltr.fullRangeStartSec = fullRangeMinTimeSecs;
ltr.fullRangeEndSec = fullRangeMaxTimeSecs;
ltr.startTimeSec = cutStartTimeSec;
ltr.endTimeSec = cutEndTimeSec;
}
resultTimeRangeTree.add(ltr);
}
private boolean runBackgroundTaskForTimeRange() {
if (resultAreas.isEmpty()
|| resultAreas.get(0) != requestedAreas.get(0)
|| resultIncomplete) {
resultTimeRangeTree = new TreeSet<AreaTimeRange>();
updateTimeRangeTree(requestedStartTimeSec, requestedEndTimeSec);
if (workerInterrupted) {
//note that we don't set upToDate to be true
workerInterrupted = false;
return false; //if we we're up to date before and we're not null
//it doesn't really affect the viewer
}
copyRequestedInputToResultInput(true);
//note that we can't synchronize here because this could cause an rwtm to upToDateLock deadlock
upToDate = true;
return true;
}
if (!resultTimeRangeTree.isEmpty()) {
//we need to clear out time ranges outside of the range. Technically we
//could keep them as a cache, but it makes listing them out more complex...
//"How many time ranges in the list?" would be hard to determine for example.
//We do keep long time ranges alone if they extend beyond the start and end
//of the requested time. However, they at least need to overlap the requested
// start and end time somewhat to survive this.
//check if all items should be removed
if (resultTimeRangeTree.first().fullRangeStartSec >= requestedEndTimeSec
|| resultTimeRangeTree.last().fullRangeEndSec <= requestedStartTimeSec) {
resultTimeRangeTree = new TreeSet<AreaTimeRange>();
updateTimeRangeTree(requestedStartTimeSec, requestedEndTimeSec);
if (workerInterrupted) {
//note that we don't set upToDate to be true
workerInterrupted = false;
return false; //if we we're up to date before and we're not null
//it doesn't really affect the viewer
}
} else {
TreeSet<AreaTimeRange> newResultTimeRangeTree = new TreeSet<AreaTimeRange>();
t1TimeRange.startTimeSec = requestedStartTimeSec;
t2TimeRange.startTimeSec = requestedEndTimeSec;
newResultTimeRangeTree.addAll(resultTimeRangeTree.subSet(
t1TimeRange, t2TimeRange));
AreaTimeRange leftEdge = resultTimeRangeTree.lower(t1TimeRange);
if (leftEdge != null
&& leftEdge.endTimeSec > requestedStartTimeSec)
newResultTimeRangeTree.add(leftEdge);
resultTimeRangeTree = newResultTimeRangeTree;
}
} else
//this is to make sure that any old references floating out there won't
//be changed by this new calculation (there currently aren't any, but you know...)
resultTimeRangeTree = new TreeSet<AreaTimeRange>();
if (requestedStartTimeSec < resultStartTime) {
updateTimeRangeTree(requestedStartTimeSec, resultStartTime);
if (workerInterrupted) {
//note that we don't set upToDate to be true
workerInterrupted = false;
return false; //if we we're up to date before and we're not null
//it doesn't really affect the viewer
}
}
if (requestedEndTimeSec > resultEndTime)
updateTimeRangeTree(resultEndTime, requestedEndTimeSec);
copyRequestedInputToResultInput(true);
upToDate = true;
return true;
}
private void copyRequestedInputToResultInput(boolean isTimeTree) {
lastTimeRange = null;
resultAreas.clear();
resultAreas.addAll(requestedAreas);
resultStartTime = requestedStartTimeSec;
resultEndTime = requestedEndTimeSec;
if (isTimeTree) {
isResultTimeTree = true;
resultPaths = null;
} else {
isResultTimeTree = false;
resultTimeRangeTree = null;
}
}
/**
* If upToDate is true, and the result is paths,
* results the resultPaths, otherwise null. The
* caller can assume the resultPaths will never
* be modified by the sas thread
*
* @return
*/
public ArrayList<Path> getResultPaths() {
synchronized (upToDateLock) {
if (upToDate && !isResultTimeTree)
return resultPaths;
return null;
}
}
public int getTotalTimeSecs() {
rwtm.registerReadingThread();
int totalTime = 0;
for(TimeRange tr : resultTimeRangeTree)
{
int startTimeSec = Math.max(tr.startTimeSec, requestedStartTimeSec);
int endTimeSec = Math.min(tr.endTimeSec, requestedEndTimeSec);
if(startTimeSec > endTimeSec)
throw new CacheException("Why tr out of range? "+this+" tr: "+tr);
totalTime += endTimeSec - startTimeSec;
}
rwtm.unregisterReadingThread();
return totalTime;
}
public double getTotalDistM() {
rwtm.registerReadingThread();
double res = 0;
for(TimeRange tr : resultTimeRangeTree)
res += tr.dist;
rwtm.unregisterReadingThread();
return res;
}
public int getTimesInArea() {
rwtm.registerReadingThread();
try {
return resultTimeRangeTree.size();
}
finally {
rwtm.unregisterReadingThread();
}
}
@Override
public String toString() {
return "SelectedAreaSet [isRunning=" + isRunning + ", isShutdown="
+ isShutdown + ", rwtm=" + rwtm + ", activity=" + activity
+ ", requestedAreas=" + requestedAreas
+ ", requestedStartTimeSec=" + requestedStartTimeSec
+ ", requestedEndTimeSec=" + requestedEndTimeSec
+ ", resultPaths=" + resultPaths + ", resultTimeRangeTree="
+ resultTimeRangeTree + ", resultAreas=" + resultAreas
+ ", resultEndTime=" + resultEndTime + ", resultStartTime="
+ resultStartTime + ", upToDate=" + upToDate
+ ", distCalculated=" + distCalculated + ", timeZone="
+ timeZone + ", p1=" + p1 + ", p2=" + p2 + ", lastTimeRange="
+ lastTimeRange + ", lastIndex=" + lastIndex + ", observers="
+ observers + ", getTimeRangeTr=" + getTimeRangeTr
+ ", workerInterrupted=" + workerInterrupted
+ ", resultIncomplete=" + resultIncomplete + ", upToDateLock="
+ upToDateLock + ", t1TimeRange=" + t1TimeRange
+ ", t2TimeRange=" + t2TimeRange + ", resultId=" + resultId
+ ", isResultTimeTree=" + isResultTimeTree + "]";
}
public List<Area> getRequestedAreas() {
return requestedAreas;
}
}