forked from upstream-mirrors/OpenTracks
468 lines
16 KiB
Java
468 lines
16 KiB
Java
/*
|
|
* Copyright 2008 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
* use this file except in compliance with the License. You may obtain a copy of
|
|
* the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations under
|
|
* the License.
|
|
*/
|
|
package com.google.android.apps.mytracks.util;
|
|
|
|
import com.google.android.apps.mytracks.Constants;
|
|
import com.google.android.apps.mytracks.content.DescriptionGenerator;
|
|
import com.google.android.apps.mytracks.content.Track;
|
|
import com.google.android.apps.mytracks.content.Waypoint;
|
|
import com.google.android.apps.mytracks.stats.TripStatistics;
|
|
import com.google.android.maps.mytracks.R;
|
|
|
|
import android.content.Context;
|
|
import android.content.SharedPreferences;
|
|
|
|
import java.text.DecimalFormat;
|
|
import java.text.ParsePosition;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.Date;
|
|
import java.util.SimpleTimeZone;
|
|
import java.util.Vector;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* Various string manipulation methods.
|
|
*
|
|
* @author Sandor Dornbush
|
|
* @author Rodrigo Damazio
|
|
*/
|
|
public class StringUtils implements DescriptionGenerator {
|
|
|
|
private final Context context;
|
|
|
|
/**
|
|
* Formats a number of milliseconds as a string.
|
|
*
|
|
* @param time - A period of time in milliseconds.
|
|
* @return A string of the format M:SS, MM:SS or HH:MM:SS
|
|
*/
|
|
public static String formatTime(long time) {
|
|
return formatTimeInternal(time, false);
|
|
}
|
|
|
|
/**
|
|
* Formats a number of milliseconds as a string. To be used when we need the
|
|
* hours to be shown even when it is zero, e.g. exporting data to a
|
|
* spreadsheet.
|
|
*
|
|
* @param time - A period of time in milliseconds
|
|
* @return A string of the format HH:MM:SS even if time is less than 1 hour
|
|
*/
|
|
public static String formatTimeAlwaysShowingHours(long time) {
|
|
return formatTimeInternal(time, true);
|
|
}
|
|
|
|
private static final DecimalFormat SINGLE_DECIMAL_PLACE_FORMAT = new DecimalFormat("#.#");
|
|
|
|
/**
|
|
* Formats a double precision number as decimal number with a single decimal
|
|
* place.
|
|
*
|
|
* @param number A double precision number
|
|
* @return A string representation of a decimal number, derived from the input
|
|
* double, with a single decimal place
|
|
*/
|
|
public static final String formatSingleDecimalPlace(double number) {
|
|
return SINGLE_DECIMAL_PLACE_FORMAT.format(number);
|
|
}
|
|
|
|
/**
|
|
* Formats the given text as a CDATA element to be used in a XML file. This
|
|
* includes adding the starting and ending CDATA tags. Please notice that this
|
|
* may result in multiple consecutive CDATA tags.
|
|
*
|
|
* @param unescaped the unescaped text to be formatted
|
|
* @return the formatted text, inside one or more CDATA tags
|
|
*/
|
|
public static String stringAsCData(String unescaped) {
|
|
// "]]>" needs to be broken into multiple CDATA segments, like:
|
|
// "Foo]]>Bar" becomes "<![CDATA[Foo]]]]><![CDATA[>Bar]]>"
|
|
// (the end of the first CDATA has the "]]", the other has ">")
|
|
String escaped = unescaped.replaceAll("]]>", "]]]]><![CDATA[>");
|
|
return "<![CDATA[" + escaped + "]]>";
|
|
}
|
|
|
|
private static final SimpleDateFormat BASE_XML_DATE_FORMAT =
|
|
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
|
static {
|
|
BASE_XML_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "UTC"));
|
|
}
|
|
private static final Pattern XML_DATE_EXTRAS_PATTERN =
|
|
Pattern.compile("^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
|
|
|
|
/**
|
|
* Parses an XML dateTime element as defined by the XML standard.
|
|
*
|
|
* @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">dateTime</a>
|
|
*/
|
|
public static long parseXmlDateTime(String xmlTime) {
|
|
// Parse the base date (fixed format)
|
|
ParsePosition position = new ParsePosition(0);
|
|
Date date = BASE_XML_DATE_FORMAT.parse(xmlTime, position);
|
|
if (date == null) {
|
|
throw new IllegalArgumentException("Invalid XML dateTime value: '" + xmlTime
|
|
+ "' (at position " + position.getErrorIndex() + ")");
|
|
}
|
|
|
|
// Parse the extras
|
|
Matcher matcher =
|
|
XML_DATE_EXTRAS_PATTERN.matcher(xmlTime.substring(position.getIndex()));
|
|
if (!matcher.matches()) {
|
|
// This will match even an empty string as all groups are optional,
|
|
// so a non-match means some other garbage was there
|
|
throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlTime);
|
|
}
|
|
|
|
long time = date.getTime();
|
|
|
|
// Account for fractional seconds
|
|
String fractional = matcher.group(1);
|
|
if (fractional != null) {
|
|
// Regex ensures fractional part is in (0,1(
|
|
float fractionalSeconds = Float.parseFloat(fractional);
|
|
long fractionalMillis = (long) (fractionalSeconds * 1000.0f);
|
|
time += fractionalMillis;
|
|
}
|
|
|
|
// Account for timezones
|
|
String sign = matcher.group(2);
|
|
String offsetHoursStr = matcher.group(3);
|
|
String offsetMinsStr = matcher.group(4);
|
|
if (sign != null && offsetHoursStr != null && offsetMinsStr != null) {
|
|
// Regex ensures sign is + or -
|
|
boolean plusSign = sign.equals("+");
|
|
int offsetHours = Integer.parseInt(offsetHoursStr);
|
|
int offsetMins = Integer.parseInt(offsetMinsStr);
|
|
|
|
// Regex ensures values are >= 0
|
|
if (offsetHours > 14 || offsetMins > 59) {
|
|
throw new IllegalArgumentException("Bad timezone in " + xmlTime);
|
|
}
|
|
|
|
long totalOffsetMillis = (offsetMins + offsetHours * 60L) * 60000L;
|
|
|
|
// Make time go back to UTC
|
|
if (plusSign) {
|
|
time -= totalOffsetMillis;
|
|
} else {
|
|
time += totalOffsetMillis;
|
|
}
|
|
}
|
|
|
|
return time;
|
|
}
|
|
|
|
/**
|
|
* Formats a number of milliseconds as a string.
|
|
*
|
|
* @param time - A period of time in milliseconds
|
|
* @param alwaysShowHours - Whether to display 00 hours if time is less than 1
|
|
* hour
|
|
* @return A string of the format HH:MM:SS
|
|
*/
|
|
private static String formatTimeInternal(long time, boolean alwaysShowHours) {
|
|
int[] parts = getTimeParts(time);
|
|
StringBuilder builder = new StringBuilder();
|
|
if (parts[2] > 0 || alwaysShowHours) {
|
|
builder.append(parts[2]);
|
|
builder.append(':');
|
|
if (parts[1] <= 9) {
|
|
builder.append("0");
|
|
}
|
|
}
|
|
|
|
builder.append(parts[1]);
|
|
builder.append(':');
|
|
if (parts[0] <= 9) {
|
|
builder.append("0");
|
|
}
|
|
builder.append(parts[0]);
|
|
|
|
return builder.toString();
|
|
}
|
|
|
|
/**
|
|
* Gets the time as an array of parts.
|
|
*/
|
|
public static int[] getTimeParts(long time) {
|
|
if (time < 0) {
|
|
int[] parts = getTimeParts(time * -1);
|
|
parts[0] *= -1;
|
|
parts[1] *= -1;
|
|
parts[2] *= -1;
|
|
return parts;
|
|
}
|
|
int[] parts = new int[3];
|
|
|
|
long seconds = time / 1000;
|
|
parts[0] = (int) (seconds % 60);
|
|
int tmp = (int) (seconds / 60);
|
|
parts[1] = tmp % 60;
|
|
parts[2] = tmp / 60;
|
|
|
|
return parts;
|
|
}
|
|
|
|
public StringUtils(Context context) {
|
|
this.context = context;
|
|
}
|
|
|
|
/**
|
|
* Generates a description for a track (with information about the
|
|
* statistics).
|
|
*
|
|
* @param track the track
|
|
* @return a track description
|
|
*/
|
|
public String generateTrackDescription(Track track, Vector<Double> distances,
|
|
Vector<Double> elevations) {
|
|
boolean displaySpeed = true;
|
|
SharedPreferences preferences =
|
|
context.getSharedPreferences(Constants.SETTINGS_NAME, 0);
|
|
if (preferences != null) {
|
|
displaySpeed =
|
|
preferences.getBoolean(context.getString(R.string.report_speed_key), true);
|
|
}
|
|
|
|
TripStatistics trackStats = track.getStatistics();
|
|
final double distanceInKm = trackStats.getTotalDistance() / 1000;
|
|
final double distanceInMiles = distanceInKm * UnitConversions.KM_TO_MI;
|
|
final long minElevationInMeters = Math.round(trackStats.getMinElevation());
|
|
final long minElevationInFeet =
|
|
Math.round(trackStats.getMinElevation() * UnitConversions.M_TO_FT);
|
|
final long maxElevationInMeters = Math.round(trackStats.getMaxElevation());
|
|
final long maxElevationInFeet =
|
|
Math.round(trackStats.getMaxElevation() * UnitConversions.M_TO_FT);
|
|
final long elevationGainInMeters =
|
|
Math.round(trackStats.getTotalElevationGain());
|
|
final long elevationGainInFeet = Math.round(
|
|
trackStats.getTotalElevationGain() * UnitConversions.M_TO_FT);
|
|
|
|
long minGrade = 0;
|
|
long maxGrade = 0;
|
|
double trackMaxGrade = trackStats.getMaxGrade();
|
|
double trackMinGrade = trackStats.getMinGrade();
|
|
if (!Double.isNaN(trackMaxGrade)
|
|
&& !Double.isInfinite(trackMaxGrade)) {
|
|
maxGrade = Math.round(trackMaxGrade * 100);
|
|
}
|
|
if (!Double.isNaN(trackMinGrade) && !Double.isInfinite(trackMinGrade)) {
|
|
minGrade = Math.round(trackMinGrade * 100);
|
|
}
|
|
|
|
String category = context.getString(R.string.unknown);
|
|
String trackCategory = track.getCategory();
|
|
if (trackCategory != null && trackCategory.length() > 0) {
|
|
category = trackCategory;
|
|
}
|
|
|
|
String averageSpeed =
|
|
getSpeedString(trackStats.getAverageSpeed(),
|
|
R.string.average_speed_label,
|
|
R.string.average_pace_label,
|
|
displaySpeed);
|
|
|
|
String averageMovingSpeed =
|
|
getSpeedString(trackStats.getAverageMovingSpeed(),
|
|
R.string.average_moving_speed_label,
|
|
R.string.average_moving_pace_label,
|
|
displaySpeed);
|
|
|
|
String maxSpeed =
|
|
getSpeedString(trackStats.getMaxSpeed(),
|
|
R.string.max_speed_label,
|
|
R.string.min_pace_label,
|
|
displaySpeed);
|
|
|
|
return String.format("%s<p>"
|
|
+ "%s: %.2f %s (%.1f %s)<br>"
|
|
+ "%s: %s<br>"
|
|
+ "%s: %s<br>"
|
|
+ "%s %s %s"
|
|
+ "%s: %d %s (%d %s)<br>"
|
|
+ "%s: %d %s (%d %s)<br>"
|
|
+ "%s: %d %s (%d %s)<br>"
|
|
+ "%s: %d %%<br>"
|
|
+ "%s: %d %%<br>"
|
|
+ "%s: %tc<br>"
|
|
+ "%s: %s<br>"
|
|
+ "<img border=\"0\" src=\"%s\"/>",
|
|
|
|
// Line 1
|
|
context.getString(R.string.send_to_google_by_mytracks),
|
|
|
|
// Line 2
|
|
context.getString(R.string.total_distance_label),
|
|
distanceInKm, context.getString(R.string.kilometer),
|
|
distanceInMiles, context.getString(R.string.mile),
|
|
|
|
// Line 3
|
|
context.getString(R.string.total_time_label),
|
|
StringUtils.formatTime(trackStats.getTotalTime()),
|
|
|
|
// Line 4
|
|
context.getString(R.string.moving_time_label),
|
|
StringUtils.formatTime(trackStats.getMovingTime()),
|
|
|
|
// Line 5
|
|
averageSpeed, averageMovingSpeed, maxSpeed,
|
|
|
|
// Line 6
|
|
context.getString(R.string.min_elevation_label),
|
|
minElevationInMeters, context.getString(R.string.meter),
|
|
minElevationInFeet, context.getString(R.string.feet),
|
|
|
|
// Line 7
|
|
context.getString(R.string.max_elevation_label),
|
|
maxElevationInMeters, context.getString(R.string.meter),
|
|
maxElevationInFeet, context.getString(R.string.feet),
|
|
|
|
// Line 8
|
|
context.getString(R.string.elevation_gain_label),
|
|
elevationGainInMeters, context.getString(R.string.meter),
|
|
elevationGainInFeet, context.getString(R.string.feet),
|
|
|
|
// Line 9
|
|
context.getString(R.string.max_grade_label), maxGrade,
|
|
|
|
// Line 10
|
|
context.getString(R.string.min_grade_label), minGrade,
|
|
|
|
// Line 11
|
|
context.getString(R.string.recorded_date),
|
|
new Date(trackStats.getStartTime()),
|
|
|
|
// Line 12
|
|
context.getString(R.string.category), category,
|
|
|
|
// Line 13
|
|
ChartURLGenerator.getChartUrl(distances, elevations, track, context));
|
|
}
|
|
|
|
private String getSpeedString(double speed, int speedLabel, int paceLabel,
|
|
boolean displaySpeed) {
|
|
double speedInKph = speed * 3.6;
|
|
double speedInMph = speedInKph * UnitConversions.KMH_TO_MPH;
|
|
if (displaySpeed) {
|
|
return String.format("%s: %.2f %s (%.1f %s)<br>",
|
|
context.getString(speedLabel),
|
|
speedInKph, context.getString(R.string.kilometer_per_hour),
|
|
speedInMph, context.getString(R.string.mile_per_hour));
|
|
} else {
|
|
double paceInKm;
|
|
double paceInMi;
|
|
if (speed == 0) {
|
|
paceInKm = 0.0;
|
|
paceInMi = 0.0;
|
|
} else {
|
|
paceInKm = 60.0 / speedInKph;
|
|
paceInMi = 60.0 / speedInMph;
|
|
}
|
|
return String.format("%s: %.2f %s (%.1f %s)<br>",
|
|
context.getString(paceLabel),
|
|
paceInKm, context.getString(R.string.min_per_kilometer),
|
|
paceInMi, context.getString(R.string.min_per_mile));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a description for a waypoint (with information about the
|
|
* statistics).
|
|
*
|
|
* @return a track description
|
|
*/
|
|
public String generateWaypointDescription(Waypoint waypoint) {
|
|
TripStatistics stats = waypoint.getStatistics();
|
|
|
|
final double distanceInKm = stats.getTotalDistance() / 1000;
|
|
final double distanceInMiles = distanceInKm * UnitConversions.KM_TO_MI;
|
|
final double averageSpeedInKmh = stats.getAverageSpeed() * 3.6;
|
|
final double averageSpeedInMph =
|
|
averageSpeedInKmh * UnitConversions.KMH_TO_MPH;
|
|
final double movingSpeedInKmh = stats.getAverageMovingSpeed() * 3.6;
|
|
final double movingSpeedInMph =
|
|
movingSpeedInKmh * UnitConversions.KMH_TO_MPH;
|
|
final double maxSpeedInKmh = stats.getMaxSpeed() * 3.6;
|
|
final double maxSpeedInMph = maxSpeedInKmh * UnitConversions.KMH_TO_MPH;
|
|
final long minElevationInMeters = Math.round(stats.getMinElevation());
|
|
final long minElevationInFeet =
|
|
Math.round(stats.getMinElevation() * UnitConversions.M_TO_FT);
|
|
final long maxElevationInMeters = Math.round(stats.getMaxElevation());
|
|
final long maxElevationInFeet =
|
|
Math.round(stats.getMaxElevation() * UnitConversions.M_TO_FT);
|
|
final long elevationGainInMeters =
|
|
Math.round(stats.getTotalElevationGain());
|
|
final long elevationGainInFeet = Math.round(
|
|
stats.getTotalElevationGain() * UnitConversions.M_TO_FT);
|
|
long theMinGrade = 0;
|
|
long theMaxGrade = 0;
|
|
double maxGrade = stats.getMaxGrade();
|
|
double minGrade = stats.getMinGrade();
|
|
if (!Double.isNaN(maxGrade) &&
|
|
!Double.isInfinite(maxGrade)) {
|
|
theMaxGrade = Math.round(maxGrade * 100);
|
|
}
|
|
if (!Double.isNaN(minGrade) &&
|
|
!Double.isInfinite(minGrade)) {
|
|
theMinGrade = Math.round(minGrade * 100);
|
|
}
|
|
final String percent = "%";
|
|
|
|
return String.format(
|
|
"%s: %.2f %s (%.1f %s)\n"
|
|
+ "%s: %s\n"
|
|
+ "%s: %s\n"
|
|
+ "%s: %.2f %s (%.1f %s)\n"
|
|
+ "%s: %.2f %s (%.1f %s)\n"
|
|
+ "%s: %.2f %s (%.1f %s)\n"
|
|
+ "%s: %d %s (%d %s)\n"
|
|
+ "%s: %d %s (%d %s)\n"
|
|
+ "%s: %d %s (%d %s)\n"
|
|
+ "%s: %d %s\n"
|
|
+ "%s: %d %s\n",
|
|
context.getString(R.string.total_distance_label),
|
|
distanceInKm, context.getString(R.string.kilometer),
|
|
distanceInMiles, context.getString(R.string.mile),
|
|
context.getString(R.string.total_time_label),
|
|
StringUtils.formatTime(stats.getTotalTime()),
|
|
context.getString(R.string.moving_time_label),
|
|
StringUtils.formatTime(stats.getMovingTime()),
|
|
context.getString(R.string.average_speed_label),
|
|
averageSpeedInKmh, context.getString(R.string.kilometer_per_hour),
|
|
averageSpeedInMph, context.getString(R.string.mile_per_hour),
|
|
context.getString(R.string.average_moving_speed_label),
|
|
movingSpeedInKmh, context.getString(R.string.kilometer_per_hour),
|
|
movingSpeedInMph, context.getString(R.string.mile_per_hour),
|
|
context.getString(R.string.max_speed_label),
|
|
maxSpeedInKmh, context.getString(R.string.kilometer_per_hour),
|
|
maxSpeedInMph, context.getString(R.string.mile_per_hour),
|
|
context.getString(R.string.min_elevation_label),
|
|
minElevationInMeters, context.getString(R.string.meter),
|
|
minElevationInFeet, context.getString(R.string.feet),
|
|
context.getString(R.string.max_elevation_label),
|
|
maxElevationInMeters, context.getString(R.string.meter),
|
|
maxElevationInFeet, context.getString(R.string.feet),
|
|
context.getString(R.string.elevation_gain_label),
|
|
elevationGainInMeters, context.getString(R.string.meter),
|
|
elevationGainInFeet, context.getString(R.string.feet),
|
|
context.getString(R.string.max_grade_label),
|
|
theMaxGrade, percent,
|
|
context.getString(R.string.min_grade_label),
|
|
theMinGrade, percent);
|
|
}
|
|
}
|