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

Click Tolerance for Markers #9640

Merged
merged 9 commits into from
Sep 2, 2020

Conversation

ChristopherChudzicki
Copy link
Contributor

@ChristopherChudzicki ChristopherChudzicki commented Apr 28, 2020

Implements a clickTolerance option for Markers, as discussed in #9636. Click tolerance is optionally set on a per-marker basis. If not set, the map's click tolerance is used.

Launch Checklist

  • briefly describe the changes in this PR
  • write tests for all new functionality
  • document any changes to public APIs
  • manually test the debug page
  • apply changelog label ('bug', 'feature', 'docs', etc) or use the label 'skip changelog'
  • tagged @mapbox/map-design-team @mapbox/static-apis if this PR includes style spec API or visual changes
  • add an entry inside this element for inclusion in the mapbox-gl-js changelog: <changelog></changelog>

Changelog Entry:

Added `clickTolerance` as an option for `Marker`s. Pointer movement below `clickTolerance` does not fire `Marker` drag events.

This is so that Marker instances can access map._clickTolerance if the Markers do not have their own click tolerances.
If the pointer movement ever exceeds (or equals) click tolerance, then the movement fires drag events, otherwise it fires click events.

Includes getClickTolerance and setClickTolerance methods. Marker clickTolerance is inherited from the map if not provided as an option.
@ChristopherChudzicki
Copy link
Contributor Author

@ryanhamley I believe that you described this issue as a bug in #9624. It could also be considered a feature and a breaking change. (Would be non-breaking if the default tolerance was 0px instead of inherited from map). Anyway, I guess all bug fixes are breaking changes. I'll let you all decide what to do with it.

As also discussed in #9624, I think there should be a separate issue / PR to suppress map click events when dragging markers. Currently, if pointer movement is ABOVE the marker click tolerance but BELOW the map click tolerance, then marker fires drag events AND map fires click events. That seems undesirable. (This really is a separate issue from this PR, and is currently present on master, where marker click tolerance is effectively 0px.)

Thanks for considering this PR, and for mapbox!

PS: The commit history in this PR looks a little out of order and I do not know why. It is in the correct order on my fork: https://github.com/ChristopherChudzicki/mapbox-gl-js/commits/marker-clicktol

src/ui/marker.js Outdated
@@ -36,6 +37,7 @@ type Options = {
* @param {string} [options.color='#3FB1CE'] The color to use for the default marker if options.element is not provided. The default is light blue.
* @param {number} [options.scale=1] The scale to use for the default marker if options.element is not provided. The default scale corresponds to a height of `41px` and a width of `27px`.
* @param {boolean} [options.draggable=false] A boolean indicating whether or not a marker is able to be dragged to a new position on the map.
* @param {?number} [options.clickTolerance=null] A number specifying the click tolerance in pixels. For draggable markers, movement must exceed this tolerance to be treated as a drag. A `null` value inherits click tolerance from `Map`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is click tolerance here? Is it pixels away from the marker that are still counted as clicking the marker or is a small drag while holding the mouse down within the tolerance is still counted as a click and not a drag?

They are two different things and I can see users getting confused which one this affects.

Copy link
Collaborator

@andrewharvey andrewharvey Apr 29, 2020

Choose a reason for hiding this comment

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

I see from the changelog entry this would be the latter. I think we should make this clearer in the description to avoid people getting confused it's the former.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that is a good clarification. I do still think it should be called clickTolerance because that is the name used for the Map option. (The ambiguity you allude to doesn't really exist for the map, though.)

I'll change the jsdoc description here to mirror the map description more closely:

The max number of pixels a user can shift the mouse pointer during a click for it to be considered a valid click (as opposed to a mouse drag).

@ChristopherChudzicki
Copy link
Contributor Author

@ryanhamley any progress on this?

Copy link
Contributor

@ryanhamley ryanhamley left a comment

Choose a reason for hiding this comment

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

Hi @ChristopherChudzicki thanks for submitting the PR and sorry for the delay in getting to it. This works but I believe the code could be simplified substantially to achieve the same effect. I've outlined a number of changes in specific code comments.

I'd also note that this behavior causes noticeable jumps when the click tolerance is more than a few pixels (in the gif below, the click tolerance is set to 100px). I'm not sure there's anyway around this if we desire this behavior, but I do want to make note of it because it begins to look like something is broken. @andrewharvey what do you think about this?

clicktolerance

src/ui/marker.js Outdated
@@ -36,6 +37,7 @@ type Options = {
* @param {string} [options.color='#3FB1CE'] The color to use for the default marker if options.element is not provided. The default is light blue.
* @param {number} [options.scale=1] The scale to use for the default marker if options.element is not provided. The default scale corresponds to a height of `41px` and a width of `27px`.
* @param {boolean} [options.draggable=false] A boolean indicating whether or not a marker is able to be dragged to a new position on the map.
* @param {?number} [options.clickTolerance=null] The max number of pixels a user can shift the mouse pointer during a click on the marker for it to be considered a valid click (as opposed to a marker drag).
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that we need to muck around with null and undefined values when 0 will work fine as a falsey value, especially since we want it to be a number. I also think it's misleading to say that the default tolerance is null or 0 when the default tolerance is actually whatever the map tolerance is, usually 3. We'll have to make a decision on what the default should be to more precisely word this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would love not to muck around with null/undefined types and I will plan to make this change. The reason I did not do it this way initially is that using 0 to mean "inherit from the map" means that the v1.11.0 behavior cannot be reproduced.

That said, the v1.11.0 behavior (where markers have 0 tolerance and map has 3px by default) seems undesirable to me, and people could always set the tolerance to be 0.00001.

src/ui/marker.js Outdated
@@ -58,8 +60,11 @@ export default class Marker extends Evented {
_scale: number;
_defaultMarker: boolean;
_draggable: boolean;
_clickTolerance: ?number;
Copy link
Contributor

Choose a reason for hiding this comment

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

If we change from null to 0, then we don't need this to be a Maybe type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I belive this is resolved.

src/ui/marker.js Outdated
@@ -86,6 +91,8 @@ export default class Marker extends Evented {
this._color = options && options.color || '#3FB1CE';
this._scale = options && options.scale || 1;
this._draggable = options && options.draggable || false;
this._clickTolerance = options && options.clickTolerance || null;
Copy link
Contributor

Choose a reason for hiding this comment

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

null -> 0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe this is resolved.

src/ui/marker.js Outdated
Comment on lines 493 to 507
/**
* Returns the marker's click tolerance. If the marker's `clickTolerance`
* has not been set, then returns the associated `Map`'s click tolerance. If
* there is no associated `Map`, returns 0.
* @returns {number} the 's click tolerance.
*/
getClickTolerance() {
if (this._clickTolerance !== null && this._clickTolerance !== undefined) {
return this._clickTolerance;
}
if (this._map) {
return this._map._clickTolerance;
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Changing the value to 0 eliminates the need for this function. We can just check this._clickTolerance || this._map_clickTolerance in the _onMove function. If this._clickTolerance is 0, we'll fallback to the map click tolerance and otherwise we'll use the marker's click tolerance. I don't think we need the ability to change the clickTolerance outside of the constructor options so we should choose the simpler API by not adding new methods without a good reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deleted this method.

src/ui/marker.js Outdated
Comment on lines 515 to 521
if (value === null || value === undefined) {
this._clickTolerance = null; // set to inherit from map.
}
const numeric = +value;
if (!Number.isNaN(numeric)) {
this._clickTolerance = numeric;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is overly complex with the null/undefined checks. By using 0 for false, we can just make this this._clickTolerance = value and value no longer has to be a Maybe type.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, going back to the discussion above, we may not need this at all. I'm not sure developers are really going to need/want to dynamically update the click tolerance after creation so we could probably remove this function entirely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deleted this method.

src/ui/marker.js Outdated

/**
* Set the `Marker`'s click tolerance.
* @param {number} value the click tolerance in pixels
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: value of the click tolerance in pixels

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deleted this method.

src/ui/marker.js Outdated
Comment on lines 524 to 528
_hasMovedBeyondClickTolerance(point: Point) {
const clickTolerance = this.getClickTolerance();
if (clickTolerance === null) return false;
return point.distSqr(this._pointerdownPos) >= clickTolerance ** 2;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is unnecessary. We can get the clickTolerance value as described in the getClickTolerance comment above and take the final line of this function (minus the return) and move it all directly into _onMove to simplify the API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deleted this method.

src/ui/marker.js Outdated
Comment on lines 531 to 535
if (!this._isDragging) {
this._isDragging = this._hasMovedBeyondClickTolerance(e.point);
}
if (!this._isDragging) return;

Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than worrying about keeping the _isDragging boolean in check, we can just check for the click tolerance directly. What we're attempting to do is check if the tolerance has been exceeded and if so, return.

 const clickTolerance = this._clickTolerance || this._map._clickTolerance;
 if (e.point.distSqr(this._pointerdownPos) <= clickTolerance ** 2) {
    return;
}

Copy link
Contributor Author

@ChristopherChudzicki ChristopherChudzicki Jun 11, 2020

Choose a reason for hiding this comment

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

@ryanhamley I think the _isDragging boolean is necessary. What we want to check is

  • "has pointer movement ever exceeded the threshhold while pointer down" not,
  • "is current pointer beyond threshhold".

The latter condition is checked by e.point.distSqr(this._pointerdownPos) <= clickTolerance ** 2) and results in jerky drag if the marker is dragged back to near where it started. The boolean lets us check the first condition.

src/ui/marker.js Outdated
_hasMovedBeyondClickTolerance(point: Point) {
const clickTolerance = this.getClickTolerance();
if (clickTolerance === null) return false;
return point.distSqr(this._pointerdownPos) >= clickTolerance ** 2;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to square these numbers?

Copy link
Contributor Author

@ChristopherChudzicki ChristopherChudzicki Jun 11, 2020

Choose a reason for hiding this comment

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

Computes the squared distance from this vector to v. If you are just comparing the distance with another distance, you should compare the distance squared instead as it is slightly more efficient to calculate.

That's from the ThreeJS documetnation for distanceToSquared (for 3d webgl stuff). (Compared squared distances avoids square roots.) Obviously Mapbox isn't threejs, but when I saw that Mapbox Point objects have a distSqr method, I figured Mapbox encouraged similar squaring.

Even though the onMove handler is called frequently, this is very likely a premature optimization. I'm certainly happy to remove it.

@@ -329,6 +329,41 @@ test('Popup anchors around default Marker', (t) => {
t.end();
});

test('Marker#getClickTolerance gets clickTolerance set via options', (t) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The tests will need to be reworked to remove calls to the simpler API.

@arindam1993 arindam1993 changed the base branch from master to main June 18, 2020 18:13
 - Make the tolerance numeric instead of ?numeric.
 - 0 marker clickTolerance now inherits from the map
 - remove get/set methods
@ChristopherChudzicki
Copy link
Contributor Author

@ryanhamley Sorry it took me so long to get back to this. I believe the most recent commit addressed all the comments, except #9640 (comment) for the reason described there.

Question for you: Your comment about keeping the API small hit home with me, and now I am wondering whether mapbox benefits much from marker-specific click tolerances at all. To be clear: I strongly believe marker dragging should respect SOME click tolerance. But maybe markers should ALWAYS use the map click tolerance, rather than having their own.

Pros:

  • simpler interface
  • probably what people actually want?

Cons:

  • a "more" breaking change, in that the existing behavior of 0-tolerance markers, nonzero-tolerance maps cannot be recovered.

If you think that ALWAYS using the map's click tolerance for markers is a better way to go, I am happy to make a separate, cleaner PR for that and I'll try to do it much sooner than I got around to making these changes.

@ChristopherChudzicki
Copy link
Contributor Author

@ryanhamley any thoughts on #9640 (comment), or if per-marker tolerance still seems worth while, any chance this might be reviewed again soon?

Copy link
Contributor

@ryanhamley ryanhamley left a comment

Choose a reason for hiding this comment

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

Hi @ChristopherChudzicki thanks so much for your diligence and patience. I think this looks great. I'm all for keeping the API minimal, but this is just adding one new option to the marker constructor so I think it strikes a nice balance between minimalism and flexibility. The tests look good too.

I think there should be a separate issue / PR to suppress map click events when dragging markers

If you want to submit a new PR to address this as well, I should have more bandwidth in the future than I did the past couple months and I'll gladly take a look.

Thanks again for the contribution and good work!

@ryanhamley ryanhamley merged commit 38f0072 into mapbox:main Sep 2, 2020
@ChristopherChudzicki
Copy link
Contributor Author

ChristopherChudzicki commented Sep 2, 2020

@ryanhamley Glad this got merged! Np about the reviews, I was slow with changes, too---life gets hectic. I'd be happy to submit another PR to supress the map clicks when dragging markers.

@nstele
Copy link

nstele commented May 6, 2021

Is there any similar property for Android SDK symbolManager? Have the same problem on Android draggable symbol

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.

4 participants