Skip to content

Commit 43156f6

Browse files
authored
Fixed: Event listener cancellation and hover handling on web. (#623)
Bug fixes: - Resolved issue where _map.off() wasn't properly removing event listeners because it wasn't passing the JavaScript function reference - Fixed issue where hovered event handling wasn't working propery with different layers on the map Added: - Parse layer ID from feature json object on web.
1 parent bebd63d commit 43156f6

File tree

6 files changed

+95
-68
lines changed

6 files changed

+95
-68
lines changed

maplibre_gl/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Added `onFeatureHover` to the controller for listening to hover interactions.
88
* Fixed: annotationConsumeTapEvents previously had no effect. This has been fixed, and it now properly controls whether tap events on annotations are consumed.
99
* Fixed: Avoided calling notifyListeners() on a disposed controller.
10+
* Fixed: Internal event listeners wasn't properly removed/off in web.
1011

1112
## [0.22.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.21.0...v0.22.0)
1213

maplibre_gl/lib/src/controller.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ typedef OnMapClickCallback = void Function(
88
Point<double> point, LatLng coordinates);
99

1010
typedef OnFeatureInteractionCallback = void Function(Point<double> point,
11-
LatLng coordinates, Annotation annotation, String? layerId);
11+
LatLng coordinates, Annotation annotation, String layerId);
1212

1313
typedef OnFeatureDragCallback = void Function(
1414
Point<double> point,

maplibre_gl_web/lib/src/geo/geojson.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,22 @@ class Feature extends JsObjectWrapper<FeatureJsImpl> {
3636

3737
String get source => jsObject.source;
3838

39+
String get layerId => jsObject.layer.id;
40+
3941
factory Feature({
4042
dynamic id,
4143
required Geometry geometry,
4244
Map<String, dynamic>? properties,
4345
String? source,
46+
String? layerId,
4447
}) =>
4548
Feature.fromJsObject(FeatureJsImpl(
4649
type: 'Feature',
4750
id: id,
4851
geometry: geometry.jsObject,
4952
properties: properties == null ? jsify({}) : jsify(properties),
5053
source: source,
54+
layer: layerId != null ? FeatureLayerJsImpl(id: layerId) : null,
5155
));
5256

5357
Feature copyWith({

maplibre_gl_web/lib/src/interop/geo/geojson_interop.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ class FeatureJsImpl {
2323
external GeometryJsImpl get geometry;
2424
external dynamic get properties;
2525
external String get source;
26+
external FeatureLayerJsImpl get layer;
2627
external factory FeatureJsImpl({
2728
dynamic id,
2829
String? type,
2930
GeometryJsImpl geometry,
3031
dynamic properties,
3132
String? source,
33+
FeatureLayerJsImpl? layer,
3234
});
3335
}
3436

@@ -42,3 +44,12 @@ class GeometryJsImpl {
4244
dynamic coordinates,
4345
});
4446
}
47+
48+
@JS()
49+
@anonymous
50+
class FeatureLayerJsImpl {
51+
external String get id;
52+
external factory FeatureLayerJsImpl({
53+
String id,
54+
});
55+
}

maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ class MapLibreMapController extends MapLibrePlatform
77
late Map<String, dynamic> _creationParams;
88
late MapLibreMap _map;
99
dynamic _draggedFeatureId;
10-
List<dynamic> _hoveredFeatureIds = [];
1110
LatLng? _dragOrigin;
1211
LatLng? _dragPrevious;
1312
bool _dragEnabled = true;
1413
final _addedFeaturesByLayer = <String, FeatureCollection>{};
14+
final _hoveredFeatureIdsByLayer = <String, List<dynamic>>{};
1515

1616
final _interactiveFeatureLayerIds = <String>{};
1717

@@ -109,7 +109,7 @@ class MapLibreMapController extends MapLibrePlatform
109109
await addImage(imagePath, bytes.buffer.asUint8List());
110110
}
111111

112-
_onMouseDown(Event e) {
112+
void _onMouseDown(Event e, String? layerId) {
113113
final isDraggable = e.features[0].properties['draggable'];
114114
if (isDraggable != null && isDraggable) {
115115
// Prevent the default map drag behavior.
@@ -528,6 +528,7 @@ class MapLibreMapController extends MapLibrePlatform
528528
'point': Point<double>(e.point.x.toDouble(), e.point.y.toDouble()),
529529
'latLng': LatLng(e.lngLat.lat.toDouble(), e.lngLat.lng.toDouble()),
530530
if (features.isNotEmpty) "id": features.first.id,
531+
if (features.isNotEmpty) "layerId": features.first.layerId,
531532
};
532533
if (features.isNotEmpty) {
533534
onFeatureTappedPlatform(payload);
@@ -782,9 +783,9 @@ class MapLibreMapController extends MapLibrePlatform
782783
void setStyle(dynamic styleObject) {
783784
//remove old mouseenter callbacks to avoid multicalling
784785
for (final layerId in _interactiveFeatureLayerIds) {
785-
_map.off('mouseenter', layerId, _onMouseEnterFeature);
786-
_map.off('mousemove', layerId, _onMouseMoveInFeature);
787-
_map.off('mouseleave', layerId, _onMouseLeaveFeature);
786+
_map.off('mouseenter', layerId, _handleLayerMouseMove);
787+
_map.off('mousemove', layerId, _handleLayerMouseMove);
788+
_map.off('mouseleave', layerId, _handleLayerMouseMove);
788789
if (_dragEnabled) _map.off('mousedown', layerId, _onMouseDown);
789790
}
790791
_interactiveFeatureLayerIds.clear();
@@ -1070,34 +1071,37 @@ class MapLibreMapController extends MapLibrePlatform
10701071

10711072
if (enableInteraction) {
10721073
_interactiveFeatureLayerIds.add(layerId);
1073-
_map.on('mouseenter', layerId, _onMouseEnterFeature);
1074-
_map.on('mousemove', layerId, _onMouseMoveInFeature);
1075-
_map.on('mouseleave', layerId, _onMouseLeaveFeature);
1074+
_map.on('mouseenter', layerId, _handleLayerMouseMove);
1075+
_map.on('mousemove', layerId, _handleLayerMouseMove);
1076+
_map.on('mouseleave', layerId, _handleLayerMouseMove);
10761077
if (_dragEnabled) _map.on('mousedown', layerId, _onMouseDown);
10771078
}
10781079
}
10791080

1080-
void _onMouseEnterFeature(Event e) {
1081-
if (_draggedFeatureId == null) {
1082-
_map.getCanvas().style.cursor = 'pointer';
1083-
}
1084-
_onFeatureHover(e);
1085-
}
1081+
void _handleLayerMouseMove(Event e, String layerId) {
1082+
final currentHoveredFeatures = e.features.map((f) => f.id).toList();
1083+
final lastHoveredFeatures = _hoveredFeatureIdsByLayer[layerId] ?? [];
1084+
final features = <String>{
1085+
...currentHoveredFeatures,
1086+
...lastHoveredFeatures
1087+
};
1088+
_hoveredFeatureIdsByLayer[layerId] = currentHoveredFeatures;
10861089

1087-
void _onFeatureHover(Event e) {
1088-
final currentFeatureIds = e.features.map((f) => f.id).toList();
1089-
final allFeatureIds = <String>{...currentFeatureIds, ..._hoveredFeatureIds};
1090-
for (final feature in allFeatureIds) {
1091-
final isCurrentlyHovered = currentFeatureIds.contains(feature);
1092-
final isPreviouslyHovered = _hoveredFeatureIds.contains(feature);
1090+
for (final feature in features) {
1091+
final isCurrentlyHovered = currentHoveredFeatures.contains(feature);
1092+
final isPreviouslyHovered = lastHoveredFeatures.contains(feature);
10931093
late final String eventType;
10941094
if (isCurrentlyHovered && isPreviouslyHovered) {
10951095
eventType = 'move';
10961096
} else if (isCurrentlyHovered && !isPreviouslyHovered) {
10971097
eventType = 'enter';
1098+
if (_draggedFeatureId == null) {
1099+
_map.getCanvas().style.cursor = 'pointer';
1100+
}
10981101
} else if (!isCurrentlyHovered && isPreviouslyHovered) {
10991102
eventType = 'leave';
11001103
}
1104+
11011105
onFeatureHoverPlatform({
11021106
'id': feature,
11031107
'point': Point<double>(e.point.x.toDouble(), e.point.y.toDouble()),
@@ -1106,16 +1110,14 @@ class MapLibreMapController extends MapLibrePlatform
11061110
});
11071111
}
11081112

1109-
_hoveredFeatureIds = currentFeatureIds;
1110-
}
1111-
1112-
void _onMouseMoveInFeature(Event e) {
1113-
_onFeatureHover(e);
1114-
}
1115-
1116-
void _onMouseLeaveFeature(Event e) {
1117-
_map.getCanvas().style.cursor = '';
1118-
_onFeatureHover(e);
1113+
final isAnyFeatureHovered = _hoveredFeatureIdsByLayer.values
1114+
.any((hoveredFeatures) => hoveredFeatures.isNotEmpty);
1115+
if (isAnyFeatureHovered && _draggedFeatureId == null) {
1116+
_map.getCanvas().style.cursor = 'pointer';
1117+
}
1118+
if (!isAnyFeatureHovered) {
1119+
_map.getCanvas().style.cursor = '';
1120+
}
11191121
}
11201122

11211123
@override

maplibre_gl_web/lib/src/util/evented.dart

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import 'dart:js';
22

33
import 'package:maplibre_gl_web/src/geo/geojson.dart';
44
import 'package:maplibre_gl_web/src/geo/lng_lat.dart';
5+
import 'package:maplibre_gl_web/src/geo/point.dart';
56
import 'package:maplibre_gl_web/src/interop/interop.dart';
67
import 'package:maplibre_gl_web/src/ui/control/geolocate_control.dart';
78
import 'package:maplibre_gl_web/src/ui/map.dart';
8-
import 'package:maplibre_gl_web/src/geo/point.dart';
99

1010
typedef Listener = dynamic Function(Event object);
1111
typedef GeoListener = dynamic Function(dynamic object);
12+
typedef LayerEventListener = dynamic Function(Event object, String layerId);
1213

1314
class Event extends JsObjectWrapper<EventJsImpl> {
1415
String get id => jsObject.id;
@@ -46,38 +47,51 @@ class Event extends JsObjectWrapper<EventJsImpl> {
4647
}
4748

4849
class Evented extends JsObjectWrapper<EventedJsImpl> {
50+
/// Store listener references so `off` can use the same one.
51+
/// Key is a composite of (eventType, layerIdOrListener.hashCode?, listener.hashCode)
52+
final _listeners = <String, dynamic>{};
53+
54+
/// Build a composite key (eventType::layerId::listenerHashCode).
55+
String _listenerKey(
56+
String type, dynamic layerIdOrListener, LayerEventListener? listener) {
57+
return '$type::${layerIdOrListener?.hashCode}::${listener?.hashCode}';
58+
}
59+
4960
/// Adds a listener to a specified event type.
5061
///
5162
/// @param {string} type The event type to add a listen for.
5263
/// @param {Function} listener The function to be called when the event is fired.
5364
/// The listener function is called with the data object passed to `fire`,
5465
/// extended with `target` and `type` properties.
5566
/// @returns {Object} `this`
56-
MapLibreMap on(String type, [dynamic layerIdOrListener, Listener? listener]) {
67+
MapLibreMap on(String type,
68+
[dynamic layerIdOrListener, LayerEventListener? listener]) {
69+
final ListenerJsImpl jsFn;
70+
final MapLibreMapJsImpl mapJsImpl;
5771
if (this is GeolocateControl && layerIdOrListener is GeoListener) {
58-
return MapLibreMap.fromJsObject(
59-
jsObject.on(type, allowInterop(
60-
(dynamic position) {
61-
layerIdOrListener(position);
62-
},
63-
)),
72+
jsFn = allowInterop(
73+
(dynamic position) {
74+
layerIdOrListener(position);
75+
},
6476
);
65-
}
66-
if (layerIdOrListener is Listener) {
67-
return MapLibreMap.fromJsObject(
68-
jsObject.on(type, allowInterop(
69-
(EventJsImpl object) {
70-
layerIdOrListener(Event.fromJsObject(object));
71-
},
72-
)),
77+
mapJsImpl = jsObject.on(type, jsFn);
78+
} else if (layerIdOrListener is Listener) {
79+
jsFn = allowInterop(
80+
(EventJsImpl object) {
81+
layerIdOrListener(Event.fromJsObject(object));
82+
},
7383
);
84+
mapJsImpl = jsObject.on(type, jsFn);
85+
} else {
86+
jsFn = allowInterop((EventJsImpl object) {
87+
listener!(Event.fromJsObject(object), layerIdOrListener);
88+
});
89+
mapJsImpl = jsObject.on(type, layerIdOrListener, jsFn);
7490
}
75-
return MapLibreMap.fromJsObject(
76-
jsObject.on(type, layerIdOrListener, allowInterop(
77-
(EventJsImpl object) {
78-
listener!(Event.fromJsObject(object));
79-
},
80-
)));
91+
92+
_listeners[_listenerKey(type, layerIdOrListener, listener)] = jsFn;
93+
94+
return MapLibreMap.fromJsObject(mapJsImpl);
8195
}
8296

8397
/// Removes a previously registered event listener.
@@ -86,22 +100,17 @@ class Evented extends JsObjectWrapper<EventedJsImpl> {
86100
/// @param {Function} listener The listener function to remove.
87101
/// @returns {Object} `this`
88102
MapLibreMap off(String type,
89-
[dynamic layerIdOrListener, Listener? listener]) {
90-
if (layerIdOrListener is Listener) {
91-
return MapLibreMap.fromJsObject(
92-
jsObject.off(type, allowInterop(
93-
(EventJsImpl object) {
94-
layerIdOrListener(Event.fromJsObject(object));
95-
},
96-
)),
97-
);
103+
[dynamic layerIdOrListener, LayerEventListener? listener]) {
104+
final key = _listenerKey(type, layerIdOrListener, listener);
105+
final jsFn = _listeners.remove(key);
106+
final MapLibreMapJsImpl mapJsImpl;
107+
108+
if (layerIdOrListener is Listener || layerIdOrListener is GeoListener) {
109+
mapJsImpl = jsObject.off(type, jsFn);
110+
} else {
111+
mapJsImpl = jsObject.off(type, layerIdOrListener, jsFn);
98112
}
99-
return MapLibreMap.fromJsObject(
100-
jsObject.off(type, layerIdOrListener, allowInterop(
101-
(EventJsImpl object) {
102-
listener!(Event.fromJsObject(object));
103-
},
104-
)));
113+
return MapLibreMap.fromJsObject(mapJsImpl);
105114
}
106115

107116
/// Adds a listener that will be called only once to a specified event type.

0 commit comments

Comments
 (0)