/** 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 android.graphics.Point; import android.graphics.PointF; import android.os.Debug; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import com.mapzen.tangram.LngLat; import com.mapzen.tangram.MapController; import com.mapzen.tangram.MapData; import com.rareventure.android.AndroidPreferenceSet; import com.rareventure.android.SortedBestOfIntArray; import com.rareventure.android.SuperThread; import com.rareventure.android.Util; import com.rareventure.gps2.GTG; 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.database.cachecreator.ViewNode; import com.rareventure.gps2.reviewer.map.OsmMapGpsTrailerReviewerMapActivity.OngoingProcessEnum; import com.rareventure.gps2.reviewer.map.sas.Area; import com.rareventure.gps2.reviewer.map.sas.SelectedAreaSet; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; /** * This figures the points for the screen. Because point finding can take a long time * if the user visited the same area many times, we calculate points in stages, in a drill down * fashion. ie. We may first return a point for a depth corresponding to an entire city, and then * drill down to sub points, and then to the sub sub points, and so forth until we get to a point * size that is reasonable given the current display depth. */ //TODO 3: we need a hard limit and a soft limit for number of points displayed. Once we cross the soft limit, // we won't draw the next depth unless the stb changes //TODD 2.1: do we really need superthreads anymore? The only reason we have them is to "pause" // drawer, and map tile threads as well as to shut them down. (see onPause and onDestroy in the // activity) //I suppose that's ok, but why do we have such a complicated // object for this? public class GpsTrailerOverlay extends SuperThread.Task implements GpsOverlay { private final int minCirclePxRadius; //this is where we write our data to, which gets picked up by mapzen and drawn //using the yaml file to style the points and lines private MapData mapData; private final OsmMapView osmMapView; private boolean viewUpToDate = true; public OsmMapGpsTrailerReviewerMapActivity activity; public int latestOnScreenPointSec; public int earliestOnScreenPointSec; public SelectedAreaSet sas; /** * This is what we request to be drawn next */ private AreaPanelSpaceTimeBox requestedStBox; private int lastCalculatedGpsLatM; private int lastCalculatedGpsLonM; private float lastCalculatedRadius; Handler myHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { activity.createNewUserLocation(lastCalculatedGpsLatM, lastCalculatedGpsLonM, lastCalculatedRadius); } }; public static boolean doMethodTracing; private int minTimeTreeLengthForLineCalc = Integer.MAX_VALUE; /** * The number of lines betweem view nodes to process between redrawing the picture * Each view node processes ViewNode.MAX_CALCULATED_LINES_PER_ROUND lines * per round */ private static final int VIEW_NODES_TO_CALC_LINES_FOR_PER_ROUND = 50; /** * The maximum number of lines to draw on the screen */ private static final int MAX_LINES = 2000; private static final int BEST_LARGEST_TIME_TREE_LENGTH_FOR_UNCREATED_LINES_LIST_TOTAL = 20; private static final int NUM_COLORS = 32; private static final int MAX_TIME_DRAWING_BEFORE_DISPLAY_NOTICE_MS = 500; public static Preferences prefs = new Preferences(); private MapController mapController; public int[] paintColors; /** * If true, then when a new area is created, the previous ones will remain * TODO 3: (currently not used because there is no way in the interface to activate this) */ private boolean selectedAreaAddLock; private SasDrawer sasDrawer; public GpsTrailerOverlay(OsmMapGpsTrailerReviewerMapActivity activity, SuperThread superThread, OsmMapView osmMapView) { super(GTG.GPS_TRAILER_OVERLAY_DRAWER_PRIORITY); this.activity = activity; this.sas = new SelectedAreaSet(activity); this.osmMapView = osmMapView; this.superThread = superThread; paintColors = new int[NUM_COLORS]; minCirclePxRadius = (int) Util.convertDpToPixel(GpsTrailerOverlay.prefs.minGpsRadiusDp, activity); updateForColorRangeChange(); } public void updateForColorRangeChange() { // MEMPERF PERF consider just changing the color everytime for (int i = 0; i < NUM_COLORS; i++) { int result = 0; int c1i = i * (OsmMapGpsTrailerReviewerMapActivity.prefs.colorRange.length - 1) / (NUM_COLORS - 1); int c2i = c1i + 1; if (c2i > OsmMapGpsTrailerReviewerMapActivity.prefs.colorRange.length - 1) c2i = c1i; for (int j = 0xFF; j > 0; j = j << 8) { int c1 = OsmMapGpsTrailerReviewerMapActivity.prefs.colorRange[c2i] & j; int c0 = OsmMapGpsTrailerReviewerMapActivity.prefs.colorRange[c1i] & j; result |= ((int) (((double) c1 - c0) * i / NUM_COLORS + c0)) & j; } result |= 0xFF000000; paintColors[i] = result; } } public void doWork() { AreaPanelSpaceTimeBox localApStbox = null; synchronized (this) { if(viewUpToDate) { activity.notifyDoneProcessing(OngoingProcessEnum.DRAW_POINTS); //wait for the cache to need to be changed or we're ready to retry looking for new points stWait(0,this); return; } } abortOrPauseIfNecessary(); GTG.cacheCreatorLock.registerReadingThread(); if(doMethodTracing) Debug.startMethodTracing("/sdcard/it.trace"); try { boolean stillMoreNodesToCalc = true; boolean allLinesCalculated = false; long timeStartedDrawingMs = System.currentTimeMillis(); while((!allLinesCalculated || stillMoreNodesToCalc) && !superThread.manager.isShutdown) { if(System.currentTimeMillis() - timeStartedDrawingMs > MAX_TIME_DRAWING_BEFORE_DISPLAY_NOTICE_MS) activity.notifyProcessing(OngoingProcessEnum.DRAW_POINTS); if(!distanceUpToDate) { int startTime, endTime; synchronized(this) { startTime = requestedStBox.minZ; endTime = requestedStBox.maxZ; } activity.notifyDistUpdated(GpsTrailerCacheCreator.calcTrDist(startTime, endTime, null)); synchronized(this) { distanceUpToDate = true; } } synchronized(this) { if(localApStbox == null || !localApStbox.equals(requestedStBox)) { //copy the stbox to local box, so if it changes we won't get weirded out. localApStbox = requestedStBox; stillMoreNodesToCalc = true; } } long startTime = System.currentTimeMillis(); int minDepth = getMinDepth(localApStbox); //Log.d(GTG.TAG,"calc points for "+localApStbox+" minDepth "+minDepth); TimeTree.hackTimesLookingUpTimeTrees = 0; while(System.currentTimeMillis() - startTime < prefs.maxDrawCalcTime && stillMoreNodesToCalc) { int status = GTG.cacheCreator.calcViewableNodes(localApStbox, minDepth, earliestOnScreenPointSec, latestOnScreenPointSec); stillMoreNodesToCalc = ((status & GpsTrailerCacheCreator.CALC_VIEW_NODES_STILL_MORE_NODES_TO_CALC) != 0); //if there were some view nodes that were reset or new ones created and the lines need to be recalculated if((status & GpsTrailerCacheCreator.CALC_VIEW_NODES_LINES_NEED_RECALC) != 0) { minTimeTreeLengthForLineCalc = Integer.MAX_VALUE; lastNumberOfViewNodesLinesCalculatedFor = Integer.MAX_VALUE; } } ///* ttt_installer:remove_line */Log.d("GPS","times looking up time trees is "+TimeTree.hackTimesLookingUpTimeTrees); allLinesCalculated = calcLines(localApStbox, minDepth); //PERF: it would be faster to store the pixel locations //if while calculating all lines, we got to the maximum line count, and drawLines deletes //some of them, we have to recalculate to add some more back in if(allLinesCalculated && isAtMaxLines()) { if(drawLines(localApStbox, minDepth)) { ///* ttt_installer:remove_line */Log.d("GPS", "setting all lines calculated false"); allLinesCalculated = false; } } else { drawLines(localApStbox, minDepth); } calcStartAndEndTimeOnScreen(); int currPoints = drawPoints(localApStbox); //TODO 2 handle photos // if(OsmMapGpsTrailerReviewerMapActivity.prefs.showPhotos) // drawMedia(localApStbox); boolean stBoxNotChanged; synchronized(this) { //we only say view is up to date if its up to date according to the requeted stbox // (which may have changed while we were calculating) if((stBoxNotChanged = localApStbox.equals(requestedStBox)) && allLinesCalculated && !stillMoreNodesToCalc) viewUpToDate = true; } if(stBoxNotChanged) { activity.runOnUiThread(activity.NOTIFY_HAS_DRAWN_RUNNABLE); } } } finally { if(doMethodTracing) { doMethodTracing = false; Debug.stopMethodTracing(); } GTG.cacheCreatorLock.unregisterReadingThread(); activity.notifyDoneProcessing(OngoingProcessEnum.DRAW_POINTS); } } private boolean isAtMaxLines() { return GTG.cacheCreator.startTimeToViewLine.size() >= MAX_LINES; } private boolean distanceUpToDate; public int closestToCenterTimeSec; /** * For each round when we calculate lines for viewnodes, we must calculate the most * important lines first, which we determine by the time length with the time trees of the nodes. * This is used to keep track of the view nodes with the biggest time jumps that we didn't * process this round, so that next round, we know which ones to start from */ private SortedBestOfIntArray bestLargestTimeTreeLengthForUncreatedLinesList = new SortedBestOfIntArray(BEST_LARGEST_TIME_TREE_LENGTH_FOR_UNCREATED_LINES_LIST_TOTAL); /** * We use this to determine how many lines at a time should be pulled from each viewnode. At the beginning, we pull very few, but as * the number of active nodes becomes less and less, we pull more and more, so we don't have to keep relooping over and over. * The reason we pull a few at first is because we want to give priority to viewnodes with few lines coming from them, because * this often means an easily viewable trail */ private int lastNumberOfViewNodesLinesCalculatedFor = Integer.MAX_VALUE;; /** * Updates startTimeToViewLines and endTimeToViewLines with visible lines. * * @return true if there are no more lines to calculate, false otherwise */ private boolean calcLines(AreaPanelSpaceTimeBox apStBox, int minDepth) { // float metersPerApUnits = activity.calcMetersPerApUnits(); // Log.d(GTG.TAG,"vn calcLines ----"); //note, technically, this isn't needed, since writing to the view nodes will //never interfere with code at this point to read them. But to make the code //clearer, we do it anyway GTG.cacheCreator.viewNodeThreadManager.registerReadingThread(); int numberOfViewNodesLinesCalculatedFor = 0; try { Iterator<ViewNode> iter = GTG.cacheCreator.getViewNodeIter(); ArrayList<TimeTree> scratchPath = new ArrayList<TimeTree>(); //if there are no visible ap's (view nodes) if(!iter.hasNext()) { //we still may need to draw a line. Consider, A at 10:00, // B at 11:00 and the time range at 10:30 - 10:35 AreaPanel topAp = GTG.apCache.getTopRow(); //if there is no data at all if(topAp == null || topAp.getTimeTree() == null) return true; //start and end ap are the same because we are in the middle of the line, //and we don't know which one we'll get AreaPanel ap = topAp.getChildApAtDepthAndTime(minDepth, apStBox.minZ); //if there isn't an ap, we must be off the edge of the start and end times if(ap == null) return true; TimeTree tt = ap.getTimeTree().getBottomLevelEncompassigTimeTree(apStBox.minZ); ViewLine vl; int endCutTime = tt.calcTimeRangeCutEnd(); if(endCutTime <= apStBox.minZ) { vl = new ViewLine(endCutTime, tt.getMaxTimeSecs()); vl.startApId = ap.id; vl.endApId = tt.getNextApId(); } else { vl = new ViewLine(tt.getMinTimeSecs(), tt.calcTimeRangeCutStart()); vl.startApId = tt.getPrevApId(); vl.endApId = ap.id; } GTG.cacheCreator.startTimeToViewLine.put(vl.startTimeSec, vl); GTG.cacheCreator.endTimeToViewLine.put(vl.endTimeSec, vl); return true; } //PERF: we could combine this with drawPoints and not iterate twice (and draw lines after drawing points shadow) while(iter.hasNext()) { ViewNode vn = iter.next(); //if we need to calculate lines for the view node... we don't do this for // ap's not at min depth, since they'll be replaced anyway if(vn.largestTimeTreeLengthForUncreatedLines >= minTimeTreeLengthForLineCalc && vn.ap().getDepth() == minDepth) { vn.calcLinesForStBox(scratchPath, apStBox, GTG.cacheCreator.startTimeToViewLine, GTG.cacheCreator.endTimeToViewLine, lastNumberOfViewNodesLinesCalculatedFor); numberOfViewNodesLinesCalculatedFor++; if(numberOfViewNodesLinesCalculatedFor > VIEW_NODES_TO_CALC_LINES_FOR_PER_ROUND) //this is a fine point. The first round, we need to process all the view nodes //to get their start and end times done first. So we don't want to change //minTimeTreeLengthForLineCalc until that is done. However, when the ends are //finished, we need to choose the next minTimeTreeLengthForLineCalc, so at that //time we will exit the while loop. This means a lot of restarting the iterator // and skipping view nodes, but since calculating view nodes will change the //underlying tree, I can't see reusing the iterator to be very easy //PERF reuse the iterator return false; if(isAtMaxLines()) { /* ttt_installer:remove_line */Log.d(GTG.TAG,"Reached max lines: "+GTG.cacheCreator.startTimeToViewLine.size()+" minTimeTreeLengthForLineCalc is "+minTimeTreeLengthForLineCalc); return true; } } } //we've gone through the whole list of viewnodes for this round bestLargestTimeTreeLengthForUncreatedLinesList.clear(); iter = GTG.cacheCreator.getViewNodeIter(); while(iter.hasNext()) { ViewNode vn = iter.next(); if(vn.ap().getDepth() != minDepth) continue; bestLargestTimeTreeLengthForUncreatedLinesList.add(vn.largestTimeTreeLengthForUncreatedLines); } //if there are no time gaps that are more finely grained then those we have already //created viewlines for if(bestLargestTimeTreeLengthForUncreatedLinesList.data[BEST_LARGEST_TIME_TREE_LENGTH_FOR_UNCREATED_LINES_LIST_TOTAL-1] <= 0) return true; ///we're done //choose a min time jump for next round minTimeTreeLengthForLineCalc = bestLargestTimeTreeLengthForUncreatedLinesList.data[0]; return false; } finally { GTG.cacheCreator.viewNodeThreadManager.unregisterReadingThread(); lastNumberOfViewNodesLinesCalculatedFor = numberOfViewNodesLinesCalculatedFor; /* ttt_installer:remove_line */Log.d(GTG.TAG,"numberOfViewNodesLinesCalculatedFor: "+numberOfViewNodesLinesCalculatedFor+" lines: "+GTG.cacheCreator.startTimeToViewLine.size()+ /* ttt_installer:remove_line */ " minTimeTreeLengthForLineCalc is "+minTimeTreeLengthForLineCalc+ /* ttt_installer:remove_line */ " blttlfull0="+bestLargestTimeTreeLengthForUncreatedLinesList.data[0] /* ttt_installer:remove_line */ +" blttlfullMAX="+bestLargestTimeTreeLengthForUncreatedLinesList.data[BEST_LARGEST_TIME_TREE_LENGTH_FOR_UNCREATED_LINES_LIST_TOTAL-1]); } } /** * This both draws and calculates lines at the same time. We do this together so * we don't have to iterate through all the ap's twice. The strategy here is * to incrementally calculate some lines, draw our work, and keep looping * until all the lines are calculated and drawn. By doing it this way, * we show our work to the user periodically as we draw more and more lines * * @param apStBox * @param minDepth * @return */ //TODO 3: update lines calculated for viewnodes when the time of the stb changes... later: not sure what this means?? // moving back in forth in time works fine for lines private boolean drawLines(AreaPanelSpaceTimeBox apStBox, int minDepth) { // float metersPerApUnits = activity.calcMetersPerApUnits(); if(1==1) return false; /* ttt_installer:remove_line */Log.d(GTG.TAG,"vn drawLines ----"); boolean removedLines = false; for(Iterator<Entry<Integer, ViewLine>> i = GTG.cacheCreator.startTimeToViewLine.entrySet().iterator(); i.hasNext();) { Entry<Integer, ViewLine> e = i.next(); ViewLine vl = e.getValue(); if(vl.endTimeSec > apStBox.minZ && vl.startTimeSec < apStBox.maxZ) { AreaPanel priorAp = vl.getStartAp(); if(priorAp.getDepth() == minDepth) { AreaPanel nextAp = vl.getEndAp(); //if the start or end part of the line is within the area panel if(apStBox.contains(priorAp.getX(), priorAp.getY()) || apStBox.contains(nextAp.getX(), nextAp.getY())) { drawLine(priorAp,nextAp, apStBox); continue; } } } //if we didn't draw the line, we remove it here i.remove(); GTG.cacheCreator.endTimeToViewLine.remove(vl.endTimeSec); removedLines = true; } return removedLines; } private void drawLine(AreaPanel ap1, AreaPanel ap2, AreaPanelSpaceTimeBox apStBox) { if(ap1 == null || ap2 == null) return; //if one of the aps is out of time range if(ap2.getStartTimeSec() >= apStBox.maxZ || ap1.getEndTimeSec() <= apStBox.minZ) return; //PERF if points are directly next to each other, don't draw lines int apX1 = ap1.getCenterX(); int apY1 = ap1.getCenterY(); int apX2 = ap2.getCenterX(); int apY2 = ap2.getCenterY(); if(apX2 - apX1 > AreaPanel.MAX_AP_UNITS>>1) { int yAtEdge = apY1+ (int) (((long)apX1)*(apY2 - apY1)/(apX1 + AreaPanel.MAX_AP_UNITS - apX2)); drawLineBetweeenApPoints(apStBox, apX1, apY1, 0, yAtEdge); drawLineBetweeenApPoints(apStBox, AreaPanel.MAX_AP_UNITS-1, yAtEdge, apX2, apY2); } else if(apX1 - apX2 > AreaPanel.MAX_AP_UNITS>>1) { int yAtEdge = apY1 + (int) (((long)AreaPanel.MAX_AP_UNITS - apX1)*(apY2 - apY1)/(apX2 + AreaPanel.MAX_AP_UNITS - apX1)); drawLineBetweeenApPoints(apStBox, apX1, apY1, AreaPanel.MAX_AP_UNITS-1, yAtEdge); drawLineBetweeenApPoints(apStBox, 0, yAtEdge, apX2, apY2); } else drawLineBetweeenApPoints(apStBox, apX1, apY1, apX2, apY2); } private static Point p1 = new Point(); private static Point p2 = new Point(); private void drawLineBetweeenApPoints(AreaPanelSpaceTimeBox stBox, int apX1, int apY1, int apX2, int apY2) { //TODO 1.5 fix lines! // stBox.apUnitsToPixels(p1, // apX1, apY1, // drawingBitmap.getWidth(), drawingBitmap.getHeight()); // stBox.apUnitsToPixels(p2, // apX2, apY2, // drawingBitmap.getWidth(), drawingBitmap.getHeight()); // // drawingCanvas.drawLine(p1.x, p1.y, p2.x, p2.y, // linePaint); } private static int getMaxDepth(AreaPanelSpaceTimeBox stBox) { /*index of the search key, if it is contained in the array; otherwise, (-(insertion point) - 1). * The insertion point is defined as the point at which the key would be inserted into the array: * the index of the first element greater than the key, or a.length if all elements in the array * are less than the specified key. Note that this guarantees that the return value will be >= 0 * if and only if the key is found.*/ int index = Arrays.binarySearch(AreaPanel.DEPTH_TO_WIDTH, (int)(stBox.getWidth() * GpsTrailerOverlay.prefs.maxPointSizePerc)); //if we exactly matched a depth, that will be the max depth if(index >= 0) return index; //if our max depth is below the minimum depth if (index == -1) return 0; //otherwise we want the depth 1 below our max return -index - 2; } public static int getMinDepth(AreaPanelSpaceTimeBox apStbox) { int index = Arrays.binarySearch(AreaPanel.DEPTH_TO_WIDTH, (int)(apStbox.getWidth() * GpsTrailerOverlay.prefs.minPointSizePerc)); //if we exactly matched a depth, that will be the min depth if(index >= 0) return index; //if our min depth is below the level 0 depth if (index == -1) return 0; //otherwise we want the depth 1 above our min //TODO 3 or should it be one below our min? return -index - 1; } /** * This calculates the start and end time the path is visible on screen, so we can choose * appropriate colors. It uses the points calculated by cache creator * and places the results in {@code earliestOnScreenPointSec} and {@code latestOnScreenPointSec} */ private void calcStartAndEndTimeOnScreen() { GTG.cacheCreator.viewNodeThreadManager.registerReadingThread(); try { int localEarliestOnScreenPointSec = Integer.MAX_VALUE; int localLatestOnScreenPointSec = Integer.MIN_VALUE; Iterator<ViewNode> iter = GTG.cacheCreator.getViewNodeIter(); while(iter.hasNext()) { ViewNode vn = iter.next(); if (localLatestOnScreenPointSec < vn.overlappingRange[1]) localLatestOnScreenPointSec = vn.overlappingRange[1]; if (localEarliestOnScreenPointSec > vn.overlappingRange[0]) localEarliestOnScreenPointSec = vn.overlappingRange[0]; //note that there is a thread race with the main thread and time view here //but I don't think this will affect much //TODO 2.5 should we synchronize here? It's growing with the addition of colorRangeStartEndSec latestOnScreenPointSec = localLatestOnScreenPointSec; earliestOnScreenPointSec = localEarliestOnScreenPointSec; } } finally { GTG.cacheCreator.viewNodeThreadManager.unregisterReadingThread(); } } /** * @return number of points drawn */ private int drawPoints(AreaPanelSpaceTimeBox apStBox) { //note, technically, this isn't needed, since writing to the view nodes will //never interfere with code at this point to read them. But to make the code //clearer, we do it anyway GTG.cacheCreator.viewNodeThreadManager.registerReadingThread(); // Log.d(GTG.TAG,"Drawing points for "+apStBox); try { long time = System.currentTimeMillis(); int pointCount = 0; int maxDepth = getMaxDepth(apStBox); Iterator<ViewNode> iter = GTG.cacheCreator.getViewNodeIter(); Point p = new Point(); Point p2 = new Point(); float closestPointDistSquared = Float.MAX_VALUE; int closestToCenterEndTimeSec = 0; AreaPanel closestToCenterAp = null; LngLat ll = new LngLat(); Map<String, String> props = new HashMap<>(); mapData.beginChangeBlock(); mapData.clear(); //co:hack to show top and bottom of view area // LngLat tl = mapController.coordinatesAtScreenPosition(0,0); // LngLat br = mapController.coordinatesAtScreenPosition(osmMapView.windowWidth, // osmMapView.pointAreaHeight); // // props.put("color","#ffffff"); // props.put("size",String.format("%dpx %dpx", 10, 10)); // mapData.addPoint(tl,props); // // props.put("color","#000000"); // props.put("size",String.format("%dpx %dpx", 10, 10)); // mapData.addPoint(br,props); while(iter.hasNext()) { ViewNode vn = iter.next(); AreaPanel areaPanel = vn.ap(); //skip any areaPanel that's "too big" to display (and will look weird) // if(areaPanel.getDepth() > maxDepth) // continue; pointCount++; ll.set( AreaPanel.convertXToLon(areaPanel.getCenterX()), AreaPanel.convertYToLat(areaPanel.getCenterY()) ); // Log.d(GTG.TAG,"Drawing point at lon "+ll.longitude+" lat "+ll.latitude); //TODO 3 maybe one day handle altitude float speedMult = calcSpeedMult(vn, areaPanel.getDepth()); int circleSizePx = (int) (Math.max(minCirclePxRadius, 2*(p2.x-p.x))* speedMult) * 2; int paintIndex = figurePaintIndex(vn.overlappingRange[0],vn.overlappingRange[1]); props.put("color",String.format("#%06x",paintColors[paintIndex])); props.put("size",String.format("%dpx %dpx", circleSizePx, circleSizePx)); //Here we add the actual point into mapzen. // // Note that we aren't in the UI thread //at this point. However, even though it's not specified in the documentation, I //believe its safe to call this method from our thread. This is because mapzen //uses opengl which doesn't use the UI thread, *and* I examined the code from the //current version, and it does use a mutex before adding the point. See here: //tangram-es/core/src/data/clientGeoJsonSource.cpp: //void ClientGeoJsonSource::addPoint(const Properties& _tags, LngLat _point) mapData.addPoint(ll,props); //we need to figure out what timezone to use. We do this by looking for the point //closest to the center of the screen and using the timezone we were in during //the same time the point was recorded to choose a good timezone. //(Otherwise figuring it out based on where the point is on the earth is rather // complex) float distSquared = Util.square(osmMapView.centerX - p.x) + Util.square(osmMapView.centerY - p.y); if(distSquared < closestPointDistSquared) { closestToCenterEndTimeSec = vn.overlappingRange[1]; closestToCenterAp = areaPanel; closestPointDistSquared = distSquared; } } //while examining pointCount mapData.endChangeBlock(); mapController.requestRender(); if(closestToCenterAp != null) { //note that we don't get the bottom level here because the overlapping range may contain some fuzziness TimeTree tt = closestToCenterAp.getTimeTree().getEncompassigTimeTreeOrMaxTimeTreeBeforeTime(closestToCenterEndTimeSec-1, false); closestToCenterTimeSec = tt.calcTimeRangeCutEnd(); } ///* ttt_installer:remove_line */Log.d("GPS","drew "+pointCount+" pointCount"); return pointCount; } finally { GTG.cacheCreator.viewNodeThreadManager.unregisterReadingThread(); } } //see depth_to_max_seconds.ods private static float [] DEPTH_TO_MAX_SECONDS = new float [] { 0.625f, 1.25f, 2.5f, 5f, 10f, 20f, 40f, 80f, 160f, 320f, 640f, 1280f, 2560f, 5120f, 10240f, 20480f, 40960f, 81920f, 163840f, 327680f, 655360f, 1310720f, 2621440f, 5242880f, 10485760f, 20971520f, 41943040f }; private float calcSpeedMult(ViewNode vn, int depth) { return .5f* (1-DEPTH_TO_MAX_SECONDS[depth]/(DEPTH_TO_MAX_SECONDS[depth]+(vn.overlappingRange[1] - vn.overlappingRange[0])))+.5f; } private int figurePaintIndex(int startTimeSec, int endTimeSec) { //we convert everything to long to avoid overflow problems //co: used to use average time // int val = (int)( ((long)(startTimeSec>>1) + (endTimeSec>>1) - earliestOnScreenPointSec) // * (paint.length) / ((latestOnScreenPointSec - earliestOnScreenPointSec)+1)); //we use the latest time that the point was visited. If we use the average point, and //lets say the user started in a position, traveled in a circle and came back, then //the color would be green, or represent the user was there at the midpoint of the trip. //This is quite confusing, so we go with the idea that the later points "paint over" the //earlier ones when the user was there more than once int val = (int)( ((long)endTimeSec - earliestOnScreenPointSec) * (NUM_COLORS) / ((latestOnScreenPointSec - earliestOnScreenPointSec)+1)); if (val > NUM_COLORS - 1) return NUM_COLORS - 1; if (val < 0) return 0; return val; } public void notifyScreenChanged(AreaPanelSpaceTimeBox newStBox) { synchronized(this) { if(requestedStBox != null) newStBox.pathList = requestedStBox.pathList; if(requestedStBox == null || !requestedStBox.equals(newStBox)) { viewUpToDate = false; if(requestedStBox == null || requestedStBox.minZ != newStBox.minZ || requestedStBox.maxZ != newStBox.maxZ) { //this sets up requestedStbBox to the current view sas.setRequestedTime(newStBox.minZ, newStBox.maxZ); distanceUpToDate = false; } requestedStBox = newStBox; this.stNotify(this); } } } @Override public boolean onTap(float x, float y) { // if(handleTapForPhotos(x,y)) // return true; return handleTapForSelectedArea(x,y); } public void notifyViewNodesChanged() { synchronized (this) { if(requestedStBox != null) { viewUpToDate = false; distanceUpToDate = false; this.stNotify(this); } } } public void notifyPathsChanged() { //TODO 3 technically we should register as a reading thread, but we are actually //being called by the writing thread, so registering the reading thread here would //cause the rwtm to deadlock the thread on itself.. maybe we should make this is a //noop for the writing thread and // use a thread local to identify this? // overlay.sas.rwtm.registerReadingThread(); synchronized (this) { if(requestedStBox != null) { requestedStBox = new AreaPanelSpaceTimeBox(requestedStBox); requestedStBox.pathList = sas.getResultPaths(); viewUpToDate = false; this.stNotify(this); } } // overlay.sas.rwtm.unregisterReadingThread(); } @Override public void startTask(MapController mapController) { this.mapController = mapController; mapData = mapController.addDataLayer("gt_point"); sasDrawer = new SasDrawer(sas,mapController); superThread.addTask(this); } @Override public void onPause() { //this is handled automatically by the super thread manager } @Override public void onResume() { //this is handled automatically by the super thread manager } public void shutdown() { sas.shutdown(); } @Override public boolean onLongPressEnd(float startX, float startY, float endX, float endY) { getApUnitsFromPixels(p1, startX, startY); getApUnitsFromPixels(p2, endX, endY); if (p1.x > p2.x) { int t = p2.x; p2.x = p1.x; p1.x = t; } if (p1.y > p2.y) { int t = p2.y; p2.y = p1.y; p1.y = t; } Area a = new Area(p1.x, p1.y, p2.x, p2.y, getMinDepth(requestedStBox)); sas.addArea(a); sasDrawer.resetToSas(); activity.notifySelectedAreasChanged(true); return true; } private void getApUnitsFromPixels(Point p, float x, float y) { //co; doesn't work when zoomed out all the way for some reason, x value is off //requestedStBox.apUnitsToPixels(); LngLat l = Util.normalizeLngLat(mapController.screenPositionToLngLat(new PointF(x, y))); p.x = AreaPanel.convertLonToX(l.longitude); p.y = AreaPanel.convertLatToY(l.latitude); } @Override public boolean onLongPressMove(float startX, float startY, float endX, float endY) { //if (!selectedAreaAddLock) //PERF we only need to do this once //sas.clearAreas(); int minDepth = getMinDepth(requestedStBox); getApUnitsFromPixels(p1, startX, startY); getApUnitsFromPixels(p2, endX, endY); sasDrawer.setRectangle(p1.x, p1.y, p2.x, p2.y); return true; } private boolean handleTapForSelectedArea(float x, float y) { Point apUnitsPoint = new Point(); getApUnitsFromPixels(apUnitsPoint, x, y); //align to a minDepth boundary for speed when sas computes time through area int minDepth = getMinDepth(requestedStBox); //compute radius in ap units int radius = (int) (Util.convertDpToPixel(prefs.clickDefaultSelectedAreaDp, activity) * requestedStBox.getWidth() / osmMapView.windowWidth / 2); if (!selectedAreaAddLock) sas.clearAreas(); //create an area, rounded to minDepth Area a = new Area(apUnitsPoint.x - radius, apUnitsPoint.y - radius, apUnitsPoint.x + radius, apUnitsPoint.y + radius, minDepth); //make it a perfect square (since we may round in one axis but not another if (a.y2 - a.y1 > a.x2 - a.x1) a.x2 = a.x1 + (a.y2 - a.y1); else a.y2 = a.y1 + (a.x2 - a.x1); //if they actually selected a point if (GTG.cacheCreator.doViewNodesIntersect(a.x1, a.y1, a.x2, a.y2)) { sas.addArea(a); activity.notifySelectedAreasChanged(true); } else if (!selectedAreaAddLock) activity.notifySelectedAreasChanged(false); sasDrawer.resetToSas(); return true; } public static class Preferences implements AndroidPreferenceSet.AndroidPreferences { public float clickDefaultSelectedAreaDp = 30; /** * Maximum time to calculate between redraws */ public long maxDrawCalcTime = 200; /** * The percentage of screen size the size of smallest viewable AreaPanel should be. * This prevents too many points from being drawn when they are too small to see. * and saves on performance. */ public float minPointSizePerc = .01f; /** * The percentage of screen size the size of the biggest viewable point should be. * When displaying points, we sometimes put a larger point as a placeholder while * we are calculating it's components points. If it's too big, it looks really * weird to have it flicker on. */ public float maxPointSizePerc = .15f; /** * The amount of guesswork allowed when determining the time that the stb overlaps * an area panel, in terms of a percentage of leeway of the overlap. This is used * to choose a color for the area panel, so we allow a lot of leeway typically. */ public float timeTreeFuzzinessPerc = .33f; /** * The min radius of the points on the graph */ public float minGpsRadiusDp = 2f; } }