ru.moscow.tuzlukov.sergey.weatherlog.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for ru.moscow.tuzlukov.sergey.weatherlog.MainActivity.java

Source

/*
 * WeatherLog is an app for logging air temperature changes and calculating
 * total time of preset limits exceeding in the nearest past. The project
 * began as a way to help moto-bikers decide if it is safe to drive in the morning
 * after cold night, or for automobile owners to decide if it is time to change tires
 * to/from winter ones. It also may be useful when man thinks about clothes/shoes
 * to dress in this time, or in all other cases when there needs to analyze
 * temperature's behaviour.
 *
 * Copyright  2015 Sergey Tuzlukov <s.tuzlukov@ya.ru>.
 *
 *
 * This file is part of WeatherLog.
 *
 * WeatherLog 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.
 *
 * WeatherLog 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 WeatherLog.  If not, see <http://www.gnu.org/licenses/>.
 */

package ru.moscow.tuzlukov.sergey.weatherlog;

import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.ActionBarActivity;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.jjoe64.graphview.GraphView;
import com.jjoe64.graphview.GridLabelRenderer;
import com.jjoe64.graphview.Viewport;
import com.jjoe64.graphview.series.DataPoint;
import com.jjoe64.graphview.series.LineGraphSeries;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import uk.co.senab.actionbarpulltorefresh.extras.actionbarcompat.PullToRefreshLayout;
import uk.co.senab.actionbarpulltorefresh.library.ActionBarPullToRefresh;
import uk.co.senab.actionbarpulltorefresh.library.listeners.OnRefreshListener;

public class MainActivity extends ActionBarActivity {

    private static final String SAVED_CURRENT_TIME = "SAVED_CURRENT_TIME";
    private static final String SAVED_CURRENT_TIME_MINUS_12 = "SAVED_CURRENT_TIME_MINUS_12";
    private static final String SAVED_CURRENT_TIME_MINUS_24 = "SAVED_CURRENT_TIME_MINUS_24";
    private static final String SAVED_TIME_ARRAY = "SAVED_TIME_ARRAY";
    private static final String SAVED_TEMP_ARRAY = "SAVED_TEMP_ARRAY";
    private static final String SAVED_CURRENT_GAINED = "SAVED_CURRENT_GAINED";
    private static final String SAVED_HISTORY_GAINED = "SAVED_HISTORY_GAINED";
    private static final String SAVED_LOADER_VISIBILITY = "SAVED_LOADER_VISIBILITY";
    private static final String SAVED_DIALOG_APPID = "SAVED_DIALOG_APPID";
    private static final String SAVED_DIALOG_VISIBILITY = "SAVED_DIALOG_VISIBILITY";
    private static final String IS_FIRST_LAUNCH = "IS_FIRST_LAUNCH";
    private static final String CACHE_TIMESTAMP = "CACHE_TIMESTAMP";
    private static final String CACHED_MAP_FILE = "cached_map_file.dat";
    private static final long CACHE_EXPIRE_TIME = 15 * 60 * 1000; //15 min.
    private static final int FAKE_REFRESH_DELAY = 300; //should be less, than CACHE_QUICK_REFRESH_TIME
    private static final int REQUEST_SETTINGS = 0;

    private final double temperatureLimit1 = +5.0;
    private final double temperatureLimit2 = 0.0;

    private SharedPreferences preferences;
    private NetworkQuery networkQuery;
    private int cityId;
    private static boolean refreshWasCancelled;
    private SettingsActivity.RegisterDialog registerDialog;

    private long currentTime, currentTimeMinus12h, currentTimeMinus24h;
    private boolean currentIsGained = false, historyIsGained = false;
    private Map<Long, Double> temperatureMap = new TreeMap<>();
    private long cachingTimestamp;

    private TextView tvTemperatureLimit1;
    private TextView tvTemperatureLimit2;
    private TextView tvTime1Limit1;
    private TextView tvTime1Limit2;
    private TextView tvTime2Limit1;
    private TextView tvTime2Limit2;
    private TextView tvDayTemperatureSpan;
    private TextView tvDayAverageTemperature;
    private TextView tvDayMedianTemperature;
    private GraphView graphView;
    private LinearLayout llLoader;
    private PullToRefreshLayout ptrLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        preferences = getSharedPreferences("preferences", MODE_PRIVATE);
        cityId = preferences.getInt(NetworkQuery.Params.ID, NetworkQuery.Defaults.CITY_ID);
        cachingTimestamp = preferences.getLong(CACHE_TIMESTAMP, 0L);

        tvTemperatureLimit1 = (TextView) findViewById(R.id.tvTemperatureLimit1);
        tvTemperatureLimit2 = (TextView) findViewById(R.id.tvTemperatureLimit2);
        tvTime1Limit1 = (TextView) findViewById(R.id.tvTime1Limit1);
        tvTime1Limit2 = (TextView) findViewById(R.id.tvTime1Limit2);
        tvTime2Limit1 = (TextView) findViewById(R.id.tvTime2Limit1);
        tvTime2Limit2 = (TextView) findViewById(R.id.tvTime2Limit2);
        tvDayTemperatureSpan = (TextView) findViewById(R.id.tvDayTemperatureSpan);
        tvDayAverageTemperature = (TextView) findViewById(R.id.tvDayAverageTemperature);
        tvDayMedianTemperature = (TextView) findViewById(R.id.tvDayMedianTemperature);
        graphView = (GraphView) findViewById(R.id.graph);
        llLoader = (LinearLayout) findViewById(R.id.llLoader);
        ptrLayout = (PullToRefreshLayout) findViewById(R.id.ptr_layout);

        resetValues();

        ActionBarPullToRefresh.from(this).allChildrenArePullable().listener(new OnRefreshListener() {
            @Override
            public void onRefreshStarted(View view) {
                networkQuery.cancelAllRequests(MainActivity.this);
                refreshWeatherData();
            }
        }).setup(ptrLayout);

        String appId = preferences.getString(NetworkQuery.Params.APPID, "");
        networkQuery = NetworkQuery.getInstance(getApplicationContext());
        networkQuery.setAppId(appId);

        registerDialog = new SettingsActivity.RegisterDialog(this, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String appId = registerDialog.getEtAppId().getText().toString().trim();
                networkQuery.setAppId(appId);
                preferences.edit().putString(NetworkQuery.Params.APPID, appId).apply();
                cachingTimestamp = 0;
                refreshWeatherData();
            }
        });

        if (savedInstanceState == null) {
            refreshWeatherData();
            if (preferences.getBoolean(IS_FIRST_LAUNCH, true)) {
                registerDialog.show();
                preferences.edit().putBoolean(IS_FIRST_LAUNCH, false).apply();
            }
        } else {
            currentTime = savedInstanceState.getLong(SAVED_CURRENT_TIME);
            currentTimeMinus12h = savedInstanceState.getLong(SAVED_CURRENT_TIME_MINUS_12);
            currentTimeMinus24h = savedInstanceState.getLong(SAVED_CURRENT_TIME_MINUS_24);
            currentIsGained = savedInstanceState.getBoolean(SAVED_CURRENT_GAINED);
            historyIsGained = savedInstanceState.getBoolean(SAVED_HISTORY_GAINED);
            long[] timeArray = savedInstanceState.getLongArray(SAVED_TIME_ARRAY);
            double[] tempArray = savedInstanceState.getDoubleArray(SAVED_TEMP_ARRAY);
            boolean refreshWasRun = savedInstanceState.getBoolean(SAVED_LOADER_VISIBILITY) || refreshWasCancelled;
            if (refreshWasRun || (timeArray == null || tempArray == null))
                refreshWeatherData();
            else {
                temperatureMap.clear();
                for (int i = 0; i < timeArray.length && i < tempArray.length; i++)
                    temperatureMap.put(timeArray[i], tempArray[i]);
                processValues(true);
            }
            if (savedInstanceState.getBoolean(SAVED_DIALOG_VISIBILITY)) {
                registerDialog.getEtAppId().setText(savedInstanceState.getString(SAVED_DIALOG_APPID));
                registerDialog.show();
            }
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        List<Long> baseLongList = new ArrayList<>(temperatureMap.keySet());
        List<Double> baseDoubleList = new ArrayList<>(temperatureMap.values());
        long[] timeArray = new long[baseLongList.size()];
        double[] tempArray = new double[baseDoubleList.size()];
        for (int i = 0; i < temperatureMap.size(); i++) {
            timeArray[i] = baseLongList.get(i);
            tempArray[i] = baseDoubleList.get(i);
        }
        outState.putLong(SAVED_CURRENT_TIME, currentTime);
        outState.putLong(SAVED_CURRENT_TIME_MINUS_12, currentTimeMinus12h);
        outState.putLong(SAVED_CURRENT_TIME_MINUS_24, currentTimeMinus24h);
        outState.putLongArray(SAVED_TIME_ARRAY, timeArray);
        outState.putDoubleArray(SAVED_TEMP_ARRAY, tempArray);
        outState.putBoolean(SAVED_CURRENT_GAINED, currentIsGained);
        outState.putBoolean(SAVED_HISTORY_GAINED, historyIsGained);
        outState.putBoolean(SAVED_LOADER_VISIBILITY, llLoader.getVisibility() == View.VISIBLE);
        outState.putString(SAVED_DIALOG_APPID, registerDialog.getEtAppId().getText().toString());
        outState.putBoolean(SAVED_DIALOG_VISIBILITY, registerDialog.isVisible());
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (refreshWasCancelled)
            refreshWeatherData();
    }

    @Override
    protected void onStop() {
        super.onStop();
        networkQuery.cancelAllRequests(MainActivity.this);
        if (llLoader.getVisibility() == View.VISIBLE) {
            refreshWasCancelled = true;
            llLoader.setVisibility(View.GONE);
        }
    }

    @Override
    protected void onDestroy() {
        preferences.edit().putLong(CACHE_TIMESTAMP, cachingTimestamp).apply();
        registerDialog.dismiss();
        super.onDestroy();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_settings) {
            startActivityForResult(new Intent(MainActivity.this, SettingsActivity.class), REQUEST_SETTINGS);
            return true;
        } else if (id == R.id.action_about) {
            startActivity(new Intent(MainActivity.this, AboutActivity.class));
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_SETTINGS && resultCode == RESULT_OK) {
            cityId = preferences.getInt(NetworkQuery.Params.ID, NetworkQuery.Defaults.CITY_ID);
            cachingTimestamp = 0;
            refreshWeatherData();
        }
    }

    private void resetValues() {
        String degreeSign = getString(R.string.celsius_degree_caption);
        String format1 = "%" + (temperatureLimit1 == 0 ? "" : "+") + ".0f%s";
        String format2 = "%" + (temperatureLimit2 == 0 ? "" : "+") + ".0f%s";
        tvTemperatureLimit1.setText(String.format(format1, temperatureLimit1, degreeSign));
        tvTemperatureLimit2.setText(String.format(format2, temperatureLimit2, degreeSign));
        tvDayTemperatureSpan.setText(getString(R.string.day_temperature_span_label).replace("%.1f", "- "));
        tvDayAverageTemperature.setText(getString(R.string.day_average_temperature_label).replace("%.1f", "- "));
        tvDayMedianTemperature.setText(getString(R.string.day_median_temperature_label).replace("%.1f", "- "));
        graphView.removeAllSeries();
    }

    private void refreshWeatherData() {
        refreshWasCancelled = false;
        resetValues();
        temperatureMap.clear();

        Calendar calendar = new GregorianCalendar();
        currentTime = calendar.getTimeInMillis() / 1000;
        calendar.add(Calendar.HOUR_OF_DAY, -12);
        currentTimeMinus12h = calendar.getTimeInMillis() / 1000;
        calendar.add(Calendar.HOUR_OF_DAY, -12);
        currentTimeMinus24h = calendar.getTimeInMillis() / 1000;
        calendar.add(Calendar.HOUR_OF_DAY, -24); //getting redundant data (previous 48 hours) because of some troubles on service
        long startTime = calendar.getTimeInMillis() / 1000;

        llLoader.setVisibility(View.VISIBLE);
        if (!isCacheExpired())
            try {
                ObjectInputStream inputStream = new ObjectInputStream(openFileInput(CACHED_MAP_FILE));
                Object loadedMap = inputStream.readObject();
                inputStream.close();
                temperatureMap.putAll((Map<Long, Double>) loadedMap);
                currentIsGained = historyIsGained = true;
                // Do "fake update": use delay to show progress animation correctly
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        processValues(true);
                    }
                }, FAKE_REFRESH_DELAY);
                return;
            } catch (Exception e) {
                temperatureMap.clear();
                e.printStackTrace();
            }
        currentIsGained = historyIsGained = false;
        networkQuery.addRequest(NetworkQuery.CURRENT_URL,
                new NetworkQuery.Params().addParam(NetworkQuery.Params.ID, cityId), responseCurrentListener,
                errorResponseListener, MainActivity.this);
        networkQuery.addRequest(NetworkQuery.HISTORY_URL,
                new NetworkQuery.Params().addParam(NetworkQuery.Params.ID, cityId)
                        .addParam(NetworkQuery.Params.TYPE, NetworkQuery.Params.TYPE_HOUR)
                        .addParam(NetworkQuery.Params.START, startTime).addParam(NetworkQuery.Params.END,
                                currentTime),
                responseHistoryListener, errorResponseListener, MainActivity.this);
    }

    private void processValues(boolean useCached) {
        if (!(currentIsGained && historyIsGained))
            return;
        if (temperatureMap.size() >= 2 && !useCached)
            try {
                ObjectOutputStream outputStream = new ObjectOutputStream(
                        openFileOutput(CACHED_MAP_FILE, MODE_PRIVATE));
                outputStream.writeObject(temperatureMap);
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        llLoader.setVisibility(View.GONE);
        ptrLayout.setRefreshComplete();

        if (temperatureMap.size() < 2) {
            Toast.makeText(MainActivity.this, getString(R.string.no_data_error_message), Toast.LENGTH_SHORT).show();
            cachingTimestamp = 0;
            return;
        }

        // remove all redundant values, that are older than 24 hours ago,
        // and calculate temperature at accurate 24-hours-back limit (first value in timeline)
        List<Long> sortedTimeList = new ArrayList<>(temperatureMap.keySet());
        int indexOfMinus24h = Collections.binarySearch(sortedTimeList, currentTimeMinus24h);
        double temperatureAtMinus24h;
        if (indexOfMinus24h < 0) {
            indexOfMinus24h = -indexOfMinus24h - 1;
            temperatureAtMinus24h = temperatureByTime(sortedTimeList, indexOfMinus24h, currentTimeMinus24h);
        } else {
            temperatureAtMinus24h = temperatureMap.get(currentTimeMinus24h);
        }
        for (int n = 0; n < indexOfMinus24h; n++)
            temperatureMap.remove(sortedTimeList.get(n));
        temperatureMap.put(currentTimeMinus24h, temperatureAtMinus24h);

        List<Double> sortedTempList = new ArrayList<>(temperatureMap.values());
        Collections.sort(sortedTempList);
        // statistical indicators
        double average = averageInList(sortedTempList), median = medianInList(sortedTempList),
                span = spanInList(sortedTempList);

        // total time, at which temperature was below 0 and below +5, in intervals of previous 12 and 24 hours
        long totalTimeFirstLimit12h, totalTimeSecondLimit12h, totalTimeFirstLimit24h, totalTimeSecondLimit24h;
        totalTimeFirstLimit12h = calculateTotalTimeLessThan(temperatureLimit1, currentTimeMinus12h);
        totalTimeSecondLimit12h = calculateTotalTimeLessThan(temperatureLimit2, currentTimeMinus12h);
        totalTimeFirstLimit24h = calculateTotalTimeLessThan(temperatureLimit1, currentTimeMinus24h);
        totalTimeSecondLimit24h = calculateTotalTimeLessThan(temperatureLimit2, currentTimeMinus24h);

        // display calculated values on screen
        String format = "%.1f " + getString(R.string.hours_caption);
        tvTime1Limit1.setText(String.format(format, timeRangeApproxHours(totalTimeFirstLimit12h)));
        tvTime1Limit2.setText(String.format(format, timeRangeApproxHours(totalTimeSecondLimit12h)));
        tvTime2Limit1.setText(String.format(format, timeRangeApproxHours(totalTimeFirstLimit24h)));
        tvTime2Limit2.setText(String.format(format, timeRangeApproxHours(totalTimeSecondLimit24h)));
        tvDayTemperatureSpan.setText(String.format(getString(R.string.day_temperature_span_label), span));
        tvDayAverageTemperature.setText(String.format(getString(R.string.day_average_temperature_label), average));
        tvDayMedianTemperature.setText(String.format(getString(R.string.day_median_temperature_label), median));

        makePlot(sortedTempList.get(0), sortedTempList.get(sortedTempList.size() - 1));

        if (!useCached)
            cachingTimestamp = System.currentTimeMillis();
    }

    private void makePlot(double minTemp, double maxTemp) {
        //all graphics should have a room, to be visible in plot viewport, i.e. max from greatest and min from the least ordinates:
        double lowLimit = Math.min(Math.round(minTemp), temperatureLimit2);
        double highLimit = Math.max(Math.round(maxTemp), temperatureLimit1);
        //round for drawing scale with 5-degrees step:
        lowLimit = Math.floor(lowLimit / 5.0) * 5.0;
        highLimit = Math.ceil(highLimit / 5.0) * 5.0;

        //fill in the plot with all data series:
        List<DataPoint> dataPoints = new ArrayList<>();
        for (Long time : temperatureMap.keySet())
            dataPoints.add(new DataPoint(time, temperatureMap.get(time)));
        dataPoints.add(new DataPoint(currentTime, dataPoints.get(dataPoints.size() - 1).getY())); //fix for using data from cache
        LineGraphSeries<DataPoint> temperatureSeries = new LineGraphSeries<>(
                dataPoints.toArray(new DataPoint[dataPoints.size()]));
        LineGraphSeries<DataPoint> temperatureLimit1Series = new LineGraphSeries<>(
                new DataPoint[] { new DataPoint(currentTimeMinus24h, temperatureLimit1),
                        new DataPoint(currentTime, temperatureLimit1) });
        LineGraphSeries<DataPoint> temperatureLimit2Series = new LineGraphSeries<>(
                new DataPoint[] { new DataPoint(currentTimeMinus24h, temperatureLimit2),
                        new DataPoint(currentTime, temperatureLimit2) });
        graphView.addSeries(temperatureSeries);
        graphView.addSeries(temperatureLimit1Series);
        graphView.addSeries(temperatureLimit2Series);

        //lay out the plot:
        GridLabelRenderer gridLabelRenderer = graphView.getGridLabelRenderer();
        Viewport viewport = graphView.getViewport();
        //adjust grid settings:
        gridLabelRenderer.setGridStyle(GridLabelRenderer.GridStyle.BOTH);
        gridLabelRenderer.setHighlightZeroLines(true);
        gridLabelRenderer.setHorizontalLabelsVisible(false);
        gridLabelRenderer.setVerticalLabelsVisible(true);
        //tune view of lines:
        viewport.setBackgroundColor(getResources().getColor(R.color.plot_viewport_background_color));
        temperatureSeries.setColor(getResources().getColor(R.color.temperature_series_color));
        temperatureSeries.setThickness(2);
        temperatureLimit1Series.setColor(getResources().getColor(R.color.limit1_series_color));
        temperatureLimit1Series.setThickness(2);
        temperatureLimit2Series.setColor(getResources().getColor(R.color.limit2_series_color));
        temperatureLimit2Series.setThickness(2);
        //set viewport bounds and set the scale:
        //...in horizontal:
        gridLabelRenderer.setNumHorizontalLabels(2);
        viewport.setMinX(currentTimeMinus24h);
        viewport.setMaxX(currentTime);
        viewport.setXAxisBoundsManual(true);
        //...in vertical:
        int numVerticalLabels = (int) (highLimit - lowLimit) / 5 + 1;
        numVerticalLabels = numVerticalLabels < 2 ? 2 : numVerticalLabels;
        gridLabelRenderer.setNumVerticalLabels(numVerticalLabels);
        viewport.setMinY(lowLimit);
        viewport.setMaxY(highLimit);
        viewport.setYAxisBoundsManual(true);
        //set horizontal labels:
        LinearLayout llHorizontalLabels = (LinearLayout) findViewById(R.id.llHorizontalLabels);
        if (llHorizontalLabels.getChildCount() == 0) {
            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) llHorizontalLabels.getLayoutParams();
            params.bottomMargin = 0;
            llHorizontalLabels.setLayoutParams(params);
            gridLabelRenderer.setTextSize(gridLabelRenderer.getTextSize() - 2); //make text a bit smaller
            for (int n = -24; n < 0; n += 3) {
                TextView textView = new TextView(MainActivity.this);
                textView.setText(String.valueOf(n));
                textView.setGravity(Gravity.START);
                textView.setSingleLine();
                params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                        LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f / 8.0f);
                textView.setLayoutParams(params);
                textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, gridLabelRenderer.getTextSize());
                textView.setTextColor(gridLabelRenderer.getVerticalLabelsColor());
                llHorizontalLabels.addView(textView);
            }
            TextView tvZeroLabel = (TextView) findViewById(R.id.tvZeroLabel);
            tvZeroLabel.setText("-0 " + getString(R.string.hours_caption));
            tvZeroLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, gridLabelRenderer.getTextSize());
            tvZeroLabel.setTextColor(gridLabelRenderer.getVerticalLabelsColor());
            tvZeroLabel.setVisibility(View.VISIBLE);
            gridLabelRenderer.setPadding(gridLabelRenderer.getPadding() + 2); //make plot a bit smaller
        }
    }

    private long calculateTotalTimeLessThan(double temperatureLimit, long timeLeftLimit) {
        long totalTime = 0L;

        // calculating temperature at given left time limit:
        double temperatureAtLeftLimit;
        List<Long> timeList = new ArrayList<>(temperatureMap.keySet());
        int indexLeftLimit = Collections.binarySearch(timeList, timeLeftLimit);
        if (indexLeftLimit < 0) {
            indexLeftLimit = -indexLeftLimit - 1;
            temperatureAtLeftLimit = temperatureByTime(timeList, indexLeftLimit, timeLeftLimit);
            for (int n = indexLeftLimit; n > 0; n--)
                timeList.remove(0);
            timeList.add(0, timeLeftLimit);
        } else {
            temperatureAtLeftLimit = temperatureMap.get(timeLeftLimit);
            for (int n = indexLeftLimit; n > 0; n--)
                timeList.remove(0);
        }

        // count total time when temperature was less than given limit, on all timeline
        for (int i = 1; i < timeList.size(); i++) {
            long t1 = timeList.get(i - 1), t2 = timeList.get(i);
            double y1 = i == 1 ? temperatureAtLeftLimit : temperatureMap.get(t1), y2 = temperatureMap.get(t2);
            if (y2 < temperatureLimit && y1 < temperatureLimit)
                totalTime += t2 - t1;
            else if (y2 > temperatureLimit && y1 < temperatureLimit)
                totalTime += timeByTemperature(t1, y1, t2, y2, temperatureLimit) - t1;
            else if (y2 < temperatureLimit && y1 > temperatureLimit)
                totalTime += t2 - timeByTemperature(t1, y1, t2, y2, temperatureLimit);
        }

        return totalTime;
    }

    private double kelvinToCelsius(double tempInKelvin) {
        return tempInKelvin - 273.15;
    }

    private double timeRangeApproxHours(long timeRange) {
        timeRange /= 60;
        return Math.round(timeRange / 60.0 * 2) / 2.0; //rounding with step 0.5 hours
        //return Math.round(timeRange / 60.0 * 10) / 10.0; //to rounding with step 0.1 hours
    }

    private double averageInList(List<Double> list) {
        double avg = 0.0;
        for (Double d : list)
            avg += d;
        return avg / list.size();
    }

    private double medianInList(List<Double> sortedList) {
        int size = sortedList.size();
        return size % 2 == 0 ? (sortedList.get(size / 2) + sortedList.get(size / 2 - 1)) / 2.0
                : sortedList.get(size / 2);
    }

    private double spanInList(List<Double> sortedList) {
        return sortedList.get(sortedList.size() - 1) - sortedList.get(0);
    }

    private double temperatureByTime(List<Long> timeList, int index, long time) {
        long t1 = timeList.get(index - 1), t2 = timeList.get(index);
        double y1 = temperatureMap.get(t1), y2 = temperatureMap.get(t2);
        double k = (y2 - y1) / (t2 - t1);
        double b = y2 - k * t2;
        return k * time + b;
    }

    private double timeByTemperature(long t1, double y1, long t2, double y2, double temperature) {
        double k = (y2 - y1) / (t2 - t1);
        double b = y2 - k * t2;
        return (temperature - b) / k;
    }

    private boolean isCacheExpired() {
        return System.currentTimeMillis() > cachingTimestamp + CACHE_EXPIRE_TIME;
    }

    private Response.Listener<JSONObject> responseCurrentListener = new Response.Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject response) {
            JSONObject jsonMainSection = response.optJSONObject("main");
            if (jsonMainSection == null)
                return;
            double currentTemp = kelvinToCelsius(jsonMainSection.optDouble("temp"));
            long dt = response.optLong("dt"); //time of temperature registration can be not synchronized with time of request done
            temperatureMap.put(currentTime, currentTemp);
            currentIsGained = true;
            processValues(false);
        }
    };

    private Response.Listener<JSONObject> responseHistoryListener = new Response.Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject response) {
            JSONArray jsonArray = response.optJSONArray("list");
            if (jsonArray == null)
                return;
            try {
                for (int i = 0; i < jsonArray.length(); i++) {
                    JSONObject jsonObject = jsonArray.getJSONObject(i);
                    double temp = kelvinToCelsius(jsonObject.getJSONObject("main").optDouble("temp"));
                    long time = jsonObject.optLong("dt");
                    temperatureMap.put(time, temp);
                }
                historyIsGained = true;
            } catch (JSONException e) {
                Toast.makeText(MainActivity.this, getString(R.string.response_error_message), Toast.LENGTH_SHORT)
                        .show();
                historyIsGained = false;
            }
            processValues(false);
        }
    };

    private Response.ErrorListener errorResponseListener = new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            Toast.makeText(MainActivity.this, getString(R.string.response_error_message), Toast.LENGTH_SHORT)
                    .show();
            currentIsGained = historyIsGained = false;
            llLoader.setVisibility(View.GONE);
            ptrLayout.setRefreshComplete();
        }
    };

}