From 1d3ecdfebbe2783b48baaca0670185426ad6026c Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Sat, 11 Jan 2020 17:11:49 +0100 Subject: [PATCH 1/3] Edit Activity: Allow recompute without edit The changes have effect immediately, just confusing with Edit-Save stage Instead adding "Are you sure" popup --- app/res/menu/detail_menu.xml | 16 +- .../org/runnerup/view/DetailActivity.java | 1876 +++++++++-------- 2 files changed, 952 insertions(+), 940 deletions(-) diff --git a/app/res/menu/detail_menu.xml b/app/res/menu/detail_menu.xml index f2c73e473..8436b5a39 100644 --- a/app/res/menu/detail_menu.xml +++ b/app/res/menu/detail_menu.xml @@ -17,23 +17,23 @@ --> + + - + android:title="@string/Recompute_activity"/> + - diff --git a/app/src/main/org/runnerup/view/DetailActivity.java b/app/src/main/org/runnerup/view/DetailActivity.java index 86af030b3..b4e62a21e 100644 --- a/app/src/main/org/runnerup/view/DetailActivity.java +++ b/app/src/main/org/runnerup/view/DetailActivity.java @@ -1,932 +1,944 @@ -/* - * Copyright (C) 2012 - 2013 jonas.oreland@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 . - */ - -package org.runnerup.view; - -import android.annotation.SuppressLint; -import android.content.ContentValues; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.graphics.Color; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnLongClickListener; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.TabHost; -import android.widget.TabHost.TabSpec; -import android.widget.TextView; - -import com.mapbox.mapboxsdk.maps.MapView; - -import org.runnerup.BuildConfig; -import org.runnerup.R; -import org.runnerup.common.util.Constants; -import org.runnerup.content.ActivityProvider; -import org.runnerup.db.ActivityCleaner; -import org.runnerup.db.DBHelper; -import org.runnerup.export.SyncManager; -import org.runnerup.export.Synchronizer; -import org.runnerup.export.Synchronizer.Feature; -import org.runnerup.util.Bitfield; -import org.runnerup.util.Formatter; -import org.runnerup.util.GraphWrapper; -import org.runnerup.util.MapWrapper; -import org.runnerup.widget.TitleSpinner; -import org.runnerup.widget.WidgetUtil; -import org.runnerup.workout.Intensity; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Locale; -import java.util.Map; - -import static org.runnerup.content.ActivityProvider.GPX_MIME; -import static org.runnerup.content.ActivityProvider.TCX_MIME; - - -public class DetailActivity extends AppCompatActivity implements Constants { - - private long mID = 0; - private SQLiteDatabase mDB = null; - private final HashSet pendingSynchronizers = new HashSet<>(); - private final HashSet alreadySynched = new HashSet<>(); - private final Map synchedExternalId = new HashMap<>(); - - private boolean lapHrPresent = false; - private ContentValues[] laps = null; - private final ArrayList reports = new ArrayList<>(); - private final ArrayList adapters = new ArrayList<>(2); - - private int mode; // 0 == save 1 == details - private final static int MODE_SAVE = 0; - private final static int MODE_DETAILS = 1; - private boolean edit = false; - private boolean uploading = false; - - private Button saveButton = null; - private Button uploadButton = null; - private Button resumeButton = null; - private TextView activityTime = null; - private TextView activityPace = null; - private View activityPaceSeparator = null; - private TextView activityDistance = null; - - private TitleSpinner sport = null; - private EditText notes = null; - private MenuItem recomputeMenuItem = null; - - private MapWrapper mapWrapper = null; - - private SyncManager syncManager = null; - private Formatter formatter = null; - - /** - * Called when the activity is first created. - */ - @SuppressLint("ObsoleteSdkInt") - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (BuildConfig.MAPBOX_ENABLED > 0) { - MapWrapper.start(this); - setContentView(R.layout.detail); - } else { - // No MapBox key, load without mapview, do not set mapWrapper - setContentView(R.layout.detail_nomap); - } - Toolbar toolbar = (Toolbar) findViewById(R.id.actionbar); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - WidgetUtil.addLegacyOverflowButton(getWindow()); - - Intent intent = getIntent(); - mID = intent.getLongExtra("ID", -1); - String mode = intent.getStringExtra("mode"); - - mDB = DBHelper.getReadableDatabase(this); - syncManager = new SyncManager(this); - formatter = new Formatter(this); - - if (mode.contentEquals("save")) { - this.mode = MODE_SAVE; - } else if (mode.contentEquals("details")) { - this.mode = MODE_DETAILS; - } else { - if (BuildConfig.DEBUG) { - throw new AssertionError(); - } - } - - saveButton = (Button) findViewById(R.id.save_button); - Button discardButton = (Button) findViewById(R.id.discard_button); - resumeButton = (Button) findViewById(R.id.resume_button); - uploadButton = (Button) findViewById(R.id.upload_button); - activityTime = (TextView) findViewById(R.id.activity_time); - activityDistance = (TextView) findViewById(R.id.activity_distance); - activityPace = (TextView) findViewById(R.id.activity_pace); - activityPaceSeparator = findViewById(R.id.activity_pace_separator); - sport = (TitleSpinner) findViewById(R.id.summary_sport); - notes = (EditText) findViewById(R.id.notes_text); - - if (BuildConfig.MAPBOX_ENABLED > 0) { - MapView mapView = (MapView) findViewById(R.id.mapview); - mapWrapper = new MapWrapper(this, mDB, mID, formatter, mapView); - mapWrapper.onCreate(savedInstanceState); - } - - saveButton.setOnClickListener(saveButtonClick); - uploadButton.setOnClickListener(uploadButtonClick); - if (this.mode == MODE_SAVE) { - resumeButton.setOnClickListener(resumeButtonClick); - discardButton.setOnClickListener(discardButtonClick); - setEdit(true); - } else if (this.mode == MODE_DETAILS) { - resumeButton.setVisibility(View.GONE); - discardButton.setVisibility(View.GONE); - setEdit(false); - } - - fillHeaderData(); - requery(); - uploadButton.setVisibility(View.GONE); - - TabHost th = (TabHost) findViewById(R.id.tabhost); - th.setup(); - TabSpec tabSpec = th.newTabSpec("notes"); - tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Notes))); - tabSpec.setContent(R.id.tab_main); - th.addTab(tabSpec); - - tabSpec = th.newTabSpec("laps"); - tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Laps))); - tabSpec.setContent(R.id.tab_lap); - th.addTab(tabSpec); - - if (BuildConfig.MAPBOX_ENABLED > 0) { - tabSpec = th.newTabSpec("map"); - tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Map))); - tabSpec.setContent(R.id.tab_map); - th.addTab(tabSpec); - } - - tabSpec = th.newTabSpec("graph"); - tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Graph))); - tabSpec.setContent(R.id.tab_graph); - th.addTab(tabSpec); - - LinearLayout graphTab = (LinearLayout) findViewById(R.id.tab_graph); - LinearLayout hrzonesBarLayout = (LinearLayout) findViewById(R.id.hrzonesBarLayout); - //noinspection UnusedAssignment - GraphWrapper graphWrapper = new GraphWrapper(this, graphTab, hrzonesBarLayout, formatter, mDB, mID); - - tabSpec = th.newTabSpec("share"); - tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Upload))); - tabSpec.setContent(R.id.tab_upload); - th.addTab(tabSpec); - - { - ListView lv = (ListView) findViewById(R.id.laplist); - LapListAdapter adapter = new LapListAdapter(); - adapters.add(adapter); - lv.setAdapter(adapter); - } - { - ListView lv = (ListView) findViewById(R.id.report_list); - ReportListAdapter adapter = new ReportListAdapter(); - adapters.add(adapter); - lv.setAdapter(adapter); - } - } - - private void setEdit(boolean value) { - edit = value; - if (value) - saveButton.setVisibility(View.VISIBLE); - else - saveButton.setVisibility(View.GONE); - WidgetUtil.setEditable(notes, value); - sport.setEnabled(value); - if (recomputeMenuItem != null) - recomputeMenuItem.setEnabled(value); - } - - private void setUploadVisibility() { - Boolean enabled = !pendingSynchronizers.isEmpty(); - if (enabled) { - uploadButton.setVisibility(View.VISIBLE); - } else { - uploadButton.setVisibility(View.GONE); - } - } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - if (mode == MODE_DETAILS) { - getMenuInflater().inflate(R.menu.detail_menu, menu); - recomputeMenuItem = menu.findItem(R.id.menu_recompute_activity); - } - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - return super.onOptionsItemSelected(item); - case R.id.menu_delete_activity: - deleteButtonClick.onClick(null); - break; - case R.id.menu_edit_activity: - if (!edit) { - setEdit(true); - notes.requestFocus(); - requery(); - } - break; - case R.id.menu_recompute_activity: - new ActivityCleaner().recompute(mDB, mID); - requery(); - break; - case R.id.menu_share_activity: - shareActivity(); - break; - } - return true; - } - - @Override - public void onResume() { - super.onResume(); - if(mapWrapper != null) { - mapWrapper.onResume(); - } - } - - @Override - public void onStart() { - super.onStart(); - if(mapWrapper != null) { - mapWrapper.onStart(); - } - } - - @Override - public void onStop() { - super.onStop(); - if(mapWrapper != null) { - mapWrapper.onStop(); - } - } - - @Override - public void onPause() { - super.onPause(); - if(mapWrapper != null) { - mapWrapper.onPause(); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if(mapWrapper != null) { - mapWrapper.onSaveInstanceState(outState); - } - } - - @Override - public void onLowMemory() { - super.onLowMemory(); - if(mapWrapper != null) { - mapWrapper.onLowMemory(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - DBHelper.closeDB(mDB); - syncManager.close(); - if(mapWrapper != null) { - mapWrapper.onDestroy(); - } - } - - private void requery() { - { - /* - * Laps - */ - String[] from = new String[]{ - "_id", DB.LAP.LAP, DB.LAP.INTENSITY, - DB.LAP.TIME, DB.LAP.DISTANCE, DB.LAP.PLANNED_TIME, - DB.LAP.PLANNED_DISTANCE, DB.LAP.PLANNED_PACE, DB.LAP.AVG_HR - }; - - Cursor c = mDB.query(DB.LAP.TABLE, from, DB.LAP.ACTIVITY + " == " + mID, - null, null, null, "_id", null); - - laps = DBHelper.toArray(c); - c.close(); - lapHrPresent = false; - for (ContentValues v : laps) { - if (v.containsKey(DB.LAP.AVG_HR) && v.getAsInteger(DB.LAP.AVG_HR) > 0) { - lapHrPresent = true; - break; - } - } - } - - { - /* - * Accounts/reports - */ - String sql = "SELECT DISTINCT " - + " acc._id, " - + (" acc." + DB.ACCOUNT.NAME + ", ") - + (" acc." + DB.ACCOUNT.FLAGS + ", ") - + (" acc." + DB.ACCOUNT.AUTH_CONFIG + ", ") - + (" rep._id as repid, ") - + (" rep." + DB.EXPORT.ACCOUNT + ", ") - + (" rep." + DB.EXPORT.ACTIVITY + ", ") - + (" rep." + DB.EXPORT.EXTERNAL_ID + ", ") - + (" rep." + DB.EXPORT.STATUS) - + (" FROM " + DB.ACCOUNT.TABLE + " acc ") - + (" LEFT OUTER JOIN " + DB.EXPORT.TABLE + " rep ") - + (" ON ( acc._id = rep." + DB.EXPORT.ACCOUNT) - + (" AND rep." + DB.EXPORT.ACTIVITY + " = " - + mID + " )"); - - Cursor c = mDB.rawQuery(sql, null); - alreadySynched.clear(); - synchedExternalId.clear(); - pendingSynchronizers.clear(); - reports.clear(); - if (c.moveToFirst()) { - do { - ContentValues tmp = DBHelper.get(c); - Synchronizer synchronizer = syncManager.add(tmp); - //Note: Show all configured accounts (also those are not currently enabled) - //Uploaded but removed accounts are not displayed - if (synchronizer == null || !synchronizer.checkSupport(Feature.UPLOAD) || !synchronizer.isConfigured()) { - continue; - } - - String name = tmp.getAsString(DB.ACCOUNT.NAME); - reports.add(tmp); - if (tmp.containsKey("repid")) { - alreadySynched.add(name); - if (tmp.containsKey(DB.EXPORT.STATUS) && tmp.getAsInteger(DB.EXPORT.STATUS) == Synchronizer.ExternalIdStatus.getInt(Synchronizer.ExternalIdStatus.OK)) { - String url = syncManager.getSynchronizerByName(name).getActivityUrl(synchedExternalId.get(name)); - if (url != null) { - synchedExternalId.put(name, tmp.getAsString(DB.EXPORT.EXTERNAL_ID)); - } - } - } else if (tmp.containsKey(DB.ACCOUNT.FLAGS) - && Bitfield.test(tmp.getAsLong(DB.ACCOUNT.FLAGS), - DB.ACCOUNT.FLAG_UPLOAD)) { - pendingSynchronizers.add(name); - } - } while (c.moveToNext()); - } - c.close(); - } - - if (mode == MODE_DETAILS) { - setUploadVisibility(); - } - - for (BaseAdapter a : adapters) { - a.notifyDataSetChanged(); - } - } - - private void fillHeaderData() { - // Fields from the database (projection) - // Must include the _id column for the adapter to work - String[] from = new String[]{ - DB.ACTIVITY.START_TIME, - DB.ACTIVITY.DISTANCE, DB.ACTIVITY.TIME, DB.ACTIVITY.COMMENT, - DB.ACTIVITY.SPORT - }; - - Cursor c = mDB.query(DB.ACTIVITY.TABLE, from, "_id == " + mID, null, - null, null, null, null); - c.moveToFirst(); - ContentValues tmp = DBHelper.get(c); - c.close(); - - if (tmp.containsKey(DB.ACTIVITY.START_TIME)) { - long st = tmp.getAsLong(DB.ACTIVITY.START_TIME); - setTitle(formatter.formatDateTime(st)); - } - double d = 0; - if (tmp.containsKey(DB.ACTIVITY.DISTANCE)) { - d = tmp.getAsDouble(DB.ACTIVITY.DISTANCE); - activityDistance.setText(formatter.formatDistance(Formatter.Format.TXT_SHORT, (long) d)); - } else { - activityDistance.setText(""); - } - - long t = 0; - if (tmp.containsKey(DB.ACTIVITY.TIME)) { - t = tmp.getAsInteger(DB.ACTIVITY.TIME); - activityTime.setText(formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, t)); - } else { - activityTime.setText(""); - } - - if (t != 0) { - activityPace.setVisibility(View.VISIBLE); - activityPaceSeparator.setVisibility(View.VISIBLE); - activityPace.setText(formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d / t)); - } else { - activityPace.setVisibility(View.GONE); - activityPaceSeparator.setVisibility(View.GONE); - } - - if (tmp.containsKey(DB.ACTIVITY.COMMENT)) { - notes.setText(tmp.getAsString(DB.ACTIVITY.COMMENT)); - } - - if (tmp.containsKey(DB.ACTIVITY.SPORT)) { - sport.setValue(tmp.getAsInteger(DB.ACTIVITY.SPORT)); - } - } - - private class ViewHolderLapList { - private TextView tv0; - private TextView tv1; - private TextView tv2; - private TextView tv3; - private TextView tv4; - private TextView tvHr; - } - - private class LapListAdapter extends BaseAdapter { - - @Override - public int getCount() { - return laps.length; - } - - @Override - public Object getItem(int position) { - return laps[position]; - } - - @Override - public long getItemId(int position) { - return laps[position].getAsLong("_id"); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - ViewHolderLapList viewHolder; - - if (view == null) { - viewHolder = new ViewHolderLapList(); - LayoutInflater inflater = LayoutInflater.from(DetailActivity.this); - view = inflater.inflate(R.layout.laplist_row, parent, false); - viewHolder.tv0 = (TextView) view.findViewById(R.id.lap_list_type); - viewHolder.tv1 = (TextView) view.findViewById(R.id.lap_list_id); - viewHolder.tv2 = (TextView) view.findViewById(R.id.lap_list_distance); - viewHolder.tv3 = (TextView) view.findViewById(R.id.lap_list_time); - viewHolder.tv4 = (TextView) view.findViewById(R.id.lap_list_pace); - viewHolder.tvHr = (TextView) view.findViewById(R.id.lap_list_hr); - - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolderLapList) view.getTag(); - } - int i = laps[position].getAsInteger(DB.LAP.INTENSITY); - Intensity intensity = Intensity.values()[i]; - switch (intensity) { - case ACTIVE: - viewHolder.tv0.setText(""); - break; - case COOLDOWN: - case RESTING: - case RECOVERY: - case WARMUP: - case REPEAT: - viewHolder.tv0.setText(String.format(Locale.getDefault(), "(%s)", - getResources().getString(intensity.getTextId()))); - default: - break; - - } - viewHolder.tv1.setText(laps[position].getAsString("_id")); - double d = laps[position].containsKey(DB.LAP.DISTANCE) ? laps[position] - .getAsDouble(DB.LAP.DISTANCE) : 0; - viewHolder.tv2.setText(formatter.formatDistance(Formatter.Format.TXT_LONG, (long) d)); - long t = laps[position].containsKey(DB.LAP.TIME) ? laps[position] - .getAsLong(DB.LAP.TIME) : 0; - viewHolder.tv3.setText(formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, t)); - if (t != 0) { - viewHolder.tv4.setText(formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d/t)); - } else { - viewHolder.tv4.setText(""); - } - int hr = laps[position].containsKey(DB.LAP.AVG_HR) ? laps[position] - .getAsInteger(DB.LAP.AVG_HR) : 0; - if (hr > 0) { - viewHolder.tvHr.setVisibility(View.VISIBLE); - // Use CUE_LONG instead of TXT_LONG to include unit - viewHolder.tvHr.setText(formatter.formatHeartRate(Formatter.Format.CUE_LONG, hr)); - } else if (lapHrPresent) { - viewHolder.tvHr.setVisibility(View.INVISIBLE); - } else { - viewHolder.tvHr.setVisibility(View.GONE); - } - - return view; - } - } - - private class ReportListAdapter extends BaseAdapter { - - @Override - public int getCount() { - return reports.size() + 1; - } - - @Override - public Object getItem(int position) { - if (position < reports.size()) - return reports.get(position); - return this; - } - - @Override - public long getItemId(int position) { - if (position < reports.size()) - return reports.get(position).getAsLong("_id"); - - return 0; - } - - private class ViewHolderDetailActivity { - private TextView tv0; - private CheckBox cb; - private TextView tv1; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (position == reports.size()) { - Button b = new Button(DetailActivity.this); - b.setText(getString(R.string.Configure_accounts)); - b.setBackgroundResource(R.drawable.btn_blue); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - b.setTextColor(getResources().getColorStateList(R.color.btn_text_color, getTheme())); - } else { - //noinspection deprecation - b.setTextColor(getResources().getColorStateList(R.color.btn_text_color)); - } - b.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - Intent i = new Intent(DetailActivity.this, - AccountListActivity.class); - DetailActivity.this.startActivityForResult(i, - SyncManager.EDIT_ACCOUNT_REQUEST); - } - }); - return b; - } - - View view = convertView; - ViewHolderDetailActivity viewHolder; - - //Note: Special ViewHolder support as the Configure button is not in the view - if (view == null || view.getTag() == null) { - viewHolder = new ViewHolderDetailActivity(); - - LayoutInflater inflater = LayoutInflater.from(DetailActivity.this); - view = inflater.inflate(R.layout.reportlist_row, parent, false); - - viewHolder.tv0 = (TextView) view.findViewById(R.id.account_id); - viewHolder.cb = (CheckBox) view.findViewById(R.id.report_sent); - viewHolder.tv1 = (TextView) view.findViewById(R.id.account_name); - - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolderDetailActivity) view.getTag(); - } - - ContentValues tmp = reports.get(position); - String name = tmp.getAsString(DB.ACCOUNT.NAME); - viewHolder.cb.setOnCheckedChangeListener(null); - viewHolder.cb.setChecked(false); - viewHolder.cb.setEnabled(false); - viewHolder.cb.setTag(name); - viewHolder.tv1.setTag(name); - viewHolder.tv1.setTextColor(viewHolder.cb.getTextColors()); - if (alreadySynched.contains(name)) { - viewHolder.cb.setChecked(true); - if (synchedExternalId.containsKey(name)) { - //Indicate Clickable label - viewHolder.tv1.setTextColor(Color.BLUE); - } - viewHolder.cb.setText(getString(R.string.Uploaded)); - viewHolder.cb.setOnLongClickListener(clearUploadClick); - } else { - if (pendingSynchronizers.contains(name)) { - viewHolder.cb.setChecked(true); - } else { - viewHolder.cb.setChecked(false); - } - viewHolder.cb.setText(getString(R.string.upload)); - viewHolder.cb.setOnLongClickListener(null); - } - if (mode == MODE_DETAILS) { - viewHolder.cb.setEnabled(true); - } else if (mode == MODE_SAVE) { - viewHolder.cb.setEnabled(true); - } - viewHolder.cb.setOnCheckedChangeListener(onSendChecked); - - viewHolder.tv0.setText(tmp.getAsString("_id")); - viewHolder.tv1.setText(name); - - return view; - } - } - - private void saveActivity() { - ContentValues tmp = new ContentValues(); - tmp.put(DB.ACTIVITY.COMMENT, notes.getText().toString()); - tmp.put(DB.ACTIVITY.SPORT, sport.getValueInt()); - String whereArgs[] = { - Long.toString(mID) - }; - mDB.update(DB.ACTIVITY.TABLE, tmp, "_id = ?", whereArgs); - } - - private final OnLongClickListener clearUploadClick = new OnLongClickListener() { - - @Override - public boolean onLongClick(View arg0) { - final String name = (String) arg0.getTag(); - AlertDialog.Builder builder = new AlertDialog.Builder(DetailActivity.this) - .setTitle("Clear upload for " + name) - .setMessage(getString(R.string.Are_you_sure)) - .setPositiveButton(getString(R.string.Yes), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - syncManager.clearUpload(name, mID); - requery(); - } - }) - .setNegativeButton(getString(R.string.No), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Do nothing but close the dialog - dialog.dismiss(); - } - - }); - builder.show(); - return false; - } - - }; - - //Note: onClick set in reportlist_row.xml - public void onClickAccountName(View arg0) { - final String name = (String) arg0.getTag(); - if (synchedExternalId.containsKey(name) && !TextUtils.isEmpty(synchedExternalId.get(name))) { - String url = syncManager.getSynchronizerByName(name).getActivityUrl(synchedExternalId.get(name)); - if (url != null) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(browserIntent); - } - } - } - - private final OnClickListener saveButtonClick = new OnClickListener() { - public void onClick(View v) { - saveActivity(); - if (mode == MODE_DETAILS) { - setEdit(false); - requery(); - return; - } - uploading = true; - syncManager.startUploading(new SyncManager.Callback() { - @Override - public void run(String synchronizerName, Synchronizer.Status status) { - uploading = false; - DetailActivity.this.setResult(RESULT_OK); - DetailActivity.this.finish(); - } - }, pendingSynchronizers, mID); - } - }; - - private final OnClickListener discardButtonClick = new OnClickListener() { - public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(DetailActivity.this) - .setTitle(getString(R.string.Discard_activity)) - .setMessage(getString(R.string.Are_you_sure)) - .setPositiveButton(getString(R.string.Yes), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - DetailActivity.this.setResult(RESULT_CANCELED); - DetailActivity.this.finish(); - } - }) - .setNegativeButton(getString(R.string.No), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Do nothing but close the dialog - dialog.dismiss(); - } - - }); - builder.show(); - } - }; - - @Override - public void onBackPressed() { - if (uploading) { - /* - * Ignore while uploading - */ - return; - } - if (mode == MODE_SAVE) { - resumeButtonClick.onClick(resumeButton); - } else { - super.onBackPressed(); - } - } - - private final OnClickListener resumeButtonClick = new OnClickListener() { - public void onClick(View v) { - DetailActivity.this.setResult(RESULT_FIRST_USER); - DetailActivity.this.finish(); - } - }; - - private final OnClickListener uploadButtonClick = new OnClickListener() { - public void onClick(View v) { - uploading = true; - syncManager.startUploading(new SyncManager.Callback() { - @Override - public void run(String synchronizerName, Synchronizer.Status status) { - uploading = false; - requery(); - } - }, pendingSynchronizers, mID); - } - }; - - private final OnCheckedChangeListener onSendChecked = new OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton arg0, boolean arg1) { - final String name = (String) arg0.getTag(); - if (alreadySynched.contains(name)) { - // Only accept long clicks - arg0.setChecked(true); - } else { - if (arg1) { - pendingSynchronizers.add((String) arg0.getTag()); - } else { - pendingSynchronizers.remove(arg0.getTag()); - } - if (mode == MODE_DETAILS) { - setUploadVisibility(); - } - } - } - }; - - private final OnClickListener deleteButtonClick = new OnClickListener() { - public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(DetailActivity.this) - .setTitle(getString(R.string.Delete_activity)) - .setMessage(getString(R.string.Are_you_sure)) - .setPositiveButton(getString(R.string.Yes), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - DBHelper.deleteActivity(mDB, mID); - dialog.dismiss(); - DetailActivity.this.setResult(RESULT_OK); - DetailActivity.this.finish(); - } - }) - .setNegativeButton(getString(R.string.No), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Do nothing but close the dialog - dialog.dismiss(); - } - - }); - builder.show(); - } - }; - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == SyncManager.CONFIGURE_REQUEST) { - syncManager.onActivityResult(requestCode, resultCode, data); - } - requery(); - } - - private void shareActivity() { - final int which[] = { - 1 //TODO preselect tcx - choice should be remembered - }; - final CharSequence items[] = { - "gpx", "tcx" /* "nike+xml" */ - }; - AlertDialog.Builder builder = new AlertDialog.Builder(this) - .setTitle(getString(R.string.Share_activity)) - .setPositiveButton(getString(R.string.OK), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int w) { - if (which[0] == -1) { - dialog.dismiss(); - return; - } - - final Context context = DetailActivity.this; - final CharSequence fmt = items[which[0]]; - final Intent intent = new Intent(Intent.ACTION_SEND); - - if (fmt.equals("tcx")) { - intent.setType(TCX_MIME); - } else { - intent.setType(GPX_MIME); - } - //Use of content:// (or STREAM?) instead of file:// is not supported in ES and other apps - //Solid Explorer File Manager works though - Uri uri = Uri.parse("content://" + ActivityProvider.AUTHORITY + "/" + fmt - + "/" + mID - + "/" + String.format(Locale.getDefault(), "RunnerUp_%04d.%s", mID, fmt)); - intent.putExtra(Intent.EXTRA_STREAM, uri); - context.startActivity(Intent.createChooser(intent, getString(R.string.Share_activity))); - } - }) - .setNegativeButton(getString(R.string.Cancel), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Do nothing but close the dialog - dialog.dismiss(); - } - - }) - .setSingleChoiceItems(items, which[0], new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int w) { - which[0] = w; - } - }); - builder.show(); - } -} +/* + * Copyright (C) 2012 - 2013 jonas.oreland@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 . + */ + +package org.runnerup.view; + +import android.annotation.SuppressLint; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TabHost; +import android.widget.TabHost.TabSpec; +import android.widget.TextView; + +import com.mapbox.mapboxsdk.maps.MapView; + +import org.runnerup.BuildConfig; +import org.runnerup.R; +import org.runnerup.common.util.Constants; +import org.runnerup.content.ActivityProvider; +import org.runnerup.db.ActivityCleaner; +import org.runnerup.db.DBHelper; +import org.runnerup.export.SyncManager; +import org.runnerup.export.Synchronizer; +import org.runnerup.export.Synchronizer.Feature; +import org.runnerup.util.Bitfield; +import org.runnerup.util.Formatter; +import org.runnerup.util.GraphWrapper; +import org.runnerup.util.MapWrapper; +import org.runnerup.widget.TitleSpinner; +import org.runnerup.widget.WidgetUtil; +import org.runnerup.workout.Intensity; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; + +import static org.runnerup.content.ActivityProvider.GPX_MIME; +import static org.runnerup.content.ActivityProvider.TCX_MIME; + + +public class DetailActivity extends AppCompatActivity implements Constants { + + private long mID = 0; + private SQLiteDatabase mDB = null; + private final HashSet pendingSynchronizers = new HashSet<>(); + private final HashSet alreadySynched = new HashSet<>(); + private final Map synchedExternalId = new HashMap<>(); + + private boolean lapHrPresent = false; + private ContentValues[] laps = null; + private final ArrayList reports = new ArrayList<>(); + private final ArrayList adapters = new ArrayList<>(2); + + private int mode; // 0 == save 1 == details + private final static int MODE_SAVE = 0; + private final static int MODE_DETAILS = 1; + private boolean edit = false; + private boolean uploading = false; + + private Button saveButton = null; + private Button uploadButton = null; + private Button resumeButton = null; + private TextView activityTime = null; + private TextView activityPace = null; + private View activityPaceSeparator = null; + private TextView activityDistance = null; + + private TitleSpinner sport = null; + private EditText notes = null; + + private MapWrapper mapWrapper = null; + + private SyncManager syncManager = null; + private Formatter formatter = null; + + /** + * Called when the activity is first created. + */ + @SuppressLint("ObsoleteSdkInt") + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (BuildConfig.MAPBOX_ENABLED > 0) { + MapWrapper.start(this); + setContentView(R.layout.detail); + } else { + // No MapBox key, load without mapview, do not set mapWrapper + setContentView(R.layout.detail_nomap); + } + Toolbar toolbar = (Toolbar) findViewById(R.id.actionbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + WidgetUtil.addLegacyOverflowButton(getWindow()); + + Intent intent = getIntent(); + mID = intent.getLongExtra("ID", -1); + String mode = intent.getStringExtra("mode"); + + mDB = DBHelper.getReadableDatabase(this); + syncManager = new SyncManager(this); + formatter = new Formatter(this); + + if (mode.contentEquals("save")) { + this.mode = MODE_SAVE; + } else if (mode.contentEquals("details")) { + this.mode = MODE_DETAILS; + } else { + if (BuildConfig.DEBUG) { + throw new AssertionError(); + } + } + + saveButton = (Button) findViewById(R.id.save_button); + Button discardButton = (Button) findViewById(R.id.discard_button); + resumeButton = (Button) findViewById(R.id.resume_button); + uploadButton = (Button) findViewById(R.id.upload_button); + activityTime = (TextView) findViewById(R.id.activity_time); + activityDistance = (TextView) findViewById(R.id.activity_distance); + activityPace = (TextView) findViewById(R.id.activity_pace); + activityPaceSeparator = findViewById(R.id.activity_pace_separator); + sport = (TitleSpinner) findViewById(R.id.summary_sport); + notes = (EditText) findViewById(R.id.notes_text); + + if (BuildConfig.MAPBOX_ENABLED > 0) { + MapView mapView = (MapView) findViewById(R.id.mapview); + mapWrapper = new MapWrapper(this, mDB, mID, formatter, mapView); + mapWrapper.onCreate(savedInstanceState); + } + + saveButton.setOnClickListener(saveButtonClick); + uploadButton.setOnClickListener(uploadButtonClick); + if (this.mode == MODE_SAVE) { + resumeButton.setOnClickListener(resumeButtonClick); + discardButton.setOnClickListener(discardButtonClick); + setEdit(true); + } else if (this.mode == MODE_DETAILS) { + resumeButton.setVisibility(View.GONE); + discardButton.setVisibility(View.GONE); + setEdit(false); + } + + fillHeaderData(); + requery(); + uploadButton.setVisibility(View.GONE); + + TabHost th = (TabHost) findViewById(R.id.tabhost); + th.setup(); + TabSpec tabSpec = th.newTabSpec("notes"); + tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Notes))); + tabSpec.setContent(R.id.tab_main); + th.addTab(tabSpec); + + tabSpec = th.newTabSpec("laps"); + tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Laps))); + tabSpec.setContent(R.id.tab_lap); + th.addTab(tabSpec); + + if (BuildConfig.MAPBOX_ENABLED > 0) { + tabSpec = th.newTabSpec("map"); + tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Map))); + tabSpec.setContent(R.id.tab_map); + th.addTab(tabSpec); + } + + tabSpec = th.newTabSpec("graph"); + tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Graph))); + tabSpec.setContent(R.id.tab_graph); + th.addTab(tabSpec); + + LinearLayout graphTab = (LinearLayout) findViewById(R.id.tab_graph); + LinearLayout hrzonesBarLayout = (LinearLayout) findViewById(R.id.hrzonesBarLayout); + //noinspection UnusedAssignment + GraphWrapper graphWrapper = new GraphWrapper(this, graphTab, hrzonesBarLayout, formatter, mDB, mID); + + tabSpec = th.newTabSpec("share"); + tabSpec.setIndicator(WidgetUtil.createHoloTabIndicator(this, getString(R.string.Upload))); + tabSpec.setContent(R.id.tab_upload); + th.addTab(tabSpec); + + { + ListView lv = (ListView) findViewById(R.id.laplist); + LapListAdapter adapter = new LapListAdapter(); + adapters.add(adapter); + lv.setAdapter(adapter); + } + { + ListView lv = (ListView) findViewById(R.id.report_list); + ReportListAdapter adapter = new ReportListAdapter(); + adapters.add(adapter); + lv.setAdapter(adapter); + } + } + + private void setEdit(boolean value) { + edit = value; + if (value) + saveButton.setVisibility(View.VISIBLE); + else + saveButton.setVisibility(View.GONE); + WidgetUtil.setEditable(notes, value); + sport.setEnabled(value); + } + + private void setUploadVisibility() { + Boolean enabled = !pendingSynchronizers.isEmpty(); + if (enabled) { + uploadButton.setVisibility(View.VISIBLE); + } else { + uploadButton.setVisibility(View.GONE); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.detail_menu, menu); + return true; + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + return super.onOptionsItemSelected(item); + + case R.id.menu_delete_activity: + deleteButtonClick.onClick(null); + break; + + case R.id.menu_edit_activity: + if (!edit) { + setEdit(true); + notes.requestFocus(); + requery(); + } + break; + + case R.id.menu_recompute_activity: + final AlertDialog.Builder builderRecompute = new AlertDialog.Builder(this) + .setTitle(R.string.Recompute_activity) + .setMessage(getString(R.string.Are_you_sure)) + .setPositiveButton(getString(R.string.Yes), (dialog, which) -> { + dialog.dismiss(); + new ActivityCleaner().recompute(mDB, mID); + requery(); + fillHeaderData(); + finish(); + }) + .setNegativeButton(getString(R.string.No),(dialog, which) -> { + dialog.dismiss(); + }); + builderRecompute.show(); + break; + + case R.id.menu_share_activity: + shareActivity(); + break; + } + return true; + } + + @Override + public void onResume() { + super.onResume(); + if(mapWrapper != null) { + mapWrapper.onResume(); + } + } + + @Override + public void onStart() { + super.onStart(); + if(mapWrapper != null) { + mapWrapper.onStart(); + } + } + + @Override + public void onStop() { + super.onStop(); + if(mapWrapper != null) { + mapWrapper.onStop(); + } + } + + @Override + public void onPause() { + super.onPause(); + if(mapWrapper != null) { + mapWrapper.onPause(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if(mapWrapper != null) { + mapWrapper.onSaveInstanceState(outState); + } + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + if(mapWrapper != null) { + mapWrapper.onLowMemory(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + DBHelper.closeDB(mDB); + syncManager.close(); + if(mapWrapper != null) { + mapWrapper.onDestroy(); + } + } + + private void requery() { + { + /* + * Laps + */ + String[] from = new String[]{ + "_id", DB.LAP.LAP, DB.LAP.INTENSITY, + DB.LAP.TIME, DB.LAP.DISTANCE, DB.LAP.PLANNED_TIME, + DB.LAP.PLANNED_DISTANCE, DB.LAP.PLANNED_PACE, DB.LAP.AVG_HR + }; + + Cursor c = mDB.query(DB.LAP.TABLE, from, DB.LAP.ACTIVITY + " == " + mID, + null, null, null, "_id", null); + + laps = DBHelper.toArray(c); + c.close(); + lapHrPresent = false; + for (ContentValues v : laps) { + if (v.containsKey(DB.LAP.AVG_HR) && v.getAsInteger(DB.LAP.AVG_HR) > 0) { + lapHrPresent = true; + break; + } + } + } + + { + /* + * Accounts/reports + */ + String sql = "SELECT DISTINCT " + + " acc._id, " + + (" acc." + DB.ACCOUNT.NAME + ", ") + + (" acc." + DB.ACCOUNT.FLAGS + ", ") + + (" acc." + DB.ACCOUNT.AUTH_CONFIG + ", ") + + (" rep._id as repid, ") + + (" rep." + DB.EXPORT.ACCOUNT + ", ") + + (" rep." + DB.EXPORT.ACTIVITY + ", ") + + (" rep." + DB.EXPORT.EXTERNAL_ID + ", ") + + (" rep." + DB.EXPORT.STATUS) + + (" FROM " + DB.ACCOUNT.TABLE + " acc ") + + (" LEFT OUTER JOIN " + DB.EXPORT.TABLE + " rep ") + + (" ON ( acc._id = rep." + DB.EXPORT.ACCOUNT) + + (" AND rep." + DB.EXPORT.ACTIVITY + " = " + + mID + " )"); + + Cursor c = mDB.rawQuery(sql, null); + alreadySynched.clear(); + synchedExternalId.clear(); + pendingSynchronizers.clear(); + reports.clear(); + if (c.moveToFirst()) { + do { + ContentValues tmp = DBHelper.get(c); + Synchronizer synchronizer = syncManager.add(tmp); + //Note: Show all configured accounts (also those are not currently enabled) + //Uploaded but removed accounts are not displayed + if (synchronizer == null || !synchronizer.checkSupport(Feature.UPLOAD) || !synchronizer.isConfigured()) { + continue; + } + + String name = tmp.getAsString(DB.ACCOUNT.NAME); + reports.add(tmp); + if (tmp.containsKey("repid")) { + alreadySynched.add(name); + if (tmp.containsKey(DB.EXPORT.STATUS) && tmp.getAsInteger(DB.EXPORT.STATUS) == Synchronizer.ExternalIdStatus.getInt(Synchronizer.ExternalIdStatus.OK)) { + String url = syncManager.getSynchronizerByName(name).getActivityUrl(synchedExternalId.get(name)); + if (url != null) { + synchedExternalId.put(name, tmp.getAsString(DB.EXPORT.EXTERNAL_ID)); + } + } + } else if (tmp.containsKey(DB.ACCOUNT.FLAGS) + && Bitfield.test(tmp.getAsLong(DB.ACCOUNT.FLAGS), + DB.ACCOUNT.FLAG_UPLOAD)) { + pendingSynchronizers.add(name); + } + } while (c.moveToNext()); + } + c.close(); + } + + if (mode == MODE_DETAILS) { + setUploadVisibility(); + } + + for (BaseAdapter a : adapters) { + a.notifyDataSetChanged(); + } + } + + private void fillHeaderData() { + // Fields from the database (projection) + // Must include the _id column for the adapter to work + String[] from = new String[]{ + DB.ACTIVITY.START_TIME, + DB.ACTIVITY.DISTANCE, DB.ACTIVITY.TIME, DB.ACTIVITY.COMMENT, + DB.ACTIVITY.SPORT + }; + + Cursor c = mDB.query(DB.ACTIVITY.TABLE, from, "_id == " + mID, null, + null, null, null, null); + c.moveToFirst(); + ContentValues tmp = DBHelper.get(c); + c.close(); + + if (tmp.containsKey(DB.ACTIVITY.START_TIME)) { + long st = tmp.getAsLong(DB.ACTIVITY.START_TIME); + setTitle(formatter.formatDateTime(st)); + } + double d = 0; + if (tmp.containsKey(DB.ACTIVITY.DISTANCE)) { + d = tmp.getAsDouble(DB.ACTIVITY.DISTANCE); + activityDistance.setText(formatter.formatDistance(Formatter.Format.TXT_SHORT, (long) d)); + } else { + activityDistance.setText(""); + } + + long t = 0; + if (tmp.containsKey(DB.ACTIVITY.TIME)) { + t = tmp.getAsInteger(DB.ACTIVITY.TIME); + activityTime.setText(formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, t)); + } else { + activityTime.setText(""); + } + + if (t != 0) { + activityPace.setVisibility(View.VISIBLE); + activityPaceSeparator.setVisibility(View.VISIBLE); + activityPace.setText(formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d / t)); + } else { + activityPace.setVisibility(View.GONE); + activityPaceSeparator.setVisibility(View.GONE); + } + + if (tmp.containsKey(DB.ACTIVITY.COMMENT)) { + notes.setText(tmp.getAsString(DB.ACTIVITY.COMMENT)); + } + + if (tmp.containsKey(DB.ACTIVITY.SPORT)) { + sport.setValue(tmp.getAsInteger(DB.ACTIVITY.SPORT)); + } + } + + private class ViewHolderLapList { + private TextView tv0; + private TextView tv1; + private TextView tv2; + private TextView tv3; + private TextView tv4; + private TextView tvHr; + } + + private class LapListAdapter extends BaseAdapter { + + @Override + public int getCount() { + return laps.length; + } + + @Override + public Object getItem(int position) { + return laps[position]; + } + + @Override + public long getItemId(int position) { + return laps[position].getAsLong("_id"); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + ViewHolderLapList viewHolder; + + if (view == null) { + viewHolder = new ViewHolderLapList(); + LayoutInflater inflater = LayoutInflater.from(DetailActivity.this); + view = inflater.inflate(R.layout.laplist_row, parent, false); + viewHolder.tv0 = (TextView) view.findViewById(R.id.lap_list_type); + viewHolder.tv1 = (TextView) view.findViewById(R.id.lap_list_id); + viewHolder.tv2 = (TextView) view.findViewById(R.id.lap_list_distance); + viewHolder.tv3 = (TextView) view.findViewById(R.id.lap_list_time); + viewHolder.tv4 = (TextView) view.findViewById(R.id.lap_list_pace); + viewHolder.tvHr = (TextView) view.findViewById(R.id.lap_list_hr); + + view.setTag(viewHolder); + } else { + viewHolder = (ViewHolderLapList) view.getTag(); + } + int i = laps[position].getAsInteger(DB.LAP.INTENSITY); + Intensity intensity = Intensity.values()[i]; + switch (intensity) { + case ACTIVE: + viewHolder.tv0.setText(""); + break; + case COOLDOWN: + case RESTING: + case RECOVERY: + case WARMUP: + case REPEAT: + viewHolder.tv0.setText(String.format(Locale.getDefault(), "(%s)", + getResources().getString(intensity.getTextId()))); + default: + break; + + } + viewHolder.tv1.setText(laps[position].getAsString("_id")); + double d = laps[position].containsKey(DB.LAP.DISTANCE) ? laps[position] + .getAsDouble(DB.LAP.DISTANCE) : 0; + viewHolder.tv2.setText(formatter.formatDistance(Formatter.Format.TXT_LONG, (long) d)); + long t = laps[position].containsKey(DB.LAP.TIME) ? laps[position] + .getAsLong(DB.LAP.TIME) : 0; + viewHolder.tv3.setText(formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, t)); + if (t != 0) { + viewHolder.tv4.setText(formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d/t)); + } else { + viewHolder.tv4.setText(""); + } + int hr = laps[position].containsKey(DB.LAP.AVG_HR) ? laps[position] + .getAsInteger(DB.LAP.AVG_HR) : 0; + if (hr > 0) { + viewHolder.tvHr.setVisibility(View.VISIBLE); + // Use CUE_LONG instead of TXT_LONG to include unit + viewHolder.tvHr.setText(formatter.formatHeartRate(Formatter.Format.CUE_LONG, hr)); + } else if (lapHrPresent) { + viewHolder.tvHr.setVisibility(View.INVISIBLE); + } else { + viewHolder.tvHr.setVisibility(View.GONE); + } + + return view; + } + } + + private class ReportListAdapter extends BaseAdapter { + + @Override + public int getCount() { + return reports.size() + 1; + } + + @Override + public Object getItem(int position) { + if (position < reports.size()) + return reports.get(position); + return this; + } + + @Override + public long getItemId(int position) { + if (position < reports.size()) + return reports.get(position).getAsLong("_id"); + + return 0; + } + + private class ViewHolderDetailActivity { + private TextView tv0; + private CheckBox cb; + private TextView tv1; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (position == reports.size()) { + Button b = new Button(DetailActivity.this); + b.setText(getString(R.string.Configure_accounts)); + b.setBackgroundResource(R.drawable.btn_blue); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + b.setTextColor(getResources().getColorStateList(R.color.btn_text_color, getTheme())); + } else { + //noinspection deprecation + b.setTextColor(getResources().getColorStateList(R.color.btn_text_color)); + } + b.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent i = new Intent(DetailActivity.this, + AccountListActivity.class); + DetailActivity.this.startActivityForResult(i, + SyncManager.EDIT_ACCOUNT_REQUEST); + } + }); + return b; + } + + View view = convertView; + ViewHolderDetailActivity viewHolder; + + //Note: Special ViewHolder support as the Configure button is not in the view + if (view == null || view.getTag() == null) { + viewHolder = new ViewHolderDetailActivity(); + + LayoutInflater inflater = LayoutInflater.from(DetailActivity.this); + view = inflater.inflate(R.layout.reportlist_row, parent, false); + + viewHolder.tv0 = (TextView) view.findViewById(R.id.account_id); + viewHolder.cb = (CheckBox) view.findViewById(R.id.report_sent); + viewHolder.tv1 = (TextView) view.findViewById(R.id.account_name); + + view.setTag(viewHolder); + } else { + viewHolder = (ViewHolderDetailActivity) view.getTag(); + } + + ContentValues tmp = reports.get(position); + String name = tmp.getAsString(DB.ACCOUNT.NAME); + viewHolder.cb.setOnCheckedChangeListener(null); + viewHolder.cb.setChecked(false); + viewHolder.cb.setEnabled(false); + viewHolder.cb.setTag(name); + viewHolder.tv1.setTag(name); + viewHolder.tv1.setTextColor(viewHolder.cb.getTextColors()); + if (alreadySynched.contains(name)) { + viewHolder.cb.setChecked(true); + if (synchedExternalId.containsKey(name)) { + //Indicate Clickable label + viewHolder.tv1.setTextColor(Color.BLUE); + } + viewHolder.cb.setText(getString(R.string.Uploaded)); + viewHolder.cb.setOnLongClickListener(clearUploadClick); + } else { + if (pendingSynchronizers.contains(name)) { + viewHolder.cb.setChecked(true); + } else { + viewHolder.cb.setChecked(false); + } + viewHolder.cb.setText(getString(R.string.upload)); + viewHolder.cb.setOnLongClickListener(null); + } + if (mode == MODE_DETAILS) { + viewHolder.cb.setEnabled(true); + } else if (mode == MODE_SAVE) { + viewHolder.cb.setEnabled(true); + } + viewHolder.cb.setOnCheckedChangeListener(onSendChecked); + + viewHolder.tv0.setText(tmp.getAsString("_id")); + viewHolder.tv1.setText(name); + + return view; + } + } + + private void saveActivity() { + ContentValues tmp = new ContentValues(); + tmp.put(DB.ACTIVITY.COMMENT, notes.getText().toString()); + tmp.put(DB.ACTIVITY.SPORT, sport.getValueInt()); + String whereArgs[] = { + Long.toString(mID) + }; + mDB.update(DB.ACTIVITY.TABLE, tmp, "_id = ?", whereArgs); + } + + private final OnLongClickListener clearUploadClick = new OnLongClickListener() { + + @Override + public boolean onLongClick(View arg0) { + final String name = (String) arg0.getTag(); + AlertDialog.Builder builder = new AlertDialog.Builder(DetailActivity.this) + .setTitle("Clear upload for " + name) + .setMessage(getString(R.string.Are_you_sure)) + .setPositiveButton(getString(R.string.Yes), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + syncManager.clearUpload(name, mID); + requery(); + } + }) + .setNegativeButton(getString(R.string.No), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // Do nothing but close the dialog + dialog.dismiss(); + } + + }); + builder.show(); + return false; + } + + }; + + //Note: onClick set in reportlist_row.xml + public void onClickAccountName(View arg0) { + final String name = (String) arg0.getTag(); + if (synchedExternalId.containsKey(name) && !TextUtils.isEmpty(synchedExternalId.get(name))) { + String url = syncManager.getSynchronizerByName(name).getActivityUrl(synchedExternalId.get(name)); + if (url != null) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(browserIntent); + } + } + } + + private final OnClickListener saveButtonClick = new OnClickListener() { + public void onClick(View v) { + saveActivity(); + if (mode == MODE_DETAILS) { + setEdit(false); + requery(); + return; + } + uploading = true; + syncManager.startUploading(new SyncManager.Callback() { + @Override + public void run(String synchronizerName, Synchronizer.Status status) { + uploading = false; + DetailActivity.this.setResult(RESULT_OK); + DetailActivity.this.finish(); + } + }, pendingSynchronizers, mID); + } + }; + + private final OnClickListener discardButtonClick = new OnClickListener() { + public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(DetailActivity.this) + .setTitle(getString(R.string.Discard_activity)) + .setMessage(getString(R.string.Are_you_sure)) + .setPositiveButton(getString(R.string.Yes), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + DetailActivity.this.setResult(RESULT_CANCELED); + DetailActivity.this.finish(); + } + }) + .setNegativeButton(getString(R.string.No), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // Do nothing but close the dialog + dialog.dismiss(); + } + + }); + builder.show(); + } + }; + + @Override + public void onBackPressed() { + if (uploading) { + /* + * Ignore while uploading + */ + return; + } + if (mode == MODE_SAVE) { + resumeButtonClick.onClick(resumeButton); + } else { + super.onBackPressed(); + } + } + + private final OnClickListener resumeButtonClick = new OnClickListener() { + public void onClick(View v) { + DetailActivity.this.setResult(RESULT_FIRST_USER); + DetailActivity.this.finish(); + } + }; + + private final OnClickListener uploadButtonClick = new OnClickListener() { + public void onClick(View v) { + uploading = true; + syncManager.startUploading(new SyncManager.Callback() { + @Override + public void run(String synchronizerName, Synchronizer.Status status) { + uploading = false; + requery(); + } + }, pendingSynchronizers, mID); + } + }; + + private final OnCheckedChangeListener onSendChecked = new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton arg0, boolean arg1) { + final String name = (String) arg0.getTag(); + if (alreadySynched.contains(name)) { + // Only accept long clicks + arg0.setChecked(true); + } else { + if (arg1) { + pendingSynchronizers.add((String) arg0.getTag()); + } else { + pendingSynchronizers.remove(arg0.getTag()); + } + if (mode == MODE_DETAILS) { + setUploadVisibility(); + } + } + } + }; + + private final OnClickListener deleteButtonClick = new OnClickListener() { + public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(DetailActivity.this) + .setTitle(getString(R.string.Delete_activity)) + .setMessage(getString(R.string.Are_you_sure)) + .setPositiveButton(getString(R.string.Yes), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + DBHelper.deleteActivity(mDB, mID); + dialog.dismiss(); + DetailActivity.this.setResult(RESULT_OK); + DetailActivity.this.finish(); + } + }) + .setNegativeButton(getString(R.string.No), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // Do nothing but close the dialog + dialog.dismiss(); + } + + }); + builder.show(); + } + }; + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == SyncManager.CONFIGURE_REQUEST) { + syncManager.onActivityResult(requestCode, resultCode, data); + } + requery(); + } + + private void shareActivity() { + final int which[] = { + 1 //TODO preselect tcx - choice should be remembered + }; + final CharSequence items[] = { + "gpx", "tcx" /* "nike+xml" */ + }; + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setTitle(getString(R.string.Share_activity)) + .setPositiveButton(getString(R.string.OK), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int w) { + if (which[0] == -1) { + dialog.dismiss(); + return; + } + + final Context context = DetailActivity.this; + final CharSequence fmt = items[which[0]]; + final Intent intent = new Intent(Intent.ACTION_SEND); + + if (fmt.equals("tcx")) { + intent.setType(TCX_MIME); + } else { + intent.setType(GPX_MIME); + } + //Use of content:// (or STREAM?) instead of file:// is not supported in ES and other apps + //Solid Explorer File Manager works though + Uri uri = Uri.parse("content://" + ActivityProvider.AUTHORITY + "/" + fmt + + "/" + mID + + "/" + String.format(Locale.getDefault(), "RunnerUp_%04d.%s", mID, fmt)); + intent.putExtra(Intent.EXTRA_STREAM, uri); + context.startActivity(Intent.createChooser(intent, getString(R.string.Share_activity))); + } + }) + .setNegativeButton(getString(R.string.Cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // Do nothing but close the dialog + dialog.dismiss(); + } + + }) + .setSingleChoiceItems(items, which[0], new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int w) { + which[0] = w; + } + }); + builder.show(); + } +} From 0baae882c7cb893469e0d3cd9875b4415707b2f9 Mon Sep 17 00:00:00 2001 From: Denise Ratasich Date: Sat, 27 Jul 2019 17:31:44 +0200 Subject: [PATCH 2/3] Add hgoebl/simplify-java implementation Simplifies a polyline, e.g., the locations of an activity, by reducing its resolution given a tolerance. The algorithm only remove points (cf. averaging). Simplification can be done when saving a recorded activity, in the detail activity menu item and share to GPX and GPX file sync --- app/res/menu/detail_menu.xml | 5 + app/res/values/pref_keys.xml | 5 + app/res/xml/settings.xml | 39 ++++ .../com/goebl/simplify/AbstractSimplify.java | 156 +++++++++++++ app/src/main/com/goebl/simplify/Point.java | 41 ++++ .../com/goebl/simplify/PointExtractor.java | 41 ++++ app/src/main/com/goebl/simplify/Simplify.java | 121 ++++++++++ .../runnerup/content/ActivityProvider.java | 8 +- .../main/org/runnerup/db/ActivityCleaner.java | 22 ++ .../main/org/runnerup/db/PathSimplifier.java | 209 ++++++++++++++++++ .../org/runnerup/export/FileSynchronizer.java | 13 +- .../main/org/runnerup/export/SyncManager.java | 2 +- .../main/org/runnerup/export/format/GPX.java | 34 ++- .../main/org/runnerup/tracker/Tracker.java | 17 +- .../org/runnerup/view/DetailActivity.java | 22 +- common/src/main/res/values/array.xml | 9 + common/src/main/res/values/strings.xml | 10 + 17 files changed, 739 insertions(+), 15 deletions(-) create mode 100755 app/src/main/com/goebl/simplify/AbstractSimplify.java create mode 100755 app/src/main/com/goebl/simplify/Point.java create mode 100755 app/src/main/com/goebl/simplify/PointExtractor.java create mode 100755 app/src/main/com/goebl/simplify/Simplify.java create mode 100644 app/src/main/org/runnerup/db/PathSimplifier.java diff --git a/app/res/menu/detail_menu.xml b/app/res/menu/detail_menu.xml index 8436b5a39..946907e2c 100644 --- a/app/res/menu/detail_menu.xml +++ b/app/res/menu/detail_menu.xml @@ -32,6 +32,11 @@ android:orderInCategory="100" android:title="@string/Recompute_activity"/> + + pref_pace_graph_smoothing pref_pace_graph_smoothing_filters + pref_path_simplification_save + pref_path_simplification_export_gpx + pref_path_simplification_tolerance + pref_path_simplification_algorithm + pref_mapbox_default_style2 pref_runneruplive_active pref_runneruplive_serveradress diff --git a/app/res/xml/settings.xml b/app/res/xml/settings.xml index b8f73d17d..90239533d 100644 --- a/app/res/xml/settings.xml +++ b/app/res/xml/settings.xml @@ -161,6 +161,45 @@ android:title="@string/log_extended_gps_title" android:summary="@string/log_extended_gps_summary" /> + + + + + + + + + + + + + { + + private T[] sampleArray; + + protected AbstractSimplify(T[] sampleArray) { + this.sampleArray = sampleArray; + } + + /** + * Simplifies a list of points to a shorter list of points. + * @param points original list of points + * @param tolerance tolerance in the same measurement as the point coordinates + * @param highestQuality true for using Douglas-Peucker only, + * false for using Radial-Distance algorithm before + * applying Douglas-Peucker (should be a bit faster) + * @return simplified list of points + */ + public T[] simplify(T[] points, + double tolerance, + boolean highestQuality) { + + if (points == null || points.length <= 2) { + return points; + } + + double sqTolerance = tolerance * tolerance; + + if (!highestQuality) { + points = simplifyRadialDistance(points, sqTolerance); + } + + points = simplifyDouglasPeucker(points, sqTolerance); + + return points; + } + + T[] simplifyRadialDistance(T[] points, double sqTolerance) { + T point = null; + T prevPoint = points[0]; + + List newPoints = new ArrayList(); + newPoints.add(prevPoint); + + for (int i = 1; i < points.length; ++i) { + point = points[i]; + + if (getSquareDistance(point, prevPoint) > sqTolerance) { + newPoints.add(point); + prevPoint = point; + } + } + + if (prevPoint != point) { + newPoints.add(point); + } + + return newPoints.toArray(sampleArray); + } + + private static class Range { + private Range(int first, int last) { + this.first = first; + this.last = last; + } + + int first; + int last; + } + + T[] simplifyDouglasPeucker(T[] points, double sqTolerance) { + + BitSet bitSet = new BitSet(points.length); + bitSet.set(0); + bitSet.set(points.length - 1); + + List stack = new ArrayList(); + stack.add(new Range(0, points.length - 1)); + + while (!stack.isEmpty()) { + Range range = stack.remove(stack.size() - 1); + + int index = -1; + double maxSqDist = 0f; + + // find index of point with maximum square distance from first and last point + for (int i = range.first + 1; i < range.last; ++i) { + double sqDist = getSquareSegmentDistance(points[i], points[range.first], points[range.last]); + + if (sqDist > maxSqDist) { + index = i; + maxSqDist = sqDist; + } + } + + if (maxSqDist > sqTolerance) { + bitSet.set(index); + + stack.add(new Range(range.first, index)); + stack.add(new Range(index, range.last)); + } + } + + List newPoints = new ArrayList(bitSet.cardinality()); + for (int index = bitSet.nextSetBit(0); index >= 0; index = bitSet.nextSetBit(index + 1)) { + newPoints.add(points[index]); + } + + return newPoints.toArray(sampleArray); + } + + + public abstract double getSquareDistance(T p1, T p2); + + public abstract double getSquareSegmentDistance(T p0, T p1, T p2); +} diff --git a/app/src/main/com/goebl/simplify/Point.java b/app/src/main/com/goebl/simplify/Point.java new file mode 100755 index 000000000..3ac932290 --- /dev/null +++ b/app/src/main/com/goebl/simplify/Point.java @@ -0,0 +1,41 @@ +/* +The MIT License (MIT) + +Copyright (c) 2013 Heinrich Göbl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* + * Source retrieved from: https://github.com/hgoebl/simplify-java + * No changes made. + */ + +package com.goebl.simplify; + +/** + * Access to X and Y coordinates (2D-Point). + * + * @author hgoebl + * @since 06.07.13 + */ +public interface Point { + double getX(); + double getY(); +} diff --git a/app/src/main/com/goebl/simplify/PointExtractor.java b/app/src/main/com/goebl/simplify/PointExtractor.java new file mode 100755 index 000000000..073e3e314 --- /dev/null +++ b/app/src/main/com/goebl/simplify/PointExtractor.java @@ -0,0 +1,41 @@ +/* +The MIT License (MIT) + +Copyright (c) 2013 Heinrich Göbl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* + * Source retrieved from: https://github.com/hgoebl/simplify-java + * No changes made. + */ + +package com.goebl.simplify; + +/** + * Helper to get X and Y coordinates from a foreign class T. + * + * @author hgoebl + * @since 06.07.13 + */ +public interface PointExtractor { + double getX(T point); + double getY(T point); +} diff --git a/app/src/main/com/goebl/simplify/Simplify.java b/app/src/main/com/goebl/simplify/Simplify.java new file mode 100755 index 000000000..bb0fbb7d9 --- /dev/null +++ b/app/src/main/com/goebl/simplify/Simplify.java @@ -0,0 +1,121 @@ +/* +The MIT License (MIT) + +Copyright (c) 2013 Heinrich Göbl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* + * Source retrieved from: https://github.com/hgoebl/simplify-java + * No changes made. + */ + +package com.goebl.simplify; + +/** + * Simplification of a 2D-polyline. + * + * @author hgoebl + * @since 06.07.13 + */ +public class Simplify extends AbstractSimplify { + + private final PointExtractor pointExtractor; + + /** + * Simple constructor for 2D-Simplifier. + *
+ * With this simple constructor your array elements must implement {@link Point}.
+ * If you have coordinate classes which cannot be changed to implement Point, use + * {@link #Simplify(Object[], PointExtractor)} constructor! + * + * @param sampleArray pass just an empty array (new MyPoint[0]) - necessary for type consistency. + */ + public Simplify(T[] sampleArray) { + super(sampleArray); + this.pointExtractor = new PointExtractor() { + @Override + public double getX(T point) { + return ((Point) point).getX(); + } + + @Override + public double getY(T point) { + return ((Point) point).getY(); + } + }; + } + + /** + * Alternative constructor for 2D-Simplifier. + *
+ * With this constructor your array elements do not have to implement a special interface like {@link Point}.
+ * Implement a {@link PointExtractor} to give Simplify access to your coordinates. + * + * @param sampleArray pass just an empty array (new MyPoint[0]) - necessary for type consistency. + * @param pointExtractor your implementation to extract X and Y coordinates from you array elements. + */ + public Simplify(T[] sampleArray, PointExtractor pointExtractor) { + super(sampleArray); + this.pointExtractor = pointExtractor; + } + + @Override + public double getSquareDistance(T p1, T p2) { + + double dx = pointExtractor.getX(p1) - pointExtractor.getX(p2); + double dy = pointExtractor.getY(p1) - pointExtractor.getY(p2); + + return dx * dx + dy * dy; + } + + @Override + public double getSquareSegmentDistance(T p0, T p1, T p2) { + double x0, y0, x1, y1, x2, y2, dx, dy, t; + + x1 = pointExtractor.getX(p1); + y1 = pointExtractor.getY(p1); + x2 = pointExtractor.getX(p2); + y2 = pointExtractor.getY(p2); + x0 = pointExtractor.getX(p0); + y0 = pointExtractor.getY(p0); + + dx = x2 - x1; + dy = y2 - y1; + + if (dx != 0.0d || dy != 0.0d) { + t = ((x0 - x1) * dx + (y0 - y1) * dy) + / (dx * dx + dy * dy); + + if (t > 1.0d) { + x1 = x2; + y1 = y2; + } else if (t > 0.0d) { + x1 += dx * t; + y1 += dy * t; + } + } + + dx = x0 - x1; + dy = y0 - y1; + + return dx * dx + dy * dy; + } +} diff --git a/app/src/main/org/runnerup/content/ActivityProvider.java b/app/src/main/org/runnerup/content/ActivityProvider.java index d3720eec9..d3aaa9e47 100644 --- a/app/src/main/org/runnerup/content/ActivityProvider.java +++ b/app/src/main/org/runnerup/content/ActivityProvider.java @@ -33,6 +33,7 @@ import org.runnerup.BuildConfig; import org.runnerup.db.DBHelper; +import org.runnerup.db.PathSimplifier; import org.runnerup.export.format.FacebookCourse; import org.runnerup.export.format.GPX; import org.runnerup.export.format.GoogleStaticMap; @@ -158,8 +159,11 @@ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) } else if (res == GPX) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.getContext()); //The data must exist if log, use the log option as a possibility to "deactivate" too - boolean enabled = prefs.getBoolean(this.getContext().getString(org.runnerup.R.string.pref_log_gpx_accuracy), false); - GPX gpx = new GPX(mDB, true, enabled); + boolean extraData = prefs.getBoolean(this.getContext().getString(org.runnerup.R.string.pref_log_gpx_accuracy), false); + PathSimplifier simplifier = PathSimplifier.isEnabledForExportGpx(getContext()) ? + new PathSimplifier(getContext(), true) : + null; + GPX gpx = new GPX(mDB, true, extraData, simplifier); gpx.export(activityId, new OutputStreamWriter(out.second)); Log.e(getClass().getName(), "export gpx"); } else if (res == NIKE) { diff --git a/app/src/main/org/runnerup/db/ActivityCleaner.java b/app/src/main/org/runnerup/db/ActivityCleaner.java index dc0c12bd5..7af627065 100644 --- a/app/src/main/org/runnerup/db/ActivityCleaner.java +++ b/app/src/main/org/runnerup/db/ActivityCleaner.java @@ -18,11 +18,20 @@ package org.runnerup.db; import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.location.Location; +import android.preference.PreferenceManager; +import android.text.TextUtils; import android.util.Log; +import com.goebl.simplify.PointExtractor; +import com.goebl.simplify.Simplify; + +import org.runnerup.R; import org.runnerup.common.util.Constants; import java.util.ArrayList; @@ -273,4 +282,17 @@ private static int trimLap(SQLiteDatabase db, long activityId, long lap) { c.close(); return cnt; } + + /** + * Deletes locations with given IDs from the database. + * + * @param db Database. + * @param ids ID to delete. + */ + public static void deleteLocations(SQLiteDatabase db, ArrayList ids) { + String strIDs = TextUtils.join(",", ids); + db.execSQL("delete from " + DB.LOCATION.TABLE + + " where _id in (" + strIDs + ")" + + " and " + DB.LOCATION.TYPE + " = " + DB.LOCATION.TYPE_GPS); + } } diff --git a/app/src/main/org/runnerup/db/PathSimplifier.java b/app/src/main/org/runnerup/db/PathSimplifier.java new file mode 100644 index 000000000..ab0f4b59d --- /dev/null +++ b/app/src/main/org/runnerup/db/PathSimplifier.java @@ -0,0 +1,209 @@ +package org.runnerup.db; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.location.Location; +import android.preference.PreferenceManager; + +import com.goebl.simplify.Simplify; +import com.goebl.simplify.PointExtractor; + +import org.runnerup.R; +import org.runnerup.common.util.Constants; + +import java.util.ArrayList; + +/** + * Wrapper for com.goebl.simplify.Simplify. + */ +public class PathSimplifier { + /** Simplifies a polyline. */ + private Simplify simplifier; + + /** Indicates if simplification is enabled. */ + private boolean enabled; + /** Indicates when the simplification is performed (e.g., on export). */ + private boolean enabled_save; + private boolean enabled_export_gpx; + /** + * Tolerance in meters for path simplification. + * + * The higher the tolerance, the smoother the path. + * Note, if too big, the total distance might be reduced and won't match the reality. + */ + private double tolerance; + /** High quality (true) or fast (false) simplification. */ + private boolean high_quality; + + /** Multiplier to avoid deltas < 1 between points. */ + private static final double multiplier = 1e6; + /** Indicates which types of locations shall be used for path simplificaiton. */ + private boolean gps_type_only; + + /** + * Wrapper needed by simplifyPath to use Location as 2D point. + * Extracts (lat,long) from the Location class needed by com.goebl.simplify.simplify for (x,y). + * + * The functions return a multiple of the lat/long values to avoid deltas < 1 between points. + * + * https://github.com/hgoebl/simplify-java + */ + private static PointExtractor locationPointExtractor = new PointExtractor() { + @Override + public double getX(Location point) { + return point.getLatitude() * PathSimplifier.multiplier; + } + + @Override + public double getY(Location point) { + return point.getLongitude() * PathSimplifier.multiplier; + } + }; + + /** + * Initializes path simplification. + * + * @param context Context used to extract parameters for path simplification. + */ + public PathSimplifier(Context context) { + Resources res = context.getResources(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + // get user settings + this.tolerance = Double.parseDouble(prefs.getString( + res.getString(R.string.pref_path_simplification_tolerance), "3")); + String algorithm = prefs.getString( + res.getString(R.string.pref_path_simplification_algorithm), "ramer_douglas_peucker"); + + // convert algorithm to parameter for simplify method + this.high_quality = algorithm.matches("ramer_douglas_peucker"); + + // simplify path given all locations + this.gps_type_only = false; + } + + /** + * Initialization, given the type of location to simplify. + * @param context Context used to extract parameters for path simplification. + * @param gps_type_only If true, then other location types than GPS are ignored for simplification. + */ + public PathSimplifier(Context context, boolean gps_type_only) { + this(context); + this.gps_type_only = gps_type_only; + } + + /** Returns true, if simplification is enabled and can be performed at given 'when'. */ + public static boolean isEnabledForSave(Context context) { + Resources res = context.getResources(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + return prefs.getBoolean( + res.getString(R.string.pref_path_simplification_save), false); + } + + public static boolean isEnabledForExportGpx(Context context) { + Resources res = context.getResources(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + return prefs.getBoolean( + res.getString(R.string.pref_path_simplification_export_gpx), false); + } + + /** + * Returns the IDs (list of strings) of the location entries + * that would simplify the path of an activity, + * i.e., reduce the path's resolution. + * + * We use only 2D because we cannot mix degrees (lat,long) with meters (altitude), + * regarding the tolerance of simplify. + * Conversion of lat and long to meters is not necessary to simplify the path. + * + * https://github.com/hgoebl/simplify-java + * + * @param db Database. + * @param activityId ID of the activity to simplify. + */ + public ArrayList getNoisyLocationIDsAsStrings(SQLiteDatabase db, long activityId) { + // columns to query from the database, table "LOCATION" + String[] pColumns = { + "_id", Constants.DB.LOCATION.LATITUDE, Constants.DB.LOCATION.LONGITUDE + }; + + // optional constraint to extract GPS type locations only + String constraint = ""; + if (gps_type_only) + constraint = " and " + Constants.DB.LOCATION.TYPE + " = " + Constants.DB.LOCATION.TYPE_GPS; + + // get table from database + Cursor c = db.query(Constants.DB.LOCATION.TABLE, pColumns, + Constants.DB.LOCATION.ACTIVITY + " = " + activityId + constraint, + null, null, null, Constants.DB.LOCATION.TIME); + + // data base rows to lists + ArrayList locations = new ArrayList<>(); /** List of the activity's locations (lat,long). */ + if (c.moveToFirst()) { + do { + // save ID of the location entry (row ID) as provider + Location l = new Location(String.format("%d", c.getInt(0))); + // get point data + l.setLatitude(c.getDouble(1)); + l.setLongitude(c.getDouble(2)); + locations.add(l); + } while (c.moveToNext()); + } + c.close(); + + // squared tolerance in meters has to be transformed to tolerance in degrees + // get meters per 1° with android.location.Location.distanceTo + Location zeroDegrees = new Location("null island"); // 0°N 0°E + zeroDegrees.setLatitude(0); + zeroDegrees.setLongitude(0); + Location oneDegrees = new Location(zeroDegrees); // 1°N 0°E + oneDegrees.setLatitude(1); + // tolerance in meters / meters per degree + double toleranceDeg = this.tolerance / zeroDegrees.distanceTo(oneDegrees); + + // create an instance of the simplifier (empty array needed by List.toArray) + Location[] sampleArray = new Location[0]; + Simplify simplify = new Simplify(sampleArray, locationPointExtractor); + // removes unnecessary intermediate points (note this does not change lat/long values!) + Location[] simplifiedLocations = simplify.simplify(locations.toArray(sampleArray), + toleranceDeg*this.multiplier, this.high_quality); + + // remove the locations (skipped by simplify) from the database + ArrayList ids = new ArrayList<>(); // IDs of all the activity's locations + ArrayList simplifiedIDs = new ArrayList<>(); // IDs to remove from location table + for (Location l: locations) { + ids.add(l.getProvider()); + } + for (Location l: simplifiedLocations) { + simplifiedIDs.add(l.getProvider()); + } + ids.removeAll(simplifiedIDs); // IDs to delete or ignore + + return ids; + } + + /** + * Returns the IDs (list of integers) of the location entries + * that would simplify the path of an activity, + * i.e., reduce the path's resolution. + * + * @param db Database. + * @param activityId ID of the activity to simplify. + * @param when The current point in the application (e.g., "save" or "export"). + */ + public ArrayList getNoisyLocationIDs(SQLiteDatabase db, long activityId) { + ArrayList strIDs = getNoisyLocationIDsAsStrings(db, activityId); + + // convert (back) to integers + ArrayList ids = new ArrayList<>(); + for (String str: strIDs) + ids.add(Integer.parseInt(str)); + + return ids; + } +} diff --git a/app/src/main/org/runnerup/export/FileSynchronizer.java b/app/src/main/org/runnerup/export/FileSynchronizer.java index 32ff28de6..32c6b53ee 100644 --- a/app/src/main/org/runnerup/export/FileSynchronizer.java +++ b/app/src/main/org/runnerup/export/FileSynchronizer.java @@ -18,6 +18,7 @@ package org.runnerup.export; import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; @@ -29,6 +30,7 @@ import org.runnerup.R; import org.runnerup.common.util.Constants; import org.runnerup.common.util.Constants.DB; +import org.runnerup.db.PathSimplifier; import org.runnerup.export.format.GPX; import org.runnerup.export.format.TCX; import org.runnerup.workout.Sport; @@ -49,8 +51,15 @@ public class FileSynchronizer extends DefaultSynchronizer { private long id = 0; private String mPath; private String mFormat; + private PathSimplifier simplifier = null; - FileSynchronizer() { + FileSynchronizer() {} + + FileSynchronizer(Context context) { + this(); + this.simplifier = PathSimplifier.isEnabledForExportGpx(context) ? + new PathSimplifier(context, true) : + null; } @Override @@ -177,7 +186,7 @@ public Status upload(SQLiteDatabase db, final long mID) { s.externalIdStatus = ExternalIdStatus.NONE; //Not working yet } if (mFormat.contains("gpx")) { - GPX gpx = new GPX(db, true, true); + GPX gpx = new GPX(db, true, true, simplifier); File file = new File(fileBase + "gpx"); OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); gpx.export(mID, new OutputStreamWriter(out)); diff --git a/app/src/main/org/runnerup/export/SyncManager.java b/app/src/main/org/runnerup/export/SyncManager.java index 5ae9bde78..5bad6ec02 100644 --- a/app/src/main/org/runnerup/export/SyncManager.java +++ b/app/src/main/org/runnerup/export/SyncManager.java @@ -212,7 +212,7 @@ public Synchronizer add(ContentValues config) { } else if (synchronizerName.contentEquals(RunningFreeOnlineSynchronizer.NAME)) { synchronizer = new RunningFreeOnlineSynchronizer(); } else if (synchronizerName.contentEquals(FileSynchronizer.NAME)) { - synchronizer = new FileSynchronizer(); + synchronizer = new FileSynchronizer(mContext); } else if (synchronizerName.contentEquals(RunalyzeSynchronizer.NAME)) { synchronizer = new RunalyzeSynchronizer(); } else if (synchronizerName.contentEquals(DropboxSynchronizer.NAME)) { diff --git a/app/src/main/org/runnerup/export/format/GPX.java b/app/src/main/org/runnerup/export/format/GPX.java index 5f7ac6b8a..fb8aa5b0e 100644 --- a/app/src/main/org/runnerup/export/format/GPX.java +++ b/app/src/main/org/runnerup/export/format/GPX.java @@ -19,14 +19,17 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.util.Log; import org.runnerup.common.util.Constants.DB; +import org.runnerup.db.PathSimplifier; import org.runnerup.util.KXmlSerializer; import org.runnerup.workout.Sport; import java.io.IOException; import java.io.Writer; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -34,22 +37,29 @@ public class GPX { - private SQLiteDatabase mDB = null; - private KXmlSerializer mXML = null; - private SimpleDateFormat simpleDateFormat = null; + private SQLiteDatabase mDB; + private KXmlSerializer mXML; + private SimpleDateFormat simpleDateFormat; final private boolean mGarminExt; //Also Cluetrust private final boolean mAccuracyExtensions; + private PathSimplifier simplifier; public GPX(SQLiteDatabase mDB) { - this(mDB, true, false); + this(mDB, true, false, null); } - public GPX(SQLiteDatabase mDB, boolean garminExt, boolean accuracyExtensions) { + public GPX(SQLiteDatabase mDB, boolean garminExt, boolean accuracyExtensions, PathSimplifier simplifier) { this.mDB = mDB; + mXML = new KXmlSerializer(); simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); this.mGarminExt = garminExt; this.mAccuracyExtensions = accuracyExtensions; + this.simplifier = simplifier; + } + + public GPX(SQLiteDatabase mDB, boolean garminExt, boolean accuracyExtensions) { + this(mDB, garminExt, accuracyExtensions, null); } private String formatTime(long time) { @@ -73,7 +83,6 @@ public void export(long activityId, Writer writer) throws IOException { long startTime = cursor.getLong(2); // epoch try { - mXML = new KXmlSerializer(); mXML.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); mXML.setOutput(writer); mXML.startDocument("UTF-8", true); @@ -154,7 +163,8 @@ private void exportLaps(long activityId, long startTime) throws IOException { DB.LOCATION.ALTITUDE, DB.LOCATION.TYPE, DB.LOCATION.HR, DB.LOCATION.CADENCE, DB.LOCATION.TEMPERATURE, DB.LOCATION.PRESSURE, DB.LOCATION.ACCURANCY, DB.LOCATION.BEARING, DB.LOCATION.SPEED, - DB.LOCATION.SATELLITES, DB.LOCATION.GPS_ALTITUDE + DB.LOCATION.SATELLITES, DB.LOCATION.GPS_ALTITUDE, + DB.PRIMARY_KEY }; Cursor cLocation = mDB.query(DB.LOCATION.TABLE, pColumns, DB.LOCATION.ACTIVITY + " = " + activityId, null, null, null, @@ -162,6 +172,11 @@ private void exportLaps(long activityId, long startTime) throws IOException { boolean lok = cLap.moveToFirst(); boolean pok = cLocation.moveToFirst(); + // simplify path, if this option is selected by the user + ArrayList ignoreIDs = simplifier != null ? + simplifier.getNoisyLocationIDs(mDB, activityId) : + new ArrayList<>(); + while (lok) { if (cLap.getFloat(1) != 0 && cLap.getLong(2) != 0) { long lap = cLap.getLong(0); @@ -186,7 +201,10 @@ private void exportLaps(long activityId, long startTime) throws IOException { } mXML.comment(" State change: " + locType + " " + formatTime(time)); } - } else if (time > last_time) { + } else if (time > last_time && + // ignore IDs that have been marked unnecessary by path simplification + // reduces resolution of the exported path + ! ignoreIDs.contains(cLocation.getInt(15))) { hasPoints = true; mXML.startTag("", "trkpt"); diff --git a/app/src/main/org/runnerup/tracker/Tracker.java b/app/src/main/org/runnerup/tracker/Tracker.java index fb4c67f40..498c4b720 100644 --- a/app/src/main/org/runnerup/tracker/Tracker.java +++ b/app/src/main/org/runnerup/tracker/Tracker.java @@ -40,7 +40,9 @@ import org.runnerup.common.tracker.TrackerState; import org.runnerup.common.util.Constants; import org.runnerup.common.util.ValueModel; +import org.runnerup.db.ActivityCleaner; import org.runnerup.db.DBHelper; +import org.runnerup.db.PathSimplifier; import org.runnerup.export.SyncManager; import org.runnerup.hr.HRProvider; import org.runnerup.notification.ForegroundNotificationDisplayStrategy; @@ -562,6 +564,18 @@ public void completeActivity(boolean save) { if (save) { saveActivity(); liveLog(DB.LOCATION.TYPE_END); + + // path simplification (reduce resolution of location entries in database) + try { + if (PathSimplifier.isEnabledForSave(this)) { + PathSimplifier simplifier = new PathSimplifier(this); + ArrayList ids = simplifier.getNoisyLocationIDsAsStrings(mDB, mActivityId); + ActivityCleaner.deleteLocations(mDB, ids); + (new ActivityCleaner()).recompute(mDB, mActivityId); + } + } catch (Exception e) { + Log.e(getClass().getName(), "Failed to simplify path: " + e.getMessage()); + } } else { ContentValues tmp = new ContentValues(); tmp.put("deleted", 1); @@ -571,6 +585,7 @@ public void completeActivity(boolean save) { mDB.update(DB.ACTIVITY.TABLE, tmp, "_id = ?", key); liveLog(DB.LOCATION.TYPE_DISCARD); } + components.onComplete(!save); notificationStateManager.cancelNotification(); reset(); @@ -970,4 +985,4 @@ public Double getCurrentSpeed() { public Workout getWorkout() { return workout; } -} \ No newline at end of file +} diff --git a/app/src/main/org/runnerup/view/DetailActivity.java b/app/src/main/org/runnerup/view/DetailActivity.java index b4e62a21e..4c7ccd517 100644 --- a/app/src/main/org/runnerup/view/DetailActivity.java +++ b/app/src/main/org/runnerup/view/DetailActivity.java @@ -59,6 +59,7 @@ import org.runnerup.content.ActivityProvider; import org.runnerup.db.ActivityCleaner; import org.runnerup.db.DBHelper; +import org.runnerup.db.PathSimplifier; import org.runnerup.export.SyncManager; import org.runnerup.export.Synchronizer; import org.runnerup.export.Synchronizer.Feature; @@ -259,7 +260,6 @@ public boolean onCreateOptionsMenu(Menu menu) { return true; } - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -295,6 +295,26 @@ public boolean onOptionsItemSelected(MenuItem item) { builderRecompute.show(); break; + case R.id.menu_simplify_path: + final AlertDialog.Builder builderSimplify = new AlertDialog.Builder(this) + .setTitle(R.string.path_simplification_menu) + .setMessage(getString(R.string.Are_you_sure)) + .setPositiveButton(getString(R.string.Yes), (dialog, which) -> { + dialog.dismiss(); + PathSimplifier simplifier = new PathSimplifier(this); + ArrayList ids = simplifier.getNoisyLocationIDsAsStrings(mDB, mID); + ActivityCleaner.deleteLocations(mDB, ids); + new ActivityCleaner().recompute(mDB, mID); + requery(); + fillHeaderData(); + finish(); + }) + .setNegativeButton(getString(R.string.No),(dialog, which) -> { + dialog.dismiss(); + }); + builderSimplify.show(); + break; + case R.id.menu_share_activity: shareActivity(); break; diff --git a/common/src/main/res/values/array.xml b/common/src/main/res/values/array.xml index e2c4a6702..e8f364f2c 100644 --- a/common/src/main/res/values/array.xml +++ b/common/src/main/res/values/array.xml @@ -66,4 +66,13 @@ Male Female + + + Radial-Distance (fast) + Ramer-Douglas-Peucker (high quality) + + + radial_distance + ramer_douglas_peucker + diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index e6b35cccc..c0530ab34 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -341,4 +341,14 @@ Lock activity buttons by fast clicking the header Speed Speed unit + Path simplification + Reduce the resolution of the GPS path (logged locations over time). Smooths the path and decreases the size of an activity in the database or an exported file. + Simplify at activity save + Simplify GPS path in the database when saving + Simplify at GPX export + Simplify GPS path when exporting to GPX + Tolerance in meters + Algorithm + Choose fast or high quality path simplification + Simplify path From 2c4daeede32f2deab157faead059b32bf563df61 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Fri, 10 Jan 2020 22:53:12 +0100 Subject: [PATCH 3/3] GPX export: Do not use trkseg when simplifying --- .../main/org/runnerup/export/format/GPX.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/org/runnerup/export/format/GPX.java b/app/src/main/org/runnerup/export/format/GPX.java index fb8aa5b0e..1129dded6 100644 --- a/app/src/main/org/runnerup/export/format/GPX.java +++ b/app/src/main/org/runnerup/export/format/GPX.java @@ -19,7 +19,6 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.util.Log; import org.runnerup.common.util.Constants.DB; import org.runnerup.db.PathSimplifier; @@ -176,7 +175,12 @@ private void exportLaps(long activityId, long startTime) throws IOException { ArrayList ignoreIDs = simplifier != null ? simplifier.getNoisyLocationIDs(mDB, activityId) : new ArrayList<>(); + // Tracks with time gaps may show as unconnected, mostly the same as this setting + final boolean useTrkSeg = (simplifier == null); + if (!useTrkSeg) { + mXML.startTag("", "trkseg"); + } while (lok) { if (cLap.getFloat(1) != 0 && cLap.getLong(2) != 0) { long lap = cLap.getLong(0); @@ -184,7 +188,9 @@ private void exportLaps(long activityId, long startTime) throws IOException { pok = cLocation.moveToNext(); } boolean hasPoints = false; - mXML.startTag("", "trkseg"); + if (useTrkSeg) { + mXML.startTag("", "trkseg"); + } if (pok && cLocation.getLong(0) == lap) { long last_time = 0; while (pok && cLocation.getLong(0) == lap) { @@ -337,11 +343,16 @@ else if (!cLocation.isNull(4)) { pok = cLocation.moveToNext(); } } - mXML.endTag("", "trkseg"); + if (useTrkSeg) { + mXML.endTag("", "trkseg"); + } } lok = cLap.moveToNext(); } + if (!useTrkSeg) { + mXML.endTag("", "trkseg"); + } cLap.close(); cLocation.close(); }