org.mythdroid.activities.Guide.java Source code

Java tutorial

Introduction

Here is the source code for org.mythdroid.activities.Guide.java

Source

/*
MythDroid: Android MythTV Remote
Copyright (C) 2009-2010 foobum@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/>.
*/

package org.mythdroid.activities;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Pattern;

import org.mythdroid.Globals;
import org.mythdroid.R;
import org.mythdroid.Enums.Extras;
import org.mythdroid.Enums.Category;
import org.mythdroid.data.Channel;
import org.mythdroid.data.Program;
import org.mythdroid.data.XMLHandler;
import org.mythdroid.data.Channel.ChannelListener;
import org.mythdroid.data.Channel.ChannelXMLParser;
import org.mythdroid.data.XMLHandler.Element;
import org.mythdroid.remote.TVRemote;
import org.mythdroid.resource.Messages;
import org.mythdroid.services.GuideService;
import org.mythdroid.util.ErrUtil;
import org.mythdroid.util.HttpFetcher;
import org.mythdroid.util.LogUtil;
import org.xml.sax.SAXException;

import android.R.drawable;
import android.annotation.SuppressLint;
import android.app.DatePickerDialog;
import android.app.Dialog;
import android.app.TimePickerDialog;
import android.app.DatePickerDialog.OnDateSetListener;
import android.app.TimePickerDialog.OnTimeSetListener;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.sax.EndTextElementListener;
import android.support.v4.view.MenuItemCompat;
import android.util.Xml;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.widget.DatePicker;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.Scroller;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import android.widget.TimePicker;
import android.widget.TableRow.LayoutParams;

/** Display a program guide */
@SuppressLint("SimpleDateFormat")
public class Guide extends MDActivity {

    final private static int MENU_DATE = 0, MENU_TIME = 1;
    final private static int DIALOG_DATE = 0, DIALOG_TIME = 1;

    final private static Pattern catPat = Pattern.compile("[\\s/-]"); //$NON-NLS-1$

    final private SimpleDateFormat date = new SimpleDateFormat("d MMM yy"), //$NON-NLS-1$
            time = new SimpleDateFormat("HH:mm"); //$NON-NLS-1$

    final private LayoutParams rowLayout = new LayoutParams(), chanLayout = new LayoutParams(),
            hdrDateLayout = new LayoutParams(), hdrTimeLayout = new LayoutParams(),
            spacerLayout = new LayoutParams();

    final private Handler handler = new Handler();

    final private static int colMins = 5, hdrSpan = 6;

    /** ArrayList of channel objects, capacity ensured during XML parsing */
    private ArrayList<Channel> channels = new ArrayList<Channel>();

    /**
     * Change numHours to configure how many hours are displayed at a time 
     * This value is doubled for devices with one screen dimension > 1000 pixels 
     */
    private int numHours = 2, numTimes;
    private Date now = null, later = null;
    private long[] times = null;
    private String[] hdrTimes = null;
    private String hdrDate = null;
    private TableLayout tbl = null;
    /** Scale factor for pixel values for different display densities */
    private float scale = 1;
    /**
     * Tweak colWidth to alter the visible width of the columns
     * Tweak rowHeight to alter the visible height of rows
     */
    private int colWidth, rowHeight, chanWidth, hOff, vOff;

    private Drawable recordedIcon = null, willRecordIcon = null, failedIcon = null, conflictIcon = null,
            otherIcon = null;

    private HorizontalScrollView hScroll = null;
    private ScrollView vScroll = null;
    private GestureDetector gDetector = null;
    private GuideService guideService = null;

    private class GuideGestureListener extends SimpleOnGestureListener {

        final private float minFlingSpeed = 300 * scale;
        private Scroller scroller = new Scroller(ctx);
        private MotionEvent lastDown = null;

        // A runnable that animates flings by scrolling the scroll views
        private Runnable animateFling = new Runnable() {
            @Override
            public void run() {
                if (scroller.isFinished())
                    return;
                boolean more = scroller.computeScrollOffset();
                hScroll.scrollTo(scroller.getCurrX(), 0);
                vScroll.scrollTo(0, scroller.getCurrY());
                if (more)
                    handler.post(this);
            }
        };

        @Override
        public boolean onDown(MotionEvent me) {
            // Save this down so we can send it on if it turns out to be a tap
            if (lastDown != null)
                lastDown.recycle();
            lastDown = MotionEvent.obtain(me);
            // Abort any in-progress flings
            scroller.forceFinished(true);
            return true;
        }

        @Override
        public boolean onSingleTapUp(MotionEvent me) {
            // Dispatch both the down and up events to the view hierarchy
            superDispatchTouchEvent(lastDown);
            superDispatchTouchEvent(me);
            lastDown.recycle();
            lastDown = null;
            return true;
        }

        @Override
        public boolean onFling(MotionEvent start, MotionEvent end, float vX, float vY) {
            // Check it was fast enough
            if (Math.abs(vX) < minFlingSpeed)
                vX = 0;
            if (Math.abs(vY) < minFlingSpeed)
                vY = 0;
            // Check for noop
            if (vX == 0 && vY == 0)
                return true;
            int height = vScroll.getHeight();
            int bottom = vScroll.getChildAt(0).getHeight();
            int width = hScroll.getWidth();
            int right = hScroll.getChildAt(0).getWidth();
            // Set the scroller going
            scroller.fling(hScroll.getScrollX(), vScroll.getScrollY(), (int) -vX, (int) -vY, 0,
                    Math.max(0, right - width), 0, Math.max(0, bottom - height));
            // Animate the fling
            handler.post(animateFling);
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent down, MotionEvent move, float dX, float dY) {
            hScroll.scrollBy((int) dX, 0);
            vScroll.scrollBy(0, (int) dY);
            return true;
        }
    };

    /** Get and sort the list of channels, add them to table in UI thread */
    final private Runnable getData = new Runnable() {
        @Override
        public void run() {
            getGuideData(now, later);
            Collections.sort(channels);
            handler.post(new Runnable() {
                @Override
                public void run() {
                    displayGuideData();
                }
            });
        }
    };

    final private OnClickListener progClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            Globals.curProg = (Program) v.getTag();
            startActivityForResult(
                    new Intent().putExtra(Extras.GUIDE.toString(), true).setClass(ctx, RecordingDetail.class), 0);
        }
    };

    final private OnClickListener chanClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            startActivity(new Intent().putExtra(Extras.LIVETV.toString(), true)
                    .putExtra(Extras.JUMPCHAN.toString(), (Integer) v.getTag()).setClass(ctx, TVRemote.class));
        }
    };

    final private OnLongClickListener chanLongClickListener = new OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            setExtra(Extras.LIVETV.toString());
            setExtra(Extras.JUMPCHAN.toString(), (Integer) v.getTag());
            nextActivity = TVRemote.class;
            showDialog(FRONTEND_CHOOSER);
            return true;
        }
    };

    @Override
    public void onCreate(Bundle icicle) {

        super.onCreate(icicle);
        setContentView(R.layout.guide);

        hScroll = (HorizontalScrollView) findViewById(R.id.guidehscroll);
        vScroll = (ScrollView) findViewById(R.id.guidevscroll);

        Resources res = getResources();

        scale = res.getDisplayMetrics().density;

        gDetector = new GestureDetector(this, new GuideGestureListener());

        if (getWindowManager().getDefaultDisplay().getWidth() > 1000
                || getWindowManager().getDefaultDisplay().getHeight() > 1000)
            numHours *= 2;

        numTimes = numHours * 60 / colMins;

        times = new long[numTimes + 1];
        hdrTimes = new String[numTimes / hdrSpan];

        colWidth = (int) (40 * scale + 0.5f);
        rowHeight = (int) (60 * scale + 0.5f);
        chanWidth = (int) (100 * scale + 0.5f);

        tbl = (TableLayout) findViewById(R.id.table);

        rowLayout.topMargin = rowLayout.bottomMargin = chanLayout.topMargin = chanLayout.bottomMargin = chanLayout.leftMargin = chanLayout.rightMargin = hdrDateLayout.leftMargin = hdrDateLayout.rightMargin = hdrTimeLayout.leftMargin = hdrTimeLayout.rightMargin = 1;

        rowLayout.height = chanLayout.height = rowHeight;

        chanLayout.column = hdrDateLayout.column = 0;
        chanLayout.span = hdrDateLayout.span = 1;
        chanLayout.width = hdrDateLayout.width = chanWidth;

        hdrTimeLayout.width = colWidth * hdrSpan;
        hdrTimeLayout.span = hdrSpan;

        spacerLayout.height = 1;
        spacerLayout.width = colWidth;
        spacerLayout.span = 1;

        recordedIcon = res.getDrawable(R.drawable.recorded);
        willRecordIcon = res.getDrawable(R.drawable.willrecord);
        failedIcon = res.getDrawable(R.drawable.failed);
        conflictIcon = res.getDrawable(R.drawable.conflict);
        otherIcon = res.getDrawable(R.drawable.other);

        date.setTimeZone(TimeZone.getDefault());
        time.setTimeZone(TimeZone.getDefault());

        if (Globals.haveServices())
            try {
                guideService = new GuideService(Globals.getBackend().addr);
            } catch (IOException e) {
                ErrUtil.err(this, e);
                finish();
                return;
            }

    }

    @Override
    public void onResume() {
        super.onResume();
        displayGuide(new Date());
    }

    @Override
    public void onPause() {
        super.onPause();
        Globals.removeThreadPoolTask(getData);
        hOff = hScroll.getScrollX();
        vOff = vScroll.getScrollY();
        // Free up some memory
        tbl.removeAllViews();
        channels.clear();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuItemCompat.setShowAsAction(
                menu.add(Menu.NONE, MENU_DATE, Menu.NONE, R.string.chStartDate).setIcon(drawable.ic_menu_today),
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
        MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, MENU_TIME, Menu.NONE, R.string.chStartTime)
                .setIcon(drawable.ic_menu_recent_history), MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        switch (item.getItemId()) {
        case MENU_DATE:
            showDialog(DIALOG_DATE);
            return true;
        case MENU_TIME:
            showDialog(DIALOG_TIME);
            return true;
        }

        return super.onOptionsItemSelected(item);

    }

    @Override
    public Dialog onCreateDialog(int id) {

        switch (id) {

        case DIALOG_DATE:
            return new DatePickerDialog(this, new OnDateSetListener() {
                private int Year = -1, Month = -1, Day = -1;

                @Override
                public void onDateSet(DatePicker view, int year, int month, int day) {
                    if (year == Year && month == Month && day == Day)
                        return;
                    Year = year;
                    Month = month;
                    Day = day;
                    now.setYear(year - 1900);
                    now.setMonth(month);
                    now.setDate(day);
                    displayGuide(now);
                }
            }, now.getYear() + 1900, now.getMonth(), now.getDate());

        case DIALOG_TIME:
            return new TimePickerDialog(this, new OnTimeSetListener() {
                private int Hour = -1, Min = -1;

                @Override
                public void onTimeSet(TimePicker view, int hour, int min) {
                    if (hour == Hour && min == Min)
                        return;
                    Hour = hour;
                    Min = min;
                    now.setHours(hour);
                    now.setMinutes(min);
                    displayGuide(now);
                }
            }, now.getHours(), now.getMinutes(), true);

        default:
            return super.onCreateDialog(id);

        }

    }

    @Override
    public void onPrepareDialog(int id, Dialog dialog) {

        switch (id) {

        case DIALOG_DATE:
            ((DatePickerDialog) dialog).updateDate(now.getYear() + 1900, now.getMonth(), now.getDate());
            break;

        case DIALOG_TIME:
            ((TimePickerDialog) dialog).updateTime(now.getHours(), now.getMinutes());
            break;

        default:
            super.onPrepareDialog(id, dialog);

        }

    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent me) {
        return gDetector.onTouchEvent(me);
    }

    private boolean superDispatchTouchEvent(MotionEvent me) {
        return super.dispatchTouchEvent(me);
    }

    /**
     * Display the guide
     * @param when Date to start at
     */
    private void displayGuide(Date when) {

        showDialog(DIALOG_LOAD);

        tbl.removeAllViews();
        channels.clear();

        // round down to last 30 min boundary
        long nowTime = when.getTime();
        nowTime -= nowTime % 1000;
        now = new Date(nowTime);
        now.setSeconds(0);
        if (now.getMinutes() >= 30)
            now.setMinutes(30);
        else
            now.setMinutes(0);
        nowTime = now.getTime();
        hdrDate = date.format(now);

        long laterTime = nowTime + numHours * 3600000;
        later = new Date(laterTime);

        int j = 1;
        // 0th column is channel name
        times[0] = 0;
        // rest are at 5 min intervals
        for (long i = nowTime; i < laterTime; i += colMins * 60000)
            times[j++] = i;

        j = 1;

        for (int i = 0; i < hdrTimes.length; i++) {
            hdrTimes[i] = time.format(new Date(times[j]));
            j += hdrSpan;
        }

        Globals.runOnThreadPool(getData);

    }

    /**
     * Fetch and parse guide data from the backend
     * @param start Date that guide should start at
     * @param end Date that guide should end at
     */
    private void getGuideData(Date start, Date end) {

        if (Globals.haveServices()) {
            try {
                channels = guideService.GetProgramGuide(start, end);
            } catch (IOException e) {
                ErrUtil.postErr(this, e);
                return;
            }
            return;
        }

        // No services api - use MythXML

        XMLHandler handler = new XMLHandler("GetProgramGuideResponse"); //$NON-NLS-1$
        Element root = handler.rootElement();

        root.getChild("NumOfChannels").setTextElementListener( //$NON-NLS-1$
                new EndTextElementListener() {
                    @Override
                    public void end(String body) {
                        channels.ensureCapacity(Integer.valueOf(body));
                    }
                });

        Element chanElement = root.getChild("ProgramGuide") //$NON-NLS-1$
                .getChild("Channels") //$NON-NLS-1$
                .getChild("Channel"); //$NON-NLS-1$

        chanElement.setStartElementListener(new ChannelXMLParser(this, chanElement, new ChannelListener() {
            @Override
            public void channel(Channel chan) {
                channels.add(chan);
            }
        }));

        HttpFetcher fetcher = null;

        try {

            URL url = new URL(Globals.getBackend().getStatusURL() + "/Myth/GetProgramGuide?" + //$NON-NLS-1$
                    "StartTime=" + Globals.dateFormat(start) + //$NON-NLS-1$
                    "&EndTime=" + Globals.dateFormat(end) + //$NON-NLS-1$
                    "&StartChanId=0" + "&NumOfChannels=-1" + "&Details=1" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            );

            LogUtil.debug("Fetching XML from " + url.toString()); //$NON-NLS-1$

            fetcher = new HttpFetcher(url.toString(), Globals.muxConns);
            InputStream is = fetcher.getInputStream();
            if (is == null)
                throw new IOException(Messages.getString("Guide.0")); //$NON-NLS-1$

            Xml.parse(is, Xml.Encoding.UTF_8, handler);

        } catch (SAXException e) {
            ErrUtil.postErr(this, Messages.getString("Guide.13")); //$NON-NLS-1$
        } catch (IOException e) {
            ErrUtil.postErr(this, e);
        } finally {
            if (fetcher != null)
                try {
                    fetcher.endStream();
                } catch (IOException e) {
                }
        }

    }

    private void displayGuideData() {

        dismissLoadingDialog();

        tbl.addView(getHeader());
        // this is necessary to get proper layout
        tbl.addView(getSpacer());

        int j = 0;
        int maxChan = channels.size();

        for (int i = 0; i < maxChan; i++) {

            Channel current = channels.get(i);
            if (current.num.length() == 0)
                continue;

            /*
             * MythTV 0.24 sometimes splits programs amongst
             * channels with different ids but the same num
             * and callsign.. :/
             */
            if (i < maxChan - 1) {

                // Luckily, they're always adjacent
                Channel next = channels.get(i + 1);

                /* 
                 * See if the next channel has the same num
                 * and callsign
                 */
                if (current.num.equals(next.num) && current.callSign.equals(next.callSign)) {
                    /* 
                     * It does, add all our programs to it and
                     * skip the current channel
                     */
                    next.programs.addAll(current.programs);
                    continue;
                }

            }

            // Add a header every 7 rows
            if (j++ == 7) {
                tbl.addView(getHeader());
                j = 0;
            }

            // This became necessary in MythTV 0.24
            Collections.sort(current.programs);
            tbl.addView(getRowFromChannel(current));

        }

        /* Restore the scroll position, we have to post this otherwise it
           has no effect.. The delay might not be required, used as a safety */
        if (hOff != 0)
            hScroll.postDelayed(new Runnable() {
                @Override
                public void run() {
                    hScroll.scrollTo(hOff, 0);
                }
            }, 25);

        if (vOff != 0)
            vScroll.postDelayed(new Runnable() {
                @Override
                public void run() {
                    vScroll.scrollTo(0, vOff);
                }
            }, 25);

    }

    /**
     * Add a small indicator to the provided TextView based on the Program
     * recording status
     * @param tv TextView to add an indicator to
     * @param prog Program to retrieve a recording status from
     */
    private void setStatusDrawable(TextView tv, Program prog) {

        Drawable icon = null;

        if (prog.Status == null)
            return;

        switch (prog.Status) {
        case RECORDED:
        case CURRENT:
            icon = recordedIcon;
            break;
        case WILLRECORD:
        case RECORDING:
        case TUNING:
        case OTHERTUNING:
        case OTHERRECORDING:
            icon = willRecordIcon;
            break;
        case CANCELLED:
        case ABORTED:
        case DONTRECORD:
        case FAILED:
        case TUNERBUSY:
        case MISSED:
        case MISSEDFUTURE:
        case LOWSPACE:
            icon = failedIcon;
            break;
        case CONFLICT:
            icon = conflictIcon;
            break;
        case EARLIER:
        case LATER:
        case OTHER:
            icon = otherIcon;
            break;
        }

        if (icon != null) {
            tv.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
            tv.setCompoundDrawablePadding(8);
        }

    }

    /**
     * Generate a TableRow from the provided Channel
     * @param ch Channel to generate a row for
     * @return populated TableRow representing the channel
     */
    @SuppressLint("DefaultLocale")
    private TableRow getRowFromChannel(Channel ch) {

        final TableRow row = new TableRow(this);
        row.setLayoutParams(rowLayout);

        TextView tv = new TextView(this);
        tv.setBackgroundColor(0xffe0e0f0);
        tv.setTextColor(0xff202020);
        tv.setPadding(4, 4, 4, 4);
        tv.setMaxLines(2);
        tv.setTag(ch.ID);
        tv.setText(ch.num + " " + ch.callSign); //$NON-NLS-1$
        tv.setOnClickListener(chanClickListener);
        tv.setOnLongClickListener(chanLongClickListener);
        tv.setLayoutParams(chanLayout);

        row.addView(tv);

        LayoutParams layout = null;

        Program[] progs = ch.programs.toArray(new Program[ch.programs.size()]);
        int numprogs = progs.length;

        for (int i = 0; i < numprogs; i++) {

            if (progs[i].StartTime.equals(later))
                continue;

            tv = new TextView(this);
            layout = new LayoutParams(this, null);
            layout.topMargin = layout.bottomMargin = layout.leftMargin = layout.rightMargin = 1;
            layout.height = rowHeight;

            String cat = catPat.matcher(progs[i].Category.toLowerCase()).replaceAll(""); //$NON-NLS-1$

            try {
                tv.setBackgroundColor(Category.valueOf(cat).color());
            } catch (IllegalArgumentException e) {
                tv.setBackgroundColor(Category.unknown.color());
            }

            tv.setTextColor(0xfff0f0f0);
            tv.setPadding(4, 4, 4, 4);
            tv.setMaxLines(2);
            tv.setText(progs[i].Title);
            setStatusDrawable(tv, progs[i]);

            int width = setLayoutParams(layout, progs[i]) * colWidth;
            if (width < 1)
                continue;

            tv.setWidth(width);
            tv.setLayoutParams(layout);
            tv.setTag(progs[i]);
            tv.setOnClickListener(progClickListener);
            row.addView(tv);

        }

        return row;

    }

    /**
     * Get a header row containing column time values
     * @return header TableRow
     */
    private TableRow getHeader() {

        final TableRow row = new TableRow(this, null);
        TextView tv = new TextView(this, null);
        tv.setLayoutParams(hdrDateLayout);
        tv.setPadding(4, 4, 4, 4);
        tv.setBackgroundColor(0xffd0d0ff);
        tv.setTextColor(0xff161616);
        tv.setMaxLines(1);
        tv.setText(hdrDate);
        row.addView(tv);

        int j = 0;

        for (int i = 1; i < times.length; i += hdrSpan) {
            tv = new TextView(this, null);
            tv.setLayoutParams(hdrTimeLayout);
            tv.setPadding(4, 4, 4, 4);
            tv.setBackgroundColor(0xffd0d0ff);
            tv.setTextColor(0xff161616);
            tv.setMaxLines(1);
            tv.setText(hdrTimes[j++]);
            row.addView(tv);
        }

        return row;

    }

    /**
     * Get a spacer row
     * @return a spacer TableRow
     */
    private TableRow getSpacer() {

        final TableRow row = new TableRow(this, null);
        LayoutParams params = new LayoutParams();
        params.height = 1;
        row.setLayoutParams(params);
        View view = new View(this, null);
        params = new LayoutParams();
        params.height = 1;
        params.width = chanWidth;
        params.column = 0;
        params.span = 1;
        view.setLayoutParams(params);
        row.addView(view);

        for (int i = 1; i < times.length; i++) {
            view = new View(this, null);
            view.setLayoutParams(spacerLayout);
            row.addView(view);
        }

        return row;

    }

    /**
     * Round the given Date to the nearest column time
     * @param time
     */
    private void roundTime(Date time) {
        final int mins = time.getMinutes();
        final int off = mins % colMins;
        if (off == 0)
            return;
        int half = colMins / 2;
        if (half * 2 < colMins)
            half++;
        if (off < half)
            time.setMinutes(mins - off);
        else
            time.setMinutes(mins + (colMins - off));
    }

    /**
     * Populate the given LayoutParams based on a Program
     * @param params LayoutParams to populate
     * @param prog Program to generate LayoutParams from
     * @return integer containing the number of columns the program should span
     */
    private int setLayoutParams(LayoutParams params, Program prog) {

        final Date startTime = prog.StartTime, endTime = prog.EndTime;

        roundTime(startTime);
        roundTime(endTime);

        long start = startTime.getTime();
        long end = endTime.getTime();

        int maxcol = numTimes + 1;

        if (start < times[1]) {
            params.column = 1;
            start = times[1];
        } else {
            for (int i = 1; i < maxcol; i++) {
                if (start == times[i]) {
                    params.column = i;
                    break;
                }
            }
        }

        int span = (int) ((end - start) / (colMins * 60000));
        if (span + params.column > maxcol)
            span = maxcol - params.column;
        params.span = span;

        return span;

    }

}