/**
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.timeview;
import java.util.Calendar;
import java.util.TimeZone;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Region.Op;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.rareventure.android.Util;
import com.rareventure.gps2.GTG;
import com.rareventure.gps2.database.TimeZoneTimeRow;
import com.rareventure.gps2.reviewer.map.OsmMapGpsTrailerReviewerMapActivity;
public class TimeView extends View {
private static final String[] MONTHS = new String[] { "Jan", "Feb", "Mar",
"Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
private static final String[] DAYS_OF_WEEK = new String[] { "Sun", "Mon",
"Tue", "Wed", "Thu", "Fri", "Sat" };
private static final float Y_TO_SCREEN = 2f;
private static final double MOVE_X_SCALE = 1;
private static final double MOVE_Y_SCALE = 1;
private static final float SELECTED_AREA_WIDTH_PERC = .70f;
private static final float SELECTED_AREA_HEIGHT_DP = 40;
public static final float YPOS_LINE_STEP = .2f;
private static final int SELECTED_AREA_Y_POS_FROM_BOTTOM_DP = 25;
private static final int MAX_LABEL_LINES = 4;
private TextPaint textPaint;
private FontMetricsInt textPaintFontMetrics;
public static interface Listener {
void notifyTimeViewChange();
void notifyTimeViewReady();
}
private interface Strip {
public abstract void processLabels(DrawerLabelProcessor lp,
int startSec, int endSec, int selectedStartTime,
int selectedEndTime, long nowMs, long nowMidnightMs);
public abstract void processLines(DrawerLineProcessor lp, int startSec,
int endSec);
public abstract int roundTo(int timeSec);
public abstract boolean isTimeAndDate();
}
public class CalendarStrip implements Strip {
protected int calendarId;
protected int step;
private StaticLabels staticLabels = new StaticLabels();
public CalendarStrip(int calendarId, int step) {
super();
this.calendarId = calendarId;
this.step = step;
}
@Override
public void processLabels(DrawerLabelProcessor lp, int startSec,
int endSec, int selectedTimeStart, int selectedTimeEnd,
long nowMs, long nowMidnightMs) {
calendar.setTimeInMillis(startSec * 1000l);
Util.clearCalendarValuesUnder(calendar, calendarId);
// truncate the calendar id value to its step
calendar.set(calendarId,
calendar.get(calendarId) - calendar.get(calendarId) % step);
int time = (int) (calendar.getTimeInMillis() / 1000);
String[] dynamicLabels = new String[3];
lp.process(time, TimeView.getDynamicLabels(dynamicLabels, calendar,
calendarId));
while (time < endSec) {
stepCalendar();
time = (int) (calendar.getTimeInMillis() / 1000);
lp.process(time, TimeView.getDynamicLabels(dynamicLabels,
calendar, calendarId));
}
getStaticLabels(staticLabels, calendar, calendarId,
selectedTimeStart, selectedTimeEnd, nowMs, nowMidnightMs);
lp.processStaticLabels(startSec, endSec, staticLabels);
}
@Override
public void processLines(DrawerLineProcessor lp, int startSec,
int endSec) {
calendar.setTimeInMillis(startSec * 1000l);
Util.clearCalendarValuesUnder(calendar, calendarId);
// truncate the calendar id value to its step
calendar.set(calendarId,
calendar.get(calendarId) - calendar.get(calendarId) % step);
int time = (int) (calendar.getTimeInMillis() / 1000);
lp.process(time);
while (time < endSec) {
stepCalendar();
time = (int) (calendar.getTimeInMillis() / 1000);
lp.process(time);
}
}
protected void stepCalendar() {
calendar.add(calendarId, step);
}
public int roundTo(int timeSec) {
calendar.setTimeInMillis(timeSec * 1000l);
Util.clearCalendarValuesUnder(calendar, calendarId);
int prevTime = (int) (calendar.getTimeInMillis() / 1000);
stepCalendar();
int nextTime = (int) (calendar.getTimeInMillis() / 1000);
if (timeSec - prevTime < nextTime - timeSec)
return prevTime;
return nextTime;
}
@Override
public boolean isTimeAndDate() {
switch (calendarId) {
case Calendar.HOUR:
case Calendar.HOUR_OF_DAY:
case Calendar.MINUTE:
case Calendar.SECOND:
case Calendar.MILLISECOND:
return true;
default:
return false;
}
}
}
// like a calendar strip for days, but will skip 30th and 31st
// and go right to the 1st
public class DayOfMonthStrip extends CalendarStrip {
public DayOfMonthStrip(Calendar calendar, int step) {
super(Calendar.DAY_OF_MONTH, step);
}
@Override
protected void stepCalendar() {
calendar.add(Calendar.DAY_OF_MONTH, step);
if (calendar.get(calendarId) >= 29) {
calendar.add(Calendar.MONTH, 1);
calendar.set(Calendar.DAY_OF_MONTH,
calendar.getActualMinimum(Calendar.DAY_OF_MONTH));
}
}
}
public TimeViewOvalDrawer ovalDrawer;
public class StripData {
private static final float STRIP_LABEL_SCALE = 1.1f;
float yPos;
// this is the min x to time sec at the label of the strip
float xToTimeSec;
Strip strip;
private double w;
public StripData(float xToTimeSec, Strip strip) {
super();
init(xToTimeSec, strip);
}
public StripData(float xToTimeSec, int calendarId, int step) {
init(xToTimeSec, new CalendarStrip(calendarId,
step));
}
public StripData(String biggestText, int timePerStep, int calendarId,
int step) {
init(calcXToTimeSec(biggestText, timePerStep), new CalendarStrip(
calendarId, step));
}
private void init(float xToTimeSec, Strip strip) {
this.xToTimeSec = xToTimeSec;
this.strip = strip;
}
private float calcXToTimeSec(String biggestText, int timePerStep) {
Rect bounds = new Rect();
textPaint.getTextBounds(biggestText, 0, biggestText.length(),
bounds);
return (bounds.width() * STRIP_LABEL_SCALE + STRIP_LABEL_EXTRA_PAD)
/ timePerStep;
}
public StripData(String biggestText, int timePerStep, Strip strip) {
init(calcXToTimeSec(biggestText, timePerStep), strip);
}
public void calcGradient(int yPos, StripData sd2) {
this.yPos = yPos;
w = Math.log(sd2.xToTimeSec / this.xToTimeSec);
}
}
private StripData[] stripData;
/**
* The center of the selected area in the y direction (which is not the
* center of the the screen)
*/
double yPos;
private OsmMapGpsTrailerReviewerMapActivity gtum;
public TimeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public TimeView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TimeView(Context context) {
super(context);
init();
}
public void setActivity(OsmMapGpsTrailerReviewerMapActivity gtum) {
this.gtum = gtum;
}
private RectF tempRect = new RectF();
private float downX;
private Listener listener;
private float lastX;
private long lastDownTime;
private float lastY;
private TimeZoneTimeRow currTimeZone;
private TimeZone localTimeZone = Util.getCurrTimeZone();
private int minTimeSec;
private int maxTimeSec;
private DrawerLabelProcessor drawerLabelProcessor = new DrawerLabelProcessor();
private DrawerLineProcessor drawerLineProcessor = new DrawerLineProcessor();
private Rect selectedAreaDim;
public int selectedTimeStart;
public int selectedTimeEnd;
private int centerTimeSec;
private int yPosSdIndex;
private Rect bounds = new Rect();
protected final float STRIP_LABEL_EXTRA_PAD = Util.convertSpToPixel(10,
getContext());
private Paint minMaxPaint;
private long nowMidnightMs;
/**
* This calendar is updated to the current timezone
*/
private Calendar calendar;
private void init() {
calendar = Calendar.getInstance();
Util.clearCalendarValuesUnder(calendar, Calendar.DATE);
nowMidnightMs = calendar.getTimeInMillis();
minMaxPaint = new Paint();
minMaxPaint.setStrokeWidth(Util.convertDpToPixel(3, getContext()));
minMaxPaint.setColor(Color.LTGRAY);
textPaint = new TextPaint();
textPaint.setTextSize(Util.convertSpToPixel(12, getContext()));
textPaint.setColor(Color.WHITE);
textPaint.setAntiAlias(true);
textPaintFontMetrics = textPaint.getFontMetricsInt();
ovalDrawer = new TimeViewOvalDrawer(getContext());
stripData = new StripData[] {
new StripData("2000", Util.SECONDS_IN_YEAR, Calendar.YEAR, 1),
new StripData("2000", Util.SECONDS_IN_MONTH * 3,
Calendar.MONTH, 3),
new StripData("2000", Util.SECONDS_IN_MONTH, Calendar.MONTH, 1),
new StripData("Aug 30", Util.SECONDS_IN_MONTH / 2,
new DayOfMonthStrip(calendar, 15)),
new StripData("Aug 30", Util.SECONDS_IN_MONTH / 4,
new DayOfMonthStrip(calendar, 7)),
new StripData("Aug 30", Util.SECONDS_IN_DAY * 2,
Calendar.DAY_OF_MONTH, 2),
new StripData("Aug 30", Util.SECONDS_IN_DAY,
Calendar.DAY_OF_MONTH, 1),
new StripData("12:00 AM", 3600 * 12, Calendar.HOUR_OF_DAY, 12),
new StripData("12:00 AM", 3600 * 6, Calendar.HOUR_OF_DAY, 6),
new StripData("12:00 AM", 3600 * 3, Calendar.HOUR_OF_DAY, 3),
new StripData("12:00 AM", 3600, Calendar.HOUR_OF_DAY, 1),
new StripData("12:00 AM", (3600 / 2), Calendar.MINUTE, 30),
new StripData("12:00 AM", (3600 / 4), Calendar.MINUTE, 15),
new StripData("12:00 AM", (3600 / 20), Calendar.MINUTE, 5),
new StripData("12:00 AM", (3600 / 60), Calendar.MINUTE, 1) };
//default ypos
yPos = stripData[6].yPos;
yPosSdIndex = getStripDataIndex(yPos);
centerTimeSec = (int) (System.currentTimeMillis() / 1000);
// setup stripdata
for (int i = 0; i < stripData.length - 1; i++) {
stripData[i].calcGradient(i, stripData[i + 1]);
}
stripData[stripData.length - 1].yPos = stripData.length - 1;
//initialize the current time to a good value
int now = (int) (System.currentTimeMillis()/1000l);
Util.runWhenGetWidthWorks(this, new Runnable()
{
@Override
public void run() {
setupSelectedAreaDim();
listener.notifyTimeViewReady();
}});
}
public static String[] getDynamicLabels(String[] out, Calendar calendar,
int calendarId) {
int value = calendar.get(calendarId);
out[0] = out[1] = "";
switch (calendarId) {
case Calendar.YEAR:
out[1] = String.valueOf(value);
break;
case Calendar.MONTH:
out[1] = MONTHS[value - calendar.getActualMinimum(Calendar.MONTH)];
break;
case Calendar.DATE:
out[0] = MONTHS[calendar.get(Calendar.MONTH)
- calendar.getActualMinimum(Calendar.MONTH)]
+ " "
+ (value - calendar.getActualMinimum(Calendar.DATE) + 1);
out[1] = DAYS_OF_WEEK[calendar.get(Calendar.DAY_OF_WEEK)
- calendar.getActualMinimum(Calendar.DAY_OF_WEEK)];
break;
case Calendar.HOUR_OF_DAY:
out[1] = (value % 12 == 0 ? "12:00" : (value % 12) + ":00 ")
+ (value >= 12 ? "PM" : "AM");
break;
case Calendar.MINUTE:
int hour = calendar.get(Calendar.HOUR_OF_DAY);
out[1] = String.format("%02d:%02d %s", hour % 12 == 0 ? 12
: hour % 12, value, (hour >= 12 ? " PM" : " AM"));
break;
default:
throw new IllegalStateException("what is ?" + calendarId);
}
return out;
}
private static class StaticLabels {
StringBuffer[] out1;
StringBuffer[] out2;
public StaticLabels() {
out1 = new StringBuffer[3];
out2 = new StringBuffer[3];
for (int i = 0; i < 3; i++) {
out1[i] = new StringBuffer();
out2[i] = new StringBuffer();
}
}
public void reset() {
for (int i = 0; i < 3; i++) {
out1[i].delete(0, out1[i].length());
out2[i].delete(0, out2[i].length());
}
}
}
/**
*
* @param out
* an output array of length 3
* @param calendar
* @param calendarId
* @param endSec
* @param startSec
*/
public void getStaticLabels(StaticLabels out, Calendar calendar,
int calendarId, int startSec, int endSec, long nowMs,
long nowMidnightMs) {
boolean atStart = false, atEnd = false;
if (endSec > maxTimeSec) {
endSec = maxTimeSec;
atEnd = true;
}
if (startSec < minTimeSec) {
startSec = minTimeSec;
atStart = true;
}
//sometimes the bar can get completely out of range of the
//selected area. If this happens we want to make sure the
// static labels show "now" rather than "now to 7 minutes ago (end)"
if (startSec > maxTimeSec) {
startSec = maxTimeSec;
}
if (endSec < minTimeSec) {
endSec = minTimeSec;
}
out.reset();
calendar.setTimeInMillis(startSec * 1000l);
switch (calendarId) {
case Calendar.MINUTE:
case Calendar.HOUR_OF_DAY:
int startDate = calendar.get(Calendar.DATE);
out.out1[2].append(DAYS_OF_WEEK[calendar.get(Calendar.DAY_OF_WEEK)
- calendar.getActualMinimum(Calendar.DAY_OF_WEEK)]);
out.out1[1]
.append(MONTHS[calendar.get(Calendar.MONTH)
- calendar.getActualMinimum(Calendar.MONTH)])
.append(" ")
.append(calendar.get(Calendar.DATE)
- calendar.getActualMinimum(Calendar.DATE) + 1);
chooseFromNowDescription(out.out1[0], calendar, nowMs,
nowMidnightMs);
// update calendar to end sec here
calendar.setTimeInMillis(endSec * 1000l);
StringBuffer end = chooseFromNowDescription(null, calendar, nowMs,
nowMidnightMs);
addStartAndEnd(out.out1[0], end, atStart, atEnd);
// since not all days are 24 hours long (dst, leap seconds, etc),
// but we know they definately are under 48 hours long
// we do this check as so
if (endSec > startSec + Util.SECONDS_IN_DAY * 2
|| calendar.get(Calendar.DATE) != startDate) {
out.out1[1].append(" - ");
out.out2[1]
.append(MONTHS[calendar.get(Calendar.MONTH)
- calendar.getActualMinimum(Calendar.MONTH)])
.append(" ")
.append(calendar.get(Calendar.DATE)
- calendar.getActualMinimum(Calendar.DATE) + 1);
out.out2[2].append(DAYS_OF_WEEK[calendar
.get(Calendar.DAY_OF_WEEK)
- calendar.getActualMinimum(Calendar.DAY_OF_WEEK)]);
}
break;
case Calendar.DATE:
case Calendar.MONTH:
case Calendar.YEAR:
chooseFromNowDescription(out.out1[0], calendar, nowMs,
nowMidnightMs);
calendar.setTimeInMillis(endSec * 1000l);
end = chooseFromNowDescription(null, calendar, nowMs,
nowMidnightMs);
addStartAndEnd(out.out1[0], end, atStart, atEnd);
break;
default:
throw new IllegalStateException("what is ?" + calendarId);
}
String tztStr = getTimeZoneStr();
if(tztStr != null)
{
if (out.out2[1].length() > 0)
out.out2[1].append(" ").append(getTimeZoneStr());
else if (out.out1[1].length() > 0)
out.out1[1].append(" ").append(getTimeZoneStr());
else
out.out1[1].append(getTimeZoneStr());
}
}
private String getTimeZoneStr() {
if(currTimeZone != null && currTimeZone.getTimeZone() != null) return currTimeZone.getTimeZone().getDisplayName();
return null;
}
private void addStartAndEnd(StringBuffer sb, CharSequence end, boolean atStart, boolean atEnd) {
// PERF: bla object creation, StringBuffer.equals doesn't work
boolean endSameAsSb = sb.toString().equals(end.toString());
if(atStart && !atEnd && !endSameAsSb)
{
sb.append(" (start) to ").append(end);
}
else {
if(!endSameAsSb)
sb.append(" to ").append(end);
if(minTimeSec == maxTimeSec)
sb.append(" (no data)");
else if (atEnd && atStart) {
sb.append(" (all data)");
} else if (atEnd) {
sb.append(" (end)");
} else if (atStart) //in this case endSameAsSb is true (because we check above), so
//we just add start to the end
{
sb.append(" (start)");
}
}
}
/**
*
* @param out
* @param thenCalendar
* @param nowMs
* @param nowMidnightMs
* today at midnight. We use this because if it's 5 AM, 11:59 PM
* last night is yesterday
* @param atStart
* true if at the end of recorded data
* @param atEnd
* true if at the start of recorded data
* @return
*/
private static StringBuffer chooseFromNowDescription(StringBuffer out,
Calendar thenCalendar, long nowMs, long nowMidnightMs) {
if (out == null)
out = new StringBuffer();
int months = Util.calcDiff(thenCalendar, nowMidnightMs, Calendar.MONTH);
if (months >= 3) {
out.append( MONTHS[thenCalendar.get(Calendar.MONTH)
- thenCalendar.getActualMinimum(Calendar.MONTH)]).append(" ").
append(thenCalendar.get(Calendar.DATE)).append(", ").append(thenCalendar.get(Calendar.YEAR));
return out;
}
if (months >= 2) {
// TODO 3 internationalize
out.append(months).append(" months ago");
return out;
}
int days = Util.calcDiff(thenCalendar, nowMidnightMs, Calendar.DATE) + 1;
int weeks = days / 7;
if (weeks >= 2) {
out.append(weeks).append(" weeks ago");
return out;
}
if (days >= 2) {
out.append(days).append(" days ago");
return out;
}
int hours = Util.calcDiff(thenCalendar, nowMs, Calendar.HOUR_OF_DAY);
int minutes = 0;
if (hours >= 2) {
out.append(hours).append(" hours ago");
return out;
} else if (hours == 1) {
out.append("1 hour, ");
minutes = -60;
}
minutes += Util.calcDiff(thenCalendar, nowMs, Calendar.MINUTE);
if (minutes > 1) {
out.append(minutes).append(" minutes ago");
return out;
} else if (minutes == 1) {
out.append("1 minutes ago");
return out;
}
if (hours == 1) {
out.append("0 minutes ago");
} else
out.append("now");
return out;
}
private static String getIth(int value) {
String ith;
int valueMod10 = value % 10;
if (valueMod10 == 1)
ith = "st";
else if (valueMod10 == 2)
ith = "nd";
else if (valueMod10 == 3)
ith = "rd";
else
ith = "th";
return value + ith;
}
private Canvas canvas;
private class DrawerLabelProcessor {
private int sdIndex;
public void init(int sdIndex) {
this.sdIndex = sdIndex;
}
public void process(int timeSec, String[] dynamicLabels) {
// draw labels between lines
int screenX = convertTimeSecToX(timeSec, yPos, stripData[sdIndex],
stripData[sdIndex]);
int screenY = selectedAreaDim.top;
chooseColor(textPaint, timeSec, timeSec);
for (int i = 1; i >= 0; i--) {
canvas.drawText(dynamicLabels[i], screenX, screenY, textPaint);
screenY -= textPaint.getFontMetricsInt(textPaintFontMetrics);
}
}
public void processStaticLabels(int startTimeSec, int endTimeSec,
StaticLabels staticLabels) {
// draw labels between lines
int screenX = selectedAreaDim.left;
int screenY = selectedAreaDim.top;
textPaint.setColor(Color.WHITE);
int secondColumnX = -1;
for (int i = 2; i >= 0; i--) {
screenY -= textPaint.getFontMetricsInt(textPaintFontMetrics);
String text = staticLabels.out1[i].toString();
canvas.drawText(text, screenX, screenY, textPaint);
if (staticLabels.out2[i].length() != 0) {
// the string manipulation bit is because getTextWidth will
// ignore spaces at the end
// PERF: hack creates a lot of objects, oh well
secondColumnX = Math
.max(getTextWidth(
textPaint,
text.substring(0, text.length() - 2) + " -",
0, text.length())
+ screenX, secondColumnX);
}
}
screenY = selectedAreaDim.top;
for (int i = 2; i >= 0; i--) {
screenY -= textPaint.getFontMetricsInt(textPaintFontMetrics);
if (staticLabels.out2[i].length() != 0)
canvas.drawText(staticLabels.out2[i].toString(),
secondColumnX, screenY, textPaint);
}
}
}
private class DrawerLineProcessor {
StripData sd1;
StripData sd2;
public void process(int timeSec) {
chooseColor(textPaint, timeSec, timeSec);
drawLine(timeSec, textPaint);
}
public void drawLine(int timeSec, Paint paint) {
int screenX = convertTimeSecToXForLabel(timeSec, sd1);
// int screenX = convertTimeSecToX(timeSec, yPos, sd1, sd2);
int screenY = convertYToScreenY(sd1.yPos);
int endScreenX = convertTimeSecToXForLabel(timeSec, sd2);
int endScreenY = convertYToScreenY(sd2.yPos);
int lastScreenX;
int lastScreenY;
for (double y = sd1.yPos + YPOS_LINE_STEP; y < sd2.yPos; y += YPOS_LINE_STEP) {
lastScreenX = screenX;
lastScreenY = screenY;
screenX = convertTimeSecToX(timeSec, y, sd1, sd2);
// screenX = convertTimeSecToX(timeSec, yPos, sd1, sd2);
screenY = convertYToScreenY(y);
canvas.drawLine(lastScreenX, lastScreenY, screenX, screenY,
paint);
}
canvas.drawLine(screenX, screenY, endScreenX, endScreenY, paint);
}
}
private int getTextWidth(TextPaint textPaint, String text, int start,
int end) {
textPaint.getTextBounds(text, start, end, bounds);
return bounds.width();
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
GTG.cacheCreatorLock.registerReadingThread();
try {
// selectedTimeStart =
// roundTo(convertXToTimeSec(selectedAreaDim.left, yPos),
// stripData[yPosSdIndex+1]);
// selectedTimeEnd =
// roundTo(convertXToTimeSec(selectedAreaDim.right, yPos),
// stripData[yPosSdIndex+1]);
// double yStart = yPos - Y_TO_SCREEN/2;
// double yEnd = yPos + Y_TO_SCREEN/2;
this.canvas = canvas;
drawSelectedArea(gtum.gpsTrailerOverlay.earliestOnScreenPointSec,
gtum.gpsTrailerOverlay.latestOnScreenPointSec);
// // if there is no data
// if (minTimeSec == maxTimeSec) {
// // report it
//
// // co: we just want to draw nothing, because it may be that the
// // cache is just destroyed,
// // but there are gps points (like when we do a restore)
// // zODO 3 internationalize
// // String text = "No Data";
// //
// // canvas.drawText(text, (getWidth() - getTextWidth(textPaint,
// // text, 0 , text.length()))/2, getHeight()/2, textPaint);
// return;
// }
int screenMinTs = convertXToTimeSec(0, yPos,
stripData[yPosSdIndex], stripData[yPosSdIndex + 1]);
int screenMaxTs = convertXToTimeSec(getWidth(), yPos,
stripData[yPosSdIndex], stripData[yPosSdIndex + 1]);
drawerLabelProcessor.init(yPosSdIndex);
long nowMs = System.currentTimeMillis();
// TODO 4: doesn't work for DST days, oh well
// if we've crossed the day line, we need to update midnight to the
// next day
if (nowMs - nowMidnightMs > Util.MS_PER_DAY)
nowMidnightMs += Util.MS_PER_DAY;
// draw the strip lines
for (int i = 0; i < stripData.length; i++) {
if (i != 0) {
drawerLineProcessor.sd1 = stripData[i - 1];
drawerLineProcessor.sd2 = stripData[i];
int startTs = convertXToTimeSecForLabel(0, stripData[i - 1]);
int endTs = convertXToTimeSecForLabel(getWidth(),
stripData[i - 1]);
// this draws the individual marker lines
stripData[i - 1].strip.processLines(drawerLineProcessor,
// we use the time from the previous level so we get lines
// that are visible at
// the start but cut off at the end
startTs, endTs);
// draw min/max lines
drawerLineProcessor.drawLine(minTimeSec,
minMaxPaint);
drawerLineProcessor.drawLine(maxTimeSec,
minMaxPaint);
}
}
// draw the strip labels
stripData[yPosSdIndex].strip.processLabels(drawerLabelProcessor,
screenMinTs, screenMaxTs, selectedTimeStart,
selectedTimeEnd, System.currentTimeMillis(), nowMidnightMs);
this.canvas = null;
} finally {
GTG.cacheCreatorLock.unregisterReadingThread();
}
}
private void drawSelectedArea(int earliestOnScreenPointSec,
int latestOnScreenPointSec) {
canvas.drawLine(selectedAreaDim.left, selectedAreaDim.top,
selectedAreaDim.left, selectedAreaDim.bottom,
ovalDrawer.selectedRegionPaint);
canvas.drawLine(selectedAreaDim.right, selectedAreaDim.top,
selectedAreaDim.right, selectedAreaDim.bottom,
ovalDrawer.selectedRegionPaint);
tempRect.left = ovalDrawer.selectedRegionPaint.getStrokeWidth() / 2
+ selectedAreaDim.left;
tempRect.top = 0;
tempRect.right = selectedAreaDim.right
- ovalDrawer.selectedRegionPaint.getStrokeWidth() / 2;
tempRect.bottom = getHeight();
canvas.clipRect(tempRect);
int onScreenPointStartX = convertTimeSecToX(earliestOnScreenPointSec,
yPos, stripData[yPosSdIndex], stripData[yPosSdIndex + 1]);
int onScreenPointEndX = convertTimeSecToX(latestOnScreenPointSec, yPos,
stripData[yPosSdIndex], stripData[yPosSdIndex + 1]);
int selectedAreaCenterY = (selectedAreaDim.top + selectedAreaDim.bottom) / 2;
// draw line to the left side of selector
if (onScreenPointStartX - ovalDrawer.DISPLAYED_POINTS_BAR_RADIUS_PX > selectedAreaDim.left) {
canvas.drawLine(selectedAreaDim.left, selectedAreaCenterY,
onScreenPointStartX
- ovalDrawer.DISPLAYED_POINTS_BAR_RADIUS_PX,
selectedAreaCenterY, ovalDrawer.selectedRegionPaint);
}
// draw line to the right side of selector
if (onScreenPointEndX + ovalDrawer.DISPLAYED_POINTS_BAR_RADIUS_PX < selectedAreaDim.right) {
canvas.drawLine(onScreenPointEndX
+ ovalDrawer.DISPLAYED_POINTS_BAR_RADIUS_PX,
selectedAreaCenterY, selectedAreaDim.right,
selectedAreaCenterY, ovalDrawer.selectedRegionPaint);
}
ovalDrawer.drawOval(canvas, selectedAreaDim, onScreenPointStartX,
onScreenPointEndX);
tempRect.left = 0;
tempRect.top = 0;
tempRect.right = getWidth();
tempRect.bottom = getHeight();
canvas.clipRect(tempRect, Op.REPLACE);
}
private int getStripDataIndex(double currYPos) {
int i;
for (i = 0; i < stripData.length - 2; i++) {
if (stripData[i + 1].yPos >= currYPos)
break;
}
return i;
}
private void chooseColor(Paint paint, int timeSec, int timeSec2) {
paint.setColor(timeSec2 >= selectedTimeStart
&& timeSec < selectedTimeEnd
&& timeSec2 >= minTimeSec
&& timeSec < maxTimeSec ? Color.WHITE
: Color.DKGRAY);
}
private int convertYToScreenY(double yPos) {
return (int) ((yPos - this.yPos) / Y_TO_SCREEN * getHeight() + (selectedAreaDim.top + selectedAreaDim.bottom) / 2);
}
private double convertScreenYToY(float screenY) {
return (screenY - (selectedAreaDim.top + selectedAreaDim.bottom) / 2)
* Y_TO_SCREEN / getHeight() + this.yPos;
}
private int convertTimeSecToX(int timeSec, double yPos, StripData sd1,
StripData sd2) {
return (int) (Math.exp((yPos - sd1.yPos) * sd1.w)
* (timeSec - centerTimeSec) * sd1.xToTimeSec + getWidth() / 2);
}
private int convertTimeSecToXForLabel(int timeSec, StripData sd) {
return (int) (sd.xToTimeSec * (timeSec - centerTimeSec) + getWidth() / 2);
}
private int convertXToTimeSec(float x, double yPos, StripData sd1,
StripData sd2) {
return (int) ((x - getWidth() / 2)
/ (sd1.xToTimeSec * Math.exp((yPos - sd1.yPos) * sd1.w)) + centerTimeSec);
}
private int convertXToTimeSecForLabel(int x, StripData sd) {
return (int) ((x - getWidth() / 2) / sd.xToTimeSec + centerTimeSec);
}
private int convertXToTimeSec(int x, double yPos) {
int i;
for (i = 0; i < stripData.length - 1; i++) {
if (stripData[i + 1].yPos >= yPos)
break;
}
StripData sd1, sd2;
if (i == stripData.length - 1) {
sd1 = sd2 = stripData[i];
} else {
sd1 = stripData[i];
sd2 = stripData[i + 1];
}
return convertXToTimeSec(x, yPos, sd1, sd2);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
GTG.cacheCreatorLock.registerReadingThread();
try {
long time = System.currentTimeMillis();
if (event.getAction() == MotionEvent.ACTION_DOWN) {
lastY = event.getY();
downX = lastX = event.getX();
lastDownTime = time;
return true;
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
double currYPos = Math.min(convertScreenYToY(event.getY()),
stripData[stripData.length - 1].yPos);
if (Math.abs(lastY - event.getY()) > Math.abs(lastX
- event.getX()))
//if we moved vertically
{
double lastYPos = Math.min(convertScreenYToY(lastY),
stripData[stripData.length - 1].yPos);
int xTimeSec = convertXToTimeSec((int) downX, yPos,
stripData[yPosSdIndex], stripData[yPosSdIndex + 1]);
yPos -= (currYPos - lastYPos) * MOVE_Y_SCALE;
if (yPos > stripData[stripData.length - 1].yPos)
yPos = stripData[stripData.length - 1].yPos;
if (yPos < 0)
yPos = 0;
yPosSdIndex = getStripDataIndex(yPos);
//move sideways so we don't scroll wherever the user finger is draped over
int newXTimeSec = convertXToTimeSec(downX, yPos,
stripData[yPosSdIndex], stripData[yPosSdIndex + 1]);
centerTimeSec += xTimeSec - newXTimeSec;
} else {
int lastTime = convertXToTimeSec((int) lastX, yPos,
stripData[yPosSdIndex], stripData[yPosSdIndex + 1]);
int currTime = convertXToTimeSec((int) event.getX(), yPos,
stripData[yPosSdIndex], stripData[yPosSdIndex + 1]);
this.centerTimeSec -= (int) ((currTime - lastTime) * MOVE_X_SCALE);
// if (centerTimeSec < minTimeSec)
// centerTimeSec = minTimeSec;
// if (centerTimeSec > maxTimeSec)
// centerTimeSec = maxTimeSec;
}
int startTime = convertXToTimeSec(selectedAreaDim.left,
yPos);
int endTime = convertXToTimeSec(selectedAreaDim.right, yPos);
if (endTime < minTimeSec)
centerTimeSec += minTimeSec - endTime;
if (startTime > maxTimeSec)
centerTimeSec -= startTime - maxTimeSec;
lastX = event.getX();
lastY = event.getY();
invalidate();
selectedTimeStart = convertXToTimeSec(selectedAreaDim.left,
yPos);
selectedTimeEnd = convertXToTimeSec(selectedAreaDim.right, yPos);
// selectedTimeStart =
// roundTo(convertXToTimeSec(selectedAreaDim.left, yPos),
// stripData[yPosSdIndex+1]);
// selectedTimeEnd =
// roundTo(convertXToTimeSec(selectedAreaDim.right, yPos),
// stripData[yPosSdIndex+1]);
// Log.d(GTG.TAG,
// String.format("move currYPos: %10.5f eX: %10.5f eY: %10.5f",
// currYPos, event.getX(), event.getY()));
if (listener != null) {
listener.notifyTimeViewChange();
}
return true;
}
// else if(event.getAction() == MotionEvent.ACTION_UP)
// {
// Log.d("GPS","action up");
// if(momentumPixelsPerMs != 0)
// {
// lastMomentumUpdatedTimeMs = System.currentTimeMillis();
// timer.start(prefs.dialAnimationDelayMs);
// }
//
//
// return true;
// }
return false;
} finally {
GTG.cacheCreatorLock.unregisterReadingThread();
}
}
private int getRoundedStripDataIndex(double currYPos) {
int i;
for (i = 0; i < stripData.length - 2; i++) {
if ((stripData[i + 1].yPos + stripData[i].yPos) / 2 >= currYPos)
break;
}
return i;
}
private int roundTo(int timeSec, StripData stripData) {
return stripData.strip.roundTo(timeSec);
}
public void setListener(Listener listener) {
this.listener = listener;
}
public void setMinMaxTime(int minTimeSec,
int maxTimeSec)
{
this.minTimeSec = minTimeSec;
this.maxTimeSec = maxTimeSec;
// redraw
invalidate();
}
public void setSelectedStartAndEndTime(int startTimeSec, int endTimeSec)
{
//FIXES bug #26
//Somehow the prefs.currTimePeriodSec gets set to zero, which causes yPos to
//be infinite. Since its saved that way, crashes on boot
//60 is, of course, meaningless, but I don't want to bother
//figuring out a more sane value for such a one-off case.
if(endTimeSec - startTimeSec < 60)
{
endTimeSec = startTimeSec + 60;
}
centerTimeSec = startTimeSec / 2 + endTimeSec / 2;
// find the correct ypos
int i;
// first find the strip data that contains it
for (i = 0; i < stripData.length - 2; i++) {
if (convertXToTimeSecForLabel(
selectedAreaDim.left - getWidth() / 2, stripData[i + 1]) > startTimeSec)
break;
}
yPos = Math.log((getWidth() / 2 - selectedAreaDim.left)
/ ((endTimeSec - centerTimeSec) * stripData[i].xToTimeSec))
/ stripData[i].w + stripData[i].yPos;
yPosSdIndex = getStripDataIndex(yPos);
if(Double.isInfinite(yPos) || Double.isNaN(yPos))
throw new IllegalStateException("yPos is bad: "+yPos);
selectedTimeStart = startTimeSec;
selectedTimeEnd = endTimeSec;
// redraw
invalidate();
if (listener != null) {
listener.notifyTimeViewChange();
}
}
public int getMinSelectableTimeSec() {
return convertXToTimeSecForLabel(selectedAreaDim.right,
stripData[stripData.length - 1])
- convertXToTimeSecForLabel(selectedAreaDim.left,
stripData[stripData.length - 1]);
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int minHeight = (int) Util.convertDpToPixel(SELECTED_AREA_HEIGHT_DP
+ SELECTED_AREA_Y_POS_FROM_BOTTOM_DP, getContext())
+ textPaint.getFontMetricsInt(textPaintFontMetrics)
* MAX_LABEL_LINES;
setMeasuredDimension(MeasureSpec.getSize(widthSpec),
Util.chooseAtLeastForOnMeasure(minHeight, heightSpec));
}
private void setupSelectedAreaDim() {
if (selectedAreaDim == null) {
int width = (int) (getWidth() * SELECTED_AREA_WIDTH_PERC);
int height = (int) Util.convertDpToPixel(SELECTED_AREA_HEIGHT_DP,
getContext());
int centerY = (int) (getHeight() - Util.convertDpToPixel(
SELECTED_AREA_HEIGHT_DP / 2
+ SELECTED_AREA_Y_POS_FROM_BOTTOM_DP, getContext()));
selectedAreaDim = new Rect((getWidth() - width) / 2, centerY
- height / 2, (getWidth() + width) / 2, centerY + height
/ 2);
if (getWidth() == 0)
throw new IllegalStateException(
"I hate you, getWidth()!!!!!!!!!!!!!");
}
}
/**
* Must be called from ui thread
*/
public void updateTimeZone(TimeZoneTimeRow newTimeZone) {
//if its the current time zone, don't display anything
if(newTimeZone != null && newTimeZone.isLocalTimeZone())
newTimeZone = null;
if(currTimeZone != newTimeZone)
{
currTimeZone = newTimeZone;
if(currTimeZone == null || currTimeZone.getTimeZone() == null)
calendar.setTimeZone(localTimeZone );
else
calendar.setTimeZone(currTimeZone.getTimeZone());
invalidate();
}
}
}