map: coordinate filter
This commit is contained in:
parent
10e9caaffe
commit
5338cb47c2
18 changed files with 474 additions and 250 deletions
|
@ -1,32 +0,0 @@
|
|||
import 'package:aves/utils/math_utils.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
String _decimal2sexagesimal(final double degDecimal) {
|
||||
List<int> _split(final double value) {
|
||||
// NumberFormat is necessary to create digit after comma if the value
|
||||
// has no decimal point (only necessary for browser)
|
||||
final tmp = NumberFormat('0.0#####').format(roundToPrecision(value, decimals: 10)).split('.');
|
||||
return <int>[
|
||||
int.parse(tmp[0]).abs(),
|
||||
int.parse(tmp[1]),
|
||||
];
|
||||
}
|
||||
|
||||
final deg = _split(degDecimal)[0];
|
||||
final minDecimal = (degDecimal.abs() - deg) * 60;
|
||||
final min = _split(minDecimal)[0];
|
||||
final sec = (minDecimal - min) * 60;
|
||||
|
||||
return '$deg° $min′ ${roundToPrecision(sec, decimals: 2).toStringAsFixed(2)}″';
|
||||
}
|
||||
|
||||
// returns coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E']
|
||||
List<String> toDMS(LatLng latLng) {
|
||||
final lat = latLng.latitude;
|
||||
final lng = latLng.longitude;
|
||||
return [
|
||||
'${_decimal2sexagesimal(lat)} ${lat < 0 ? 'S' : 'N'}',
|
||||
'${_decimal2sexagesimal(lng)} ${lng < 0 ? 'W' : 'E'}',
|
||||
];
|
||||
}
|
63
lib/model/filters/coordinate.dart
Normal file
63
lib/model/filters/coordinate.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/coordinate_format.dart';
|
||||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/geo_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CoordinateFilter extends CollectionFilter {
|
||||
static const type = 'coordinate';
|
||||
|
||||
final LatLng sw;
|
||||
final LatLng ne;
|
||||
final bool minuteSecondPadding;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [sw, ne];
|
||||
|
||||
const CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false});
|
||||
|
||||
CoordinateFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
LatLng.fromJson(json['sw']),
|
||||
LatLng.fromJson(json['ne']),
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'sw': sw.toJson(),
|
||||
'ne': ne.toJson(),
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => (entry) => GeoUtils.contains(sw, ne, entry.latLng);
|
||||
|
||||
String _formatBounds(CoordinateFormat format) {
|
||||
String s(LatLng latLng) => format.format(
|
||||
latLng,
|
||||
minuteSecondPadding: minuteSecondPadding,
|
||||
dmsSecondDecimals: 0,
|
||||
);
|
||||
return '${s(ne)}\n${s(sw)}';
|
||||
}
|
||||
|
||||
@override
|
||||
String get universalLabel => _formatBounds(CoordinateFormat.decimal);
|
||||
|
||||
@override
|
||||
String getLabel(BuildContext context) => _formatBounds(context.read<Settings>().coordinateFormat);
|
||||
|
||||
@override
|
||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.geoBounds, size: size);
|
||||
|
||||
@override
|
||||
String get category => type;
|
||||
|
||||
@override
|
||||
String get key => '$type-$sw-$ne';
|
||||
}
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/coordinate.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
|
@ -24,6 +25,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
TypeFilter.type,
|
||||
AlbumFilter.type,
|
||||
LocationFilter.type,
|
||||
CoordinateFilter.type,
|
||||
TagFilter.type,
|
||||
PathFilter.type,
|
||||
];
|
||||
|
@ -35,20 +37,22 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
switch (type) {
|
||||
case AlbumFilter.type:
|
||||
return AlbumFilter.fromMap(jsonMap);
|
||||
case CoordinateFilter.type:
|
||||
return CoordinateFilter.fromMap(jsonMap);
|
||||
case FavouriteFilter.type:
|
||||
return FavouriteFilter.instance;
|
||||
case LocationFilter.type:
|
||||
return LocationFilter.fromMap(jsonMap);
|
||||
case TypeFilter.type:
|
||||
return TypeFilter.fromMap(jsonMap);
|
||||
case MimeFilter.type:
|
||||
return MimeFilter.fromMap(jsonMap);
|
||||
case PathFilter.type:
|
||||
return PathFilter.fromMap(jsonMap);
|
||||
case QueryFilter.type:
|
||||
return QueryFilter.fromMap(jsonMap);
|
||||
case TagFilter.type:
|
||||
return TagFilter.fromMap(jsonMap);
|
||||
case PathFilter.type:
|
||||
return PathFilter.fromMap(jsonMap);
|
||||
case TypeFilter.type:
|
||||
return TypeFilter.fromMap(jsonMap);
|
||||
}
|
||||
}
|
||||
debugPrint('failed to parse filter from json=$jsonString');
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:aves/geo/format.dart';
|
||||
import 'package:aves/utils/geo_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
@ -15,10 +15,10 @@ extension ExtraCoordinateFormat on CoordinateFormat {
|
|||
}
|
||||
}
|
||||
|
||||
String format(LatLng latLng) {
|
||||
String format(LatLng latLng, {bool minuteSecondPadding = false, int dmsSecondDecimals = 2}) {
|
||||
switch (this) {
|
||||
case CoordinateFormat.dms:
|
||||
return toDMS(latLng).join(', ');
|
||||
return GeoUtils.toDMS(latLng, minuteSecondPadding: minuteSecondPadding, secondDecimals: dmsSecondDecimals).join(', ');
|
||||
case CoordinateFormat.decimal:
|
||||
return [latLng.latitude, latLng.longitude].map((n) => n.toStringAsFixed(6)).join(', ');
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ class Settings extends ChangeNotifier {
|
|||
Future<void> setContextualDefaults() async {
|
||||
// performance
|
||||
final performanceClass = await deviceService.getPerformanceClass();
|
||||
enableOverlayBlurEffect = performanceClass >= 30;
|
||||
enableOverlayBlurEffect = performanceClass >= 29;
|
||||
|
||||
// availability
|
||||
final hasPlayServices = await availability.hasPlayServices;
|
||||
|
|
|
@ -46,6 +46,7 @@ class AIcons {
|
|||
static const IconData flip = Icons.flip_outlined;
|
||||
static const IconData favourite = Icons.favorite_border;
|
||||
static const IconData favouriteActive = Icons.favorite;
|
||||
static const IconData geoBounds = Icons.public_outlined;
|
||||
static const IconData goUp = Icons.arrow_upward_outlined;
|
||||
static const IconData group = Icons.group_work_outlined;
|
||||
static const IconData hide = Icons.visibility_off_outlined;
|
||||
|
|
|
@ -1,8 +1,48 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/utils/math_utils.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
LatLng getLatLngCenter(List<LatLng> points) {
|
||||
class GeoUtils {
|
||||
static String _decimal2sexagesimal(final double degDecimal, final bool minuteSecondPadding, final int secondDecimals) {
|
||||
List<int> _split(final double value) {
|
||||
// NumberFormat is necessary to create digit after comma if the value
|
||||
// has no decimal point (only necessary for browser)
|
||||
final tmp = NumberFormat('0.0#####').format(roundToPrecision(value, decimals: 10)).split('.');
|
||||
return <int>[
|
||||
int.parse(tmp[0]).abs(),
|
||||
int.parse(tmp[1]),
|
||||
];
|
||||
}
|
||||
|
||||
final deg = _split(degDecimal)[0];
|
||||
final minDecimal = (degDecimal.abs() - deg) * 60;
|
||||
final min = _split(minDecimal)[0];
|
||||
final sec = (minDecimal - min) * 60;
|
||||
|
||||
final secRounded = roundToPrecision(sec, decimals: secondDecimals);
|
||||
var minText = '$min';
|
||||
var secText = secRounded.toStringAsFixed(secondDecimals);
|
||||
if (minuteSecondPadding) {
|
||||
minText = minText.padLeft(2, '0');
|
||||
secText = secText.padLeft(secondDecimals > 0 ? 3 + secondDecimals : 2, '0');
|
||||
}
|
||||
|
||||
return '$deg° $minText′ $secText″';
|
||||
}
|
||||
|
||||
// returns coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E']
|
||||
static List<String> toDMS(LatLng latLng, {bool minuteSecondPadding = false, int secondDecimals = 2}) {
|
||||
final lat = latLng.latitude;
|
||||
final lng = latLng.longitude;
|
||||
return [
|
||||
'${_decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals)} ${lat < 0 ? 'S' : 'N'}',
|
||||
'${_decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals)} ${lng < 0 ? 'W' : 'E'}',
|
||||
];
|
||||
}
|
||||
|
||||
static LatLng getLatLngCenter(List<LatLng> points) {
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
double z = 0;
|
||||
|
@ -25,3 +65,15 @@ LatLng getLatLngCenter(List<LatLng> points) {
|
|||
final lat = atan2(z, hyp);
|
||||
return LatLng(radianToDeg(lat), radianToDeg(lng));
|
||||
}
|
||||
|
||||
static bool contains(LatLng sw, LatLng ne, LatLng? point) {
|
||||
if (point == null) return false;
|
||||
final lat = point.latitude;
|
||||
final lng = point.longitude;
|
||||
final south = sw.latitude;
|
||||
final north = ne.latitude;
|
||||
final west = sw.longitude;
|
||||
final east = ne.longitude;
|
||||
return (south <= lat && lat <= north) && (west <= east ? (west <= lng && lng <= east) : (west <= lng || lng <= east));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,12 +37,11 @@ class AvesFilterDecoration {
|
|||
|
||||
class AvesFilterChip extends StatefulWidget {
|
||||
final CollectionFilter filter;
|
||||
final bool removable;
|
||||
final bool showGenericIcon;
|
||||
final bool removable, showGenericIcon, useFilterColor;
|
||||
final AvesFilterDecoration? decoration;
|
||||
final String? banner;
|
||||
final Widget? details;
|
||||
final double padding;
|
||||
final double padding, maxWidth;
|
||||
final HeroType heroType;
|
||||
final FilterCallback? onTap;
|
||||
final OffsetFilterCallback? onLongPress;
|
||||
|
@ -52,7 +51,7 @@ class AvesFilterChip extends StatefulWidget {
|
|||
static const double outlineWidth = 2;
|
||||
static const double minChipHeight = kMinInteractiveDimension;
|
||||
static const double minChipWidth = 80;
|
||||
static const double maxChipWidth = 160;
|
||||
static const double defaultMaxChipWidth = 160;
|
||||
static const double iconSize = 18;
|
||||
static const double fontSize = 14;
|
||||
static const double decoratedContentVerticalPadding = 5;
|
||||
|
@ -62,10 +61,12 @@ class AvesFilterChip extends StatefulWidget {
|
|||
required this.filter,
|
||||
this.removable = false,
|
||||
this.showGenericIcon = true,
|
||||
this.useFilterColor = true,
|
||||
this.decoration,
|
||||
this.banner,
|
||||
this.details,
|
||||
this.padding = 6.0,
|
||||
this.maxWidth = defaultMaxChipWidth,
|
||||
this.heroType = HeroType.onTap,
|
||||
this.onTap,
|
||||
this.onLongPress = showDefaultLongPressMenu,
|
||||
|
@ -181,7 +182,6 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
|
@ -216,7 +216,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
);
|
||||
} else {
|
||||
content = Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: 2),
|
||||
padding: EdgeInsets.symmetric(horizontal: padding * 2),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
@ -224,9 +224,9 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius));
|
||||
final banner = widget.banner;
|
||||
Widget chip = Container(
|
||||
constraints: const BoxConstraints(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: AvesFilterChip.minChipWidth,
|
||||
maxWidth: AvesFilterChip.maxChipWidth,
|
||||
maxWidth: widget.maxWidth,
|
||||
minHeight: AvesFilterChip.minChipHeight,
|
||||
),
|
||||
child: Stack(
|
||||
|
@ -263,16 +263,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: _outlineColor,
|
||||
color: widget.useFilterColor ? _outlineColor : AvesFilterChip.defaultOutlineColor,
|
||||
width: AvesFilterChip.outlineWidth,
|
||||
)),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
position: DecorationPosition.foreground,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: decoration != null ? 0 : 8),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import 'package:aves/model/filters/coordinate.dart';
|
||||
import 'package:aves/model/settings/enums.dart';
|
||||
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/theme/icons.dart';
|
||||
import 'package:aves/utils/debouncer.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/map/compass.dart';
|
||||
import 'package:aves/widgets/common/map/theme.dart';
|
||||
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -58,14 +62,11 @@ class MapButtonPanel extends StatelessWidget {
|
|||
break;
|
||||
}
|
||||
|
||||
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);
|
||||
final visualDensity = context.select<MapThemeData, VisualDensity?>((v) => v.visualDensity);
|
||||
final double padding = visualDensity == VisualDensity.compact ? 4 : 8;
|
||||
|
||||
return Positioned.fill(
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(padding),
|
||||
child: TooltipTheme(
|
||||
data: TooltipTheme.of(context).copyWith(
|
||||
preferBelow: false,
|
||||
|
@ -75,7 +76,13 @@ class MapButtonPanel extends StatelessWidget {
|
|||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
left: padding,
|
||||
right: padding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: padding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@ -115,12 +122,17 @@ class MapButtonPanel extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MapOverlayButton(
|
||||
showCoordinateFilter
|
||||
? Expanded(
|
||||
child: _OverlayCoordinateFilterChip(
|
||||
boundsNotifier: boundsNotifier,
|
||||
padding: padding,
|
||||
),
|
||||
)
|
||||
: const Spacer(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: padding),
|
||||
child: MapOverlayButton(
|
||||
icon: const Icon(AIcons.layers),
|
||||
onPressed: () async {
|
||||
final hasPlayServices = await availability.hasPlayServices;
|
||||
|
@ -145,12 +157,13 @@ class MapButtonPanel extends StatelessWidget {
|
|||
},
|
||||
tooltip: context.l10n.mapStyleTooltip,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
right: padding,
|
||||
bottom: padding,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@ -172,8 +185,6 @@ class MapButtonPanel extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -225,3 +236,102 @@ class MapOverlayButton extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OverlayCoordinateFilterChip extends StatefulWidget {
|
||||
final ValueNotifier<ZoomedBounds> boundsNotifier;
|
||||
final double padding;
|
||||
|
||||
const _OverlayCoordinateFilterChip({
|
||||
Key? key,
|
||||
required this.boundsNotifier,
|
||||
required this.padding,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_OverlayCoordinateFilterChipState createState() => _OverlayCoordinateFilterChipState();
|
||||
}
|
||||
|
||||
class _OverlayCoordinateFilterChipState extends State<_OverlayCoordinateFilterChip> {
|
||||
final Debouncer _debouncer = Debouncer(delay: Durations.mapInfoDebounceDelay);
|
||||
final ValueNotifier<ZoomedBounds?> _idleBoundsNotifier = ValueNotifier(null);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _OverlayCoordinateFilterChip oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_unregisterWidget(oldWidget);
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unregisterWidget(widget);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget(_OverlayCoordinateFilterChip widget) {
|
||||
widget.boundsNotifier.addListener(_onBoundsChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(_OverlayCoordinateFilterChip widget) {
|
||||
widget.boundsNotifier.removeListener(_onBoundsChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final blurred = settings.enableOverlayBlurEffect;
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
scaffoldBackgroundColor: overlayBackgroundColor(blurred: blurred),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Selector<MapThemeData, Animation<double>>(
|
||||
selector: (context, v) => v.scale,
|
||||
builder: (context, scale, child) => SizeTransition(
|
||||
sizeFactor: scale,
|
||||
axisAlignment: 1,
|
||||
child: FadeTransition(
|
||||
opacity: scale,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: ValueListenableBuilder<ZoomedBounds?>(
|
||||
valueListenable: _idleBoundsNotifier,
|
||||
builder: (context, bounds, child) {
|
||||
if (bounds == null) return const SizedBox();
|
||||
final filter = CoordinateFilter(
|
||||
bounds.sw,
|
||||
bounds.ne,
|
||||
// more stable format when bounds change
|
||||
minuteSecondPadding: true,
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(widget.padding),
|
||||
child: BlurredRRect(
|
||||
enabled: blurred,
|
||||
borderRadius: AvesFilterChip.defaultRadius,
|
||||
child: AvesFilterChip(
|
||||
filter: filter,
|
||||
useFilterColor: false,
|
||||
maxWidth: double.infinity,
|
||||
onTap: (filter) => FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne) ).dispatch(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onBoundsChanged() {
|
||||
_debouncer(() => _idleBoundsNotifier.value = widget.boundsNotifier.value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ class _GeoMapState extends State<GeoMap> {
|
|||
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`)
|
||||
_slowMarkerCluster ??= _buildFluster(nodeSize: smallestPowerOf2(widget.entries.length));
|
||||
_slowMarkerCluster ??= _buildFluster(nodeSize: smallestPowerOf2(entries.length));
|
||||
points = _slowMarkerCluster!.points(clusterId);
|
||||
assert(points.length == geoEntry.pointsSize, 'got ${points.length}/${geoEntry.pointsSize} for geoEntry=$geoEntry');
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
|
|||
enum MapNavigationButton { back, map }
|
||||
|
||||
class MapTheme extends StatelessWidget {
|
||||
final bool interactive;
|
||||
final bool interactive, showCoordinateFilter;
|
||||
final MapNavigationButton navigationButton;
|
||||
final Animation<double> scale;
|
||||
final VisualDensity? visualDensity;
|
||||
|
@ -15,6 +15,7 @@ class MapTheme extends StatelessWidget {
|
|||
const MapTheme({
|
||||
Key? key,
|
||||
required this.interactive,
|
||||
required this.showCoordinateFilter,
|
||||
required this.navigationButton,
|
||||
this.scale = kAlwaysCompleteAnimation,
|
||||
this.visualDensity,
|
||||
|
@ -28,6 +29,7 @@ class MapTheme extends StatelessWidget {
|
|||
update: (context, settings, __) {
|
||||
return MapThemeData(
|
||||
interactive: interactive,
|
||||
showCoordinateFilter: showCoordinateFilter,
|
||||
navigationButton: navigationButton,
|
||||
scale: scale,
|
||||
visualDensity: visualDensity,
|
||||
|
@ -40,7 +42,7 @@ class MapTheme extends StatelessWidget {
|
|||
}
|
||||
|
||||
class MapThemeData {
|
||||
final bool interactive;
|
||||
final bool interactive, showCoordinateFilter;
|
||||
final MapNavigationButton navigationButton;
|
||||
final Animation<double> scale;
|
||||
final VisualDensity? visualDensity;
|
||||
|
@ -48,6 +50,7 @@ class MapThemeData {
|
|||
|
||||
const MapThemeData({
|
||||
required this.interactive,
|
||||
required this.showCoordinateFilter,
|
||||
required this.navigationButton,
|
||||
required this.scale,
|
||||
required this.visualDensity,
|
||||
|
|
|
@ -13,7 +13,7 @@ class ZoomedBounds extends Equatable {
|
|||
// returns [southwestLng, southwestLat, northeastLng, northeastLat], as expected by Fluster
|
||||
List<double> get boundingBox => [sw.longitude, sw.latitude, ne.longitude, ne.latitude];
|
||||
|
||||
LatLng get center => getLatLngCenter([sw, ne]);
|
||||
LatLng get center => GeoUtils.getLatLngCenter([sw, ne]);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [sw, ne, zoom, rotation];
|
||||
|
@ -63,13 +63,5 @@ class ZoomedBounds extends Equatable {
|
|||
);
|
||||
}
|
||||
|
||||
bool contains(LatLng point) {
|
||||
final lat = point.latitude;
|
||||
final lng = point.longitude;
|
||||
final south = sw.latitude;
|
||||
final north = ne.latitude;
|
||||
final west = sw.longitude;
|
||||
final east = ne.longitude;
|
||||
return (south <= lat && lat <= north) && (west <= east ? (west <= lng && lng <= east) : (west <= lng || lng <= east));
|
||||
}
|
||||
bool contains(LatLng point) => GeoUtils.contains(sw, ne, point);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/coordinate.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/map_style.dart';
|
||||
|
@ -8,6 +10,7 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/utils/debouncer.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
|
@ -20,6 +23,7 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
|||
import 'package:aves/widgets/common/thumbnail/scroller.dart';
|
||||
import 'package:aves/widgets/map/map_info_row.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -87,10 +91,10 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
|
|||
late AnimationController _overlayAnimationController;
|
||||
late Animation<double> _overlayScale, _scrollerSize;
|
||||
|
||||
List<AvesEntry> get entries => widget.collection.sortedEntries;
|
||||
|
||||
CollectionLens? get regionCollection => _regionCollectionNotifier.value;
|
||||
|
||||
CollectionLens get openingCollection => widget.collection;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -154,7 +158,12 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Settings, EntryMapStyle>(
|
||||
return NotificationListener<FilterSelectedNotification>(
|
||||
onNotification: (notification) {
|
||||
_goToCollection(notification.filter);
|
||||
return true;
|
||||
},
|
||||
child: Selector<Settings, EntryMapStyle>(
|
||||
selector: (context, s) => s.infoMapStyle,
|
||||
builder: (context, mapStyle, child) {
|
||||
late Widget scroller;
|
||||
|
@ -198,19 +207,21 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
|
|||
_buildScroller(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMap() {
|
||||
return MapTheme(
|
||||
interactive: true,
|
||||
showCoordinateFilter: true,
|
||||
navigationButton: MapNavigationButton.back,
|
||||
scale: _overlayScale,
|
||||
child: GeoMap(
|
||||
// key is expected by test driver
|
||||
key: const Key('map_view'),
|
||||
controller: _mapController,
|
||||
entries: entries,
|
||||
entries: openingCollection.sortedEntries,
|
||||
initialEntry: widget.initialEntry,
|
||||
isAnimatingNotifier: _isPageAnimatingNotifier,
|
||||
dotEntryNotifier: _dotEntryNotifier,
|
||||
|
@ -285,9 +296,13 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
|
|||
}
|
||||
|
||||
_regionCollectionNotifier.value = CollectionLens(
|
||||
source: widget.collection.source,
|
||||
source: openingCollection.source,
|
||||
listenToSource: false,
|
||||
fixedSelection: entries.where((entry) => bounds.contains(entry.latLng!)).toList(),
|
||||
fixedSelection: openingCollection.sortedEntries,
|
||||
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
|
||||
|
@ -345,6 +360,24 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
|
|||
);
|
||||
}
|
||||
|
||||
void _goToCollection(CollectionFilter filter) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) {
|
||||
return CollectionPage(
|
||||
collection: CollectionLens(
|
||||
source: openingCollection.source,
|
||||
filters: openingCollection.filters,
|
||||
)..addFilter(filter),
|
||||
);
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
|
||||
// overlay
|
||||
|
||||
void _toggleOverlay() => _overlayVisible.value = !_overlayVisible.value;
|
||||
|
|
|
@ -21,7 +21,7 @@ class FilterTable extends StatelessWidget {
|
|||
required this.onFilterSelection,
|
||||
}) : super(key: key);
|
||||
|
||||
static const chipWidth = AvesFilterChip.maxChipWidth;
|
||||
static const chipWidth = AvesFilterChip.defaultMaxChipWidth;
|
||||
static const countWidth = 32.0;
|
||||
static const percentIndicatorMinWidth = 80.0;
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ class _LocationSectionState extends State<LocationSection> {
|
|||
if (widget.showTitle) const SectionRow(icon: AIcons.location),
|
||||
MapTheme(
|
||||
interactive: false,
|
||||
showCoordinateFilter: false,
|
||||
navigationButton: MapNavigationButton.map,
|
||||
visualDensity: VisualDensity.compact,
|
||||
mapHeight: 200,
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import 'package:aves/geo/format.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('Decimal degrees to DMS (sexagesimal)', () {
|
||||
expect(toDMS(LatLng(37.496667, 127.0275)), ['37° 29′ 48.00″ N', '127° 1′ 39.00″ E']); // Gangnam
|
||||
expect(toDMS(LatLng(78.9243503, 11.9230465)), ['78° 55′ 27.66″ N', '11° 55′ 22.97″ E']); // Ny-Ålesund
|
||||
expect(toDMS(LatLng(-38.6965891, 175.9830047)), ['38° 41′ 47.72″ S', '175° 58′ 58.82″ E']); // Taupo
|
||||
expect(toDMS(LatLng(-64.249391, -56.6556145)), ['64° 14′ 57.81″ S', '56° 39′ 20.21″ W']); // Marambio
|
||||
expect(toDMS(LatLng(0, 0)), ['0° 0′ 0.00″ N', '0° 0′ 0.00″ E']);
|
||||
});
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/coordinate.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
|
@ -8,6 +9,7 @@ import 'package:aves/model/filters/query.dart';
|
|||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/type.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
@ -30,6 +32,9 @@ void main() {
|
|||
const album = AlbumFilter('path/to/album', 'album');
|
||||
expect(album, jsonRoundTrip(album));
|
||||
|
||||
final bounds = CoordinateFilter(LatLng(29.979167, 28.223615), LatLng(36.451000, 31.134167));
|
||||
expect(bounds, jsonRoundTrip(bounds));
|
||||
|
||||
const fav = FavouriteFilter.instance;
|
||||
expect(fav, jsonRoundTrip(fav));
|
||||
|
||||
|
|
|
@ -3,8 +3,16 @@ import 'package:latlong2/latlong.dart';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('Decimal degrees to DMS (sexagesimal)', () {
|
||||
expect(GeoUtils.toDMS(LatLng(37.496667, 127.0275)), ['37° 29′ 48.00″ N', '127° 1′ 39.00″ E']); // Gangnam
|
||||
expect(GeoUtils.toDMS(LatLng(78.9243503, 11.9230465)), ['78° 55′ 27.66″ N', '11° 55′ 22.97″ E']); // Ny-Ålesund
|
||||
expect(GeoUtils.toDMS(LatLng(-38.6965891, 175.9830047)), ['38° 41′ 47.72″ S', '175° 58′ 58.82″ E']); // Taupo
|
||||
expect(GeoUtils.toDMS(LatLng(-64.249391, -56.6556145)), ['64° 14′ 57.81″ S', '56° 39′ 20.21″ W']); // Marambio
|
||||
expect(GeoUtils.toDMS(LatLng(0, 0)), ['0° 0′ 0.00″ N', '0° 0′ 0.00″ E']);
|
||||
});
|
||||
|
||||
test('bounds center', () {
|
||||
expect(getLatLngCenter([LatLng(10, 30), LatLng(30, 50)]), LatLng(20.28236664671092, 39.351653000319956));
|
||||
expect(getLatLngCenter([LatLng(10, -179), LatLng(30, 179)]), LatLng(20.00279344048298, -179.9358157370226));
|
||||
expect(GeoUtils.getLatLngCenter([LatLng(10, 30), LatLng(30, 50)]), LatLng(20.28236664671092, 39.351653000319956));
|
||||
expect(GeoUtils.getLatLngCenter([LatLng(10, -179), LatLng(30, 179)]), LatLng(20.00279344048298, -179.9358157370226));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue