diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index bbb2bcd..5d6f3cc 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -145,6 +145,25 @@ class _HomeScreenState extends State with TickerProviderStateMixin { // Disable follow-me when editing a camera so the map doesn't jump around appState.setFollowMeMode(FollowMeMode.off); + final session = appState.editSession!; // should be non-null when this is called + + // Center map on the node being edited (same animation as openNodeTagSheet) + try { + _mapController.animateTo( + dest: session.originalNode.coord, + zoom: _mapController.mapController.camera.zoom, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } catch (_) { + // Map controller not ready, fallback to immediate move + try { + _mapController.mapController.move(session.originalNode.coord, _mapController.mapController.camera.zoom); + } catch (_) { + // Controller really not ready, skip centering + } + } + // Set transition flag to prevent map bounce _transitioningToEdit = true; @@ -152,8 +171,6 @@ class _HomeScreenState extends State with TickerProviderStateMixin { if (_tagSheetHeight > 0) { Navigator.of(context).pop(); } - - final session = appState.editSession!; // should be non-null when this is called // Small delay to let tag sheet close smoothly Future.delayed(const Duration(milliseconds: 150), () { diff --git a/lib/state/upload_queue_state.dart b/lib/state/upload_queue_state.dart index ceb5c88..619407d 100644 --- a/lib/state/upload_queue_state.dart +++ b/lib/state/upload_queue_state.dart @@ -61,8 +61,13 @@ class UploadQueueState extends ChangeNotifier { // Add a completed edit session to the upload queue void addFromEditSession(EditNodeSession session, {required UploadMode uploadMode}) { + // For constrained nodes, always use original position regardless of session.target + final coordToUse = session.originalNode.isConstrained + ? session.originalNode.coord + : session.target; + final upload = PendingUpload( - coord: session.target, + coord: coordToUse, direction: _formatDirectionsAsString(session.directions), profile: session.profile!, // Safe to use ! because commitEditSession() checks for null operatorProfile: session.operatorProfile, diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index bdf3252..1c79586 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -63,6 +63,7 @@ class MapViewState extends State { final Debouncer _cameraDebounce = Debouncer(kDebounceCameraRefresh); final Debouncer _tileDebounce = Debouncer(const Duration(milliseconds: 150)); final Debouncer _mapPositionDebounce = Debouncer(const Duration(milliseconds: 1000)); + final Debouncer _constrainedNodeSnapBack = Debouncer(const Duration(milliseconds: 100)); late final MapPositionManager _positionManager; late final TileLayerManager _tileManager; @@ -262,13 +263,13 @@ class MapViewState extends State { } /// Get interaction options for the map based on whether we're editing a constrained node. - /// Allows zoom and rotation but disables panning/dragging for constrained nodes. + /// Allows zoom and rotation but disables all forms of panning for constrained nodes. InteractionOptions _getInteractionOptions(EditNodeSession? editSession) { // Check if we're editing a constrained node if (editSession?.originalNode.isConstrained == true) { - // Constrained node: disable dragging/panning but keep zoom, rotate, etc. + // Constrained node: only allow pinch zoom and rotation, disable ALL panning return const InteractionOptions( - flags: InteractiveFlag.all & ~InteractiveFlag.drag, + flags: InteractiveFlag.pinchZoom | InteractiveFlag.rotate, scrollWheelVelocity: kScrollWheelVelocity, pinchZoomThreshold: kPinchZoomThreshold, pinchMoveThreshold: kPinchMoveThreshold, @@ -277,6 +278,7 @@ class MapViewState extends State { // Normal case: all interactions allowed return const InteractionOptions( + flags: InteractiveFlag.all, scrollWheelVelocity: kScrollWheelVelocity, pinchZoomThreshold: kPinchZoomThreshold, pinchMoveThreshold: kPinchMoveThreshold, @@ -577,7 +579,35 @@ class MapViewState extends State { appState.updateSession(target: pos.center); } if (editSession != null) { - appState.updateEditSession(target: pos.center); + // For constrained nodes, always snap back to original position + if (editSession.originalNode.isConstrained) { + final originalPos = editSession.originalNode.coord; + + // Always keep session target as original position + appState.updateEditSession(target: originalPos); + + // Only snap back if position actually drifted, and debounce to wait for gesture completion + if (pos.center.latitude != originalPos.latitude || pos.center.longitude != originalPos.longitude) { + _constrainedNodeSnapBack(() { + // Only animate if we're still in a constrained edit session and still drifted + final currentEditSession = appState.editSession; + if (currentEditSession?.originalNode.isConstrained == true) { + final currentPos = _controller.mapController.camera.center; + if (currentPos.latitude != originalPos.latitude || currentPos.longitude != originalPos.longitude) { + _controller.animateTo( + dest: originalPos, + zoom: _controller.mapController.camera.zoom, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 250), + ); + } + } + }); + } + } else { + // Normal unconstrained node - allow position updates + appState.updateEditSession(target: pos.center); + } } // Update provisional pin location during navigation search/routing