map: update clusters when idle, fixed cluster entry selection
This commit is contained in:
parent
1f192e58f2
commit
42425f6fcf
5 changed files with 147 additions and 124 deletions
|
@ -70,6 +70,7 @@ class Durations {
|
||||||
static const searchDebounceDelay = Duration(milliseconds: 250);
|
static const searchDebounceDelay = Duration(milliseconds: 250);
|
||||||
static const contentChangeDebounceDelay = Duration(milliseconds: 1000);
|
static const contentChangeDebounceDelay = Duration(milliseconds: 1000);
|
||||||
static const mapScrollDebounceDelay = Duration(milliseconds: 150);
|
static const mapScrollDebounceDelay = Duration(milliseconds: 150);
|
||||||
|
static const mapIdleDebounceDelay = Duration(milliseconds: 100);
|
||||||
|
|
||||||
// app life
|
// app life
|
||||||
static const lastVersionCheckInterval = Duration(days: 7);
|
static const lastVersionCheckInterval = Duration(days: 7);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import 'package:aves/widgets/common/map/google/map.dart';
|
||||||
import 'package:aves/widgets/common/map/leaflet/map.dart';
|
import 'package:aves/widgets/common/map/leaflet/map.dart';
|
||||||
import 'package:aves/widgets/common/map/marker.dart';
|
import 'package:aves/widgets/common/map/marker.dart';
|
||||||
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:fluster/fluster.dart';
|
import 'package:fluster/fluster.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -83,9 +84,12 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
final onTap = widget.onMarkerTap;
|
final onTap = widget.onMarkerTap;
|
||||||
if (onTap == null) return;
|
if (onTap == null) return;
|
||||||
|
|
||||||
final geoEntries = <GeoEntry>[];
|
|
||||||
final clusterId = geoEntry.clusterId;
|
final clusterId = geoEntry.clusterId;
|
||||||
if (clusterId != null) {
|
Set<AvesEntry> getClusterEntries() {
|
||||||
|
if (clusterId == null) {
|
||||||
|
return {geoEntry.entry!};
|
||||||
|
}
|
||||||
|
|
||||||
var points = _defaultMarkerCluster.points(clusterId);
|
var points = _defaultMarkerCluster.points(clusterId);
|
||||||
if (points.length != geoEntry.pointsSize) {
|
if (points.length != geoEntry.pointsSize) {
|
||||||
// `Fluster.points()` method does not always return all the points contained in a cluster
|
// `Fluster.points()` method does not always return all the points contained in a cluster
|
||||||
|
@ -94,11 +98,20 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
points = _slowMarkerCluster!.points(clusterId);
|
points = _slowMarkerCluster!.points(clusterId);
|
||||||
assert(points.length == geoEntry.pointsSize, 'got ${points.length}/${geoEntry.pointsSize} for geoEntry=$geoEntry');
|
assert(points.length == geoEntry.pointsSize, 'got ${points.length}/${geoEntry.pointsSize} for geoEntry=$geoEntry');
|
||||||
}
|
}
|
||||||
geoEntries.addAll(points);
|
return points.map((geoEntry) => geoEntry.entry!).toSet();
|
||||||
} else {
|
}
|
||||||
geoEntries.add(geoEntry);
|
|
||||||
|
AvesEntry? markerEntry;
|
||||||
|
if (clusterId != null) {
|
||||||
|
final uri = geoEntry.childMarkerId;
|
||||||
|
markerEntry = entries.firstWhereOrNull((v) => v.uri == uri);
|
||||||
|
} else {
|
||||||
|
markerEntry = geoEntry.entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markerEntry != null) {
|
||||||
|
onTap(markerEntry, getClusterEntries);
|
||||||
}
|
}
|
||||||
onTap(geoEntries.map((geoEntry) => geoEntry.entry!).toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return FutureBuilder<bool>(
|
return FutureBuilder<bool>(
|
||||||
|
@ -110,7 +123,7 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
builder: (context, mapStyle, child) {
|
builder: (context, mapStyle, child) {
|
||||||
final isGoogleMaps = mapStyle.isGoogleMaps;
|
final isGoogleMaps = mapStyle.isGoogleMaps;
|
||||||
final progressive = !isGoogleMaps;
|
final progressive = !isGoogleMaps;
|
||||||
Widget _buildMarker(MarkerKey key) => ImageMarker(
|
Widget _buildMarkerWidget(MarkerKey key) => ImageMarker(
|
||||||
key: key,
|
key: key,
|
||||||
entry: key.entry,
|
entry: key.entry,
|
||||||
count: key.count,
|
count: key.count,
|
||||||
|
@ -127,9 +140,8 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
minZoom: 0,
|
minZoom: 0,
|
||||||
maxZoom: 20,
|
maxZoom: 20,
|
||||||
style: mapStyle,
|
style: mapStyle,
|
||||||
markerBuilder: _buildMarker,
|
markerClusterBuilder: _buildMarkerClusters,
|
||||||
markerCluster: _defaultMarkerCluster,
|
markerWidgetBuilder: _buildMarkerWidget,
|
||||||
markerEntries: entries,
|
|
||||||
onUserZoomChange: widget.onUserZoomChange,
|
onUserZoomChange: widget.onUserZoomChange,
|
||||||
onMarkerTap: _onMarkerTap,
|
onMarkerTap: _onMarkerTap,
|
||||||
)
|
)
|
||||||
|
@ -140,9 +152,8 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
minZoom: 2,
|
minZoom: 2,
|
||||||
maxZoom: 16,
|
maxZoom: 16,
|
||||||
style: mapStyle,
|
style: mapStyle,
|
||||||
markerBuilder: _buildMarker,
|
markerClusterBuilder: _buildMarkerClusters,
|
||||||
markerCluster: _defaultMarkerCluster,
|
markerWidgetBuilder: _buildMarkerWidget,
|
||||||
markerEntries: entries,
|
|
||||||
markerSize: Size(
|
markerSize: Size(
|
||||||
GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2,
|
GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2,
|
||||||
GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2 + GeoMap.pointerSize.height,
|
GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2 + GeoMap.pointerSize.height,
|
||||||
|
@ -232,6 +243,19 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat),
|
createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<MarkerKey, GeoEntry> _buildMarkerClusters() {
|
||||||
|
final bounds = _boundsNotifier.value;
|
||||||
|
final geoEntries = _defaultMarkerCluster.clusters(bounds.boundingBox, bounds.zoom.round());
|
||||||
|
return Map.fromEntries(geoEntries.map((v) {
|
||||||
|
if (v.isCluster!) {
|
||||||
|
final uri = v.childMarkerId;
|
||||||
|
final entry = entries.firstWhere((v) => v.uri == uri);
|
||||||
|
return MapEntry(MarkerKey(entry, v.pointsSize), v);
|
||||||
|
}
|
||||||
|
return MapEntry(MarkerKey(v.entry!, null), v);
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -245,6 +269,7 @@ class MarkerKey extends LocalKey with EquatableMixin {
|
||||||
const MarkerKey(this.entry, this.count);
|
const MarkerKey(this.entry, this.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef EntryMarkerBuilder = Widget Function(MarkerKey key);
|
typedef MarkerClusterBuilder = Map<MarkerKey, GeoEntry> Function();
|
||||||
|
typedef MarkerWidgetBuilder = Widget Function(MarkerKey key);
|
||||||
typedef UserZoomChangeCallback = void Function(double zoom);
|
typedef UserZoomChangeCallback = void Function(double zoom);
|
||||||
typedef MarkerTapCallback = void Function(List<AvesEntry> entries);
|
typedef MarkerTapCallback = void Function(AvesEntry markerEntry, Set<AvesEntry> Function() getClusterEntries);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
|
||||||
import 'package:aves/model/entry_images.dart';
|
import 'package:aves/model/entry_images.dart';
|
||||||
import 'package:aves/model/settings/enums.dart';
|
import 'package:aves/model/settings/enums.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
|
@ -12,8 +11,6 @@ import 'package:aves/widgets/common/map/geo_entry.dart';
|
||||||
import 'package:aves/widgets/common/map/geo_map.dart';
|
import 'package:aves/widgets/common/map/geo_map.dart';
|
||||||
import 'package:aves/widgets/common/map/google/marker_generator.dart';
|
import 'package:aves/widgets/common/map/google/marker_generator.dart';
|
||||||
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fluster/fluster.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:latlong2/latlong.dart' as ll;
|
import 'package:latlong2/latlong.dart' as ll;
|
||||||
|
@ -24,9 +21,8 @@ class EntryGoogleMap extends StatefulWidget {
|
||||||
final bool interactive;
|
final bool interactive;
|
||||||
final double? minZoom, maxZoom;
|
final double? minZoom, maxZoom;
|
||||||
final EntryMapStyle style;
|
final EntryMapStyle style;
|
||||||
final EntryMarkerBuilder markerBuilder;
|
final MarkerClusterBuilder markerClusterBuilder;
|
||||||
final Fluster<GeoEntry> markerCluster;
|
final MarkerWidgetBuilder markerWidgetBuilder;
|
||||||
final List<AvesEntry> markerEntries;
|
|
||||||
final UserZoomChangeCallback? onUserZoomChange;
|
final UserZoomChangeCallback? onUserZoomChange;
|
||||||
final void Function(GeoEntry geoEntry)? onMarkerTap;
|
final void Function(GeoEntry geoEntry)? onMarkerTap;
|
||||||
|
|
||||||
|
@ -38,9 +34,8 @@ class EntryGoogleMap extends StatefulWidget {
|
||||||
this.minZoom,
|
this.minZoom,
|
||||||
this.maxZoom,
|
this.maxZoom,
|
||||||
required this.style,
|
required this.style,
|
||||||
required this.markerBuilder,
|
required this.markerClusterBuilder,
|
||||||
required this.markerCluster,
|
required this.markerWidgetBuilder,
|
||||||
required this.markerEntries,
|
|
||||||
this.onUserZoomChange,
|
this.onUserZoomChange,
|
||||||
this.onMarkerTap,
|
this.onMarkerTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
@ -52,6 +47,7 @@ class EntryGoogleMap extends StatefulWidget {
|
||||||
class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObserver {
|
class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObserver {
|
||||||
GoogleMapController? _googleMapController;
|
GoogleMapController? _googleMapController;
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
|
Map<MarkerKey, GeoEntry> _geoEntryByMarkerKey = {};
|
||||||
final Map<MarkerKey, Uint8List> _markerBitmaps = {};
|
final Map<MarkerKey, Uint8List> _markerBitmaps = {};
|
||||||
final AChangeNotifier _markerBitmapChangeNotifier = AChangeNotifier();
|
final AChangeNotifier _markerBitmapChangeNotifier = AChangeNotifier();
|
||||||
|
|
||||||
|
@ -66,28 +62,34 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_registerWidget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant EntryGoogleMap oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_unregisterWidget(oldWidget);
|
||||||
|
_registerWidget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_unregisterWidget(widget);
|
||||||
|
_googleMapController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerWidget(EntryGoogleMap widget) {
|
||||||
final avesMapController = widget.controller;
|
final avesMapController = widget.controller;
|
||||||
if (avesMapController != null) {
|
if (avesMapController != null) {
|
||||||
_subscriptions.add(avesMapController.moveEvents.listen((event) => _moveTo(_toGoogleLatLng(event.latLng))));
|
_subscriptions.add(avesMapController.moveEvents.listen((event) => _moveTo(_toGoogleLatLng(event.latLng))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _unregisterWidget(EntryGoogleMap widget) {
|
||||||
void didUpdateWidget(covariant EntryGoogleMap oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
const eq = DeepCollectionEquality();
|
|
||||||
if (!eq.equals(widget.markerEntries, oldWidget.markerEntries)) {
|
|
||||||
_markerBitmaps.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_googleMapController?.dispose();
|
|
||||||
_subscriptions
|
_subscriptions
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
..clear();
|
..clear();
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -107,51 +109,35 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder<ZoomedBounds?>(
|
return Stack(
|
||||||
valueListenable: boundsNotifier,
|
children: [
|
||||||
builder: (context, visibleRegion, child) {
|
MarkerGeneratorWidget<MarkerKey>(
|
||||||
final allEntries = widget.markerEntries;
|
markers: _geoEntryByMarkerKey.keys.map(widget.markerWidgetBuilder).toList(),
|
||||||
final geoEntries = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
|
isReadyToRender: (key) => key.entry.isThumbnailReady(extent: GeoMap.markerImageExtent),
|
||||||
final geoEntryByMarkerKey = Map.fromEntries(geoEntries.map((v) {
|
onRendered: (key, bitmap) {
|
||||||
if (v.isCluster!) {
|
_markerBitmaps[key] = bitmap;
|
||||||
final uri = v.childMarkerId;
|
_markerBitmapChangeNotifier.notifyListeners();
|
||||||
final entry = allEntries.firstWhere((v) => v.uri == uri);
|
},
|
||||||
return MapEntry(MarkerKey(entry, v.pointsSize), v);
|
),
|
||||||
}
|
MapDecorator(
|
||||||
return MapEntry(MarkerKey(v.entry!, null), v);
|
interactive: interactive,
|
||||||
}));
|
child: _buildMap(),
|
||||||
|
),
|
||||||
return Stack(
|
MapButtonPanel(
|
||||||
children: [
|
boundsNotifier: boundsNotifier,
|
||||||
MarkerGeneratorWidget<MarkerKey>(
|
zoomBy: _zoomBy,
|
||||||
markers: geoEntryByMarkerKey.keys.map(widget.markerBuilder).toList(),
|
resetRotation: interactive ? _resetRotation : null,
|
||||||
isReadyToRender: (key) => key.entry.isThumbnailReady(extent: GeoMap.markerImageExtent),
|
),
|
||||||
onRendered: (key, bitmap) {
|
],
|
||||||
_markerBitmaps[key] = bitmap;
|
|
||||||
_markerBitmapChangeNotifier.notifyListeners();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MapDecorator(
|
|
||||||
interactive: interactive,
|
|
||||||
child: _buildMap(geoEntryByMarkerKey),
|
|
||||||
),
|
|
||||||
MapButtonPanel(
|
|
||||||
boundsNotifier: boundsNotifier,
|
|
||||||
zoomBy: _zoomBy,
|
|
||||||
resetRotation: interactive ? _resetRotation : null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMap(Map<MarkerKey, GeoEntry> geoEntryByMarkerKey) {
|
Widget _buildMap() {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _markerBitmapChangeNotifier,
|
animation: _markerBitmapChangeNotifier,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final markers = <Marker>{};
|
final markers = <Marker>{};
|
||||||
geoEntryByMarkerKey.forEach((markerKey, geoEntry) {
|
_geoEntryByMarkerKey.forEach((markerKey, geoEntry) {
|
||||||
final bytes = _markerBitmaps[markerKey];
|
final bytes = _markerBitmaps[markerKey];
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
final point = LatLng(geoEntry.latitude!, geoEntry.longitude!);
|
final point = LatLng(geoEntry.latitude!, geoEntry.longitude!);
|
||||||
|
@ -195,11 +181,17 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
||||||
myLocationButtonEnabled: false,
|
myLocationButtonEnabled: false,
|
||||||
markers: markers,
|
markers: markers,
|
||||||
onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: -position.bearing),
|
onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: -position.bearing),
|
||||||
|
onCameraIdle: _updateClusters,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateClusters() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _geoEntryByMarkerKey = widget.markerClusterBuilder());
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _updateVisibleRegion({required double zoom, required double rotation}) async {
|
Future<void> _updateVisibleRegion({required double zoom, required double rotation}) async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
@ -226,7 +218,6 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
||||||
final controller = _googleMapController;
|
final controller = _googleMapController;
|
||||||
if (controller == null) return;
|
if (controller == null) return;
|
||||||
|
|
||||||
final bounds = boundsNotifier.value;
|
|
||||||
await controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(
|
await controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(
|
||||||
target: _toGoogleLatLng(bounds.center),
|
target: _toGoogleLatLng(bounds.center),
|
||||||
zoom: bounds.zoom,
|
zoom: bounds.zoom,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
|
||||||
import 'package:aves/model/settings/enums.dart';
|
import 'package:aves/model/settings/enums.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/utils/debouncer.dart';
|
||||||
import 'package:aves/widgets/common/map/buttons.dart';
|
import 'package:aves/widgets/common/map/buttons.dart';
|
||||||
import 'package:aves/widgets/common/map/controller.dart';
|
import 'package:aves/widgets/common/map/controller.dart';
|
||||||
import 'package:aves/widgets/common/map/decorator.dart';
|
import 'package:aves/widgets/common/map/decorator.dart';
|
||||||
|
@ -11,7 +12,6 @@ import 'package:aves/widgets/common/map/latlng_tween.dart';
|
||||||
import 'package:aves/widgets/common/map/leaflet/scale_layer.dart';
|
import 'package:aves/widgets/common/map/leaflet/scale_layer.dart';
|
||||||
import 'package:aves/widgets/common/map/leaflet/tile_layers.dart';
|
import 'package:aves/widgets/common/map/leaflet/tile_layers.dart';
|
||||||
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
||||||
import 'package:fluster/fluster.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
@ -22,9 +22,8 @@ class EntryLeafletMap extends StatefulWidget {
|
||||||
final bool interactive;
|
final bool interactive;
|
||||||
final double minZoom, maxZoom;
|
final double minZoom, maxZoom;
|
||||||
final EntryMapStyle style;
|
final EntryMapStyle style;
|
||||||
final EntryMarkerBuilder markerBuilder;
|
final MarkerClusterBuilder markerClusterBuilder;
|
||||||
final Fluster<GeoEntry> markerCluster;
|
final MarkerWidgetBuilder markerWidgetBuilder;
|
||||||
final List<AvesEntry> markerEntries;
|
|
||||||
final Size markerSize;
|
final Size markerSize;
|
||||||
final UserZoomChangeCallback? onUserZoomChange;
|
final UserZoomChangeCallback? onUserZoomChange;
|
||||||
final void Function(GeoEntry geoEntry)? onMarkerTap;
|
final void Function(GeoEntry geoEntry)? onMarkerTap;
|
||||||
|
@ -37,9 +36,8 @@ class EntryLeafletMap extends StatefulWidget {
|
||||||
this.minZoom = 0,
|
this.minZoom = 0,
|
||||||
this.maxZoom = 22,
|
this.maxZoom = 22,
|
||||||
required this.style,
|
required this.style,
|
||||||
required this.markerBuilder,
|
required this.markerClusterBuilder,
|
||||||
required this.markerCluster,
|
required this.markerWidgetBuilder,
|
||||||
required this.markerEntries,
|
|
||||||
required this.markerSize,
|
required this.markerSize,
|
||||||
this.onUserZoomChange,
|
this.onUserZoomChange,
|
||||||
this.onMarkerTap,
|
this.onMarkerTap,
|
||||||
|
@ -52,6 +50,8 @@ class EntryLeafletMap extends StatefulWidget {
|
||||||
class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderStateMixin {
|
class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderStateMixin {
|
||||||
final MapController _leafletMapController = MapController();
|
final MapController _leafletMapController = MapController();
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
|
Map<MarkerKey, GeoEntry> _geoEntryByMarkerKey = {};
|
||||||
|
final Debouncer _debouncer = Debouncer(delay: Durations.mapIdleDebounceDelay);
|
||||||
|
|
||||||
ValueNotifier<ZoomedBounds> get boundsNotifier => widget.boundsNotifier;
|
ValueNotifier<ZoomedBounds> get boundsNotifier => widget.boundsNotifier;
|
||||||
|
|
||||||
|
@ -65,58 +65,59 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_registerWidget(widget);
|
||||||
|
WidgetsBinding.instance!.addPostFrameCallback((_) => _updateVisibleRegion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant EntryLeafletMap oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_unregisterWidget(oldWidget);
|
||||||
|
_registerWidget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_unregisterWidget(widget);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerWidget(EntryLeafletMap widget) {
|
||||||
final avesMapController = widget.controller;
|
final avesMapController = widget.controller;
|
||||||
if (avesMapController != null) {
|
if (avesMapController != null) {
|
||||||
_subscriptions.add(avesMapController.moveEvents.listen((event) => _moveTo(event.latLng)));
|
_subscriptions.add(avesMapController.moveEvents.listen((event) => _moveTo(event.latLng)));
|
||||||
}
|
}
|
||||||
_subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion()));
|
_subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion()));
|
||||||
WidgetsBinding.instance!.addPostFrameCallback((_) => _updateVisibleRegion());
|
boundsNotifier.addListener(_onBoundsChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _unregisterWidget(EntryLeafletMap widget) {
|
||||||
void dispose() {
|
boundsNotifier.removeListener(_onBoundsChange);
|
||||||
_subscriptions
|
_subscriptions
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
..clear();
|
..clear();
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder<ZoomedBounds?>(
|
return Stack(
|
||||||
valueListenable: boundsNotifier,
|
children: [
|
||||||
builder: (context, visibleRegion, child) {
|
MapDecorator(
|
||||||
final allEntries = widget.markerEntries;
|
interactive: interactive,
|
||||||
final geoEntries = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
|
child: _buildMap(),
|
||||||
final geoEntryByMarkerKey = Map.fromEntries(geoEntries.map((v) {
|
),
|
||||||
if (v.isCluster!) {
|
MapButtonPanel(
|
||||||
final uri = v.childMarkerId;
|
boundsNotifier: boundsNotifier,
|
||||||
final entry = allEntries.firstWhere((v) => v.uri == uri);
|
zoomBy: _zoomBy,
|
||||||
return MapEntry(MarkerKey(entry, v.pointsSize), v);
|
resetRotation: interactive ? _resetRotation : null,
|
||||||
}
|
),
|
||||||
return MapEntry(MarkerKey(v.entry!, null), v);
|
],
|
||||||
}));
|
|
||||||
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
MapDecorator(
|
|
||||||
interactive: interactive,
|
|
||||||
child: _buildMap(geoEntryByMarkerKey),
|
|
||||||
),
|
|
||||||
MapButtonPanel(
|
|
||||||
boundsNotifier: boundsNotifier,
|
|
||||||
zoomBy: _zoomBy,
|
|
||||||
resetRotation: interactive ? _resetRotation : null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMap(Map<MarkerKey, GeoEntry> geoEntryByMarkerKey) {
|
Widget _buildMap() {
|
||||||
final markerSize = widget.markerSize;
|
final markerSize = widget.markerSize;
|
||||||
final markers = geoEntryByMarkerKey.entries.map((kv) {
|
final markers = _geoEntryByMarkerKey.entries.map((kv) {
|
||||||
final markerKey = kv.key;
|
final markerKey = kv.key;
|
||||||
final geoEntry = kv.value;
|
final geoEntry = kv.value;
|
||||||
final latLng = LatLng(geoEntry.latitude!, geoEntry.longitude!);
|
final latLng = LatLng(geoEntry.latitude!, geoEntry.longitude!);
|
||||||
|
@ -124,7 +125,7 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
|
||||||
point: latLng,
|
point: latLng,
|
||||||
builder: (context) => GestureDetector(
|
builder: (context) => GestureDetector(
|
||||||
onTap: () => widget.onMarkerTap?.call(geoEntry),
|
onTap: () => widget.onMarkerTap?.call(geoEntry),
|
||||||
child: widget.markerBuilder(markerKey),
|
child: widget.markerWidgetBuilder(markerKey),
|
||||||
),
|
),
|
||||||
width: markerSize.width,
|
width: markerSize.width,
|
||||||
height: markerSize.height,
|
height: markerSize.height,
|
||||||
|
@ -173,6 +174,13 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onBoundsChange() => _debouncer(_updateClusters);
|
||||||
|
|
||||||
|
void _updateClusters() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _geoEntryByMarkerKey = widget.markerClusterBuilder());
|
||||||
|
}
|
||||||
|
|
||||||
void _updateVisibleRegion() {
|
void _updateVisibleRegion() {
|
||||||
final bounds = _leafletMapController.bounds;
|
final bounds = _leafletMapController.bounds;
|
||||||
if (bounds != null) {
|
if (bounds != null) {
|
||||||
|
|
|
@ -74,14 +74,12 @@ class _MapPageState extends State<MapPage> {
|
||||||
entries: entries,
|
entries: entries,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
isAnimatingNotifier: _isAnimatingNotifier,
|
isAnimatingNotifier: _isAnimatingNotifier,
|
||||||
onMarkerTap: (markerEntries) {
|
onMarkerTap: (markerEntry, getClusterEntries) {
|
||||||
if (markerEntries.isEmpty) return;
|
final index = entries.indexOf(markerEntry);
|
||||||
final entry = markerEntries.first;
|
|
||||||
final index = entries.indexOf(entry);
|
|
||||||
if (_selectedIndexNotifier.value != index) {
|
if (_selectedIndexNotifier.value != index) {
|
||||||
_selectedIndexNotifier.value = index;
|
_selectedIndexNotifier.value = index;
|
||||||
} else {
|
} else {
|
||||||
_moveToEntry(entry);
|
_moveToEntry(markerEntry);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue