Java tutorial
package com.jogden.spunkycharts.pricebyvolumechart; /* Copyright (C) 2014 Jonathon Ogden < jeog.dev@gmail.com > 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. */ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.NavigableSet; import java.util.UUID; import java.util.concurrent.BlockingDeque; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.os.ConditionVariable; import android.os.Handler; import android.support.v4.content.LocalBroadcastManager; import android.text.format.Time; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import com.jogden.spunkycharts.ApplicationPreferences; import com.jogden.spunkycharts.MainApplication; import com.jogden.spunkycharts.R; import com.jogden.spunkycharts.data.DataClientLocalDebug; import com.jogden.spunkycharts.data.DataContentService; import com.jogden.spunkycharts.data.DataContentService.DataClientInterface; import com.jogden.spunkycharts.data.DataContentService.DataConsumerInterface; import com.jogden.spunkycharts.misc.OHLC; import com.jogden.spunkycharts.misc.Pair; import com.jogden.spunkycharts.traditionalchart.YAxisPriceLabel; import com.jogden.spunkycharts.pricebyvolumechart.draw.DrawSemantics_SILO; @SuppressLint("InflateParams") public class PriceByVolumeChartFragmentAdapter implements DataContentService.DataConsumerInterface { @SuppressWarnings("serial") public static class PriceByVolumeChartLogicError extends RuntimeException { public PriceByVolumeChartLogicError(String msg) { super(msg); } } /**/ public final String MY_ID = UUID.randomUUID().toString(); /**/ private String _symbol = null; private final int MSPM = 60000; private final int GMS = MSPM * DataClientInterface.TIME_GRANULARITY; /* the important child views */ private final PriceByVolumeChartPanel _mainSgmnt; private final LinearLayout _priceAxis; private final LinearLayout _volumeAxis; private final ViewGroup _myContainer; private final LayoutInflater _inflater; private final Context _cntxt; private final Handler _guiThrdHndlr; /* also chart specific settings but * currently changed for all charts of this type */ private int _axisFontSz = ApplicationPreferences.getAxisFontSize(); private int _axisFontColor = ApplicationPreferences.getAxisFontColor(); private int _axisTimeout = ApplicationPreferences.getLongTimeout(); private int _axisTimeoutIncr = ApplicationPreferences.getTimeoutIncrement(); private int _priceElemHght = 0; private int _volSegHght = 0; private volatile float _highPrice = 0; private volatile float _lowPrice = Float.MAX_VALUE; private volatile int _highVolume = 0; private int _segCount = 0; private float _segSize = 0; private volatile Time _updateTime = new Time(); private Cursor _myCursor; static private String[] columns = { "OPEN_COLUMN", "HIGH_COLUMN", "LOW_COLUMN", "CLOSE_COLUMN", "VOLUMN_COLUMN" }; @SuppressWarnings("serial") static private List<Pair<String, SQLType>> myColumnInfo = new ArrayList<Pair<String, SQLType>>() { { add(new Pair<String, SQLType>(columns[0], SQLType.FloatNotNull)); add(new Pair<String, SQLType>(columns[1], SQLType.FloatNotNull)); add(new Pair<String, SQLType>(columns[2], SQLType.FloatNotNull)); add(new Pair<String, SQLType>(columns[3], SQLType.FloatNotNull)); add(new Pair<String, SQLType>(columns[4], SQLType.IntNotNull)); } }; private AtomicBoolean _hasSegment = new AtomicBoolean(false); private AtomicBoolean _streamReady = new AtomicBoolean(false); private AtomicBoolean _updateReady = new AtomicBoolean(false); private volatile OHLC _lastGranularPriceSeg = new OHLC(0); private volatile int _lastGranularVolSeg = 0; private volatile Time _lastGranularSegTime = new Time(); private final Lock _volumeAxisLck = new ReentrantLock(); private final Lock _priceAxisLck = new ReentrantLock(); private volatile Thread _priceAxisThrd = null; private final Object _priceAxisThrdMntr = new Object(); private int _priceAxisPrmptCnt = 0; private final BlockingDeque<Thread> _priceAxisPrmptStck = new LinkedBlockingDeque<Thread>(); private final NavigableSet<Thread> _activeThreads = new ConcurrentSkipListSet<Thread>(new Comparator<Thread>() { @Override public int compare(Thread lhs, Thread rhs) { return (int) lhs.getId() - (int) rhs.getId(); } }); private LocalBroadcastManager _localBcastMngr; private static final IntentFilter _rangeFltr = new IntentFilter(); static { _rangeFltr.addAction(HighLowChange.NEW_HIGH_PRICE); _rangeFltr.addAction(HighLowChange.NEW_LOW_PRICE); _rangeFltr.addAction(HighLowChange.NEW_HIGH_VOL); } /*|---------> begin PUBLIC interface <--------|*/ public PriceByVolumeChartFragmentAdapter(Context context, Handler handler, LocalBroadcastManager broadcastManager, PriceByVolumeChartPanel mainChartPanel, LinearLayout volumeAxisLayout, LinearLayout priceAxisLayout, ViewGroup fragmentViewGroup, int volumeSegmentHeight, Cursor cursor, String symbol, Boolean... bArgs) { this._cntxt = context; this._guiThrdHndlr = handler; this._localBcastMngr = broadcastManager; this._mainSgmnt = mainChartPanel; this._volumeAxis = volumeAxisLayout; this._priceAxis = priceAxisLayout; this._myContainer = fragmentViewGroup; this._volSegHght = volumeSegmentHeight; this._myCursor = cursor; this._symbol = symbol; this._localBcastMngr.registerReceiver(new HighLowChange(), _rangeFltr); this._inflater = LayoutInflater.from(_cntxt); this._setPricePanelDrawSemantics(); _mainSgmnt.init(new PriceByVolumeChartPanel.OnNewHighVolume() { @Override public void onNewHighVolume(int v) { Intent iii = new Intent(HighLowChange.NEW_HIGH_VOL); iii.putExtra(HighLowChange.EXTRA_VALUE, v); iii.putExtra(HighLowChange.FRAGMENT_ID, MY_ID); _localBcastMngr.sendBroadcast(iii); } }, handler); if (_myCursor != null) this._init(); } @Override public boolean updateReady() { return _updateReady.get(); } @Override public Cursor swapCursor(Cursor cursor) { Cursor tmp = _myCursor; _myCursor = cursor; if (_myCursor != null) reset(); return tmp; } @Override public void update(DataClientInterface dataClient, DataConsumerInterface.InsertCallback iCallback) { Pair<OHLC, Time> pricePair = dataClient.getLast(_symbol, DataClientLocalDebug.DATA_PRICE_DEF); Pair<Integer, Time> volPair = dataClient.getLast(_symbol, DataClientLocalDebug.DATA_VOL_DEF); OHLC price = new OHLC(pricePair.first); int vol = volPair.first.intValue(); Time time = new Time(pricePair.second); /* if we've rolled past TIME_GRANULARITY */ if (_hasSegment.get() && _segHasRolled(_lastGranularSegTime, time, DataClientInterface.TIME_GRANULARITY)) { iCallback.insertRow( _createContentValues(_lastGranularPriceSeg, _lastGranularVolSeg, _lastGranularSegTime), _symbol, (DataConsumerInterface) this); _lastGranularSegTime = _truncateTime(time, GMS, true, false); /* Log.d("Data-Consumer-Update-DB", _lastGranularSegTime.format("%H:%M:%S") ); */ } _lastGranularPriceSeg = new OHLC(price); _lastGranularVolSeg = vol; if (!_streamReady.get() || _myCursor == null) { _updateTime.set(time); return; } /* Log.d("Data-Consumer-Update-Current", _lastPriceSeg.toString() + " " + String.valueOf(_lastVolSeg) + " " +time.format("%H:%M:%S")); */ if (price.high > _highPrice) { _highPrice = price.high; Intent iii = new Intent(HighLowChange.NEW_HIGH_PRICE); iii.putExtra(HighLowChange.EXTRA_VALUE, price.high); iii.putExtra(HighLowChange.FRAGMENT_ID, MY_ID); _localBcastMngr.sendBroadcast(iii); } if (price.low < _lowPrice) { _lowPrice = price.low; Intent iii = new Intent(HighLowChange.NEW_LOW_PRICE); iii.putExtra(HighLowChange.EXTRA_VALUE, price.low); iii.putExtra(HighLowChange.FRAGMENT_ID, MY_ID); _localBcastMngr.sendBroadcast(iii); } try { /* remember vol is cumm. vis-a-vis the granular seg */ _mainSgmnt.update(_getBucketIndex(price.close), vol); } catch (RuntimeException e) { //DEBUG Log.d("UPDATE EXCEPTION", "::BEGIN:: " + e.getMessage()); e.printStackTrace(); Log.d("UPDATE EXCEPTION", "::END:: " + e.getMessage()); } /* high volume is checked in the panel, where it is agglomerated */ _updateTime.set(time); } @Override public void bridge(DataClientInterface dataClient, DataConsumerInterface.InsertCallback iCallback) { _updateReady.set(false); iCallback.clear(_symbol, (DataConsumerInterface) this); Pair<OHLC[], Time> pricePair = dataClient.getBulk(_symbol, null, DataClientLocalDebug.DATA_PRICE_DEF); Pair<Integer[], Time> volPair = dataClient.getBulk(_symbol, null, DataClientLocalDebug.DATA_VOL_DEF); final OHLC[] prices = pricePair.first; final Integer[] vols = volPair.first; int len = vols.length; if (prices.length != len) throw new IllegalStateException("price / volume getBulk size inconconsistency"); _updateTime.set(pricePair.second); --len; long endMs = _updateTime.toMillis(true); endMs /= GMS; endMs *= GMS; List<ContentValues> valsList = new ArrayList<ContentValues>(); Time t = new Time(); for (int i = 0; i < len; ++i) { t.set(endMs - ((len - i) * GMS)); valsList.add(_createContentValues(prices[i], vols[i], t)); /*Log.d("Data-Consumer-Get-Bulk", prices[i].toString() +" " + String.valueOf(vols[i]) + " " + t.format("%H:%M:%S")); */ } OHLC price = prices[len]; _lastGranularPriceSeg = new OHLC(price); _lastGranularVolSeg = vols[len]; _lastGranularSegTime.set(endMs); _updateReady.set(true); iCallback.insertRows(valsList, _symbol, (DataConsumerInterface) this); } @Override public List<Pair<String, SQLType>> getColumns() { return myColumnInfo; } public void reset() { cleanUpAndUpdate(); _init(); } public void cleanUpAndUpdate() { _streamReady.set(false); for (Thread t : _activeThreads) t.interrupt(); _clear(); /* leave display values */ _highPrice = 0; _lowPrice = Float.MAX_VALUE; _highVolume = 0; } public void axisRefresh() { try { _guiThrdHndlr.post(new Runnable() { public void run() { _volumeAxis.invalidate(); } }); } catch (IllegalStateException e) { e.printStackTrace(); } } /* adjust vertical zoom * NEED TO REDRAW EVERYTHING FOR THIS public void setSegmentHeight(int h) { if( h > MAX_SEG_HEIGHT ) h = MAX_SEG_HEIGHT; else if( h < MIN_SEG_HEIGHT ) h = MIN_SEG_HEIGHT; _volSegHght = h; _mainSgmnt.setSegmentHeight(h); _reset(); }*/ public int getSegmentHeight() { return _volSegHght; } public void setAxisFontSize(int sp) { _axisFontSz = sp; reset(); } public void setAxisFontColor(int colorId) { _axisFontColor = colorId; axisRefresh(); } public void setAxisTimeout(int value) { /* should reset but cost not worth benefit*/ _axisTimeout = value; axisRefresh(); } public void setTimeoutIncrement(int value) { /* should reset but cost not worth benefit*/ _axisTimeoutIncr = value; axisRefresh(); } /*|---------> end PUBLIC interface <--------|*/ /*|---------> begin PRIVATE methods <--------|*/ // CONSIDER MAKING THESE GLOBAL UTILITIES private boolean _segHasRolled(Time t1, Time t2, int minutes) { /* this only checks up to hours, i.e. same time tomorrow won't be treated as a roll (erroneously) */ int rMin1 = t1.minute / minutes; int rMin2 = t2.minute / minutes; return (rMin1 != rMin2 || t1.hour != t2.hour); } private Time _truncateTime(Time t, long msInterval, boolean ignoreDST, boolean inPlace) { long ms = t.toMillis(ignoreDST); ms /= msInterval; ms *= msInterval; if (inPlace) { t.set(ms); return t; } else { Time nTime = new Time(); nTime.set(ms); return nTime; } } private void _createVolumeAxisX() { Thread thisThread = new Thread() { public void run() { if (!_volumeAxisLck.tryLock()) return; try { final YAxisPriceLabel tv = (YAxisPriceLabel) _inflater.inflate(R.layout.y_axis_price_label, null); tv.setText(Integer.toString(_highVolume)); tv.setTextSize(_axisFontSz); tv.setTextColor(_axisFontColor); _guiThrdHndlr.post(new Runnable() { public void run() { _volumeAxis.removeAllViewsInLayout(); _volumeAxis.addView(tv); } }); /*adjust volume axis to align*/ int tvWidth = 0; int failCount = 0; while ((tvWidth = tv.getWidth() / 2) == 0) { Thread.sleep(_axisTimeoutIncr); if ((failCount += _axisTimeoutIncr) >= _axisTimeout) return; /* if timeout forget about align */ } _mainSgmnt.setHorizontalBuffers(0, tvWidth); _guiThrdHndlr.post(new Runnable() { public void run() { _mainSgmnt.forceDraw(); } }); } catch (InterruptedException e) { } catch (RuntimeException e) { throw e; } finally { _activeThreads.remove(this); _volumeAxisLck.unlock(); } } }; _activeThreads.add(thisThread); thisThread.start(); } private int _getBucketIndex(float p) { return (int) ((p - _lowPrice + (_segSize / 2f)) / _segSize); } /* sync to avoid race with _agglomerate */ private synchronized void _findPriceRange() { float tmpHigh, tmpLow; _myCursor.moveToPosition(-1); while (_myCursor.moveToNext()) { tmpHigh = _myCursor.getFloat(2); tmpLow = _myCursor.getFloat(3); if (tmpHigh > _highPrice) _highPrice = tmpHigh; if (tmpLow < _lowPrice) _lowPrice = tmpLow; } _myCursor.moveToPosition(-1); } /// adjust high and low the assumed high and low from // end buckets/segs private synchronized void _agglomerate() { // should we just pass these in vs global?? if (_segCount <= 0 || _segSize <= 0) { throw new IllegalStateException("_segCount and _segSize can't be <= 0"); } final int[] buckets = new int[_segCount]; Arrays.fill(buckets, 0); float mPrice; int gVol; _myCursor.moveToPosition(-1); /* loop through and agglomerate the volumes by bucket */ while (_myCursor.moveToNext()) { mPrice = /* take the midpoint of the OHLC */ (_myCursor.getFloat(2) + _myCursor.getFloat(3)) / 2; gVol = _myCursor.getInt(5); buckets[_getBucketIndex(mPrice)] += gVol; } _myCursor.moveToPosition(-1); /* pass the buckets to the panel */ _mainSgmnt.populate(buckets); //, true); _streamReady.set(true); } private Thread _createPriceAxisAndMainPanel() { Thread thisThread = new Thread() { public void run() { /////////////////////////////////// /* START PREEMPTION LOGIC */ ///////////////////////////////// boolean noWait; final int MAX_PREEMPT_COUNT = ApplicationPreferences.getMaxPreemptCount(); if (!(noWait = _priceAxisLck.tryLock())) { synchronized (_priceAxisThrdMntr) { if (_priceAxisPrmptCnt++ > MAX_PREEMPT_COUNT) return; if (_priceAxisThrd != null) _priceAxisThrd.interrupt(); } _priceAxisPrmptStck.offer(this); _priceAxisLck.lock(); } try { /* everything that follows should assume we own monitor */ if (!noWait) { if (this != _priceAxisPrmptStck.peekLast()) return; else { /* don't assume stack hasn't grown since the peek */ Iterator<Thread> dIter = _priceAxisPrmptStck.iterator(); do { _priceAxisPrmptStck.poll(); } while (dIter.next() != this); } } synchronized (_priceAxisThrdMntr) { _priceAxisThrd = this; } ///////////////////////////////// /* END PREEMPTION LOGIC */ ////////////////////////////// float rangeDiff = _highPrice - _lowPrice; /* deal with problematic high/low parameters */ if (rangeDiff <= 0 || _highPrice < 0 || _lowPrice < 0) if (rangeDiff == 0 && _highPrice > 0) { _highPrice *= 1.001; _lowPrice *= .999; rangeDiff = _highPrice - _lowPrice; } else throw new IllegalStateException("Invalid high and/or low price in the ChartAdapter"); /* if we haven't calculated the height of a price-label view */ if (_priceElemHght == 0) {/* can cached value doesn't go stale? */ final YAxisPriceLabel tv1 = (YAxisPriceLabel) _inflater.inflate(R.layout.y_axis_price_label, null); tv1.setText("X"); tv1.setTextSize(_axisFontSz); tv1.setVisibility(View.INVISIBLE); final ConditionVariable cond = new ConditionVariable(); _guiThrdHndlr.post(new Runnable() { public void run() { _priceAxis.removeAllViews(); _priceAxis.addView(tv1); cond.open(); } }); cond.block(); cond.close(); YAxisPriceLabel tv1b = (YAxisPriceLabel) _priceAxis.getChildAt(0); /* make sure a valid priceElemHeightt, or quit entirely */ /* just spin, a new thread preempts us anyway */ while ((_priceElemHght = tv1b.getHeight()) == 0) Thread.sleep(_axisTimeoutIncr); } _guiThrdHndlr.post(new Runnable() { public void run() { _priceAxis.removeAllViews(); } }); int totalHeight; /* make sure a valid totalHeight, or quit entirely */ /* just spin, a new thread preempts us anyway */ while ((totalHeight = _priceAxis.getHeight()) == 0 || totalHeight > _myContainer.getHeight()) Thread.sleep(_axisTimeoutIncr); float[] incrVals = new float[2]; try { int maxNodes = (int) (totalHeight / _priceElemHght); if (rangeDiff < 0 || maxNodes < 0) throw new PriceByVolumeChartLogicError("rangeDiff and maxNodes can't be negative."); incrVals = /* call down to our native sub to find increment values */ MainApplication.IncrementRegressNative(rangeDiff, maxNodes); if (incrVals[0] < 0 || incrVals[1] <= 0) throw new PriceByVolumeChartLogicError("IncrementRegressNative() sub-routine aborted. " + "retVals[0]: " + incrVals[0] + "retVals[1]: " + incrVals[1] + "adjRangeDiff: " + rangeDiff + "maxNodes" + maxNodes); _segCount = (int) incrVals[1]; _segSize = incrVals[0]; } catch (PriceByVolumeChartLogicError e) { Log.e("PriceByVolumeChartLogicError", e.getMessage()); return; /* just leave the axis empty*/ } /* adjust height to new increment values */ final int adjPriceElemHeight = (int) (totalHeight / _segCount); /* we'll need to account for any unused spaced in the axis */ final int vacantHeight = totalHeight - (int) (adjPriceElemHeight * _segCount); double distFromIncr = Math.IEEEremainder((double) _highPrice, (double) _segSize); /* distance from rounded incr value */ double adjTopNodeVal = (double) _highPrice - distFromIncr; if (distFromIncr > 0) /* be sure to round to the upper incr */ adjTopNodeVal += _segSize; DecimalFormat df = new DecimalFormat("0.00"); double lastNodeVal = adjTopNodeVal; int count = 0; do { /* loop through the increments */ final YAxisPriceLabel tv = (YAxisPriceLabel) _inflater.inflate(R.layout.y_axis_price_label, null); tv.setTextSize(_axisFontSz); tv.setText(df.format(lastNodeVal)); tv.setTextColor(_axisFontColor); tv.setLayoutParams( new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, 0, 1)); _guiThrdHndlr.post(new Runnable() { public void run() { _priceAxis.addView(tv); } }); /* for the loop-contingent side-effect */ } while (++count < (int) _segCount && (lastNodeVal -= _segSize) > 0); final float halfVacantHeight = vacantHeight / 2; _mainSgmnt.setVerticalBuffers((int) halfVacantHeight, (int) halfVacantHeight); _guiThrdHndlr.post(new Runnable() { public void run() { _priceAxis.setPadding( /* center price-views */ 0, (int) halfVacantHeight, 0, (int) halfVacantHeight); _priceAxis.setClipChildren(false); _priceAxis.setClipToPadding(false); _priceAxis.invalidate(); } }); synchronized (_priceAxisThrdMntr) { _priceAxisPrmptCnt = 0; } } catch (InterruptedException e) { } catch (IllegalStateException e) { Log.d("IllegalState(High, Low): ", "IllegalStateException caught in _createPriceAxisAndMainPanelY: " + Float.toString(_highPrice) + " - " + Float.toString(_lowPrice)); } catch (RuntimeException e) { e.printStackTrace(); throw e; } finally { synchronized (_priceAxisThrdMntr) { _priceAxisThrd = null; } _activeThreads.remove(this); _priceAxisLck.unlock(); } } }; _activeThreads.add(thisThread); thisThread.start(); return thisThread; } private void _setPricePanelDrawSemantics() { _mainSgmnt.setDrawSemantics(DrawSemantics_SILO.class); } private ContentValues _createContentValues(OHLC price, int volume, Time t) { ContentValues tmp = new ContentValues(); tmp.put(DataConsumerInterface.primaryKey, t.toMillis(true)); tmp.put(columns[0], price.open); tmp.put(columns[1], price.high); tmp.put(columns[2], price.low); tmp.put(columns[3], price.close); tmp.put(columns[4], volume); return tmp; } private void _init() { if (_myCursor != null) { final int INCR = ApplicationPreferences.getTimeoutIncrement(); final int TOUT = ApplicationPreferences.getLongTimeout(); int timeOutCount = 0; /* stall for draw completion */ while (_mainSgmnt.getMeasuredHeight() == 0 && (timeOutCount += INCR) < TOUT) try { Thread.sleep(INCR); } catch (InterruptedException e) { } try { _findPriceRange(); _createVolumeAxisX(); _createPriceAxisAndMainPanel().join(); } catch (InterruptedException e) { } finally { _agglomerate(); _mainSgmnt.forceDraw(); } } else throw new IllegalStateException( "ChartFragmentAdapter can not be initialized " + "without a valid Cursor"); } private void _clear() { _activeThreads.clear(); _mainSgmnt.clear(); _priceAxis.removeAllViewsInLayout(); _volumeAxis.removeAllViewsInLayout(); } /*|---------> end PRIVATE methods <--------|*/ /*|---------> begin NESTED OBJECT DEFS <--------|*/ /* APPLICATION WIDE BROADCASTS: broadcast when a price * or volume update is at a new extreme. Currently these broadcasts * are only relevant within the adapter. */ class HighLowChange extends BroadcastReceiver { public static final String NEW_HIGH_PRICE = "com.jogden.integratedtrader.NEW_HIGH_PRICE"; public static final String NEW_LOW_PRICE = "com.jogden.integratedtrader.NEW_LOW_PRICE"; public static final String NEW_HIGH_VOL = "com.jogden.integratedtrader.NEW_HIGH_VOL"; public static final String EXTRA_VALUE = "EXTRA_VALUE"; public static final String FRAGMENT_ID = "FRAGMENT_ID"; @Override public void onReceive(Context c, Intent i) { /* if its for another chart exit quietly */ if (!i.getStringExtra(FRAGMENT_ID).equals(MY_ID)) return; String action = i.getAction(); float price; if (action.equals(HighLowChange.NEW_HIGH_PRICE)) { price = i.getFloatExtra(EXTRA_VALUE, _highPrice); if (price > _highPrice) { _highPrice = price; try { _createPriceAxisAndMainPanel().join(); } catch (InterruptedException e) { } finally { _agglomerate(); } } } else if (action.equals(HighLowChange.NEW_LOW_PRICE)) { price = i.getFloatExtra(EXTRA_VALUE, _lowPrice); if (price < _lowPrice) { _lowPrice = price; try { _createPriceAxisAndMainPanel().join(); } catch (InterruptedException e) { } finally { _agglomerate(); } } } else if (action.equals(HighLowChange.NEW_HIGH_VOL)) { int vol = i.getIntExtra(EXTRA_VALUE, _highVolume); if (vol > _highVolume) { _highVolume = vol; _createVolumeAxisX(); } } } } }