/* * This source is part of the * _____ ___ ____ * __ / / _ \/ _ | / __/___ _______ _ * / // / , _/ __ |/ _/_/ _ \/ __/ _ `/ * \___/_/|_/_/ |_/_/ (_)___/_/ \_, / * /___/ * repository. * * Copyright (C) 2013 Benoit 'BoD' Lubek (BoD@JRAF.org) * * 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.jraf.android.bikey.backend.log; import java.util.ArrayList; import java.util.List; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.location.Location; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; import com.google.android.gms.maps.model.LatLng; import org.jraf.android.bikey.app.Application; import org.jraf.android.bikey.backend.location.LocationManager; import org.jraf.android.bikey.backend.location.LocationPair; import org.jraf.android.bikey.backend.provider.log.LogColumns; import org.jraf.android.bikey.backend.provider.log.LogContentValues; import org.jraf.android.bikey.backend.provider.log.LogCursor; import org.jraf.android.bikey.backend.provider.log.LogSelection; import org.jraf.android.bikey.backend.ride.RideManager; import org.jraf.android.util.listeners.Listeners; import org.jraf.android.util.log.Log; public class LogManager { private static final LogManager INSTANCE = new LogManager(); public static LogManager get() { return INSTANCE; } private final Context mContext; private Listeners<LogListener> mListeners = Listeners.newInstance(); private LogManager() { mContext = Application.getApplication(); } @WorkerThread public Uri add(@NonNull Uri rideUri, Location location, Location previousLocation, Float cadence, Integer heartRate) { // Add a log LogContentValues values = new LogContentValues(); long rideId = ContentUris.parseId(rideUri); values.putRideId(rideId); values.putRecordedDate(location.getTime()); values.putLat(location.getLatitude()); values.putLon(location.getLongitude()); values.putEle(location.getAltitude()); if (previousLocation != null) { LocationPair locationPair = new LocationPair(previousLocation, location); float speed = locationPair.getSpeed(); if (speed < LocationManager.SPEED_MIN_THRESHOLD_M_S) { Log.d("Speed under threshold, not logging it"); } else { values.putLogDuration(locationPair.getDuration()); values.putLogDistance(locationPair.getDistance()); values.putSpeed(speed); } } values.putCadence(cadence); values.putHeartRate(heartRate); Uri res = values.insert(mContext); // Update total distance for ride float totalDistance = getTotalDistance(rideUri); RideManager.get().updateTotalDistance(rideUri, totalDistance); // Dispatch to listeners mListeners.dispatch(listener -> listener.onLogAdded(rideUri)); return res; } @WorkerThread public float getTotalDistance(@NonNull Uri rideUri) { long rideId = ContentUris.parseId(rideUri); String[] projection = {"sum(" + LogColumns.LOG_DISTANCE + ")"}; LogSelection where = new LogSelection(); where.rideId(rideId); Cursor c = where.query(mContext, projection); try { if (!c.moveToNext()) return 0; return c.getFloat(0); } finally { c.close(); } } /** * Note: the top 10% points are discarded to account for imprecise values. */ @WorkerThread public float getAverageMovingSpeed(@NonNull Uri rideUri) { // First get the max float max = getMaxSpeed(rideUri); long rideId = ContentUris.parseId(rideUri); String[] projection = {"sum(" + LogColumns.LOG_DISTANCE + ")/sum(" + LogColumns.LOG_DURATION + ")*1000"}; LogSelection where = new LogSelection(); where.rideId(rideId).and().speedGt(LocationManager.SPEED_MIN_THRESHOLD_M_S).and().speedLtEq(max); Cursor c = where.query(mContext, projection); try { if (!c.moveToNext()) return 0; return c.getFloat(0); } finally { c.close(); } } /** * Note: the top 10% points are discarded to account for imprecise values. */ @WorkerThread public Float getAverageCadence(@NonNull Uri rideUri) { // First get the min and max float min = getMinCadence(rideUri); float max = getMaxCadence(rideUri); long rideId = ContentUris.parseId(rideUri); String[] projection = {"avg(" + LogColumns.CADENCE + ")"}; LogSelection where = new LogSelection(); where.rideId(rideId).and().cadenceGtEq(min).and().cadenceLtEq(max); Cursor c = where.query(mContext, projection); try { if (!c.moveToNext()) return null; if (c.isNull(0)) return null; return c.getFloat(0); } finally { c.close(); } } /** * Note: the top 10% points are discarded to account for imprecise values. */ @WorkerThread public Float getAverageHeartRate(@NonNull Uri rideUri) { // First get the min and max float min = getMinHeartRate(rideUri); float max = getMaxHeartRate(rideUri); long rideId = ContentUris.parseId(rideUri); String[] projection = {"avg(" + LogColumns.HEART_RATE + ")"}; LogSelection where = new LogSelection(); where.rideId(rideId).and().heartRateGtEq((int) min).and().heartRateLtEq((int) max); Cursor c = where.query(mContext, projection); try { if (!c.moveToNext()) return null; if (c.isNull(0)) return null; return c.getFloat(0); } finally { c.close(); } } @WorkerThread public Long getMovingDuration(@NonNull Uri rideUri) { long rideId = ContentUris.parseId(rideUri); String[] projection = {"sum(" + LogColumns.LOG_DURATION + ")"}; LogSelection where = new LogSelection(); where.rideId(rideId).and().speedGt(LocationManager.SPEED_MIN_THRESHOLD_M_S); Cursor c = where.query(mContext, projection); try { if (!c.moveToNext()) return null; if (c.isNull(0)) return null; return c.getLong(0); } finally { c.close(); } } /** * Note: the top 10% points are discarded to account for imprecise values. */ @WorkerThread private float getMax(@NonNull Uri rideUri, @NonNull String column) { // Get the point count to discard the top 10% values Integer count = getLogCount(rideUri); if (count == null) return 0; long rideId = ContentUris.parseId(rideUri); String[] projection = {column}; LogSelection where = new LogSelection() .rideId(rideId).and().addRaw(column + " IS NOT NULL") .orderBy(column, true) .limit(count / 10); Cursor c = where.query(mContext, projection); try { if (!c.moveToLast()) return 0; return c.getFloat(0); } finally { c.close(); } } /** * Note: the bottom 10% points are discarded to account for imprecise values. */ @WorkerThread private float getMin(@NonNull Uri rideUri, @NonNull String column) { long rideId = ContentUris.parseId(rideUri); String[] projection = {column}; LogSelection where = new LogSelection() .rideId(rideId).and().addRaw(column + " IS NOT NULL") .orderBy(column); Cursor c = where.query(mContext, projection); try { if (!c.moveToFirst()) return 0; int count = c.getCount(); // Discard the first 10% int index = count / 10; c.moveToPosition(index); return c.getFloat(0); } finally { c.close(); } } @WorkerThread public float getMaxSpeed(@NonNull Uri rideUri) { return getMax(rideUri, LogColumns.SPEED); } @WorkerThread public float getMaxCadence(@NonNull Uri rideUri) { return getMax(rideUri, LogColumns.CADENCE); } private float getMinCadence(@NonNull Uri rideUri) { return getMin(rideUri, LogColumns.CADENCE); } @WorkerThread public float getMaxHeartRate(@NonNull Uri rideUri) { return getMax(rideUri, LogColumns.HEART_RATE); } @WorkerThread public float getMinHeartRate(@NonNull Uri rideUri) { return getMin(rideUri, LogColumns.HEART_RATE); } @WorkerThread public Long getFirstLogDate(@NonNull Uri rideUri) { long rideId = ContentUris.parseId(rideUri); String[] projection = {"min(" + LogColumns.RECORDED_DATE + ")"}; LogSelection where = new LogSelection(); where.rideId(rideId); Cursor c = where.query(mContext, projection); try { if (!c.moveToNext()) return null; if (c.isNull(0)) return null; return c.getLong(0); } finally { c.close(); } } @WorkerThread public Long getLastLogDate(@NonNull Uri rideUri) { long rideId = ContentUris.parseId(rideUri); String[] projection = {"max(" + LogColumns.RECORDED_DATE + ")"}; LogSelection where = new LogSelection(); where.rideId(rideId); Cursor c = where.query(mContext, projection); try { if (!c.moveToNext()) return null; if (c.isNull(0)) return null; return c.getLong(0); } finally { c.close(); } } private Integer getLogCount(@NonNull Uri rideUri) { String[] projection = {"count(*)"}; long rideId = ContentUris.parseId(rideUri); LogSelection where = new LogSelection(); where.rideId(rideId); int count; Cursor c = where.query(mContext, projection); try { if (!c.moveToNext()) return null; count = c.getInt(0); } finally { c.close(); } return count; } @WorkerThread public List<LatLng> getLatLngArray(@NonNull Uri rideUri, int max) { // Get the point count to determine the ratio to apply to not get more than max values Integer count = getLogCount(rideUri); if (count == null) return null; int ratio = count / max; if (ratio == 0) ratio = 1; ArrayList<LatLng> res = new ArrayList<>(max); // Get the values String[] projection = new String[] {LogColumns.LAT, LogColumns.LON}; LogSelection where = new LogSelection(); // Get at most max rows by applying a modulo on the id long rideId = ContentUris.parseId(rideUri); where.rideId(rideId).and().addRaw(LogColumns._ID + "%" + ratio + "=0"); LogCursor cursor = where.query(mContext, projection); try { while (cursor.moveToNext()) { res.add(new LatLng(cursor.getLat(), cursor.getLon())); } } finally { cursor.close(); } return res; } @WorkerThread public List<Float> getSpeedArray(@NonNull Uri rideUri, int max) { // Get the point count to determine the ratio to apply to not get more than max values Integer count = getLogCount(rideUri); if (count == null) return null; int ratio = count / max; if (ratio == 0) ratio = 1; ArrayList<Float> res = new ArrayList<>(max); // Get the values String[] projection = new String[] {LogColumns.SPEED}; LogSelection where = new LogSelection(); // Get at most max rows by applying a modulo on the id long rideId = ContentUris.parseId(rideUri); where.rideId(rideId).and().speedNot((Float) null).and().addRaw(LogColumns._ID + "%" + ratio + "=0"); LogCursor cursor = where.query(mContext, projection); try { while (cursor.moveToNext()) { res.add(cursor.getSpeed()); } } finally { cursor.close(); } return res; } @WorkerThread public List<Float> getCadenceArray(@NonNull Uri rideUri, int max) { // Get the point count to determine the ratio to apply to not get more than max values Integer count = getLogCount(rideUri); if (count == null) return null; int ratio = count / max; if (ratio == 0) ratio = 1; ArrayList<Float> res = new ArrayList<>(max); // Get the values String[] projection = new String[] {LogColumns.CADENCE}; LogSelection where = new LogSelection(); // Get at most max rows by applying a modulo on the id long rideId = ContentUris.parseId(rideUri); where.rideId(rideId).and().cadenceNot((Float) null).and().addRaw(LogColumns._ID + "%" + ratio + "=0"); LogCursor cursor = where.query(mContext, projection); try { while (cursor.moveToNext()) { res.add(cursor.getCadence()); } } finally { cursor.close(); } return res; } @WorkerThread public List<Float> getHeartRateArray(@NonNull Uri rideUri, int max) { // Get the point count to determine the ratio to apply to not get more than max values Integer count = getLogCount(rideUri); if (count == null) return null; int ratio = count / max; if (ratio == 0) ratio = 1; ArrayList<Float> res = new ArrayList<>(max); // Get the values String[] projection = new String[] {LogColumns.HEART_RATE}; LogSelection where = new LogSelection(); // Get at most max rows by applying a modulo on the id long rideId = ContentUris.parseId(rideUri); where.rideId(rideId).and().heartRateNot((Integer) null).and().addRaw(LogColumns._ID + "%" + ratio + "=0"); LogCursor cursor = where.query(mContext, projection); try { while (cursor.moveToNext()) { res.add(Float.valueOf(cursor.getHeartRate())); } } finally { cursor.close(); } return res; } /* * Listeners. */ public void addListener(@NonNull LogListener listener) { mListeners.add(listener); } public void removeListener(@NonNull LogListener listener) { mListeners.remove(listener); } }