[routing] Add possibility to save routes as tracks

Signed-off-by: cyber-toad <the.cyber.toad@proton.me>
This commit is contained in:
cyber-toad
2025-03-11 10:13:57 +01:00
committed by Konstantin Pastbin
parent 9e8accc8f5
commit 45bba5fb5e
16 changed files with 165 additions and 18 deletions

View File

@@ -1143,6 +1143,12 @@ Java_app_organicmaps_Framework_nativeShowTrackRect(JNIEnv * env, jclass, jlong t
frm()->ShowTrack(static_cast<kml::TrackId>(track)); frm()->ShowTrack(static_cast<kml::TrackId>(track));
} }
JNIEXPORT void JNICALL
Java_app_organicmaps_Framework_nativeSaveRoute(JNIEnv *, jclass)
{
frm()->SaveRoute();
}
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_app_organicmaps_Framework_nativeGetBookmarkDir(JNIEnv * env, jclass) Java_app_organicmaps_Framework_nativeGetBookmarkDir(JNIEnv * env, jclass)
{ {

View File

@@ -356,4 +356,6 @@ public class Framework
public static native void nativeDidCloseProductsPopup(String reason); public static native void nativeDidCloseProductsPopup(String reason);
public static native void nativeDidSelectProduct(String title, String link); public static native void nativeDidSelectProduct(String title, String link);
public static native void nativeSaveRoute();
} }

View File

@@ -90,7 +90,7 @@ final class RoutingBottomMenuController implements View.OnClickListener
@NonNull @NonNull
static RoutingBottomMenuController newInstance(@NonNull Activity activity, @NonNull View frame, static RoutingBottomMenuController newInstance(@NonNull Activity activity, @NonNull View frame,
@Nullable RoutingBottomMenuListener listener) @NonNull RoutingBottomMenuListener listener)
{ {
View altitudeChartFrame = getViewById(activity, frame, R.id.altitude_chart_panel); View altitudeChartFrame = getViewById(activity, frame, R.id.altitude_chart_panel);
View timeElevationLine = getViewById(activity, frame, R.id.time_elevation_line); View timeElevationLine = getViewById(activity, frame, R.id.time_elevation_line);
@@ -158,6 +158,9 @@ final class RoutingBottomMenuController implements View.OnClickListener
res.getDimensionPixelSize(R.dimen.margin_half)); res.getDimensionPixelSize(R.dimen.margin_half));
Button manageRouteButton = altitudeChartFrame.findViewById(R.id.btn__manage_route); Button manageRouteButton = altitudeChartFrame.findViewById(R.id.btn__manage_route);
manageRouteButton.setOnClickListener(this); manageRouteButton.setOnClickListener(this);
Button saveButton = altitudeChartFrame.findViewById(R.id.btn__save);
saveButton.setOnClickListener(this);
} }
void showAltitudeChartAndRoutingDetails() void showAltitudeChartAndRoutingDetails()
@@ -168,6 +171,9 @@ final class RoutingBottomMenuController implements View.OnClickListener
showRouteAltitudeChart(); showRouteAltitudeChart();
showRoutingDetails(); showRoutingDetails();
UiUtils.show(mAltitudeChartFrame); UiUtils.show(mAltitudeChartFrame);
Button saveButton = mAltitudeChartFrame.findViewById(R.id.btn__save);
saveButton.setText(R.string.save);
saveButton.setEnabled(true);
} }
void hideAltitudeChartAndRoutingDetails() void hideAltitudeChartAndRoutingDetails()
@@ -484,14 +490,21 @@ final class RoutingBottomMenuController implements View.OnClickListener
public void onClick(View v) public void onClick(View v)
{ {
final int id = v.getId(); final int id = v.getId();
if (id == R.id.btn__my_position_use && mListener != null) if (id == R.id.btn__my_position_use)
mListener.onUseMyPositionAsStart(); mListener.onUseMyPositionAsStart();
else if (id == R.id.btn__search_point && mListener != null) else if (id == R.id.btn__search_point)
{ {
final RouteMarkType pointType = (RouteMarkType) mActionMessage.getTag(); final RouteMarkType pointType = (RouteMarkType) mActionMessage.getTag();
mListener.onSearchRoutePoint(pointType); mListener.onSearchRoutePoint(pointType);
} }
else if (id == R.id.btn__manage_route && mListener != null) else if (id == R.id.btn__manage_route)
mListener.onManageRouteOpen(); mListener.onManageRouteOpen();
else if (id == R.id.btn__save)
{
Framework.nativeSaveRoute();
Button saveButton = v.findViewById(R.id.btn__save);
saveButton.setEnabled(false);
saveButton.setText(R.string.saved);
}
} }
} }

View File

@@ -86,7 +86,7 @@ public class RoutingPlanController extends ToolbarController
RoutingPlanController(View root, Activity activity, RoutingPlanController(View root, Activity activity,
ActivityResultLauncher<Intent> startDrivingOptionsForResult, ActivityResultLauncher<Intent> startDrivingOptionsForResult,
@NonNull RoutingPlanInplaceController.RoutingPlanListener routingPlanListener, @NonNull RoutingPlanInplaceController.RoutingPlanListener routingPlanListener,
@Nullable RoutingBottomMenuListener listener) @NonNull RoutingBottomMenuListener listener)
{ {
super(root, activity); super(root, activity);
mFrame = root; mFrame = root;

View File

@@ -73,6 +73,17 @@
android:drawableStart="@drawable/ic_manage_route" android:drawableStart="@drawable/ic_manage_route"
android:drawablePadding="6dp"/> android:drawablePadding="6dp"/>
<Button
android:id="@+id/btn__save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/margin_base"
android:minHeight="@dimen/primary_button_min_height"
style="@style/MwmWidget.Button"
android:text="@string/save"/>
<Button <Button
android:id="@+id/start" android:id="@+id/start"
style="@style/MwmWidget.Button.Primary" style="@style/MwmWidget.Button.Primary"
@@ -80,7 +91,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="0" android:layout_weight="0"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/margin_base" android:layout_marginStart="@dimen/margin_double"
android:minWidth="@dimen/start_button_width" android:minWidth="@dimen/start_button_width"
android:text="@string/p2p_start" android:text="@string/p2p_start"
tools:showIn="@layout/menu_route_plan_line" /> tools:showIn="@layout/menu_route_plan_line" />

View File

@@ -92,6 +92,15 @@
android:drawableStart="@drawable/ic_manage_route" android:drawableStart="@drawable/ic_manage_route"
android:drawablePadding="6dp"/> android:drawablePadding="6dp"/>
<Button
android:id="@+id/btn__save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_base"
style="@style/MwmWidget.Button"
android:minHeight="@dimen/primary_button_min_height"
android:text="@string/save"/>
<Button <Button
android:id="@+id/start" android:id="@+id/start"
style="@style/MwmWidget.Button.Primary" style="@style/MwmWidget.Button.Primary"
@@ -99,7 +108,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="0" android:layout_weight="0"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/margin_base" android:layout_marginStart="@dimen/margin_double"
android:minWidth="@dimen/start_button_width" android:minWidth="@dimen/start_button_width"
android:text="@string/p2p_start" android:text="@string/p2p_start"
tools:showIn="@layout/menu_route_plan_line" /> tools:showIn="@layout/menu_route_plan_line" />

View File

@@ -83,6 +83,11 @@
android:layout_gravity="center_vertical" /> android:layout_gravity="center_vertical" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical" >
<Button <Button
android:id="@+id/btn__manage_route" android:id="@+id/btn__manage_route"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -92,4 +97,14 @@
android:drawableStart="@drawable/ic_manage_route" android:drawableStart="@drawable/ic_manage_route"
android:drawablePadding="6dp"/> android:drawablePadding="6dp"/>
<Button
android:id="@+id/btn__save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/MwmWidget.Button"
android:layout_marginStart="@dimen/margin_base"
android:minHeight="@dimen/primary_button_min_height"
android:text="@string/save"/>
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -655,6 +655,7 @@
<string name="button_plan">Plan</string> <string name="button_plan">Plan</string>
<string name="placepage_remove_stop">Remove Stop</string> <string name="placepage_remove_stop">Remove Stop</string>
<string name="placepage_add_stop">Add Stop</string> <string name="placepage_add_stop">Add Stop</string>
<string name="saved">Saved</string>
<!-- Alert to ask user relogin to OpenStreetMap with OAuth2 flow after OAuth1 authentication is deprecated. --> <!-- Alert to ask user relogin to OpenStreetMap with OAuth2 flow after OAuth1 authentication is deprecated. -->
<string name="alert_reauth_message">Please login to OpenStreetMap to automatically upload all your map edits. Learn more <a href="https://github.com/organicmaps/organicmaps/issues/6144">here</a>.</string> <string name="alert_reauth_message">Please login to OpenStreetMap to automatically upload all your map edits. Learn more <a href="https://github.com/organicmaps/organicmaps/issues/6144">here</a>.</string>
<string name="dialog_error_storage_title">Storage access problem</string> <string name="dialog_error_storage_title">Storage access problem</string>

View File

@@ -1170,6 +1170,51 @@ dp::Color BookmarkManager::GenerateTrackRecordingColor() const
return kml::ColorFromPredefinedColor(kml::GetRandomPredefinedColor()); return kml::ColorFromPredefinedColor(kml::GetRandomPredefinedColor());
} }
std::string BookmarkManager::GenerateSavedRouteName(std::string const & from, std::string const & to)
{
if (!from.empty() && !to.empty())
return from + " - " + to;
if (!from.empty())
return from;
if (!to.empty())
return to;
return GenerateTrackRecordingName();
}
kml::TrackId BookmarkManager::SaveRoute(std::vector<m2::PointD> const & points, std::string const & from, std::string const & to)
{
kml::MultiGeometry geometry;
geometry.m_lines.emplace_back();
geometry.m_timestamps.emplace_back();
auto & line = geometry.m_lines.back();
for (auto const & pt : points)
line.emplace_back(pt);
kml::TrackData trackData;
trackData.m_geometry = std::move(geometry);
auto trackName = GenerateSavedRouteName(from, to);
kml::SetDefaultStr(trackData.m_name, trackName);
kml::ColorData colorData;
colorData.m_rgba = GenerateTrackRecordingColor().GetRGBA();
kml::TrackLayer layer;
layer.m_color = colorData;
std::vector<kml::TrackLayer> m_layers;
m_layers.emplace_back(layer);
trackData.m_layers = std::move(m_layers);
trackData.m_timestamp = kml::TimestampClock::now();
auto editSession = GetEditSession();
auto const track = editSession.CreateTrack(std::move(trackData));
auto const groupId = LastEditedBMCategory();
auto const trackId = track->GetId();
AttachTrack(trackId, groupId);
return trackId;
}
void BookmarkManager::PrepareBookmarksAddresses(std::vector<SortBookmarkData> & bookmarksForSort, void BookmarkManager::PrepareBookmarksAddresses(std::vector<SortBookmarkData> & bookmarksForSort,
AddressesCollection & newAddresses) AddressesCollection & newAddresses)
{ {

View File

@@ -29,7 +29,6 @@
#include <string> #include <string>
#include <vector> #include <vector>
namespace storage namespace storage
{ {
class CountryInfoGetter; class CountryInfoGetter;
@@ -436,6 +435,8 @@ public:
std::string GenerateTrackRecordingName() const; std::string GenerateTrackRecordingName() const;
dp::Color GenerateTrackRecordingColor() const; dp::Color GenerateTrackRecordingColor() const;
kml::TrackId SaveRoute(std::vector<m2::PointD> const & points, std::string const & from, std::string const & to);
private: private:
class MarksChangesTracker : public df::UserMarksProvider class MarksChangesTracker : public df::UserMarksProvider
{ {
@@ -624,6 +625,7 @@ private:
void CleanupInvalidMetadata(); void CleanupInvalidMetadata();
std::string GetMetadataEntryName(kml::MarkGroupId groupId) const; std::string GetMetadataEntryName(kml::MarkGroupId groupId) const;
std::string GenerateSavedRouteName(std::string const & from, std::string const & to);
void NotifyAboutStartAsyncLoading(); void NotifyAboutStartAsyncLoading();
void NotifyAboutFinishAsyncLoading(KMLDataCollectionPtr && collection); void NotifyAboutFinishAsyncLoading(KMLDataCollectionPtr && collection);
void NotifyAboutFile(bool success, std::string const & filePath, bool isTemporaryFile); void NotifyAboutFile(bool success, std::string const & filePath, bool isTemporaryFile);

View File

@@ -1773,6 +1773,11 @@ bool Framework::IsTrackRecordingEnabled() const
return GpsTracker::Instance().IsEnabled(); return GpsTracker::Instance().IsEnabled();
} }
void Framework::SaveRoute()
{
m_routingManager.SaveRoute();
}
void Framework::OnUpdateGpsTrackPointsCallback(vector<pair<size_t, location::GpsInfo>> && toAdd, void Framework::OnUpdateGpsTrackPointsCallback(vector<pair<size_t, location::GpsInfo>> && toAdd,
pair<size_t, size_t> const & toRemove, pair<size_t, size_t> const & toRemove,
TrackStatistics const & trackStatistics) TrackStatistics const & trackStatistics)

View File

@@ -444,6 +444,8 @@ public:
void SaveTrackRecordingWithName(std::string const & name); void SaveTrackRecordingWithName(std::string const & name);
bool IsTrackRecordingEmpty() const; bool IsTrackRecordingEmpty() const;
bool IsTrackRecordingEnabled() const; bool IsTrackRecordingEnabled() const;
void SaveRoute();
/// Returns the elevation profile data of the currently recorded track. /// Returns the elevation profile data of the currently recorded track.
/// To get the data on the every track recording state update, this function should be called after receiving the callback from the `SetTrackRecordingUpdateHandler`. /// To get the data on the every track recording state update, this function should be called after receiving the callback from the `SetTrackRecordingUpdateHandler`.
static const ElevationInfo & GetTrackRecordingElevationInfo(); static const ElevationInfo & GetTrackRecordingElevationInfo();

View File

@@ -1639,4 +1639,18 @@ UNIT_CLASS_TEST(Runner, Bookmarks_RecentlyDeleted)
TEST(!Platform::IsFileExistsByFullPath(filePath), ()); TEST(!Platform::IsFileExistsByFullPath(filePath), ());
TEST(!Platform::IsFileExistsByFullPath(deletedFilePath), ()); TEST(!Platform::IsFileExistsByFullPath(deletedFilePath), ());
} }
UNIT_CLASS_TEST(Runner, Bookmarks_TestSaveRoute)
{
BookmarkManager bmManager(BM_CALLBACKS);
bmManager.EnableTestMode(true);
auto const points = {m2::PointD(0.0, 0.0), m2::PointD(0.001, 0.001)};
auto const trackId = bmManager.SaveRoute(points, "London", "Paris");
auto const * track = bmManager.GetTrack(trackId);
TEST_EQUAL(track->GetName(), "London - Paris", ());
auto const line = track->GetData().m_geometry.m_lines[0];
std::vector const expectedLine = {{geometry::PointWithAltitude(m2::PointD(0.0, 0.0)), geometry::PointWithAltitude(m2::PointD(0.001, 0.001))}};
TEST_EQUAL(line, expectedLine, ());
}
} // namespace bookmarks_test } // namespace bookmarks_test

View File

@@ -1091,6 +1091,27 @@ void RoutingManager::SetUserCurrentPosition(m2::PointD const & position)
} }
} }
static std::string GetNameFromPoint(RouteMarkData const & rmd)
{
if (rmd.m_subTitle.empty())
return "";
return rmd.m_title;
}
void RoutingManager::SaveRoute()
{
auto points = GetRoutePolyline().GetPolyline().GetPoints();
auto const routePoints = GetRoutePoints();
std::string const from = GetNameFromPoint(routePoints.front());
std::string const to = GetNameFromPoint(routePoints.back());
// remove equal sequential points
points.erase(
std::unique(points.begin(), points.end(), [](const m2::PointD & p1, const m2::PointD & p2) { return AlmostEqualAbs(p1, p2, kMwmPointAccuracy); }),
points.end());
m_bmManager->SaveRoute(points, from, to);
}
bool RoutingManager::DisableFollowMode() bool RoutingManager::DisableFollowMode()
{ {
bool const disabled = m_routingSession.DisableFollowMode(); bool const disabled = m_routingSession.DisableFollowMode();

View File

@@ -142,6 +142,7 @@ public:
// This method was added because we do not want to break the behaviour that is familiar to our // This method was added because we do not want to break the behaviour that is familiar to our
// users. // users.
bool DisableFollowMode(); bool DisableFollowMode();
void SaveRoute();
void SetRouteBuildingListener(RouteBuildingCallback const & buildingCallback) void SetRouteBuildingListener(RouteBuildingCallback const & buildingCallback)
{ {
@@ -204,7 +205,7 @@ public:
return m_routingSession.GetTurnNotificationsLocale(); return m_routingSession.GetTurnNotificationsLocale();
} }
// @return polyline of the route. // @return polyline of the route.
routing::FollowedPolyline const & GetRoutePolylineForTests() const routing::FollowedPolyline const & GetRoutePolyline() const
{ {
return m_routingSession.GetRouteForTests()->GetFollowedPolyline(); return m_routingSession.GetRouteForTests()->GetFollowedPolyline();
} }

View File

@@ -4,7 +4,7 @@ namespace qt
{ {
void RoutingTurnsVisualizer::Visualize(RoutingManager & routingManager, df::DrapeApi & drape) void RoutingTurnsVisualizer::Visualize(RoutingManager & routingManager, df::DrapeApi & drape)
{ {
auto const & polyline = routingManager.GetRoutePolylineForTests().GetPolyline(); auto const & polyline = routingManager.GetRoutePolyline().GetPolyline();
auto const & turns = routingManager.GetTurnsOnRouteForTests(); auto const & turns = routingManager.GetTurnsOnRouteForTests();
for (auto const & turn : turns) for (auto const & turn : turns)