From 2f7137a2dd2036ea59f9fb79e08e031235d922fa Mon Sep 17 00:00:00 2001 From: ArthurBeaulieu Date: Wed, 17 Jul 2024 22:26:45 +0200 Subject: [PATCH] feat: add map options and stadia lines and labels to esri maps --- lib/src/data/data_controller.dart | 14 +++ lib/src/data/data_service.dart | 26 ++++ lib/src/localization/app_en.arb | 4 - lib/src/localization/app_fr.arb | 4 - lib/src/utils/app_const.dart | 1 + .../fragment/map_options_fragment_view.dart | 115 ++++++++++++++++++ lib/src/view/map_explore_view.dart | 60 ++++++++- 7 files changed, 210 insertions(+), 14 deletions(-) create mode 100644 lib/src/view/fragment/map_options_fragment_view.dart diff --git a/lib/src/data/data_controller.dart b/lib/src/data/data_controller.dart index 0623b24..2cdf4b1 100644 --- a/lib/src/data/data_controller.dart +++ b/lib/src/data/data_controller.dart @@ -23,6 +23,7 @@ class DataController with ChangeNotifier { late bool _startupHelpFlag; late String _userMainCity; // Map and marks utils + late String _mapLayer; final Map> _citiesBounds = {}; final Map _citiesMarkers = {}; @@ -31,6 +32,7 @@ class DataController with ChangeNotifier { Locale get appLocale => _appLocale; bool get startupHelpFlag => _startupHelpFlag; String get userMainCity => _userMainCity; + String get mapLayer => _mapLayer; Map> get citiesBounds => _citiesBounds; Map get citiesMarkers => _citiesMarkers; @@ -40,6 +42,7 @@ class DataController with ChangeNotifier { _appLocale = await _dataService.getAppLocale(); _startupHelpFlag = await _dataService.getStartupHelpFlag(); _userMainCity = await _dataService.getUserMainCity(); + _mapLayer = await _dataService.getMapLayer(); for (var i = 0; i < AppConst.citiesCode.length; ++i) { // Get cities boundaries from assets @@ -97,4 +100,15 @@ class DataController with ChangeNotifier { await _dataService.setUserMainCity(_userMainCity); // Save into storage notifyListeners(); } + // Update map layer + Future setMapLayer( + String? newValue, + ) async { + if (newValue == null) return; // Avoid null exception + if (newValue == _mapLayer) return; // Avoid setting same value + if (['osm', 'esri'].contains(newValue) == false) return; // Avoid writing value not supported + _mapLayer = newValue; // Update controller value + await _dataService.setMapLayer(_mapLayer); // Save into storage + notifyListeners(); + } } diff --git a/lib/src/data/data_service.dart b/lib/src/data/data_service.dart index 3d0bc53..027e743 100644 --- a/lib/src/data/data_service.dart +++ b/lib/src/data/data_service.dart @@ -97,6 +97,22 @@ class DataService { // Return stored value return value; } + // The user map layer preference + Future getMapLayer() async { + final SharedPreferences appPreferences = await SharedPreferences.getInstance(); + // Read preference from device shared preference storage + String? value = appPreferences.getString('md-map-layer'); + // First app launch, set pref value to 'BRX' and return it + if (value == null) { + await appPreferences.setString( + 'md-map-layer', + 'osm', + ); + return 'osm'; + } + // Return stored value + return value; + } /* Data service setters */ @@ -153,4 +169,14 @@ class DataService { newValue, ); } + // Persists the user's selected map layer + Future setMapLayer( + String newValue, + ) async { + final SharedPreferences appPreferences = await SharedPreferences.getInstance(); + await appPreferences.setString( + 'md-map-layer', + newValue, + ); + } } diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index d77f22d..f0da21e 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -29,10 +29,6 @@ "mapOptionsLayerStyle": "Style du fond de carte", "mapOptionsLayerPlan": "Plan", "mapOptionsLayerSatellite": "Satellite", - "mapOptionsDisplayedMarkers": "Visibilité des marqueurs", - "mapOptionsDisplaySpots": "Afficher les spots", - "mapOptionsDisplayShops": "Afficher les magasins", - "mapOptionsDisplayBars": "Afficher les bars", "mapOSMContributors": "Contributeurs OpenStreeMap", "mapEsriContributors": "Alimenté par Esri", "mapFlutterMapContributors": "Développeurs de Flutter Maps", diff --git a/lib/src/localization/app_fr.arb b/lib/src/localization/app_fr.arb index 7572c0d..1fd0011 100644 --- a/lib/src/localization/app_fr.arb +++ b/lib/src/localization/app_fr.arb @@ -30,10 +30,6 @@ "mapOptionsLayerStyle": "Style du fond de carte", "mapOptionsLayerPlan": "Plan", "mapOptionsLayerSatellite": "Satellite", - "mapOptionsDisplayedMarkers": "Visibilité des marqueurs", - "mapOptionsDisplaySpots": "Afficher les spots", - "mapOptionsDisplayShops": "Afficher les magasins", - "mapOptionsDisplayBars": "Afficher les bars", "mapOSMContributors": "Contributeurs OpenStreeMap", "mapEsriContributors": "Alimenté par Esri", "mapFlutterMapContributors": "Développeurs de Flutter Maps", diff --git a/lib/src/utils/app_const.dart b/lib/src/utils/app_const.dart index 321183a..d17fa54 100644 --- a/lib/src/utils/app_const.dart +++ b/lib/src/utils/app_const.dart @@ -2,6 +2,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; class AppConst { static String? osrApiKey = dotenv.env['OSR_API_KEY']; + static String? stadiaMapsApiKey = dotenv.env['STADIA_MAPS_KEY']; static const int maxDistanceForRoute = 30000; // 30km max range to trace routes static const List supportedLang = ['en', 'fr']; diff --git a/lib/src/view/fragment/map_options_fragment_view.dart b/lib/src/view/fragment/map_options_fragment_view.dart new file mode 100644 index 0000000..0bcd059 --- /dev/null +++ b/lib/src/view/fragment/map_options_fragment_view.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:toggle_switch/toggle_switch.dart'; + +import '/src/data/data_controller.dart'; +import '/src/utils/size_config.dart'; + +class MapOptionsFragmentView { + static void showModal( + BuildContext context, + DataController dataController, + Function setter, + ) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + barrierColor: Colors.black.withOpacity(0.1), + builder: ( + BuildContext context, + ) { + SizeConfig().init(context); + MediaQueryData mediaQueryData = MediaQuery.of(context); + // Local working values + String mapLayer = dataController.mapLayer; + + return StatefulBuilder( + builder: ( + BuildContext context, + StateSetter setModalState, + ) { + return Container( + height: (25 * mediaQueryData.size.height) / 100, + color: Theme.of(context).colorScheme.background, + // Modal inner padding + padding: EdgeInsets.symmetric( + horizontal: SizeConfig.padding, + ), + child: Center( + child: ListView( + children: [ + Column( + children: [ + SizedBox( + height: SizeConfig.padding, + ), + // Modal title + Text( + AppLocalizations.of(context)!.mapOptionsTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: SizeConfig.fontTextTitleSize, + ), + ), + SizedBox( + height: SizeConfig.padding, + ), + // Map layer type subtitle + Text( + AppLocalizations.of(context)!.mapOptionsLayerStyle, + style: TextStyle( + fontSize: SizeConfig.fontTextLargeSize, + ), + ), + SizedBox( + height: SizeConfig.paddingSmall, + ), + // Map layer type switch + ToggleSwitch( + customWidths: [ + mediaQueryData.size.width / 3, + mediaQueryData.size.width / 3, + ], + initialLabelIndex: (mapLayer == 'osm') + ? 0 + : 1, + totalSwitches: 2, + labels: [ + AppLocalizations.of(context)!.mapOptionsLayerPlan, + AppLocalizations.of(context)!.mapOptionsLayerSatellite, + ], + onToggle: ( + index, + ) { + if (index == 0) { + mapLayer = 'osm'; + setter( + 'mapLayer', + mapLayer, + ); + setModalState(() {}); + } else { + mapLayer = 'esri'; + setter( + 'mapLayer', + mapLayer, + ); + setModalState(() {}); + } + }, + ), + SizedBox( + height: SizeConfig.paddingLarge, + ), + ], + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/src/view/map_explore_view.dart b/lib/src/view/map_explore_view.dart index a4d5e6a..bf81dcf 100644 --- a/lib/src/view/map_explore_view.dart +++ b/lib/src/view/map_explore_view.dart @@ -17,6 +17,7 @@ import '/src/data/data_controller.dart'; import '/src/utils/app_const.dart'; import '/src/utils/map_utils.dart'; import '/src/utils/size_config.dart'; +import '/src/view/fragment/map_options_fragment_view.dart'; import '/src/view/settings_view.dart'; // Hold the main widget map view, that contains // all spots, shops and bars saved on server. Handle @@ -42,8 +43,6 @@ class MapExploreViewState extends State with TickerProviderState // Map data late List citiesBoundsPolygons; late List citiesMarkers; - // Map user session settings (not saved upon restart) - String mapLayer = 'osm'; bool doubleTap = false; // Enter double tap mode bool doubleTapPerformed = false; // Double tap actually happened final OpenRouteService ors = OpenRouteService( @@ -84,7 +83,6 @@ class MapExploreViewState extends State with TickerProviderState _alignPositionStreamController.close(); super.dispose(); } - // Navigation routing void computeRouteToMark( LatLng destination, @@ -211,6 +209,16 @@ class MapExploreViewState extends State with TickerProviderState } } } +// Calback function to set MapView internal values according to option changed + void mapOptionsSetter( + String type, + dynamic value, + ) { + if (type == 'mapLayer') { + widget.dataController.setMapLayer(value); + } + setState(() {}); + } // Map widget builing @override Widget build( @@ -301,11 +309,32 @@ class MapExploreViewState extends State with TickerProviderState ), children: [ TileLayer( - urlTemplate: (mapLayer == 'osm') + urlTemplate: (widget.dataController.mapLayer == 'osm') ? 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' : 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', userAgentPackageName: 'com.messebasseproduction.mondourdannais', ), + // For satelite layer, adding lines and labels tile overlays + (widget.dataController.mapLayer == 'esri') + ? TileLayer(//&api_key=${AppConst.stadiaMapsApiKey} + urlTemplate: 'https://tiles-eu.stadiamaps.com/tiles/stamen_terrain-lines/{z}/{x}/{y}{r}.png?api_key=${AppConst.stadiaMapsApiKey}', + userAgentPackageName: 'com.messebasseproduction.mondourdannais', + retinaMode: true, + additionalOptions: { + 'api_key': AppConst.stadiaMapsApiKey!, + }, + ) + : const SizedBox.shrink(), + (widget.dataController.mapLayer == 'esri') + ? TileLayer( + urlTemplate: 'https://tiles-eu.stadiamaps.com/tiles/stamen_terrain-labels/{z}/{x}/{y}{r}.png?api_key=${AppConst.stadiaMapsApiKey}', + userAgentPackageName: 'com.messebasseproduction.mondourdannais', + retinaMode: true, + additionalOptions: { + 'api_key': AppConst.stadiaMapsApiKey!, + }, + ) + : const SizedBox.shrink(), CurrentLocationLayer( alignPositionStream: _alignPositionStreamController.stream, alignPositionOnUpdate: _alignPositionOnUpdate, @@ -362,7 +391,7 @@ class MapExploreViewState extends State with TickerProviderState }, attributions: [ TextSourceAttribution( - (mapLayer == 'osm') + (widget.dataController.mapLayer == 'osm') ? AppLocalizations.of(context)!.mapOSMContributors : AppLocalizations.of(context)!.mapEsriContributors, textStyle: TextStyle( @@ -370,7 +399,7 @@ class MapExploreViewState extends State with TickerProviderState fontStyle: FontStyle.italic, ), onTap: () => launchUrl( - (mapLayer == 'osm') + (widget.dataController.mapLayer == 'osm') ? Uri.parse('https://openstreetmap.org/copyright') : Uri.parse('https://www.esri.com'), ), @@ -425,6 +454,25 @@ class MapExploreViewState extends State with TickerProviderState ), ), ), + // Map filtering operations + Container( + margin: EdgeInsets.symmetric( + vertical: SizeConfig.paddingTiny, + ), + child: FloatingActionButton( + heroTag: 'filterButton', + onPressed: () => MapOptionsFragmentView.showModal( + context, + widget.dataController, + mapOptionsSetter, + ), + foregroundColor: null, + backgroundColor: null, + child: const Icon( + Icons.map, + ), + ), + ), // Zoom out Container( margin: EdgeInsets.symmetric(