package uk.org.smithfamily.mslogger.dashboards;
import java.util.*;
import org.metalev.multitouch.controller.*;
import org.metalev.multitouch.controller.MultiTouchController.MultiTouchObjectCanvas;
import org.metalev.multitouch.controller.MultiTouchController.PointInfo;
import org.metalev.multitouch.controller.MultiTouchController.PositionAndScale;
import uk.org.smithfamily.mslogger.DataManager;
import uk.org.smithfamily.mslogger.dialog.EditIndicatorDialog;
import uk.org.smithfamily.mslogger.dialog.EditIndicatorDialog.OnEditIndicatorResult;
import uk.org.smithfamily.mslogger.widgets.Indicator;
import uk.org.smithfamily.mslogger.widgets.Location;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.view.*;
/**
*
*/
public class DashboardView extends SurfaceView implements Observer, SurfaceHolder.Callback, MultiTouchObjectCanvas<DashboardElement>
{
private final int position;
private final Context context;
private final List<DashboardElement> elements;
private DashboardThread thread = null;
private final int MAX_FPS = 50;
private final int DELAY_PER_FRAME = 1000 / MAX_FPS;
private int measuredHeight;
private int measuredWidth;
private final DashboardViewPager parentPager;
private final MultiTouchController<DashboardElement> multiTouchController;
private DashboardElement editedItem;
private final GestureDetector gestureScanner;
private final Dashboard dashboard;
/**
*
*/
class GestureProcessor extends GestureDetector.SimpleOnGestureListener
{
/**
*
* @param e
*/
@Override
public boolean onDoubleTap(final MotionEvent e)
{
final float px = e.getX();
final float py = e.getY();
final DashboardElement element = getElementAtCoOrds(px, py);
// Double tap on an empty section of the dashboard, we're going to trigger the creation of a new element
if (element == null)
{
addNewElement(px, py);
}
// Double tap on an existing indicator on the dash, we're going to trigger the edit dialog for the element
else
{
editElement(element);
}
return super.onDoubleTap(e);
}
}
/**
*
* @param context
* @param position
* @param parent
*/
public DashboardView(final Context context, final int position, final DashboardViewPager parent, final Dashboard dashboard)
{
super(context);
this.context = context;
this.position = position;
this.parentPager = parent;
this.dashboard = dashboard;
elements = new ArrayList<DashboardElement>();
getHolder().addCallback(this);
// DataManager will ping when the ECU has finished a cycle
DataManager.getInstance().addObserver(this);
multiTouchController = new MultiTouchController<DashboardElement>(this);
gestureScanner = new GestureDetector(context, new GestureProcessor());
}
/**
* Edit an element from the DashboardView
*
* @param element DashboardElement to edit
*/
public void editElement(final DashboardElement element)
{
final Indicator indicator = element.getIndicator();
final EditIndicatorDialog dialog = new EditIndicatorDialog(context, indicator);
dialog.show();
dialog.setDialogResult(new OnEditIndicatorResult()
{
@Override
public void finish(final Indicator indicator, final boolean toRemove)
{
// User asked to delete this indicator from Remove button on the edit indicator dialog
if (toRemove)
{
synchronized (elements)
{
elements.remove(element);
}
boolean landscape = false;
// Figure out in which orientation the device is
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
{
landscape = true;
}
// Remove indicator from current dashboard
dashboard.remove(indicator, landscape);
}
else
{
element.checkPainterMatchesIndicator();
element.scaleToParent(getWidth(), getHeight());
}
// Let the dash know it's dirty
if (thread != null)
{
thread.invalidate();
}
}
});
}
/**
* Add a new element to the DashboardView
*
* @param px X where to add the element
* @param py Y where to add the element
*/
public void addNewElement(final float px, final float py)
{
// Create new Indicator
final Indicator indicator = new Indicator();
final DashboardView dv = this;
// Open dialog and bind dialog result event
final EditIndicatorDialog dialog = new EditIndicatorDialog(context, indicator);
dialog.show();
dialog.setDialogResult(new OnEditIndicatorResult()
{
// Back from the dialog, lets add the element
@Override
public void finish(final Indicator indicator, final boolean toRemove)
{
// Set position and scale for the element
double left = px / dv.getWidth();
double top = py / dv.getHeight();
double right = left + 0.3; // 30% of the screen for the width
double bottom = top + 0.3; // 30% of the screen for the height
// Would go outside of screen, going to move left and right some
if (right > 1.0)
{
final double diff = right - 1.0;
right = 1.0;
left -= diff;
}
// Would go outside of screen, going to move top and bottom some
if (bottom > 1.0)
{
final double diff = bottom - 1.0;
bottom = 1.0;
top -= diff;
}
// Set location for the new indicator
indicator.setLocation(new Location(left, top, right, bottom));
boolean landscape = false;
// Figure out in which orientation the device is
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
{
landscape = true;
}
// Add indicator to current dashboard
dashboard.add(indicator, landscape);
// Create dashboard element
final DashboardElement element = new DashboardElement(context, indicator, dv);
// Add dashboard element to dashboard view
synchronized (elements)
{
elements.add(element);
}
// Let the dash know it's dirty
if (thread != null)
{
thread.invalidate();
}
}
});
}
/**
*
* @param holder
* @param format
* @param width
* @param height
*/
@Override
public void surfaceChanged(final SurfaceHolder holder, final int format, final int width, final int height)
{
}
/**
*
* @param holder
*/
@Override
public void surfaceCreated(final SurfaceHolder holder)
{
thread = new DashboardThread(getHolder(), context, this);
thread.setRunning(true);
thread.start();
}
/**
*
* @param holder
*/
@Override
public void surfaceDestroyed(final SurfaceHolder holder)
{
if (thread != null)
{
boolean retry = true;
thread.setRunning(false);
while (retry)
{
try
{
thread.interrupt();
thread.invalidate();
thread.join();
retry = false;
}
catch (final InterruptedException e)
{
// we will try it again and again...
}
}
thread = null;
}
}
/**
*
* @param d
*/
public void setDashboard(final Dashboard d)
{
synchronized (elements)
{
elements.clear();
List<Indicator> indicatorsFromDash;
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
{
indicatorsFromDash = d.getLandscape();
}
else
{
indicatorsFromDash = d.getPortrait();
}
for (final Indicator i : indicatorsFromDash)
{
elements.add(new DashboardElement(context, i, this));
}
}
}
/**
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)
{
measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
}
/**
*
* @param observable
* @param data
*/
@Override
public void update(final Observable observable, final Object data)
{
// Delegate the update
if (thread != null)
{
thread.update(observable, data);
}
}
/**
*
*/
class DashboardThread extends Thread implements Observer
{
private volatile boolean running = false;
private final Context context;
private final SurfaceHolder holder;
@SuppressWarnings("unused")
private final Resources resources;
private final Object updateLock = new Object();
private final DashboardView parent;
private volatile boolean isDirty;
private long lastUpdate = System.currentTimeMillis();
/**
*
* @param holder
* @param context
* @param parent
*/
public DashboardThread(final SurfaceHolder holder, final Context context, final DashboardView parent)
{
this.holder = holder;
this.context = context;
this.parent = parent;
this.resources = context.getResources();
this.setName("DashboardThread" + parent.position);
}
/**
*
* @param r
*/
public void setRunning(final boolean r)
{
running = r;
isDirty = r;
}
/**
*
*/
@Override
public void run()
{
Canvas c;
while (running)
{
// Only bother updating if this is the displayed page
while (running && isDirty && (parentPager.getCurrentItem() == position))
{
c = null;
try
{
if (running)
{
c = holder.lockCanvas();
if ((c != null) && running)
{
// Clear the background
c.drawARGB(255, 0, 0, 0);
drawIndicators(c);
}
}
}
finally
{
if (c != null)
{
holder.unlockCanvasAndPost(c);
}
}
final long now = System.currentTimeMillis();
final long delay = now - lastUpdate;
if (delay < DELAY_PER_FRAME)
{
// We're running faster than the desired framerate, so throttle back
final long inducedDelay = DELAY_PER_FRAME - delay;
try
{
Thread.sleep(inducedDelay);
}
catch (final InterruptedException e)
{
// Swallow
}
}
lastUpdate = System.currentTimeMillis();
}
try
{
synchronized (updateLock)
{
if (running)
{
updateLock.wait();
}
isDirty = true;
}
}
catch (final InterruptedException e)
{
// Swallow
}
}
}
/**
*
* @param observable
* @param data
*/
@Override
public void update(final Observable observable, final Object data)
{
synchronized (elements)
{
for (final DashboardElement i : elements)
{
i.setTargetValue();
}
}
invalidate();
}
public void invalidate()
{
synchronized (updateLock)
{
isDirty = true;
updateLock.notifyAll();
}
}
/**
*
* @param c
*/
public void drawIndicators(final Canvas c)
{
isDirty = false;
boolean indicatorDirty = false;
synchronized (elements)
{
for (final DashboardElement i : elements)
{
indicatorDirty |= i.updateAnimation();
i.renderFrame(c);
isDirty |= indicatorDirty;
}
}
}
}
/**
* Pass touch events to the MT controller if the gestureScanner doesn't like it
*
* @param event
*/
@Override
public boolean onTouchEvent(final MotionEvent event)
{
if (gestureScanner.onTouchEvent(event))
{
return true;
}
else
{
return multiTouchController.onTouchEvent(event);
}
}
/**
*
* @param touchPoint
*/
@Override
public DashboardElement getDraggableObjectAtPoint(final PointInfo touchPoint)
{
final float px = touchPoint.getX();
final float py = touchPoint.getY();
return getElementAtCoOrds(px, py);
}
/**
*
* @param px
* @param py
* @return
*/
private DashboardElement getElementAtCoOrds(final float px, final float py)
{
DashboardElement found = null;
synchronized (elements)
{
for (final DashboardElement i : elements)
{
if (i.contains(px, py))
{
found = i;
}
}
if (found != null)
{
// This little bit of trickery pushes the touched element to the end of the list,
// which in turn makes it last to be drawn, so it gets pulled to the front.
elements.remove(found);
elements.add(found);
}
}
return found;
}
/**
*
* @param e
* @param objPosAndScaleOut
*/
@Override
public void getPositionAndScale(final DashboardElement e, final PositionAndScale objPosAndScaleOut)
{
final boolean isotropic = e.getPainter().isIsotropic();
final float centreX = e.getCentreX();
final float centreY = e.getCentreY();
final float scale = e.getScale();
final float scaleX = e.getScaleX();
final float scaleY = e.getScaleY();
final float angle = (float) e.getAngle();
objPosAndScaleOut.set(centreX, centreY, true, scale, !isotropic, scaleX, scaleY, true, angle);
}
/**
*
* @param obj
* @param newObjPosAndScale
* @param touchPoint
*/
@Override
public boolean setPositionAndScale(final DashboardElement obj, final PositionAndScale newObjPosAndScale, final PointInfo touchPoint)
{
final boolean b = obj.setPos(newObjPosAndScale);
if (b)
{
thread.invalidate();
}
return b;
}
/**
*
* @param obj
* @param touchPoint
*/
@Override
public void selectObject(final DashboardElement obj, final PointInfo touchPoint)
{
if (obj != null)
{
if (editedItem == null)
{
editedItem = obj;
editedItem.editStart();
}
}
else
{
if (editedItem != null)
{
editedItem.editFinished();
editedItem = null;
}
}
}
/**
*
*
* @param w Width
* @param h Height
* @param oldw Old width
* @param oldh Old height
*/
@Override
protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
synchronized (elements)
{
for (final DashboardElement i : elements)
{
i.scaleToParent(w, h);
}
}
}
}