com.jogden.spunkycharts.traditionalchart.TraditionalChartFragmentAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.jogden.spunkycharts.traditionalchart.TraditionalChartFragmentAdapter.java

Source

package com.jogden.spunkycharts.traditionalchart;

/* 
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.BlockingQueue;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
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.content.res.Resources;
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.misc.HideHorizontalLeftOverflowWrapper;
import com.jogden.spunkycharts.misc.OHLC;
import com.jogden.spunkycharts.misc.Pair;
import com.jogden.spunkycharts.traditionalchart.draw.*;
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;

/** This adapter the hub between our fragment object, its data, 
 * and its display. It handles all the logic for the chart internals; 
 * e.g how an axis is constructed or how we zoom in.
 *  </p>
 *  The idea is to create a flexible abstraction for all the internal 
 *  logic of the chart, allowing the fragment object to focus on 
 *  things like managing its life-cycle, state, and, along with 
 *  the docking panel, deal with high(er)-level events: add, move, 
 *  animate, remove, and stack charts.
 *  </p>
 *  For the core implementation details and algorithms that make 
 *  the thing tick (couldn't resist), scroll down  to the section titled: 
 *  ' begin PRIVATE methods '.
 */
@SuppressLint("InflateParams")
public class TraditionalChartFragmentAdapter implements DataContentService.DataConsumerInterface {
    @SuppressWarnings("serial")
    public static class TraditionalChartLogicError extends RuntimeException {
        public TraditionalChartLogicError(String msg) {
            super(msg);
        }
    }

    private final String _myId = UUID.randomUUID().toString();
    private String _symbol = null;

    private final DataConsumerInterface _thisConsumer = this;

    /* these need to find their way into xml resources / prefs*/
    private final int MAX_SEG_WIDTH = 40;
    private final int MIN_SEG_WIDTH = 4;
    private final float MAX_DISPLAY_MULTIPLE = 3;
    private final float DEF_DISPLAY_MULTIPLE = 1.1f;

    private final int MSPM = 60000;
    private final int GMS = MSPM * DataClientInterface.TIME_GRANULARITY;

    /* the important child views */
    private final TraditionalChartPanel _priceSgmnt;
    private final TraditionalChartPanel _volumeSgmnt;
    private final LinearLayout _timeAxis;
    private final LinearLayout _priceAxis;
    private final LinearLayout _volumeAxis;
    private final ViewGroup _myContainer;

    private final LayoutInflater _inflater;
    private final Context _cntxt;
    private final Handler _guiThrdHndlr;
    private final Resources _myResources;

    /* chart specific settings */
    private TraditionalChartPreferences.Type _chartType;
    private TraditionalChartPreferences.Frequency _chartFreq;
    /* 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 _xAxisPadSz = ApplicationPreferences.getXAxisPaddingSize();

    private int _priceElemHght = 0;
    private int _priceSegWdth = 0;

    private volatile boolean drawPriceNow = false;
    private volatile boolean drawVolumeNow = false;

    private volatile float _highPriceDsply = 0;
    private volatile float _lowPriceDsply = Float.MAX_VALUE;
    private volatile float _highPrice = 0;
    private volatile float _lowPrice = Float.MAX_VALUE;
    private volatile int _highVolume = 0;

    /*CACHE VALS OR ALL RECENT DATA*/
    /*our last full segment(what's actually on the chart)*/
    private volatile OHLC _lastPriceSeg = null;
    private volatile int _lastVolSeg = 0;
    private volatile Time _lastSegTime = new Time();
    /*the base segment used between client and consumer(what we build on)*/
    private volatile OHLC _lastGranularPriceSeg = null;
    private volatile int _lastGranularVolSeg = 0;
    private volatile Time _lastGranularSegTime = new Time();
    /*our penultimate granular segment for sync issues*/
    private volatile OHLC _penultGranularPriceSeg = null;
    private volatile int _penultGranularVolSeg = 0;
    private volatile Time _penultGranularSegTime = new Time();
    /* vol is cummulative; this reconciles full vs. granular seg */
    private volatile int _lastVolPartialCummulative = 0;
    /* keep track of the last update so we know when a seg 'rolls' */
    private volatile Time _updateTime = new Time();
    /* keep track of the last full seg that was put into a sync buffer(for consistency) */
    private volatile Time _lastPutTime = null;

    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 final Lock _priceAxisLck = new ReentrantLock();
    private final Lock _timeAxisLck = new ReentrantLock();
    private final Lock _volumeAxisLck = new ReentrantLock();
    private final Lock _priceSgmntLck = new ReentrantLock();
    private final Lock _volumeSgmntLck = new ReentrantLock();
    private final Lock _populateLck = new ReentrantLock();

    private CountDownLatch _updateLtch = new CountDownLatch(0);
    private AtomicBoolean _hasSegment = new AtomicBoolean(false);
    private AtomicBoolean _streamReady = new AtomicBoolean(false);
    private AtomicBoolean _updateReady = new AtomicBoolean(false);

    // should we just use LinkedBlockingQueue??
    private final BlockingQueue<String> _timeStampQueue = new SynchronousQueue<String>();
    private final BlockingQueue<OHLC> _ohlcQueue = new SynchronousQueue<OHLC>();
    private final BlockingQueue<Integer> _volumeQueue = new SynchronousQueue<Integer>();

    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 TraditionalChartFragmentAdapter(Context context, Handler handler, LocalBroadcastManager broadcastManager,
            TraditionalChartPanel priceChartPanel, LinearLayout timeAxisLayout, LinearLayout priceAxisLayout,
            TraditionalChartPanel volumeChartPanel, LinearLayout volumeAxisLayout, ViewGroup fragmentViewGroup,
            int priceSegmentWidth, TraditionalChartPreferences.Type chartType,
            TraditionalChartPreferences.Frequency chartFrequency, Cursor cursor, String symbol, Boolean... bArgs) {
        this._cntxt = context;
        this._guiThrdHndlr = handler;
        this._localBcastMngr = broadcastManager;
        this._priceSgmnt = priceChartPanel;
        this._timeAxis = timeAxisLayout;
        this._priceAxis = priceAxisLayout;
        this._volumeSgmnt = volumeChartPanel;
        this._volumeAxis = volumeAxisLayout;
        this._myContainer = fragmentViewGroup;
        this._priceSegWdth = priceSegmentWidth;
        this._chartType = chartType;
        this._chartFreq = chartFrequency;
        this._myCursor = cursor;
        this._updateTime = new Time();
        this._myResources = _cntxt.getResources();
        this._symbol = symbol;
        this._localBcastMngr.registerReceiver(new HighLowChange(), _rangeFltr);
        this._inflater = LayoutInflater.from(_cntxt);
        this._setCallbacks();
        this._setPricePanelDrawSemantics();
        this._volumeSgmnt.setDrawSemantics(DrawSemantics_SILO.class);
        if (_myCursor != null)
            this._init();
    }

    @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)) {
            /* cache penultimate values */
            _penultGranularPriceSeg = new OHLC(_lastGranularPriceSeg);
            _penultGranularVolSeg = _lastGranularVolSeg;
            _penultGranularSegTime.set(_lastGranularSegTime);

            iCallback.insertRow(
                    _createContentValues(_lastGranularPriceSeg, _lastGranularVolSeg, _lastGranularSegTime), _symbol,
                    (DataConsumerInterface) this);
            _lastVolPartialCummulative += _lastGranularVolSeg;
            _lastGranularSegTime = _truncateTime(time, GMS, true, false); /*
                                                                          Log.d("Data-Consumer-Update-DB", 
                                                                          _lastGranularSegTime.format("%H:%M:%S")
                                                                          );   */
        }
        _lastGranularPriceSeg = new OHLC(price);
        _lastGranularVolSeg = vol;

        /* BE SURE TO CALLBACK TO DATABASE and
         * SET _lastGranular... CACHE VALS, REGARDLESS; */
        /* then we can leave if necessary */
        try {
            if (!_updateLtch.await(ApplicationPreferences.getShortTimeout(), TimeUnit.MILLISECONDS)
                    || _myCursor == null)
                return;
        } catch (InterruptedException e) {
            return;
        }

        /* if everything else is shut down bail so we don't
         *  block on the synchronous queue  */
        if (!_streamReady.get()) {
            _updateTime.set(time);
            return;
        }

        /* value depends on if we've rolled to a new full seg */
        int fullSegVol = 0;

        /* if we've rolled past Chart Frequency Value */
        if (_segHasRolled(_lastSegTime, time, _chartFreq.value) || !_hasSegment.get()) {
            try {
                fullSegVol = vol;
                _lastVolPartialCummulative = 0;
                Time tTime = _truncateTime(time, MSPM * _chartFreq.value, true, false);
                _lastSegTime.set(tTime);

                // _timeStampQueue.put( tTime.format("%H:%M") );      
                /* time must come first since it may fill gaps in price/vol */
                try {
                    _populateLck.lock();
                    _putTime(tTime);
                    _ohlcQueue.put(new OHLC(price));
                    _volumeQueue.put(fullSegVol);
                } finally {
                    _populateLck.unlock();
                }
                /*                            
                Log.d("Data-Consumer-Update-New", 
                    price.toString() + "   " +
                    String.valueOf(vol) + "   " +
                    tTime.format("%H:%M:%S")); */
            } catch (InterruptedException e) {
            }
        } else {
            fullSegVol = vol + _lastVolPartialCummulative;
            _priceSgmnt.update(price.close);
            _volumeSgmnt.update(fullSegVol);
            _lastPriceSeg.close = price.close;
            if (price.high > _lastPriceSeg.high)
                _lastPriceSeg.high = price.high;
            if (price.low < _lastPriceSeg.low)
                _lastPriceSeg.low = price.low;
            _lastVolSeg = fullSegVol; /*
                                      Log.d("Data-Consumer-Update-Current", 
                                      _lastPriceSeg.toString() + "    "
                                      + String.valueOf(_lastVolSeg) + "   " 
                                      +time.format("%H:%M:%S"));            */
        }

        if (price.high > _highPrice) {
            Intent iii = new Intent(HighLowChange.NEW_HIGH_PRICE);
            iii.putExtra(HighLowChange.EXTRA_VALUE, price.high);
            iii.putExtra(HighLowChange.FRAGMENT_ID, _myId);
            _localBcastMngr.sendBroadcast(iii);
        }
        if (price.low < _lowPrice) {
            Intent iii = new Intent(HighLowChange.NEW_LOW_PRICE);
            iii.putExtra(HighLowChange.EXTRA_VALUE, price.low);
            iii.putExtra(HighLowChange.FRAGMENT_ID, _myId);
            _localBcastMngr.sendBroadcast(iii);
        }
        if (fullSegVol > _highVolume) {
            Intent iii = new Intent(HighLowChange.NEW_HIGH_VOL);
            iii.putExtra(HighLowChange.EXTRA_VALUE, fullSegVol);
            iii.putExtra(HighLowChange.FRAGMENT_ID, _myId);
            _localBcastMngr.sendBroadcast(iii);
            ;
        }
        _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")); */
        }

        final long fEndMs = endMs;
        final int lastIndx = len;
        (new Thread() {
            public void run() {
                final int INCR = ApplicationPreferences.getTimeoutIncrement();
                /* THIS HAS TO WAIT FOR POPULATE */
                while (!_streamReady.get())
                    try {
                        Thread.sleep(INCR);
                    } catch (InterruptedException e) {
                    }
                /* INITIALIZE OUR CACHE VALUES */
                OHLC price = prices[lastIndx];
                _lastPriceSeg.close = price.close;
                if (price.high > _lastPriceSeg.high)
                    _lastPriceSeg.high = price.high;
                if (price.low < _lastPriceSeg.low)
                    _lastPriceSeg.low = price.low;
                _lastVolSeg += vols[lastIndx];
                _lastGranularPriceSeg = new OHLC(price);
                _lastGranularVolSeg = vols[lastIndx];
                _lastGranularSegTime.set(fEndMs);
                _updateReady.set(true);
            }
        }).start();

        iCallback.insertRows(valsList, _symbol, (DataConsumerInterface) this);

    }

    @Override
    public final boolean updateReady() {
        return /*_streamReady.get() && */ _updateReady.get();
    }

    @Override
    public Cursor swapCursor(Cursor cursor) {
        Cursor tmp = _myCursor;
        _myCursor = cursor;
        if (_myCursor != null)
            _reset();
        return tmp;
    }

    @Override
    public List<Pair<String, SQLType>> getColumns() {
        return myColumnInfo;
    }

    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 {
            _createPriceAxisY();
            _guiThrdHndlr.post(new Runnable() {
                public void run() {
                    _timeAxis.invalidate();
                    _volumeAxis.invalidate();
                }
            });
        } catch (IllegalStateException e) {
            e.printStackTrace();
        }
    }

    public boolean isFullOfSegments() {
        return _priceSgmnt.isFull();
    }

    /* adjust vertical zoom */
    public void setHighLowDisplayMultiple(float m) {
        float displayDiff = _highPriceDsply - _lowPriceDsply;
        float actualDiff = _highPrice - _lowPrice;
        displayDiff *= m;
        float multiple = displayDiff / actualDiff;
        if (multiple > MAX_DISPLAY_MULTIPLE)
            displayDiff = MAX_DISPLAY_MULTIPLE * actualDiff;
        else if (multiple < 1)
            displayDiff = actualDiff;
        float halfDiffDiff = (displayDiff - actualDiff) / 2;
        _lowPriceDsply = _lowPrice - halfDiffDiff;
        _highPriceDsply = _highPrice + halfDiffDiff;
        if (_lowPriceDsply < 0) {
            _highPriceDsply += _lowPriceDsply;
            _lowPriceDsply = 0;
        }
        _priceSgmnt.setYRange(_highPriceDsply, _lowPriceDsply, _highPrice, _lowPrice);
        _priceSgmnt.forceDraw();
    }

    public float getHighLowDisplayMultiple() {
        return (_highPriceDsply - _lowPriceDsply) / (_highPrice - _lowPrice);
    }

    public void resetHighLowDisplay() {
        _highPriceDsply = _highPrice;
        _lowPriceDsply = _lowPrice;
        _priceSgmnt.setYRange(_highPriceDsply, _lowPriceDsply, _highPrice, _lowPrice);
        _createPriceAxisY();
        _priceSgmnt.forceDraw();
    }

    /* adjust horizontal zoom */
    public void setSegmentWidth(int w) {
        if (w > MAX_SEG_WIDTH)
            w = MAX_SEG_WIDTH;
        else if (w < MIN_SEG_WIDTH)
            w = MIN_SEG_WIDTH;
        _priceSegWdth = w;
        _priceSgmnt.setSegmentWidth(w);
        _volumeSgmnt.setSegmentWidth(w);
        ((InnerXAxis) _myContainer.findViewById(R.id.inner_xAxis)).setSegmentWidth(w);
        _reset();
    }

    public int getSegmentWidth() {
        return _priceSegWdth;
    }

    /* some basic getters / setters */
    public void setChartType(TraditionalChartPreferences.Type type) {
        _chartType = type;
        this._setPricePanelDrawSemantics();
        _priceSgmnt.forceDraw();
    }

    public TraditionalChartPreferences.Type getChartType() {
        return _chartType;
    }

    public void setChartFrequency(TraditionalChartPreferences.Frequency frequency) {
        _chartFreq = frequency;
        _priceSgmnt.forceDraw();
    }

    public TraditionalChartPreferences.Frequency getChartFrequency() {
        return _chartFreq;
    }

    public void setAxisFontSize(int sp) {
        _axisFontSz = sp;
        axisRefresh();
    }

    public void setAxisFontColor(int colorId) {
        _axisFontColor = colorId;
        axisRefresh();
    }

    public void setXAxisPaddingSize(int sz) {
        _xAxisPadSz = sz;
        _reset();
    }

    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         <--------|*/

    /* PRICE AXIS ALGORITHM:  handles what price labels are used, 
     * their scale, how they are displayed, and how related views should 
     * be adjusted -  i.e where to start( the high price), what increment to 
     * use, how to pad space etc. It runs in its own thread preempting 
     * earlier, concurrent instances of itself. It is called by _init() and the 
     * HighLowChange BroadcastReciever when a new high/low price 
     * signal is received.
     *  
     *  NOTE: this does NOT persist, it is called as needed     *  
     *  TODO: attempt to limit extraneous and excessive calls            
     */
    private void _createPriceAxisY() {
        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     */
                    ///////////////////////////////
                    _updateLtch.await(); /* wait for historic data to be inserted */
                    float rangeDiff = _highPriceDsply - _lowPriceDsply;
                    /* deal with problematic high/low parameters */
                    if (rangeDiff <= 0 || _highPriceDsply < 0 || _lowPriceDsply < 0)
                        if (rangeDiff == 0 && _highPriceDsply > 0) {
                            _highPriceDsply *= 1.001;
                            _lowPriceDsply *= .999;
                            rangeDiff = _highPriceDsply - _lowPriceDsply;
                            _priceSgmnt.setYRange(_highPriceDsply, _lowPriceDsply, _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 TraditionalChartLogicError("rangeDiff and maxNodes can't be negative.");
                        /* call down to our native sub to find increment values */
                        incrVals = MainApplication.IncrementRegressNative(rangeDiff, maxNodes);
                        if (incrVals[0] < 0 || incrVals[1] <= 0)
                            throw new TraditionalChartLogicError("IncrementRegressNative() sub-routine aborted. "
                                    + "retVals[0]: " + incrVals[0] + "retVals[1]: " + incrVals[1] + "adjRangeDiff: "
                                    + rangeDiff + "maxNodes" + maxNodes);
                    } catch (TraditionalChartLogicError e) {
                        Log.e("TraditionalChartLogicError", e.getMessage());
                        return; /* just leave the axis empty*/
                    }

                    /* adjust height to new increment values */
                    final int adjPriceElemHeight = (int) (totalHeight / (int) incrVals[1]);
                    /* we'll need to account for any unused space in the axis */
                    final int vacantHeight = totalHeight - (int) (adjPriceElemHeight * (int) incrVals[1]);
                    double distFromIncr = Math.IEEEremainder((double) _highPriceDsply,
                            (double) incrVals[0]); /* distance from rounded incr */
                    double adjTopNodeVal = (double) _highPriceDsply - distFromIncr;
                    if (distFromIncr > 0) /* be sure to round to the upper incr */
                        adjTopNodeVal += incrVals[0];
                    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) incrVals[1] && (lastNodeVal -= incrVals[0]) > 0);
                    /* values for padding the axis and adjusting the segments */
                    final double adjTopNodeOffset = ((adjTopNodeVal - _highPriceDsply) / rangeDiff) * totalHeight;
                    final double adjBottomNodeOffset = ((_lowPriceDsply - lastNodeVal) / rangeDiff) * totalHeight;
                    final float halfVacantHeight = vacantHeight / 2;
                    final float halfPriceElemHeight = adjPriceElemHeight / 2;
                    final int topBuf = (int) (adjTopNodeOffset + halfVacantHeight + halfPriceElemHeight);
                    final int bottomBuf = (int) (adjBottomNodeOffset + halfVacantHeight + halfPriceElemHeight);
                    /* adjust price segments so they align w/ axis vals */
                    _priceSgmnt.setVerticalBuffers(topBuf, bottomBuf);
                    _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();
                            _priceSgmnt.forceDraw();
                        }
                    });
                    synchronized (_priceAxisThrdMntr) {
                        _priceAxisPrmptCnt = 0;
                    }
                } catch (InterruptedException e) {
                } catch (IllegalStateException e) {
                    Log.d("IllegalState(High, Low): ", "IllegalStateException caught in _createPriceAxisY: "
                            + Float.toString(_highPriceDsply) + " - " + Float.toString(_lowPriceDsply));
                } catch (RuntimeException e) {
                    e.printStackTrace();
                    throw e;
                } finally {
                    synchronized (_priceAxisThrdMntr) {
                        _priceAxisThrd = null;
                    }
                    _activeThreads.remove(this);
                    _priceAxisLck.unlock();
                }
            }
        };
        _activeThreads.add(thisThread);
        thisThread.start();
    }

    /* TIME AXIS ALGORITHM:  this handles what time-stamps should 
     * be placed in the axis, how and when they are inserted, and how 
     * they are positioned relative to themselves and surrounding views.
     *  
     *  NOTE: this DOES persist, waiting for new segment data       
     *  TODO: 
     *      analyze inter-time-view margins to avoid text congestion
     *      consider value sensitive increment adjusts   
     */
    private void _createTimeAxisX() {
        Thread thisThread = new Thread() {
            public void run() {
                if (!_timeAxisLck.tryLock()) /* be sure only one running */
                    return;
                try {
                    int axisWidth;
                    final HideHorizontalLeftOverflowWrapper axisWrapper = (HideHorizontalLeftOverflowWrapper) _myContainer
                            .findViewById(R.id.ofwrap_xAxis);
                    int failCount = 0;
                    /* if we can't get valid width data, exit completely */
                    while ((axisWidth = axisWrapper.getWidth()) == 0 || axisWidth > _myContainer.getWidth()) {
                        Thread.sleep(_axisTimeoutIncr);
                        if ((failCount += _axisTimeoutIncr) >= _axisTimeout)
                            return;
                    }
                    int xElemWidth = 0;
                    final XAxisTimeLabel tvSent = (XAxisTimeLabel) _inflater.inflate(R.layout.x_axis_time_label,
                            null);
                    tvSent.setTag("sentinel_x_axis_view");
                    tvSent.setTextSize(_axisFontSz);
                    /* sacrifice first time-stamp for size info, otherwise block */
                    //TODO: re-think why we have to sacrifice it ??
                    // could be a big deal on narrow charts
                    tvSent.setText(_timeStampQueue.take());
                    /*    Log.d("Data-Consumer-Time-Take",                                 
                    String.valueOf(lastElem) 
                    );*/
                    final ConditionVariable cond = new ConditionVariable();
                    _guiThrdHndlr.post(new Runnable() {
                        public void run() {
                            tvSent.setVisibility(View.INVISIBLE);
                            _timeAxis.addView(tvSent);
                            cond.open();
                        }
                    });
                    cond.block();
                    cond.close();
                    final XAxisTimeLabel tvSentB = (XAxisTimeLabel) _timeAxis
                            .findViewWithTag("sentinel_x_axis_view");
                    failCount = 0;
                    while ((xElemWidth = tvSentB.getWidth()) == 0) {
                        Thread.sleep(_axisTimeoutIncr);
                        if ((failCount += _axisTimeoutIncr) >= _axisTimeout)
                            return;
                    }
                    _guiThrdHndlr.post(new Runnable() {
                        public void run() {
                            _timeAxis.removeView(tvSentB);
                        }
                    });
                    /* how much space we'll need to 'stradle' price segs */
                    int adjElem = xElemWidth + _xAxisPadSz;
                    int reqPriceSegs = (int) Math.ceil((double) adjElem / _priceSegWdth);
                    if ((reqPriceSegs % 2) == 0)
                        ++reqPriceSegs;
                    adjElem = reqPriceSegs * _priceSegWdth;
                    /* how much space is open for  a new view at each iteration*/
                    int openSpace = _priceSegWdth * (reqPriceSegs / 2);
                    /* also, do we need to drop views from the other side ? */
                    int fullSpace = _priceSegWdth * (reqPriceSegs / 2);
                    CharSequence cachedElem = null;
                    int rOffset = _myResources.getDimensionPixelSize(R.dimen.chart_frag_trad_segs_right_padding);
                    /* a view to hold open space that isn't big enough yet */
                    final XAxisTimeLabel spaceHolder = (XAxisTimeLabel) _inflater
                            .inflate(R.layout.x_axis_time_label, null);
                    spaceHolder.setTag("front_adj_x_axis_view");
                    spaceHolder.setVisibility(View.INVISIBLE);

                    /// START MAIN TIME-STAMP UPDATE LOOP ///
                    while (!this.isInterrupted()) {
                        CharSequence lastElem = _timeStampQueue.take();
                        /*    Log.d("Data-Consumer-Time-Take",                                 
                            String.valueOf(lastElem) 
                            );*/
                        openSpace += _priceSegWdth;
                        /* if no open space but need THIS element */
                        if ((openSpace >= (adjElem / 2)) && cachedElem == null)
                            cachedElem = lastElem;
                        /* if this pushes past the container limits, drop oldest */
                        if ((fullSpace + _priceSegWdth) > (axisWidth - rOffset)) {
                            _guiThrdHndlr.post(new Runnable() {
                                public void run() { /* be sure its big enough */
                                    if (_timeAxis.getChildCount() > 0)
                                        _timeAxis.removeViewAt(0);
                                    _timeAxis.forceLayout();
                                }
                            });
                            fullSpace -= adjElem;
                        }
                        /* if element cached and open space for it is large enough */
                        if ((cachedElem != null) && openSpace == adjElem) {
                            final XAxisTimeLabel tvVal = (XAxisTimeLabel) _inflater
                                    .inflate(R.layout.x_axis_time_label, null);
                            tvVal.setTag("valid_x_axis_view");
                            tvVal.setTextSize(_axisFontSz);
                            tvVal.setTextColor(_axisFontColor);
                            tvVal.setText(cachedElem);
                            tvVal.setWidth(adjElem);
                            _guiThrdHndlr.post(new Runnable() {
                                public void run() {
                                    /* remove the view holding space */
                                    _timeAxis.removeView(spaceHolder);
                                    _timeAxis.addView(tvVal);
                                    spaceHolder.setWidth(0);
                                    spaceHolder.invalidate();
                                    /* after adjusting re-insert at front */
                                    _timeAxis.addView(spaceHolder);
                                    _timeAxis.forceLayout();
                                }
                            });
                            /* no open space and that much more full space */
                            cachedElem = null;
                            openSpace = 0;
                            fullSpace += _priceSegWdth;
                        } else { /* if no element cached or not enough space */
                            final int fOpenSpace = openSpace;
                            _guiThrdHndlr.post(new Runnable() {
                                public void run() {
                                    /* shift all views left regardless */
                                    spaceHolder.setWidth(fOpenSpace);
                                    spaceHolder.invalidate();
                                    _timeAxis.forceLayout();
                                }
                            });
                            fullSpace += _priceSegWdth;
                        }
                    }
                    ///    END MAIN TIME-STAMP UPDATE LOOP    ///                        
                } catch (InterruptedException e) {
                } catch (RuntimeException e) {
                    throw e;
                } finally {
                    _activeThreads.remove(this);
                    _timeAxisLck.unlock();
                }
            }
        };
        _activeThreads.add(thisThread);
        thisThread.start();
    }

    /* CREATE VOLUME AXIS */
    private void _createVolumeAxis() {
        Thread thisThread = new Thread() {
            public void run() {
                if (!_volumeAxisLck.tryLock())
                    return;
                try {
                    _updateLtch.await();
                    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 tvHeight = 0;
                    int failCount = 0;
                    while ((tvHeight = tv.getHeight() / 2) == 0) {
                        Thread.sleep(_axisTimeoutIncr);
                        if ((failCount += _axisTimeoutIncr) >= _axisTimeout)
                            return; /* if timeout forget about align */
                    }
                    _volumeSgmnt.setVerticalBuffers(tvHeight, 0);
                    _guiThrdHndlr.post(new Runnable() {
                        public void run() {
                            _volumeSgmnt.forceDraw();
                        }
                    });
                } catch (InterruptedException e) {
                } catch (RuntimeException e) {
                    throw e;
                } finally {
                    _activeThreads.remove(this);
                    _volumeAxisLck.unlock();
                }
            }
        };
        _activeThreads.add(thisThread);
        thisThread.start();
    }

    /* CREATE PRICE PANEL */
    private void _createPricePanel() {
        Thread thisThread = new Thread() {
            public void run() {
                /* can't bail(risk deadlocking on the count-down latch) */
                _priceSgmntLck.lock();
                try {
                    (new Thread() {
                        public void run() {
                            try {
                                _updateLtch.await();
                                _adjustDisplayPrices();
                                _priceSgmnt.setYRange(_highPriceDsply, _lowPriceDsply, _highPrice, _lowPrice);
                                _createPriceAxisY();
                                _priceSgmnt.forceDraw();
                                drawPriceNow = true;
                            } catch (InterruptedException e) {
                            }
                        }
                    }).start();
                    drawPriceNow = false;
                    while (!this.isInterrupted()) {
                        _lastPriceSeg = _ohlcQueue.take(); /*
                                                           Log.d("Data-Consumer-Price-Take",                                 
                                                           _lastPriceSeg.toString()                                
                                                           );*/
                        _hasSegment.set(true);
                        if (_lastPriceSeg.high > _highPrice) {
                            _highPrice = _lastPriceSeg.high;
                            if (_highPrice >= _highPriceDsply)
                                _highPriceDsply = _highPrice;
                        }
                        if (_lastPriceSeg.low < _lowPrice) {
                            _lowPrice = _lastPriceSeg.low;
                            if (_lowPrice <= _lowPriceDsply)
                                _lowPriceDsply = _lowPrice;
                        }
                        _priceSgmnt.addSubSegment(_lastPriceSeg, drawPriceNow);
                        _updateLtch.countDown();
                    }
                } catch (InterruptedException e) {
                    _priceSgmnt.clear();
                } catch (RuntimeException e) {
                    throw e;
                } finally {
                    _activeThreads.remove(this);
                    _priceSgmntLck.unlock();
                }
            }
        };
        _activeThreads.add(thisThread);
        thisThread.start();
    }

    /* CREATE VOLUME PANEL */
    private void _createVolumePanel() {
        Thread thisThread = new Thread() {
            public void run() {
                /* can't bail(risk deadlocking on the count-down latch) */
                _volumeSgmntLck.lock();
                try {
                    (new Thread() {
                        public void run() {
                            try {
                                _updateLtch.await();
                                _volumeSgmnt.setYRange(_highVolume, 0, _highVolume, 0);
                                _volumeSgmnt.forceDraw();
                                _createVolumeAxis();
                                drawVolumeNow = true;
                            } catch (InterruptedException e) {
                            }
                        }
                    }).start();
                    drawVolumeNow = false;
                    while (!this.isInterrupted()) {
                        _lastVolSeg = _volumeQueue.take();
                        if (_lastVolSeg > _highVolume) {
                            _highVolume = _lastVolSeg;
                            _volumeSgmnt.setYRange(_highVolume, 0, _highVolume, 0);
                        }
                        _volumeSgmnt.addSubSegment(new OHLC(0, _lastVolSeg, 0, _lastVolSeg), drawVolumeNow);
                        _updateLtch.countDown();
                    }
                } catch (InterruptedException e) {
                    _volumeSgmnt.clear();
                } catch (RuntimeException e) {
                    throw e;
                } finally {
                    _activeThreads.remove(this);
                    _volumeSgmntLck.unlock();
                }
            }
        };
        _activeThreads.add(thisThread);
        thisThread.start();
    }

    private void _populateQueues(final int cursorStartPos) {
        Thread thisThread = new Thread() {
            public void run() {
                /* can't bail(risk deadlocking on the count-down latch) */
                _populateLck.lock();
                try {
                    if (cursorStartPos > 0) /* back one because moveToNext() ) */
                        _myCursor.moveToPosition(cursorStartPos - 1);
                    else
                        _myCursor.moveToPosition(-1);
                    int mergeNum = _chartFreq.value / DataClientInterface.TIME_GRANULARITY;
                    /* reset _lastPutTime */
                    _lastPutTime = null;
                    // DEBUG
                    DataContentService.dumpDatabaseTableToLogCat(_symbol, _thisConsumer);
                    /* start on a clean seg , i.e mod freq == 0 */
                    boolean cleanStart = true;
                    Time time = new Time();
                    while (_myCursor.moveToNext()) {
                        time.set(_myCursor.getLong(0));
                        if (time.minute % mergeNum == 0)
                            break;
                        cleanStart = false;
                    }
                    _myCursor.moveToPrevious();
                    OHLC[] prices = new OHLC[mergeNum];
                    int[] vols = new int[mergeNum];

                    time.set(0);
                    int modCount = 0;
                    // issue will null time if cursor is done here !! 
                    // we throw a *LogicError if its a problem later.
                    while (_myCursor.moveToNext() && !this.isInterrupted()) {
                        prices[modCount] = new OHLC(_myCursor.getFloat(1), _myCursor.getFloat(2),
                                _myCursor.getFloat(3), _myCursor.getFloat(4));
                        vols[modCount] = _myCursor.getInt(5);
                        if (modCount == 0)
                            time.set(_myCursor.getLong(0));
                        if (modCount + 1 >= mergeNum) {
                            OHLC mPrices = _mergePrices(prices);
                            int mVols = _mergeVolumes(vols);
                            //_timeStampQueue.put( time.format("%H:%M") );
                            /* time must come first since it may fill gaps in price/vol */
                            _putTime(time);
                            _ohlcQueue.put(mPrices);
                            _volumeQueue.put(mVols);
                            modCount = 0;/*
                                         Log.d("Data-Consumer-Populate", 
                                         mPrices.toString +"   " +
                                         String.valueOf(mVols) + "   " + time );*/
                        } else
                            ++modCount;
                    }
                    /* don't forget about an unclean end */
                    if (modCount != 0) {
                        // _timeStampQueue.put( time.format("%H:%M") );
                        /* time must come first since it may fill gaps in price/vol */
                        _putTime(time);
                        _ohlcQueue.put(_mergePrices(Arrays.copyOf(prices, modCount)));
                        _volumeQueue.put(_mergeVolumes(Arrays.copyOf(vols, modCount)));
                        /*
                        Log.d("Data-Consumer-Populate", 
                            _lastPriceSeg.toString()  +"   " +
                            String.valueOf(_lastVolSeg) + "   " + time );*/
                    }
                    if (time.toMillis(true) == 0)
                        throw new TraditionalChartLogicError("attempt to evaluate a null time  in PopulateQueues; "
                                + "its possible there were no granular segs after cleaning "
                                + "the non-logically aligned front segs, and thus no valid time.");
                    _lastSegTime.setToNow();
                    _lastSegTime.hour = time.hour;
                    _lastSegTime.minute = time.minute;
                    // _updateTime.setToNow();  
                    _streamReady.set(true);
                    /* unclean start needs extra counts to avoid deadlock */
                    for (int i = 0; !cleanStart && i < 2; ++i)
                        _updateLtch.countDown();
                } catch (InterruptedException e) {
                    _clear();
                } catch (RuntimeException e) {
                    _clear();
                    throw e;
                } finally {
                    _activeThreads.remove(this);
                    _populateLck.unlock();
                }
            }
        };
        _activeThreads.add(thisThread);
        thisThread.start();
    }

    /* check if we've skipped any segs; this doesn't re-draw skipped 
     * segs, it just adds blanks to indicate the data is inconsistent, user 
     * can force chart to reload the cursor and re-draw the 'missing' segs 
     * if they exist (i.e the data back-end was able to deliver the data)
    /* [[ DST issues in here ]]*/
    private void _putTime(Time t) throws InterruptedException {
        if (_lastPutTime == null) { /* if first, ignore everything else */
            _lastPutTime = new Time();
        } else { /* otherwise fill in the gaps */
            long oldTime = _lastPutTime.toMillis(true);
            long newTime = t.toMillis(true);

            /* round to avoid getting screwed over a few ms */
            int minDiff = Math.round((float) (newTime - oldTime) / (float) (_chartFreq.value * 60000));

            Log.d("DataContentService-1",
                    "Old Time: " + _lastPutTime.format("%H:%M:%S") + "    ,    New Time: " + t.format("%H:%M:%S"));

            while (minDiff > _chartFreq.value) {
                _lastPutTime.minute += _chartFreq.value;
                /* the following  'handles' a unique sync issue where the cursor is loaded,
                 * a new granular seg is inserted into the database AND THEN a new 
                 * full seg is 'put' into the queues leading to the new full seg being skipped.
                 *      1) this happens around the minute mark (when segs roll)
                 *      2) we could (and may) simply re-load the cursor
                 *      3) not sure how this works with full segs > 1 min
                 */
                if (_chartFreq.value == DataClientInterface.TIME_GRANULARITY && minDiff == (2 * _chartFreq.value)) {
                    _ohlcQueue.put(new OHLC(_penultGranularPriceSeg));
                    _volumeQueue.put(_penultGranularVolSeg);
                } else {
                    _ohlcQueue.put(new OHLC(_lastPriceSeg.open));
                    _volumeQueue.put(0);
                }

                _timeStampQueue.put(_lastPutTime.format("%H:%M"));
                minDiff -= _chartFreq.value;
            }

        }
        _timeStampQueue.put(t.format("%H:%M"));
        _lastPutTime.set(t);
    }

    private OHLC _mergePrices(OHLC[] vals) {
        int len = vals.length;
        OHLC tmp = new OHLC(vals[0]);
        for (int i = 1; i < len; ++i) {
            if (vals[i].high > tmp.high)
                tmp.high = vals[i].high;
            if (vals[i].low < tmp.low)
                tmp.low = vals[i].low;
        }
        tmp.close = vals[len - 1].close;
        return tmp;
    }

    private int _mergeVolumes(int[] vals) {
        int total = 0;
        int len = vals.length;
        for (int i = 0; i < len; ++i)
            total += vals[i];
        return total;
    }

    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 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;
    }

    /* adjust the high/low display to provider some vertical cushion */
    private synchronized void _adjustDisplayPrices() {
        if (_lowPrice <= _highPrice) {
            float avg = (_highPrice + _lowPrice) / 2;
            float mid = (_highPrice - _lowPrice) / 2;
            _highPriceDsply = avg + (mid * DEF_DISPLAY_MULTIPLE);
            _lowPriceDsply = avg - (mid * DEF_DISPLAY_MULTIPLE);
            if (_lowPriceDsply < 0) {
                _lowPriceDsply = 0;
                _highPriceDsply = _lowPrice + _highPrice;
            }
        } else {
            _highPriceDsply = _highPrice;
            _lowPriceDsply = _lowPrice;
        }
    }

    private void _setPricePanelDrawSemantics() {
        switch (_chartType) {
        case OC:
            _priceSgmnt.setDrawSemantics(DrawSemantics_OC.class);
            break;
        case LINE:
            _priceSgmnt.setDrawSemantics(DrawSemantics_LINE.class);
            break;
        case CANDLE:
            _priceSgmnt.setDrawSemantics(DrawSemantics_CANDLE.class);
            break;
        case POINT:
            _priceSgmnt.setDrawSemantics(DrawSemantics_POINT.class);
            break;
        case SILO:
            _priceSgmnt.setDrawSemantics(DrawSemantics_SILO.class);
            break;
        default:
            _priceSgmnt.setDrawSemantics(DrawSemantics_OHLC.class);
            break;
        }
    }

    /* update range fields (_highPrice etc.) on segment removal */
    private final void _setCallbacks() {
        this._priceSgmnt.setOnRemoveCallBack(new TraditionalChartPanel.CallBackOnRemove() {
            public void call(Number high, Number low) {
                float m = (_highPriceDsply - _lowPriceDsply) / (_highPrice - _lowPrice);
                _highPrice = high.floatValue();
                _lowPrice = low.floatValue();
                float actualDiff = (_highPrice - _lowPrice);
                float displayDiff = actualDiff * m;
                float midDiffDiff = (displayDiff - actualDiff) / 2;
                if ((_lowPriceDsply = _lowPrice - midDiffDiff) < 0) {
                    _lowPriceDsply = 0;
                    midDiffDiff = _lowPrice;
                }
                _highPriceDsply = _highPrice + midDiffDiff;
                _createPriceAxisY();
            }
        });
        this._volumeSgmnt.setOnRemoveCallBack(new TraditionalChartPanel.CallBackOnRemove() {
            public void call(Number high, Number low) {
                _highVolume = high.intValue();
                _createVolumeAxis();
            }
        });
    }

    private void _init() {
        if (_myCursor != null) {
            /* DataContentService.dumpDatabaseTableToLogCat(
            _symbol, (DataConsumerInterface)this
            );*/
            int cursorCount = _myCursor.getCount();
            final int INCR = ApplicationPreferences.getTimeoutIncrement();
            final int TOUT = ApplicationPreferences.getLongTimeout();
            int timeOutCount = 0;
            int measuredSegW = 0;
            while ((measuredSegW = _priceSgmnt.getMeasuredWidth()) == 0 && (timeOutCount += INCR) < TOUT)
                try {
                    Thread.sleep(INCR);
                } catch (InterruptedException e) {
                }
            int mergeCount = _chartFreq.value / DataClientInterface.TIME_GRANULARITY;
            int rowsNeeded = /* add 2 extra */
                    (measuredSegW * mergeCount / _priceSegWdth) + 2;
            try {
                _populateLck.lock();
                _myCursor.moveToPosition(-1);
                _priceSgmntLck.lock();
                _volumeSgmntLck.lock();
                _updateLtch = new CountDownLatch(Math.min(rowsNeeded, cursorCount) / mergeCount * 2);
            } finally {
                _populateLck.unlock();
                _priceSgmntLck.unlock();
                _volumeSgmntLck.unlock();
            }
            _createTimeAxisX();
            _createPriceAxisY();
            _createVolumeAxis();
            _createPricePanel();
            _createVolumePanel();
            _populateQueues(cursorCount - rowsNeeded);
        } else
            throw new IllegalStateException(
                    "ChartFragmentAdapter can not be initialized " + "without a valid Cursor");
    }

    private void _clear() {
        _activeThreads.clear();
        _priceSgmnt.clear();
        _volumeSgmnt.clear();
        _timeAxis.removeAllViewsInLayout();
        _priceAxis.removeAllViewsInLayout();
        _volumeAxis.removeAllViewsInLayout();
    }

    private void _reset() {
        cleanUpAndUpdate();
        _init();
    }

    /*|--------->         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(_myId))
                return;
            String action = i.getAction();
            if (action.equals(HighLowChange.NEW_HIGH_PRICE)) {
                _highPrice = i.getFloatExtra(EXTRA_VALUE, _highPrice);
                if (_highPrice > _highPriceDsply) {
                    _adjustDisplayPrices();
                    _priceSgmnt.setYRange(_highPriceDsply, _lowPriceDsply, _highPrice, _lowPrice);
                    _priceSgmnt.forceDraw();
                    _createPriceAxisY();
                }
            } else if (action.equals(HighLowChange.NEW_LOW_PRICE)) {
                _lowPrice = i.getFloatExtra(EXTRA_VALUE, _lowPrice);
                if (_lowPrice < _lowPriceDsply) {
                    _adjustDisplayPrices();
                    _priceSgmnt.setYRange(_highPriceDsply, _lowPriceDsply, _highPrice, _lowPrice);
                    _priceSgmnt.forceDraw();
                    _createPriceAxisY();
                }
            } else if (action.equals(HighLowChange.NEW_HIGH_VOL)) {
                _highVolume = i.getIntExtra(EXTRA_VALUE, _highVolume);
                _volumeSgmnt.setYRange(_highVolume, 0, _highVolume, 0);
                _volumeSgmnt.forceDraw();
                _createVolumeAxis();
            }
        }
    }

}