From 2bb8b7aa50e5ad3f4bc12e17d2694f65d7903782 Mon Sep 17 00:00:00 2001 From: Gonzalo Pesquero Date: Tue, 16 Apr 2024 20:32:00 +0200 Subject: [PATCH] [android] Add Speed class Signed-off-by: Gonzalo Pesquero --- .../main/cpp/app/organicmaps/Framework.cpp | 6 + .../cpp/app/organicmaps/util/StringUtils.cpp | 8 + .../main/java/app/organicmaps/Framework.java | 6 + .../routing/NavigationController.java | 2 +- .../main/java/app/organicmaps/util/Speed.java | 171 ++++++++++++++++++ .../app/organicmaps/util/StringUtils.java | 1 + .../app/organicmaps/widget/menu/NavMenu.java | 46 ++++- 7 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 android/app/src/main/java/app/organicmaps/util/Speed.java diff --git a/android/app/src/main/cpp/app/organicmaps/Framework.cpp b/android/app/src/main/cpp/app/organicmaps/Framework.cpp index 4eaf04efa2..e584ca32db 100644 --- a/android/app/src/main/cpp/app/organicmaps/Framework.cpp +++ b/android/app/src/main/cpp/app/organicmaps/Framework.cpp @@ -1953,4 +1953,10 @@ Java_app_organicmaps_Framework_nativeGetKayakHotelLink(JNIEnv * env, jclass, jst return url.empty() ? nullptr : jni::ToJavaString(env, url); } +JNIEXPORT jint JNICALL +Java_app_organicmaps_Framework_nativeGetUnits(JNIEnv *, jclass) +{ + return static_cast(measurement_utils::GetMeasurementUnits()); +} + } // extern "C" diff --git a/android/app/src/main/cpp/app/organicmaps/util/StringUtils.cpp b/android/app/src/main/cpp/app/organicmaps/util/StringUtils.cpp index e99a34b0ed..dbf7199809 100644 --- a/android/app/src/main/cpp/app/organicmaps/util/StringUtils.cpp +++ b/android/app/src/main/cpp/app/organicmaps/util/StringUtils.cpp @@ -57,6 +57,14 @@ JNIEXPORT jobject JNICALL Java_app_organicmaps_util_StringUtils_nativeFormatSpee platform::GetLocalizedSpeedUnits(units)); } +JNIEXPORT jobject JNICALL Java_app_organicmaps_util_StringUtils_nativeStringFormatSpeedAndUnits( + JNIEnv * env, jclass thiz, jdouble metersPerSecond) +{ + auto const units = measurement_utils::GetMeasurementUnits(); + return jni::ToJavaString(env, measurement_utils::FormatSpeedNumeric(metersPerSecond, units) + + ";" + platform::GetLocalizedSpeedUnits(units)); +} + JNIEXPORT jobject JNICALL Java_app_organicmaps_util_StringUtils_nativeFormatDistance(JNIEnv * env, jclass, jdouble distanceInMeters) { diff --git a/android/app/src/main/java/app/organicmaps/Framework.java b/android/app/src/main/java/app/organicmaps/Framework.java index 2a076225c3..fe8c879f0d 100644 --- a/android/app/src/main/java/app/organicmaps/Framework.java +++ b/android/app/src/main/java/app/organicmaps/Framework.java @@ -60,6 +60,10 @@ public class Framework public static final int ROUTER_TYPE_TRANSIT = 3; public static final int ROUTER_TYPE_RULER = 4; + // Units values shall be the same as the ones defined in c++ in . + public static final int UNITS_METRIC = 0; + public static final int UNITS_IMPERIAL = 1; + @Retention(RetentionPolicy.SOURCE) @IntDef({ROUTE_REBUILD_AFTER_POINTS_LOADING}) public @interface RouteRecommendationType {} @@ -460,4 +464,6 @@ public class Framework @Nullable public static native String nativeGetKayakHotelLink(@NonNull String countryIsoCode, @NonNull String uri, long firstDaySec, long lastDaySec, boolean isReferral); + + public static native int nativeGetUnits(); } diff --git a/android/app/src/main/java/app/organicmaps/routing/NavigationController.java b/android/app/src/main/java/app/organicmaps/routing/NavigationController.java index a789dd4c2f..41fab8cb1f 100644 --- a/android/app/src/main/java/app/organicmaps/routing/NavigationController.java +++ b/android/app/src/main/java/app/organicmaps/routing/NavigationController.java @@ -155,7 +155,7 @@ public class NavigationController implements TrafficManager.TrafficCallback, updateVehicle(info); updateStreetView(info); - mNavMenu.update(info); + mNavMenu.update(info, Framework.nativeGetUnits()); } private void updateStreetView(@NonNull RoutingInfo info) diff --git a/android/app/src/main/java/app/organicmaps/util/Speed.java b/android/app/src/main/java/app/organicmaps/util/Speed.java new file mode 100644 index 0000000000..15c7b5b485 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/util/Speed.java @@ -0,0 +1,171 @@ +package app.organicmaps.util; + +import android.content.Context; +import android.util.Pair; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Formatter; +import java.util.Locale; + +import app.organicmaps.Framework; +import app.organicmaps.util.log.Logger; + +public class Speed +{ + private static String mUnitStringKmh = "km/h"; + private static String mUnitStringMiph = "mph"; + + private static char mDecimalSeparator = Character.MIN_VALUE; + + public static double MpsToKmph(double mps) { return mps * 3.6; } + public static double MpsToMiph(double mps) { return mps * 2.236936; } + + public static void setUnitStringKmh(String unitStringKmh) { mUnitStringKmh = unitStringKmh; } + public static void setUnitStringMiph(String unitStringMiph) { mUnitStringMiph = unitStringMiph; } + + private final static DecimalFormat mDecimalFormatNoDecimal = new DecimalFormat("#"); + private final static DecimalFormat mDecimalFormatOneDecimal = new DecimalFormat("0.0"); + + private final static Locale mLocale = Locale.getDefault(); + + private final static StringBuilder mSb = new StringBuilder(); + private final static Formatter mFormatter = new Formatter(mSb, mLocale); + + private final static NumberFormat mNumberFormatNoDecimal = NumberFormat.getInstance(mLocale); + private final static NumberFormat mNumberFormatOneDecimal = NumberFormat.getInstance(mLocale); + + public static Pair formatMeasurements(double speedInMetersPerSecond, int units, + Context context) + { + double speedValue; + String unitsString; + + if (units == Framework.UNITS_IMPERIAL) + { + speedValue = MpsToMiph(speedInMetersPerSecond); + unitsString = mUnitStringMiph; + } + else + { + speedValue = MpsToKmph(speedInMetersPerSecond); + unitsString = mUnitStringKmh; + } + + // Option 1: String.format() + long start1 = System.nanoTime(); + String formatString = (speedValue < 10.0)? "%.1f" : "%.0f"; + String speedString = String.format(mLocale, formatString, speedValue); + long elapsed1 = System.nanoTime() - start1; + Logger.i("LOCALE_MEASURE", "1) " + speedString); + + // Option 2: DecimalFormat class + long start2 = System.nanoTime(); + if (speedValue < 10.0) + speedString = mDecimalFormatOneDecimal.format(speedValue); + else + speedString = mDecimalFormatNoDecimal.format(speedValue); + long elapsed2 = System.nanoTime() - start2; + Logger.i("LOCALE_MEASURE", "2) " + speedString); + + // Option 3: Long.toString() + StringBuffer.insert() + long start3 = System.nanoTime(); + if (speedValue < 10.0) + { + speedString = Long.toString(Math.round(speedValue * 10.0)); + + StringBuffer buffer = new StringBuffer(speedString); + + if (mDecimalSeparator == Character.MIN_VALUE) + mDecimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator(); + + // For low values (< 1.0), force to have 2 characters in string. + if (buffer.length() < 2) + buffer.insert(0, "0"); + + buffer.insert(1, mDecimalSeparator); + + speedString = buffer.toString(); + } + else + speedString = Long.toString(Math.round(speedValue)); + long elapsed3 = System.nanoTime() - start3; + Logger.i("LOCALE_MEASURE", "3) " + speedString); + + // Option 4: Formatter class + mSb.setLength(0); + long start4 = System.nanoTime(); + if (speedValue < 10.0) + mFormatter.format("%.1f", speedValue); + else + mFormatter.format("%d", Math.round(speedValue)); + speedString = mSb.toString(); + long elapsed4 = System.nanoTime() - start4; + Logger.i("LOCALE_MEASURE", "4) " + speedString); + + // Option 5: NumberFormat class + mNumberFormatNoDecimal.setMaximumFractionDigits(0); + mNumberFormatOneDecimal.setMaximumFractionDigits(1); + long start5 = System.nanoTime(); + if (speedValue < 10.0) + speedString = mNumberFormatOneDecimal.format(speedValue); + else + speedString = mNumberFormatNoDecimal.format(Math.round(speedValue)); + long elapsed5 = System.nanoTime() - start5; + Logger.i("LOCALE_MEASURE", "5) " + speedString); + + String text = String.format(Locale.US, + "Java calls: %5d / %5d / %5d / %5d / %5d", + Math.round(0.001 * elapsed1), + Math.round(0.001 * elapsed2), + Math.round(0.001 * elapsed3), + Math.round(0.001 * elapsed4), + Math.round(0.001 * elapsed5)); + Logger.i("LOCALE_MEASURE", text); + + return new Pair<>(speedString, unitsString); + } + + public static Pair format(double speedInMetersPerSecond, int units, + Context context) + { + double speedValue; + String unitsString; + + if (units == Framework.UNITS_IMPERIAL) + { + speedValue = MpsToMiph(speedInMetersPerSecond); + unitsString = mUnitStringMiph; + } + else + { + speedValue = MpsToKmph(speedInMetersPerSecond); + unitsString = mUnitStringKmh; + } + + String speedString; + + if (speedValue < 10.0) + { + speedString = Long.toString(Math.round(speedValue * 10.0)); + + StringBuffer buffer = new StringBuffer(speedString); + + if (mDecimalSeparator == Character.MIN_VALUE) + mDecimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator(); + + // For low values (< 1.0), force to have 2 characters in string. + if (buffer.length() < 2) + buffer.insert(0, "0"); + + buffer.insert(1, mDecimalSeparator); + + speedString = buffer.toString(); + } + else + speedString = Long.toString(Math.round(speedValue)); + + return new Pair<>(speedString, unitsString); + } +} diff --git a/android/app/src/main/java/app/organicmaps/util/StringUtils.java b/android/app/src/main/java/app/organicmaps/util/StringUtils.java index d82b80a092..1ae77dd330 100644 --- a/android/app/src/main/java/app/organicmaps/util/StringUtils.java +++ b/android/app/src/main/java/app/organicmaps/util/StringUtils.java @@ -30,6 +30,7 @@ public class StringUtils public static native String[] nativeFilterContainsNormalized(String[] strings, String substr); public static native Pair nativeFormatSpeedAndUnits(double metersPerSecond); + public static native String nativeStringFormatSpeedAndUnits(double metersPerSecond); public static native Distance nativeFormatDistance(double meters); @NonNull public static native Pair nativeGetLocalizedDistanceUnits(); diff --git a/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java b/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java index 1c772cd69d..518d0bb956 100644 --- a/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java +++ b/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java @@ -1,6 +1,7 @@ package app.organicmaps.widget.menu; import android.location.Location; +import android.util.Log; import android.util.Pair; import android.view.View; import android.widget.Button; @@ -15,12 +16,16 @@ import app.organicmaps.location.LocationHelper; import app.organicmaps.routing.RoutingInfo; import app.organicmaps.sound.TtsPlayer; import app.organicmaps.util.Graphics; +import app.organicmaps.util.Speed; import app.organicmaps.util.StringUtils; import app.organicmaps.util.UiUtils; +import app.organicmaps.util.log.Logger; + import com.google.android.material.progressindicator.LinearProgressIndicator; import java.time.LocalTime; import java.time.format.DateTimeFormatter; +import java.util.Locale; import java.util.concurrent.TimeUnit; public class NavMenu @@ -203,22 +208,57 @@ public class NavMenu mTimeEstimate.setText(localTime.format(DateTimeFormatter.ofPattern(format))); } - private void updateSpeedView(@NonNull RoutingInfo info) + private void updateSpeedView(@NonNull RoutingInfo info, int units) { final Location last = LocationHelper.from(mActivity).getSavedLocation(); if (last == null) return; + // Log measurements for different Java implementations. + Speed.formatMeasurements(last.getSpeed(), units, mActivity.getApplicationContext()); + + // Speed formatting using native calls. + long start1 = System.nanoTime(); Pair speedAndUnits = StringUtils.nativeFormatSpeedAndUnits(last.getSpeed()); + long elapsed1 = System.nanoTime() - start1; mSpeedUnits.setText(speedAndUnits.second); mSpeedValue.setText(speedAndUnits.first); + + // Speed formatting using native calls. Speed and units is returned in a single string. + long start2 = System.nanoTime(); + String speedAndUnitsString = StringUtils.nativeStringFormatSpeedAndUnits(last.getSpeed()); + // Speed and units are separated by semicolon ";" in returned string. + int separatorPos = speedAndUnitsString.indexOf(";"); + String speedString = speedAndUnitsString.substring(0, separatorPos); + String unitsString = speedAndUnitsString.substring(separatorPos + 1); + long elapsed2 = System.nanoTime() - start2; + + mSpeedUnits.setText(speedString); + mSpeedValue.setText(unitsString); + + // Speed formatting using Android Java calls. + long start3 = System.nanoTime(); + speedAndUnits = Speed.format(last.getSpeed(), units, mActivity.getApplicationContext()); + long elapsed3 = System.nanoTime() - start3; + + mSpeedUnits.setText(speedAndUnits.second); + mSpeedValue.setText(speedAndUnits.first); + + String text = String.format(Locale.US, + "Native calls: %5d / %5d / %5d", + Math.round(0.001 * elapsed1), + Math.round(0.001 * elapsed2), + Math.round(0.001 * elapsed3)); + + Logger.i("LOCALE_MEASURE", text); + mSpeedViewContainer.setActivated(info.isSpeedLimitExceeded()); } - public void update(@NonNull RoutingInfo info) + public void update(@NonNull RoutingInfo info, int units) { - updateSpeedView(info); + updateSpeedView(info, units); updateTime(info.totalTimeInSeconds); mDistanceValue.setText(info.distToTarget.mDistanceStr); mDistanceUnits.setText(info.distToTarget.getUnitsStr(mActivity.getApplicationContext())); -- 2.45.3