map: cluster entry fetch fix
This commit is contained in:
parent
fd33904658
commit
91ab2ef17f
5 changed files with 86 additions and 61 deletions
|
@ -26,6 +26,16 @@ class Constants {
|
||||||
|
|
||||||
static final pointNemo = LatLng(-48.876667, -123.393333);
|
static final pointNemo = LatLng(-48.876667, -123.393333);
|
||||||
|
|
||||||
|
static final wonders = [
|
||||||
|
LatLng(29.979167, 31.134167),
|
||||||
|
LatLng(36.451000, 28.223615),
|
||||||
|
LatLng(32.5355, 44.4275),
|
||||||
|
LatLng(31.213889, 29.885556),
|
||||||
|
LatLng(37.0379, 27.4241),
|
||||||
|
LatLng(37.637861, 21.63),
|
||||||
|
LatLng(37.949722, 27.363889),
|
||||||
|
];
|
||||||
|
|
||||||
static const int infoGroupMaxValueLength = 140;
|
static const int infoGroupMaxValueLength = 140;
|
||||||
|
|
||||||
static const List<Dependency> androidDependencies = [
|
static const List<Dependency> androidDependencies = [
|
||||||
|
|
|
@ -6,6 +6,8 @@ import 'package:aves/model/settings/map_style.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/services.dart';
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:aves/widgets/common/map/attribution.dart';
|
import 'package:aves/widgets/common/map/attribution.dart';
|
||||||
import 'package:aves/widgets/common/map/buttons.dart';
|
import 'package:aves/widgets/common/map/buttons.dart';
|
||||||
import 'package:aves/widgets/common/map/decorator.dart';
|
import 'package:aves/widgets/common/map/decorator.dart';
|
||||||
|
@ -18,7 +20,6 @@ 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';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class GeoMap extends StatefulWidget {
|
class GeoMap extends StatefulWidget {
|
||||||
|
@ -27,7 +28,7 @@ class GeoMap extends StatefulWidget {
|
||||||
final double? mapHeight;
|
final double? mapHeight;
|
||||||
final ValueNotifier<bool> isAnimatingNotifier;
|
final ValueNotifier<bool> isAnimatingNotifier;
|
||||||
final UserZoomChangeCallback? onUserZoomChange;
|
final UserZoomChangeCallback? onUserZoomChange;
|
||||||
final GeoEntryTapCallback? onEntryTap;
|
final MarkerTapCallback? onMarkerTap;
|
||||||
|
|
||||||
static const markerImageExtent = 48.0;
|
static const markerImageExtent = 48.0;
|
||||||
static const pointerSize = Size(8, 6);
|
static const pointerSize = Size(8, 6);
|
||||||
|
@ -39,7 +40,7 @@ class GeoMap extends StatefulWidget {
|
||||||
this.mapHeight,
|
this.mapHeight,
|
||||||
required this.isAnimatingNotifier,
|
required this.isAnimatingNotifier,
|
||||||
this.onUserZoomChange,
|
this.onUserZoomChange,
|
||||||
this.onEntryTap,
|
this.onMarkerTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -52,7 +53,9 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
// it is especially severe the first time, but still significant afterwards
|
// it is especially severe the first time, but still significant afterwards
|
||||||
// so we prevent loading it while scrolling or animating
|
// so we prevent loading it while scrolling or animating
|
||||||
bool _googleMapsLoaded = false;
|
bool _googleMapsLoaded = false;
|
||||||
late ValueNotifier<ZoomedBounds> boundsNotifier;
|
late final ValueNotifier<ZoomedBounds> _boundsNotifier;
|
||||||
|
late final Fluster<GeoEntry> _defaultMarkerCluster;
|
||||||
|
Fluster<GeoEntry>? _slowMarkerCluster;
|
||||||
|
|
||||||
List<AvesEntry> get entries => widget.entries;
|
List<AvesEntry> get entries => widget.entries;
|
||||||
|
|
||||||
|
@ -60,50 +63,40 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
|
|
||||||
double? get mapHeight => widget.mapHeight;
|
double? get mapHeight => widget.mapHeight;
|
||||||
|
|
||||||
static final wonders = [
|
|
||||||
LatLng(29.979167, 31.134167),
|
|
||||||
LatLng(36.451000, 28.223615),
|
|
||||||
LatLng(32.5355, 44.4275),
|
|
||||||
LatLng(31.213889, 29.885556),
|
|
||||||
LatLng(37.0379, 27.4241),
|
|
||||||
LatLng(37.637861, 21.63),
|
|
||||||
LatLng(37.949722, 27.363889),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final points = entries.map((v) => v.latLng!).toSet();
|
final points = entries.map((v) => v.latLng!).toSet();
|
||||||
boundsNotifier = ValueNotifier(ZoomedBounds.fromPoints(
|
_boundsNotifier = ValueNotifier(ZoomedBounds.fromPoints(
|
||||||
points: points.isNotEmpty ? points : {wonders[Random().nextInt(wonders.length)]},
|
points: points.isNotEmpty ? points : {Constants.wonders[Random().nextInt(Constants.wonders.length)]},
|
||||||
collocationZoom: settings.infoMapZoom,
|
collocationZoom: settings.infoMapZoom,
|
||||||
));
|
));
|
||||||
|
_defaultMarkerCluster = _buildFluster();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final markers = entries.map((entry) {
|
void _onMarkerTap(GeoEntry geoEntry) {
|
||||||
var latLng = entry.latLng!;
|
final onTap = widget.onMarkerTap;
|
||||||
return GeoEntry(
|
if (onTap == null) return;
|
||||||
entry: entry,
|
|
||||||
latitude: latLng.latitude,
|
final geoEntries = <GeoEntry>[];
|
||||||
longitude: latLng.longitude,
|
final clusterId = geoEntry.clusterId;
|
||||||
markerId: entry.uri,
|
if (clusterId != null) {
|
||||||
);
|
var points = _defaultMarkerCluster.points(clusterId);
|
||||||
}).toList();
|
if (points.length != geoEntry.pointsSize) {
|
||||||
final markerCluster = Fluster<GeoEntry>(
|
// `Fluster.points()` method does not always return all the points contained in a cluster
|
||||||
// we keep clustering on the whole range of zooms (including the maximum)
|
// the higher `nodeSize` is, the higher the chance to get all the points (i.e. as many as the cluster `pointsSize`)
|
||||||
// to avoid collocated entries overlapping
|
_slowMarkerCluster ??= _buildFluster(nodeSize: smallestPowerOf2(widget.entries.length));
|
||||||
minZoom: 0,
|
points = _slowMarkerCluster!.points(clusterId);
|
||||||
maxZoom: 22,
|
assert(points.length == geoEntry.pointsSize);
|
||||||
// TODO TLAD [map] derive `radius` / `extent`, from device pixel ratio and marker extent?
|
}
|
||||||
// (radius=120, extent=2 << 8) is equivalent to (radius=240, extent=2 << 9)
|
geoEntries.addAll(points);
|
||||||
radius: 240,
|
} else {
|
||||||
extent: 2 << 9,
|
geoEntries.add(geoEntry);
|
||||||
nodeSize: 64,
|
}
|
||||||
points: markers,
|
onTap(geoEntries.map((geoEntry) => geoEntry.entry!).toList());
|
||||||
createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
return FutureBuilder<bool>(
|
return FutureBuilder<bool>(
|
||||||
future: availability.isConnected,
|
future: availability.isConnected,
|
||||||
|
@ -125,28 +118,28 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
|
|
||||||
Widget child = isGoogleMaps
|
Widget child = isGoogleMaps
|
||||||
? EntryGoogleMap(
|
? EntryGoogleMap(
|
||||||
boundsNotifier: boundsNotifier,
|
boundsNotifier: _boundsNotifier,
|
||||||
interactive: interactive,
|
interactive: interactive,
|
||||||
style: mapStyle,
|
style: mapStyle,
|
||||||
markerBuilder: _buildMarker,
|
markerBuilder: _buildMarker,
|
||||||
markerCluster: markerCluster,
|
markerCluster: _defaultMarkerCluster,
|
||||||
markerEntries: entries,
|
markerEntries: entries,
|
||||||
onUserZoomChange: widget.onUserZoomChange,
|
onUserZoomChange: widget.onUserZoomChange,
|
||||||
onEntryTap: widget.onEntryTap,
|
onMarkerTap: _onMarkerTap,
|
||||||
)
|
)
|
||||||
: EntryLeafletMap(
|
: EntryLeafletMap(
|
||||||
boundsNotifier: boundsNotifier,
|
boundsNotifier: _boundsNotifier,
|
||||||
interactive: interactive,
|
interactive: interactive,
|
||||||
style: mapStyle,
|
style: mapStyle,
|
||||||
markerBuilder: _buildMarker,
|
markerBuilder: _buildMarker,
|
||||||
markerCluster: markerCluster,
|
markerCluster: _defaultMarkerCluster,
|
||||||
markerEntries: entries,
|
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,
|
||||||
),
|
),
|
||||||
onUserZoomChange: widget.onUserZoomChange,
|
onUserZoomChange: widget.onUserZoomChange,
|
||||||
onEntryTap: widget.onEntryTap,
|
onMarkerTap: _onMarkerTap,
|
||||||
);
|
);
|
||||||
|
|
||||||
child = Column(
|
child = Column(
|
||||||
|
@ -179,7 +172,7 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
interactive: interactive,
|
interactive: interactive,
|
||||||
),
|
),
|
||||||
MapButtonPanel(
|
MapButtonPanel(
|
||||||
latLng: boundsNotifier.value.center,
|
latLng: _boundsNotifier.value.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -203,6 +196,33 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Fluster<GeoEntry> _buildFluster({int nodeSize = 64}) {
|
||||||
|
final markers = entries.map((entry) {
|
||||||
|
final latLng = entry.latLng!;
|
||||||
|
return GeoEntry(
|
||||||
|
entry: entry,
|
||||||
|
latitude: latLng.latitude,
|
||||||
|
longitude: latLng.longitude,
|
||||||
|
markerId: entry.uri,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return Fluster<GeoEntry>(
|
||||||
|
// we keep clustering on the whole range of zooms (including the maximum)
|
||||||
|
// to avoid collocated entries overlapping
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 22,
|
||||||
|
// TODO TLAD [map] derive `radius` / `extent`, from device pixel ratio and marker extent?
|
||||||
|
// (radius=120, extent=2 << 8) is equivalent to (radius=240, extent=2 << 9)
|
||||||
|
radius: 240,
|
||||||
|
extent: 2 << 9,
|
||||||
|
// node size: 64 by default, higher means faster indexing but slower search
|
||||||
|
nodeSize: nodeSize,
|
||||||
|
points: markers,
|
||||||
|
createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -218,4 +238,4 @@ class MarkerKey extends LocalKey with EquatableMixin {
|
||||||
|
|
||||||
typedef EntryMarkerBuilder = Widget Function(MarkerKey key);
|
typedef EntryMarkerBuilder = Widget Function(MarkerKey key);
|
||||||
typedef UserZoomChangeCallback = void Function(double zoom);
|
typedef UserZoomChangeCallback = void Function(double zoom);
|
||||||
typedef GeoEntryTapCallback = void Function(List<GeoEntry> geoEntries);
|
typedef MarkerTapCallback = void Function(List<AvesEntry> entries);
|
||||||
|
|
|
@ -25,7 +25,7 @@ class EntryGoogleMap extends StatefulWidget {
|
||||||
final Fluster<GeoEntry> markerCluster;
|
final Fluster<GeoEntry> markerCluster;
|
||||||
final List<AvesEntry> markerEntries;
|
final List<AvesEntry> markerEntries;
|
||||||
final UserZoomChangeCallback? onUserZoomChange;
|
final UserZoomChangeCallback? onUserZoomChange;
|
||||||
final GeoEntryTapCallback? onEntryTap;
|
final void Function(GeoEntry geoEntry)? onMarkerTap;
|
||||||
|
|
||||||
const EntryGoogleMap({
|
const EntryGoogleMap({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
@ -36,7 +36,7 @@ class EntryGoogleMap extends StatefulWidget {
|
||||||
required this.markerCluster,
|
required this.markerCluster,
|
||||||
required this.markerEntries,
|
required this.markerEntries,
|
||||||
this.onUserZoomChange,
|
this.onUserZoomChange,
|
||||||
this.onEntryTap,
|
this.onMarkerTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -134,7 +134,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
||||||
animation: _markerBitmapChangeNotifier,
|
animation: _markerBitmapChangeNotifier,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final markers = <Marker>{};
|
final markers = <Marker>{};
|
||||||
final onTap = widget.onEntryTap;
|
final onEntryTap = widget.onMarkerTap;
|
||||||
geoEntryByMarkerKey.forEach((markerKey, geoEntry) {
|
geoEntryByMarkerKey.forEach((markerKey, geoEntry) {
|
||||||
final bytes = _markerBitmaps[markerKey];
|
final bytes = _markerBitmaps[markerKey];
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
|
@ -143,12 +143,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
||||||
markerId: MarkerId(geoEntry.markerId!),
|
markerId: MarkerId(geoEntry.markerId!),
|
||||||
icon: BitmapDescriptor.fromBytes(bytes),
|
icon: BitmapDescriptor.fromBytes(bytes),
|
||||||
position: latLng,
|
position: latLng,
|
||||||
onTap: onTap != null
|
onTap: onEntryTap != null ? () => onEntryTap(geoEntry) : null,
|
||||||
? () {
|
|
||||||
final clusterId = geoEntry.clusterId;
|
|
||||||
onTap(clusterId != null ? widget.markerCluster.points(clusterId) : [geoEntry]);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,7 +24,7 @@ class EntryLeafletMap extends StatefulWidget {
|
||||||
final List<AvesEntry> markerEntries;
|
final List<AvesEntry> markerEntries;
|
||||||
final Size markerSize;
|
final Size markerSize;
|
||||||
final UserZoomChangeCallback? onUserZoomChange;
|
final UserZoomChangeCallback? onUserZoomChange;
|
||||||
final GeoEntryTapCallback? onEntryTap;
|
final void Function(GeoEntry geoEntry)? onMarkerTap;
|
||||||
|
|
||||||
const EntryLeafletMap({
|
const EntryLeafletMap({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
@ -36,7 +36,7 @@ class EntryLeafletMap extends StatefulWidget {
|
||||||
required this.markerEntries,
|
required this.markerEntries,
|
||||||
required this.markerSize,
|
required this.markerSize,
|
||||||
this.onUserZoomChange,
|
this.onUserZoomChange,
|
||||||
this.onEntryTap,
|
this.onMarkerTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -118,8 +118,7 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
|
||||||
point: latLng,
|
point: latLng,
|
||||||
builder: (context) => GestureDetector(
|
builder: (context) => GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final clusterId = geoEntry.clusterId;
|
widget.onMarkerTap?.call(geoEntry);
|
||||||
widget.onEntryTap?.call(clusterId != null ? widget.markerCluster.points(clusterId) : [geoEntry]);
|
|
||||||
_moveTo(latLng);
|
_moveTo(latLng);
|
||||||
},
|
},
|
||||||
child: widget.markerBuilder(markerKey),
|
child: widget.markerBuilder(markerKey),
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/map/geo_map.dart';
|
import 'package:aves/widgets/common/map/geo_map.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/scroller.dart';
|
import 'package:aves/widgets/common/thumbnail/scroller.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
@ -61,8 +62,8 @@ class _MapPageState extends State<MapPage> {
|
||||||
entries: entries,
|
entries: entries,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
isAnimatingNotifier: _isAnimatingNotifier,
|
isAnimatingNotifier: _isAnimatingNotifier,
|
||||||
onEntryTap: (geoEntries) {
|
onMarkerTap: (entries) {
|
||||||
debugPrint('TLAD count=${geoEntries.length} entry=${geoEntries.first.entry}');
|
debugPrint('TLAD count=${entries.length} entry=${entries.firstOrNull?.bestTitle}');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue