/* Copyright (C) 2013, TecVis, support@tecvis.co.uk This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation as version 2.1 of the License. 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.airs.visualisations; import java.util.Calendar; import java.util.Locale; import com.airs.AIRS_local; import com.airs.R; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.DatePickerDialog; import android.app.TimePickerDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.format.Time; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.Display; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.Window; import android.widget.Button; import android.widget.DatePicker; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.TimePicker; import android.widget.Toast; import android.widget.ZoomButton; /** * Class to implement a timeline view for all sensors that support it. This activity is started from the {@link com.airs.platform.History} class after an item supporting timeline has been clicked on by the user * */ public class TimelineActivity extends Activity implements OnTouchListener, OnClickListener, TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener { // handler variables private static final int FINISH_ACTIVITY = 1; private static final int PUSH_VALUES = 2; // offset for full day timestamp private static final long FULL_DAY = 1000*60*60*24; // milliseconds per day // Layout Views private TextView mTitle; private TextView minX, maxX, minY, maxY; private TimelineView DisplayView; private ProgressBar progressbar; private ZoomButton zoomIn, zoomOut; private Bundle bundle; private float history_f[]; private long time[]; private int first_values; private int repeatInterval, repeatJump; private long minTime = Long.MAX_VALUE; private long maxTime = Long.MIN_VALUE; private long startedTime, endTime, windowTime, minReadingTime, maxWindowTime; private int currentIndex = 0, valuesShown; private int currentZoom = 0, maxZoom = 6; private float min = Float.MAX_VALUE; private float max = Float.MIN_VALUE; private boolean showGrid; private boolean showAverage; private boolean setMax; private float averageValue; private String Symbol; // database variables private SQLiteDatabase airs_storage; private Cursor values = null; private AsyncTask<String, Long, Long> task; /** Called when the activity is first created. * @param savedInstanceState a Bundle of the saved state, according to Android lifecycle model */ @Override public void onCreate(Bundle savedInstanceState) { String title; Intent intent = getIntent(); // Set up the window layout super.onCreate(savedInstanceState); // get activity parameters bundle = intent.getExtras(); Symbol = bundle.getString("com.airs.Symbol"); // get symbol // get time of midnight Calendar cal = Calendar.getInstance(Locale.getDefault()); cal.set(Calendar.HOUR, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); cal.set(Calendar.AM_PM, Calendar.AM); startedTime = cal.getTimeInMillis(); // timestamp of start of visualisation endTime = startedTime + FULL_DAY; // end time windowTime = FULL_DAY; // start with full timewindow currentZoom = 0; // show full zoom currentIndex = 0; // now open database airs_storage = AIRS_local.airs_storage; if (airs_storage != null) { // get preferences repeatInterval = 500; repeatJump = 25; showGrid = true; showAverage = true; // set window title requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); setContentView(R.layout.timelineview); getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.timeline_title); // get window title fields mTitle = (TextView) findViewById(R.id.timeline_title_text); // set title of window to string with sensor description title = bundle.getString("com.airs.Title"); if (title != null) mTitle.setText(title); else mTitle.setText("-"); // get Desktop dimensions Display display = getWindowManager().getDefaultDisplay(); int width = display.getWidth(); int height = display.getHeight(); // set dialog dimensions if (width*9/16 > height) getWindow().setLayout(height*16/9, height); else getWindow().setLayout(width, width*9/16); // get axis text fields minX = (TextView) findViewById(R.id.timeline_minx); maxX = (TextView) findViewById(R.id.timeline_maxx); minY = (TextView) findViewById(R.id.timeline_miny); maxY = (TextView) findViewById(R.id.timeline_maxy); // get zoom buttons zoomIn = (ZoomButton) findViewById(R.id.timeline_zoomIn); zoomIn.setOnClickListener(this); zoomIn.setEnabled(true); zoomOut = (ZoomButton) findViewById(R.id.timeline_zoomOut); zoomOut.setOnClickListener(this); zoomOut.setEnabled(false); // set listener for button clicks Button button = (Button) findViewById(R.id.timeline_backward); button.setOnTouchListener(this); button = (Button) findViewById(R.id.timeline_forward); button.setOnTouchListener(this); ImageView select = (ImageView) findViewById(R.id.timeline_select_maxx); select.setOnClickListener(this); select = (ImageView) findViewById(R.id.timeline_select_minx); select.setOnClickListener(this); // set timeline view DisplayView = (TimelineView) findViewById(R.id.surfaceMeasure); DisplayView.invalidate(); // get progress bar progressbar = (ProgressBar) findViewById(R.id.timeline_progress); // now draw markers task = new GatherTask(); task.execute(Symbol); } else finish(); } /** Called when the activity is resumed. */ @Override public void onResume() { super.onResume(); } /** Called when the activity is paused. */ @Override public synchronized void onPause() { super.onPause(); } /** Called when the activity is stopped. */ @Override public void onStop() { super.onStop(); } /** Called when the activity is destroyed. */ @Override public void onDestroy() { super.onDestroy(); if (task != null) task.cancel(true); // free DB resources if (values != null) values.close(); } /** Called when the configuration of the activity has changed. * @param newConfig new configuration after change */ @Override public void onConfigurationChanged(Configuration newConfig) { //ignore orientation change super.onConfigurationChanged(newConfig); // get Desktop dimensions Display display = getWindowManager().getDefaultDisplay(); int width = display.getWidth(); int height = display.getHeight(); // set dialog dimensions if (width*9/16 > height) getWindow().setLayout(height*16/9, height); else getWindow().setLayout(width, width*9/16); // now push values into display Message push_msg = mHandler.obtainMessage(PUSH_VALUES); mHandler.sendMessage(push_msg); } /** Called when the Options menu is opened * @param menu Reference to the {@link android.view.Menu} */ @Override public boolean onPrepareOptionsMenu(Menu menu) { MenuInflater inflater; menu.clear(); inflater = getMenuInflater(); inflater.inflate(R.menu.options_about, menu); return true; } /** Called when a button has been clicked on by the user * @param v Reference to the {@link android.view.View} of the button */ @Override public void onClick(View v) { TimePickerDialog timepicker; Time timestamp = new Time(); switch (v.getId()) { case R.id.timeline_select_minx: // setting minimum time? // indicate that min time is set timestamp.set(minTime); setMax = false; // create time picker dialog and show timepicker = new TimePickerDialog(this, this, timestamp.hour, timestamp.minute, true); timepicker.setTitle(R.string.Timeline_Viewer3); timepicker.show(); return; case R.id.timeline_select_maxx: // setting maximum time? // indicate that max time is set timestamp.set(maxTime); setMax = true; // create time picker dialog and show timepicker = new TimePickerDialog(this, this, timestamp.hour, timestamp.minute, true); timepicker.setTitle(R.string.Timeline_Viewer4); timepicker.show(); return; case R.id.timeline_zoomIn: currentZoom++; if (currentZoom>maxZoom) { zoomIn.setEnabled(false); currentZoom--; } zoomOut.setEnabled(true); break; case R.id.timeline_zoomOut: currentZoom--; if (currentZoom<0) { zoomOut.setEnabled(false); currentZoom++; } zoomIn.setEnabled(true); break; } // select zoom data based on level now switch(currentZoom) { case 0: windowTime = maxWindowTime; currentIndex = 0; break; case 1: windowTime = 6 * 3600 * 1000; break; case 2: windowTime = 3 * 3600 * 1000; break; case 3: windowTime = 1 * 3600 * 1000; break; case 4: windowTime = 30 * 60 * 1000; break; case 5: windowTime = 10 * 60 * 1000; break; case 6: windowTime = 5 * 60 * 1000; break; default: break; } progressTimeline(0); } /** * Called when time is set in {@link android.widget.TimePicker} * @param view Reference to {@link android.widget.TimePicker} view * @param hourOfDay hour of day being chosen * @param minute minute of hour being chosen */ public void onTimeSet (TimePicker view, int hourOfDay, int minute) { Calendar cal = Calendar.getInstance(); int i; long chosen_mills; boolean redraw = false; // set to today's min time cal.setTimeInMillis(minTime); // now set hour and minutes cal.set(Calendar.HOUR_OF_DAY, hourOfDay); cal.set(Calendar.MINUTE, minute); // get milliseconds chosen_mills = cal.getTimeInMillis(); // set minimum time if (setMax == false) { // valid time set? if (chosen_mills>=time[0] && chosen_mills<time[first_values-1]) { for (i=0;i<first_values && time[i] <= chosen_mills;i++) ; currentIndex = i; windowTime = maxTime - time[currentIndex]; redraw = true; } } else { // valid time set? if (chosen_mills>time[0] && chosen_mills<=time[first_values-1]) { for (i=first_values-1;i>=0 && time[i] >= chosen_mills;i--) ; windowTime = time[i] - time[currentIndex]; redraw = true; } } // force redraw? if (redraw == true) mHandler.sendMessage(mHandler.obtainMessage(PUSH_VALUES)); } /** * Called when date is set in {@link android.widget.DatePicker} * @param view Reference to {@link android.widget.DatePicker} view * @param year year being chosen * @param monthOfYear month of the year being chosen * @param dayOfMonth day of the month being chosen */ public void onDateSet (DatePicker view, int year, int monthOfYear, int dayOfMonth) { Calendar cal = Calendar.getInstance(); int i; long chosen_mills; boolean redraw = false; // set to today's min time cal.setTimeInMillis(minTime); // now set hour and minutes cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, monthOfYear); cal.set(Calendar.DAY_OF_MONTH, dayOfMonth); // get milliseconds chosen_mills = cal.getTimeInMillis(); // set minimum time if (setMax == false) { // valid time set? if (chosen_mills>=time[0]) { for (i=0;i<first_values && time[i] <= chosen_mills;i++) ; currentIndex = i; windowTime = maxTime - time[currentIndex]; redraw = true; } } else { // valid time set? if (chosen_mills<=time[first_values-1]) { for (i=first_values-1;i>=0 && time[i] >= chosen_mills;i--) ; windowTime = time[i] - time[currentIndex]; redraw = true; } } // force redraw? if (redraw == true) mHandler.sendMessage(mHandler.obtainMessage(PUSH_VALUES)); } /** Called when an option menu item has been selected by the user * @param item Reference to the {@link android.view.MenuItem} clicked on */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.main_about: AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.AIRS_Timeline)) .setMessage(getString(R.string.TimelineAbout)) .setIcon(R.drawable.about) .setNeutralButton(getString(R.string.OK), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); } }); AlertDialog alert = builder.create(); alert.show(); // Make the textview clickable. Must be called after show() ((TextView)alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); return true; } return true; } /** * Called for dispatching key events sent to the Activity * @param event Reference to the {@link android.view.KeyEvent} being pressed * @return true, if consumed, false otherwise */ @Override public boolean dispatchKeyEvent(KeyEvent event) { // key de-pressed? if (event.getAction() == KeyEvent.ACTION_UP) // is it the BACK key? if (event.getKeyCode()==KeyEvent.KEYCODE_BACK) finish(); return super.dispatchKeyEvent(event); } /** * Called when touch event occurred * @param v Reference to the {@link android.view.View} that has focus * @param me Reference to the {@link android.view.MotionEvent} of the touch event */ public boolean onTouch(View v, MotionEvent me) { long progress = (long)(valuesShown*repeatJump)/100; // ensure at least one value increment! if (progress == 0) progress = 1; if (me.getAction() == MotionEvent.ACTION_DOWN) { // set state to pressed v.setPressed(true); switch(v.getId()) { case R.id.timeline_forward: progressTimeline(progress); new PressedThread((Button)v); return true; case R.id.timeline_backward: progressTimeline(-progress); new PressedThread((Button)v); return true; } } if (me.getAction() == MotionEvent.ACTION_UP) { v.setPressed(false); return true; } return false; } // repeat progress in timeline window after arrow buttons have been pressed // run until button is depressed private class PressedThread implements Runnable { Button button; PressedThread(Button pressed) { button = pressed; (new Thread(this)).start(); } public void run() { // wait first before setting on repeat try { Thread.sleep(repeatInterval); } catch(Exception e) { } // repeat while being pressed while(button.isPressed() == true) { long progress = (long)(valuesShown*repeatJump)/100; // ensure at least one value increment! if (progress == 0) progress = 1; switch(button.getId()) { case R.id.timeline_forward: progressTimeline(progress); break; case R.id.timeline_backward: progressTimeline(-progress); break; } try { Thread.sleep(repeatInterval); } catch(Exception e) { } } } } private void progressTimeline(long progress) { int new_index = currentIndex, i; Message push_msg = mHandler.obtainMessage(PUSH_VALUES); new_index += progress; // new index out of day? -> determine boundaries if (new_index < 0) new_index = 0; if (new_index>=first_values) new_index = first_values-1; if (windowTime + time[new_index] > time[first_values-1]) { // move from rightmost value (end of day) down until window size is filled for (i=first_values-1;i>=0;i--) { if (time[i] + windowTime>=time[first_values-1]) new_index = i; else continue; } } currentIndex = new_index; // current index is new index // does current window go beyond end of day? if (windowTime + time[currentIndex] > minReadingTime + maxWindowTime) windowTime = minReadingTime + maxWindowTime - time[currentIndex]; // now push values into display mHandler.sendMessage(push_msg); } private class GatherTask extends AsyncTask<String, Long, Long> { protected Long doInBackground(String... params) { int i, number_values; int t_column, v_column; String query; // issue query to the database if (values == null) { // single or double value query? query = new String("SELECT Timestamp, Value from 'airs_values' WHERE Timestamp BETWEEN " + String.valueOf(startedTime) + " AND " + String.valueOf(endTime) + " AND Symbol='" + Symbol + "'"); values = airs_storage.rawQuery(query, null); } if (values == null) return Long.valueOf(-1); // get column index for timestamp and value t_column = values.getColumnIndex("Timestamp"); v_column = values.getColumnIndex("Value"); if (t_column == -1 || v_column == -1) return Long.valueOf(-1); number_values = values.getCount(); // are there any values? if (number_values != 0) { // allocate history fields time = new long[number_values]; history_f = new float[number_values]; // prepare averaging averageValue = 0.0f; first_values = 0; // move to first row to start values.moveToFirst(); // read DB values into arrays for (i=0;i<number_values;i++) { // get timestamp time[first_values] = values.getLong(t_column); // store minimal time in the DB readings if (first_values==0) minReadingTime = time[0]; // maximum window time windowTime = maxWindowTime = time[first_values] - minReadingTime; // get value history_f[first_values] = values.getFloat(v_column); // count for averaging averageValue += history_f[first_values]; // count first values first_values++; // now move to next row values.moveToNext(); } // now average if (first_values != 0) averageValue /= first_values; else // if there's no first symbol value, return error return Long.valueOf(-1); // return task and show values return Long.valueOf(0); } else return Long.valueOf(-1); } protected void onPreExecute() { progressbar.setVisibility(View.VISIBLE); } protected void onPostExecute(Long result) { int i; // everything ok -> show values if (result.longValue() == 0) { // handle the case of a single value switch(first_values) { case 0: finish(); break; case 1: Time timeStamp = new Time(); timeStamp.set(time[0]); Toast.makeText(getApplicationContext(), getString(R.string.First_sensing) + " " + timeStamp.format("%H:%M:%S") + " " + getString(R.string.First_sensing2) + " " + Float.toString(history_f[0]), Toast.LENGTH_LONG).show(); finish(); } // determine min/max values if (getMaxMin() == true) { // set scaling in view properly DisplayView.setMinMax(min, max, minTime, maxTime); // push values into path for (i=currentIndex;i<first_values;i++) { if (time[i]<=time[currentIndex] + windowTime) DisplayView.pushPath(time[i], history_f[i]); else { DisplayView.pushPath(time[currentIndex] + windowTime, history_f[i]); break; } } // showing average? if (showAverage == true) DisplayView.pushAverage(averageValue); // showing grid? if (showGrid == true) DisplayView.pushGrid(); // set progress bar with display view to make it invisible after drawing DisplayView.setProgressBar(progressbar); DisplayView.postInvalidate(); } } else { Log.e("AIRS", "...terminated GatherThread()"); finish(); } } } @SuppressLint("FloatMath") private boolean getMaxMin() { int i; Time timeStamp = new Time(); String minS, maxS; // reset min/max values minTime = Long.MAX_VALUE; min = Float.MAX_VALUE; max = -Float.MAX_VALUE; valuesShown = 0; // determine min/max of first value set for (i=currentIndex;i<first_values;i++) { if (time[i]<=time[currentIndex] + windowTime) { if (history_f[i]<min) min = history_f[i]; if (history_f[i]>max) max = history_f[i]; if (time[i]<minTime) minTime = time[i]; // at least one value fits valuesShown++; } else break; } if (valuesShown == 0) return false; // if all values are the same, draw epsilon around them! if (min == max) { min -= min/10; max += min/10; } maxTime = minTime + windowTime; // if values before left of decimal are the same -> need to show float decimals if (android.util.FloatMath.floor(min) == android.util.FloatMath.floor(max)) { minS = Float.toString(min); maxS = Float.toString(max); // show same length decimals! if (minS.length() > maxS.length()) { minY.setText(minS.substring(0, maxS.length())); maxY.setText(maxS); } else { minY.setText(minS); maxY.setText(maxS.substring(0, minS.length())); } } else // otherwise only show integers { minY.setText(Integer.toString((int)(min))); maxY.setText(Integer.toString((int)(max))); } // get current maxY text String oldMax = maxY.getText().toString(); // y-axis padding: is max text smaller than min text? if (oldMax.length()<minY.getText().length()) { int difference = minY.getText().length() - oldMax.length(); StringBuffer maxPadding = new StringBuffer(); // now create string with spaces to pad for (i=0;i<difference * 3;i++) maxPadding.append(" "); maxY.setText(maxPadding.toString() + oldMax); } // set time axis now timeStamp.set(minTime); minX.setText(timeStamp.format(getString(R.string.TimeFormat2))); timeStamp.set(maxTime); maxX.setText(timeStamp.format(getString(R.string.TimeFormat2))); return true; } // The Handler that gets information back from the other services private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { int i; switch(msg.what) { case PUSH_VALUES: // determine min/max values if (getMaxMin() == true) { // set scaling in view properly DisplayView.setMinMax(min, max, minTime, maxTime); // push values into path for (i=currentIndex;i<first_values;i++) { if (time[i]<=time[currentIndex] + windowTime) DisplayView.pushPath(time[i], history_f[i]); else { DisplayView.pushPath(time[currentIndex] + windowTime, history_f[i]); break; } } // showing average? if (showAverage == true) DisplayView.pushAverage(averageValue); // showing grid? if (showGrid == true) DisplayView.pushGrid(); // set progress bar with display view to make it invisible after drawing DisplayView.setProgressBar(progressbar); DisplayView.postInvalidate(); } break; case FINISH_ACTIVITY: break; } } }; }