WIP: [Android] Speed limit exceed check improvements #9167

Draft
strump wants to merge 8 commits from android/speed-limit-exceed-flag into master
14 changed files with 210 additions and 31 deletions

View file

@ -7,6 +7,7 @@
#include "app/organicmaps/util/Distance.hpp"
#include "app/organicmaps/util/FeatureIdBuilder.hpp"
#include "app/organicmaps/util/NetworkPolicy.hpp"
#include "app/organicmaps/util/SpeedFormatted.hpp"
#include "app/organicmaps/vulkan/android_vulkan_context_factory.hpp"
#include "map/bookmark_helpers.hpp"
@ -48,6 +49,7 @@
#include "platform/platform.hpp"
#include "platform/preferred_languages.hpp"
#include "platform/settings.hpp"
#include "platform/speed_formatted.hpp"
#include "platform/utm_mgrs_utils.hpp"
#include "base/assert.hpp"
@ -1259,7 +1261,9 @@ Java_app_organicmaps_Framework_nativeGetRouteFollowingInfo(JNIEnv * env, jclass)
jni::GetConstructorID(env, klass,
"(Lapp/organicmaps/util/Distance;Lapp/organicmaps/util/Distance;"
"Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;DIIIII"
"[Lapp/organicmaps/routing/SingleLaneInfo;DZZ)V");
"[Lapp/organicmaps/routing/SingleLaneInfo;"
"Lapp/organicmaps/util/SpeedFormatted;"
"Lapp/organicmaps/util/SpeedFormatted;ZZ)V");
vector<routing::FollowingInfo::SingleLaneInfoClient> const & lanes = info.m_lanes;
jobjectArray jLanes = nullptr;
@ -1287,6 +1291,7 @@ Java_app_organicmaps_Framework_nativeGetRouteFollowingInfo(JNIEnv * env, jclass)
}
auto const & rm = frm()->GetRoutingManager();
auto const speed = rm.RoutingSession().GetLastSpeed();
auto const isSpeedCamLimitExceeded = rm.IsRoutingActive() ? rm.IsSpeedCamLimitExceeded() : false;
auto const shouldPlaySignal = frm()->GetRoutingManager().GetSpeedCamManager().ShouldPlayBeepSignal();
jobject const result = env->NewObject(
@ -1294,8 +1299,9 @@ Java_app_organicmaps_Framework_nativeGetRouteFollowingInfo(JNIEnv * env, jclass)
ToJavaDistance(env, info.m_distToTurn), jni::ToJavaString(env, info.m_currentStreetName),
jni::ToJavaString(env, info.m_nextStreetName), jni::ToJavaString(env, info.m_nextNextStreetName),
info.m_completionPercent, info.m_turn, info.m_nextTurn, info.m_pedestrianTurn, info.m_exitNum,
info.m_time, jLanes, info.m_speedLimitMps, static_cast<jboolean>(isSpeedCamLimitExceeded),
static_cast<jboolean>(shouldPlaySignal));
info.m_time, jLanes, ToJavaSpeedFormatted(env, platform::SpeedFormatted(speed)),
ToJavaSpeedFormatted(env, platform::SpeedFormatted(info.m_speedLimitMps)),
static_cast<jboolean>(isSpeedCamLimitExceeded), static_cast<jboolean>(shouldPlaySignal));
ASSERT(result, (jni::DescribeException()));
return result;
}

View file

@ -0,0 +1,18 @@
#pragma once
#include "app/organicmaps/core/jni_helper.hpp"
#include "platform/speed_formatted.hpp"
inline jobject ToJavaSpeedFormatted(JNIEnv * env, platform::SpeedFormatted const & speedFormatted)
{
static jclass const speedFormattedClass = jni::GetGlobalClassRef(env, "app/organicmaps/util/SpeedFormatted");
static jmethodID const speedFormattedConstructor = jni::GetConstructorID(env, speedFormattedClass, "(DLjava/lang/String;B)V");
jobject distanceObject = env->NewObject(
speedFormattedClass, speedFormattedConstructor,
speedFormatted.GetSpeed(), jni::ToJavaString(env, speedFormatted.GetSpeedString()), static_cast<uint8_t>(speedFormatted.GetUnits()));
biodranik commented 2024-09-06 11:17:56 +00:00 (Migrated from github.com)
Review

nit: Looks like a lot of code, with a lot of jni overhead. Can formatting/string preparation be done all in C++ code, passed as several strings (or maybe just one string returned as a concatenation of several strings?) to the iOS and Android UI just for displaying?

nit: Looks like a lot of code, with a lot of jni overhead. Can formatting/string preparation be done all in C++ code, passed as several strings (or maybe just one string returned as a concatenation of several strings?) to the iOS and Android UI just for displaying?
return distanceObject;
}

View file

@ -49,14 +49,6 @@ Java_app_organicmaps_util_StringUtils_nativeFilterContainsNormalized(JNIEnv * en
return jni::ToJavaStringArray(env, filtered);
}
JNIEXPORT jobject JNICALL Java_app_organicmaps_util_StringUtils_nativeFormatSpeedAndUnits(
JNIEnv * env, jclass thiz, jdouble metersPerSecond)
{
auto const units = measurement_utils::GetMeasurementUnits();
return MakeJavaPair(env, measurement_utils::FormatSpeedNumeric(metersPerSecond, units),
platform::GetLocalizedSpeedUnits(units));
}
JNIEXPORT jobject JNICALL
Java_app_organicmaps_util_StringUtils_nativeFormatDistance(JNIEnv * env, jclass, jdouble distanceInMeters)
{

View file

@ -187,11 +187,11 @@ public class Framework
public static native DistanceAndAzimut nativeGetDistanceAndAzimuthFromLatLon(double dstLat, double dstLon, double srcLat, double srcLon, double north);
public static native String nativeFormatLatLon(double lat, double lon, int coordFormat);
public static native String nativeFormatLatLon(double lat, double lon, int coordFormat); // TODO: move this method to StringUtils class.
public static native String nativeFormatAltitude(double alt);
public static native String nativeFormatAltitude(double alt); // TODO: move this method to StringUtils class.
public static native String nativeFormatSpeed(double speed);
public static native String nativeFormatSpeed(double speed); // TODO: move this method to StringUtils class.
public static native String nativeGetGe0Url(double lat, double lon, double zoomLevel, String name);
Review

I left these TODO comments to make changes later. Too many changes for a single PR.

I left these `TODO` comments to make changes later. Too many changes for a single PR.

View file

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import app.organicmaps.R;
import app.organicmaps.util.Distance;
import app.organicmaps.util.SpeedFormatted;
// Called from JNI.
@Keep
@ -36,7 +37,8 @@ public class RoutingInfo
public final PedestrianTurnDirection pedestrianTurnDirection;
// Current speed limit in meters per second.
// If no info about speed limit then speedLimitMps < 0.
public final double speedLimitMps;
public final SpeedFormatted speed;
public final SpeedFormatted speedLimit;
private final boolean speedCamLimitExceeded;
private final boolean shouldPlayWarningSignal;
@ -143,8 +145,8 @@ public class RoutingInfo
public RoutingInfo(Distance distToTarget, Distance distToTurn, String currentStreet, String nextStreet, String nextNextStreet, double completionPercent,
int vehicleTurnOrdinal, int vehicleNextTurnOrdinal, int pedestrianTurnOrdinal, int exitNum,
int totalTime, SingleLaneInfo[] lanes, double speedLimitMps, boolean speedLimitExceeded,
boolean shouldPlayWarningSignal)
int totalTime, SingleLaneInfo[] lanes, SpeedFormatted speed, SpeedFormatted speedLimit,
boolean speedCamLimitExceeded, boolean shouldPlayWarningSignal)
{
this.distToTarget = distToTarget;
this.distToTurn = distToTurn;
@ -158,8 +160,9 @@ public class RoutingInfo
this.lanes = lanes;
this.exitNum = exitNum;
this.pedestrianTurnDirection = PedestrianTurnDirection.values()[pedestrianTurnOrdinal];
this.speedLimitMps = speedLimitMps;
this.speedCamLimitExceeded = speedLimitExceeded;
this.speed = speed;
this.speedLimit = speedLimit;
this.speedCamLimitExceeded = speedCamLimitExceeded;
this.shouldPlayWarningSignal = shouldPlayWarningSignal;
}

View file

@ -0,0 +1,55 @@
package app.organicmaps.util;
import android.content.Context;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import app.organicmaps.R;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class SpeedFormatted
{
/**
* IMPORTANT : Order of enum values MUST BE the same as native measurement_utils::Units enum.
*/
public enum Units
{
KilometersPerHour(R.string.kilometers_per_hour),
MilesPerHour(R.string.miles_per_hour);
@StringRes
public final int mStringRes;
Units(@StringRes int stringRes)
{
mStringRes = stringRes;
}
}
public final double mSpeed;
@NonNull
public final String mSpeedStr;
public final SpeedFormatted.Units mUnits;
public SpeedFormatted(double mSpeed, @NonNull String mSpeedStr, byte unitsIndex)
biodranik commented 2024-09-06 11:19:40 +00:00 (Migrated from github.com)
Review

nit: Are there simpler ideas on the implementation? E.g. return an array of 3 strings instead of introducing a class? How will it work on iOS? On Qt/desktop platforms?

nit: Are there simpler ideas on the implementation? E.g. return an array of 3 strings instead of introducing a class? How will it work on iOS? On Qt/desktop platforms?
Review

In iOS code navigation UI get speed from iOS API and speed limit from DTO MWMNavigationDashboardEntity (see NavigationControlView.swift:159 ). iOS converts m/s to km/h or mi/h in UI code calling measurement_utils::MpsToUnits(...). Also it compares speed and limit values in m/s and doesn't do rounding.

To avoid JNI conversion from m/s to metric or imperial units I pass current speed and speed limit using SpeedFormatted class. So we have only one JNI call to Framework.nativeGetRouteFollowingInfo().

You propose to pass String speedStr, String speedLimitStr, byte speedUnits, bool isSpeedLimitExceeded instead, right?

In iOS code navigation UI get speed from iOS API and speed limit from DTO `MWMNavigationDashboardEntity` (see [NavigationControlView.swift:159](https://github.com/organicmaps/organicmaps/blob/master/iphone/Maps/Classes/CustomViews/NavigationDashboard/Views/NavigationControlView.swift#L159) ). iOS converts m/s to km/h or mi/h in UI code calling `measurement_utils::MpsToUnits(...)`. Also it compares speed and limit values in m/s and doesn't do rounding. To avoid JNI conversion from m/s to metric or imperial units I pass current speed and speed limit using `SpeedFormatted` class. So we have only one JNI call to `Framework.nativeGetRouteFollowingInfo()`. You propose to pass `String speedStr`, `String speedLimitStr`, `byte speedUnits`, `bool isSpeedLimitExceeded` instead, right?
biodranik commented 2024-09-06 18:53:26 +00:00 (Migrated from github.com)
Review

On iOS information comes from two methods:

- (void)updateFollowingInfo:(routing::FollowingInfo const &)info routePoints:(NSArray<MWMRoutePoint *> *)points type:(MWMRouterType)type

and

- (void)updateTransitInfo:(TransitRouteInfo const &)info

It would be great to have one source of truth on all platforms, without any logic. E.g. just strings to display and flags like "speed limit was hit", or better 3-state like "no speeding/limit hit/fine limit hit".

To avoid slow JNI calls, something like this may be used:
return currentSpeedStr.append(';').append(speedLimit).append(';').append(speedingFlag).apend(';').append(unitsStr);

Then it can be decoded in Java right before displaying.

Passing all values separately is also ok, just slower a bit.

On iOS information comes from two methods: ``` - (void)updateFollowingInfo:(routing::FollowingInfo const &)info routePoints:(NSArray<MWMRoutePoint *> *)points type:(MWMRouterType)type and - (void)updateTransitInfo:(TransitRouteInfo const &)info ``` It would be great to have one source of truth on all platforms, without any logic. E.g. just strings to display and flags like "speed limit was hit", or better 3-state like "no speeding/limit hit/fine limit hit". To avoid slow JNI calls, something like this may be used: `return currentSpeedStr.append(';').append(speedLimit).append(';').append(speedingFlag).apend(';').append(unitsStr);` Then it can be decoded in Java right before displaying. Passing all values separately is also ok, just slower a bit.
Review

As far as I can tell, iOS doesn't have problem with units and formatting. Swift calls C++ function from measurement_utils namespace to convert m/s to units-per-hour and format speed to string. No problems with Swift<->C++ calls, as far as I can see.

New approach when MWMRoutingManager for iOS and Framework.nativeGetRouteFollowingInfo() for Android do speed convertions and have some new protocol with ';' separators looks like overengineering to me.

I propose to drop SpeedFormatted code and let UI do units conversions and string formatting with plain Java. I think functions within measurement_utils namespace would be easy to port to Java. This would minimize objects allocation between C++ and Java (get back double speedLimitMps as in master). What does iOS devs think of this?

As far as I can tell, iOS doesn't have problem with units and formatting. Swift calls C++ function from `measurement_utils` namespace to convert m/s to units-per-hour and format speed to string. No problems with Swift<->C++ calls, as far as I can see. New approach when `MWMRoutingManager` for iOS and `Framework.nativeGetRouteFollowingInfo()` for Android do speed convertions and have some new protocol with `';'` separators looks like overengineering to me. I propose to drop `SpeedFormatted` code and let UI do units conversions and string formatting with plain Java. I think functions within `measurement_utils` namespace would be easy to port to Java. This would minimize objects allocation between C++ and Java (get back `double speedLimitMps` as in `master`). What does iOS devs think of this?
{
this.mSpeed = mSpeed;
this.mSpeedStr = mSpeedStr;
this.mUnits = Units.values()[unitsIndex];
}
public boolean isValid()
{
return mSpeed >= 0.0;
}
@NonNull
public String getUnitsStr(@NonNull final Context context)
{
return context.getString(mUnits.mStringRes);
}
}

View file

@ -29,7 +29,6 @@ public class StringUtils
public static native boolean nativeContainsNormalized(String str, String substr);
public static native String[] nativeFilterContainsNormalized(String[] strings, String substr);
public static native Pair<String, String> nativeFormatSpeedAndUnits(double metersPerSecond);
public static native Distance nativeFormatDistance(double meters);
@NonNull
public static native Pair<String, String> nativeGetLocalizedDistanceUnits();

View file

@ -17,6 +17,7 @@ import app.organicmaps.location.LocationHelper;
import app.organicmaps.routing.RoutingInfo;
import app.organicmaps.sound.TtsPlayer;
import app.organicmaps.util.Graphics;
import app.organicmaps.util.SpeedFormatted;
import app.organicmaps.util.StringUtils;
import app.organicmaps.util.ThemeUtils;
import app.organicmaps.util.UiUtils;
@ -208,21 +209,20 @@ public class NavMenu
private void updateSpeedView(@NonNull RoutingInfo info)
{
final Location last = LocationHelper.from(mActivity).getSavedLocation();
if (last == null)
return;
SpeedFormatted speed = info.speed;
boolean speedLimitExceeded = false;
Pair<String, String> speedAndUnits = StringUtils.nativeFormatSpeedAndUnits(last.getSpeed());
if (info.speedLimitMps > 0.0)
if (info.speedLimit != null && info.speedLimit.isValid())
{
Pair<String, String> speedLimitAndUnits = StringUtils.nativeFormatSpeedAndUnits(info.speedLimitMps);
mSpeedValue.setText(speedAndUnits.first + "\u202F/\u202F" + speedLimitAndUnits.first);
SpeedFormatted speedLimit = info.speedLimit;
mSpeedValue.setText(speed.mSpeedStr + "\u202F/\u202F" + speedLimit.mSpeedStr);
speedLimitExceeded = (int)speed.mSpeed > (int)speedLimit.mSpeed;
}
else
mSpeedValue.setText(speedAndUnits.first);
mSpeedValue.setText(speed.mSpeedStr);
if (info.speedLimitMps > 0.0 && last.getSpeed() > info.speedLimitMps)
if (speedLimitExceeded)
Review

No more Pair<> classes. SpeedFormatted contains all details for UI.

No more `Pair<>` classes. `SpeedFormatted` contains all details for UI.
{
if (info.isSpeedCamLimitExceeded())
mSpeedValue.setTextColor(ContextCompat.getColor(mActivity, R.color.white_primary));
@ -232,7 +232,7 @@ public class NavMenu
else
mSpeedValue.setTextColor(ThemeUtils.getColor(mActivity, android.R.attr.textColorPrimary));
mSpeedUnits.setText(speedAndUnits.second);
mSpeedUnits.setText(info.speed.getUnitsStr(mActivity));
mSpeedViewContainer.setActivated(info.isSpeedCamLimitExceeded());
Review

Here we decide if there was speeding

Here we decide if there was speeding
biodranik commented 2024-09-06 11:21:09 +00:00 (Migrated from github.com)
Review

Here we compare km with km, or mi with mi, right? What about the threshold idea?

Here we compare km with km, or mi with mi, right? What about the threshold idea?
}

View file

@ -56,6 +56,8 @@ set(SRC
socket.hpp
string_storage_base.cpp
string_storage_base.hpp
speed_formatted.cpp
speed_formatted.hpp
utm_mgrs_utils.cpp
utm_mgrs_utils.hpp
)

View file

@ -23,6 +23,7 @@ inline double MilesToFeet(double mi) { return mi * 5280.0; }
inline double MiphToKmph(double miph) { return MilesToMeters(miph) / 1000.0; }
inline double KmphToMiph(double kmph) { return MetersToMiles(kmph * 1000.0); }
inline double MpsToKmph(double mps) { return mps * 3.6; }
inline double MpsToMiph(double mps) { return mps * 3.6 * 0.621371192; }
inline double MetersToFeet(double m) { return m * 3.2808399; }
inline double FeetToMeters(double ft) { return ft * 0.3048; }
inline double FeetToMiles(double ft) { return ft * 0.00018939; }

View file

@ -0,0 +1,68 @@
#include "speed_formatted.hpp"
#include "platform/locale.hpp"
#include "platform/localization.hpp"
#include "platform/measurement_utils.hpp"
#include "base/assert.hpp"
namespace platform
{
using namespace measurement_utils;
SpeedFormatted::SpeedFormatted(double speedMps) : SpeedFormatted(speedMps, GetMeasurementUnits()) {}
biodranik commented 2024-09-06 11:23:41 +00:00 (Migrated from github.com)
Review

It would be great to avoid introducing an implicit call to some external GetMeasurementUnits() method, and always pass it explicitly into the second constructor below.

It would be great to avoid introducing an implicit call to some external GetMeasurementUnits() method, and always pass it explicitly into the second constructor below.
SpeedFormatted::SpeedFormatted(double speedMps, Units units) : m_units(units)
{
switch (units)
{
case Units::Metric:
m_speed = speedMps < 0 ? speedMps : MpsToKmph(speedMps);
break;
case Units::Imperial:
m_speed = speedMps < 0 ? speedMps : MpsToMiph(speedMps);
break;
default: UNREACHABLE();
}
}
bool SpeedFormatted::IsValid() const { return m_speed >= 0.0; }
double SpeedFormatted::GetSpeed() const { return m_speed; }
Units SpeedFormatted::GetUnits() const { return m_units; }
std::string SpeedFormatted::GetSpeedString() const
{
if (!IsValid())
biodranik commented 2024-09-06 11:24:23 +00:00 (Migrated from github.com)
Review

What is the situation when this class can be invalid? Can we guarantee that when it is created, it is always valid?

What is the situation when this class can be invalid? Can we guarantee that when it is created, it is always valid?
Review

Speed limit could be 0.0 which means no limits at all. But if speed limit in OSM data is not set we have -1.0. So it makes sense to return "" if OSM data has no limit defined.

Speed limit could be `0.0` which means no limits at all. But if speed limit in OSM data is not set we have `-1.0`. So it makes sense to return `""` if OSM data has no limit defined.
biodranik commented 2024-09-06 18:56:58 +00:00 (Migrated from github.com)
Review

Store "invalid" value in valid class doesn't look clean. C++ has std::optional for that, there are also nullptr and empty string that can be used in JNI context to define "missing" speed limit vs "unlimited". AFAIR unlimited roads are only in Germany, and passing there something like 999 should work fine too.

Store "invalid" value in valid class doesn't look clean. C++ has std::optional for that, there are also nullptr and empty string that can be used in JNI context to define "missing" speed limit vs "unlimited". AFAIR unlimited roads are only in Germany, and passing there something like 999 should work fine too.
return "";
biodranik commented 2024-09-06 11:24:34 +00:00 (Migrated from github.com)
Review

nit: return {}

nit: return {}
// Default precision is 0 (no decimals).
int precision = 0;
// Set 1 decimal precision for speed per hour (km/h, miles/h) lower than 10.0 (9.5, 7.0,...).
if (m_speed < 10.0)
precision = 1;
return ToStringPrecision(m_speed, precision);
}
std::string SpeedFormatted::GetUnitsString() const
biodranik commented 2024-09-06 11:26:17 +00:00 (Migrated from github.com)
Review

nit: We have in the UI:

  1. Distance
  2. Speed
  3. Time
  4. Elevation

There are already several classes for formatting. Does it make sense to write a single wrapper with simple interface for all these cases?

nit: We have in the UI: 1. Distance 2. Speed 3. Time 4. Elevation There are already several classes for formatting. Does it make sense to write a single wrapper with simple interface for all these cases?
{
switch (m_units)
{
case Units::Metric: return GetLocalizedString("kilometers_per_hour");
case Units::Imperial: return GetLocalizedString("miles_per_hour");
default: UNREACHABLE();
}
}
std::string SpeedFormatted::ToString() const
{
if (!IsValid())
return "";
return GetSpeedString() + kNarrowNonBreakingSpace + GetUnitsString();
}
} // namespace platform

View file

@ -0,0 +1,32 @@
#pragma once
#include "platform/measurement_utils.hpp"
#include <string>
namespace platform
{
class SpeedFormatted
biodranik commented 2024-09-06 11:27:30 +00:00 (Migrated from github.com)
Review

nit: Can it be a struct with 3 values, filled by a single function call?

nit: Can it be a struct with 3 values, filled by a single function call?
{
public:
SpeedFormatted(double speedInMps); // Initialize with m/s value and default measurement units
SpeedFormatted(double speedInMps, measurement_utils::Units units);
bool IsValid() const;
double GetSpeed() const;
measurement_utils::Units GetUnits() const;
std::string GetSpeedString() const;
std::string GetUnitsString() const;
std::string ToString() const;
friend std::string DebugPrint(SpeedFormatted const & d) { return d.ToString(); }
private:
double m_speed; // Speed in km/h or mile/h depending on m_units.
measurement_utils::Units m_units;
};
} // namespace platform

View file

@ -273,6 +273,7 @@ SessionState RoutingSession::OnLocationPositionChanged(GpsInfo const & info)
return m_state;
m_turnNotificationsMgr.SetSpeedMetersPerSecond(info.m_speed);
m_lastSpeed = info.m_speed;
auto const formerIter = m_route->GetCurrentIteratorTurn();
if (m_route->MoveIterator(info))

View file

@ -173,6 +173,7 @@ public:
void SetGuidesForTests(GuidesTracks guides) { m_router->SetGuidesTracks(std::move(guides)); }
double GetCompletionPercent() const;
double GetLastSpeed() const { return m_lastSpeed; }
private:
struct DoReadyCallback
@ -208,6 +209,7 @@ private:
double m_lastDistance = 0.0;
int m_moveAwayCounter = 0;
m2::PointD m_lastGoodPosition;
double m_lastSpeed = -1.0;
m2::PointD m_userCurrentPosition;
bool m_userCurrentPositionValid = false;