Skip to content

Fix Attach to roads flows after Plan Route refactor - #5740

Open
aleksandr-tata wants to merge 17 commits into
r5.4from
task_5735_fix_attach_to_road
Open

Fix Attach to roads flows after Plan Route refactor#5740
aleksandr-tata wants to merge 17 commits into
r5.4from
task_5735_fix_attach_to_road

Conversation

@aleksandr-tata

@aleksandr-tata aleksandr-tata commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #5735.

This PR restores the Attach to the roads flows that regressed after the Plan Route refactor in #5484.

Manual Attach from Route Info could open an empty Plan Route screen with 0 ft and No points added yet. The same migration also lost part of the editing and navigation state previously passed by the 5.3 implementation.

Root cause

Route Info uses a synthesized in-memory GPX created from the calculated route. This GPX intentionally has no file path. After the refactor, the new Plan Route controller received only gpx.path, so it could not load the track.

The refactor also stopped forwarding the selected segment, navigation profile, follow-track mode, explicit approximation-warning request, viewport policy, and post-Apply navigation behavior.

Two active-navigation lifecycle cases also made the Attach button appear unresponsive:

  • Route Info dismissal completed after Plan Route had already been presented and restored the route-following layout over it.
  • Live or simulated route progress invalidated Route Info's cached in-memory GPX while the Attach banner remained visible.

Changes

  • Added an object-based Plan Route entry point for in-memory GPX data while preserving the existing file-path entry point for stored tracks.
  • Restored the selected GPX segment and active navigation profile when creating the editing context.
  • Preserved route-point profile precedence over the navigation-profile fallback.
  • Restored the explicit approximation warning for manual Attach, including GPX tracks without timestamps.
  • Restored follow-track Apply behavior:
    • update an active followed route in place;
    • return to Route Planning when navigation is not active.
  • Removed the implicit navigation-time GPX write. Navigation now uses the exported in-memory GPX unless the user explicitly saves it.
  • Restored track fit-to-bounds for normal Plan Route openings while preserving the existing viewport for Track Menu and My Places flows.
  • Restored editing of the pathless current recording and its Track Menu return state.
  • Deferred Plan Route presentation until Route Info has fully closed.
  • Rebuilt the synthesized Route Info GPX when live or simulated navigation progress invalidates its cached copy.
  • Added focused unit coverage for pathless GPX loading, selected-segment transfer, navigation-profile fallback, and route-point profile precedence.

Behavior intentionally unchanged

  • Detailed track guidance = Always continues to use the existing automatic attachment and recalculation flow.
  • The GPX approximation algorithm and GPX format are unchanged.
  • No localization strings were added or changed.
  • Stored tracks can still be opened through the existing file-path API.

Testing

Automated and static checks

  • OAPlanRouteEditingBridgeTest/testInMemoryGpxWithoutPathLoadsSelectedSegmentWithNavigationMode
  • OAPlanRouteEditingBridgeTest/testRoutePointProfileOverridesNavigationMode
  • Both focused tests passed with 0 assertion failures on a physical device.
  • Both focused tests passed with 0 assertion failures on an iPhone 17 Pro simulator running iOS 26.0.
  • Targeted Objective-C++ and Swift source checks passed for the changed Plan Route files.
  • SwiftLint passed for the changed Swift files.
  • plutil -lint OsmAnd.xcodeproj/project.pbxproj passed.
  • Scoped git diff --check passed.
  • The affected flows were compared with the 5.3 implementation.

The hosted test application can terminate after XCTest reports success: the physical-device run stopped in background map-style parsing, and the simulator reported QThread: Destroyed while thread is still running. Both test methods had already completed with 0 failures. The test-host shutdown behavior is separate from the assertions covered by this PR and still needs comparison with an unrelated focused test.

Manual verification

  • Route Info banner with Ask every time: Plan Route contains twisty-route.gpx, has a non-zero distance, and shows the approximation warning despite missing timestamps.
  • Navigation -> Settings -> Advanced -> Follow track -> Attach to the roads: the selected track opens and Apply returns to navigation.
  • Detailed track guidance = Always: automatic attachment finishes without opening an empty Plan Route screen.
  • Active navigation with simulation off and on: Attach opens after route progress, Apply updates the route, and navigation remains active without a Stop/Start cycle. Opening Attach has been verified in both states; the final post-Apply check with simulation remains pending.
  • Bicycle with twisty-route.gpx: Bicycle remains the navigation-profile fallback.
  • Bicycle with My_35_Km (1).gpx: the Car route-point profile takes precedence.

AI disclaimer

Implementation:

  • Tool / Agent: Codex
  • Model: GPT-5

Prompts used, summarized:

  • Analyze issue Attach to road no longer working #5735 and the regression introduced by Task 1323 plan route top toolbar #5484.
  • Compare the affected Attach flows with the 5.3 implementation.
  • Review the supplied GPX files, screenshots, videos, and external analysis.
  • Fix the affected navigation and Plan Route state handoff.
  • Add focused automated regression coverage and prepare the PR description.

Decisions made by the agent:

  • Restored an explicit GPX-object boundary instead of serializing the synthesized Route Info GPX only to reopen it by path.
  • Centralized editing-context construction so object- and path-based entry points share segment and profile semantics.
  • Split successful approximation Apply from generic popup dismissal so follow-track navigation resumes only after Apply.
  • Preserved the 5.3 viewport policy for each entry point.
  • Kept the automatic attachment algorithm unchanged.

Final review:

  • Tool / Agent: Codex
  • Model: GPT-5
  • Final diff reviewed

@aleksandr-tata aleksandr-tata linked an issue Sep 7, 2026 that may be closed by this pull request
@alex-dev-neo

Copy link
Copy Markdown
Contributor

Reviewed this against r5.4 and against the pre-refactor code (248425f2^). The restoration looks faithful: object-based entry point, forced snap warning that bypasses hasTimestamps, selectedSegment/appMode transfer with route-point profile precedence, and enterNavigationWithTrackName:followTrackMode: matching 5.3's runNavigation:appMode: branch for branch, including dropping the implicit GPX write. Two things I think still need attention before this ships in 5.4.

1. The Navigation menu item becomes a silent no-op (introduced by this PR)

enterNavigationWithTrackName:followTrackMode: now returns early on:

if (ctx == nil || (![ctx hasRoute] && ![ctx hasChanges]))
    return;

This restores the 5.3 guard, but hasRoute is orderedRoadSegmentData.count > 0, and _roadSegmentData is only populated for points that carry a profile. For a plain track opened from Track Menu with no edits and no routed segments, both conditions are false.

PlanRouteMenuAction.navigation is still shown and enabled in that state, so the user taps Navigation and nothing happens, with no feedback. In 5.4 as shipped this path works, since the old guard was only ctx == nil; 5.3 at least logged. Suggest either a toast or disabling the menu item when the guard would reject.

2. + POI is inert and Save ignores the track folder in the Route Info attach flow

openExistingTrack(gpxFile:fileName:showSnapWarning:) passes filePath = gpxFile.path.isEmpty ? nil : gpxFile.path, which is nil for the synthesized Route Info GPX. In PlanRouteEditingContextDataProvider that means:

  • openAddPoi(from:) returns early on guard mode.isNewRoute || (filePath?.isEmpty == false), so the + POI button in the bottom toolbar does nothing;
  • editTrackFolder returns nil, so the save dialog does not pre-select the followed track's folder and defaults to the GPX root.

This is pre-existing in 5.4 rather than introduced here, but it sits in exactly the flow this PR restores. Passing _routingHelper.getCurrentGPXRoute.file.path alongside the object would cover both.

@alex-dev-neo

alex-dev-neo commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Remaining #5484 regressions this branch does not cover yet, continuing the numbering from my previous comment. The first one below is the only item in this set with irreversible consequences; the other two are repository cleanup.

3. Data already lost to the implicit GPX save

The 5.4 implementation wrote <gpxDir>/<mode.title>.gpx on every Navigation from Plan Route: no uniqueness check, no folder handling, no OASKException check, no addGPXFileToDBIfNeeded, and — before this PR restores it — no hasRoute || hasChanges guard, so it fired even when nothing had been edited.

  • For .newRoute the name is always the localized New route, so the second use silently overwrote the result of the first.
  • For .editTrack the name is the bare file name, so a track in Trips/route.gpx produced a second route.gpx in the GPX root and overwrote any unrelated track already sitting there.

Dropping the write in enterNavigationWithTrackName:followTrackMode: is the right fix going forward, and restoring the guard closes the "wrote even with no changes" case. Neither can undo what 5.4.0 and 5.4.1 have been doing on devices since early August. Overwritten tracks are gone. The files that were created are not in the GPX database, and GpxDbHelper.reconcileFilesystem() only walks database to disk — it drops entries whose files vanished and re-analyses modified ones, but never adopts unknown files found on disk. So the leftovers are invisible in My Places and cannot be deleted from inside the app.

Those leftovers need handling here too: either register them in the GPX database so they become visible and removable in My Places, or delete them on migration.

4. Orphaned OARoutePlanningHudViewController.mm

#5484 deleted OARoutePlanningHudViewController.h and .xib and dropped the file from the target, but left OARoutePlanningHudViewController.mm (~2000 lines) in the repository. It is still there on this branch, and git grep OARoutePlanningHudViewController over Sources/ and project.pbxproj returns nothing outside the file itself. Since it is now the only surviving record of the pre-refactor behaviour, it keeps surfacing in searches for "how this used to work". Suggest deleting it as part of this cleanup.

5. OAReorderSegmentCommand added twice

#5484 added the same command in two places:

  • Sources/Controllers/RoutePlanning/OAReorderSegmentCommand.{h,mm}
  • Sources/Controllers/RoutePlanning/Commands/OAReorderSegmentCommand.{h,mm}

The files are byte-identical and only the Commands/ copy is referenced from project.pbxproj. Both copies are still present on this branch; the stray pair under RoutePlanning/ should go.

@aleksandr-tata

Copy link
Copy Markdown
Contributor Author

PR #5740 — Changes after review

Related issue / task

Fixes #5735

PR: #5740

Summary

This update addresses the findings raised after the previous review of PR #5740.

Review range:

  • Previous review baseline: 7b33713aa (Fix Attach to roads during active navigation)

The follow-up changes:

  • preserve the original GPX source identity without assigning a file path to the synthesized in-memory GPX;
  • separate the Attach to the roads Apply action from generic Plan Route Navigation;
  • restore the pre-refactor Navigation branches for unchanged, single-point, and non-routed tracks;
  • preserve the current or embedded navigation-profile fallback when the editing context contains the DEFAULT sentinel;
  • keep the selected segment and navigation-profile fallback when opening Plan Route;
  • restore waypoint editing and source-folder selection for pathless Attach tracks;
  • prevent implicit GPX writes and accidental creation of New route.gpx during navigation;
  • remove obsolete Plan Route files left by the refactor;
  • add focused regression tests for the restored behavior.

Review feedback addressed

Source GPX identity

The GPX created by Route Info remains an in-memory object with an empty gpx.path. The original track path is now passed independently as sourceFilePath.

The source path is used only where the workflow needs the identity of the original track:

  • active Follow Track navigation;
  • inactive Route Planning;
  • waypoint editing;
  • initial folder selection in the Save dialog.

The source path is not copied into the editable in-memory GPX, and the source GPX is not implicitly overwritten.

Related commits:

  • 3b5cd017fPreserve source track identity in Attach to roads flow
  • 610fefd59Preserve source GPX path during active Follow Track
  • bd4814660Preserve source GPX path when entering Route Planning

Attach Apply and generic Navigation

Attach to the roads Apply and the generic Navigation action now use separate bridge operations.

The Attach Apply path keeps the hasRoute || hasChanges precondition from the 5.3 Follow Track flow. It reports an explicit failure reason when the context, approximation result, export, or transition is invalid. Plan Route closes only after a successful transition.

The generic Navigation action does not use the Attach-only guard. An unchanged plain track can enter Route Planning again.

Related commits:

  • 7a81ff1fdSeparate Attach apply from Plan Route navigation
  • ab662ff2aHandle Attach apply failures without closing Plan Route
  • 2e7fb0db5Fix generic Plan Route navigation for unchanged tracks

Restored Navigation decisions

The pre-refactor branches required by generic Navigation were restored:

  • exactly one point is handled as a destination through navigateToPoint;
  • a multi-point track without routed segments presents approximation before Navigation when approximation is required;
  • after approximation is applied, the pending Navigation action resumes;
  • cancelling approximation clears the pending action;
  • the single-point branch has priority over approximation.

Before generic Navigation enters Route Planning, OAApplicationMode.DEFAULT is normalized to nil. This preserves the existing resolution order in OAMapActions: an embedded GPX route profile is used when present; otherwise, the currently selected navigation profile remains active. Explicit non-default editing modes are passed through unchanged.

Related commits:

  • b2fefbe22Fix single-point Plan Route navigation
  • ae2ddbfd7Restore approximation before Plan Route navigation

Waypoints and Save folder

+ POI now resolves the original source path while the editable Attach GPX remains pathless. The added waypoint stays in draft state until the user explicitly saves. Discarding Plan Route leaves the original GPX unchanged.

The Save dialog now preselects the original track folder. A source track in the GPX root continues to use the root folder.

Related commits:

  • 505a32201Fix adding POIs to pathless Attach tracks
  • 4cafd0ce0Preserve source folder for pathless Attach tracks

Refactor cleanup

Removed the unreferenced OARoutePlanningHudViewController.mm and the duplicate OAReorderSegmentCommand.{h,mm} outside the live Commands directory.

Related commits:

  • 3c396bfbcRemove obsolete route planning controller
  • faabbc21dRemove duplicate reorder segment command

Testing

Tested on

  • Device / Simulator: physical iPhone
  • iOS: 26.x

Automated tests

OAPlanRouteEditingBridgeTest:

Executed 11 tests, with 0 failures (0 unexpected)

Coverage includes:

  • loading a pathless in-memory GPX;
  • selected-segment and navigation-profile fallback;
  • route-point profile precedence;
  • source-path resolution without changing gpx.path;
  • Attach Apply preconditions and failure results;
  • generic Navigation for an unchanged track;
  • single-point Navigation;
  • approximation decision precedence;
  • normalization of the DEFAULT editing mode before generic Navigation;
  • preservation of an explicit non-default editing mode.

PlanRouteTrackSourceTests:

Executed 6 tests, with 0 failures (0 unexpected)

Coverage includes:

  • separation of sourceFilePath and the editable GPX path;
  • waypoint editing through the source path;
  • source Save-folder preservation;
  • GPX-root folder behavior;
  • fallback to a real gpx.path;
  • pathless current or draft GPX handling.

Both suites were run on a physical iPhone. The existing duplicate-runtime-class and QThread host warnings appeared outside the test assertions, after the suites had reported success.

In total, the two focused suites executed 17 tests with 0 failures.

Manual scenarios

  • Route Info → Attach to the roads with Detailed track guidance = Ask every time:
    • twisty-route.gpx opens with its points and a non-zero distance;
    • the approximation warning appears even though the track has no timestamps;
    • Apply returns to Navigation with calculated road geometry.
  • Follow Track bottom sheet → Attach to the roads opens the selected track and completes Apply.
  • Detailed track guidance = Always completes automatic attachment without an empty Plan Route screen.
  • Active Follow Track remains active and retains the original source GPX after Apply.
  • Inactive Attach Apply returns to Route Planning without creating New route.gpx.
  • A selected segment from a multi-segment GPX opens without artificial straight lines between all segments.
  • A GPX without a route-point profile retains the selected navigation profile.
  • A route-point profile takes precedence when present.
  • Generic Navigation with a DEFAULT editing context and no embedded profile falls back to the currently selected Bicycle profile.
  • My_35_Km (1).gpx retains its embedded Car profile while Bicycle is selected globally.
  • Generic Navigation works for an unchanged plain track.
  • A one-point Plan Route becomes a regular destination route.
  • Generic Navigation presents approximation when required and resumes after Apply.
  • + POI opens for a pathless Attach track; Discard leaves the original five waypoints unchanged.
  • Save preselects the source track's import folder.
  • No temporary Issue5735 diagnostic logs remain in the changed Plan Route files.

Relevant checks

  • Physical-device tests passed
  • Ask every time and Always guidance modes tested
  • Active and inactive navigation tested
  • Different navigation profiles tested
  • DEFAULT, explicit, and embedded profile resolution tested
  • Multi-segment GPX tested
  • Source GPX data preservation checked
  • Large-track approximation completed successfully
  • Light and dark appearance observed
  • Portrait orientation tested

AI disclaimer

Implementation:

  • Tool / Agent: Codex
  • Model: GPT-5

Prompts used (summarised):

  • Analyze issue Attach to road no longer working #5735, the review feedback, and the affected Plan Route behavior.
  • Compare the current implementation with the corresponding behavior in version 5.3.
  • Help evaluate device logs and recordings collected while reproducing each reported regression.
  • Prepare focused implementation changes and regression tests for the confirmed cases.
  • Summarize the completed fixes and verification results for the PR update.

Decided by the agent, not requested explicitly:

  • Recommended keeping the original GPX identity in a separate sourceFilePath instead of mutating the pathless in-memory GPX.
  • Recommended separate intents for Attach Apply and generic Navigation to preserve their different preconditions.
  • Recommended an explicit Attach Apply result so the UI closes only after a successful transition.
  • Recommended extracting deterministic source-resolution and Navigation-decision logic for focused regression tests.

Final review:

  • Tool / Agent: Developer review with Codex assistance
  • Model: GPT-5
  • Confirmed cases were reproduced before the corresponding fixes
  • Automated test results were reviewed
  • Fixed scenarios were manually verified on a physical device
  • Final post-review diff was reviewed by the developer
  • Temporary diagnostic logs were removed

Significant findings:

  • Verified that a pathless synthesized GPX and the original source-track identity are separate concepts.
  • Verified that the Attach-only hasRoute || hasChanges guard must not block generic Navigation.
  • Verified the missing single-point and approximation branches against the pre-refactor flow.
  • Verified that the DEFAULT editing sentinel must be normalized before entering Route Planning so the existing profile fallback remains active.
  • Verified that waypoint edits remain draft-only until an explicit Save.
  • Verified that the source folder is restored without assigning a fake path to the editable GPX.

@alex-dev-neo

Copy link
Copy Markdown
Contributor

Re-reviewed the eight commits since 7b33713aa.

Items 1, 2, 4 and 5 are confirmed fixed. Splitting Attach Apply from generic Navigation is the right shape: the Attach path keeps the hasRoute || hasChanges precondition and now surfaces a failure instead of doing nothing, and the generic path no longer inherits it. PlanRouteTrackSource covers both + POI and the Save folder. Routing sourceFilePath into setGPXRouteParamsWithDocument:path: is better than the 5.3 behaviour it restores, since followTheGpxRoute now gets a real path instead of an empty string.

Item 3 is half done: the write is gone, but the files 5.4.0/5.4.1 already created on devices are still unreferenced by the GPX database and unreachable from inside the app. Nothing in the branch adopts or removes them.

One more #5484 regression surfaced while comparing the restored generic Navigation against 5.3.

6. Multi-point route with no calculated route follows the straight line instead of routing between points

onNavigationButtonPressed in the pre-refactor controller had three branches for points.count > 1, and they used deliberately different GPX shapes:

if (_editingContext.hasRoute) {
    OASGpxFile *gpx = [_editingContext exportGpx:trackName];   // trk segments + rte points
    ...
} else if (_editingContext.isApproximationNeeded) {
    [self enterApproximationMode];
} else {
    OASGpxFile *gpx = [[OASGpxFile alloc] initWithAuthor:...];
    [gpx addRoutePointsPoints:points addRoute:NO];             // rte points only, no track
    [targetPointsHelper clearAllPoints:NO];
    ...
}

The current branch routes all non-single-point cases through navigationGpxWithEditingContextexportGpx, which always produces a track (OARouteExporter exportRoute: adds trkSegments to a track and then addRoutePointsPoints:addRoute:YES). That changes what setGPXRouteParamsWithDocument: decides:

if ([doc hasRtePt] && ![doc hasTrkPt])
    [_settings.gpxCalculateRtept set:YES];
else
    [_settings.gpxCalculateRtept set:NO];
  • 5.3 third branch: rte points only, no track → gpxCalculateRtept = YES → the router connects the points.
  • Current: track present → gpxCalculateRtept = NO → navigation follows the drawn line as a track.

So a freshly drawn multi-point route with no calculated route and no approximation required now navigates along the straight segments rather than being routed between the points. The 5.3 branch also called clearAllPoints:NO before entering route planning, which the current path does not.

Restoring the third branch — rte-only GPX plus clearAllPoints:NO when !hasRoute && !isApproximationNeeded — should bring this back.

Minor

With zero points, exportGpx: returns nil, so Navigation now ends in the generic unexpected_error_occurred_warn alert. That state is predictable rather than unexpected; either disable the menu item when there are no points or use a message that says so.

@tigrim tigrim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review: two inline comments added.

@@ -0,0 +1,265 @@
#import <XCTest/XCTest.h>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this test written in Swift?

import XCTest

final class PlanRouteTrackSourceTests: XCTestCase {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private let gpxDirectory = "/Documents/GPX"

OASGpxFile *gpx = _gpx;
NSString *fileName = gpx.path.lastPathComponent.stringByDeletingPathExtension;
[self dismissViewControllerAnimated:NO completion:^{
[[OARootViewController instance].mapPanel closeRouteInfo];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] closeRouteInfo animates for 0.2 seconds and only then runs OARouteInfoView.onDismiss, but Plan Route is presented immediately after this call. During active navigation, that delayed onDismiss can still call switchToRouteFollowingLayout over the newly presented Plan Route screen—the same lifecycle race fixed in the direct Route Info entry point. Please present Plan Route from closeRouteInfo:YES onComplete: here as well.

dataProvider.fitTrackOnMap(bottomInset: isLeftSidePresentation() ? 0 : getViewHeight(),
leftInset: isLeftSidePresentation() ? getLandscapeViewWidth() : 0)
}
if !hasPresentedInitialSnapWarning

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] The edit button and the gpx_route row both call openPlanRoute(false), but the object-based entry point always creates this controller with followTrackMode == true. Consequently, every unattached track forces this approximation popup even when showSnapWarning is false; applying it then calls applyAttachedTrackToNavigation and closes the editor. This changes the ordinary Edit Track flow into Attach. Please gate the forced initial popup on showSnapWarning, or pass a separate flag that distinguishes editing from Attach.

dataProvider.onPointEditModeRequested = { [weak self] mode in
self?.showPointEditingView(mode: mode)
}
dataProvider.onApproximationApplied = { [weak self] in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Inside DataProvider
DispatchQueue.main.async {
self.onApproximationApplied?()
}

self?.handleApproximationApplied()
}
}
dataProvider.onApproximationPopupDismissed = { [weak self] in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

up

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Attach to road no longer working

3 participants