Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On Rebuild a DragMarkers position is reset to its Intial Position #18

Open
SheenaJacob opened this issue Aug 25, 2022 · 14 comments
Open

Comments

@SheenaJacob
Copy link

SheenaJacob commented Aug 25, 2022

Bug

Whenever a rebuild is triggered while dragging a DragMarker it returns to its initial position. The example below shows two such dragMarkers. Although the position of Marker1 (black) should change on dragging, rebuilding the widget causes it to always return to its initial position. Maker2 (red) on the other hand is continuously updated on Drag update which allows for it to be moved around, but when it is dragged outside the border of the map it sometimes gets stuck even though its still being dragged.

flutter_drag_marker_bug.mov

Expected Output:

A rebuild should not cause any change in the position of a Drag Marker while it is being dragged.

Reproducing the Bug:

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:flutter_map_dragmarker/dragmarker.dart';
import 'package:latlong2/latlong.dart';

void main() {
  runApp(const DragMarkerTest());
}

class DragMarkerTest extends StatefulWidget {
  const DragMarkerTest({Key? key}) : super(key: key);

  @override
  DragMarkerTestState createState() => DragMarkerTestState();
}

class DragMarkerTestState extends State<DragMarkerTest> {
  LatLng _marker1Position = LatLng(44.1461, 9.6400);
  LatLng _marker2Position = LatLng(44.1461, 9.6412);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        floatingActionButton: Text(' Marker1 Position : $_marker1Position \n Marker2 Position : $_marker2Position'),
        body: FlutterMap(
          options: MapOptions(
            allowPanningOnScrollingParent: false,
            plugins: [
              DragMarkerPlugin(),
            ],
            center: _marker1Position,
            zoom: 18,
          ),
          layers: [
            TileLayerOptions(
              urlTemplate:
              'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
              subdomains: ['a', 'b', 'c'],
            ),
            DragMarkerPluginOptions(
              markers: [
                DragMarker(
                  point: LatLng(44.1461, 9.6400),
                  width: 80.0,
                  height: 80.0,
                  offset: const Offset(0.0, -8.0),
                  builder: (ctx) =>
                  const Icon(Icons.location_on, size: 50),
                  feedbackBuilder: (ctx) =>
                  const Icon(Icons.edit_location, size: 50),
                  feedbackOffset: const Offset(0.0, -8.0),
                  onDragUpdate: (details, point) {
                    setState(() {
                      _marker1Position = point;
                    });
                  },
                ),
                DragMarker(
                  point: _marker2Position,
                  width: 80.0,
                  height: 80.0,
                  offset: const Offset(0.0, -8.0),
                  builder: (ctx) =>
                  const Icon(Icons.location_on, size: 50, color : Colors.red),
                  feedbackBuilder: (ctx) =>
                  const Icon(Icons.edit_location, size: 50, color: Colors.red),
                  feedbackOffset: const Offset(0.0, -8.0),
                  onDragUpdate: (details, point) {
                    setState(() {
                      _marker2Position = point;
                    });
                  },
                )
              ],
            ),
          ],
        ),
      ),
    );
  }
}
@ibrierley
Copy link
Owner

Thanks for this! Good spot and surprised I missed this. I haven't got a lot of time for the next week, but I will try and sort this when I get chance.

@ibrierley
Copy link
Owner

This may fix it for version 4 (readying for flutter_map v3), so I'll try and get this type of solution added there where I get chance. If you need a quick hack, look at the stuff with marker.point and markerPoint which is now saved by initState and amend anything with those in it.

It may be a few days before I get time to test this a bit more and push to Git and publish to pub.dev.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:flutter_map/plugin_api.dart';

class DragMarkers extends StatefulWidget {
  final List<DragMarker> markers;

  DragMarkers({Key? key, this.markers = const []});

  @override
  State<DragMarkers> createState() => _DragMarkersState();
}

class _DragMarkersState extends State<DragMarkers> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    var dragMarkers = <Widget>[];

    FlutterMapState? mapState = FlutterMapState.maybeOf(context);

    for (var marker in widget.markers) {
      if (!_boundsContainsMarker(mapState, marker)) continue;

      dragMarkers.add(DragMarkerWidget(
          mapState: mapState,
          marker: marker));
    }
    return Stack(children: dragMarkers);
  }

  static bool _boundsContainsMarker(FlutterMapState? map, DragMarker marker) {
    var pixelPoint = map!.project(marker.point);

    final width = marker.width - marker.anchor.left;
    final height = marker.height - marker.anchor.top;

    var sw = CustomPoint(pixelPoint.x + width, pixelPoint.y - height);
    var ne = CustomPoint(pixelPoint.x - width, pixelPoint.y + height);

    return map.pixelBounds.containsPartialBounds(Bounds(sw, ne));
  }
}

class DragMarkerWidget extends StatefulWidget {
  const DragMarkerWidget(
      {Key? key,
      this.mapState,
      required this.marker,
      AnchorPos? anchorPos})
      //: anchor = Anchor.forPos(anchorPos, marker.width, marker.height);
      : super(key: key);

  final FlutterMapState? mapState;
  //final Anchor anchor;
  final DragMarker marker;

  @override
  State<DragMarkerWidget> createState() => _DragMarkerWidgetState();
}

class _DragMarkerWidgetState extends State<DragMarkerWidget> {
  CustomPoint pixelPosition = const CustomPoint(0.0, 0.0);
  late LatLng dragPosStart;
  late LatLng markerPointStart;
  late LatLng oldDragPosition;
  bool isDragging = false;
  late LatLng markerPoint;

  static Timer? autoDragTimer;

  @override
  void initState() {
    markerPoint = widget.marker.point;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    DragMarker marker = widget.marker;
    updatePixelPos(markerPoint);

    bool feedBackEnabled = isDragging && marker.feedbackBuilder != null;
    Widget displayMarker = feedBackEnabled
        ? marker.feedbackBuilder!(context)
        : marker.builder!(context);

    return GestureDetector(
      onPanStart: marker.useLongPress ? null : onPanStart,
      onPanUpdate: marker.useLongPress ? null : onPanUpdate,
      onPanEnd: marker.useLongPress ? null : onPanEnd,
      onLongPressStart: marker.useLongPress ? onLongPanStart : null,
      onLongPressMoveUpdate: marker.useLongPress ? onLongPanUpdate : null,
      onLongPressEnd: marker.useLongPress ? onLongPanEnd : null,
      onTap: () {
        if (marker.onTap != null) {
          marker.onTap!(markerPoint);
        }
      },
      onLongPress: () {
        if (marker.onLongPress != null) {
          marker.onLongPress!(markerPoint);
        }
      },
      child: Stack(children: [
        Positioned(
            width: marker.width,
            height: marker.height,
            left: pixelPosition.x +
                ((isDragging) ? marker.feedbackOffset.dx : marker.offset.dx),
            top: pixelPosition.y +
                ((isDragging) ? marker.feedbackOffset.dy : marker.offset.dy),
            child: widget.marker.rotateMarker
                ? Transform.rotate(
                    angle: -widget.mapState!.rotationRad, child: displayMarker)
                : displayMarker)
      ]),
    );
  }

  void updatePixelPos(point) {
    DragMarker marker = widget.marker;
    FlutterMapState? mapState = widget.mapState;

    CustomPoint pos;
    if (mapState != null) {
      pos = mapState.project(point);
      pos =
          pos.multiplyBy(mapState.getZoomScale(mapState.zoom, mapState.zoom)) -
              mapState.pixelOrigin;

      pixelPosition = CustomPoint(
          (pos.x - (marker.width - widget.marker.anchor.left)).toDouble(),
          (pos.y - (marker.height - widget.marker.anchor.top)).toDouble());
    }
  }

  void _start(Offset localPosition) {
    isDragging = true;
    dragPosStart = _offsetToCrs(localPosition);
    markerPointStart =
        LatLng(markerPoint.latitude, markerPoint.longitude);
  }

  void onPanStart(DragStartDetails details) {
    _start(details.localPosition);
    DragMarker marker = widget.marker;
    if (marker.onDragStart != null) marker.onDragStart!(details, markerPoint);
  }

  void onLongPanStart(LongPressStartDetails details) {
    _start(details.localPosition);
    DragMarker marker = widget.marker;
    if (marker.onLongDragStart != null) {
      marker.onLongDragStart!(details, markerPoint);
    }
  }

  void _pan(Offset localPosition) {
    bool isDragging = true;
    DragMarker marker = widget.marker;
    FlutterMapState? mapState = widget.mapState;

    var dragPos = _offsetToCrs(localPosition);

    var deltaLat = dragPos.latitude - dragPosStart.latitude;
    var deltaLon = dragPos.longitude - dragPosStart.longitude;

    var pixelB = mapState?.getPixelBounds(mapState.zoom);    //getLastPixelBounds();
    var pixelPoint = mapState?.project(markerPoint);

    /// If we're near an edge, move the map to compensate.

    if (marker.updateMapNearEdge) {
      /// How much we'll move the map by to compensate

      var autoOffsetX = 0.0;
      var autoOffsetY = 0.0;
      if (pixelB != null && pixelPoint != null) {
        if (pixelPoint.x + marker.width * marker.nearEdgeRatio >=
            pixelB.topRight.x) autoOffsetX = marker.nearEdgeSpeed;
        if (pixelPoint.x - marker.width * marker.nearEdgeRatio <=
            pixelB.bottomLeft.x) autoOffsetX = -marker.nearEdgeSpeed;
        if (pixelPoint.y - marker.height * marker.nearEdgeRatio <=
            pixelB.topRight.y) autoOffsetY = -marker.nearEdgeSpeed;
        if (pixelPoint.y + marker.height * marker.nearEdgeRatio >=
            pixelB.bottomLeft.y) autoOffsetY = marker.nearEdgeSpeed;
      }

      /// Sometimes when dragging the onDragEnd doesn't fire, so just stops dead.
      /// Here we allow a bit of time to keep dragging whilst user may move
      /// around a bit to keep it going.

      var lastTick = 0;
      if (autoDragTimer != null) lastTick = autoDragTimer!.tick;

      if ((autoOffsetY != 0.0) || (autoOffsetX != 0.0)) {
        adjustMapToMarker(widget, autoOffsetX, autoOffsetY);

        if ((autoDragTimer == null || autoDragTimer?.isActive == false) &&
            (isDragging == true)) {
          autoDragTimer =
              Timer.periodic(const Duration(milliseconds: 10), (Timer t) {
            var tick = autoDragTimer?.tick;
            bool tickCheck = false;
            if (tick != null) {
              if (tick > lastTick + 15) {
                tickCheck = true;
              }
            }
            if (isDragging == false || tickCheck) {
              autoDragTimer?.cancel();
            } else {
              /// Note, we may have adjusted a few lines up in same drag,
              /// so could test for whether we've just done that
              /// this, but in reality it seems to work ok as is.

              adjustMapToMarker(widget, autoOffsetX, autoOffsetY);
            }
          });
        }
      }
    }

    setState(() {
      markerPoint = LatLng(markerPointStart.latitude + deltaLat,
          markerPointStart.longitude + deltaLon);
      updatePixelPos(markerPoint);
    });
  }

  void onPanUpdate(DragUpdateDetails details) {
    _pan(details.localPosition);
    DragMarker marker = widget.marker;
    if (marker.onDragUpdate != null) {
      marker.onDragUpdate!(details, markerPoint);
    }
  }

  void onLongPanUpdate(LongPressMoveUpdateDetails details) {
    _pan(details.localPosition);
    DragMarker marker = widget.marker;
    if (marker.onLongDragUpdate != null) {
      marker.onLongDragUpdate!(details, markerPoint);
    }
  }

  /// If dragging near edge of the screen, adjust the map so we keep dragging
  void adjustMapToMarker(DragMarkerWidget widget, autoOffsetX, autoOffsetY) {
    DragMarker marker = widget.marker;
    FlutterMapState? mapState = widget.mapState;

    var oldMapPos = mapState?.project(mapState.center);
    LatLng? newMapLatLng;
    CustomPoint<num>? oldMarkerPoint;
    if (oldMapPos != null) {
      newMapLatLng = mapState?.unproject(
          CustomPoint(oldMapPos.x + autoOffsetX, oldMapPos.y + autoOffsetY));
      oldMarkerPoint = mapState?.project(markerPoint);
    }
    if (mapState != null && newMapLatLng != null && oldMarkerPoint != null) {
      markerPoint = mapState.unproject(CustomPoint(
          oldMarkerPoint.x + autoOffsetX, oldMarkerPoint.y + autoOffsetY));

      mapState.move(newMapLatLng, mapState.zoom, source: MapEventSource.onDrag);
    }
  }

  void _end() {
    isDragging = false;
    if (autoDragTimer != null) autoDragTimer?.cancel();
  }

  void onPanEnd(details) {
    _end();
    if (widget.marker.onDragEnd != null) {
      widget.marker.onDragEnd!(details, markerPoint);
    }
    setState(() {}); // Needed if using a feedback widget
  }

  void onLongPanEnd(details) {
    _end();
    if (widget.marker.onLongDragEnd != null) {
      widget.marker.onLongDragEnd!(details, markerPoint);
    }
    setState(() {}); // Needed if using a feedback widget
  }

  static CustomPoint _offsetToPoint(Offset offset) {
    return CustomPoint(offset.dx, offset.dy);
  }

  LatLng _offsetToCrs(Offset offset) {
    // Get the widget's offset
    var renderObject = context.findRenderObject() as RenderBox;
    var width = renderObject.size.width;
    var height = renderObject.size.height;
    var mapState = widget.mapState;

    // convert the point to global coordinates
    var localPoint = _offsetToPoint(offset);
    var localPointCenterDistance =
        CustomPoint((width / 2) - localPoint.x, (height / 2) - localPoint.y);
    if (mapState != null) {
      var mapCenter = mapState.project(mapState.center);
      var point = mapCenter - localPointCenterDistance;
      return mapState.unproject(point);
    }
    return LatLng(0, 0);
  }
}

class DragMarker {
  LatLng point;
  final WidgetBuilder? builder;
  final WidgetBuilder? feedbackBuilder;
  final double width;
  final double height;
  final Offset offset;
  final Offset feedbackOffset;
  final bool useLongPress;
  final Function(DragStartDetails, LatLng)? onDragStart;
  final Function(DragUpdateDetails, LatLng)? onDragUpdate;
  final Function(DragEndDetails, LatLng)? onDragEnd;
  final Function(LongPressStartDetails, LatLng)? onLongDragStart;
  final Function(LongPressMoveUpdateDetails, LatLng)? onLongDragUpdate;
  final Function(LongPressEndDetails, LatLng)? onLongDragEnd;
  final Function(LatLng)? onTap;
  final Function(LatLng)? onLongPress;
  final bool updateMapNearEdge;
  final double nearEdgeRatio;
  final double nearEdgeSpeed;
  final bool rotateMarker;
  late Anchor anchor;

  DragMarker({
    required this.point,
    this.builder,
    this.feedbackBuilder,
    this.width = 30.0,
    this.height = 30.0,
    this.offset = const Offset(0.0, 0.0),
    this.feedbackOffset = const Offset(0.0, 0.0),
    this.useLongPress = false,
    this.onDragStart,
    this.onDragUpdate,
    this.onDragEnd,
    this.onLongDragStart,
    this.onLongDragUpdate,
    this.onLongDragEnd,
    this.onTap,
    this.onLongPress,
    this.updateMapNearEdge = false, // experimental
    this.nearEdgeRatio = 1.5,
    this.nearEdgeSpeed = 1.0,
    this.rotateMarker = true,
    AnchorPos? anchorPos,
  }) {
    anchor = Anchor.forPos(anchorPos, width, height);
  }
}

@SheenaJacob
Copy link
Author

Thanks for the quick response @ibrierley

@pablojimpas
Copy link
Collaborator

@SheenaJacob can you confirm if the problem is solved to close this issue?

@SheenaJacob
Copy link
Author

So the first part of the issue is now fixed, which means that a rebuild no longer triggers a change in the position of a marker while dragging.
I'm still facing an issue when dragging a marker whose position is constantly updated on onDragUpdate() outside the border though. At some point, the marker gets stuck when moving it outside the border even though the drag action is still taking place.

For example, in the video below there are two markers. In the first part of the video the black marker is dragged outside the border and I can bring it back without facing any problems. On the other hand, when dragging the red marker outside, the drag event can no longer be recognized and at 0:05 seconds you can see that the position displayed at the bottom is no longer updated even though I'm still moving the mouse. The difference between the two markers is that the black marker's position is not updated, while the red marker's position is updated every time the onDragUpdate is called.

Screen.Recording.2023-05-19.at.08.19.26.mov

The code for the example above is :

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:flutter_map_dragmarker/dragmarker.dart';
import 'package:latlong2/latlong.dart';

class DragMarkerBoundaryTest extends StatefulWidget {
  const DragMarkerBoundaryTest({super.key});

  @override
  State<DragMarkerBoundaryTest> createState() => _DragMarkerBoundaryTestState();
}

class _DragMarkerBoundaryTestState extends State<DragMarkerBoundaryTest> {
  final LatLng _marker1Position = LatLng(44.1461, 9.9000);
  LatLng _marker2Position = LatLng(44.1461, 10.1122);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        floatingActionButton: Text(
            ' Marker1 Position : $_marker1Position \n Marker2 Position : $_marker2Position'),
        body: Center(
          child: FlutterMap(
            options: MapOptions(
                absorbPanEventsOnScrollables: false,
                center: _marker1Position,
                zoom: 10.4,
                maxZoom: 18.0),
            children: [
              TileLayer(
                  urlTemplate:
                      'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
                  subdomains: const ['a', 'b', 'c']),
              DragMarkers(
                markers: [
                  DragMarker(
                    point: _marker1Position,
                    width: 80.0,
                    height: 80.0,
                    offset: const Offset(0.0, -8.0),
                    builder: (ctx) => const Icon(Icons.location_on,
                        size: 50, color: Colors.black),
                    feedbackBuilder: (ctx) => const Icon(Icons.edit_location,
                        size: 50, color: Colors.black),
                    feedbackOffset: const Offset(0.0, -8.0),
                  ),
                  DragMarker(
                    point: _marker2Position,
                    width: 80.0,
                    height: 80.0,
                    offset: const Offset(0.0, -8.0),
                    builder: (ctx) => const Icon(Icons.location_on,
                        size: 50, color: Colors.red),
                    feedbackBuilder: (ctx) => const Icon(Icons.edit_location,
                        size: 50, color: Colors.red),
                    feedbackOffset: const Offset(0.0, -8.0),
                    onDragUpdate: (details, point) {
                      setState(() {
                        _marker2Position = point;
                      });
                    },
                  )
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

Platform Tested on : macOS
flutter_map: ^3.1.0
flutter_map_dragmarker: ^4.1.2

I'm not sure if this is the intended behavior or if it is not recommended to update the position onDragUpdate, but in my use case, I need the position of the marker when it's being dragged.

@ibrierley
Copy link
Owner

Hmm I don't think that's a good idea in general (currently), it's not currently intended for that to be updated iirc. However, what's the use case, because it provides the location of the marker, so not quite sure why you need to update it whilst it's mid drag ?

@SheenaJacob
Copy link
Author

Ok. That makes sense. My current use case is to find the distance between two markers, and visually it would look something like this. So I need to update the position of the markers so that I can draw a line between the two points.

Screenshot 2023-05-19 at 10 04 26

@ibrierley
Copy link
Owner

Ok, you could try updating the point, but not calling setState on the map out of interest, see what happens. I think the problem is that when setState is called, the gesture handlers lose their dragging (That would need a bit longer to test all of that to check if I'm going mad or not :))

@ibrierley
Copy link
Owner

Actually, scrap that I think, as you would probably need setState to update the lines...

@ibrierley
Copy link
Owner

One thing you could do, is take a look at my other plugin flutter_map_line_editor, as it seems to sort of what you want as a by product, and not have the issue you have (when it goes offscreen). It is using an older version of dragmarker tho, so that may have some effect. I don't have a lot of time to debug further into that atm tho, but it may help spot where the difference is.

@SheenaJacob
Copy link
Author

Hehe. Yea. So I can use a workaround where I just have two variables that define the same point like this:

LatLng _markerPosition = LatLng(44.1461, 9.9000);
final LatLng _initialMarkerPosition = LatLng(44.1461, 9.9000);
DragMarker(
                    point: _initialMarkerPosition,
                    width: 80.0,
                    height: 80.0,
                    offset: const Offset(0.0, -8.0),
                    builder: (ctx) => const Icon(Icons.location_on,
                        size: 50, color: Colors.black),
                    feedbackBuilder: (ctx) => const Icon(Icons.edit_location,
                        size: 50, color: Colors.black),
                    feedbackOffset: const Offset(0.0, -8.0),
                    onDragUpdate: (details, point) {
                      setState(() {
                        _markerPosition = point;
                      });
                    },
                  ),

but I don't understand why updating the value causes the gesture callbacks to stop working and also it only happens when it's dragged outside.

@SheenaJacob
Copy link
Author

And I'll take a look at the flutter_map_line_editor. Thanks a ton : )

@pablojimpas
Copy link
Collaborator

And I'll take a look at the flutter_map_line_editor. Thanks a ton : )

Be aware that ibrierley/flutter_map_line_editor#36 might get merged, so that line editor reuses this package instead of its drag marker implementation and things can break for your use case.

@ibrierley
Copy link
Owner

Yep, I wasn't really meaning to use that, just to try and spot why that doesn't break the dragging off screen when calling setState in comparison.

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

No branches or pull requests

3 participants