pl.llp.aircasting.view.presenter.MeasurementPresenter.java Source code

Java tutorial

Introduction

Here is the source code for pl.llp.aircasting.view.presenter.MeasurementPresenter.java

Source

/**
 AirCasting - Share your Air!
 Copyright (C) 2011-2012 HabitatMap, Inc.
    
 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/>.
    
 You can contact the authors by email at <info@habitatmap.org>
 */
package pl.llp.aircasting.view.presenter;

import pl.llp.aircasting.activity.ApplicationState;
import pl.llp.aircasting.activity.events.SessionChangeEvent;
import pl.llp.aircasting.android.Logger;
import pl.llp.aircasting.event.ui.ViewStreamEvent;
import pl.llp.aircasting.helper.SettingsHelper;
import pl.llp.aircasting.model.Measurement;
import pl.llp.aircasting.model.MeasurementStream;
import pl.llp.aircasting.model.Sensor;
import pl.llp.aircasting.model.SensorManager;
import pl.llp.aircasting.model.SessionManager;
import pl.llp.aircasting.model.events.MeasurementEvent;
import pl.llp.aircasting.sensor.builtin.SimpleAudioReader;

import android.content.SharedPreferences;
import com.google.common.base.Function;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Lists;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

import static com.google.common.collect.Iterables.getLast;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.newCopyOnWriteArrayList;
import static com.google.common.collect.Lists.newLinkedList;
import static com.google.common.collect.Multimaps.index;
import static java.util.Collections.sort;

@Singleton
public class MeasurementPresenter implements SharedPreferences.OnSharedPreferenceChangeListener {
    private static final long MIN_ZOOM = 30000;
    private static final long SCROLL_TIMEOUT = 1000;

    @Inject
    SessionManager sessionManager;
    @Inject
    SettingsHelper settingsHelper;
    @Inject
    SharedPreferences preferences;
    @Inject
    EventBus eventBus;
    @Inject
    SensorManager sensorManager;
    @Inject
    MeasurementAggregator aggregator;

    private CopyOnWriteArrayList<Measurement> fullView = null;
    private int measurementsSize;

    private int anchor;
    private long visibleMilliseconds = MIN_ZOOM;
    private long lastScrolled;
    private Date timeAnchor;

    private final CopyOnWriteArrayList<Measurement> timelineView = new CopyOnWriteArrayList<Measurement>();
    private List<Listener> listeners = newArrayList();

    @Inject
    private ApplicationState state;

    public void setSensor(Sensor sensor) {
        this.sensor = sensor;
    }

    private Sensor sensor = SimpleAudioReader.getSensor();

    @Inject
    public void init() {
        preferences.registerOnSharedPreferenceChangeListener(this);
        eventBus.register(this);
    }

    @Subscribe
    public synchronized void onEvent(MeasurementEvent event) {
        onMeasurement(event);
    }

    private void onMeasurement(MeasurementEvent event) {
        if (!state.recording().isRecording())
            return;
        if (!event.getSensor().equals(this.sensor))
            return;

        Measurement measurement = event.getMeasurement();

        prepareFullView();
        updateFullView(measurement);

        if (anchor == 0) {
            updateTimelineView();
        }

        notifyListeners();
    }

    private synchronized void updateTimelineView() {
        Stopwatch stopwatch = new Stopwatch().start();

        Measurement measurement = aggregator.getAverage();

        if (aggregator.isComposite()) {
            if (!timelineView.isEmpty()) {
                timelineView.remove(timelineView.size() - 1);
            }
            timelineView.add(measurement);
            Logger.logGraphPerformance(
                    "updateTimelineView step 0 took " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
        } else {
            long firstToDisplay = measurement.getTime().getTime() - visibleMilliseconds;
            while (!timelineView.isEmpty() && firstToDisplay >= timelineView.get(0).getTime().getTime()) {
                timelineView.remove(0);
            }
            Logger.logGraphPerformance(
                    "updateTimelineView step 1 took " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
            measurementsSize += 1;
            timelineView.add(measurement);
        }

        Logger.logGraphPerformance("updateTimelineView step n took " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    private void updateFullView(Measurement measurement) {
        boolean newBucket = isNewBucket(measurement);

        if (newBucket) {
            if (!aggregator.isEmpty()) {
                for (Listener listener : listeners) {
                    listener.onAveragedMeasurement(aggregator.getAverage());
                }
            }
            aggregator.reset();
        } else {
            fullView.remove(fullView.size() - 1);
        }

        aggregator.add(measurement);
        fullView.add(aggregator.getAverage());
    }

    private boolean isNewBucket(Measurement measurement) {
        if (fullView.isEmpty())
            return true;

        Measurement last = getLast(fullView);

        long b1 = bucketBySecond(last);
        long b2 = bucketBySecond(measurement);
        return b1 != b2;
    }

    @Subscribe
    public synchronized void onEvent(SessionChangeEvent event) {
        this.sensor = sensorManager.getVisibleSensor();
        reset();
        anchor = 0;
    }

    @Subscribe
    public synchronized void onEvent(ViewStreamEvent event) {
        this.sensor = event.getSensor();
        reset();
    }

    private synchronized void reset() {
        fullView = null;
        timelineView.clear();

        notifyListeners();
    }

    private CopyOnWriteArrayList<Measurement> prepareFullView() {
        if (fullView != null)
            return fullView;

        Stopwatch stopwatch = new Stopwatch().start();

        String sensorName = sensor.getSensorName();
        MeasurementStream stream = sessionManager.getMeasurementStream(sensorName);
        Iterable<Measurement> measurements;
        if (stream == null) {
            measurements = newArrayList();
        } else {
            measurements = stream.getMeasurements();
        }

        ImmutableListMultimap<Long, Measurement> forAveraging = index(measurements,
                new Function<Measurement, Long>() {
                    @Override
                    public Long apply(Measurement measurement) {
                        return measurement.getSecond() / settingsHelper.getAveragingTime();
                    }
                });

        Logger.logGraphPerformance("prepareFullView step 1 took " + stopwatch.elapsed(TimeUnit.MILLISECONDS));

        ArrayList<Long> times = newArrayList(forAveraging.keySet());
        sort(times);

        Logger.logGraphPerformance("prepareFullView step 2 took " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
        List<Measurement> timeboxedMeasurements = newLinkedList();
        for (Long time : times) {
            ImmutableList<Measurement> chunk = forAveraging.get(time);
            timeboxedMeasurements.add(average(chunk));
        }

        Logger.logGraphPerformance("prepareFullView step 3 took " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
        CopyOnWriteArrayList<Measurement> result = Lists.newCopyOnWriteArrayList(timeboxedMeasurements);

        Logger.logGraphPerformance("prepareFullView step n took " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
        fullView = result;
        return result;
    }

    private long bucketBySecond(Measurement measurement) {
        return measurement.getSecond() / settingsHelper.getAveragingTime();
    }

    private Measurement average(ImmutableList<Measurement> measurements) {
        aggregator.reset();

        for (Measurement measurement : measurements) {
            aggregator.add(measurement);
        }

        return aggregator.getAverage();
    }

    public synchronized List<Measurement> getTimelineView() {
        if (state.recording().isShowingASession()) {
            prepareTimelineView();
            return timelineView;
        } else {
            return newArrayList();
        }
    }

    private int timeToAnchor(Date d, List<Measurement> measurements) {
        if (measurements.isEmpty())
            return 0;
        int position = 0;
        for (int i = 1; i < measurements.size(); i++) {
            if (Math.abs(d.getTime() - measurements.get(i).getTime().getTime()) < Math
                    .abs(d.getTime() - measurements.get(position).getTime().getTime())) {
                position = i;
            }
        }
        return measurements.size() - 1 - position;
    }

    protected synchronized void prepareTimelineView() {
        if (!timelineView.isEmpty()) {
            return;
        }

        Stopwatch stopwatch = new Stopwatch().start();

        final List<Measurement> measurements = getFullView();

        if (anchor != 0 && new Date().getTime() - lastScrolled > SCROLL_TIMEOUT) {
            anchor = timeToAnchor(timeAnchor, measurements);
        }

        int position = measurements.size() - 1 - anchor;
        final long lastMeasurementTime = measurements.isEmpty() ? 0
                : measurements.get(position).getTime().getTime();

        timelineView.clear();
        TreeMap<Long, Measurement> measurementsMap = new TreeMap<Long, Measurement>();
        for (Measurement m : measurements) {
            measurementsMap.put(m.getTime().getTime(), m);
        }

        //    +1 because subMap parameters are (inclusive, exclusive)
        timelineView.addAll(measurementsMap
                .subMap(lastMeasurementTime - visibleMilliseconds, lastMeasurementTime + 1).values());
        measurementsSize = measurements.size();

        Logger.logGraphPerformance("prepareTimelineView for [" + timelineView.size() + "] took "
                + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    public void registerListener(Listener listener) {
        listeners.add(listener);
    }

    public void unregisterListener(Listener listener) {
        listeners.remove(listener);
    }

    public boolean canZoomIn() {
        return visibleMilliseconds > MIN_ZOOM;
    }

    public synchronized boolean canZoomOut() {
        prepareTimelineView();
        return timelineView.size() < measurementsSize;
    }

    public void zoomIn() {
        if (canZoomIn()) {
            anchor += timelineView.size() / 4;
            fixAnchor();
            setZoom(visibleMilliseconds / 2);
        }
    }

    public synchronized void zoomOut() {
        if (canZoomOut()) {
            anchor -= timelineView.size() / 2;
            fixAnchor();
            setZoom(visibleMilliseconds * 2);
        }
    }

    synchronized void setZoom(long zoom) {
        this.visibleMilliseconds = zoom;
        timelineView.clear();
        notifyListeners();
    }

    private synchronized void fixAnchor() {
        if (anchor > measurementsSize - timelineView.size()) {
            anchor = measurementsSize - timelineView.size();
        }
        if (anchor < 0) {
            anchor = 0;
        }
        int position = fullView.size() - 1 - anchor;
        timeAnchor = fullView.isEmpty() ? new Date() : fullView.get(position).getTime();
    }

    public synchronized void scroll(double scrollAmount) {

        lastScrolled = new Date().getTime();
        prepareTimelineView();

        anchor -= scrollAmount * timelineView.size();
        fixAnchor();
        timelineView.clear();

        notifyListeners();
    }

    public List<Measurement> getFullView() {
        if (state.recording().isJustShowingCurrentValues()) {
            fullView = newCopyOnWriteArrayList();
        } else {
            fullView = prepareFullView();
        }
        return fullView;
    }

    public synchronized boolean canScrollRight() {
        return anchor != 0;
    }

    public synchronized boolean canScrollLeft() {
        prepareTimelineView();
        return anchor < measurementsSize - timelineView.size();
    }

    @Override
    public synchronized void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
        if (key.equals(SettingsHelper.AVERAGING_TIME)) {
            reset();
        }
    }

    public interface Listener {
        void onViewUpdated();

        void onAveragedMeasurement(Measurement measurement);
    }

    private void notifyListeners() {
        for (Listener listener : listeners) {
            listener.onViewUpdated();
        }
    }
}