Java tutorial
/* * StatsCollector - Copyright(c) 2014 Joe Pasqua * Provided under the MIT License. See the LICENSE file for details. * Created: Nov 30, 2014 */ package org.noroomattheinn.visibletesla.data; import com.google.common.collect.Range; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.NavigableMap; import javafx.beans.property.IntegerProperty; import org.noroomattheinn.tesla.ChargeState; import org.noroomattheinn.tesla.StreamState; import org.noroomattheinn.tesla.Vehicle; import org.noroomattheinn.timeseries.CachedTimeSeries; import org.noroomattheinn.timeseries.IndexedTimeSeries; import org.noroomattheinn.timeseries.Row; import org.noroomattheinn.timeseries.TimeSeries; import org.noroomattheinn.utils.GeoUtils; import org.noroomattheinn.utils.Utils; import org.noroomattheinn.utils.ThreadManager; import org.noroomattheinn.visibletesla.vehicle.VTVehicle; import static org.noroomattheinn.tesla.Tesla.logger; /** * StatsCollector: Collect stats as they are generated, store them in * a TimeSeries, and allow queries against the data. * * @author Joe Pasqua <joe at NoRoomAtTheInn dot org> */ class StatsCollector implements ThreadManager.Stoppable { private static final long TenMinutes = 10 * 60 * 1000; /*------------------------------------------------------------------------------ * * Internal State * *----------------------------------------------------------------------------*/ private final VTData vtData; private final VTVehicle vtVehicle; private final CachedTimeSeries ts; private final IntegerProperty minTime; private final IntegerProperty minDist; private VTData.TimeBasedPredicate collectNow = new VTData.TimeBasedPredicate() { @Override public void setTime(long time) { } @Override public boolean eval() { return false; } }; /*============================================================================== * ------- ------- * ------- Public Interface To This Class ------- * ------- ------- *============================================================================*/ /** * Create a new StatsCollector that will monitor new states being generated * by the StatsStreamer and persist them as appropriate. Not every state will * be persisted and not every value from each state is persisted. A state may * not be persisted if a previous state was persisted too "recently". This * constructor might result in upgrading the underlying repository if its * format has changed. * * @throws IOException If the underlying persistent store has a problem. */ StatsCollector(File container, VTData vtData, VTVehicle v, Range<Long> loadPeriod, IntegerProperty minTime, IntegerProperty minDist) throws IOException { this.vtData = vtData; this.minTime = minTime; this.minDist = minDist; this.vtVehicle = v; this.ts = new CachedTimeSeries(container, vtVehicle.getVehicle().getVIN(), VTData.schema, loadPeriod); vtVehicle.streamState.addTracker(new Runnable() { @Override public void run() { handleStreamState(vtVehicle.streamState.get()); } }); vtVehicle.chargeState.addTracker(new Runnable() { @Override public void run() { handleChargeState(vtVehicle.chargeState.get()); } }); ThreadManager.get().addStoppable((ThreadManager.Stoppable) this); } /** * Create a Row based on the Charge and Stream States provided. The timestamp * if based on the timestamp of the newest state object * @param cs The ChargeState from which various column values will be pulled * @param ss The StreamState from which various column values will be pulled * @return The newly created and initialized Row */ static Row rowFromStates(ChargeState cs, StreamState ss) { Row r = new Row(Math.max(cs.timestamp, ss.timestamp), 0L, VTData.schema.nColumns); r.set(VTData.schema, VTData.VoltageKey, cs.chargerVoltage); r.set(VTData.schema, VTData.CurrentKey, cs.chargerActualCurrent); r.set(VTData.schema, VTData.EstRangeKey, cs.range); r.set(VTData.schema, VTData.SOCKey, cs.batteryPercent); r.set(VTData.schema, VTData.ROCKey, cs.chargeRate); r.set(VTData.schema, VTData.BatteryAmpsKey, cs.batteryCurrent); r.set(VTData.schema, VTData.LatitudeKey, ss.estLat); r.set(VTData.schema, VTData.LongitudeKey, ss.estLng); r.set(VTData.schema, VTData.HeadingKey, ss.heading); r.set(VTData.schema, VTData.SpeedKey, ss.speed); r.set(VTData.schema, VTData.OdometerKey, ss.odometer); r.set(VTData.schema, VTData.PowerKey, ss.power); return r; } /** * Return a TimeSeries for all of the collected data. * @return The TimeSeries */ TimeSeries getFullTimeSeries() { return ts; } /** * Return an IndexedTimeSeries for only the data loaded into memory. The * range of data loaded is controlled by a preference. * @return The IndexedTimeSeries */ IndexedTimeSeries getLoadedTimeSeries() { return ts.getCachedSeries(); } /** * Return an index on a set of rows covered by the period [startTime..endTime]. * * @param startTime Starting time for the period * @param endTime Ending time for the period * @return A map from time -> Row for all rows in the time range */ NavigableMap<Long, Row> getRangeOfLoadedRows(long startTime, long endTime) { return getLoadedTimeSeries().getIndex(Range.open(startTime, endTime)); } /** * Return an index on the cached rows in the data store. * * @return A map from time -> Row for all rows in the store */ NavigableMap<Long, Row> getAllLoadedRows() { return getLoadedTimeSeries().getIndex(); } boolean export(File file, Range<Long> exportPeriod, String[] columns) { return ts.export(file, exportPeriod, Arrays.asList(columns), true); } /** * Shut down the StatsCollector. */ @Override public void stop() { ts.close(); } void setCollectNow(VTData.TimeBasedPredicate p) { collectNow = p; } /*------------------------------------------------------------------------------ * * Upgrading the Stats store if needed * *----------------------------------------------------------------------------*/ static boolean upgradeRequired(File dir, Vehicle v) { return DBConverter.conversionRequired(dir, v.getVIN()); } static boolean doUpgrade(File dir, Vehicle v) { DBConverter converter = new DBConverter(dir, v.getVIN()); try { converter.convert(); } catch (IOException e) { logger.severe("Unable to upgrade database: " + e); return false; } return true; } /*------------------------------------------------------------------------------ * * PRIVATE - Methods related to storing new samples * *----------------------------------------------------------------------------*/ private synchronized void handleChargeState(ChargeState state) { Row r = new Row(state.timestamp, 0L, VTData.schema.nColumns); r.set(VTData.schema, VTData.VoltageKey, state.chargerVoltage); r.set(VTData.schema, VTData.CurrentKey, state.chargerActualCurrent); r.set(VTData.schema, VTData.EstRangeKey, state.range); r.set(VTData.schema, VTData.SOCKey, state.batteryPercent); r.set(VTData.schema, VTData.ROCKey, state.chargeRate); r.set(VTData.schema, VTData.BatteryAmpsKey, state.batteryCurrent); ts.storeRow(r); vtData.lastStoredChargeState.set(state); } private synchronized void handleStreamState(StreamState state) { StreamState lastRecorded = vtData.lastStoredStreamState.get(); if (worthRecording(state, lastRecorded)) { Row r = new Row(state.timestamp, 0L, VTData.schema.nColumns); r.set(VTData.schema, VTData.LatitudeKey, state.estLat); r.set(VTData.schema, VTData.LongitudeKey, state.estLng); r.set(VTData.schema, VTData.HeadingKey, state.heading); r.set(VTData.schema, VTData.SpeedKey, Utils.round(state.speed, 1)); r.set(VTData.schema, VTData.OdometerKey, state.odometer); r.set(VTData.schema, VTData.PowerKey, state.power); ts.storeRow(r); vtData.lastStoredStreamState.set(state); } } private boolean worthRecording(StreamState cur, StreamState last) { double meters = GeoUtils.distance(cur.estLat, cur.estLng, last.estLat, last.estLng); // The app becoming active makes it worth recording collectNow.setTime(last.timestamp); if (collectNow.eval()) return true; // A big turn makes it worth recording. Note that heading changes can be // spurious. They can happen when the car is sitting still. Ignore those. double turn = 180.0 - Math.abs((Math.abs(cur.heading - last.heading) % 360.0) - 180.0); if (turn > 10 && moving(cur)) return true; // A long time between readings makes it worth recording long timeDelta = Math.abs(cur.timestamp - last.timestamp); if (timeDelta > TenMinutes) { return true; } // A change in motion (moving->stationary or stationaty->moving) is worth recording if (moving(last) != moving(cur)) { return true; } // If you're moving and it's been a while since a reading, it's worth recording if ((timeDelta >= minTime.get() * 1000) && (meters >= minDist.get())) return true; return false; } private boolean moving(StreamState state) { return state.speed > 0.1; } }