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/entry.dart';
|
||||||
import 'package:aves/model/filters/album.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/favourite.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
|
@ -24,6 +25,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
||||||
TypeFilter.type,
|
TypeFilter.type,
|
||||||
AlbumFilter.type,
|
AlbumFilter.type,
|
||||||
LocationFilter.type,
|
LocationFilter.type,
|
||||||
|
CoordinateFilter.type,
|
||||||
TagFilter.type,
|
TagFilter.type,
|
||||||
PathFilter.type,
|
PathFilter.type,
|
||||||
];
|
];
|
||||||
|
@ -35,20 +37,22 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case AlbumFilter.type:
|
case AlbumFilter.type:
|
||||||
return AlbumFilter.fromMap(jsonMap);
|
return AlbumFilter.fromMap(jsonMap);
|
||||||
|
case CoordinateFilter.type:
|
||||||
|
return CoordinateFilter.fromMap(jsonMap);
|
||||||
case FavouriteFilter.type:
|
case FavouriteFilter.type:
|
||||||
return FavouriteFilter.instance;
|
return FavouriteFilter.instance;
|
||||||
case LocationFilter.type:
|
case LocationFilter.type:
|
||||||
return LocationFilter.fromMap(jsonMap);
|
return LocationFilter.fromMap(jsonMap);
|
||||||
case TypeFilter.type:
|
|
||||||
return TypeFilter.fromMap(jsonMap);
|
|
||||||
case MimeFilter.type:
|
case MimeFilter.type:
|
||||||
return MimeFilter.fromMap(jsonMap);
|
return MimeFilter.fromMap(jsonMap);
|
||||||
|
case PathFilter.type:
|
||||||
|
return PathFilter.fromMap(jsonMap);
|
||||||
case QueryFilter.type:
|
case QueryFilter.type:
|
||||||
return QueryFilter.fromMap(jsonMap);
|
return QueryFilter.fromMap(jsonMap);
|
||||||
case TagFilter.type:
|
case TagFilter.type:
|
||||||
return TagFilter.fromMap(jsonMap);
|
return TagFilter.fromMap(jsonMap);
|
||||||
case PathFilter.type:
|
case TypeFilter.type:
|
||||||
return PathFilter.fromMap(jsonMap);
|
return TypeFilter.fromMap(jsonMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debugPrint('failed to parse filter from json=$jsonString');
|
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:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:latlong2/latlong.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) {
|
switch (this) {
|
||||||
case CoordinateFormat.dms:
|
case CoordinateFormat.dms:
|
||||||
return toDMS(latLng).join(', ');
|
return GeoUtils.toDMS(latLng, minuteSecondPadding: minuteSecondPadding, secondDecimals: dmsSecondDecimals).join(', ');
|
||||||
case CoordinateFormat.decimal:
|
case CoordinateFormat.decimal:
|
||||||
return [latLng.latitude, latLng.longitude].map((n) => n.toStringAsFixed(6)).join(', ');
|
return [latLng.latitude, latLng.longitude].map((n) => n.toStringAsFixed(6)).join(', ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,7 +142,7 @@ class Settings extends ChangeNotifier {
|
||||||
Future<void> setContextualDefaults() async {
|
Future<void> setContextualDefaults() async {
|
||||||
// performance
|
// performance
|
||||||
final performanceClass = await deviceService.getPerformanceClass();
|
final performanceClass = await deviceService.getPerformanceClass();
|
||||||
enableOverlayBlurEffect = performanceClass >= 30;
|
enableOverlayBlurEffect = performanceClass >= 29;
|
||||||
|
|
||||||
// availability
|
// availability
|
||||||
final hasPlayServices = await availability.hasPlayServices;
|
final hasPlayServices = await availability.hasPlayServices;
|
||||||
|
|
|
@ -46,6 +46,7 @@ class AIcons {
|
||||||
static const IconData flip = Icons.flip_outlined;
|
static const IconData flip = Icons.flip_outlined;
|
||||||
static const IconData favourite = Icons.favorite_border;
|
static const IconData favourite = Icons.favorite_border;
|
||||||
static const IconData favouriteActive = Icons.favorite;
|
static const IconData favouriteActive = Icons.favorite;
|
||||||
|
static const IconData geoBounds = Icons.public_outlined;
|
||||||
static const IconData goUp = Icons.arrow_upward_outlined;
|
static const IconData goUp = Icons.arrow_upward_outlined;
|
||||||
static const IconData group = Icons.group_work_outlined;
|
static const IconData group = Icons.group_work_outlined;
|
||||||
static const IconData hide = Icons.visibility_off_outlined;
|
static const IconData hide = Icons.visibility_off_outlined;
|
||||||
|
|
|
@ -1,27 +1,79 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/utils/math_utils.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
LatLng getLatLngCenter(List<LatLng> points) {
|
class GeoUtils {
|
||||||
double x = 0;
|
static String _decimal2sexagesimal(final double degDecimal, final bool minuteSecondPadding, final int secondDecimals) {
|
||||||
double y = 0;
|
List<int> _split(final double value) {
|
||||||
double z = 0;
|
// 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]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
points.forEach((point) {
|
final deg = _split(degDecimal)[0];
|
||||||
final lat = point.latitudeInRad;
|
final minDecimal = (degDecimal.abs() - deg) * 60;
|
||||||
final lng = point.longitudeInRad;
|
final min = _split(minDecimal)[0];
|
||||||
x += cos(lat) * cos(lng);
|
final sec = (minDecimal - min) * 60;
|
||||||
y += cos(lat) * sin(lng);
|
|
||||||
z += sin(lat);
|
|
||||||
});
|
|
||||||
|
|
||||||
final pointCount = points.length;
|
final secRounded = roundToPrecision(sec, decimals: secondDecimals);
|
||||||
x /= pointCount;
|
var minText = '$min';
|
||||||
y /= pointCount;
|
var secText = secRounded.toStringAsFixed(secondDecimals);
|
||||||
z /= pointCount;
|
if (minuteSecondPadding) {
|
||||||
|
minText = minText.padLeft(2, '0');
|
||||||
|
secText = secText.padLeft(secondDecimals > 0 ? 3 + secondDecimals : 2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
final lng = atan2(y, x);
|
return '$deg° $minText′ $secText″';
|
||||||
final hyp = sqrt(x * x + y * y);
|
}
|
||||||
final lat = atan2(z, hyp);
|
|
||||||
return LatLng(radianToDeg(lat), radianToDeg(lng));
|
// 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;
|
||||||
|
|
||||||
|
points.forEach((point) {
|
||||||
|
final lat = point.latitudeInRad;
|
||||||
|
final lng = point.longitudeInRad;
|
||||||
|
x += cos(lat) * cos(lng);
|
||||||
|
y += cos(lat) * sin(lng);
|
||||||
|
z += sin(lat);
|
||||||
|
});
|
||||||
|
|
||||||
|
final pointCount = points.length;
|
||||||
|
x /= pointCount;
|
||||||
|
y /= pointCount;
|
||||||
|
z /= pointCount;
|
||||||
|
|
||||||
|
final lng = atan2(y, x);
|
||||||
|
final hyp = sqrt(x * x + y * y);
|
||||||
|
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 {
|
class AvesFilterChip extends StatefulWidget {
|
||||||
final CollectionFilter filter;
|
final CollectionFilter filter;
|
||||||
final bool removable;
|
final bool removable, showGenericIcon, useFilterColor;
|
||||||
final bool showGenericIcon;
|
|
||||||
final AvesFilterDecoration? decoration;
|
final AvesFilterDecoration? decoration;
|
||||||
final String? banner;
|
final String? banner;
|
||||||
final Widget? details;
|
final Widget? details;
|
||||||
final double padding;
|
final double padding, maxWidth;
|
||||||
final HeroType heroType;
|
final HeroType heroType;
|
||||||
final FilterCallback? onTap;
|
final FilterCallback? onTap;
|
||||||
final OffsetFilterCallback? onLongPress;
|
final OffsetFilterCallback? onLongPress;
|
||||||
|
@ -52,7 +51,7 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
static const double outlineWidth = 2;
|
static const double outlineWidth = 2;
|
||||||
static const double minChipHeight = kMinInteractiveDimension;
|
static const double minChipHeight = kMinInteractiveDimension;
|
||||||
static const double minChipWidth = 80;
|
static const double minChipWidth = 80;
|
||||||
static const double maxChipWidth = 160;
|
static const double defaultMaxChipWidth = 160;
|
||||||
static const double iconSize = 18;
|
static const double iconSize = 18;
|
||||||
static const double fontSize = 14;
|
static const double fontSize = 14;
|
||||||
static const double decoratedContentVerticalPadding = 5;
|
static const double decoratedContentVerticalPadding = 5;
|
||||||
|
@ -62,10 +61,12 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
required this.filter,
|
required this.filter,
|
||||||
this.removable = false,
|
this.removable = false,
|
||||||
this.showGenericIcon = true,
|
this.showGenericIcon = true,
|
||||||
|
this.useFilterColor = true,
|
||||||
this.decoration,
|
this.decoration,
|
||||||
this.banner,
|
this.banner,
|
||||||
this.details,
|
this.details,
|
||||||
this.padding = 6.0,
|
this.padding = 6.0,
|
||||||
|
this.maxWidth = defaultMaxChipWidth,
|
||||||
this.heroType = HeroType.onTap,
|
this.heroType = HeroType.onTap,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress = showDefaultLongPressMenu,
|
this.onLongPress = showDefaultLongPressMenu,
|
||||||
|
@ -181,7 +182,6 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
),
|
),
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
maxLines: 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (trailing != null) ...[
|
if (trailing != null) ...[
|
||||||
|
@ -216,7 +216,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content = Padding(
|
content = Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: 2),
|
padding: EdgeInsets.symmetric(horizontal: padding * 2),
|
||||||
child: content,
|
child: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -224,9 +224,9 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius));
|
final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius));
|
||||||
final banner = widget.banner;
|
final banner = widget.banner;
|
||||||
Widget chip = Container(
|
Widget chip = Container(
|
||||||
constraints: const BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minWidth: AvesFilterChip.minChipWidth,
|
minWidth: AvesFilterChip.minChipWidth,
|
||||||
maxWidth: AvesFilterChip.maxChipWidth,
|
maxWidth: widget.maxWidth,
|
||||||
minHeight: AvesFilterChip.minChipHeight,
|
minHeight: AvesFilterChip.minChipHeight,
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
@ -263,16 +263,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.fromBorderSide(BorderSide(
|
border: Border.fromBorderSide(BorderSide(
|
||||||
color: _outlineColor,
|
color: widget.useFilterColor ? _outlineColor : AvesFilterChip.defaultOutlineColor,
|
||||||
width: AvesFilterChip.outlineWidth,
|
width: AvesFilterChip.outlineWidth,
|
||||||
)),
|
)),
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
position: DecorationPosition.foreground,
|
position: DecorationPosition.foreground,
|
||||||
child: Padding(
|
child: content,
|
||||||
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/enums.dart';
|
||||||
import 'package:aves/model/settings/map_style.dart';
|
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/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.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/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.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/compass.dart';
|
||||||
import 'package:aves/widgets/common/map/theme.dart';
|
import 'package:aves/widgets/common/map/theme.dart';
|
||||||
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.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:aves/widgets/viewer/overlay/common.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
@ -58,119 +62,126 @@ class MapButtonPanel extends StatelessWidget {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);
|
||||||
final visualDensity = context.select<MapThemeData, VisualDensity?>((v) => v.visualDensity);
|
final visualDensity = context.select<MapThemeData, VisualDensity?>((v) => v.visualDensity);
|
||||||
final double padding = visualDensity == VisualDensity.compact ? 4 : 8;
|
final double padding = visualDensity == VisualDensity.compact ? 4 : 8;
|
||||||
|
|
||||||
return Positioned.fill(
|
return Positioned.fill(
|
||||||
child: Align(
|
child: TooltipTheme(
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
data: TooltipTheme.of(context).copyWith(
|
||||||
child: Padding(
|
preferBelow: false,
|
||||||
padding: EdgeInsets.all(padding),
|
),
|
||||||
child: TooltipTheme(
|
child: SafeArea(
|
||||||
data: TooltipTheme.of(context).copyWith(
|
bottom: false,
|
||||||
preferBelow: false,
|
child: Stack(
|
||||||
),
|
children: [
|
||||||
child: SafeArea(
|
Positioned(
|
||||||
bottom: false,
|
left: padding,
|
||||||
child: Stack(
|
right: padding,
|
||||||
children: [
|
child: Row(
|
||||||
Positioned(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
left: 0,
|
children: [
|
||||||
child: Column(
|
Padding(
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: EdgeInsets.only(top: padding),
|
||||||
children: [
|
child: Column(
|
||||||
if (navigationButton != null) ...[
|
mainAxisSize: MainAxisSize.min,
|
||||||
navigationButton,
|
children: [
|
||||||
SizedBox(height: padding),
|
if (navigationButton != null) ...[
|
||||||
],
|
navigationButton,
|
||||||
ValueListenableBuilder<ZoomedBounds>(
|
SizedBox(height: padding),
|
||||||
valueListenable: boundsNotifier,
|
],
|
||||||
builder: (context, bounds, child) {
|
ValueListenableBuilder<ZoomedBounds>(
|
||||||
final degrees = bounds.rotation;
|
valueListenable: boundsNotifier,
|
||||||
final opacity = degrees == 0 ? .0 : 1.0;
|
builder: (context, bounds, child) {
|
||||||
final animationDuration = context.select<DurationsData, Duration>((v) => v.viewerOverlayAnimation);
|
final degrees = bounds.rotation;
|
||||||
return IgnorePointer(
|
final opacity = degrees == 0 ? .0 : 1.0;
|
||||||
ignoring: opacity == 0,
|
final animationDuration = context.select<DurationsData, Duration>((v) => v.viewerOverlayAnimation);
|
||||||
child: AnimatedOpacity(
|
return IgnorePointer(
|
||||||
opacity: opacity,
|
ignoring: opacity == 0,
|
||||||
duration: animationDuration,
|
child: AnimatedOpacity(
|
||||||
child: MapOverlayButton(
|
opacity: opacity,
|
||||||
icon: Transform(
|
duration: animationDuration,
|
||||||
origin: iconSize.center(Offset.zero),
|
child: MapOverlayButton(
|
||||||
transform: Matrix4.rotationZ(degToRadian(degrees)),
|
icon: Transform(
|
||||||
child: CustomPaint(
|
origin: iconSize.center(Offset.zero),
|
||||||
painter: CompassPainter(
|
transform: Matrix4.rotationZ(degToRadian(degrees)),
|
||||||
color: iconTheme.color!,
|
child: CustomPaint(
|
||||||
|
painter: CompassPainter(
|
||||||
|
color: iconTheme.color!,
|
||||||
|
),
|
||||||
|
size: iconSize,
|
||||||
),
|
),
|
||||||
size: iconSize,
|
|
||||||
),
|
),
|
||||||
|
onPressed: () => resetRotation?.call(),
|
||||||
|
tooltip: context.l10n.mapPointNorthUpTooltip,
|
||||||
),
|
),
|
||||||
onPressed: () => resetRotation?.call(),
|
|
||||||
tooltip: context.l10n.mapPointNorthUpTooltip,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
showCoordinateFilter
|
||||||
Positioned(
|
? Expanded(
|
||||||
right: 0,
|
child: _OverlayCoordinateFilterChip(
|
||||||
child: Column(
|
boundsNotifier: boundsNotifier,
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: padding,
|
||||||
children: [
|
),
|
||||||
MapOverlayButton(
|
)
|
||||||
icon: const Icon(AIcons.layers),
|
: const Spacer(),
|
||||||
onPressed: () async {
|
Padding(
|
||||||
final hasPlayServices = await availability.hasPlayServices;
|
padding: EdgeInsets.only(top: padding),
|
||||||
final availableStyles = EntryMapStyle.values.where((style) => !style.isGoogleMaps || hasPlayServices);
|
child: MapOverlayButton(
|
||||||
final preferredStyle = settings.infoMapStyle;
|
icon: const Icon(AIcons.layers),
|
||||||
final initialStyle = availableStyles.contains(preferredStyle) ? preferredStyle : availableStyles.first;
|
onPressed: () async {
|
||||||
final style = await showDialog<EntryMapStyle>(
|
final hasPlayServices = await availability.hasPlayServices;
|
||||||
context: context,
|
final availableStyles = EntryMapStyle.values.where((style) => !style.isGoogleMaps || hasPlayServices);
|
||||||
builder: (context) {
|
final preferredStyle = settings.infoMapStyle;
|
||||||
return AvesSelectionDialog<EntryMapStyle>(
|
final initialStyle = availableStyles.contains(preferredStyle) ? preferredStyle : availableStyles.first;
|
||||||
initialValue: initialStyle,
|
final style = await showDialog<EntryMapStyle>(
|
||||||
options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))),
|
context: context,
|
||||||
title: context.l10n.mapStyleTitle,
|
builder: (context) {
|
||||||
);
|
return AvesSelectionDialog<EntryMapStyle>(
|
||||||
},
|
initialValue: initialStyle,
|
||||||
);
|
options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
// wait for the dialog to hide as applying the change may block the UI
|
title: context.l10n.mapStyleTitle,
|
||||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
);
|
||||||
if (style != null && style != settings.infoMapStyle) {
|
},
|
||||||
settings.infoMapStyle = style;
|
);
|
||||||
}
|
// wait for the dialog to hide as applying the change may block the UI
|
||||||
},
|
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||||
tooltip: context.l10n.mapStyleTooltip,
|
if (style != null && style != settings.infoMapStyle) {
|
||||||
),
|
settings.infoMapStyle = style;
|
||||||
],
|
}
|
||||||
|
},
|
||||||
|
tooltip: context.l10n.mapStyleTooltip,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
Positioned(
|
),
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
MapOverlayButton(
|
|
||||||
icon: const Icon(AIcons.zoomIn),
|
|
||||||
onPressed: zoomBy != null ? () => zoomBy?.call(1) : null,
|
|
||||||
tooltip: context.l10n.mapZoomInTooltip,
|
|
||||||
),
|
|
||||||
SizedBox(height: padding),
|
|
||||||
MapOverlayButton(
|
|
||||||
icon: const Icon(AIcons.zoomOut),
|
|
||||||
onPressed: zoomBy != null ? () => zoomBy?.call(-1) : null,
|
|
||||||
tooltip: context.l10n.mapZoomOutTooltip,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
|
right: padding,
|
||||||
|
bottom: padding,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
MapOverlayButton(
|
||||||
|
icon: const Icon(AIcons.zoomIn),
|
||||||
|
onPressed: zoomBy != null ? () => zoomBy?.call(1) : null,
|
||||||
|
tooltip: context.l10n.mapZoomInTooltip,
|
||||||
|
),
|
||||||
|
SizedBox(height: padding),
|
||||||
|
MapOverlayButton(
|
||||||
|
icon: const Icon(AIcons.zoomOut),
|
||||||
|
onPressed: zoomBy != null ? () => zoomBy?.call(-1) : null,
|
||||||
|
tooltip: context.l10n.mapZoomOutTooltip,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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) {
|
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
|
||||||
// the higher `nodeSize` is, the higher the chance to get all the points (i.e. as many as the cluster `pointsSize`)
|
// 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);
|
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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
|
||||||
enum MapNavigationButton { back, map }
|
enum MapNavigationButton { back, map }
|
||||||
|
|
||||||
class MapTheme extends StatelessWidget {
|
class MapTheme extends StatelessWidget {
|
||||||
final bool interactive;
|
final bool interactive, showCoordinateFilter;
|
||||||
final MapNavigationButton navigationButton;
|
final MapNavigationButton navigationButton;
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
final VisualDensity? visualDensity;
|
final VisualDensity? visualDensity;
|
||||||
|
@ -15,6 +15,7 @@ class MapTheme extends StatelessWidget {
|
||||||
const MapTheme({
|
const MapTheme({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.interactive,
|
required this.interactive,
|
||||||
|
required this.showCoordinateFilter,
|
||||||
required this.navigationButton,
|
required this.navigationButton,
|
||||||
this.scale = kAlwaysCompleteAnimation,
|
this.scale = kAlwaysCompleteAnimation,
|
||||||
this.visualDensity,
|
this.visualDensity,
|
||||||
|
@ -28,6 +29,7 @@ class MapTheme extends StatelessWidget {
|
||||||
update: (context, settings, __) {
|
update: (context, settings, __) {
|
||||||
return MapThemeData(
|
return MapThemeData(
|
||||||
interactive: interactive,
|
interactive: interactive,
|
||||||
|
showCoordinateFilter: showCoordinateFilter,
|
||||||
navigationButton: navigationButton,
|
navigationButton: navigationButton,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
visualDensity: visualDensity,
|
visualDensity: visualDensity,
|
||||||
|
@ -40,7 +42,7 @@ class MapTheme extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapThemeData {
|
class MapThemeData {
|
||||||
final bool interactive;
|
final bool interactive, showCoordinateFilter;
|
||||||
final MapNavigationButton navigationButton;
|
final MapNavigationButton navigationButton;
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
final VisualDensity? visualDensity;
|
final VisualDensity? visualDensity;
|
||||||
|
@ -48,6 +50,7 @@ class MapThemeData {
|
||||||
|
|
||||||
const MapThemeData({
|
const MapThemeData({
|
||||||
required this.interactive,
|
required this.interactive,
|
||||||
|
required this.showCoordinateFilter,
|
||||||
required this.navigationButton,
|
required this.navigationButton,
|
||||||
required this.scale,
|
required this.scale,
|
||||||
required this.visualDensity,
|
required this.visualDensity,
|
||||||
|
|
|
@ -13,7 +13,7 @@ class ZoomedBounds extends Equatable {
|
||||||
// returns [southwestLng, southwestLat, northeastLng, northeastLat], as expected by Fluster
|
// returns [southwestLng, southwestLat, northeastLng, northeastLat], as expected by Fluster
|
||||||
List<double> get boundingBox => [sw.longitude, sw.latitude, ne.longitude, ne.latitude];
|
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
|
@override
|
||||||
List<Object?> get props => [sw, ne, zoom, rotation];
|
List<Object?> get props => [sw, ne, zoom, rotation];
|
||||||
|
@ -63,13 +63,5 @@ class ZoomedBounds extends Equatable {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool contains(LatLng point) {
|
bool contains(LatLng point) => GeoUtils.contains(sw, ne, 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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
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/highlight.dart';
|
||||||
import 'package:aves/model/settings/enums.dart';
|
import 'package:aves/model/settings/enums.dart';
|
||||||
import 'package:aves/model/settings/map_style.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/model/source/collection_lens.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/utils/debouncer.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/behaviour/routes.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/empty.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/common/thumbnail/scroller.dart';
|
||||||
import 'package:aves/widgets/map/map_info_row.dart';
|
import 'package:aves/widgets/map/map_info_row.dart';
|
||||||
import 'package:aves/widgets/viewer/entry_viewer_page.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:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
@ -87,10 +91,10 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
|
||||||
late AnimationController _overlayAnimationController;
|
late AnimationController _overlayAnimationController;
|
||||||
late Animation<double> _overlayScale, _scrollerSize;
|
late Animation<double> _overlayScale, _scrollerSize;
|
||||||
|
|
||||||
List<AvesEntry> get entries => widget.collection.sortedEntries;
|
|
||||||
|
|
||||||
CollectionLens? get regionCollection => _regionCollectionNotifier.value;
|
CollectionLens? get regionCollection => _regionCollectionNotifier.value;
|
||||||
|
|
||||||
|
CollectionLens get openingCollection => widget.collection;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -154,49 +158,55 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<Settings, EntryMapStyle>(
|
return NotificationListener<FilterSelectedNotification>(
|
||||||
selector: (context, s) => s.infoMapStyle,
|
onNotification: (notification) {
|
||||||
builder: (context, mapStyle, child) {
|
_goToCollection(notification.filter);
|
||||||
late Widget scroller;
|
return true;
|
||||||
if (mapStyle.isGoogleMaps) {
|
|
||||||
// the Google map widget is too heavy for a smooth resizing animation
|
|
||||||
// so we just toggle visibility when overlay animation is done
|
|
||||||
scroller = ValueListenableBuilder<double>(
|
|
||||||
valueListenable: _overlayAnimationController,
|
|
||||||
builder: (context, animation, child) {
|
|
||||||
return Visibility(
|
|
||||||
visible: !_overlayAnimationController.isDismissed,
|
|
||||||
child: child!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// the Leaflet map widget is light enough for a smooth resizing animation
|
|
||||||
scroller = FadeTransition(
|
|
||||||
opacity: _scrollerSize,
|
|
||||||
child: SizeTransition(
|
|
||||||
sizeFactor: _scrollerSize,
|
|
||||||
axisAlignment: 1.0,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildMap()),
|
|
||||||
scroller,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Selector<Settings, EntryMapStyle>(
|
||||||
mainAxisSize: MainAxisSize.min,
|
selector: (context, s) => s.infoMapStyle,
|
||||||
children: [
|
builder: (context, mapStyle, child) {
|
||||||
const SizedBox(height: 8),
|
late Widget scroller;
|
||||||
const Divider(height: 0),
|
if (mapStyle.isGoogleMaps) {
|
||||||
_buildScroller(),
|
// the Google map widget is too heavy for a smooth resizing animation
|
||||||
],
|
// so we just toggle visibility when overlay animation is done
|
||||||
|
scroller = ValueListenableBuilder<double>(
|
||||||
|
valueListenable: _overlayAnimationController,
|
||||||
|
builder: (context, animation, child) {
|
||||||
|
return Visibility(
|
||||||
|
visible: !_overlayAnimationController.isDismissed,
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// the Leaflet map widget is light enough for a smooth resizing animation
|
||||||
|
scroller = FadeTransition(
|
||||||
|
opacity: _scrollerSize,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: _scrollerSize,
|
||||||
|
axisAlignment: 1.0,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildMap()),
|
||||||
|
scroller,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Divider(height: 0),
|
||||||
|
_buildScroller(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -204,13 +214,14 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
|
||||||
Widget _buildMap() {
|
Widget _buildMap() {
|
||||||
return MapTheme(
|
return MapTheme(
|
||||||
interactive: true,
|
interactive: true,
|
||||||
|
showCoordinateFilter: true,
|
||||||
navigationButton: MapNavigationButton.back,
|
navigationButton: MapNavigationButton.back,
|
||||||
scale: _overlayScale,
|
scale: _overlayScale,
|
||||||
child: GeoMap(
|
child: GeoMap(
|
||||||
// key is expected by test driver
|
// key is expected by test driver
|
||||||
key: const Key('map_view'),
|
key: const Key('map_view'),
|
||||||
controller: _mapController,
|
controller: _mapController,
|
||||||
entries: entries,
|
entries: openingCollection.sortedEntries,
|
||||||
initialEntry: widget.initialEntry,
|
initialEntry: widget.initialEntry,
|
||||||
isAnimatingNotifier: _isPageAnimatingNotifier,
|
isAnimatingNotifier: _isPageAnimatingNotifier,
|
||||||
dotEntryNotifier: _dotEntryNotifier,
|
dotEntryNotifier: _dotEntryNotifier,
|
||||||
|
@ -285,9 +296,13 @@ class _MapPageContentState extends State<MapPageContent> with SingleTickerProvid
|
||||||
}
|
}
|
||||||
|
|
||||||
_regionCollectionNotifier.value = CollectionLens(
|
_regionCollectionNotifier.value = CollectionLens(
|
||||||
source: widget.collection.source,
|
source: openingCollection.source,
|
||||||
listenToSource: false,
|
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
|
// 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
|
// overlay
|
||||||
|
|
||||||
void _toggleOverlay() => _overlayVisible.value = !_overlayVisible.value;
|
void _toggleOverlay() => _overlayVisible.value = !_overlayVisible.value;
|
||||||
|
|
|
@ -21,7 +21,7 @@ class FilterTable extends StatelessWidget {
|
||||||
required this.onFilterSelection,
|
required this.onFilterSelection,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
static const chipWidth = AvesFilterChip.maxChipWidth;
|
static const chipWidth = AvesFilterChip.defaultMaxChipWidth;
|
||||||
static const countWidth = 32.0;
|
static const countWidth = 32.0;
|
||||||
static const percentIndicatorMinWidth = 80.0;
|
static const percentIndicatorMinWidth = 80.0;
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
if (widget.showTitle) const SectionRow(icon: AIcons.location),
|
if (widget.showTitle) const SectionRow(icon: AIcons.location),
|
||||||
MapTheme(
|
MapTheme(
|
||||||
interactive: false,
|
interactive: false,
|
||||||
|
showCoordinateFilter: false,
|
||||||
navigationButton: MapNavigationButton.map,
|
navigationButton: MapNavigationButton.map,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
mapHeight: 200,
|
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/album.dart';
|
||||||
|
import 'package:aves/model/filters/coordinate.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.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/tag.dart';
|
||||||
import 'package:aves/model/filters/type.dart';
|
import 'package:aves/model/filters/type.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
@ -30,6 +32,9 @@ void main() {
|
||||||
const album = AlbumFilter('path/to/album', 'album');
|
const album = AlbumFilter('path/to/album', 'album');
|
||||||
expect(album, jsonRoundTrip(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;
|
const fav = FavouriteFilter.instance;
|
||||||
expect(fav, jsonRoundTrip(fav));
|
expect(fav, jsonRoundTrip(fav));
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,16 @@ import 'package:latlong2/latlong.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
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', () {
|
test('bounds center', () {
|
||||||
expect(getLatLngCenter([LatLng(10, 30), LatLng(30, 50)]), LatLng(20.28236664671092, 39.351653000319956));
|
expect(GeoUtils.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, -179), LatLng(30, 179)]), LatLng(20.00279344048298, -179.9358157370226));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue