/**
AirCasting - Share your Air!
Copyright (C) 2011-2012 HabitatMap, Inc.
This program 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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
You can contact the authors by email at <info@habitatmap.org>
*/
package pl.llp.aircasting.view;
import android.view.MotionEvent;
import pl.llp.aircasting.R;
import pl.llp.aircasting.activity.AirCastingActivity;
import pl.llp.aircasting.android.Logger;
import pl.llp.aircasting.event.ui.TapEvent;
import pl.llp.aircasting.helper.ResourceHelper;
import pl.llp.aircasting.helper.SettingsHelper;
import pl.llp.aircasting.model.Measurement;
import pl.llp.aircasting.model.Note;
import pl.llp.aircasting.model.Sensor;
import pl.llp.aircasting.model.internal.MeasurementLevel;
import pl.llp.aircasting.sensor.ThresholdsHolder;
import pl.llp.aircasting.util.Search;
import pl.llp.aircasting.view.presenter.MeasurementAggregator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import com.google.common.base.Stopwatch;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import static com.google.common.collect.Iterables.getLast;
import static com.google.common.collect.Lists.newArrayList;
import static pl.llp.aircasting.util.DrawableTransformer.centerBottomAt;
import static pl.llp.aircasting.util.Search.binarySearch;
public class NoisePlot extends View
{
public static final int OPAQUE = 255;
private static final double CLICK_RADIUS = 50;
private AirCastingActivity activity;
private SettingsHelper settingsHelper;
private ThresholdsHolder thresholds;
private ArrayList<Measurement> measurements = new ArrayList<Measurement>();
private ArrayList<Note> notes;
private ResourceHelper resourceHelper;
private int bottom;
private int top;
private Sensor sensor;
MeasurementAggregator aggregator = new MeasurementAggregator();
@SuppressWarnings("UnusedDeclaration")
public NoisePlot(Context context) {
super(context);
}
@SuppressWarnings("UnusedDeclaration")
public NoisePlot(Context context, AttributeSet attrs) {
super(context, attrs);
}
@SuppressWarnings("UnusedDeclaration")
public NoisePlot(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void initialize(AirCastingActivity activity, SettingsHelper settingsHelper, ThresholdsHolder thresholds, ResourceHelper resourceHelper) {
this.activity = activity;
this.settingsHelper = settingsHelper;
this.thresholds = thresholds;
this.resourceHelper = resourceHelper;
}
@SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
public void update(Sensor sensor, ArrayList<Measurement> measurements, ArrayList<Note> notes) {
this.measurements = measurements;
this.notes = notes;
this.sensor = sensor;
invalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
Stopwatch stopwatch = new Stopwatch().start();
bottom = thresholds.getValue(sensor, MeasurementLevel.VERY_LOW);
top = thresholds.getValue(sensor, MeasurementLevel.VERY_HIGH);
Paint paint = new Paint();
drawBackground(canvas, paint);
if (!measurements.isEmpty()) {
measurements = aggregator.smoothenSamplesToReduceCount(newArrayList(measurements), 1000);
Path path = new Path();
float lastY = project(measurements.get(0).getValue());
path.moveTo(0, lastY);
// Avoid concurrent modification
for (int i = 1; i < measurements.size(); i++) {
Measurement measurement = measurements.get(i);
Point place = place(measurement);
path.lineTo(place.x, place.y);
}
Logger.logGraphPerformance("onDraw to path creation took " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
initializePaint(paint);
canvas.drawPath(path, paint);
Logger.logGraphPerformance("onDraw to path draw took " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
for (Note note : notes) {
drawNote(canvas, note);
}
if(settingsHelper.showGraphMetadata())
{
String message = "[" + measurements.size() + "] pts";
String message2 = "drawing took " + stopwatch.elapsed(TimeUnit.MILLISECONDS);
long textSize = getResources().getDimensionPixelSize(R.dimen.debugFontSize);
Paint textPaint = new Paint();
textPaint.setColor(Color.WHITE);
textPaint.setAlpha(OPAQUE);
textPaint.setAntiAlias(true);
textPaint.setTextSize(textSize);
float textWidth = Math.max(textPaint.measureText(message), textPaint.measureText(message2));
canvas.drawText(message, getWidth() - textWidth - 5, getHeight() - textSize -5, textPaint);
canvas.drawText(message2, getWidth() - textWidth - 5, getHeight() - 5, textPaint);
}
}
}
private Point place(Measurement measurement) {
long time = measurement.getTime().getTime();
float span = lastTime() - firstTime();
float place = time - firstTime();
int x = (int) (getWidth() * (place / span));
double value = measurement.getValue();
int y = project(value);
return new Point(x, y);
}
private void drawNote(Canvas canvas, Note note) {
if (!inRange(note)) return;
Point place = place(note);
Drawable noteArrow = resourceHelper.getNoteArrow();
centerBottomAt(noteArrow, place);
noteArrow.draw(canvas);
}
private Point place(Note note) {
Measurement measurement = findClosestMeasurement(note);
return place(measurement);
}
private boolean inRange(Note note) {
long noteTime = note.getDate().getTime();
return noteTime >= firstTime() && noteTime <= lastTime();
}
private long lastTime() {
return getLast(measurements).getTime().getTime();
}
private long firstTime() {
return measurements.get(0).getTime().getTime();
}
private Measurement findClosestMeasurement(final Note note) {
int index = binarySearch(measurements, new Search.Visitor<Measurement>() {
@Override
public int compareTo(Measurement value) {
return note.getDate().compareTo(value.getTime());
}
});
return measurements.get(index);
}
private void initializePaint(Paint paint)
{
paint.setColor(Color.WHITE);
paint.setAlpha(OPAQUE);
paint.setStrokeWidth(3);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setAntiAlias(true);
}
private void drawBackground(Canvas canvas, Paint paint) {
// Make the NoisePlot play nicely with Layout preview
if (resourceHelper == null || settingsHelper == null) return;
paint.setStrokeWidth(0);
paint.setStyle(Paint.Style.FILL);
int lastThreshold = top;
for (MeasurementLevel measurementLevel : new MeasurementLevel[]{MeasurementLevel.HIGH, MeasurementLevel.MID, MeasurementLevel.LOW}) {
paint.setColor(resourceHelper.getGraphColor(measurementLevel));
int threshold = settingsHelper.getThreshold(sensor, measurementLevel);
canvas.drawRect(0, project(lastThreshold), getWidth(), project(threshold), paint);
lastThreshold = threshold;
}
paint.setColor(resourceHelper.getGraphColor(MeasurementLevel.VERY_LOW));
canvas.drawRect(0, project(lastThreshold), getWidth(), getHeight(), paint);
}
private int project(double value) {
return (int) (getHeight() * (top - value) / (top - bottom));
}
public boolean onTap(MotionEvent event) {
for (int i = 0; i < notes.size(); i++) {
Note note = notes.get(i);
Point place = place(note);
if (inRange(note) && isClose(event, place)) {
activity.noteClicked(i);
return true;
}
}
return false;
}
private boolean isClose(MotionEvent event, Point place) {
float x = event.getX() - place.x;
float y = event.getY() - place.y;
return Math.sqrt(x * x + y * y) < CLICK_RADIUS;
}
}