/*
* Copyright (C) 2016 gerhard.nospam@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.runnerup.tracker.component;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
import org.runnerup.BuildConfig;
import org.runnerup.R;
import org.runnerup.tracker.Tracker;
import java.io.IOException;
import java.util.HashMap;
import org.matthiaszimmermann.location.egm96.Geoid;
public class TrackerElevation extends DefaultTrackerComponent implements SensorEventListener {
private static final String NAME = "Elevation";
@Override
public String getName() {
return NAME;
}
private final Tracker tracker;
private final TrackerGPS trackerGPS;
private final TrackerPressure trackerPressure;
private Double mElevationOffset = null;
private Double mAverageGpsElevation = null;
private long minEleAverageCutoffTime = Long.MAX_VALUE;
private boolean mAltitudeAdjust = true;
private boolean mAltitudeFromGpsAverage = true;
@SuppressWarnings("unused")
private boolean isStarted;
public TrackerElevation(Tracker tracker, TrackerGPS trackerGPS, TrackerPressure trackerPressure){
this.tracker = tracker;
this.trackerGPS = trackerGPS;
this.trackerPressure = trackerPressure;
}
@SuppressLint("NewApi")
public Double getValue() {
Double val;
Float pressure = tracker.getCurrentPressure();
if (pressure != null && BuildConfig.VERSION_CODE >= 9) {
//Pressure available - use it for elevation
//TODO get real sea level pressure (online) or set offset from start/end
//noinspection InlinedApi
val = ((Float) SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, pressure)).doubleValue();
if (mElevationOffset == null) {
//"Lock" the offset (can be unlocked in onLocationChanged)
if (mAltitudeFromGpsAverage && mAverageGpsElevation != null) {
//pressure is low-pass filtered, compare to low-pass GPS elevation
mElevationOffset = mAverageGpsElevation - val;
} else {
mElevationOffset = 0D;
}
if (tracker.getLastKnownLocation() != null && mAltitudeAdjust) {
mElevationOffset -= Geoid.getOffset(tracker.getLastKnownLocation().getLatitude(),
tracker.getLastKnownLocation().getLongitude());
}
}
val += mElevationOffset;
} else if (tracker.getLastKnownLocation() != null && tracker.getLastKnownLocation().hasAltitude()) {
val = tracker.getLastKnownLocation().getAltitude();
if (mAltitudeAdjust) {
val -= Geoid.getOffset(tracker.getLastKnownLocation().getLatitude(),
tracker.getLastKnownLocation().getLongitude());
}
} else {
val = null;
}
return val;
}
public void onLocationChanged(android.location.Location arg0) {
if (arg0.hasAltitude()
&& (mElevationOffset == null || arg0.getTime() < minEleAverageCutoffTime || !isStarted)) {
//If mElevationOffset is not "used" yet or shortly after first GPS, update the average
Double ele = arg0.getAltitude();
final int minElevationStabilizeTime = 60;
if (minEleAverageCutoffTime == Long.MAX_VALUE) {
minEleAverageCutoffTime = arg0.getTime() + minElevationStabilizeTime * 1000;
}
if (mAverageGpsElevation == null) {
mAverageGpsElevation = ele;
} else {
final float alpha = 0.5f;
mAverageGpsElevation = mAverageGpsElevation * alpha + (1 - alpha) * ele;
}
//Recalculate offset when needed
mElevationOffset = null;
}
}
@Override
public void onSensorChanged(SensorEvent event) { }
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
/**
* Sensor is available
*/
//@SuppressWarnings("unused")
//public static boolean isAvailable(@SuppressWarnings("UnusedParameters") final Context context) {
// //Need trackerGPS or trackerPressure to determine this
// //GPS is mandatory
// return true;
//}
/**
* Called by Tracker during initialization
*/
@Override
public ResultCode onInit(Callback callback, Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
mAltitudeAdjust = prefs.getBoolean(context.getString(R.string.pref_altitude_adjust), true);
mAltitudeFromGpsAverage = prefs.getBoolean(context.getString(org.runnerup.R.string.pref_pressure_elevation_gps_average), false);
if (mAltitudeAdjust) {
try {
Geoid.init(context.getAssets().open("egm96-delta.dat"));
} catch (IOException e) {
Log.e("TrackerElevation", "Altitude correction " + e);
}
}
return ResultCode.RESULT_OK;
}
@Override
public ResultCode onConnecting(final Callback callback, final Context context) {
ResultCode res;
if (trackerGPS.isConnected() || trackerPressure.isConnected()) {
res = ResultCode.RESULT_OK;
} else {
res = ResultCode.RESULT_NOT_SUPPORTED;
}
return res;
}
@Override
public boolean isConnected() {
return (trackerGPS.isConnected() || trackerPressure.isConnected());
}
@Override
public void onConnected() {
}
/**
* Called by Tracker before start
* Component shall populate bindValues
* with objects that will then be passed
* to workout
*/
//public void onBind(HashMap<String, Object> bindValues) {
//}
/**
* Called by Tracker when workout starts
*/
@Override
public void onStart() {
isStarted = true;
}
/**
* Called by Tracker when workout is paused
*/
@Override
public void onPause() {
isStarted = false;
minEleAverageCutoffTime = Long.MAX_VALUE;
mElevationOffset = null;
}
/**
* Called by Tracker when workout is resumed
*/
@Override
public void onResume() {
isStarted = true;
}
/**
* Called by Tracker when workout is complete
*/
@Override
public void onComplete(boolean discarded) {
isStarted = false;
minEleAverageCutoffTime = Long.MAX_VALUE;
mElevationOffset = null;
mAverageGpsElevation = null;
}
/**
* Called by tracked after workout has ended
*/
@Override
public ResultCode onEnd(Callback callback, Context context) {
isStarted = false;
minEleAverageCutoffTime = Long.MAX_VALUE;
mElevationOffset = null;
mAverageGpsElevation = null;
return ResultCode.RESULT_OK;
}
}