/** Copyright 2015 Tim Engler, Rareventure LLC This file is part of Tiny Travel Tracker. Tiny Travel Tracker is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Tiny Travel Tracker is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>. */ package com.rareventure.gps2.database.cache; import java.util.ArrayList; import java.util.Iterator; import android.util.Log; import com.rareventure.android.DbUtil; import com.rareventure.android.Util; import com.rareventure.android.database.Cache; import com.rareventure.android.database.DbDatastoreAccessor; import com.rareventure.android.database.TableInfo; import com.rareventure.android.database.timmy.TimmyDatastoreAccessor; import com.rareventure.android.database.timmy.TimmyTable; import com.rareventure.android.encryption.EncryptedRow; import com.rareventure.gps2.GTG; import com.rareventure.gps2.GpsTrailerCrypt; import com.rareventure.gps2.database.cache.AreaPanel.PointCoverageData; import com.rareventure.gps2.database.cache.TimeTree.IntersectsTimeStatus; import com.rareventure.gps2.reviewer.map.sas.Path; public class AreaPanelCache extends Cache<AreaPanel> { private static final int MAX_AUTOZOOM_TO_TEST_AP_PANEL_SIZE = 8; public AreaPanelCache(DatastoreAccessor<AreaPanel> timmyDatastoreAccessor) { // super(new DbDatastoreAccessor<AreaPanel>(AreaPanel.TABLE_INFO), prefs.maxCache); super(timmyDatastoreAccessor, prefs.maxCache); for(int i = 0; i < 4; i++) apArrays[i] = new ArrayList<AreaPanel>(); } @Override public AreaPanel allocateRow() { return GpsTrailerCrypt.allocateAreaPanel(); } public static Preferences prefs = new Preferences(); public static int TOP_ROW_ID = 0; public AreaPanel getTopRow() { if(isDbFilled()) return super.getRow(TOP_ROW_ID); return null; } /** * True if there is an area panel. Note that this does not mean * there are any gps points for the area panel * @param db * @return */ public boolean isDbFilled() { if(!isEmpty()) return true; return super.getNextRowId() != 0; } /** * True if there is an ap, and it contains actual gps points */ public boolean hasGpsPoints() { AreaPanel ap = getTopRow(); return ap != null && ap.getTimeTree() != null; } @Override public AreaPanel getRow(int id) { if(GTG.DEBUG_SHOW_AREA_PANELS) Log.d("GTG", "Cache: "+super.getRow(id)); return super.getRow(id); } @Override public AreaPanel getRowNoFail(int id) { if(GTG.DEBUG_SHOW_AREA_PANELS) Log.d("GTG", "Cache: "+super.getRowNoFail(id)); AreaPanel ap = super.getRowNoFail(id); return ap; } public Iterator<AreaPanel> getCacheRowsForArea(AreaPanelSpaceTimeBox stBox, float lonmToPixels, int maxPointsToDisplay) { final int maxDepth = AreaPanel.getDepthUnder(AreaPanel. convertLonmToX((int)(lonmToPixels * prefs.maxPointPixelWidth)+Util.MIN_LONM)); ArrayList<AreaPanel> items = new ArrayList<AreaPanel>(); //if we are wrapping off the edge of the world if(stBox.maxX < stBox.minX) { getCacheRowsForArea(items, maxDepth, stBox.minX, stBox.minY, AreaPanel.MAX_AP_UNITS, stBox.maxY, stBox.minZ, stBox.maxZ); getCacheRowsForArea(items, maxDepth, 0, stBox.minY, stBox.maxX, stBox.maxY, stBox.minZ, stBox.maxZ); } else { getCacheRowsForArea(items, maxDepth, stBox.minX, stBox.minY, stBox.maxX, stBox.maxY, stBox.minZ, stBox.maxZ); } // Log.i("GPS", "num area panels: "+items.size()); // // int [] dcount = new int [AreaPanel.DEPTH_TO_WIDTH.length]; // // for(AreaPanel ap : items) // { // dcount[ap.getDepth()]++; // } // // for(int i = 0; i < dcount.length; i++) // { // Log.i("GPS", "area panels for depth "+i+": "+dcount[i]); // } // // return items.iterator(); } /** * * @param path * @param maxDepth * @param minX * @param minY * @param maxX * @param maxY * @param endTimeSecs * @param startTimeSecs * @return true if the search range overlaps any points at all */ private boolean goDownToDepth(ArrayList<AreaPanel> path, int maxDepth, int minX, int minY, int maxX, int maxY, int startTimeSecs, int endTimeSecs) { AreaPanel currPanel = path.get(path.size()-1); while(true) { if(currPanel.getDepth() <= maxDepth) return true; currPanel = currPanel.getFirstOverlappingSubPanel(0,minX,minY,maxX,maxY, startTimeSecs, endTimeSecs); if(currPanel == null) return false; path.add(currPanel); } } private void getCacheRowsForArea(ArrayList<AreaPanel> out, int maxDepth, int minX, int minY, int maxX, int maxY, int startTimeSecs, int endTimeSecs) { ArrayList<AreaPanel> path = new ArrayList<AreaPanel>(AreaPanel.DEPTH_TO_WIDTH.length - maxDepth); path.add(getTopRow()); while(true) { goDownToDepth(path, maxDepth, minX, minY, maxX, maxY, startTimeSecs, endTimeSecs); out.add(path.get(path.size()-1)); goUpToNextOverlappingSubPath(path, minX,minY,maxX,maxY, startTimeSecs, endTimeSecs); if(path.size() == 0) return; } } private void goUpToNextOverlappingSubPath(ArrayList<AreaPanel> path, int minX, int minY, int maxX, int maxY, int startTimeSecs, int endTimeSecs) { while(true) { AreaPanel lastSubAP = path.remove(path.size()-1); if(path.size() == 0) return; AreaPanel ap = path.get(path.size()-1); AreaPanel nextSubAP = ap.getFirstOverlappingSubPanel(ap.getIndexOfSubAreaPanel(lastSubAP)+1, minX, minY, maxX, maxY, startTimeSecs, endTimeSecs); if(nextSubAP != null) { path.add(nextSubAP); return; } } } //apArrays are in the order left top right bottom private ArrayList<AreaPanel> [] apArrays = new ArrayList[4]; //WARNING: this only works when SUB_AP_COUNT is 4 public AreaPanelSpaceTimeBox getAutoZoomArea(int startTimeSec, int endTimeSec, ArrayList<Path> paths) { AreaPanel topAp = GTG.apCache.getTopRow(); if(topAp == null) return null; //we deal with the four sides of the current box and shrink it until //we are too close for it to matter for(int i = 0; i < 4; i++) { apArrays[i].add(topAp); } AreaPanelSpaceTimeBox res = new AreaPanelSpaceTimeBox(0, 0, topAp.getMaxX(), topAp.getMaxY(),startTimeSec, endTimeSec); res.pathList = paths; for(;;) { //if no ap's are left, the cache area is undefined if(apArrays[0].size() == 0) return null; AreaPanel testApPanel = apArrays[0].get(0); int currDepth = testApPanel.getDepth(); if(currDepth == 0 || (res.getWidth()+ res.getHeight()) / (testApPanel.getMaxX() - testApPanel.getX()) > MAX_AUTOZOOM_TO_TEST_AP_PANEL_SIZE) { break; } for(int i = 0; i < 4; i++) { boolean foundAnyOuterSubAps = false; //check outer area for all active area panels. Note that we throw //sub ap rows on the end of the array but that doesn't matter because //we are going backwards for(int j = apArrays[i].size()-1; j >= 0; j--) { AreaPanel ap = apArrays[i].get(j); //first set is used to determine whether to add a new item to the //array or write over the current entry (which we do the first time to get rid of the area panel at the higher level) boolean firstSet = false; //if left or top if(i == 0 || i == 1) { //top left firstSet = setAutoZoomApIfExistsAndUpdateRes(ap.getSubAreaPanel(0), j, apArrays[i], firstSet, startTimeSec, endTimeSec, paths); //bottom left (for left) / top right (for top) firstSet = setAutoZoomApIfExistsAndUpdateRes(i == 0 ? ap.getSubAreaPanel(2) : ap.getSubAreaPanel(1), j, apArrays[i], firstSet, startTimeSec, endTimeSec, paths); } else //right or bottom { //bottom right firstSet = setAutoZoomApIfExistsAndUpdateRes(ap.getSubAreaPanel(3), j, apArrays[i], firstSet, startTimeSec, endTimeSec, paths); //top right (for right) / bottom left (for bottom) firstSet = setAutoZoomApIfExistsAndUpdateRes(i == 2 ? ap.getSubAreaPanel(1) : ap.getSubAreaPanel(2), j, apArrays[i], firstSet, startTimeSec, endTimeSec, paths); } foundAnyOuterSubAps = foundAnyOuterSubAps || firstSet; } //if there are hits on the outers side, delete all the rows that didn't have hits there if(foundAnyOuterSubAps) { for(int j = apArrays[i].size()-1; j >= 0; j--) { AreaPanel ap = apArrays[i].get(j); if(ap.getDepth() == currDepth) apArrays[i].remove(j); } } else //there are no hits in the outer side, go to the inner side { //adjust the space time box inwards if(i == 0) res.minX = apArrays[i].get(0).getCenterX(); else if(i == 2) res.maxX = apArrays[i].get(0).getCenterX(); else if(i == 1) res.minY = apArrays[i].get(0).getCenterY(); else //(i == 3) res.maxY = apArrays[i].get(0).getCenterY(); //find the inner arrays for(int j = apArrays[i].size()-1; j >= 0; j--) { AreaPanel ap = apArrays[i].get(j); //first set is used to determine whether to add a new item to the //array or write over the current entry boolean firstSet = false; //if left or top if(i == 0 || i == 1) { //bottom right firstSet = setAutoZoomApIfExistsAndUpdateRes(ap.getSubAreaPanel(3), j, apArrays[i], firstSet, startTimeSec, endTimeSec, paths); //top right (for left) / bottom left (for top) firstSet = setAutoZoomApIfExistsAndUpdateRes(i == 0 ? ap.getSubAreaPanel(1) : ap.getSubAreaPanel(2), j, apArrays[i], firstSet, startTimeSec, endTimeSec, paths); } else //right or bottom { //top left firstSet = setAutoZoomApIfExistsAndUpdateRes(ap.getSubAreaPanel(0), j, apArrays[i], firstSet, startTimeSec, endTimeSec, paths); //bottom left (for right) / top right (for bottom) firstSet = setAutoZoomApIfExistsAndUpdateRes(i == 2 ? ap.getSubAreaPanel(2) : ap.getSubAreaPanel(1), j, apArrays[i], firstSet, startTimeSec, endTimeSec, paths); } if(!firstSet) { apArrays[i].remove(j); } } }//if we didn't find outer panels } } //clean up after ourselves for(int i = 0; i < 4; i++) { apArrays[i].clear(); } return res; } //if the given sub ap contains the time period, then set it up for the next autozoom round. private boolean setAutoZoomApIfExistsAndUpdateRes(AreaPanel subAp, int index, ArrayList<AreaPanel> arrayList, boolean firstSet, int startTimeSec, int endTimeSec, ArrayList<Path> paths) { if(subAp != null) { TimeTree topTt = subAp.getTimeTree(); if(subAp != null && TimeTree.intersectsTime(subAp.getTimeTree(), startTimeSec, endTimeSec).overlaps) { if(!pathsOverlap(subAp.getTimeTree(), paths)) return firstSet; // Log.d(GTG.TAG,"Found subAp for index "+index+", subAp "+subAp); //if we write over the previous levels ap in the array // (which we do the first time) if(!firstSet) arrayList.set(index, subAp); else arrayList.add(subAp); return true; } } return firstSet; } private boolean pathsOverlap(TimeTree timeTree, ArrayList<Path> paths) { if(paths != null) { for(Path p : paths) { if(TimeTree.intersectsTime(timeTree, p.startTimeSec, p.endTimeSec).overlaps) { return true; } } return false; } else return true; } public static class Preferences { /** * Max number of area panels to cache */ public int maxCache = 2048; /** * Maximum number of points where if this is met or exceeded, we consider autozoom to be finished */ public int maxAutoZoomPointCount = 10; /** * The maximum width in pixels of the points on the screen */ public float maxPointPixelWidth = 1f; } }