From fe40408c07debadd8111aa11113bdac92cbb2637 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 9 Aug 2020 14:53:14 +0900 Subject: [PATCH] info: alternate map styles --- lib/flutter_version.dart | 2 + lib/model/settings.dart | 6 + lib/widgets/app_drawer.dart | 21 ++ lib/widgets/common/icons.dart | 1 + .../fullscreen/info/location_section.dart | 145 +++--------- .../fullscreen/info/maps/google_map.dart | 118 ++++++++++ .../fullscreen/info/maps/leaflet_map.dart | 216 ++++++++++++++++++ .../fullscreen/info/maps/scale_layer.dart | 141 ++++++++++++ .../fullscreen/info/maps/scalebar_utils.dart | 126 ++++++++++ lib/widgets/settings_page.dart | 65 ++++++ pubspec.lock | 140 ++++++++++++ pubspec.yaml | 2 + 12 files changed, 867 insertions(+), 116 deletions(-) create mode 100644 lib/widgets/fullscreen/info/maps/google_map.dart create mode 100644 lib/widgets/fullscreen/info/maps/leaflet_map.dart create mode 100644 lib/widgets/fullscreen/info/maps/scale_layer.dart create mode 100644 lib/widgets/fullscreen/info/maps/scalebar_utils.dart create mode 100644 lib/widgets/settings_page.dart diff --git a/lib/flutter_version.dart b/lib/flutter_version.dart index bb9c69953..cee1bcb93 100644 --- a/lib/flutter_version.dart +++ b/lib/flutter_version.dart @@ -1,4 +1,6 @@ // run `scripts/update_flutter_version.sh` to update with the content of `flutter --version --machine` +// note on static analysis: the output of the script above yields double quotes, just like the example below +// ignore_for_file: prefer_single_quotes const Map version = { "channel": "unknown", "dartSdkVersion": "unknown", diff --git a/lib/model/settings.dart b/lib/model/settings.dart index 7de40ad4c..801340b09 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -1,4 +1,5 @@ import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -18,6 +19,7 @@ class Settings { static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; static const collectionTileExtentKey = 'collection_tile_extent'; + static const infoMapStyleKey = 'info_map_style'; static const infoMapZoomKey = 'info_map_zoom'; static const catalogTimeZoneKey = 'catalog_time_zone'; static const hasAcceptedTermsKey = 'has_accepted_terms'; @@ -50,6 +52,10 @@ class Settings { } } + EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); + + set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); + double get infoMapZoom => _prefs.getDouble(infoMapZoomKey) ?? 12; set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index e9eac9d66..c0ee4e7f3 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -21,6 +21,7 @@ import 'package:aves/widgets/common/aves_logo.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/debug_page.dart'; import 'package:aves/widgets/filter_grid_page.dart'; +import 'package:aves/widgets/settings_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -93,6 +94,15 @@ class _AppDrawerState extends State { title: 'Favourites', filter: FavouriteFilter(), ); + final settingsEntry = SafeArea( + top: false, + bottom: false, + child: ListTile( + leading: Icon(AIcons.settings), + title: Text('Preferences'), + onTap: () => _goToSettings(context), + ), + ); final aboutEntry = SafeArea( top: false, bottom: false, @@ -114,6 +124,7 @@ class _AppDrawerState extends State { _buildCountrySection(), _buildTagSection(), Divider(), + settingsEntry, aboutEntry, if (kDebugMode) ...[ Divider(), @@ -310,6 +321,16 @@ class _AppDrawerState extends State { ); } + void _goToSettings(BuildContext context) { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SettingsPage(), + ), + ); + } + void _goToAbout(BuildContext context) { Navigator.pop(context); Navigator.push( diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/icons.dart index 1c10cdcaf..e08962239 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/icons.dart @@ -19,6 +19,7 @@ class AIcons { static const IconData location = OMIcons.place; static const IconData shooting = OMIcons.camera; static const IconData removableStorage = OMIcons.sdStorage; + static const IconData settings = OMIcons.settings; static const IconData text = OMIcons.formatQuote; static const IconData tag = OMIcons.localOffer; diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index 26b57c96f..380bdbc59 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -2,13 +2,13 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/services/android_app_service.dart'; import 'package:aves/utils/geo_utils.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/maps/google_map.dart'; +import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart'; import 'package:flutter/material.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; class LocationSection extends StatefulWidget { final CollectionLens collection; @@ -94,15 +94,20 @@ class _LocationSectionState extends State { padding: EdgeInsets.only(bottom: 8), child: SectionRow(AIcons.location), ), - ImageMap( - markerId: entry.uri ?? entry.path, - latLng: LatLng( - entry.latLng.item1, - entry.latLng.item2, + if (settings.infoMapStyle == EntryMapStyle.google) + EntryGoogleMap( + markerId: entry.uri ?? entry.path, + latLng: entry.latLng, + geoUri: entry.geoUri, + initialZoom: settings.infoMapZoom, + ) + else + EntryLeafletMap( + latLng: entry.latLng, + geoUri: entry.geoUri, + initialZoom: settings.infoMapZoom, + style: settings.infoMapStyle, ), - geoUri: entry.geoUri, - initialZoom: settings.infoMapZoom, - ), if (location.isNotEmpty) Padding( padding: EdgeInsets.only(top: 8), @@ -133,113 +138,21 @@ class _LocationSectionState extends State { void _handleChange() => setState(() {}); } -class ImageMap extends StatefulWidget { - final String markerId; - final LatLng latLng; - final String geoUri; - final double initialZoom; +enum EntryMapStyle { google, osmHot, stamenToner, stamenWatercolor } - const ImageMap({ - Key key, - this.markerId, - this.latLng, - this.geoUri, - this.initialZoom, - }) : super(key: key); - - @override - State createState() => ImageMapState(); -} - -class ImageMapState extends State with AutomaticKeepAliveClientMixin { - GoogleMapController _controller; - - @override - void didUpdateWidget(ImageMap oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.latLng != oldWidget.latLng && _controller != null) { - _controller.moveCamera(CameraUpdate.newLatLng(widget.latLng)); +extension ExtraEntryMapStyle on EntryMapStyle { + String get name { + switch (this) { + case EntryMapStyle.google: + return 'Google Maps'; + case EntryMapStyle.osmHot: + return 'Humanitarian OpenStreetMap'; + case EntryMapStyle.stamenToner: + return 'Stamen Toner'; + case EntryMapStyle.stamenWatercolor: + return 'Stamen Watercolor'; + default: + return toString(); } } - - @override - Widget build(BuildContext context) { - super.build(context); - final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue; - return Row( - children: [ - Expanded( - child: GestureDetector( - onScaleStart: (details) { - // absorb scale gesture here to prevent scrolling - // and triggering by mistake a move to the image page above - }, - child: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular(16), - ), - child: Container( - color: Colors.white70, - height: 200, - child: GoogleMap( - // GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493 - initialCameraPosition: CameraPosition( - target: widget.latLng, - zoom: widget.initialZoom, - ), - onMapCreated: (controller) => setState(() => _controller = controller), - rotateGesturesEnabled: false, - scrollGesturesEnabled: false, - zoomControlsEnabled: false, - zoomGesturesEnabled: false, - liteModeEnabled: false, - tiltGesturesEnabled: false, - myLocationEnabled: false, - myLocationButtonEnabled: false, - markers: { - Marker( - markerId: MarkerId(widget.markerId), - icon: BitmapDescriptor.defaultMarkerWithHue(accentHue), - position: widget.latLng, - ) - }, - ), - ), - ), - ), - ), - SizedBox(width: 8), - TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: Column(children: [ - IconButton( - icon: Icon(AIcons.zoomIn), - onPressed: _controller == null ? null : () => _zoomBy(1), - tooltip: 'Zoom in', - ), - IconButton( - icon: Icon(AIcons.zoomOut), - onPressed: _controller == null ? null : () => _zoomBy(-1), - tooltip: 'Zoom out', - ), - IconButton( - icon: Icon(AIcons.openInNew), - onPressed: () => AndroidAppService.openMap(widget.geoUri), - tooltip: 'Show on map...', - ), - ]), - ) - ], - ); - } - - void _zoomBy(double amount) { - settings.infoMapZoom += amount; - _controller.animateCamera(CameraUpdate.zoomBy(amount)); - } - - @override - bool get wantKeepAlive => true; } diff --git a/lib/widgets/fullscreen/info/maps/google_map.dart b/lib/widgets/fullscreen/info/maps/google_map.dart new file mode 100644 index 000000000..39fae754a --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/google_map.dart @@ -0,0 +1,118 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/services/android_app_service.dart'; +import 'package:aves/widgets/common/icons.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:tuple/tuple.dart'; + +class EntryGoogleMap extends StatefulWidget { + final String markerId; + final LatLng latLng; + final String geoUri; + final double initialZoom; + + EntryGoogleMap({ + Key key, + this.markerId, + Tuple2 latLng, + this.geoUri, + this.initialZoom, + }) : latLng = LatLng(latLng.item1, latLng.item2), + super(key: key); + + @override + State createState() => EntryGoogleMapState(); +} + +class EntryGoogleMapState extends State with AutomaticKeepAliveClientMixin { + GoogleMapController _controller; + + @override + void didUpdateWidget(EntryGoogleMap oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.latLng != oldWidget.latLng && _controller != null) { + _controller.moveCamera(CameraUpdate.newLatLng(widget.latLng)); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue; + return Row( + children: [ + Expanded( + child: GestureDetector( + onScaleStart: (details) { + // absorb scale gesture here to prevent scrolling + // and triggering by mistake a move to the image page above + }, + child: ClipRRect( + borderRadius: BorderRadius.all( + Radius.circular(16), + ), + child: Container( + color: Colors.white70, + height: 200, + child: GoogleMap( + // GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493 + initialCameraPosition: CameraPosition( + target: widget.latLng, + zoom: widget.initialZoom, + ), + onMapCreated: (controller) => setState(() => _controller = controller), + rotateGesturesEnabled: false, + scrollGesturesEnabled: false, + zoomControlsEnabled: false, + zoomGesturesEnabled: false, + liteModeEnabled: false, + tiltGesturesEnabled: false, + myLocationEnabled: false, + myLocationButtonEnabled: false, + markers: { + Marker( + markerId: MarkerId(widget.markerId), + icon: BitmapDescriptor.defaultMarkerWithHue(accentHue), + position: widget.latLng, + ) + }, + ), + ), + ), + ), + ), + SizedBox(width: 8), + TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: Column(children: [ + IconButton( + icon: Icon(AIcons.zoomIn), + onPressed: _controller == null ? null : () => _zoomBy(1), + tooltip: 'Zoom in', + ), + IconButton( + icon: Icon(AIcons.zoomOut), + onPressed: _controller == null ? null : () => _zoomBy(-1), + tooltip: 'Zoom out', + ), + IconButton( + icon: Icon(AIcons.openInNew), + onPressed: () => AndroidAppService.openMap(widget.geoUri), + tooltip: 'Show on map...', + ), + ]), + ) + ], + ); + } + + void _zoomBy(double amount) { + settings.infoMapZoom += amount; + _controller.animateCamera(CameraUpdate.zoomBy(amount)); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/fullscreen/info/maps/leaflet_map.dart b/lib/widgets/fullscreen/info/maps/leaflet_map.dart new file mode 100644 index 000000000..de913348f --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/leaflet_map.dart @@ -0,0 +1,216 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/services/android_app_service.dart'; +import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/fullscreen/info/maps/scale_layer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong/latlong.dart'; +import 'package:tuple/tuple.dart'; + +import '../location_section.dart'; + +class EntryLeafletMap extends StatefulWidget { + final LatLng latLng; + final String geoUri; + final double initialZoom; + final EntryMapStyle style; + + EntryLeafletMap({ + Key key, + Tuple2 latLng, + this.geoUri, + this.initialZoom, + this.style, + }) : latLng = LatLng(latLng.item1, latLng.item2), + super(key: key); + + @override + State createState() => EntryLeafletMapState(); +} + +class EntryLeafletMapState extends State with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { + final MapController _mapController = MapController(); + + static const markerSize = 40.0; + + @override + void didUpdateWidget(EntryLeafletMap oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.latLng != oldWidget.latLng && _mapController != null) { + _mapController.move(widget.latLng, settings.infoMapZoom); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + final accentColor = Theme.of(context).accentColor; + return Row( + children: [ + Expanded( + child: GestureDetector( + onScaleStart: (details) { + // absorb scale gesture here to prevent scrolling + // and triggering by mistake a move to the image page above + }, + child: ClipRRect( + borderRadius: BorderRadius.all( + Radius.circular(16), + ), + child: Container( + color: Colors.white70, + height: 200, + child: FlutterMap( + options: MapOptions( + center: widget.latLng, + zoom: widget.initialZoom, + interactive: false, + ), + children: [ + _buildMapLayer(), + ScaleLayerWidget( + options: ScaleLayerOptions( + lineColor: accentColor, + lineWidth: 2, + textStyle: TextStyle( + color: accentColor, + fontSize: 12, + ), + padding: EdgeInsets.all(8), + ), + ), + MarkerLayerWidget( + options: MarkerLayerOptions( + markers: [ + Marker( + width: markerSize, + height: markerSize, + point: widget.latLng, + builder: (ctx) { + return Icon( + Icons.place, + size: markerSize, + color: accentColor, + ); + }, + anchorPos: AnchorPos.align(AnchorAlign.top), + ), + ], + ), + ), + ], + mapController: _mapController, + ), + ), + ), + ), + ), + SizedBox(width: 8), + TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: Column(children: [ + IconButton( + icon: Icon(AIcons.zoomIn), + onPressed: _mapController == null ? null : () => _zoomBy(1), + tooltip: 'Zoom in', + ), + IconButton( + icon: Icon(AIcons.zoomOut), + onPressed: _mapController == null ? null : () => _zoomBy(-1), + tooltip: 'Zoom out', + ), + IconButton( + icon: Icon(AIcons.openInNew), + onPressed: () => AndroidAppService.openMap(widget.geoUri), + tooltip: 'Show on map...', + ), + ]), + ) + ], + ); + } + + Widget _buildMapLayer() { + switch (widget.style) { + case EntryMapStyle.osmHot: + return OSMHotLayer(); + case EntryMapStyle.stamenToner: + return StamenTonerLayer(); + case EntryMapStyle.stamenWatercolor: + return StamenWatercolorLayer(); + default: + return SizedBox.shrink(); + } + } + + void _zoomBy(double amount) { + final endZoom = (settings.infoMapZoom + amount).clamp(1.0, 16.0); + settings.infoMapZoom = endZoom; + + final zoomTween = Tween(begin: _mapController.zoom, end: endZoom); + final controller = AnimationController(duration: const Duration(milliseconds: 200), vsync: this); + final animation = CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn); + controller.addListener(() => _mapController.move(widget.latLng, zoomTween.evaluate(animation))); + animation.addStatusListener((status) { + if (status == AnimationStatus.completed) { + controller.dispose(); + } else if (status == AnimationStatus.dismissed) { + controller.dispose(); + } + }); + controller.forward(); + } + + @override + bool get wantKeepAlive => true; +} + +class OSMHotLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TileLayerWidget( + options: TileLayerOptions( + // attribution: '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France' + minZoom: 1, + maxZoom: 19, + urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + retinaMode: MediaQuery.of(context).devicePixelRatio > 1, + ), + ); + } +} + +class StamenTonerLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TileLayerWidget( + options: TileLayerOptions( + // attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', + minZoom: 1, + maxZoom: 20, + urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png', + subdomains: ['a', 'b', 'c', 'd'], + retinaMode: MediaQuery.of(context).devicePixelRatio > 1, + ), + ); + } +} + +class StamenWatercolorLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TileLayerWidget( + options: TileLayerOptions( + // attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', + minZoom: 1, + maxZoom: 16, + urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg', + subdomains: ['a', 'b', 'c', 'd'], + retinaMode: MediaQuery.of(context).devicePixelRatio > 1, + ), + ); + } +} diff --git a/lib/widgets/fullscreen/info/maps/scale_layer.dart b/lib/widgets/fullscreen/info/maps/scale_layer.dart new file mode 100644 index 000000000..06cf170d2 --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/scale_layer.dart @@ -0,0 +1,141 @@ +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; + +import 'scalebar_utils.dart' as util; + +class ScaleLayerOptions extends LayerOptions { + TextStyle textStyle; + Color lineColor; + double lineWidth; + final EdgeInsets padding; + + ScaleLayerOptions({ + Key key, + this.textStyle, + this.lineColor = Colors.white, + this.lineWidth = 2, + this.padding, + rebuild, + }) : super(key: key, rebuild: rebuild); +} + +class ScaleLayerWidget extends StatelessWidget { + final ScaleLayerOptions options; + + ScaleLayerWidget({@required this.options}) : super(key: options.key); + + @override + Widget build(BuildContext context) { + final mapState = MapState.of(context); + return ScaleLayer(options, mapState, mapState.onMoved); + } +} + +class ScaleLayer extends StatelessWidget { + final ScaleLayerOptions scaleLayerOpts; + final MapState map; + final Stream stream; + final scale = [ + 25000000, + 15000000, + 8000000, + 4000000, + 2000000, + 1000000, + 500000, + 250000, + 100000, + 50000, + 25000, + 15000, + 8000, + 4000, + 2000, + 1000, + 500, + 250, + 100, + 50, + 25, + 10, + 5, + ]; + + ScaleLayer(this.scaleLayerOpts, this.map, this.stream) : super(key: scaleLayerOpts.key); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: stream, + builder: (context, snapshot) { + var zoom = map.zoom; + var distance = scale[max(0, min(20, zoom.round() + 2))].toDouble(); + var center = map.center; + var start = map.project(center); + var targetPoint = util.calculateEndingGlobalCoordinates(center, 90, distance); + var end = map.project(targetPoint); + var displayDistance = distance > 999 ? '${(distance / 1000).toStringAsFixed(0)} km' : '${distance.toStringAsFixed(0)} m'; + double width = (end.x - start.x); + + return CustomPaint( + painter: ScalePainter( + width, + displayDistance, + lineColor: scaleLayerOpts.lineColor, + lineWidth: scaleLayerOpts.lineWidth, + padding: scaleLayerOpts.padding, + textStyle: scaleLayerOpts.textStyle, + ), + ); + }, + ); + } +} + +class ScalePainter extends CustomPainter { + ScalePainter(this.width, this.text, {this.padding, this.textStyle, this.lineWidth, this.lineColor}); + + final double width; + final EdgeInsets padding; + final String text; + TextStyle textStyle; + double lineWidth; + Color lineColor; + + @override + void paint(ui.Canvas canvas, ui.Size size) { + final paint = Paint() + ..color = lineColor + ..strokeCap = StrokeCap.square + ..strokeWidth = lineWidth; + + var sizeForStartEnd = 4; + var paddingLeft = padding == null ? 0 : padding.left + sizeForStartEnd / 2; + var paddingTop = padding == null ? 0 : padding.top; + + var textSpan = TextSpan(style: textStyle, text: text); + var textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr)..layout(); + textPainter.paint(canvas, Offset(width / 2 - textPainter.width / 2 + paddingLeft, paddingTop)); + paddingTop += textPainter.height; + var p1 = Offset(paddingLeft, sizeForStartEnd + paddingTop); + var p2 = Offset(paddingLeft + width, sizeForStartEnd + paddingTop); + // draw start line + canvas.drawLine(Offset(paddingLeft, paddingTop), Offset(paddingLeft, sizeForStartEnd + paddingTop), paint); + // draw middle line + var middleX = width / 2 + paddingLeft - lineWidth / 2; + canvas.drawLine(Offset(middleX, paddingTop + sizeForStartEnd / 2), Offset(middleX, sizeForStartEnd + paddingTop), paint); + // draw end line + canvas.drawLine(Offset(width + paddingLeft, paddingTop), Offset(width + paddingLeft, sizeForStartEnd + paddingTop), paint); + // draw bottom line + canvas.drawLine(p1, p2, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/widgets/fullscreen/info/maps/scalebar_utils.dart b/lib/widgets/fullscreen/info/maps/scalebar_utils.dart new file mode 100644 index 000000000..df781009a --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/scalebar_utils.dart @@ -0,0 +1,126 @@ +import 'dart:math'; + +import 'package:latlong/latlong.dart'; + +const double piOver180 = PI / 180.0; + +double toDegrees(double radians) { + return radians / piOver180; +} + +double toRadians(double degrees) { + return degrees * piOver180; +} + +LatLng calculateEndingGlobalCoordinates(LatLng start, double startBearing, double distance) { + var mSemiMajorAxis = 6378137.0; //WGS84 major axis + var mSemiMinorAxis = (1.0 - 1.0 / 298.257223563) * 6378137.0; + var mFlattening = 1.0 / 298.257223563; + // double mInverseFlattening = 298.257223563; + + var a = mSemiMajorAxis; + var b = mSemiMinorAxis; + var aSquared = a * a; + var bSquared = b * b; + var f = mFlattening; + var phi1 = toRadians(start.latitude); + var alpha1 = toRadians(startBearing); + var cosAlpha1 = cos(alpha1); + var sinAlpha1 = sin(alpha1); + var s = distance; + var tanU1 = (1.0 - f) * tan(phi1); + var cosU1 = 1.0 / sqrt(1.0 + tanU1 * tanU1); + var sinU1 = tanU1 * cosU1; + + // eq. 1 + var sigma1 = atan2(tanU1, cosAlpha1); + + // eq. 2 + var sinAlpha = cosU1 * sinAlpha1; + + var sin2Alpha = sinAlpha * sinAlpha; + var cos2Alpha = 1 - sin2Alpha; + var uSquared = cos2Alpha * (aSquared - bSquared) / bSquared; + + // eq. 3 + var A = 1 + (uSquared / 16384) * (4096 + uSquared * (-768 + uSquared * (320 - 175 * uSquared))); + + // eq. 4 + var B = (uSquared / 1024) * (256 + uSquared * (-128 + uSquared * (74 - 47 * uSquared))); + + // iterate until there is a negligible change in sigma + double deltaSigma; + var sOverbA = s / (b * A); + var sigma = sOverbA; + double sinSigma; + var prevSigma = sOverbA; + double sigmaM2; + double cosSigmaM2; + double cos2SigmaM2; + + for (;;) { + // eq. 5 + sigmaM2 = 2.0 * sigma1 + sigma; + cosSigmaM2 = cos(sigmaM2); + cos2SigmaM2 = cosSigmaM2 * cosSigmaM2; + sinSigma = sin(sigma); + var cosSignma = cos(sigma); + + // eq. 6 + deltaSigma = B * sinSigma * (cosSigmaM2 + (B / 4.0) * (cosSignma * (-1 + 2 * cos2SigmaM2) - (B / 6.0) * cosSigmaM2 * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM2))); + + // eq. 7 + sigma = sOverbA + deltaSigma; + + // break after converging to tolerance + if ((sigma - prevSigma).abs() < 0.0000000000001) break; + + prevSigma = sigma; + } + + sigmaM2 = 2.0 * sigma1 + sigma; + cosSigmaM2 = cos(sigmaM2); + cos2SigmaM2 = cosSigmaM2 * cosSigmaM2; + + var cosSigma = cos(sigma); + sinSigma = sin(sigma); + + // eq. 8 + var phi2 = atan2(sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1, (1.0 - f) * sqrt(sin2Alpha + pow(sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1, 2.0))); + + // eq. 9 + // This fixes the pole crossing defect spotted by Matt Feemster. When a + // path passes a pole and essentially crosses a line of latitude twice - + // once in each direction - the longitude calculation got messed up. + // Using + // atan2 instead of atan fixes the defect. The change is in the next 3 + // lines. + // double tanLambda = sinSigma * sinAlpha1 / (cosU1 * cosSigma - sinU1 * + // sinSigma * cosAlpha1); + // double lambda = Math.atan(tanLambda); + var lambda = atan2(sinSigma * sinAlpha1, (cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1)); + + // eq. 10 + var C = (f / 16) * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha)); + + // eq. 11 + var L = lambda - (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2))); + + // eq. 12 + // double alpha2 = Math.atan2(sinAlpha, -sinU1 * sinSigma + cosU1 * + // cosSigma * cosAlpha1); + + // build result + var latitude = toDegrees(phi2); + var longitude = start.longitude + toDegrees(L); + + // if ((endBearing != null) && (endBearing.length > 0)) { + // endBearing[0] = toDegrees(alpha2); + // } + + latitude = latitude < -90 ? -90 : latitude; + latitude = latitude > 90 ? 90 : latitude; + longitude = longitude < -180 ? -180 : longitude; + longitude = longitude > 180 ? 180 : longitude; + return LatLng(latitude, longitude); +} diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart new file mode 100644 index 000000000..f12b8cd1f --- /dev/null +++ b/lib/widgets/settings_page.dart @@ -0,0 +1,65 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; +import 'package:aves/widgets/fullscreen/info/location_section.dart'; +import 'package:flutter/material.dart'; + +class SettingsPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MediaQueryDataProvider( + child: DefaultTabController( + length: 4, + child: Scaffold( + appBar: AppBar( + title: Text('Preferences'), + ), + body: SafeArea( + child: ListView( + padding: EdgeInsets.all(16), + children: [ + Text('Maps'), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Storage:'), + SizedBox(width: 8), + Flexible(child: InfoMapStyleSelector()), + ], + ), + ], + ), + ), + ), + ), + ); + } +} + +class InfoMapStyleSelector extends StatefulWidget { + @override + _InfoMapStyleSelectorState createState() => _InfoMapStyleSelectorState(); +} + +class _InfoMapStyleSelectorState extends State { + @override + Widget build(BuildContext context) { + return DropdownButton( + items: EntryMapStyle.values + .map((style) => DropdownMenuItem( + value: style, + child: Text( + style.name, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + )) + .toList(), + value: settings.infoMapStyle, + onChanged: (style) { + settings.infoMapStyle = style; + setState(() {}); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 329e3324c..cc51e1ed7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" archive: dependency: transitive description: @@ -36,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0+1" characters: dependency: transitive description: @@ -78,6 +92,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.13" + console_log_handler: + dependency: transitive + description: + name: console_log_handler + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" convert: dependency: transitive description: @@ -150,6 +171,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.1" flutter_ijkplayer: dependency: "direct main" description: @@ -159,6 +187,20 @@ packages: url: "git://github.com/deckerst/flutter_ijkplayer.git" source: git version: "0.3.7" + flutter_image: + dependency: transitive + description: + name: flutter_image + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.1+1" flutter_markdown: dependency: "direct main" description: @@ -225,6 +267,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.2" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.4" image: dependency: transitive description: @@ -246,6 +302,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.2" + latlong: + dependency: "direct main" + description: + name: latlong + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1" + lists: + dependency: transitive + description: + name: lists + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.6" logging: dependency: transitive description: @@ -274,6 +344,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.8" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" nested: dependency: transitive description: @@ -323,6 +400,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.11" path_provider_linux: dependency: transitive description: @@ -330,6 +414,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+2" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+3" path_provider_platform_interface: dependency: transitive description: @@ -409,6 +500,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + positioned_tap_detector: + dependency: transitive + description: + name: positioned_tap_detector + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" printing: dependency: "direct main" description: @@ -423,6 +521,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.13" + proj4dart: + dependency: transitive + description: + name: proj4dart + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" provider: dependency: "direct main" description: @@ -451,6 +556,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.24.1" screen: dependency: "direct main" description: @@ -575,6 +687,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.17" + transparent_image: + dependency: transitive + description: + name: transparent_image + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" tuple: dependency: "direct main" description: @@ -589,6 +708,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + unicode: + dependency: transitive + description: + name: unicode + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.4" url_launcher: dependency: "direct main" description: @@ -638,6 +764,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.0" + validate: + dependency: transitive + description: + name: validate + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" vector_math: dependency: transitive description: @@ -645,6 +778,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.7" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 677b3a265..a8344e157 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: # path: ../flutter_ijkplayer git: url: git://github.com/deckerst/flutter_ijkplayer.git + flutter_map: flutter_markdown: flutter_native_timezone: flutter_staggered_animations: @@ -56,6 +57,7 @@ dependencies: geocoder: google_maps_flutter: intl: + latlong: # for flutter_map outline_material_icons: package_info: palette_generator: