/** 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.cachecreator; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import android.database.Cursor; import android.util.Log; import com.rareventure.android.AndroidPreferenceSet.AndroidPreferences; import com.rareventure.android.DbUtil; import com.rareventure.android.Util; import com.rareventure.gps2.CacheException; import com.rareventure.gps2.GTG; import com.rareventure.gps2.GTG.GTGEvent; import com.rareventure.gps2.GpsTrailerCrypt; import com.rareventure.gps2.database.GpsLocationRow; import com.rareventure.gps2.database.TAssert; 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.ViewNode.VNStatus; import com.rareventure.gps2.reviewer.map.GpsTrailerOverlay; import com.rareventure.gps2.reviewer.map.OsmMapGpsTrailerReviewerMapActivity; import com.rareventure.gps2.reviewer.map.ViewLine; import com.rareventure.util.ReadWriteThreadManager; //TODO 3: 3d view with time being z (or id) /** * Creates the cache which is used to display the ui map in GpsTrailerOverlayDrawer. * Note, this is a thread because it takes a long time to commit, and we really can't * block the drawer thread while this is going on. Even though the user shouldn't be too * far out of sync, it can take quite awhile even to process a few days worth of points, * so we need this. * * It uses GTG.rwtm for locking purposes. Anytime a thread wants to read from * apCache or viewNode it must lock itself through GTG.rwtm. In addition, when * a thread is waiting on the lock, this thread will quit processing as soon as possible */ public class GpsTrailerCacheCreator extends Thread { private static final int SOFT_COMMITS_PER_ROUND = 25; /** * Number of gps points to process before giving a progress update */ private static final int NUM_GPS_PROCESSING_PER_PROGRESS_UPDATE = 250; /** * bit flag for calcViewNodes. Indicates all the nodes haven't finished * being calculated */ public static final int CALC_VIEW_NODES_STILL_MORE_NODES_TO_CALC = 1; /** * bit flag for calcViewNodes. Indicates some view nodes have been altered * in a way for them to require lines to/from them to be recalculated */ public static final int CALC_VIEW_NODES_LINES_NEED_RECALC = 2; /** * Amount of distance in meters used as a constant to determine how much of * the average vs the current point to use */ private static final int GPS_JITTER_METERS = 99; private static final double AVERAGE_DECAY_PERC = 0.1; public static Preferences prefs = new Preferences(); private ViewNode headVn; //note that we don't use a SparseIntArray because it is inefficient when adding or deleting, // in fact I don't see why its so hyped at all?? //not that hashmap is that great either, but if we really want to improve efficiency we should //make an integer hashmap //note that we put these in cache creator because headVn is in cache creator. If we put these // in classes related to the main screen, and the user leaves and reenters the screen, //then the view nodes would still be present, but the view lines would not //TODO 2.5 possibly fix the above??? public HashMap<Integer,ViewLine> startTimeToViewLine = new HashMap<Integer,ViewLine>(); public HashMap<Integer,ViewLine> endTimeToViewLine = new HashMap<Integer,ViewLine>(); public ReadWriteThreadManager viewNodeThreadManager = new ReadWriteThreadManager(); //we use the drawer of this to alert the map view that its viewnodes have changed //after we load some stuff in our cache //whether the medialoctimemap is up to date with the gallery private boolean mediaDirty = true; public int minTimeSec; public int maxTimeSec; public OsmMapGpsTrailerReviewerMapActivity gtum; public boolean isShutdown; public GpsTrailerCacheCreator() { // Log.d(GTG.TAG, "GpsTrailerCacheCreator: created! "+this); headVn = ViewNode.createHeadViewNode(); //we never really die, just pause or unpause. setDaemon(true); if(GTG.apCache.hasGpsPoints()) { AreaPanel ap = GTG.apCache.getTopRow(); minTimeSec = ap.getTimeTree().getMinTimeSecs(); maxTimeSec = ap.getTimeTree().getMaxTimeSecs(); } else { minTimeSec = maxTimeSec = (int) (System.currentTimeMillis()/1000); } if(!GTG.apCache.isDbFilled()) { // create top area panel AreaPanel topAreaPanel = GTG.apCache.newRow(); topAreaPanel.setData(0, 0, AreaPanel.DEPTH_TO_WIDTH.length - 1); try { GTG.timmyDb.beginTransaction(); GTG.apCache.writeDirtyRows(); GTG.timmyDb.setTransactionSuccessful(); GTG.timmyDb.endTransaction(); //we clear the dirty rows only after the transaction is committed so we //don't attempt to read an updated or inserted row while the commit is //being done, which is unsupported GTG.apCache.clearDirtyRows(); } catch(IOException e) { throw new IllegalStateException(e); } } } public void setGtum(OsmMapGpsTrailerReviewerMapActivity gtum) { this.gtum = gtum; } public static double calcTrDist(int startTime, int endTime, ReadWriteThreadManager writingRwtm) { GTG.cacheCreatorLock.registerReadingThread(); try { //now we need to find the biggest ap with a start time equal to the best start time, // and an end time that is less or equal to the minimum of the tr end time and the // stb end time, unless the lowest level ap's time tree extends beyond the end time, // in which case we use that // (which indicates that its dist is the distance of the travel to the ap plus the distance // within the time range and the ap itself and also within the stb.... because time ranges // can extend beyond the ends of the stb... However, at the lowest level, the time tree might // extend beyond the stb edge (because tt's overlap to the next ap), and if so, we must use // that, since the distance of that tt is always dist from prev point to current ap, so it // would be correct to use that. //the tt of the largest ap with a start time that is closest to startTime without exceeding it. //note that at there should be many ap's with the same start time that encapsulate each other. // We want the largest one so we can advance the time as fast as possible //note also that this won't always be start time in the case that a time range is cut off by the start of the stb int bestStartTime = Integer.MIN_VALUE; TimeTree bestTtInPath = null; AreaPanel bestApInPath = null; double dist = 0; AreaPanel ap = GTG.apCache.getTopRow(); if(ap == null || ap.getTimeTree() == null) return 0; //we limit the range to the area actually calculated so that we have conitinous //coverage over all times (to avoid triggering some sanity checks) startTime = Math.max(ap.getStartTimeSec(), startTime); endTime = Math.min(ap.getEndTimeSec(), endTime); if(startTime >= endTime) return 0; //the ap used in the last time range calculation //we use this so that we don't need to go down all the way to zero //depth to determine the biggest ap that is entered at the start time //(if the ap doesn't overlap the lastTimeAp, then we know it will be the // ap the current start time first entered) AreaPanel lastTimeAp = null; for (;;) { int i; //note, we need the following because when we are determining the initial //area panel in the time range, we may hit either the prior ap or the next //ap (since the time ranges overlap). We are only interested in the next //ap after the startTime, so we traverse all of them. TimeTree bestCurrLevelChildTt = null; int bestCurrLevelStartTime = Integer.MIN_VALUE; AreaPanel bestCurrLevelChildAp = null; //find the child that has a bottom level tt that encompasses the startTime (there should be only one) for (i = AreaPanel.NUM_SUB_PANELS-1; i >= 0; i--) { AreaPanel childAp = ap.getSubAreaPanel(i); if (childAp == null) continue; if(writingRwtm != null) { writingRwtm.pauseForReadingThreads(); if (writingRwtm.isWritingHoldingUpWritingThreads()) { return -1; } } TimeTree childTt = childAp.getTimeTree() .getBottomLevelEncompassigTimeTree(startTime); if(childTt != null && childTt.getMinTimeSecs() >= bestStartTime && childTt.getMinTimeSecs() > bestCurrLevelStartTime) { bestCurrLevelChildAp = childAp; bestCurrLevelChildTt = childTt; bestCurrLevelStartTime = childTt.getMinTimeSecs(); //if we know the ap just prior to the start time, then //we can be sure we have hit the next tt rather than the prior one if(lastTimeAp != null && !childAp.overlaps(lastTimeAp)) break; } } //note that there should always be an encompassing child because startTime is taken from a time range //which means that we have already determined that this time is within some ap somewhere if (bestCurrLevelChildTt == null) throw new CacheException( "Couldn't find a child ap encompassing time " + startTime); //note, if at the lowest level, the distance won't be anything greater than the distance from the //prev ap to the current ap, so we don't have to worry about end time at that point //(note that the reason we care about end time is we don't want an ap that extends beyond // the requested end time, since that would have a distance that includes a time that is after // the requested time. if (bestCurrLevelChildTt != null && bestCurrLevelChildTt.getMinTimeSecs() >= bestStartTime) { //notice we are only including child tt's that have a later start time and still //are valid, > vs the >= in the above if if (bestCurrLevelChildTt.getMinTimeSecs() > bestStartTime && (bestCurrLevelChildTt.getMaxTimeSecs() <= endTime || bestCurrLevelChildAp .getDepth() == 0)) { bestStartTime = bestCurrLevelChildTt.getMinTimeSecs(); bestTtInPath = bestCurrLevelChildTt; bestApInPath = bestCurrLevelChildAp; } //we need to go to depth zero on the first try, because we need to subtract the distance between //the prior ap and the current one. After that, we add the distances between the ap's together //so we only need to find an ap that exists outside of the last one we visited if(bestCurrLevelChildAp.getDepth() == 0 || bestCurrLevelChildAp == bestApInPath && lastTimeAp != null && !lastTimeAp.overlaps(bestCurrLevelChildAp)) { dist += bestTtInPath.getPrevAndCurrDistM(); if(lastTimeAp == null) { //considering that the best ap may not be zero level and getPrevAndCurrDistM returns //the distance within the area panel added to the distance from the prior ap //to the current ap, we need to remove this first part. //in order to do so, we find the bottom level ap, whos getPrevAndCurrDistM will //return just the distance from the prior ap to the current ap and is contained //within the ap dist -= bestCurrLevelChildTt.getPrevAndCurrDistM(); } //if we've reached the end of the tr if (bestTtInPath.getMaxTimeSecs() >= endTime) return dist; //we're finished, so return the distance lastTimeAp = bestApInPath; ap = GTG.apCache.getTopRow(); startTime = bestTtInPath.getMaxTimeSecs(); //note, we need to reset bestStartTime even though we are on to //the next start time which is after the current area panel. //This is because a large ap that encompasses both the previous //ap and the next one will have an encompassing tt with a start //time that may be less then the prior ap bestStartTime = Integer.MIN_VALUE; bestTtInPath = null; bestApInPath = null; } else //otherwise, we need to go to the child that encompasses the start time, recursively, //until we find one that has an encompassing time range that doesn't extend beyond the //end time { ap = bestCurrLevelChildAp; } } } // end for(;;) while we haven't reached end time } finally { GTG.cacheCreatorLock.unregisterReadingThread(); } } public void run() { try { long nextTimeToLoadPointsMs = 0; long nextTimeToLoadMediaMs = 0; for(;;) { if(isShutdown) break; //make sure we aren't running out of room if(!GTG.checkSdCard(gtum)) { return; } long currTime = System.currentTimeMillis(); //look for new gps points to be cached if(nextTimeToLoadPointsMs < currTime ) { GTG.alert(GTGEvent.LOADING_MEDIA,false); if(loadNextPoints()) nextTimeToLoadPointsMs = System.currentTimeMillis() + prefs.areaPanelUpdateGpsLocSpinLockMs; } //update the media (images and videos) r-tree based on the media // urls if(this.mediaDirty && gtum != null) { GTG.alert(GTGEvent.PROCESSING_GPS_POINTS,false); //if we finished updating all the images if(GTG.mediaLocTimeMap.updateFromGallery(this, gtum.getContentResolver())) this.mediaDirty = false; } //there are two things to wait for. First, for gps service to collect more points, // and second, for the drawer thread to finish calculating its view nodes so they are // no longer dirty (to make it easy we don't update view nodes until they are all clean) //TODO 3: we should use a push mechanism for gps updates. //TODO 3: we are using the fact that this always runs to clear out deleted mlts // periodically. we should probably have a notification mechanism GTG.mediaLocTimeMap.deleteMarkedMltsFromDb(); //we wait until we think there may be more points if we're fully updated, or // for a short while if we were not fully finished since last time long waitTime = ((mediaDirty && gtum != null) ? 0 : nextTimeToLoadPointsMs - System.currentTimeMillis()); // Log.d(GTG.TAG, "Sleeping for "+waitTime); if(isShutdown) break; synchronized(this) { if(waitTime > 0) { GTG.alert(GTGEvent.LOADING_MEDIA,false); GTG.alert(GTGEvent.PROCESSING_GPS_POINTS,false); //wait for more gps data. the gps service will notify us if it processes //a point try { wait(waitTime); } catch (InterruptedException e) { // we should never be interrupted throw new IllegalStateException(e); } } } } } catch(RuntimeException e) { Util.printAllStackTraces(); throw e; } catch(Error e) { Util.printAllStackTraces(); throw e; } } // public static void createFakeDataHack(int hackStop) { // int lastGpsLocId = prefs.lastPointCachedId; // // if(lastGpsLocId >= hackStop) // { // Log.e("HACK","Already reached hackStop, "+hackStop); // return; // } // // if(!GTG.USE_TEST_DB) // throw new // IllegalStateException("you don't want to use the real database, I think"); // // int startId = 9604; // int repeats = 300; // // Random r = new Random(1000); // // SQLiteDatabase db = GTG.db; // // //first we load the data in memory. // ArrayList<GpsLocationRow> rows = new ArrayList<GpsLocationRow>(); // // GpsLocationRow currGpsLocRow = GpsTrailerCrypt.allocateGpsLocationRow(); // // SQLiteCursor c = (SQLiteCursor) currGpsLocRow.query(db, // GpsLocationRow.TABLE_NAME, "_id >= ?", "_id", // new String [] { String.valueOf(startId) }); // // //CTODO 2: keeps drawing overlay over and over when the bubble is up in a // busy loop // //try for closing the cursor in a finally // try { // // //while processing this query // while(c.moveToNext()) // { // currGpsLocRow = GpsTrailerCrypt.allocateGpsLocationRow(); // currGpsLocRow.readRow(c); // rows.add(currGpsLocRow); // } // } // finally { // DbUtil.closeCursors(c); // } // // Log.i(GpsTrailer.TAG, // "creating cache HACK for "+startId+" to "+currGpsLocRow.id+" repeats "+repeats); // // GpsLocationRow lastGpsLocRow = GpsTrailerCrypt.allocateGpsLocationRow(); // currGpsLocRow = GpsTrailerCrypt.allocateGpsLocationRow(); // // long time = 0; // // boolean firstTime = true; // int lastIndex = 0; // // int currGpsLocId = rows.get(rows.size()-1).id; // // while(repeats-- >= 0) // { // Log.i(GpsTrailer.TAG, "repeats "+repeats); // // db.beginTransaction(); // // try { // Log.i(GpsTrailer.TAG, "fake id is "+currGpsLocId); // // int i = rows.size()-1; // boolean forwards = false; // // while(i < rows.size()) // { // if(!firstTime) // { // lastGpsLocRow.copyRow2(currGpsLocRow); // lastGpsLocRow.id = currGpsLocRow.id; // currGpsLocRow.copyRow2(rows.get(i)); // currGpsLocRow.id = rows.get(i).id; // time = currGpsLocRow.hackRandomize(r, time, // time + Math.abs(currGpsLocRow.getTime() - rows.get(lastIndex).getTime()), // 120 * 60 * 1000, 500, 3, 3); // // currGpsLocRow.id = currGpsLocId; // // if(currGpsLocId % 50 == 0) // { // GTG.turnOnMethodTracing = true; // } // // if(currGpsLocId <= lastGpsLocId) // Log.d("GTG","Skipping "+currGpsLocId); // else // processGpsPoint(lastGpsLocRow, currGpsLocRow); // // if(currGpsLocId % 50 == 0) // { // GTG.turnOnMethodTracing = false; // } // } // else // { // currGpsLocRow.copyRow2(rows.get(i)); // currGpsLocRow.id = rows.get(i).id; // time = rows.get(i).getTime(); // firstTime = false; // } // // lastIndex = i; // i += forwards ? 1 : -1; // // if(i < 0) // { // i = 0; // forwards = true; // } // // if(currGpsLocId >= GTG.HACK_STOP) // break; // currGpsLocId++; // // } // while going through the rows in both directions // // prefs.lastPointCachedId = currGpsLocRow.id; // // //save the position we've gotten to in androidprefs // GTG.prefSet.saveIndividualPrefToDatabase( db, prefs, // "lastPointCachedId"); // // GTG.apCache.commitDirtyRows(); // db.setTransactionSuccessful(); // } // finally // { // db.endTransaction(); // } // Log.i(GpsTrailer.TAG, "finished!!!!!!!!!!!!!!!!!!!!!!!!"); // } // // } public static void clearOutDbHack(boolean force) { if (GTG.CLEAR_OUT || force) { try { GTG.timmyDb.close(); GTG.timmyDb.deleteDatabase(); //TODO 3: WARNING!!!!!!! not sure if open works right after a close and delete GTG.timmyDb.open(); } catch(IOException e) { throw new IllegalStateException(e); } Log.i("GTG", "Done clearing database"); } } public synchronized void notifyMediaDirty() { /* ttt_installer:remove_line */Log.d(GTG.TAG,"notifying media dirty"); mediaDirty = true; notify(); } /** * * @return true if all points have been calculated for the current set of gps points */ public boolean loadNextPoints() { if(GTG.timmyDb.getProperty("lastPointReadId") == null) { try { GTG.timmyDb.beginTransaction(); GTG.timmyDb.setProperty("lastPointReadId","-1"); GTG.timmyDb.setProperty("lastPointCachedId","-1"); GTG.timmyDb.setProperty("lastAdjLonm","0"); GTG.timmyDb.setProperty("lastAdjLatm","0"); GTG.timmyDb.setProperty("avgLonm",Util.doubleToHex(0d)); GTG.timmyDb.setProperty("avgLatm",Util.doubleToHex(0d)); GTG.timmyDb.saveProperties(); GTG.timmyDb.setTransactionSuccessful(); GTG.timmyDb.endTransaction(); } catch(IOException e) { throw new IllegalStateException(e); } } /* * The last gps row we read from the database for caching purposes, * regardless if it was successful or not. * * Note, the reason for this is because if we try processing a whole * block of points and for some reason they all failed, we would not * want to start back at the last successful one, because then we'd get * into an infinite loop. */ int origLastPointReadId, lastPointReadId; origLastPointReadId = lastPointReadId = Integer.parseInt(GTG.timmyDb.getProperty("lastPointReadId")); /* * The last point gps row we successfully processed */ int origLastPointCachedId, lastPointCachedId; origLastPointCachedId = lastPointCachedId = Integer.parseInt(GTG.timmyDb.getProperty("lastPointCachedId")); int lastAdjLatm = Integer.parseInt(GTG.timmyDb.getProperty("lastAdjLatm")); int lastAdjLonm = Integer.parseInt(GTG.timmyDb.getProperty("lastAdjLonm")); //note these have to be hex rather than just the printed representation because // some floating point numbers cannot be represented exactly accurately using the printed notation double avgLatm = Util.hexToDouble(GTG.timmyDb.getProperty("avgLatm")); double avgLonm = Util.hexToDouble(GTG.timmyDb.getProperty("avgLonm")); //true if this is the very first point were processing //TODO 2.5 hack, we should probably not have a top row at all if there are no points boolean isVeryFirst = GTG.apCache.getTopRow() == null || GTG.apCache.getTopRow().getTimeTree() == null; int count = 0; //note that we set the limit so that we will read past all the points that we failed on //plus the cache loading step. This way we guarantee we will make progress and not //get stuck in an infinite loop ... we could skip the ones we tried to read before and //failed, but that would be kind of complicated //note, we multiply the difference times three over two, so for long stretches of corrupted points, //we will eventually get past them int limit = prefs.gpsCacheLoadingStep; GpsLocationRow currGpsLocRow = GpsTrailerCrypt.allocateGpsLocationRow(); GpsLocationRow lastGpsLocRow = null; Cursor c = null; boolean atLeastOneToProcess = false; long [] currDateMsToLatestDateMs = new long[2]; // this try is to close the transaction with a finally //and for removing the rwtm locks GTG.cacheCreatorLock.registerWritingThread(); try { /* ttt_installer:remove_line */Log.d(GTG.TAG,"Started apCaching..."); for(int softCommitCounter = 0; softCommitCounter < SOFT_COMMITS_PER_ROUND; softCommitCounter++) { if(isShutdown) break; //for the first time, we need to load the previous item because we don't have one if(lastGpsLocRow == null) c = GTG.gpsLocDbAccessor.query( "_id = ? or _id > ?", "_id limit " + limit, new String[] { String.valueOf(lastPointCachedId), String.valueOf(lastPointReadId) }); else c = GTG.gpsLocDbAccessor.query( "_id > ?", "_id limit " + limit, new String[] { String.valueOf(lastPointReadId) }); // Log.d(GTG.TAG, "Getting next batch of rows, lastPointReadId="+lastPointReadId // +", lastPointCachedId="+lastPointCachedId+" total limit="+limit+ // ", lastAdjLonm="+lastAdjLonm+", lastAdjLatm="+lastAdjLatm // +", avgLonm="+avgLonm+", avgLatm="+avgLatm); int currGpsLocId = lastPointReadId;; if(c.isAfterLast()) { break; //break out of the soft commit loop } // while processing this query while (c.moveToNext() && !GTG.HACK_TURN_OFF_APCACHE_LOADING) { currGpsLocId = c.getInt(0); try { GTG.gpsLocDbAccessor.readRow(currGpsLocRow, c); } catch (Exception e) { // sometimes the encryption fails to work (not often) // TODO 3: figure out how to fail gracefully in these // situations Log.e("GTG", "Error reading row " + c.getInt(0) + ", skipping", e); continue; } if (lastGpsLocRow == null) { lastGpsLocRow = currGpsLocRow; currGpsLocRow = GpsTrailerCrypt.allocateGpsLocationRow(); //if this is the very first processable row if(lastAdjLatm == 0 && lastAdjLonm == 0) { lastAdjLonm = lastGpsLocRow.getLonm(); lastAdjLatm = lastGpsLocRow.getLatm(); avgLonm = lastAdjLonm; avgLatm = lastAdjLatm; } continue; } currDateMsToLatestDateMs[0] = currGpsLocRow.getTime(); if(count % NUM_GPS_PROCESSING_PER_PROGRESS_UPDATE == 0) GTG.alert(GTGEvent.PROCESSING_GPS_POINTS, true, currDateMsToLatestDateMs); //if there are threads waiting to run, now is a good time to do it, since //we're at a discrete point of creating the apcaches // //Note that the drawer thread won't give up its reading lock until the view //is completely up to date, so we don't have to worry about the viewnodes //being half updated GTG.cacheCreatorLock.pauseForReadingThreads(); //note, we can use the headVn stbox because all vn's should be clean by this point //this is used strictly to populate the viewnode tree int minDepth = headVn.stBox == null ? 0 : GpsTrailerOverlay.getMinDepth(headVn.stBox); int latm = currGpsLocRow.getLatm(); int lonm = currGpsLocRow.getLonm(); double distFromAvg = Util.calcDistFromLonmLatm(lonm, latm, avgLonm, avgLatm); double k = GPS_JITTER_METERS / (Math.abs(distFromAvg + GPS_JITTER_METERS) + distFromAvg); int adjLonm = (int) (lonm*(1-k) + avgLonm * k); int adjLatm = (int) (latm*(1-k) + avgLatm * k); // //find out if we are too far away from the current location // double distToLpf = Util.calcDistFromLonmLatm(currGpsLocRow.getLonm(), currGpsLocRow.getLatm(), // lonm, latm); // // if(distToLpf > MAX_LOW_PASS_FILTER_AFFECT_METERS) // { // //adjust to the current location // lonm = (int) (currGpsLocRow.getLonm() + (currGpsLocRow.getLonm() - lonm) * MAX_LOW_PASS_FILTER_AFFECT_METERS / distToLpf); // latm = (int) (currGpsLocRow.getLatm() + (currGpsLocRow.getLatm() - latm) * MAX_LOW_PASS_FILTER_AFFECT_METERS / distToLpf); // } double dist = Util.calcDistFromLonmLatm(lastAdjLonm, lastAdjLatm, adjLonm, adjLatm); //xODO 2 HACK... note this messes up GpsTrailerService, since it affects the cache... caused a unique key exception, not sure why // currGpsLocRow.setData(currGpsLocRow.getTime(),(int) lpfLatm, (int)lpfLonm, currGpsLocRow.getAltitude()); boolean wasSuccessful = processGpsPoint(dist, lastAdjLonm, lastAdjLatm, lastGpsLocRow, adjLonm, adjLatm, currGpsLocRow, minDepth, isVeryFirst); isVeryFirst = false; // Log.d(GpsTrailer.TAG, "apcache misses: " + GTG.apCache.misses // + ", hits: " + GTG.apCache.hits + ", dirtyRows " + GTG.apCache.getDirtyRowCount()); // // Log.d(GpsTrailer.TAG, "Gps loc " + currGpsLocRow+" adjLonm= "+adjLonm+" adjLatm= "+adjLatm+" lastAdjLonm= "+lastAdjLonm+" lastAdjLatm= "+lastAdjLatm // + " added " + count + " successful? "+wasSuccessful); if (wasSuccessful) { atLeastOneToProcess = true; GpsLocationRow temp = lastGpsLocRow; lastGpsLocRow = currGpsLocRow; currGpsLocRow = temp; count++; lastAdjLonm = adjLonm; lastAdjLatm = adjLatm; //note that we use the adjusted values here, which are partially based on the average //This creates a feedback loop so that if the phone is stationary, the noise will affect // it less and less //note also we use "k" which is shared by our calculation for adjustment. This makes the average // move more or less based on whether the movement was big enough //note also that we don't move the avg if we did not save the gps point. This is because we aren't moving // the lastGpsLocRow either, which would change where we started next time in the condition that we //moved through the entire cursor *this* time. If we did change the averge for every reading, we'd //then be changing it again for the same points when we restarted, which would mean that we wouldn't //have consistent results depending on where we stopped in the data. avgLonm = k * avgLonm + (1-k) * adjLonm; avgLatm = k * avgLatm + (1-k) * adjLatm; } // else // Log.d(GpsTrailer.TAG, // "Not switching last and current because we failed to process"); if (lastGpsLocRow.id >= GTG.HACK_FAIL_STOP) TAssert.fail("failing cause you want me to, you know you do"); } // while we are moving through the cursor //we save the last *successful* gps row. This is because we need to know // the time between the last and the current row if(lastGpsLocRow != null) lastPointCachedId = lastGpsLocRow.id; lastPointReadId = currGpsLocId; if(count > 0) { minTimeSec = GTG.apCache.getTopRow().getStartTimeSec(); maxTimeSec = GTG.apCache.getTopRow().getEndTimeSec(); final OsmMapGpsTrailerReviewerMapActivity localGtum = gtum; //notify the listeners (sort of) that the data has changed //note, localizing incase drawer is changed between check for null and call if(localGtum != null) { localGtum.runOnUiThread(new Runnable() { @Override public void run() { localGtum.notifyMaxTimeChanged(); } }); localGtum.gpsTrailerOverlay.notifyViewNodesChanged(); } } if(!GTG.timmyDb.inTransaction()) { GTG.timmyDb.beginTransaction(); } GTG.apCache.writeDirtyRows(GTG.cacheCreatorLock); GTG.ttCache.writeDirtyRows(GTG.cacheCreatorLock); GTG.apCache.clearDirtyRows(); GTG.ttCache.clearDirtyRows(); }// while doing soft commits } catch (IOException e) { Log.e(GTG.TAG,"Exception",e); throw new IllegalStateException(e); } finally { /* ttt_installer:remove_line */Log.d(GTG.TAG,"loadNextPoints, count is "+count); DbUtil.closeCursors(c); GTG.cacheCreatorLock.unregisterWritingThread(); //co: the flickering is very annoying caused by turning the // processing gps points message on and off, so we just leave it on unless we are really // going to sleep // GTG.alert(GTGEvent.PROCESSING_GPS_POINTS, false); } //note that we can commit to the db while reading from the cache without problems try{ if(origLastPointCachedId != lastPointCachedId || origLastPointReadId != lastPointReadId) { if(!GTG.timmyDb.inTransaction()) GTG.timmyDb.beginTransaction(); GTG.timmyDb.setProperty("lastPointCachedId", lastPointCachedId); GTG.timmyDb.setProperty("lastPointReadId", lastPointReadId); GTG.timmyDb.setProperty("lastAdjLatm", lastAdjLatm); GTG.timmyDb.setProperty("lastAdjLonm", lastAdjLonm); GTG.timmyDb.setProperty("avgLatm", Util.doubleToHex(avgLatm)); GTG.timmyDb.setProperty("avgLonm", Util.doubleToHex(avgLonm)); GTG.timmyDb.saveProperties(); GTG.timmyDb.setTransactionSuccessful(); GTG.timmyDb.endTransaction(GTG.cacheCreatorLock); } else if(GTG.timmyDb.inTransaction()) GTG.timmyDb.endTransaction(); } catch (IOException e) { /* ttt_installer:remove_line */Log.e(GTG.TAG,"Exception",e); throw new IllegalStateException(e); } finally { DbUtil.closeCursors(c); } //if we updated any gps locs at all, we may have affected the //location of some of the images and video if(count != 0) { // note that we don't use GTG.rwtm for this because mediaLocTimeMap // doesn't use GTG.rwtm at all GTG.mediaLocTimeMap.updateTempLocs(); } return !atLeastOneToProcess; } /** * Adds one gps point to the apcache and ttcache. * @param lpfLatm * @param lpfLonm * @param lastLpfLatm * @param lastLpfLonm */ private boolean processGpsPoint(double dist, int lastLpfLonm, int lastLpfLatm, GpsLocationRow lastGpsLocRow, int lpfLonm, int lpfLatm, GpsLocationRow currGpsLocRow, int minDepth, boolean isVeryFirst) { int lastTimeSec = (int) (lastGpsLocRow.getTime() / 1000); int timeSec = (int) (currGpsLocRow.getTime() / 1000); // AreaPanels only keep time to the second, therefore we compare seconds // to check for equal times // In addition, I believe that the way that line calculations work, each // areapanel must overlap its time with the next area panel. Because so // we need at least 2 seconds of separation between them. In addition, // strangely (TODO 3) the code is only extending the previous ap to timeSec -1, // rather than timeSec, and I'm not sure why, but I don't want to tweak // it now. So I'm making them have to be at least 3 seconds apart for this // TODO 3: do we want to change that we store time as ints in AreaPanel? if (lastTimeSec >= timeSec -2) { // Log.d(GpsTrailer.TAG, "Skipping gps loc row " + currGpsLocRow.id // + " since it has the same time as previous measurement"); return false; } if (currGpsLocRow.getTime() < lastGpsLocRow.getTime()) { // TODO 3: What do we really want to do for this? It can be tricky // if the time is set to something far in // the future.. will we have to delete points? Maybe check them for // sanity beforehand? But how can we do that // if it's an on the fly thing? // Log.d(GpsTrailer.TAG, // "Skipping gps loc row " // + currGpsLocRow.id // + " since it has a previous time than that of the last successful row"); return false; } // Log.d(GpsTrailer.TAG, "Processing gps loc row " + currGpsLocRow // + " last row time " + lastGpsLocRow.getTime()); // we load the data in chunks to save memory (also if loading more than // 4000 rows, an i/o exception is thrown by // sqlite) int lastX = AreaPanel.convertLonmToX(lastLpfLonm); int lastY = AreaPanel.convertLatmToY(lastLpfLatm); int x = AreaPanel.convertLonmToX(lpfLonm); int y = AreaPanel.convertLatmToY(lpfLatm); if (lastX == x && lastY == y) { // Log.d(GpsTrailer.TAG, "Skipping gps loc row " + currGpsLocRow.id // + " since it has the same location as previous measurement"); return false; } // Log.d(GpsTrailer.TAG, "processing row lastX "+lastX+" "+"lastY "+lastY+" x "+x+" y "+y); // this will handle processing the item GTG.apCache.getTopRow().addPoint(currGpsLocRow.id, isVeryFirst ? null : GTG.apCache.getTopRow(), lastX, lastY, x, y, lastTimeSec, timeSec, dist); viewNodeThreadManager.registerWritingThread(); //if head is unknown, we'll just let calcViewableNodes handle it if(headVn.status != null) //add to the view node, so we can update the current view headVn.addPointToHead(x, y, lastTimeSec, timeSec, minDepth); viewNodeThreadManager.unregisterWritingThread(); return true; } /** * Calculate viewable nodes for a given space time box * * @param newLocalStBox * @param minDepth * @param earliestOnScreenPoint * @param latestOnScreenPoint * @return */ public int calcViewableNodes(AreaPanelSpaceTimeBox newLocalStBox, int minDepth, int earliestOnScreenPoint, int latestOnScreenPoint) { // Log.d("GPS", "calcViewableNodes start"); viewNodeThreadManager.registerWritingThread(); try { if (newLocalStBox != headVn.stBox) { /* ttt_installer:remove_line */Log.d("GPS", "Turning on all dirty flags"); headVn.turnOnAllDirtyFlags(minDepth); } //if not dirty and kids aren't dirty if (headVn.dirtyDescendents == 0) { /* ttt_installer:remove_line */Log.d("GPS", "Finished because there are no more unkNodes in head"); return 0; } if(headVn.dirtyDescendents < 0) TAssert.fail("why is dirtyDescendents below zero? " +headVn); ViewNode vn = headVn; // path from head node to current node ArrayList<ViewNode> parentsAndCurrent = new ArrayList<ViewNode>( AreaPanel.DEPTH_TO_WIDTH.length); boolean linesRecalcNeeded = false; // find an unknown guy with the smallest density (to work on next), // unless the stBox has changed since the // last time the vn has been visited, in which case, we check it // immediately. // PERF: it doesnt make sense to do only one child if its siblings also // are viewable in the display // area, since we can't really display it without knowing whether its // siblings are set or not while (!vn.needsProcessing(newLocalStBox)) { parentsAndCurrent.add(vn); // note, we know the vn has children, because a leaf node which is // not unk, // would have a zero dirtyDescendents // wouldn't be // chosen (ie, below selfOrDescendentsNeedProcessing() would return // false // for one its parents) int minDensity = Integer.MAX_VALUE; ViewNode bestChild = null; if (vn.ap().getDepth() == minDepth) TAssert.fail("Why does node say its dirty but doesn't need processing and is " + " at the lowest level? " + vn+" min depth "+minDepth); // find child with fewest "on" dots with unknown descendents // remaining to process // (this allows us not work too hard on a big blob with a huge // number of points, // and concentrate on distinct features of small sets of points // first) for (ViewNode child : vn.children) { if(child == null) continue; // in order to display one child of a parent, all the other // children // of the same parent must not be unknown, so before we work on // any // of the children of the children of this node, we have to // process // all the children of the current parent if (child.needsProcessing(newLocalStBox)) { bestChild = child; break; } int childDensity; if (child.dirtyDescendents > 0 && (childDensity = child.getDensity(newLocalStBox)) < minDensity) { minDensity = childDensity; bestChild = child; } } // if we couldn't find any child to do work on if (bestChild == null) { TAssert.fail("why is parent dirty without a child: " + parentsAndCurrent+" children "+Arrays.toString(vn.children)); } vn = bestChild; } parentsAndCurrent.add(vn); // now that we know which one we're going to work on, process it // Log.d("GPS","Handling "+parentsAndCurrent); if (vn.stBox == newLocalStBox) // if the view hasn't changed at all TAssert.fail("Why are we trying to update a view node when the stBox hasn't changed???"); boolean onStatus; int [] overlappingTimeRange = null; AreaPanel ap = vn.ap();; //note that vn.stBox can be null for new empty databases if (vn.status == null || vn.stBox == null || vn.stBox.isPathsChanged(newLocalStBox)) { onStatus = ap.overlapsStbXY(newLocalStBox) && (overlappingTimeRange = vn.checkTimeInterval(newLocalStBox)) != null; } else // we have a previous calculation to start from { if (vn.status == ViewNode.VNStatus.SET) { if (ap.outsideOfXY(newLocalStBox)) { onStatus = false; } //if the time hasn't decreased, we know the point is still displayed. However, //if we are the tail or head point, the overlapping range might expand if // the time range has expanded in the appropriate direction, so we fall through // in that case else if (!vn.timeDecreasedMeaningfully(newLocalStBox, ap) && vn.overlappingRange[1] != latestOnScreenPoint && vn.overlappingRange[0] != earliestOnScreenPoint) { onStatus = true; overlappingTimeRange = vn.overlappingRange; } else { // check the time interval and check if it overlaps onStatus = (overlappingTimeRange = vn.checkTimeInterval(newLocalStBox)) != null; } } // vn.status is EMPTY else { if (ap.outsideOfXY(newLocalStBox)) onStatus = false; // if we would have turned on based on position last time, (but // were empty), then the // only way we can be on is if the time range increased // meaningfully relative to the areapanel else if(vn.stBox != null && !vn.outsideOfXY(vn.stBox, ap) && !(vn.timeIncreasedMeaningfully(newLocalStBox, ap))) onStatus = false; else { onStatus = (overlappingTimeRange = vn.checkTimeInterval(newLocalStBox)) != null; } } } if (onStatus) { vn.overlappingRange = overlappingTimeRange; boolean childrenDirty; //if we were not set, the point is mute, because there aren't any children //but there is no need to check as well if(vn.status != VNStatus.SET|| vn.stBox.isPathsChanged(newLocalStBox)) { childrenDirty = true; vn.clearLineCalcs(); linesRecalcNeeded = true; } else if(vn.timeChangedMeaningfullyWithRegardsToChildren(newLocalStBox)) { childrenDirty = true; vn.clearLineCalcs(); linesRecalcNeeded = true; } else if(vn.xyChangedMeaningfullyWithRegardsToChildren(newLocalStBox)) childrenDirty = true; else childrenDirty = false; vn.setSetStatus(parentsAndCurrent, newLocalStBox, childrenDirty, minDepth); } else vn.setEmptyStatus(parentsAndCurrent, newLocalStBox); //note, we don't have to worry about clearing the line views for nodes where the chidren are //being changed from unknown to set for it, because the timesToLines will be the same //and will overwrite the line views that share the same timesToLines // we did some work return CALC_VIEW_NODES_STILL_MORE_NODES_TO_CALC | (linesRecalcNeeded ? CALC_VIEW_NODES_LINES_NEED_RECALC : 0); } finally { viewNodeThreadManager.unregisterWritingThread(); } } public Iterator<ViewNode> getViewNodeIter() { return new Iterator<ViewNode>() { private ArrayList<ViewNode> path = new ArrayList<ViewNode>(); private ViewNode nextNode = doNext(); public boolean hasNext() { return nextNode != null; } public ViewNode next() { ViewNode localNextNode = nextNode; nextNode = doNext(); return localNextNode; } public ViewNode doNext() { ViewNode previousSibling = null; boolean currentViewNodeWasDisplayed; if (path.size() == 0) { if (//headVn.needsProcessing(stBox) || headVn.status != VNStatus.SET) return null; // start it off so we look at headVn's children currentViewNodeWasDisplayed = false; path.add(headVn); } else { // we displayed the last element of path already for the // previous call // and are now tasked to find the next one to display // (contrast this with moving up the path to the parent // nodes and // then drilling back downwards) currentViewNodeWasDisplayed = true; } // find the next node that has unknown children, which // represents the // best information we have so far (which we are continually // updating by reading the db) while (path.size() > 0) { ViewNode vn = path.get(path.size() - 1); if (currentViewNodeWasDisplayed) { // set the previous sibling, so we will display the next // one // next time and go downwards once again previousSibling = path.remove(path.size() - 1); currentViewNodeWasDisplayed = false; //co because if we only display points that are not dirty, we get random flickering // and large points (also, we commented out the needsProcessing below // } else if (!vn.needsProcessing(stBox) && vn.status == ViewNode.VNStatus.SET) { // // if we haven't explored vn's children yet // if (vn.hasUnknownChildren(stBox)) // return vn; } else { if(vn.children == null) return vn; // find the next "set" ViewNode int i = 0; // if we already returned an area panel from this group // of children // and we need to go to the next one if (previousSibling != null) { boolean foundSibling = false; // search for the one we previously returned for (i = 0; i < AreaPanel.NUM_SUB_PANELS; i++) { if (vn.children[i] == previousSibling) { i++; foundSibling = true; break; } } if (!foundSibling) TAssert.fail("couldn't find the previous sibling: " + i + ", " + vn + " " + previousSibling); } // find the next set child and continue for (; i < AreaPanel.NUM_SUB_PANELS; i++) { if (vn.children[i] != null //&& !vn.children[i].needsProcessing(stBox) && vn.children[i].status == ViewNode.VNStatus.SET) { vn = vn.children[i]; path.add(vn); break; } } // if there are no more children to go down to, we go up // and try the next one if (i == AreaPanel.NUM_SUB_PANELS) previousSibling = path.remove(path.size() - 1); else previousSibling = null; } } return null; } @Override public void remove() { throw new IllegalStateException("not implemented"); } }; } public static class Preferences implements AndroidPreferences { /** * The amount of points to add each time before commiting */ public int gpsCacheLoadingStep = 500; /** * The number of points within an area panel to search for matches * before giving up when combining points together */ public int numPointsToSearchForMatching = 50; /** * Number of milliseconds before we check the gps location table to see if it * has points added. If points have been added, area panel cache is then updated. */ public long areaPanelUpdateGpsLocSpinLockMs = 300*1000; } /** * * @param x1 * @param y1 * @param x2 * @param y2 * @return true if any current viewable nodes overlap the given rectangle of ap units */ public boolean doViewNodesIntersect(int x1, int y1, int x2, int y2) { GTG.cacheCreatorLock.registerReadingThread(); viewNodeThreadManager.registerReadingThread(); try { ArrayList<ViewNode> path = new ArrayList<ViewNode>(); ViewNode previousSibling = null; if (headVn.status != VNStatus.SET) return false; path.add(headVn); boolean currentViewNodeWasChecked = false; // find the next node that has unknown children, which // represents the // best information we have so far (which we are continually // updating by reading the db) while (path.size() > 0) { ViewNode vn = path.get(path.size() - 1); if (currentViewNodeWasChecked) { // set the previous sibling, so we will display the next // one // next time and go downwards once again previousSibling = path.remove(path.size() - 1); currentViewNodeWasChecked = false; //co because if we only display points that are not dirty, we get random flickering // and large points (also, we commented out the needsProcessing below // } else if (!vn.needsProcessing(stBox) && vn.status == ViewNode.VNStatus.SET) { // // if we haven't explored vn's children yet // if (vn.hasUnknownChildren(stBox)) // return vn; } else { AreaPanel ap = vn.ap(); if(ap.overlapsArea(x1, y1, x2, y2)) { //if there are no children, then a visible node overlaps with area if(vn.children == null) return true; // find the next "set" ViewNode int i = 0; // if we already checked an area panel from this group // of children // and we need to go to the next one if (previousSibling != null) { boolean foundSibling = false; // search for the one we previously returned for (i = 0; i < AreaPanel.NUM_SUB_PANELS; i++) { if (vn.children[i] == previousSibling) { i++; foundSibling = true; break; } } if (!foundSibling) TAssert.fail("couldn't find the previous sibling: " + i + ", " + vn + " " + previousSibling); } // find the next set child and continue for (; i < AreaPanel.NUM_SUB_PANELS; i++) { if (vn.children[i] != null //&& !vn.children[i].needsProcessing(stBox) && vn.children[i].status == ViewNode.VNStatus.SET) { vn = vn.children[i]; path.add(vn); break; } } // if there are no more children to go down to, we go up // and try the next one if (i == AreaPanel.NUM_SUB_PANELS) previousSibling = path.remove(path.size() - 1); else previousSibling = null; } else //if the vn doesn't overlap the area, so we go up { previousSibling = path.remove(path.size() - 1); } } } return false; } finally { viewNodeThreadManager.unregisterReadingThread(); GTG.cacheCreatorLock.unregisterReadingThread(); } } /** * Shuts down the cache creator but does not join. Note that this may take awhile * to finish. */ public void shutdown() { synchronized (GTG.cacheCreator) { GTG.cacheCreator.isShutdown = true; GTG.cacheCreator.notify(); } } }