/** 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; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import rtree.AABB; import rtree.BoundedObject; import rtree.RTree; import rtree.RTree.Processor; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.media.ExifInterface; import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.MediaStore; import android.provider.MediaStore.Images.ImageColumns; import android.provider.MediaStore.Video.VideoColumns; import android.util.Log; import android.view.View; import com.rareventure.android.DbUtil; import com.rareventure.android.Util; import com.rareventure.android.database.DbDatastoreAccessor; import com.rareventure.android.database.timmy.TimmyDatastoreAccessor; import com.rareventure.gps2.CacheException; import com.rareventure.gps2.GTG; import com.rareventure.gps2.GTG.GTGEvent; 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.MediaLocTime; import com.rareventure.gps2.database.cachecreator.GpsTrailerCacheCreator; public class MediaLocTimeMap { private static final int MEDIA_SQL_LIMIT = 50; public RTree rTree = new RTree(2,12); private ArrayList<MediaLocTime> futureMltArray = new ArrayList<MediaLocTime>(); public AreaPanelSpaceTimeBox lastApStBox; private static Preferences prefs = new Preferences(); public HashSet<ViewMLT> displayedViewMlts = new HashSet<ViewMLT>(); public int currViewMltWidth; private ArrayList<MediaLocTime> tempLocMltArray = new ArrayList<MediaLocTime>(); private ArrayList<MediaLocTime> mltsToDelete = new ArrayList<MediaLocTime>(); private void resetAllMedia(ArrayList<MediaLocTime> media) { synchronized (this) { this.rTree = new RTree(2, 12); futureMltArray.clear(); tempLocMltArray.clear(); int latestTime = GTG.cacheCreator.maxTimeSec; for(MediaLocTime mlt : media) { // Log.d(GTG.TAG,"insert: "+mlt); if(mlt.getTimeSecs() > latestTime + prefs.maxFutureTimeForPlacingTempMlt) futureMltArray.add(mlt); else { if(mlt.isTempLoc()) tempLocMltArray.add(mlt); rTree.insert(mlt); } } //sort in reverse chronological order Collections.sort(futureMltArray, new Comparator<MediaLocTime>() { @Override public int compare(MediaLocTime lhs, MediaLocTime rhs) { return rhs.getTimeSecs() - lhs.getTimeSecs(); } }); //clear the variables used to determine data that doesn't need to be recalcuated //assuming the rtree hasn't changed this.lastApStBox = null; this.displayedViewMlts.clear(); } } public synchronized void remove(MediaLocTime mlt) { rTree.remove(mlt); } public synchronized void insert(MediaLocTime mlt) { rTree.insert(mlt); } public synchronized void query(ArrayList<MediaLocTime> mediaResults, AABB box) { rTree.query((ArrayList)mediaResults, box); /* ttt_installer:remove_line */Log.d(GTG.TAG,"query against "+box+" returned "+mediaResults.size()+" rows"); } /** * Loads all media from the database and resets the data held by the map * @return */ public void loadFromDb() { resetAllMedia(MediaLocTime.loadAllMediaLocTime()); } /** * Loads media from gallery and updates mediaLocTemps in database and in memory. Note * that this method turns on GTG.alert(GTGEvent.LOADING_MEDIA), but doesn't turn it * off. If the caller is just going to loop and call us again, it can leave this alert * alone, otherwise it should turn it off * @return true if media is no longer dirty */ public boolean updateFromGallery(GpsTrailerCacheCreator gtcc, ContentResolver contentResolver) { //TODO 3 we should be taking this from gps location table, although if there really is no data at all // then we are still stuck with saying the media is not dirty because we have no data at all if(!GTG.apCache.hasGpsPoints()) return true; if(GTG.timmyDb.getProperty("lastImageDateMs") == null) { try { GTG.timmyDb.beginTransaction(); //note that we use the date taken value rather than the id because id's will be reused //if the media with the highest id is deleted //note also that the dates are seperate. This is because we limit the number of images/videos // we grab at a time. So we might grab videos up to Dec 2012, but images only up to Jan 2012, // simply because there are less videos than images GTG.timmyDb.setProperty("lastImageDateMs","0"); GTG.timmyDb.setProperty("lastVideoDateMs","0"); GTG.timmyDb.saveProperties(); GTG.timmyDb.setTransactionSuccessful(); GTG.timmyDb.endTransaction(); } catch(IOException e) { throw new IllegalStateException(e); } } //we need to delete the marked mlts from the database first, because //since they are not in the rtree, we would then assume they we're already // deleted, reuse them and then when they actually did get deleted, they //would write over our handywork GTG.mediaLocTimeMap.deleteMarkedMltsFromDb(); // The latest modification time we read the last time we // updated from the gallery long lastImageDateMs = Long.parseLong(GTG.timmyDb.getProperty("lastImageDateMs")); long lastVideoDateMs = Long.parseLong(GTG.timmyDb.getProperty("lastVideoDateMs")); //TxODO 1 HACK!!!! // lastMltImageId = 2586; long startTimeMs = GTG.cacheCreator.minTimeSec * 1000l; //we load all the current photo ids from the cache into memory //This is so we can delete photo ids from the database which are //no longer present in the users gallery //We load the whole set of ids into memory, because they're can't //be more than 10000 or so pictures I would guess and otherwise //we'd need to make a tree system that we could update (delete // and insert) dynamically which seems to be too much work for //such a small set of ids // 10,000 MLTs take about 400K of memory //Furthermore, we are not using areapanels to do this, because //we'd need to somehow link the media to time trees, (since //more than one image could be associated with an area panel) //note that the fact that we will show a "card pile" for images //that are too close together doesn't help here, because if the //time interval is adjusted, we'd need to be able to show single //images again. //Furthermore we'd then have to delete time trees or join them //back up depending how we represent images, and that would //also be a lot of work. ArrayList<MediaLocTime> media = new ArrayList<MediaLocTime>(rTree.count()+futureMltArray.size()); ArrayList<Integer> deletedMediaIds = new ArrayList<Integer>(); TimmyDatastoreAccessor<MediaLocTime> dataAccessor = new TimmyDatastoreAccessor(GTG.mediaLocTimeTimmyTable); Cursor cursor = null; boolean mediaUpToDate = true; try { GTG.timmyDb.beginTransaction(); int nextRowId = dataAccessor.getNextRowId(); boolean mediaNodesRetreived = false; // //HACK // getAllNodesAndFindDeletedIds(media, deletedMediaIds, dataAccessor.getNextRowId()); // mediaNodesRetreived = true; // for(MediaLocTime mlt : media) // { // Log.d(GTG.TAG,"MediaLocTime: "+mlt); // } // Log.d(GTG.TAG,"Deleted ids: "+deletedMediaIds); //note that we go through all the data, every time. This allows us to find cases //where an image or video was deleted for(int type : new int [] {MediaLocTime.TYPE_IMAGE, MediaLocTime.TYPE_VIDEO} ) { if(type == MediaLocTime.TYPE_IMAGE) { String[] columns = new String[] { ImageColumns._ID, ImageColumns.DATA, //filename ImageColumns.DATE_TAKEN ,ImageColumns.LONGITUDE ,ImageColumns.LATITUDE ,ImageColumns.ORIENTATION }; cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, ImageColumns.DATE_TAKEN+" >= ?", new String [] { String.valueOf(Math.max(startTimeMs,lastImageDateMs+1) )} , ImageColumns.DATE_TAKEN+" limit "+MEDIA_SQL_LIMIT); } else { String [] columns = new String[] { VideoColumns._ID ,VideoColumns.DATA //filename ,VideoColumns.DATE_TAKEN ,VideoColumns.LONGITUDE ,VideoColumns.LATITUDE }; cursor = contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, columns, VideoColumns.DATE_TAKEN+" >= ?", new String [] { String.valueOf(Math.max(startTimeMs,lastVideoDateMs+1) )} , ImageColumns.DATE_TAKEN+" limit "+MEDIA_SQL_LIMIT); } if(gtcc.isShutdown) break; if(!cursor.moveToFirst()) continue; GTG.alert(GTGEvent.LOADING_MEDIA); if(!mediaNodesRetreived) { getAllNodesAndFindDeletedIds(media, deletedMediaIds, dataAccessor.getNextRowId()); mediaNodesRetreived = true; } int highestMediaIdForTypeInMlts = Integer.MIN_VALUE; for(MediaLocTime mlt : media) { if(mlt.getType() == type) highestMediaIdForTypeInMlts = Math.max(mlt.getFk(), highestMediaIdForTypeInMlts); } int mediaQueried = 0; while (!cursor.isAfterLast()) { int mediaId = cursor.getInt(0); String data = cursor.getString(1); mediaQueried++; //I'm not positive this happens but I suspect that if a file was deleted outside of the //gallery it may still report that it exists on certain os's, so I check for it here if(!new File(data).exists()) { /* ttt_installer:remove_line */Log.d(GTG.TAG,"File "+data+" returned from gallery but doesn't really exist"); cursor.moveToNext(); continue; } //note, unfortunately, there is an android bug, and this is just set to // the last modified date (by android), so we try to get the exif //information if available long dateTakenMs = cursor.getLong(2); int orientation = 0; double lon = cursor.isNull(3) ? 0 : cursor.getDouble(3); double lat = cursor.isNull(4) ? 0 : cursor.getDouble(4); if(!Util.isLonLatSane(lon,lat)) { lon = lat = 0; } //note that we use the androids dateTaken value since we are only //using lastMediaDateMs to get new images if(type == MediaLocTime.TYPE_IMAGE) lastImageDateMs = dateTakenMs; else if(type == MediaLocTime.TYPE_VIDEO) lastVideoDateMs = dateTakenMs; //if its an image we can peer into the exif data which seems to be the most accurate as //to when the picture was taken if(type == MediaLocTime.TYPE_IMAGE) { orientation = cursor.getInt(5); ExifInterface ei; try { ei = new ExifInterface(data); long exifDate = Util.getExifDateInUTC(ei); //the exif date is in local time, so we need to convert it. We do this by looking // at the timezone set. This may be off by a few hours, so if they are hiking around // timezone lines, they'll get in trouble but its the best I can do TimeZoneTimeRow tz = GTG.tztSet.getTimeZoneCovering((int)(exifDate/1000l)); if(tz != null && tz.getTimeZone() != null) { //we subtract the timezone because exif date is in the timezone time int offset = tz.getTimeZone().getOffset(exifDate - tz.getTimeZone().getRawOffset()); exifDate -= offset; /* ttt_installer:remove_line */Log.d(GTG.TAG, "adjusting exifDate by "+offset+" to "+exifDate); } /* ttt_installer:remove_line */Log.d(GTG.TAG, "exifDate is "+exifDate+" android date is "+dateTakenMs); //TODO 3: get exif lon and lat to make sure that we tried everything if(exifDate != 0) { dateTakenMs = exifDate; //skip images that have an exif date that is less than the //earliest date of gps points if(exifDate < startTimeMs) { //get the next item from the db cursor.moveToNext(); continue; } } } catch (IOException e1) { Log.d(GTG.TAG,"No exif data for image "+data); } } /* ttt_installer:remove_line */Log.d(GTG.TAG, "type "+type+", mediaId "+mediaId+" timeMs "+dateTakenMs+" data "+data); //since we're changing existing mlts we have to synchronize synchronized (this) { //if the media item might have been updated if(mediaId <= highestMediaIdForTypeInMlts) { int currMltItemIndex = findMlt(mediaId, type, media); if(currMltItemIndex != -1) { /* ttt_installer:remove_line */Log.d(GTG.TAG,"Updating media "+mediaId+" type "+type); //if the media item was updated, delete it //and we'll add it back // note. not sure when this would ever happen, // but the alternative, to ignore it, would cause // the same fk to be represented by two mlts which // would be bad MediaLocTime mlt = media.get(currMltItemIndex); mlt.markDeleted(); deletedMediaIds.add(mlt.id); dataAccessor.updateRow(mlt); media.remove(currMltItemIndex); } } //add the item to the cache MediaLocTime mlt = createMediaLocTime(mediaId, dateTakenMs, type, orientation, lon, lat); if(deletedMediaIds.isEmpty()) { mlt.id = nextRowId++; dataAccessor.insertRow(mlt); } else { mlt.id = deletedMediaIds.remove(deletedMediaIds.size()-1); dataAccessor.updateRow(mlt); } //throw it on the end of our current set of mlts media.add(mlt); //we want to get the gallery to create all the small bitmaps so that we won't //have to worry about it slowing down the strip media gallery (on the main map screen) //TODO 2.5 is this threadsafe? If no? Bitmap x = mlt.getThumbnailBitmap(contentResolver, true); if(x != null) x.recycle(); } //get the next item from the db cursor.moveToNext(); } //if we reached the limit than there may be more media items if(mediaQueried == MEDIA_SQL_LIMIT) mediaUpToDate = false; /* ttt_installer:remove_line */Log.d(GTG.TAG,"Media queried for type "+type+" is "+mediaQueried); cursor.close(); } //for each type of media (image,video) GTG.timmyDb.setProperty("lastImageDateMs", String.valueOf(lastImageDateMs)); GTG.timmyDb.setProperty("lastVideoDateMs", String.valueOf(lastVideoDateMs)); GTG.timmyDb.saveProperties(); GTG.timmyDb.setTransactionSuccessful(); //if we processed any mlts at all if(mediaNodesRetreived) { resetAllMedia(media); //TODO 3 hack to redraw when we show media final OsmMapGpsTrailerReviewerMapActivity localGtum = GTG.cacheCreator.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.notifyMediaChanged(); } } } catch (IOException e) { throw new IllegalStateException(e); } finally { DbUtil.closeCursors(cursor); try { GTG.timmyDb.endTransaction(); } catch (IOException e) { throw new IllegalStateException(e); } } //TODO 3 notify drawer when loaded.. make sure to update GTG.mediaLocTimeMap appropriately return mediaUpToDate; } private int findMlt(int mediaId, int type, ArrayList<MediaLocTime> media) { for(int i = media.size()-1; i >= 0; i--) { if(media.get(i).getType() == type && media.get(i).getFk() == mediaId) return i; } return -1; } /** * Gets all the nodes and figures the locations of the deleted mlts based on the * absense of a node */ private void getAllNodesAndFindDeletedIds(final ArrayList<MediaLocTime> result, ArrayList<Integer> deletedIds, int nextRow) { result.ensureCapacity(rTree.count()+futureMltArray.size()); //put all non deleted mlts into result rTree.query(new Processor() { @Override public boolean process(BoundedObject bo) { MediaLocTime mlt = (MediaLocTime)bo; result.add(mlt); return true; } }, null); result.addAll(futureMltArray); //sort by id Collections.sort(result, new Comparator<MediaLocTime>() { @Override public int compare(MediaLocTime lhs, MediaLocTime rhs) { return lhs.id - rhs.id; } } ); int mltIndex = 0; //find deleted mlts by going through the arrays of the mlts we have against //a counter for(int i = 0; i < nextRow; i++) { if(mltIndex >= result.size()) { deletedIds.add(i); } else { int mltId = result.get(mltIndex).id; if(mltId > i) { deletedIds.add(i); } else if(mltId == i) { mltIndex++; } else throw new CacheException("why two mlts have same id? "+result.get(mltIndex)+result.get(mltIndex-1)); } } } /** * * @param mediaId * @param timeMs * @param type * @param orientation * @param lon if known, otherwise 0 * @param lat if known, otherwise 0 * @return */ private MediaLocTime createMediaLocTime(int mediaId, long timeMs, int type, int orientation, double lon, double lat) { int timeSec = (int) (timeMs / 1000); MediaLocTime row = new MediaLocTime(); row.setData(0, 0, mediaId, timeSec, type, false, orientation); //if we don't already know the lon and lat from the picture information if(lon == 0 && lat == 0) updateMediaLocTimeLoc(row); else { int x = AreaPanel.convertLonmToX((int) (lon*1000000)); int y = AreaPanel.convertLatmToY((int) (lat*1000000)); row.setX(x); row.setY(y); row.setIsTempLoc(false); } return row; } private void updateMediaLocTimeLoc(MediaLocTime row) { int startX, startY, endX, endY; float perc; //true if the location might need to change later (ie the person took a picture // recently and the next gps point wasn't read yet) boolean isTempLoc = false; int timeSec = row.getTimeSecs(); //TODO 4 this would find the location between two ap's, but it stopped working //when ap's timetree's were changed to overlap, maybe fix this? //PERF we could combine these two calls and return an array or something AreaPanel prevAp = AreaPanel.findAreaPanelForTime(timeSec, true); AreaPanel nextAp = AreaPanel.findAreaPanelForTime(timeSec, false); //if the picture was taken before if(prevAp == null) { //co: we want the pictures to have their actual time although we don't know //where they are exactly. So if the user goes back to that time, they can //still see the picture (although in the wrong location) //TODO 3 do we really like it like this? // timeSec = nextAp.getStartTimeSec(); prevAp = nextAp; } if(nextAp == null) { nextAp = prevAp; isTempLoc = true; } if(prevAp.getDepth() != 0) throw new CacheException("prevAp depth... it's wrong! "+prevAp); if(nextAp.getDepth() != 0) throw new CacheException("nextAp depth... it's wrong! "+nextAp); //there is a slight chance that a picture was taken at the exact second that // a gps location was taken if(prevAp == nextAp) perc = 0; else { //PERF, we could report this from findAreaPanelForTime, because it already knows int prevTime = prevAp.getTimeTree().getNearestTimePoint(timeSec, true); int nextTime = nextAp.getTimeTree().getNearestTimePoint(timeSec, false); //note, in general these ap's will always be 1 second long because they are at the //minimum depth perc = ((float)timeSec - prevTime)/(nextTime - prevTime); } //TODO 2.1 handle overlapping of the world, otherwise if line happens to go from new zealand // to california, the photo will show up in the middle startX = prevAp.getCenterX(); endX = nextAp.getCenterX(); startY = prevAp.getCenterY(); endY = nextAp.getCenterY(); synchronized (this) { int x; if(endX - startX > AreaPanel.MAX_AP_UNITS>>1) { x = (int) (startX - (AreaPanel.MAX_AP_UNITS - endX + startX) * perc); if(x < 0) x += AreaPanel.MAX_AP_UNITS; } else if(startX - endX > AreaPanel.MAX_AP_UNITS >>1) { x = (int) (startX + (AreaPanel.MAX_AP_UNITS - startX + endX) * perc); if(x > AreaPanel.MAX_AP_UNITS) x -= AreaPanel.MAX_AP_UNITS; } else x = (int) (startX + (endX-startX) * perc); int y = (int) (startY + (endY-startY) * perc); row.setX(x); row.setY(y); row.setIsTempLoc(isTempLoc); } } /** * Calculates the current viewable nodes (each node may contain multiple MLT's) * based on the provided stbox */ public synchronized void calcViewableMediaNodes(Context context, AreaPanelSpaceTimeBox localApStbox) { int newViewMltWidth = (int)(localApStbox.getWidth() * prefs.multiMLTAreaWidthToApBoxWidth); if(newViewMltWidth != currViewMltWidth) lastApStBox = null; currViewMltWidth = newViewMltWidth; //create an ap box which is bigger than the requested one so that we process nodes //correctly that oerlap the edge AreaPanelSpaceTimeBox adjustedApStBox = new AreaPanelSpaceTimeBox(localApStbox); //note, since in certain circumstances, a viewmlt can grow in size (when time is increased, // more nodes can become part of the viewmlt and shift the center of it), we // use the full width of the areas, rather than half of it adjustedApStBox.addBorder(currViewMltWidth); ArrayList<MediaLocTime> mltsToClearOut = new ArrayList<MediaLocTime>(); //now add points for the new areas created Processor addViewNodeProcessor = createAddViewNodeProcessor(context, adjustedApStBox, mltsToClearOut); for(MediaLocTime mlt : mltsToClearOut) { notifyMltNotClean(mlt); } //if we are completely outside of the last box if(lastApStBox == null || adjustedApStBox.minZ >= lastApStBox.maxZ || adjustedApStBox.maxZ <= lastApStBox.minZ || adjustedApStBox.minX >= lastApStBox.maxX || adjustedApStBox.maxX <= lastApStBox.minX || adjustedApStBox.minY >= lastApStBox.maxY || adjustedApStBox.maxY <= lastApStBox.minY ) { /* ttt_installer:remove_line */Log.d(GTG.TAG,"querying box"); rTree.query(addViewNodeProcessor, adjustedApStBox); } else { //there are 6 sides that may have new points, top, bottom, left, right, future, past AABB temp = adjustedApStBox.clone(); //handle future if(adjustedApStBox.maxZ > lastApStBox.maxZ) { temp.minZ = lastApStBox.maxZ; rTree.query(addViewNodeProcessor, temp); temp.minZ = adjustedApStBox.minZ; } //handle past if(adjustedApStBox.minZ < lastApStBox.minZ) { temp.maxZ = lastApStBox.minZ; rTree.query(addViewNodeProcessor, temp); temp.maxZ = adjustedApStBox.maxZ; } //handle left if(adjustedApStBox.minX < lastApStBox.minX) { temp.maxX = lastApStBox.minX; rTree.query(addViewNodeProcessor, temp); temp.maxX = adjustedApStBox.maxX; } //handle right if(adjustedApStBox.maxX > lastApStBox.maxX) { temp.minX = lastApStBox.maxX; rTree.query(addViewNodeProcessor, temp); temp.minX = adjustedApStBox.minX; } //handle bottom (in y direction) if(adjustedApStBox.minY < lastApStBox.minY ) { temp.maxY = lastApStBox.minY; rTree.query(addViewNodeProcessor, temp); temp.maxY = adjustedApStBox.maxY; } //handle top if(adjustedApStBox.maxY > lastApStBox.maxY) { temp.minY = lastApStBox.maxY; rTree.query(addViewNodeProcessor, temp); temp.minY = adjustedApStBox.minY; } }//if the last ap stbox and the current one overlaps //remove viewmlts that are before or after the current time or out of range in the x,y //dimensions for (Iterator<ViewMLT> i = GTG.mediaLocTimeMap.displayedViewMlts.iterator(); i.hasNext();) { //it is our responsibility to cull out view mlts that should be cleared out ViewMLT viewMlt = i.next(); //now remove viewmlts that aren't displayable anymore if(viewMlt.totalNodes == 0 || viewMlt.width != GTG.mediaLocTimeMap.currViewMltWidth || viewMlt.minZ < adjustedApStBox.minZ || viewMlt.maxZ > adjustedApStBox.maxZ) { i.remove(); continue; } int centerX = viewMlt.getCenterX(); int centerY = viewMlt.getCenterY(); //if we're out of bounds, take it out of the displayed list if(centerX < adjustedApStBox.minX || centerX > adjustedApStBox.maxX || centerY < adjustedApStBox.minY || centerY > adjustedApStBox.maxY) { i.remove(); } } lastApStBox = adjustedApStBox; } private Processor createAddViewNodeProcessor(final Context context, final AreaPanelSpaceTimeBox adjustedApStBox, final ArrayList<MediaLocTime> mltsToClearOut) { final AABB tempAABB = adjustedApStBox.clone(); return new RTree.Processor() { public boolean process(BoundedObject bo) { final MediaLocTime mlt = (MediaLocTime)bo; /* ttt_installer:remove_line */Log.d(GTG.TAG, "processing mlt "+mlt); //if the mlt is already associated with a view node if(mlt.viewMlt != null) { //if the view mlt no longer applies to the current width if(!isViewMltUsable(mlt.viewMlt, currViewMltWidth, adjustedApStBox)) { //if the view mlt can't remove the mlt, because its using //the picture of the mlt if(!mlt.viewMlt.removeMlt(mlt)) { //mark it invalid to destroy it as far as its other mlt's //are concerned.. its already at a different width //so it won't be in the final displayed view mlts anyway mlt.viewMlt.width = -1; } } //otherwise if it's already associated to a valid view node, skip it else { displayedViewMlts.add(mlt.viewMlt); return true; } } //find other mlt's in range so that we can find a view node to stick this //mlt to tempAABB.minX = mlt.getX() - currViewMltWidth / 2; tempAABB.minY = mlt.getY() - currViewMltWidth / 2; tempAABB.maxX = tempAABB.minX + currViewMltWidth; tempAABB.maxY = tempAABB.minY + currViewMltWidth; //if we went through them all without finding a viewnode to attach to if(rTree.query(new RTree.Processor() { @Override public boolean process(BoundedObject bo) { MediaLocTime otherMlt = (MediaLocTime)bo; //if there already is an mlt that has a valid // mlt, then add it if(otherMlt.viewMlt != null && otherMlt.viewMlt.width == currViewMltWidth) { otherMlt.viewMlt.addMlt(mlt); displayedViewMlts.add(otherMlt.viewMlt); return false; } return true; }}, tempAABB)) { //skip mlts that are to be displayed and are unclean //note that we don't affect the rtree here because we are in //the middle of a query and it would compromise the results if(!mlt.isClean(context)) { mltsToClearOut.add(mlt); return true; } //create a new viewmlt mlt.viewMlt = new ViewMLT(currViewMltWidth, mlt); displayedViewMlts.add(mlt.viewMlt); } return true; } }; } private boolean isViewMltUsable(ViewMLT viewMlt, int currViewMltWidth, AreaPanelSpaceTimeBox adjustedApStBox) { return viewMlt.width == currViewMltWidth && viewMlt.minZ >= adjustedApStBox.minZ && viewMlt.maxZ <= adjustedApStBox.maxZ; } /** * Should be called by gtgcachecreator thread to periodically delete mlts that * are marked for deletion */ public void deleteMarkedMltsFromDb() { synchronized (this) { if(mltsToDelete.isEmpty()) { return; } } try { GTG.timmyDb.beginTransaction(); TimmyDatastoreAccessor<MediaLocTime> da = new TimmyDatastoreAccessor<MediaLocTime>(GTG.mediaLocTimeTimmyTable); synchronized (this) { for(MediaLocTime mlt : mltsToDelete) { mlt.markDeleted(); da.updateRow(mlt); } mltsToDelete.clear(); } GTG.timmyDb.setTransactionSuccessful(); } catch (IOException e) { throw new IllegalStateException(e); } finally { try { GTG.timmyDb.endTransaction(); } catch (IOException e) { throw new IllegalStateException(e); } } } /** * Updates temp location for all mlt's with a temp loc. * A temp loc indicates that the final position of the mlt hasn't been calculated * yet. This is used so that if a user takes a picture, it will immediately appear * on their trail, rather than not appear until the next gps point. */ public void updateTempLocs() { synchronized (this) { /* ttt_installer:remove_line */Log.d(GTG.TAG,"updateTempLocs: futureMltArray: "+futureMltArray.size() /* ttt_installer:remove_line */ +" tempLocMltArray: "+tempLocMltArray.size()); if(GTG.apCache.getTopRow() == null) return; int latestApTime = GTG.cacheCreator.maxTimeSec; //first move the items from future mlt array to temp array for all items that are //no longer in the future for(int i = futureMltArray.size()-1; i >= 0; i--) { MediaLocTime mlt = futureMltArray.get(i); //items are in chronological order in reverse, so break as soon as we're still in //the future if(mlt.getTimeSecs() > latestApTime + prefs.maxFutureTimeForPlacingTempMlt) break; tempLocMltArray.add(mlt); futureMltArray.remove(i); } if(!tempLocMltArray.isEmpty()) { try { GTG.timmyDb.beginTransaction(); TimmyDatastoreAccessor<MediaLocTime> dataAccessor = new TimmyDatastoreAccessor<MediaLocTime>(GTG.mediaLocTimeTimmyTable); for(int i = tempLocMltArray.size()-1; i >= 0; i--) { MediaLocTime mlt = tempLocMltArray.get(i); GTG.mediaLocTimeMap.remove(mlt); updateMediaLocTimeLoc(mlt); GTG.mediaLocTimeMap.insert(mlt); if(!mlt.isTempLoc()) tempLocMltArray.remove(i); dataAccessor.updateRow(mlt); } GTG.timmyDb.setTransactionSuccessful(); } catch (IOException e) { throw new IllegalStateException(e); } finally { try { GTG.timmyDb.endTransaction(); } catch (IOException e) { throw new IllegalStateException(e); } } lastApStBox = null; /* ttt_installer:remove_line */Log.d(GTG.TAG,"updateTempLocs end: futureMltArray: "+futureMltArray.size() /* ttt_installer:remove_line */ +" tempLocMltArray: "+tempLocMltArray.size()); } } } public static class Preferences { /** * The maximum amount of time an mlt can be in the future to * place it */ public int maxFutureTimeForPlacingTempMlt = 60 * 60 * 24; /** * Percentage of screen width to width of area where multiple photos and videos * are combined into one node */ public float multiMLTAreaWidthToApBoxWidth = .15f; } public synchronized void notifyMltNotClean(MediaLocTime mlt) { if(mlt.viewMlt != null) { if(!mlt.viewMlt.removeMlt(mlt)) { mlt.viewMlt.width = -1; //this view mlt is destroyed so we have to reload the whole //display lastApStBox = null; } } rTree.remove(mlt); mlt.markDeleted(); mltsToDelete.add(mlt); } public void notifyResume() { /* ttt_installer:remove_line */Log.d(GTG.TAG,"medialoctimemap notified resume"); lastApStBox = null; } }