map: update on entry removal

This commit is contained in:
Thibault Deckers 2021-10-08 18:31:01 +09:00
parent d0c62a602b
commit 8df538e7f7
8 changed files with 152 additions and 82 deletions

View file

@ -46,8 +46,8 @@ class CollectionLens with ChangeNotifier {
id ??= hashCode;
if (listenToSource) {
final sourceEvents = source.eventBus;
_subscriptions.add(sourceEvents.on<EntryAddedEvent>().listen((e) => onEntryAdded(e.entries)));
_subscriptions.add(sourceEvents.on<EntryRemovedEvent>().listen((e) => onEntryRemoved(e.entries)));
_subscriptions.add(sourceEvents.on<EntryAddedEvent>().listen((e) => _onEntryAdded(e.entries)));
_subscriptions.add(sourceEvents.on<EntryRemovedEvent>().listen((e) => _onEntryRemoved(e.entries)));
_subscriptions.add(sourceEvents.on<EntryMovedEvent>().listen((e) => _refresh()));
_subscriptions.add(sourceEvents.on<EntryRefreshedEvent>().listen((e) => _refresh()));
_subscriptions.add(sourceEvents.on<FilterVisibilityChangedEvent>().listen((e) => _refresh()));
@ -73,6 +73,20 @@ class CollectionLens with ChangeNotifier {
super.dispose();
}
CollectionLens copyWith({
CollectionSource? source,
Set<CollectionFilter>? filters,
bool? listenToSource,
List<AvesEntry>? fixedSelection,
}) =>
CollectionLens(
source: source ?? this.source,
filters: filters ?? this.filters,
id: id,
listenToSource: listenToSource ?? this.listenToSource,
fixedSelection: fixedSelection ?? this.fixedSelection,
);
bool get isEmpty => _filteredSortedEntries.isEmpty;
int get entryCount => _filteredSortedEntries.length;
@ -103,16 +117,16 @@ class CollectionLens with ChangeNotifier {
filters.removeWhere((old) => old.category == filter.category);
}
filters.add(filter);
onFilterChanged();
_onFilterChanged();
}
void removeFilter(CollectionFilter filter) {
if (!filters.contains(filter)) return;
filters.remove(filter);
onFilterChanged();
_onFilterChanged();
}
void onFilterChanged() {
void _onFilterChanged() {
_refresh();
filterChangeNotifier.notifyListeners();
}
@ -229,11 +243,11 @@ class CollectionLens with ChangeNotifier {
}
}
void onEntryAdded(Set<AvesEntry>? entries) {
void _onEntryAdded(Set<AvesEntry>? entries) {
_refresh();
}
void onEntryRemoved(Set<AvesEntry> entries) {
void _onEntryRemoved(Set<AvesEntry> entries) {
if (groupBursts) {
// find impacted burst groups
final obsoleteBurstEntries = <AvesEntry>{};
@ -256,6 +270,7 @@ class CollectionLens with ChangeNotifier {
// we should remove obsolete entries and sections
// but do not apply sort/section
// as section order change would surprise the user while browsing
fixedSelection?.removeWhere(entries.contains);
_filteredSortedEntries.removeWhere(entries.contains);
_sortedEntries?.removeWhere(entries.contains);
sections.forEach((key, sectionEntries) => sectionEntries.removeWhere(entries.contains));

View file

@ -288,6 +288,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
MaterialPageRoute(
settings: const RouteSettings(name: MapPage.routeName),
builder: (context) => MapPage(
// need collection with fresh ID to prevent hero from scroller on Map page to Collection page
collection: CollectionLens(
source: collection.source,
filters: collection.filters,

View file

@ -73,10 +73,7 @@ class InteractiveThumbnail extends StatelessWidget {
TransparentMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (context, a, sa) {
final viewerCollection = CollectionLens(
source: collection.source,
filters: collection.filters,
id: collection.id,
final viewerCollection = collection.copyWith(
listenToSource: false,
);
assert(viewerCollection.sortedEntries.map((e) => e.contentId).contains(entry.contentId));

View file

@ -6,6 +6,7 @@ import 'package:aves/model/settings/map_style.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:aves/widgets/common/map/attribution.dart';
@ -27,6 +28,7 @@ import 'package:provider/provider.dart';
class GeoMap extends StatefulWidget {
final AvesMapController? controller;
final Listenable? collectionListenable;
final List<AvesEntry> entries;
final AvesEntry? initialEntry;
final ValueNotifier<bool> isAnimatingNotifier;
@ -42,6 +44,7 @@ class GeoMap extends StatefulWidget {
const GeoMap({
Key? key,
this.controller,
this.collectionListenable,
required this.entries,
this.initialEntry,
required this.isAnimatingNotifier,
@ -63,8 +66,9 @@ class _GeoMapState extends State<GeoMap> {
// so we prevent loading it while scrolling or animating
bool _googleMapsLoaded = false;
late final ValueNotifier<ZoomedBounds> _boundsNotifier;
late final Fluster<GeoEntry> _defaultMarkerCluster;
Fluster<GeoEntry>? _defaultMarkerCluster;
Fluster<GeoEntry>? _slowMarkerCluster;
final AChangeNotifier _clusterChangeNotifier = AChangeNotifier();
List<AvesEntry> get entries => widget.entries;
@ -84,7 +88,29 @@ class _GeoMapState extends State<GeoMap> {
_boundsNotifier = ValueNotifier(bounds.copyWith(
zoom: max(bounds.zoom, minInitialZoom),
));
_defaultMarkerCluster = _buildFluster();
_registerWidget(widget);
_onCollectionChanged();
}
@override
void didUpdateWidget(covariant GeoMap oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(GeoMap widget) {
widget.collectionListenable?.addListener(_onCollectionChanged);
}
void _unregisterWidget(GeoMap widget) {
widget.collectionListenable?.removeListener(_onCollectionChanged);
}
@override
@ -99,7 +125,7 @@ class _GeoMapState extends State<GeoMap> {
return {geoEntry.entry!};
}
var points = _defaultMarkerCluster.points(clusterId);
var points = _defaultMarkerCluster?.points(clusterId) ?? [];
if (points.length != geoEntry.pointsSize) {
// `Fluster.points()` method does not always return all the points contained in a cluster
// the higher `nodeSize` is, the higher the chance to get all the points (i.e. as many as the cluster `pointsSize`)
@ -144,6 +170,7 @@ class _GeoMapState extends State<GeoMap> {
Widget child = isGoogleMaps
? EntryGoogleMap(
controller: widget.controller,
clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier,
minZoom: 0,
maxZoom: 20,
@ -158,6 +185,7 @@ class _GeoMapState extends State<GeoMap> {
)
: EntryLeafletMap(
controller: widget.controller,
clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier,
minZoom: 2,
maxZoom: 16,
@ -237,6 +265,12 @@ class _GeoMapState extends State<GeoMap> {
);
}
void _onCollectionChanged() {
_defaultMarkerCluster = _buildFluster();
_slowMarkerCluster = null;
_clusterChangeNotifier.notifyListeners();
}
Fluster<GeoEntry> _buildFluster({int nodeSize = 64}) {
final markers = entries.map((entry) {
final latLng = entry.latLng!;
@ -266,7 +300,7 @@ class _GeoMapState extends State<GeoMap> {
Map<MarkerKey, GeoEntry> _buildMarkerClusters() {
final bounds = _boundsNotifier.value;
final geoEntries = _defaultMarkerCluster.clusters(bounds.boundingBox, bounds.zoom.round());
final geoEntries = _defaultMarkerCluster?.clusters(bounds.boundingBox, bounds.zoom.round()) ?? [];
return Map.fromEntries(geoEntries.map((v) {
if (v.isCluster!) {
final uri = v.childMarkerId;

View file

@ -21,6 +21,7 @@ import 'package:provider/provider.dart';
class EntryGoogleMap extends StatefulWidget {
final AvesMapController? controller;
final Listenable clusterListenable;
final ValueNotifier<ZoomedBounds> boundsNotifier;
final double? minZoom, maxZoom;
final EntryMapStyle style;
@ -35,6 +36,7 @@ class EntryGoogleMap extends StatefulWidget {
const EntryGoogleMap({
Key? key,
this.controller,
required this.clusterListenable,
required this.boundsNotifier,
this.minZoom,
this.maxZoom,
@ -93,9 +95,11 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
if (avesMapController != null) {
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toGoogleLatLng(event.latLng))));
}
widget.clusterListenable.addListener(_updateMarkers);
}
void _unregisterWidget(EntryGoogleMap widget) {
widget.clusterListenable.removeListener(_updateMarkers);
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
@ -213,7 +217,8 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
onCameraIdle: _onIdle,
onTap: (position) => widget.onMapTap?.call(),
);
});
},
);
},
);
}
@ -221,6 +226,10 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
void _onIdle() {
if (!mounted) return;
widget.controller?.notifyIdle(bounds);
_updateMarkers();
}
void _updateMarkers() {
setState(() => _geoEntryByMarkerKey = widget.markerClusterBuilder());
}

View file

@ -23,6 +23,7 @@ import 'package:provider/provider.dart';
class EntryLeafletMap extends StatefulWidget {
final AvesMapController? controller;
final Listenable clusterListenable;
final ValueNotifier<ZoomedBounds> boundsNotifier;
final double minZoom, maxZoom;
final EntryMapStyle style;
@ -38,6 +39,7 @@ class EntryLeafletMap extends StatefulWidget {
const EntryLeafletMap({
Key? key,
this.controller,
required this.clusterListenable,
required this.boundsNotifier,
this.minZoom = 0,
this.maxZoom = 22,
@ -96,11 +98,13 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng)));
}
_subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion()));
boundsNotifier.addListener(_onBoundsChange);
widget.clusterListenable.addListener(_updateMarkers);
widget.boundsNotifier.addListener(_onBoundsChange);
}
void _unregisterWidget(EntryLeafletMap widget) {
boundsNotifier.removeListener(_onBoundsChange);
widget.clusterListenable.removeListener(_updateMarkers);
widget.boundsNotifier.removeListener(_onBoundsChange);
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
@ -216,6 +220,10 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
void _onIdle() {
if (!mounted) return;
widget.controller?.notifyIdle(bounds);
_updateMarkers();
}
void _updateMarkers() {
setState(() => _geoEntryByMarkerKey = widget.markerClusterBuilder());
}

View file

@ -221,6 +221,7 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
// key is expected by test driver
key: const Key('map_view'),
controller: _mapController,
collectionListenable: openingCollection,
entries: openingCollection.sortedEntries,
initialEntry: widget.initialEntry,
isAnimatingNotifier: _isPageAnimatingNotifier,
@ -255,6 +256,10 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
builder: (context, mqWidth, child) => ValueListenableBuilder<CollectionLens?>(
valueListenable: _regionCollectionNotifier,
builder: (context, regionCollection, child) {
return AnimatedBuilder(
// update when entries are added/removed
animation: regionCollection ?? ChangeNotifier(),
builder: (context, child) {
final regionEntries = regionCollection?.sortedEntries ?? [];
return ThumbnailScroller(
availableWidth: mqWidth,
@ -266,6 +271,8 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
highlightable: true,
);
},
);
},
),
),
],
@ -295,14 +302,11 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
selectedEntry = selectedIndex != null && selectedIndex < regionEntries.length ? regionEntries[selectedIndex] : null;
}
_regionCollectionNotifier.value = CollectionLens(
source: openingCollection.source,
listenToSource: false,
fixedSelection: openingCollection.sortedEntries,
filters: [
_regionCollectionNotifier.value = openingCollection.copyWith(
filters: {
...openingCollection.filters.whereNot((v) => v is CoordinateFilter),
CoordinateFilter(bounds.sw, bounds.ne),
],
},
);
// get entries from the new collection, so the entry order is the same
@ -352,7 +356,9 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
settings: const RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (context, a, sa) {
return EntryViewerPage(
collection: regionCollection,
collection: regionCollection?.copyWith(
listenToSource: false,
),
initialEntry: initialEntry,
);
},

View file

@ -126,8 +126,8 @@ class _LocationSectionState extends State<LocationSection> {
MaterialPageRoute(
settings: const RouteSettings(name: MapPage.routeName),
builder: (context) => MapPage(
collection: CollectionLens(
source: baseCollection.source,
collection: baseCollection.copyWith(
listenToSource: true,
fixedSelection: baseCollection.sortedEntries.where((entry) => entry.hasGps).toList(),
),
initialEntry: entry,