From 8e6dd56e70343fac98a6099fc6e78038575555f8 Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:01:10 -0300 Subject: [PATCH 01/13] feat: Implementa o location_service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mais informações: - Separa a lógica de atualização da localização do dispositivo no mapa para ser utilizado como um serviço nas páginas que precisarem deste recurso. --- .../lib/controllers/base_map_controller.dart | 56 ++++++++++--------- .../spothole_info_window_controller.dart | 2 + .../lib/pages/base_map_page.dart | 6 ++ .../lib/services/location_service.dart | 55 ++++++++++++++++++ spotholes_android/lib/widgets/search_bar.dart | 4 +- 5 files changed, 97 insertions(+), 26 deletions(-) create mode 100644 spotholes_android/lib/services/location_service.dart diff --git a/spotholes_android/lib/controllers/base_map_controller.dart b/spotholes_android/lib/controllers/base_map_controller.dart index db18d46..91e6a94 100644 --- a/spotholes_android/lib/controllers/base_map_controller.dart +++ b/spotholes_android/lib/controllers/base_map_controller.dart @@ -11,9 +11,9 @@ import '../package/custom_info_window.dart'; import '../package/google_places_flutter/model/place_details.dart' hide Location; import '../services/geocoding_service.dart'; +import '../services/location_service.dart'; import '../services/service_locator.dart'; import '../utilities/constants.dart'; -import '../utilities/custom_icons.dart'; import '../utilities/custom_snackbar.dart'; import '../widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart'; import '../widgets/info_window/marker_info_window.dart'; @@ -22,9 +22,19 @@ import 'spothole_info_window_controller.dart'; class BaseMapController { BaseMapController._(); - static final BaseMapController _instance = BaseMapController._(); + static BaseMapController _instance = BaseMapController._(); static BaseMapController get instance => _instance; + static void resetInstance() { + _instance = BaseMapController._(); + } + + Function? _dispose; + + final LocationService _locationService = LocationService.instance; + late final Signal _currentLocationSignal = + _locationService.currentLocationSignal; + final databaseReference = getIt(); late final dataBaseSpotholesRef = databaseReference.child('spotholes'); @@ -42,10 +52,7 @@ class BaseMapController { final _geocodingService = GeocodingService.instance; - final _location = Location(); - final _markersSignal = Signal>({}); - final _currentLocationSignal = Signal(null); get markersSignal => _markersSignal; get currentLocationSignal => _currentLocationSignal; @@ -55,10 +62,6 @@ class BaseMapController { get currentLocationLatLng => LatLng(_currentLocationSignal.value!.latitude!, _currentLocationSignal.value!.longitude!); - String currentLocationLatLngURLPattern() => - "${_currentLocationSignal.value!.latitude!.toString()}" - "%2C${_currentLocationSignal.value!.longitude!.toString()}"; - void onMapCreated(mapController, context) { _googleMapControllerCompleter.complete(mapController); _customInfoWindowControllerSignal.value.googleMapController = mapController; @@ -83,27 +86,24 @@ class BaseMapController { } void loadCurrentLocation() async { - _currentLocationSignal.value = await _location.getLocation(); - loadCurrentLocationMark(); - _location.onLocationChanged.listen((newLoc) { - _currentLocationSignal.value = newLoc; - loadCurrentLocationMark(); - }); _googleMapController = await _googleMapControllerCompleter.future; centerView(); + listenCurrentLocation(); } - void loadCurrentLocationMark() { - final newMarker = Marker( - markerId: const MarkerId("currentLocation"), - icon: CustomIcons.currentLocationIcon, - position: currentLocationLatLng, - onTap: () => _customInfoWindowControllerSignal.value.addInfoWindow!( - const MarkerInfoWindow( - title: 'Localização', textContent: 'Você está aqui!'), - currentLocationLatLng), + void listenCurrentLocation() async { + _dispose = effect( + () { + if (_currentLocationSignal.value != null) { + untracked( + () => _locationService.loadCurrentLocationMark( + _markersSignal, + _customInfoWindowControllerSignal, + ), + ); + } + }, ); - _markersSignal.value['currentLocationMarker'] = newMarker; } changeDraggableSheet(DraggableScrollableSheetType type) { @@ -154,6 +154,7 @@ class BaseMapController { .addSpotholeMarker(context, spothole); }, ); + _markersSignal.value = {..._markersSignal.value}; } }, ); @@ -165,6 +166,7 @@ class BaseMapController { position, category, type, null, newSpotHoleRef.key); newSpotHoleRef.set(newSpothole.toJson()); spotholeInfoWindowController!.addSpotholeMarker(context, newSpothole); + _markersSignal.value = {..._markersSignal.value}; } void registerSpotholeModal(context, {LatLng? position}) { @@ -220,4 +222,8 @@ class BaseMapController { ), ); } + + dispose() { + _dispose!(); + } } diff --git a/spotholes_android/lib/controllers/spothole_info_window_controller.dart b/spotholes_android/lib/controllers/spothole_info_window_controller.dart index 75a17fc..2abea60 100644 --- a/spotholes_android/lib/controllers/spothole_info_window_controller.dart +++ b/spotholes_android/lib/controllers/spothole_info_window_controller.dart @@ -69,6 +69,7 @@ class SpotholeInfoWindowController { spothole.type = type; spothole.id = spotholeId; addSpotholeMarker(context, spothole); + _markersSignal.value = {..._markersSignal.value}; _markersSignal.value[spotholeId]!.onTap!(); updateCameraGoogleMapsController(spothole.position); spotholeRef.set(spothole.toJson()); @@ -113,5 +114,6 @@ class SpotholeInfoWindowController { void removeMarkerByid(spotholeId) { _markersSignal.value.remove(spotholeId); + _markersSignal.value = {..._markersSignal.value}; } } diff --git a/spotholes_android/lib/pages/base_map_page.dart b/spotholes_android/lib/pages/base_map_page.dart index dbeb320..cbc3fc0 100644 --- a/spotholes_android/lib/pages/base_map_page.dart +++ b/spotholes_android/lib/pages/base_map_page.dart @@ -38,6 +38,12 @@ class BaseMapPageState extends State { _baseMapController.loadCurrentLocation(); } + @override + void dispose() { + _baseMapController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Watch( diff --git a/spotholes_android/lib/services/location_service.dart b/spotholes_android/lib/services/location_service.dart new file mode 100644 index 0000000..9d7dd43 --- /dev/null +++ b/spotholes_android/lib/services/location_service.dart @@ -0,0 +1,55 @@ +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:location/location.dart'; +import 'package:signals/signals_flutter.dart'; + +import '../utilities/custom_icons.dart'; +import '../widgets/info_window/marker_info_window.dart'; + +class LocationService { + LocationService._() { + _startLocationMonitoring(); + } + + static final LocationService _instance = LocationService._(); + static LocationService get instance => _instance; + + final _location = Location(); + final _currentLocationSignal = Signal(null); + + get currentLocationSignal => _currentLocationSignal; + get currentLocationLatLng => LatLng(_currentLocationSignal.value!.latitude!, + _currentLocationSignal.value!.longitude!); + + String currentLocationLatLngURLPattern() => + "${_currentLocationSignal.value!.latitude!.toString()}" + "%2C${_currentLocationSignal.value!.longitude!.toString()}"; + + void loadCurrentLocationMark(Signal> markersSignal, + final customInfoWindowControllerSignal) { + final newMarker = Marker( + markerId: const MarkerId("currentLocationMarker"), + icon: CustomIcons.currentLocationIcon, + position: currentLocationLatLng, + onTap: () => customInfoWindowControllerSignal.value.addInfoWindow!( + const MarkerInfoWindow( + title: 'Localização', + textContent: 'Você está aqui!', + ), + currentLocationLatLng, + ), + ); + markersSignal.value = { + ...markersSignal.value, + 'currentLocationMarker': newMarker, + }; + } + + void _startLocationMonitoring() async { + _currentLocationSignal.value = await _location.getLocation(); + _location.onLocationChanged.listen( + (newLoc) { + _currentLocationSignal.value = newLoc; + }, + ); + } +} diff --git a/spotholes_android/lib/widgets/search_bar.dart b/spotholes_android/lib/widgets/search_bar.dart index aaf4621..e9f839e 100644 --- a/spotholes_android/lib/widgets/search_bar.dart +++ b/spotholes_android/lib/widgets/search_bar.dart @@ -5,6 +5,7 @@ import '../config/environment_config.dart'; import '../package/google_places_flutter/google_places_flutter.dart'; import '../package/google_places_flutter/model/place_details.dart'; import '../package/google_places_flutter/model/prediction.dart'; +import '../services/location_service.dart'; class CustomHeader extends StatelessWidget { const CustomHeader({super.key}); @@ -50,6 +51,7 @@ class CustomSearchContainer extends StatelessWidget { class CustomTextField extends StatelessWidget { CustomTextField({super.key}); final _baseMapController = BaseMapController.instance; + final _locationService = LocationService.instance; late final _textEditingController = _baseMapController.textEditingController; late final searchBarfocusNode = _baseMapController.searchBarFocusNode; @@ -60,7 +62,7 @@ class CustomTextField extends StatelessWidget { textEditingController: _textEditingController, googleAPIKey: EnvironmentConfig.googleApiKey!, currentLocationLatLngURLPattern: - _baseMapController.currentLocationLatLngURLPattern, + _locationService.currentLocationLatLngURLPattern, boxDecoration: const BoxDecoration(), inputDecoration: const InputDecoration( prefixIcon: Padding( From 503e67d652c9ee741923aa1e1176adb6d4599a2f Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:43:05 -0300 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20Implementa=20o=20spothole=5Fservi?= =?UTF-8?q?ce,=20desacoplando=20a=20l=C3=B3gica=20de=20spotholes=20dos=20c?= =?UTF-8?q?ontrollers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mais informações: - Separa a lógica de registro e carregamento de dados dos spotholes; - Faz alguns ajustes para reuso dos dados do RouteController para tela de Navegação na rota. --- .../lib/controllers/base_map_controller.dart | 56 ++-------- .../lib/controllers/route_controller.dart | 68 +++++++----- .../lib/services/spothole_service.dart | 103 ++++++++++++++++++ 3 files changed, 152 insertions(+), 75 deletions(-) create mode 100644 spotholes_android/lib/services/spothole_service.dart diff --git a/spotholes_android/lib/controllers/base_map_controller.dart b/spotholes_android/lib/controllers/base_map_controller.dart index 91e6a94..e6d19ca 100644 --- a/spotholes_android/lib/controllers/base_map_controller.dart +++ b/spotholes_android/lib/controllers/base_map_controller.dart @@ -6,19 +6,17 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:location/location.dart'; import 'package:signals/signals_flutter.dart'; -import '../models/spothole.dart'; import '../package/custom_info_window.dart'; import '../package/google_places_flutter/model/place_details.dart' hide Location; import '../services/geocoding_service.dart'; import '../services/location_service.dart'; import '../services/service_locator.dart'; +import '../services/spothole_service.dart'; import '../utilities/constants.dart'; import '../utilities/custom_snackbar.dart'; import '../widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart'; import '../widgets/info_window/marker_info_window.dart'; -import '../widgets/modal/register_spothole_modal.dart'; -import 'spothole_info_window_controller.dart'; class BaseMapController { BaseMapController._(); @@ -42,7 +40,7 @@ class BaseMapController { final _googleMapControllerCompleter = Completer(); final _customInfoWindowControllerSignal = Signal(CustomInfoWindowController()); - SpotholeInfoWindowController? spotholeInfoWindowController; + SpotholeService? spotholeService; final _textEditingController = TextEditingController(); final _searchBarFocusNode = FocusNode(); @@ -65,9 +63,11 @@ class BaseMapController { void onMapCreated(mapController, context) { _googleMapControllerCompleter.complete(mapController); _customInfoWindowControllerSignal.value.googleMapController = mapController; - spotholeInfoWindowController = SpotholeInfoWindowController( - _customInfoWindowControllerSignal, _markersSignal); - loadSpotholeMarkers(context); + spotholeService = SpotholeService( + _markersSignal, + _customInfoWindowControllerSignal, + ); + spotholeService!.loadSpotholeMarkers(context); } void updateCameraGoogleMapsController(position, [zoom = defaultZoomMap]) { @@ -141,47 +141,12 @@ class BaseMapController { } void loadSpotholeMarkers(context) { - databaseReference.child('spotholes').once().then( - (DatabaseEvent event) { - final spotholesMap = event.snapshot.value as Map?; - if (spotholesMap != null) { - spotholesMap.forEach( - (key, value) { - final spothole = - Spothole.fromJson(Map.from(value as Map)); - spothole.id = key; - spotholeInfoWindowController! - .addSpotholeMarker(context, spothole); - }, - ); - _markersSignal.value = {..._markersSignal.value}; - } - }, - ); - } - - void registerSpothole(context, position, category, type) { - final newSpotHoleRef = dataBaseSpotholesRef.push(); - final newSpothole = Spothole(DateTime.now().toUtc(), DateTime.now().toUtc(), - position, category, type, null, newSpotHoleRef.key); - newSpotHoleRef.set(newSpothole.toJson()); - spotholeInfoWindowController!.addSpotholeMarker(context, newSpothole); - _markersSignal.value = {..._markersSignal.value}; + spotholeService!.loadSpotholeMarkers(context); } void registerSpotholeModal(context, {LatLng? position}) { final latLng = position ?? currentLocationLatLng; - showModalBottomSheet( - context: context, - builder: (builder) { - return RegisterSpotholeModal( - title: "Para alertar um risco, selecione:", - textOnRegisterButton: "Adicionar", - onRegister: (riskCategory, type) => - registerSpothole(context, latLng, riskCategory, type), - ); - }, - ); + spotholeService!.registerSpotholeModal(context, latLng); } void onLongPress(BuildContext context, LatLng position) async { @@ -218,7 +183,8 @@ class BaseMapController { DraggableScrollableSheetTypes.location( position: position, formattedPlacemark: formattedPlacemark, - onRegister: () => registerSpotholeModal(context, position: position), + onRegister: () => + spotholeService!.registerSpotholeModal(context, position), ), ); } diff --git a/spotholes_android/lib/controllers/route_controller.dart b/spotholes_android/lib/controllers/route_controller.dart index fb93f9b..fb715c4 100644 --- a/spotholes_android/lib/controllers/route_controller.dart +++ b/spotholes_android/lib/controllers/route_controller.dart @@ -8,15 +8,14 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:signals/signals_flutter.dart'; import '../config/environment_config.dart'; -import '../controllers/spothole_info_window_controller.dart'; import '../models/spothole.dart'; import '../package/custom_info_window.dart'; import '../services/service_locator.dart'; +import '../services/spothole_service.dart'; import '../utilities/constants.dart'; import '../utilities/custom_icons.dart'; import '../utilities/maneuver_arrow_polyline.dart'; import '../utilities/map_utils.dart'; -import '../utilities/point_on_route_haversine.dart'; import '../widgets/info_window/marker_info_window.dart'; class RouteController { @@ -33,13 +32,18 @@ class RouteController { final _spotholesInRouteList = Signal>([]); GoogleMapController? _googleMapController; + get googleMapController => _googleMapController; + set googleMapController(mapController) => + _googleMapController = mapController; + final _googleMapControllerCompleter = Completer(); + get googleMapControllerCompleter => _googleMapControllerCompleter; final _customInfoWindowControllerSignal = Signal(CustomInfoWindowController()); get customInfoWindowControllerSignal => _customInfoWindowControllerSignal; - SpotholeInfoWindowController? spotholeInfoWindowController; + SpotholeService? _spotholeService; final _markersSignal = Signal>({}); final _routePolylineCoordinatesSignal = Signal>([]); @@ -71,8 +75,8 @@ class RouteController { void onMapCreated(mapController) { _googleMapControllerCompleter.complete(mapController); _customInfoWindowControllerSignal.value.googleMapController = mapController; - spotholeInfoWindowController = SpotholeInfoWindowController( - _customInfoWindowControllerSignal, _markersSignal); + _spotholeService = + SpotholeService(_markersSignal, _customInfoWindowControllerSignal); } GeoCoord latLngToGeoCoord(LatLng latLng) => @@ -281,31 +285,8 @@ class RouteController { } void loadSpotholesInRoute(context) { - _spotholesInRouteList.value = []; - databaseReference.child('spotholes').once().then( - (DatabaseEvent event) { - final spotholesMap = event.snapshot.value as Map?; - if (spotholesMap != null) { - final spotholeList = spotholesMap.entries.map((entry) { - final spothole = Spothole.fromJson( - Map.from(entry.value as Map)); - spothole.id = entry.key; - return spothole; - }).toList(); - - _spotholesInRouteList.value = checkPointsAndStoreAccumulatedDistances( - spotholeList, _routePolylineCoordinatesSignal.value); - - for (Spothole spothole in _spotholesInRouteList.value) { - spotholeInfoWindowController!.addSpotholeMarker(context, spothole); - } - - _markersSignal.value = { - ..._markersSignal.value, - }; - } - }, - ); + _spotholesInRouteList.value = _spotholeService! + .loadSpotholesInRoute(context, routePolylineCoordinatesSignal.value); } void setupStepsPageView(int initialPage, String type) { @@ -374,4 +355,31 @@ class RouteController { polylinesSignal.value = {...polylinesSignal.value}; } + + RouteController copy() { + var copy = RouteController._(); + copy._spotholesInRouteList.value = List.from(_spotholesInRouteList.value); + copy._markersSignal.value = Map.from(_markersSignal.value); + copy._routePolylineCoordinatesSignal.value = + List.from(_routePolylineCoordinatesSignal.value); + copy._polylinesSignal.value = Map.from(_polylinesSignal.value); + copy._routePolyline.value = _routePolyline.value; + copy._directionResult.value = _directionResult.value; + copy._routeAndStepsListSignal.value = + List.from(_routeAndStepsListSignal.value); + // TODO Removi todos os casos de valores que são inicializados nulo no RouteController, assim eles são reinicializados + // copy._googleMapController = null; + // copy._customInfoWindowControllerSignal.value = + // _customInfoWindowControllerSignal.value; + // copy._spotholeService = _spotholeService; + // copy._showStepsPageSignal.value = _showStepsPageSignal.value; + // copy._pageViewTypeSignal.value = _pageViewTypeSignal.value; + // Torno null para não utilizar o msm controller em duas PageView diferentes + // copy._pageControllerSignal.value = _pageControllerSignal.value; + return copy; + } + + static RouteController getCopy() { + return instance.copy(); + } } diff --git a/spotholes_android/lib/services/spothole_service.dart b/spotholes_android/lib/services/spothole_service.dart new file mode 100644 index 0000000..f9081fa --- /dev/null +++ b/spotholes_android/lib/services/spothole_service.dart @@ -0,0 +1,103 @@ +import 'package:firebase_database/firebase_database.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:signals/signals_flutter.dart'; +import 'package:spotholes_android/controllers/spothole_info_window_controller.dart'; +import 'package:spotholes_android/services/service_locator.dart'; + +import '../models/spothole.dart'; +import '../package/custom_info_window.dart'; +import '../utilities/point_on_route_haversine.dart'; +import '../widgets/modal/register_spothole_modal.dart'; + +class SpotholeService { + final DatabaseReference databaseReference = getIt(); + late final DatabaseReference dataBaseSpotholesRef = + databaseReference.child('spotholes'); + final Signal> _markersSignal; + final Signal _customInfoWindowControllerSignal; + + late final _spotholeInfoWindowController = SpotholeInfoWindowController( + _customInfoWindowControllerSignal, _markersSignal); + + SpotholeService(this._markersSignal, this._customInfoWindowControllerSignal); + + void loadSpotholeMarkers(context) { + databaseReference.child('spotholes').once().then( + (DatabaseEvent event) { + final spotholesMap = event.snapshot.value as Map?; + if (spotholesMap != null) { + spotholesMap.forEach( + (key, value) { + final spothole = + Spothole.fromJson(Map.from(value as Map)); + spothole.id = key; + _spotholeInfoWindowController.addSpotholeMarker( + context, + spothole, + ); + }, + ); + _markersSignal.value = {..._markersSignal.value}; + } + }, + ); + } + + List loadSpotholesInRoute(context, routePolylineCoordinates) { + List spotholesInRouteList = []; + databaseReference.child('spotholes').once().then( + (DatabaseEvent event) { + final spotholesMap = event.snapshot.value as Map?; + if (spotholesMap != null) { + final spotholeList = spotholesMap.entries.map((entry) { + final spothole = Spothole.fromJson( + Map.from(entry.value as Map), + ); + spothole.id = entry.key; + return spothole; + }).toList(); + spotholesInRouteList = checkPointsAndStoreAccumulatedDistances( + spotholeList, + routePolylineCoordinates, + ); + for (Spothole spothole in spotholesInRouteList) { + _spotholeInfoWindowController.addSpotholeMarker(context, spothole); + } + _markersSignal.value = {..._markersSignal.value}; + } + }, + ); + return spotholesInRouteList; + } + + void registerSpothole(context, LatLng position, category, type) { + final newSpotHoleRef = dataBaseSpotholesRef.push(); + final newSpothole = Spothole( + DateTime.now().toUtc(), + DateTime.now().toUtc(), + position, + category, + type, + null, + newSpotHoleRef.key, + ); + newSpotHoleRef.set(newSpothole.toJson()); + _spotholeInfoWindowController.addSpotholeMarker(context, newSpothole); + _markersSignal.value = {..._markersSignal.value}; + } + + void registerSpotholeModal(context, position) { + showModalBottomSheet( + context: context, + builder: (builder) { + return RegisterSpotholeModal( + title: "Para alertar um risco, selecione:", + textOnRegisterButton: "Adicionar", + onRegister: (riskCategory, type) => + registerSpothole(context, position, riskCategory, type), + ); + }, + ); + } +} From 8360beaaf595203b9d7ce16705164440e5769669 Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Sun, 8 Dec 2024 22:47:28 -0300 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20inclui=20a=20biblioteca=20maps=5F?= =?UTF-8?q?toolkit=20e=20faz=20ajustes=20menores=20de=20formata=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Inclui o pacote maps_toolkit (^3.0.0) para auxiliar nos cálculos sobre as polylines durante a navegação. - Ajustes de formatação para melhorar a legibilidade do código. --- .../lib/services/location_service.dart | 6 +++++- .../utilities/point_on_route_haversine.dart | 20 ++++++++++++------- .../place_draggable_sheet.dart | 1 - spotholes_android/pubspec.yaml | 1 + 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/spotholes_android/lib/services/location_service.dart b/spotholes_android/lib/services/location_service.dart index 9d7dd43..3724595 100644 --- a/spotholes_android/lib/services/location_service.dart +++ b/spotholes_android/lib/services/location_service.dart @@ -14,8 +14,8 @@ class LocationService { static LocationService get instance => _instance; final _location = Location(); - final _currentLocationSignal = Signal(null); + final _currentLocationSignal = Signal(null); get currentLocationSignal => _currentLocationSignal; get currentLocationLatLng => LatLng(_currentLocationSignal.value!.latitude!, _currentLocationSignal.value!.longitude!); @@ -45,6 +45,10 @@ class LocationService { } void _startLocationMonitoring() async { + _location.changeSettings( + interval: 100, + distanceFilter: 5, + ); _currentLocationSignal.value = await _location.getLocation(); _location.onLocationChanged.listen( (newLoc) { diff --git a/spotholes_android/lib/utilities/point_on_route_haversine.dart b/spotholes_android/lib/utilities/point_on_route_haversine.dart index 68bffa1..ec7a80c 100644 --- a/spotholes_android/lib/utilities/point_on_route_haversine.dart +++ b/spotholes_android/lib/utilities/point_on_route_haversine.dart @@ -23,18 +23,24 @@ double haversine(LatLng point1, LatLng point2) { double pointToSegmentDistance(LatLng point, LatLng start, LatLng end) { final A = [ point.latitude - start.latitude, - point.longitude - start.longitude + point.longitude - start.longitude, + ]; + final B = [ + end.latitude - start.latitude, + end.longitude - start.longitude, ]; - final B = [end.latitude - start.latitude, end.longitude - start.longitude]; final bMagnitude = B[0] * B[0] + B[1] * B[1]; if (bMagnitude == 0) { return haversine(point, start); } - - final t = max(0, min(1, (A[0] * B[0] + A[1] * B[1]) / bMagnitude)); - final projection = - LatLng(start.latitude + t * B[0], start.longitude + t * B[1]); - + final t = max( + 0, + min(1, (A[0] * B[0] + A[1] * B[1]) / bMagnitude), + ); + final projection = LatLng( + start.latitude + t * B[0], + start.longitude + t * B[1], + ); return haversine(point, projection); } diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart index 9772232..2965dc2 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart @@ -63,7 +63,6 @@ class PlaceDraggableSheetState extends State { @override Widget build(BuildContext context) { Result placeDetailsResult = widget.placeDetails.result!; - return DraggableScrollableSheet( maxChildSize: 0.6, minChildSize: 0.26, diff --git a/spotholes_android/pubspec.yaml b/spotholes_android/pubspec.yaml index 5cc2e37..daafc7b 100644 --- a/spotholes_android/pubspec.yaml +++ b/spotholes_android/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: google_directions_api: ^0.10.0 flutter_html: ^3.0.0-beta.2 expandable_page_view: ^1.0.17 + maps_toolkit: ^3.0.0 dev_dependencies: flutter_test: From dc02e6c9ea2a406e26eaa39c1400f1b5ecf9598d Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Mon, 9 Dec 2024 00:18:28 -0300 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20Implementa=20a=20tela=20de=20nave?= =?UTF-8?q?ga=C3=A7=C3=A3o=20na=20rota?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Além da tela de navegação foi implementa a seguinte estrutura: - NavigationController, que contém parte da lógica de navegação e possui uma instância do NavigationService, sendo este responsável por encapsular o core da lógica de navegação. A navegação se baseia em verificar se o dispositivo está se deslocando dentro da rota, ou pelo menos dentro da margem de tolerância, atualizando a polyline que representa a rota, removendo o trecho já navegado. Além disso há o monitoramento sobre qual step (trecho que abrange uma manobra) o condutor está. Exibindo sempre a próxima manobra para o condutor e removendo os steps já trafegados. Foi necessário implementar uma lógica de cópia no RouteController para compartilhar dados e variáveis reativas (signal) entre as telas de rota e navegação, porém tendo que manter nulo parte das propriedades relacionadas a controladores de widgets, para evitar conflito entre os widgets das duas telas. --- .../controllers/navigation_controller.dart | 89 ++++++ .../lib/controllers/route_controller.dart | 213 ++++++++------ .../directions_result_extension.dart | 19 ++ .../directions_route_extension.dart | 25 ++ .../lib/pages/navigation_page.dart | 177 +++++++++++ spotholes_android/lib/pages/route_page.dart | 6 +- .../lib/services/navigation_service.dart | 182 ++++++++++++ .../lib/utilities/app_routes.dart | 9 + .../lib/utilities/constants.dart | 6 +- .../lib/utilities/location_service_mock.dart | 115 ++++++++ .../navigation_draggable_sheet.dart | 274 ++++++++++++++++++ .../route_draggable_sheet.dart | 6 +- .../widgets/nav_route_steps_page_view.dart | 212 ++++++++++++++ .../lib/widgets/route_steps_page_view.dart | 16 +- 14 files changed, 1252 insertions(+), 97 deletions(-) create mode 100644 spotholes_android/lib/controllers/navigation_controller.dart create mode 100644 spotholes_android/lib/package/extensions/directions_result_extension.dart create mode 100644 spotholes_android/lib/package/extensions/directions_route_extension.dart create mode 100644 spotholes_android/lib/pages/navigation_page.dart create mode 100644 spotholes_android/lib/services/navigation_service.dart create mode 100644 spotholes_android/lib/utilities/location_service_mock.dart create mode 100644 spotholes_android/lib/widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart create mode 100644 spotholes_android/lib/widgets/nav_route_steps_page_view.dart diff --git a/spotholes_android/lib/controllers/navigation_controller.dart b/spotholes_android/lib/controllers/navigation_controller.dart new file mode 100644 index 0000000..eaffd9d --- /dev/null +++ b/spotholes_android/lib/controllers/navigation_controller.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:location/location.dart'; +import 'package:signals/signals_flutter.dart'; +import 'package:spotholes_android/services/navigation_service.dart'; + +import '../services/location_service.dart'; +import '../utilities/constants.dart'; +import 'route_controller.dart'; + +class NavigationController { + final RouteController _routeController; + NavigationController(this._routeController); + + final LocationService _locationService = LocationService.instance; + late final Signal _currentLocationSignal = + _locationService.currentLocationSignal; + + GoogleMapController? _mapController; + static Function? _dispose; + + final _pageController = PageController(initialPage: 1); + get pageController => _pageController; + + late final _navigationService = + NavigationService(_routeController, _pageController); + get navigationService => _navigationService; + + /*Tentativa de Mock do serviço de localização + final _locationService = LocationServiceMock.create(); + late final Signal _currentLocationSignal = + _locationService.currentLocationSignal; + void startMockLocation(){ + _locationService.startMockLocationMonitoring(); + } + void stopMockLocation(){ + _locationService.stopMockLocationMonitoring(); + } */ + + void onMapCreated(mapController) { + _mapController = mapController; + _routeController.googleMapController = _mapController; + _routeController.onMapCreated(_mapController); + listenCurrentLocation(); + // _navigationService.startNavigation(); + } + + void listenCurrentLocation() async { + _dispose = effect( + () { + if (_currentLocationSignal.value != null) { + final LatLng currentLocation = _locationService.currentLocationLatLng; + final double heading = _currentLocationSignal.value!.heading!; + untracked( + () { + _locationService.loadCurrentLocationMark( + _routeController.markersSignal, + _routeController.customInfoWindowControllerSignal, + ); + _navigationService.updateRouteStatus(currentLocation); + }, + ); + _updateNavigationCamera(currentLocation, heading); + } + }, + ); + } + + void _updateNavigationCamera(LatLng position, double heading) { + final CameraPosition newCameraPosition = CameraPosition( + target: position, + zoom: defaultZoomMap, + bearing: heading, + tilt: defaultNavigationTilt, + ); + _mapController!.animateCamera( + CameraUpdate.newCameraPosition(newCameraPosition), + ); + } + + void centerCurrentLocation() { + _routeController.updateCameraLatLng(_locationService.currentLocationLatLng); + } + + static dispose() { + // _navigationService.dispose(); + _dispose!(); + } +} diff --git a/spotholes_android/lib/controllers/route_controller.dart b/spotholes_android/lib/controllers/route_controller.dart index fb715c4..a41d0ee 100644 --- a/spotholes_android/lib/controllers/route_controller.dart +++ b/spotholes_android/lib/controllers/route_controller.dart @@ -6,6 +6,7 @@ import 'package:flutter_polyline_points/flutter_polyline_points.dart' as fpp; import 'package:google_directions_api/google_directions_api.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:signals/signals_flutter.dart'; +import 'package:spotholes_android/package/extensions/directions_result_extension.dart'; import '../config/environment_config.dart'; import '../models/spothole.dart'; @@ -18,6 +19,12 @@ import '../utilities/maneuver_arrow_polyline.dart'; import '../utilities/map_utils.dart'; import '../widgets/info_window/marker_info_window.dart'; +class RoutePointsData { + List points; + List stepsIndexes; + RoutePointsData(this.points, this.stepsIndexes); +} + class RouteController { RouteController._(); static RouteController? _instance; @@ -46,21 +53,24 @@ class RouteController { SpotholeService? _spotholeService; final _markersSignal = Signal>({}); + // Store all polyline points final _routePolylineCoordinatesSignal = Signal>([]); + // Store all polylines final _polylinesSignal = Signal>({}); - final _routePolyline = - Signal(const Polyline(polylineId: PolylineId('null'))); + // Store a result from a request for a route on Google Directions API final _directionResult = Signal(const DirectionsResult()); + final Signal> _routeStepsLatLng = Signal>([]); + Signal> get routeStepsLatLng => _routeStepsLatLng; - final _routeAndStepsListSignal = Signal>([]); + // Store steps' start indexes inside a route polyline + List _stepsIndexes = []; + get stepsIndexes => _stepsIndexes; Signal> get spotholesInRouteList => _spotholesInRouteList; Signal> get markersSignal => _markersSignal; Signal> get routePolylineCoordinatesSignal => _routePolylineCoordinatesSignal; Signal> get polylinesSignal => _polylinesSignal; - Signal get routePolyline => _routePolyline; - Signal> get routeAndStepsListSignal => _routeAndStepsListSignal; Signal get directionResult => _directionResult; final _showStepsPageSignal = signal(false); @@ -132,69 +142,6 @@ class RouteController { ); } - // TODO just for dev tests! - List routeToString(DirectionsResult response) { - List strSteps = []; - String strRoute = ''; - LatLng northeastBound = - geoCoordToLatLng(response.routes![0].bounds!.northeast); - LatLng southwestBound = - geoCoordToLatLng(response.routes![0].bounds!.southwest); - //BOUNDS - strRoute += 'northeastBound: ${latLngToString(northeastBound)}\n' - 'southwestBound: ${latLngToString(southwestBound)}\n'; - //SUMARY - strRoute += 'summary: ${response.routes![0].summary}\n'; - //OVERVIEW PATH - List points = response.routes![0].overviewPath!; - strRoute += 'overviewPath: '; - for (GeoCoord geoCoord in points) { - strRoute += '${geoCoordToString(geoCoord)}, '; - } - strRoute += '\n'; - //WARNINGS - final warnings = response.routes![0].warnings; - strRoute += 'Warnings:'; - if (warnings != null) { - for (String? warning in warnings) { - strRoute += '${warning!}, '; - } - } - strRoute += '\n'; - strSteps.add(strRoute); - //STEPS - List steps = response.routes![0].legs![0].steps!; - for (Step step in steps) { - strSteps.add('Distância: ${step.distance}\n' - 'Duração: ${step.duration!.text}\n' - 'Start Location: ${geoCoordToString(step.startLocation!)}\n' - 'End Location: ${geoCoordToString(step.endLocation!)}\n' - 'Instructions: ${step.instructions}\n' - 'Maneuver: ${step.maneuver}\n' - 'Transit: ${step.transit}\n' - 'Travel Mode: ${step.travelMode}\n'); - } - //Update da string de ROTA e STEPS - _routeAndStepsListSignal.value = strSteps; - return strSteps; - } - - // TODO just for dev tests! - void showOverViewPathPoints(List points) { - Map markers = {}; - for (LatLng point in points) { - String strPoint = latLngToString(point); - markers[strPoint] = Marker( - markerId: MarkerId(strPoint), - position: point, - ); - } - _markersSignal.value = { - ..._markersSignal.value, - ...markers, - }; - } - List decodePolyline(String encoded) { return fpp.PolylinePoints() .decodePolyline(encoded) @@ -202,13 +149,19 @@ class RouteController { .toList(); } - List extractPointsFromSteps(List steps) { + RoutePointsData decodePointsFromSteps(List steps) { List routePoints = []; + List stepPoints = []; + List stepsIndexes = []; + int currentIndex = 0; for (var step in steps) { var polyline = step.polyline!.points; - routePoints.addAll(decodePolyline(polyline!)); + stepPoints = decodePolyline(polyline!); + routePoints.addAll(stepPoints); + stepsIndexes.add(currentIndex); + currentIndex += stepPoints.length; } - return routePoints; + return RoutePointsData(routePoints, stepsIndexes); } Future loadRouteWithLegsAndSteps( @@ -229,9 +182,11 @@ class RouteController { (DirectionsResult response, DirectionsStatus? status) async { if (status == DirectionsStatus.ok) { _directionResult.value = response; - final steps = response.routes!.first.legs!.first.steps; - final points = extractPointsFromSteps(steps!); - _routePolylineCoordinatesSignal.value = points; + _routeStepsLatLng.value = response.routes!.first.legs!.first.steps!; + final routePointsData = + decodePointsFromSteps(_routeStepsLatLng.value); + _routePolylineCoordinatesSignal.value = routePointsData.points; + _stepsIndexes = routePointsData.stepsIndexes; _polylinesSignal.value['route'] = Polyline( polylineId: const PolylineId("route"), points: _routePolylineCoordinatesSignal.value, @@ -295,9 +250,9 @@ class RouteController { _pageViewTypeSignal.value = type; } - void plotManeuver(int stepIndex) { - final steps = _directionResult.value.routes![0].legs![0].steps; - final step = steps![stepIndex]; + void plotManeuverPolyline(int stepIndex, {bool updateCamera = true}) { + final steps = _routeStepsLatLng.value; + final step = steps[stepIndex]; final maneuver = step.maneuver ?? 'straight'; List maneuverPoints = []; List arrowPoints = []; @@ -308,7 +263,7 @@ class RouteController { maneuverPoints = decodePolyline(step.polyline!.points!); if (maneuver == 'straight') { - newCameraLatLngBoundsFromStep(step); + if (updateCamera) newCameraLatLngBoundsFromStep(step); polylinesSignal.value.addAll({ 'straightPath': Polyline( polylineId: const PolylineId('straightPath'), @@ -321,7 +276,7 @@ class RouteController { ) }); } else { - updateCameraGeoCoord(step.startLocation!); + if (updateCamera) updateCameraGeoCoord(step.startLocation!); if (stepIndex == 0) { maneuverPoints = [sourceLocation, ...maneuverPoints]; midpointIndex = 1; @@ -356,18 +311,42 @@ class RouteController { polylinesSignal.value = {...polylinesSignal.value}; } - RouteController copy() { + void clearManeuverPolyline() { + polylinesSignal.value.remove('maneuverArrow'); + polylinesSignal.value.remove('straightPath'); + polylinesSignal.value = {...polylinesSignal.value}; + } + + RouteController isolatedCopy() { var copy = RouteController._(); copy._spotholesInRouteList.value = List.from(_spotholesInRouteList.value); copy._markersSignal.value = Map.from(_markersSignal.value); + copy._polylinesSignal.value = Map.from(_polylinesSignal.value); copy._routePolylineCoordinatesSignal.value = List.from(_routePolylineCoordinatesSignal.value); - copy._polylinesSignal.value = Map.from(_polylinesSignal.value); - copy._routePolyline.value = _routePolyline.value; + // Dica: Em tipos complexos, a nova instância do tipo pai, por si só não resolve pois as partes internas vão ser cópias por referência, a não ser que crie toda a estrutura interna até chegar nos objetos que precisa fazer a cópia por passagem, em vez de referência + copy._directionResult.value = _directionResult.value.copyWith(); + copy._stepsIndexes = List.of(_stepsIndexes); + copy._routeStepsLatLng.value = List.from(_routeStepsLatLng.value); + copy._pageControllerSignal.value = PageController(initialPage: 1); + // Removi todos os casos de valores que são inicializados nulo no RouteController, assim eles são reinicializados e não retorna erro por duplicidade, como no caso + return copy; + } + + RouteController copy() { + var copy = RouteController._(); + copy._spotholesInRouteList.value = _spotholesInRouteList.value; + copy._markersSignal.value = _markersSignal.value; + copy._polylinesSignal.value = _polylinesSignal.value; + copy._routePolylineCoordinatesSignal.value = + _routePolylineCoordinatesSignal.value; copy._directionResult.value = _directionResult.value; - copy._routeAndStepsListSignal.value = - List.from(_routeAndStepsListSignal.value); - // TODO Removi todos os casos de valores que são inicializados nulo no RouteController, assim eles são reinicializados + copy._stepsIndexes = _stepsIndexes; + copy._routeStepsLatLng.value = _routeStepsLatLng.value; + // Removi todos os casos de valores que são inicializados nulo no RouteController, assim eles são reinicializados e não retorna erro por duplicidade, como no caso de widgets que precisam de controladores únicos + // copy._pageControllerSignal.value = PageController(initialPage: 1); + // copy._routeAndStepsListSignal.value = + // List.from(_routeAndStepsListSignal.value); // copy._googleMapController = null; // copy._customInfoWindowControllerSignal.value = // _customInfoWindowControllerSignal.value; @@ -375,11 +354,73 @@ class RouteController { // copy._showStepsPageSignal.value = _showStepsPageSignal.value; // copy._pageViewTypeSignal.value = _pageViewTypeSignal.value; // Torno null para não utilizar o msm controller em duas PageView diferentes - // copy._pageControllerSignal.value = _pageControllerSignal.value; return copy; } static RouteController getCopy() { return instance.copy(); } + + // // TODO just for dev tests! + // List routeToString(DirectionsResult response) { + // List strSteps = []; + // String strRoute = ''; + // LatLng northeastBound = + // geoCoordToLatLng(response.routes![0].bounds!.northeast); + // LatLng southwestBound = + // geoCoordToLatLng(response.routes![0].bounds!.southwest); + // //BOUNDS + // strRoute += 'northeastBound: ${latLngToString(northeastBound)}\n' + // 'southwestBound: ${latLngToString(southwestBound)}\n'; + // //SUMARY + // strRoute += 'summary: ${response.routes![0].summary}\n'; + // //OVERVIEW PATH + // List points = response.routes![0].overviewPath!; + // strRoute += 'overviewPath: '; + // for (GeoCoord geoCoord in points) { + // strRoute += '${geoCoordToString(geoCoord)}, '; + // } + // strRoute += '\n'; + // //WARNINGS + // final warnings = response.routes![0].warnings; + // strRoute += 'Warnings:'; + // if (warnings != null) { + // for (String? warning in warnings) { + // strRoute += '${warning!}, '; + // } + // } + // strRoute += '\n'; + // strSteps.add(strRoute); + // //STEPS + // List steps = response.routes![0].legs![0].steps!; + // for (Step step in steps) { + // strSteps.add('Distância: ${step.distance}\n' + // 'Duração: ${step.duration!.text}\n' + // 'Start Location: ${geoCoordToString(step.startLocation!)}\n' + // 'End Location: ${geoCoordToString(step.endLocation!)}\n' + // 'Instructions: ${step.instructions}\n' + // 'Maneuver: ${step.maneuver}\n' + // 'Transit: ${step.transit}\n' + // 'Travel Mode: ${step.travelMode}\n'); + // } + // //Update da string de ROTA e STEPS + // _routeAndStepsListSignal.value = strSteps; + // return strSteps; + // } + + // // TODO just for dev tests! + // void showOverViewPathPoints(List points) { + // Map markers = {}; + // for (LatLng point in points) { + // String strPoint = latLngToString(point); + // markers[strPoint] = Marker( + // markerId: MarkerId(strPoint), + // position: point, + // ); + // } + // _markersSignal.value = { + // ..._markersSignal.value, + // ...markers, + // }; + // } } diff --git a/spotholes_android/lib/package/extensions/directions_result_extension.dart b/spotholes_android/lib/package/extensions/directions_result_extension.dart new file mode 100644 index 0000000..eae32fb --- /dev/null +++ b/spotholes_android/lib/package/extensions/directions_result_extension.dart @@ -0,0 +1,19 @@ +import 'package:google_directions_api/google_directions_api.dart'; + +extension DirectionsResultExtension on DirectionsResult { + DirectionsResult copyWith({ + List? routes, + List? geocodedWaypoints, + DirectionsStatus? status, + String? errorMessage, + List? availableTravelModes, + }) { + return DirectionsResult( + routes: routes ?? this.routes, + geocodedWaypoints: geocodedWaypoints ?? this.geocodedWaypoints, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + availableTravelModes: availableTravelModes ?? this.availableTravelModes, + ); + } +} diff --git a/spotholes_android/lib/package/extensions/directions_route_extension.dart b/spotholes_android/lib/package/extensions/directions_route_extension.dart new file mode 100644 index 0000000..107e78f --- /dev/null +++ b/spotholes_android/lib/package/extensions/directions_route_extension.dart @@ -0,0 +1,25 @@ +import 'package:google_directions_api/google_directions_api.dart'; + +extension DirectionsRouteExtension on DirectionsRoute { + DirectionsRoute copyWith({ + List? legs, + GeoCoordBounds? bounds, + String? copyrights, + OverviewPolyline? overviewPolyline, + String? summary, + List? warnings, + List? waypointOrder, + final Fare? fare, + }) { + return DirectionsRoute( + bounds: bounds ?? this.bounds, + copyrights: copyrights ?? this.copyrights, + legs: legs ?? this.legs, + overviewPolyline: overviewPolyline ?? this.overviewPolyline, + summary: summary ?? this.summary, + warnings: warnings ?? this.warnings, + waypointOrder: waypointOrder ?? this.waypointOrder, + fare: fare ?? this.fare, + ); + } +} diff --git a/spotholes_android/lib/pages/navigation_page.dart b/spotholes_android/lib/pages/navigation_page.dart new file mode 100644 index 0000000..42fb5dd --- /dev/null +++ b/spotholes_android/lib/pages/navigation_page.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:signals/signals_flutter.dart'; +import 'package:spotholes_android/utilities/dark_mode_context_extension.dart'; + +import '../controllers/navigation_controller.dart'; +import '../controllers/route_controller.dart'; +import '../package/custom_info_window.dart'; +import '../utilities/constants.dart'; +import '../widgets/button/custom_floating_action_button.dart'; +import '../widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart'; +import '../widgets/nav_route_steps_page_view.dart'; + +class NavigationPage extends StatefulWidget { + final RouteController routeController; + + const NavigationPage({ + super.key, + required this.routeController, + }); + + @override + State createState() => _NavigationPageState(); +} + +class _NavigationPageState extends State { + late final _routeController = widget.routeController; + + late final _markers = _routeController.markersSignal; + + late final _directionResult = _routeController.directionResult; + late final _route = _directionResult.value.routes![0]; + late final _leg = _route.legs![0]; + late final _sourceLocation = + _routeController.geoCoordToLatLng(_leg.startLocation!); + late final _destinationLocation = + _routeController.geoCoordToLatLng(_leg.endLocation!); + + late final _routeSignal = signal(_route); + + late final _navigationController = NavigationController(_routeController); + + late final _customInfoWindowControllerSignal = + _routeController.customInfoWindowControllerSignal; + + @override + void dispose() { + NavigationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Watch( + (context) => PopScope( + //TODO definir ação do popScope para gerar o alertDialog! + child: Scaffold( + backgroundColor: context.isDarkMode ? Colors.black : Colors.white, + body: SafeArea( + child: Container( + color: context.isDarkMode ? Colors.white : Colors.black, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + color: Theme.of(context).focusColor, + child: Column( + children: [ + Text( + 'Navegação', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Watch( + (_) => NavRouteStepsPageView( + routeController: _routeController, + pageController: _navigationController.pageController, + route: _routeSignal, + ), + ), + Expanded( + child: Stack( + children: [ + LayoutBuilder( + builder: (BuildContext context, + BoxConstraints constraints) { + return SizedBox( + height: constraints.maxHeight, + child: Watch( + (_) => GoogleMap( + onMapCreated: + (GoogleMapController controller) { + _navigationController + .onMapCreated(controller); + }, + initialCameraPosition: CameraPosition( + target: _sourceLocation, + zoom: defaultZoomMap, + tilt: 45, + ), + polylines: _routeController + .polylinesSignal.value.values + .toSet(), + markers: _markers.value.values.toSet(), + zoomControlsEnabled: false, + onCameraIdle: () => + _customInfoWindowControllerSignal + .value.onCameraMove!(), + onTap: (position) => + _customInfoWindowControllerSignal + .value.hideInfoWindow!(), + onCameraMove: (position) => + _customInfoWindowControllerSignal + .value.onCameraMove!(), + ), + ), + ); + }, + ), + CustomInfoWindow( + controller: _customInfoWindowControllerSignal.value, + ), + NavigationDraggableSheet( + routeController: _routeController, + destinationLocation: _destinationLocation, + ), + Positioned( + bottom: 220, + right: 10, + left: 0, + child: Align( + alignment: Alignment.centerRight, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + CustomFloatingActionButton( + tooltip: "Start Mock Locaiton", + onPressed: () => {}, + // _navigationController.startMockLocation(), + icon: const Icon(Icons.play_arrow), + ), + const SizedBox(height: 10), + CustomFloatingActionButton( + tooltip: "Stop Mock Location", + onPressed: () => {}, + // _navigationController.stopMockLocation(), + icon: const Icon(Icons.pause), + ), + const SizedBox(height: 10), + CustomFloatingActionButton( + tooltip: "Centralizar a câmera", + onPressed: () => _navigationController + .centerCurrentLocation(), + icon: const Icon(Icons.location_searching), + ), + ], + ), + ), + ) + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/spotholes_android/lib/pages/route_page.dart b/spotholes_android/lib/pages/route_page.dart index 86d9afa..64262b6 100644 --- a/spotholes_android/lib/pages/route_page.dart +++ b/spotholes_android/lib/pages/route_page.dart @@ -5,6 +5,7 @@ import 'package:spotholes_android/widgets/spotholes_page_view.dart'; import '../controllers/route_controller.dart'; import '../package/custom_info_window.dart'; +import '../utilities/app_routes.dart'; import '../utilities/constants.dart'; import '../utilities/dark_mode_context_extension.dart'; import '../widgets/button/custom_button.dart'; @@ -53,7 +54,10 @@ class _RoutePageState extends State { CustomButton( label: 'Iniciar viagem', bgColor: Colors.tealAccent.shade400, - onPressed: () => {}, + onPressed: () => Navigator.of(context).pushNamed( + AppRoutes.navigation, + arguments: [RouteController.getCopy()], + ), ), CustomButton( label: 'Centralizar', diff --git a/spotholes_android/lib/services/navigation_service.dart b/spotholes_android/lib/services/navigation_service.dart new file mode 100644 index 0000000..389f863 --- /dev/null +++ b/spotholes_android/lib/services/navigation_service.dart @@ -0,0 +1,182 @@ +import 'dart:async'; + +import 'package:flutter/material.dart' hide Step; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:maps_toolkit/maps_toolkit.dart' as mtk; +import 'package:signals/signals_flutter.dart'; + +import '../controllers/route_controller.dart'; +import '../utilities/constants.dart'; + +class NavigationService { + final RouteController _routeController; + final PageController _pageController; + + NavigationService(this._routeController, this._pageController); + + late final Signal> _routePolylineCoordinatesSignal = + _routeController.routePolylineCoordinatesSignal; + late final List _stepsIndexes = _routeController.stepsIndexes; + late final totalPointsOnRoute = _routePolylineCoordinatesSignal.value.length; + // late final Signal _pageController = + // _routeController.pageControllerSignal; + + int _discardedPointsCounter = 0; + List routePointsMtk = []; + + Timer? _exitRouteTimer; + + // final _location = Location.instance; + // Function? _dispose; + + // void _listenCurrentLocation() async { + // _location.onLocationChanged.listen( + // (newLoc) { + // _locationService.loadCurrentLocationMark( + // _routeController.markersSignal, + // _routeController.customInfoWindowControllerSignal, + // ); + // if (newLoc.accuracy != null && newLoc.accuracy! < 30.0) { + // _updateNavigationCamera(locationToLatLng(newLoc), newLoc.heading!); + // _updateRouteStatus(locationToMtkLatLng(newLoc)); + // } else { + // //TODO snackbar alertando que o GPS está fora. Pode ser um signal que exibe o snackbar na tela com o ícone de GPS fora + // } + // }, + // ); + // } + + void startNavigation() { + routePointsMtk = + convertGmapsToMtkList(_routePolylineCoordinatesSignal.value); + // _listenCurrentLocation(); + } + + void stopNavigation() { + _discardedPointsCounter = 0; + if (_exitRouteTimer != null) { + _exitRouteTimer!.cancel(); + } + } + + List convertGmapsToMtkList(List originalList) { + return originalList.map((LatLng point) { + return mtk.LatLng(point.latitude, point.longitude); + }).toList(); + } + + LatLng locationToLatLng(location) => + LatLng(location.latitude!, location.longitude!); + + mtk.LatLng locationToMtkLatLng(location) => + mtk.LatLng(location.latitude!, location.longitude!); + + int locationIndexOnPath( + mtk.LatLng currentPosition, List routePoints) { + return mtk.PolygonUtil.locationIndexOnPath( + currentPosition, + routePoints, + true, + tolerance: routeDeviationTolerance, + ); + } + + int verifyStep() { + for (int i = 0; i < _stepsIndexes.length; i++) { + if (_discardedPointsCounter <= _stepsIndexes[i]) { + return i; + } + } + return -1; + } + + void updateRouteStatus(LatLng currentLocation) { + // TODO Criar verificação de destino alcançado, p.ex. se está a x metros do ponto final para limpar a rota + // TODO Verificar se há outras situações de fim da rota + // Verifica se a rota está vazia, p.ex trajeto concluído. + if (_routeController.routeStepsLatLng.value.isEmpty) { + return; + } + routePointsMtk = + convertGmapsToMtkList(_routePolylineCoordinatesSignal.value); + mtk.LatLng currentLocationMtk = locationToMtkLatLng(currentLocation); + int index = locationIndexOnPath(currentLocationMtk, routePointsMtk); + debugPrint('----index on polyline: $index'); + // Se a localização atual está na rota + if (index > 0) { + // Define a posição inicial para a localização atual + routePointsMtk[index] = currentLocationMtk; + // Remove os pontos iniciais até a posição atual + routePointsMtk.removeRange(0, index + 1); + _routePolylineCoordinatesSignal.value.removeRange(0, index + 1); + // Modifica a polyline da rota + _routeController.polylinesSignal.value['route'] = Polyline( + polylineId: const PolylineId("route"), + points: _routePolylineCoordinatesSignal.value, + width: 6, + color: primaryColor, + geodesic: true, + jointType: JointType.round, + ); + // Força o update da polyline da rota + _routeController.polylinesSignal.value = { + ..._routeController.polylinesSignal.value + }; + // Atualiza o contador de pontos descartados + _discardedPointsCounter += index + 1; + debugPrint('Pontos a descartar $_discardedPointsCounter'); + // Verifica o step atual na rota + int currentStepIndex = verifyStep(); + // Se o step avançou, o pageview é atualizado + if (currentStepIndex > 0 && _pageController.hasClients) { + // Remove steps que já passaram e faz update do signal + _stepsIndexes.removeRange(0, currentStepIndex); + _routeController.routeStepsLatLng.value + .removeRange(0, currentStepIndex); + _routeController.routeStepsLatLng.value = [ + ..._routeController.routeStepsLatLng.value + ]; + // Atualiza a polyline que representa a seta de manobra no mapa + _routeController.plotManeuverPolyline(0, updateCamera: false); + debugPrint('---pages: ${_pageController.page} $currentStepIndex'); + // Verifica se está na pageView correspondente ao step atual, senão atualiza + if (_pageController.page != currentStepIndex) { + // TODO Criar uma segunda condição, talvez um boolean para chavear entre monitorar automaticamente ou com base na ação do usuário, incluindo movimento de câmera, pageview, etc. + _pageController.jumpToPage(currentStepIndex); + } + } + // Caso esteja entre a posição 0 e 1 da polyline, então a polyline é atualizada + } else if (index == 0) { + _routePolylineCoordinatesSignal.value[0] = currentLocation; + // Força o update da polyline da rota + _routeController.polylinesSignal.value = { + ..._routeController.polylinesSignal.value + }; + // Caso seja o último step e tenha menos que 3 pontos, então descarta o último step, pontos e conclui a rota + } else if (_routeController.routeStepsLatLng.value.length == 1 && + _routePolylineCoordinatesSignal.value.length < 3) { + debugPrint('---Cheguei no final'); + _stepsIndexes.clear(); + _routeController.routeStepsLatLng.value.clear(); + _routePolylineCoordinatesSignal.value.clear(); + _routeController.clearManeuverPolyline(); + // Força update dos steps + _routeController.routeStepsLatLng.value = [ + ..._routeController.routeStepsLatLng.value + ]; + } else { + //TODO Criar ação para quando index = -1, ou seja, fora da rota. Inclusive com a possibildade de recálculo da rota; + } + debugPrint( + '----[Após descarte] points ${_routePolylineCoordinatesSignal.value.length} steps:${_routeController.routeStepsLatLng.value.length}'); + } + + void recalculateRoute(LatLng currentPosition) { + // Função mock, substitua com a lógica real para recalcular a rota usando a API de Directions do Google Maps + // vou precisar do contexto atualizado para fazer isso! + } + + // dispose() { + // _dispose!(); + // } +} diff --git a/spotholes_android/lib/utilities/app_routes.dart b/spotholes_android/lib/utilities/app_routes.dart index 6b2f37f..889c3a5 100644 --- a/spotholes_android/lib/utilities/app_routes.dart +++ b/spotholes_android/lib/utilities/app_routes.dart @@ -3,9 +3,13 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:spotholes_android/pages/base_map_page.dart'; import 'package:spotholes_android/pages/route_page.dart'; +import '../controllers/route_controller.dart'; +import '../pages/navigation_page.dart'; + abstract class AppRoutes { static const baseMap = '/'; static const route = '/route'; + static const navigation = '/navigation'; static Map get routes => { baseMap: (context) { @@ -19,6 +23,11 @@ abstract class AppRoutes { sourceLocation: sourceLocation, destinationLocation: destinationLocation, ); + }, + navigation: (context) { + final args = ModalRoute.of(context)!.settings.arguments as List; + final RouteController routeController = args[0]; + return NavigationPage(routeController: routeController); } }; } diff --git a/spotholes_android/lib/utilities/constants.dart b/spotholes_android/lib/utilities/constants.dart index a411968..cf06394 100644 --- a/spotholes_android/lib/utilities/constants.dart +++ b/spotholes_android/lib/utilities/constants.dart @@ -4,4 +4,8 @@ const Color primaryColor = Color(0xFF7B61FF); const double defaultPadding = 16.0; //Google Maps -const double defaultZoomMap = 18.5; \ No newline at end of file +const double defaultZoomMap = 18.5; +const double defaultNavigationTilt = 90; + +// Navigation +const double routeDeviationTolerance = 12; \ No newline at end of file diff --git a/spotholes_android/lib/utilities/location_service_mock.dart b/spotholes_android/lib/utilities/location_service_mock.dart new file mode 100644 index 0000000..66f31ad --- /dev/null +++ b/spotholes_android/lib/utilities/location_service_mock.dart @@ -0,0 +1,115 @@ +import 'dart:async'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:location/location.dart'; +import 'package:signals/signals_flutter.dart'; + +import '../utilities/custom_icons.dart'; +import '../widgets/info_window/marker_info_window.dart'; + +class LocationServiceMock { + LocationServiceMock._({this.simulationInterval = 2}) { + // _startMockLocationMonitoring(); + } + + static LocationServiceMock create({int simulationInterval = 2}) => + LocationServiceMock._(simulationInterval: simulationInterval); + + // final List> _simulatedPoints; + + // Lista de pontos para simulação (em torno de casa) + List> simulatedPoints = [ + {"lat": -4.970759417, "lon": -39.018417578}, + {"lat": -4.970149456, "lon": -39.018437396}, + {"lat": -4.970161685, "lon": -39.019398226}, + {"lat": -4.970174385, "lon": -39.020446581}, + {"lat": -4.970180457, "lon": -39.021795791}, + {"lat": -4.969608889, "lon": -39.021805958}, + {"lat": -4.969126117, "lon": -39.021809682}, + {"lat": -4.968599344, "lon": -39.021816028}, + {"lat": -4.968595255, "lon": -39.021114206}, + {"lat": -4.968580179, "lon": -39.020130772}, + {"lat": -4.968564485, "lon": -39.019058312}, + {"lat": -4.968611291, "lon": -39.018002439}, + {"lat": -4.968595552, "lon": -39.016842749}, + {"lat": -4.968579119, "lon": -39.015583046}, + {"lat": -4.968582717, "lon": -39.014573231}, + {"lat": -4.968558842, "lon": -39.013127272}, + {"lat": -4.968514769, "lon": -39.011763115}, + {"lat": -4.968506611, "lon": -39.010432066}, + {"lat": -4.968486545, "lon": -39.009220177}, + {"lat": -4.96774864, "lon": -39.009247243}, + {"lat": -4.967359017, "lon": -39.009227059}, + {"lat": -4.967086967, "lon": -39.009124385}, + {"lat": -4.966844242, "lon": -39.008627418}, + {"lat": -4.966557347, "lon": -39.007373746}, + {"lat": -4.966318344, "lon": -39.00619485}, + {"lat": -4.96597276, "lon": -39.004459807}, + {"lat": -4.965631465, "lon": -39.002866202}, + ]; + + + final int simulationInterval; // Intervalo em segundos entre as atualizações + final Signal _currentLocationSignal = Signal(null); + Timer? _simulationTimer; + + Signal get currentLocationSignal => _currentLocationSignal; + + LatLng get currentLocationLatLng => LatLng( + _currentLocationSignal.value!.latitude!, + _currentLocationSignal.value!.longitude!, + ); + + String currentLocationLatLngURLPattern() => + "${_currentLocationSignal.value!.latitude!.toString()}" + "%2C${_currentLocationSignal.value!.longitude!.toString()}"; + + void loadCurrentLocationMark( + Signal> markersSignal, final customInfoWindowControllerSignal) { + final newMarker = Marker( + markerId: const MarkerId("currentLocationMarker"), + icon: CustomIcons.currentLocationIcon, + position: currentLocationLatLng, + onTap: () => customInfoWindowControllerSignal.value.addInfoWindow!( + const MarkerInfoWindow( + title: 'Localização', + textContent: 'Você está aqui!', + ), + currentLocationLatLng, + ), + ); + markersSignal.value = { + ...markersSignal.value, + 'currentLocationMarker': newMarker, + }; + } + + void startMockLocationMonitoring() { + int currentIndex = 0; + _simulationTimer = Timer.periodic( + Duration(seconds: simulationInterval), + (timer) { + if (currentIndex < simulatedPoints.length) { + final point = simulatedPoints[currentIndex]; + _currentLocationSignal.value = LocationData.fromMap({ + 'latitude': point['lat'], + 'longitude': point['lon'], + 'accuracy': 5.0, + 'time': DateTime.now().millisecondsSinceEpoch, + }); + currentIndex++; + } else { + stopMockLocationMonitoring(); + } + }, + ); + } + + void stopMockLocationMonitoring() { + _simulationTimer?.cancel(); + _simulationTimer = null; + } + + void dispose() { + stopMockLocationMonitoring(); + } +} diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart new file mode 100644 index 0000000..5f2e239 --- /dev/null +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart @@ -0,0 +1,274 @@ +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:signals/signals_flutter.dart'; +import 'package:spotholes_android/controllers/route_controller.dart'; +import 'package:spotholes_android/widgets/bullet_list.dart'; + +import '../button/custom_button.dart'; + +class NavigationDraggableSheet extends StatefulWidget { + const NavigationDraggableSheet({ + super.key, + required this.routeController, + required this.destinationLocation, + }); + + final LatLng destinationLocation; + final RouteController routeController; + + @override + NavigationDraggableSheetState createState() => + NavigationDraggableSheetState(); +} + +class NavigationDraggableSheetState extends State { + late final _routeController = widget.routeController; + final scrollController = ScrollController(); + + final _draggableController = DraggableScrollableController(); + final _draggableExtentNotifier = signal(0.0); + final _minDraggableChildSize = 0.25; + final _intermediateDraggableChildSize = 0.4; + final _maxDraggableChildSize = 1.0; + // int? _selectedIndex; + + late final _canvasColor = Theme.of(context).canvasColor; + + late final _directionResult = _routeController.directionResult.value; + late final _route = _directionResult.routes![0]; + late final _leg = _route.legs![0]; + // late final _steps = _leg.steps!; + + // late final _spotholesInRouteListSignal = + // _routeController.spotholesInRouteList; + + @override + void initState() { + super.initState(); + _draggableController.addListener(_updateExtent); + if (_haveWarnings()) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _showWarningsDialog(); + }); + } + } + + bool _haveWarnings() => _route.warnings?.isNotEmpty == true ? true : false; + + void _showWarningsDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.warning), + SizedBox( + width: 12, + ), + Text( + 'Alertas na Rota', + ), + ], + ), + content: BulletList( + items: _route.warnings!, + ), + actions: [ + TextButton( + child: const Text( + 'OK', + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + void _updateExtent() { + _draggableExtentNotifier.value = _draggableController.size; + } + + void changeSizeDraggableScrollableSheet(double size) => + _draggableController.animateTo( + size, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ); + + @override + void dispose() { + _draggableController.removeListener(_updateExtent); + _draggableController.dispose(); + _draggableExtentNotifier.dispose(); + super.dispose(); + } + + List _horizontalListButtons(BuildContext context, position) { + return [ + // CustomButton( + // label: 'Alertar Risco', + // bgColor: Colors.tealAccent.shade400, + // // onPressed: () => , + // ), + CustomButton( + label: 'Centralizar', + bgColor: Colors.tealAccent.shade400, + onPressed: () => { + _routeController.centerViewRoute(), + changeSizeDraggableScrollableSheet(_minDraggableChildSize), + }, + ), + ]; + } + + @override + Widget build(BuildContext context) { + return Watch( + (context) => Column( + children: [ + Expanded( + child: DraggableScrollableSheet( + controller: _draggableController, + maxChildSize: _maxDraggableChildSize, + minChildSize: _minDraggableChildSize, + initialChildSize: _minDraggableChildSize, + snap: true, + snapSizes: [ + _minDraggableChildSize, + _intermediateDraggableChildSize, + _maxDraggableChildSize + ], + builder: (context, scrollController) => Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: _canvasColor, + border: Border.all(width: 0.5), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(25), + topRight: Radius.circular(25), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverToBoxAdapter( + child: Center( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).hintColor, + borderRadius: const BorderRadius.all( + Radius.circular(10), + ), + ), + height: 4, + width: 40, + margin: const EdgeInsets.symmetric(vertical: 10), + ), + ), + ), + SliverAppBar( + title: Text( + '${_leg.distance!.text} (${_leg.duration!.text})', + style: Theme.of(context).textTheme.titleLarge, + ), + primary: false, + pinned: true, + centerTitle: true, + // toolbarHeight: 80, + // leadingWidth: 50, + backgroundColor: _canvasColor, + leading: Watch.builder( + builder: (context) => IconButton( + icon: Icon( + _draggableExtentNotifier.value < + _maxDraggableChildSize + ? Icons.expand_less + : Icons.expand_more, + ), + onPressed: () { + if (_draggableExtentNotifier.value < + _maxDraggableChildSize) { + changeSizeDraggableScrollableSheet( + _maxDraggableChildSize); + } else { + changeSizeDraggableScrollableSheet( + _minDraggableChildSize); + } + }, + ), + ), + actions: [ + if (_haveWarnings()) + IconButton( + icon: const Icon(Icons.warning), + onPressed: _showWarningsDialog, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ], + // bottom: PreferredSize( + // preferredSize: const Size.fromHeight(35), + // child: Padding( + // padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), + // child: Text( + // 'TESTE TESTE TESTE TESTE', + // style: Theme.of(context).textTheme.bodyLarge, + // maxLines: 2, + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ), + ), + SliverFillRemaining( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + color: Theme.of(context).focusColor, + child: Column( + children: [ + Text( + 'Via: ${_route.summary!}', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + )) + ], + ), + ), + ), + ), + ), + Container( + color: Colors.white, + child: SizedBox( + height: 60.0, + child: ListView( + scrollDirection: Axis.horizontal, + children: + _horizontalListButtons(context, widget.destinationLocation), + ), + ), + ), + ], + ), + ); + } +} diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/route_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/route_draggable_sheet.dart index 1df510c..e27cf0d 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/route_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/route_draggable_sheet.dart @@ -6,6 +6,7 @@ import 'package:spotholes_android/controllers/route_controller.dart'; import 'package:spotholes_android/widgets/bullet_list.dart'; import '../../models/spothole.dart'; +import '../../utilities/app_routes.dart'; import '../../utilities/custom_icons.dart'; import '../../utilities/maneuver_icons.dart'; import '../button/custom_button.dart'; @@ -125,7 +126,10 @@ class RouteDraggableSheetState extends State { CustomButton( label: 'Iniciar viagem', bgColor: Colors.tealAccent.shade400, - onPressed: () => {}, + onPressed: () => Navigator.of(context).pushNamed( + AppRoutes.navigation, + arguments: [RouteController.getCopy()], + ), ), CustomButton( label: 'Centralizar', diff --git a/spotholes_android/lib/widgets/nav_route_steps_page_view.dart b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart new file mode 100644 index 0000000..3f703c1 --- /dev/null +++ b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart @@ -0,0 +1,212 @@ +import 'dart:async'; + +import 'package:expandable_page_view/expandable_page_view.dart'; +import 'package:flutter/material.dart' hide Step; +import 'package:flutter_html/flutter_html.dart' hide Marker; +import 'package:google_directions_api/google_directions_api.dart'; +import 'package:signals/signals_flutter.dart'; + +import '../controllers/route_controller.dart'; +import '../utilities/custom_icons.dart'; +import '../utilities/maneuver_icons.dart'; + +class NavRouteStepsPageView extends StatefulWidget { + final RouteController routeController; + final PageController pageController; + final Signal route; + + const NavRouteStepsPageView({ + super.key, + required this.routeController, + required this.pageController, + required this.route, + }); + + @override + RouteStepsStatePageView createState() => RouteStepsStatePageView(); +} + +class RouteStepsStatePageView extends State { + late final _routeController = widget.routeController; + late final _pageController = widget.pageController; + int _currentPage = 0; + + late final _route = widget.route; + late final _leg = computed(() => _route.value.legs![0]); + late final _steps = _routeController.routeStepsLatLng; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _animateOnce(); + }); + _pageController.addListener( + () { + int newPage = _pageController.page!.round(); + if (newPage != _currentPage) { + _currentPage = newPage; + _routeController.clearManeuverPolyline(); + if (_currentPage == 0) { + _routeController.updateCameraGeoCoord(_leg.value.startLocation!); + } else if (_currentPage == _steps.value.length + 1) { + _routeController.updateCameraGeoCoord(_leg.value.endLocation!); + } else { + final stepIndex = _currentPage - 1; + _routeController.plotManeuverPolyline(stepIndex); + } + } + }, + ); + } + + void _animateOnce() { + if (_pageController.hasClients) { + _pageController.animateTo( + _pageController.position.pixels + + MediaQuery.of(context).size.width / 3.5, + duration: const Duration(milliseconds: 500), + curve: Curves.linear, + ); + Future.delayed( + const Duration(milliseconds: 600), + () { + if (_pageController.hasClients) { + _pageController.animateTo( + _pageController.position.pixels - + MediaQuery.of(context).size.width / 3.5, + duration: const Duration(milliseconds: 500), + curve: Curves.linear, + ); + } + }, + ); + } + } + + @override + void dispose() { + _routeController.clearManeuverPolyline(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Watch( + (context) => Material( + child: InkWell( + child: ExpandablePageView.builder( + controller: _pageController, + itemCount: _steps.value.length + 2, + itemBuilder: (context, index) { + // Se step inicial, ponto de partida + if (index == 0) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(width: 0.5), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: ListTile( + title: Text( + 'Partida: ${_leg.value.startAddress!}', + ), + leading: SizedBox( + height: 35, + width: 35, + child: CustomIcons.sourceIconAsset, + ), + onTap: () => { + // _routeController.updateCameraGeoCoord(_leg.startLocation!), + }, + ), + ), + ); + // Caso seja o step do destino + } else if (index == _steps.value.length + 1) { + return Watch( + (context) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(width: 0.5), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: ListTile( + onTap: () => { + // _routeController.updateCameraGeoCoord(_leg.endLocation!), + }, + title: Text( + 'Destino: ${_leg.value.endAddress!}', + ), + leading: SizedBox( + height: 35, + width: 35, + child: CustomIcons.destinationIconAsset, + ), + ), + ), + ), + ); + // Demais steps que contém manobras, excluindo a origem e o destino + } else if (index >= 1 && index <= _steps.value.length) { + final step = _steps.value[index - 1]; + final maneuver = step.maneuver ?? 'straight'; + final icon = maneuverIcons[maneuver] ?? Icons.directions; + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(width: 0.5), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: ListTile( + onTap: () => { + // maneuver == 'straight' + // ? _routeController.newCameraLatLngBoundsFromStep(step) + // : _routeController + // .updateCameraGeoCoord(step.startLocation!), + }, + leading: Icon( + icon, + size: 35, + ), + title: Html( + data: step.instructions!, + style: { + "body": Style( + fontStyle: Theme.of(context) + .textTheme + .bodyLarge! + .fontStyle, + margin: Margins.zero, + ), + }, + ), + subtitle: Html( + data: 'Distância: ${step.distance!.text}', + style: { + "body": Style( + fontStyle: Theme.of(context) + .textTheme + .bodyLarge! + .fontStyle, + margin: Margins.zero, + ), + }, + ), + ), + ), + ); + // TODO Testar: se fora do range retorna vazio, em vez de exceção. + } else { + return const SizedBox.shrink(); + } + }, + ), + ), + ), + ); + } +} diff --git a/spotholes_android/lib/widgets/route_steps_page_view.dart b/spotholes_android/lib/widgets/route_steps_page_view.dart index 3179b14..641f565 100644 --- a/spotholes_android/lib/widgets/route_steps_page_view.dart +++ b/spotholes_android/lib/widgets/route_steps_page_view.dart @@ -29,7 +29,7 @@ class RouteStepsStatePageView extends State { final _routeController = RouteController.instance; late final _leg = widget.route.legs![0]; - late final _steps = _leg.steps!; + late final _steps = _routeController.routeStepsLatLng; @override void initState() { @@ -45,11 +45,11 @@ class RouteStepsStatePageView extends State { _cleanPolylines(); if (_currentPage == 0) { _routeController.updateCameraGeoCoord(_leg.startLocation!); - } else if (_currentPage == _steps.length + 1) { + } else if (_currentPage == _steps.value.length + 1) { _routeController.updateCameraGeoCoord(_leg.endLocation!); } else { final stepIndex = _currentPage - 1; - _routeController.plotManeuver(stepIndex); + _routeController.plotManeuverPolyline(stepIndex); } } }, @@ -68,10 +68,10 @@ class RouteStepsStatePageView extends State { final initialPage = _pageController.initialPage; if (initialPage == 0) { _routeController.updateCameraGeoCoord(_leg.startLocation!); - } else if (initialPage == _steps.length + 1) { + } else if (initialPage == _steps.value.length + 1) { _routeController.updateCameraGeoCoord(_leg.endLocation!); } else { - final step = _steps[initialPage - 1]; + final step = _steps.value[initialPage - 1]; final maneuver = step.maneuver ?? 'straight'; maneuver == 'straight' ? _routeController.newCameraLatLngBoundsFromStep(step) @@ -116,7 +116,7 @@ class RouteStepsStatePageView extends State { child: InkWell( child: ExpandablePageView.builder( controller: _pageController, - itemCount: _steps.length + 2, + itemCount: _steps.value.length + 2, itemBuilder: (context, index) { if (index == 0) { return Container( @@ -142,7 +142,7 @@ class RouteStepsStatePageView extends State { ), ), ); - } else if (index == _steps.length + 1) { + } else if (index == _steps.value.length + 1) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), @@ -166,7 +166,7 @@ class RouteStepsStatePageView extends State { ), ); } else { - final step = _steps[index - 1]; + final step = _steps.value[index - 1]; final maneuver = step.maneuver ?? 'straight'; final icon = maneuverIcons[maneuver] ?? Icons.directions; return Container( From aa6cff2e94d8b68f92ae045fb1546543ff27786c Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Sun, 22 Dec 2024 20:39:16 -0300 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20Implementa=20melhorias=20relacion?= =?UTF-8?q?adas=20=C3=A0=20navega=C3=A7=C3=A3o=20e=20faz=20corre=C3=A7?= =?UTF-8?q?=C3=B5es.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Funcionalidades: - Implementa a lógica de recálculo automático da rota de navegação. Agora a rota é recalculada se o usuário se deslocar 5 vezes consecutivas fora da margem de tolerância da rota, durante a navegação. Sendo limitado a 3 recálculos automáticos para cada rota, evitando assim recálculos excessivos. - Implementa a visão de mapa 2d na tela de navegação para melhorar a experiência de navegação. - Implementa o uso do GlobalKey navigatorKey na raiz do app para permitir o acesso do contexto geral da aplicação fora da árvore de widgets, por exemplo nos controllers e services. - Implementa a lógica do botão de rastreio da câmera do mapa com base na localização do dispositivo. O botão alterna entre o modo de rastreio automático ativo ou inativo. Algumas ações de movimento da câmera, manual ou automático, também alteram os modos de rastreio, conforme as regras de negócio. Correções de bug: - Corrige o problema de exibição da lista de spotholes no DraggableScrollableSheet da página de visualização da rota. - Corrige exibição da seta de manobra na rota. Foi necessário criar uma variável auxiliar para manter a lista de steps da rota. - Corrige exceções relacionadas ao tentar manipular a câmera do mapa antes de garantir que o controlador do mapa seja definido. Foi implementado de forma correta o uso do Completer para fazer as verificações. - Corrige exibição do customInfoWindow nos marcadores de spotholes da tela de navegação. Foi necessário atualizar os marcadores com o novo controller do mapa ao navegar para a tela de navegação, pois o controller do customInfoWindow precisa ser atualizado com o controller do novo widget de mapa. Refatorações: - Refatora a lógica de inicialização da página inicial da aplicação, base_map_page, para simplifica-la. --- .../lib/controllers/base_map_controller.dart | 84 +++++++++++-------- .../controllers/navigation_controller.dart | 31 +++++-- .../lib/controllers/route_controller.dart | 70 ++++++++++------ .../spothole_info_window_controller.dart | 6 +- spotholes_android/lib/main.dart | 5 ++ .../lib/pages/base_map_page.dart | 24 +++--- .../lib/pages/navigation_page.dart | 23 ++++- spotholes_android/lib/pages/route_page.dart | 2 +- .../lib/services/location_service.dart | 1 + .../lib/services/navigation_service.dart | 77 ++++++----------- .../lib/services/spothole_service.dart | 33 ++++---- .../lib/utilities/constants.dart | 9 +- .../custom_floating_action_button_list.dart | 24 ++++-- .../main_draggable_sheet.dart | 2 +- .../place_draggable_sheet.dart | 4 +- .../widgets/nav_route_steps_page_view.dart | 5 +- 16 files changed, 236 insertions(+), 164 deletions(-) diff --git a/spotholes_android/lib/controllers/base_map_controller.dart b/spotholes_android/lib/controllers/base_map_controller.dart index e6d19ca..6a3d442 100644 --- a/spotholes_android/lib/controllers/base_map_controller.dart +++ b/spotholes_android/lib/controllers/base_map_controller.dart @@ -30,48 +30,67 @@ class BaseMapController { Function? _dispose; final LocationService _locationService = LocationService.instance; + late final Signal _currentLocationSignal = _locationService.currentLocationSignal; + get currentLocationSignal => _currentLocationSignal; + get currentLocationLatLng => LatLng(_currentLocationSignal.value!.latitude!, + _currentLocationSignal.value!.longitude!); final databaseReference = getIt(); late final dataBaseSpotholesRef = databaseReference.child('spotholes'); - GoogleMapController? _googleMapController; - final _googleMapControllerCompleter = Completer(); + final _googleMapControllerCompleter = Completer(); + get getGoogleMapController async => + await _googleMapControllerCompleter.future; + final _customInfoWindowControllerSignal = Signal(CustomInfoWindowController()); - SpotholeService? spotholeService; + get customInfoWindowControllerSignal => _customInfoWindowControllerSignal; + + final _markersSignal = Signal>({}); + get markersSignal => _markersSignal; + + late final spotholeService = SpotholeService( + _markersSignal, + _customInfoWindowControllerSignal, + ); + final _textEditingController = TextEditingController(); + get textEditingController => _textEditingController; + final _searchBarFocusNode = FocusNode(); + get searchBarFocusNode => _searchBarFocusNode; late Signal draggableScrollableSheetSignal = signal( DraggableScrollableSheetTypes.initial.widget, ); final _geocodingService = GeocodingService.instance; + final isTrackingLocation = signal(true); + final isProgrammaticMove = signal(true); - final _markersSignal = Signal>({}); - - get markersSignal => _markersSignal; - get currentLocationSignal => _currentLocationSignal; - get textEditingController => _textEditingController; - get searchBarFocusNode => _searchBarFocusNode; - get customInfoWindowControllerSignal => _customInfoWindowControllerSignal; - get currentLocationLatLng => LatLng(_currentLocationSignal.value!.latitude!, - _currentLocationSignal.value!.longitude!); - - void onMapCreated(mapController, context) { - _googleMapControllerCompleter.complete(mapController); + void onMapCreated(mapController) { _customInfoWindowControllerSignal.value.googleMapController = mapController; - spotholeService = SpotholeService( - _markersSignal, - _customInfoWindowControllerSignal, - ); - spotholeService!.loadSpotholeMarkers(context); + _googleMapControllerCompleter.complete(mapController); + listenCurrentLocation(); + spotholeService.loadSpotholeMarkers(); } - void updateCameraGoogleMapsController(position, [zoom = defaultZoomMap]) { - _googleMapController!.animateCamera( + void trackLocation() { + if (isTrackingLocation.value) { + isTrackingLocation.value = false; + } else { + isTrackingLocation.value = true; + centerView(); + } + } + + void updateCameraGoogleMapsController(position, + [zoom = defaultZoomMap]) async { + final mapController = await getGoogleMapController; + isProgrammaticMove.value = true; + mapController.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( zoom: zoom, @@ -85,12 +104,6 @@ class BaseMapController { updateCameraGoogleMapsController(currentLocationLatLng); } - void loadCurrentLocation() async { - _googleMapController = await _googleMapControllerCompleter.future; - centerView(); - listenCurrentLocation(); - } - void listenCurrentLocation() async { _dispose = effect( () { @@ -102,6 +115,9 @@ class BaseMapController { ), ); } + if (isTrackingLocation.value) { + centerView(); + } }, ); } @@ -114,6 +130,7 @@ class BaseMapController { _customInfoWindowControllerSignal.value.hideInfoWindow!(); removeMarkerByKey(key); changeDraggableSheet(DraggableScrollableSheetTypes.initial); + isTrackingLocation.value = true; centerView(); } @@ -140,13 +157,13 @@ class BaseMapController { markersSignal.value.remove(key); } - void loadSpotholeMarkers(context) { - spotholeService!.loadSpotholeMarkers(context); + void loadSpotholeMarkers() { + spotholeService.loadSpotholeMarkers(); } - void registerSpotholeModal(context, {LatLng? position}) { + void registerSpotholeModal({LatLng? position}) { final latLng = position ?? currentLocationLatLng; - spotholeService!.registerSpotholeModal(context, latLng); + spotholeService.registerSpotholeModal(latLng); } void onLongPress(BuildContext context, LatLng position) async { @@ -183,8 +200,7 @@ class BaseMapController { DraggableScrollableSheetTypes.location( position: position, formattedPlacemark: formattedPlacemark, - onRegister: () => - spotholeService!.registerSpotholeModal(context, position), + onRegister: () => spotholeService.registerSpotholeModal(position), ), ); } diff --git a/spotholes_android/lib/controllers/navigation_controller.dart b/spotholes_android/lib/controllers/navigation_controller.dart index eaffd9d..ef35ae1 100644 --- a/spotholes_android/lib/controllers/navigation_controller.dart +++ b/spotholes_android/lib/controllers/navigation_controller.dart @@ -16,7 +16,6 @@ class NavigationController { late final Signal _currentLocationSignal = _locationService.currentLocationSignal; - GoogleMapController? _mapController; static Function? _dispose; final _pageController = PageController(initialPage: 1); @@ -26,6 +25,18 @@ class NavigationController { NavigationService(_routeController, _pageController); get navigationService => _navigationService; + final isTrackingLocation = signal(true); + final isProgrammaticMove = signal(true); + + void trackLocation() { + if (isTrackingLocation.value) { + isTrackingLocation.value = false; + } else { + isTrackingLocation.value = true; + centerCurrentLocation(); + } + } + /*Tentativa de Mock do serviço de localização final _locationService = LocationServiceMock.create(); late final Signal _currentLocationSignal = @@ -38,11 +49,10 @@ class NavigationController { } */ void onMapCreated(mapController) { - _mapController = mapController; - _routeController.googleMapController = _mapController; - _routeController.onMapCreated(_mapController); + mapController.setMapStyle(mapStyle2D); + _routeController.onMapCreated(mapController); listenCurrentLocation(); - // _navigationService.startNavigation(); + _routeController.updateAllRouteMarkers(); } void listenCurrentLocation() async { @@ -60,25 +70,30 @@ class NavigationController { _navigationService.updateRouteStatus(currentLocation); }, ); - _updateNavigationCamera(currentLocation, heading); + if (isTrackingLocation.value) { + _updateNavigationCamera(currentLocation, heading); + } } }, ); } - void _updateNavigationCamera(LatLng position, double heading) { + void _updateNavigationCamera(LatLng position, double heading) async { final CameraPosition newCameraPosition = CameraPosition( target: position, zoom: defaultZoomMap, bearing: heading, tilt: defaultNavigationTilt, ); - _mapController!.animateCamera( + final mapController = await _routeController.getGoogleMapController; + isProgrammaticMove.value = true; + mapController.animateCamera( CameraUpdate.newCameraPosition(newCameraPosition), ); } void centerCurrentLocation() { + isProgrammaticMove.value = true; _routeController.updateCameraLatLng(_locationService.currentLocationLatLng); } diff --git a/spotholes_android/lib/controllers/route_controller.dart b/spotholes_android/lib/controllers/route_controller.dart index a41d0ee..3c76959 100644 --- a/spotholes_android/lib/controllers/route_controller.dart +++ b/spotholes_android/lib/controllers/route_controller.dart @@ -38,21 +38,22 @@ class RouteController { late final dataBaseSpotholesRef = databaseReference.child('spotholes'); final _spotholesInRouteList = Signal>([]); - GoogleMapController? _googleMapController; - get googleMapController => _googleMapController; - set googleMapController(mapController) => - _googleMapController = mapController; - - final _googleMapControllerCompleter = Completer(); - get googleMapControllerCompleter => _googleMapControllerCompleter; + final _googleMapControllerCompleter = Completer(); + get getGoogleMapController async => + await _googleMapControllerCompleter.future; final _customInfoWindowControllerSignal = Signal(CustomInfoWindowController()); get customInfoWindowControllerSignal => _customInfoWindowControllerSignal; - SpotholeService? _spotholeService; - + // Store all markers final _markersSignal = Signal>({}); + + late final _spotholeService = SpotholeService( + _markersSignal, + _customInfoWindowControllerSignal, + ); + // Store all polyline points final _routePolylineCoordinatesSignal = Signal>([]); // Store all polylines @@ -83,10 +84,8 @@ class RouteController { get pageControllerSignal => _pageControllerSignal; void onMapCreated(mapController) { - _googleMapControllerCompleter.complete(mapController); _customInfoWindowControllerSignal.value.googleMapController = mapController; - _spotholeService = - SpotholeService(_markersSignal, _customInfoWindowControllerSignal); + _googleMapControllerCompleter.complete(mapController); } GeoCoord latLngToGeoCoord(LatLng latLng) => @@ -101,8 +100,9 @@ class RouteController { String geoCoordToString(GeoCoord geoCoord) => '${geoCoord.latitude},${geoCoord.longitude}'; - void updateCameraGeoCoord(GeoCoord target, [zoom = defaultZoomMap]) { - _googleMapController!.animateCamera( + void updateCameraGeoCoord(GeoCoord target, [zoom = defaultZoomMap]) async { + final mapController = await getGoogleMapController; + mapController.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( zoom: zoom, @@ -112,8 +112,9 @@ class RouteController { ); } - void updateCameraLatLng(LatLng target, [zoom = defaultZoomMap]) { - _googleMapController!.animateCamera( + void updateCameraLatLng(LatLng target, [zoom = defaultZoomMap]) async { + final mapController = await getGoogleMapController; + mapController.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( zoom: zoom, @@ -133,8 +134,9 @@ class RouteController { newCameraLatLngBounds([startLocationLatLng, endLocationLatLng]); } - void newCameraLatLngBounds(List polylineCoordinates) { - _googleMapController!.animateCamera( + void newCameraLatLngBounds(List polylineCoordinates) async { + final mapController = await getGoogleMapController; + mapController.animateCamera( CameraUpdate.newLatLngBounds( MapUtils.boundsFromLatLngList(polylineCoordinates), 70, @@ -165,7 +167,7 @@ class RouteController { } Future loadRouteWithLegsAndSteps( - LatLng sourceLocation, LatLng destinationLocation, context) async { + LatLng sourceLocation, LatLng destinationLocation) async { DirectionsService.init(EnvironmentConfig.googleApiKey!); final directionsService = DirectionsService(); @@ -179,7 +181,7 @@ class RouteController { await directionsService.route( request, - (DirectionsResult response, DirectionsStatus? status) async { + (DirectionsResult response, DirectionsStatus? status) { if (status == DirectionsStatus.ok) { _directionResult.value = response; _routeStepsLatLng.value = response.routes!.first.legs!.first.steps!; @@ -197,9 +199,9 @@ class RouteController { ); loadRouteMarkers(_routePolylineCoordinatesSignal.value.first, _routePolylineCoordinatesSignal.value.last); - _googleMapController = await _googleMapControllerCompleter.future; centerViewRoute(); - loadSpotholesInRoute(context); + _spotholeService.loadSpotholesInRoute( + routePolylineCoordinatesSignal.value, _spotholesInRouteList); } else { // do something with error response } @@ -239,9 +241,13 @@ class RouteController { }; } - void loadSpotholesInRoute(context) { - _spotholesInRouteList.value = _spotholeService! - .loadSpotholesInRoute(context, routePolylineCoordinatesSignal.value); + // Update context and controllers related to the markers + void updateAllRouteMarkers() { + final sourceLocation = markersSignal.value['sourceRouteMarker']!.position; + final destinationLocation = + markersSignal.value['destinationRouteMarker']!.position; + loadRouteMarkers(sourceLocation, destinationLocation); + _spotholeService.addSpotholeMarkers(spotholesInRouteList); } void setupStepsPageView(int initialPage, String type) { @@ -317,6 +323,20 @@ class RouteController { polylinesSignal.value = {...polylinesSignal.value}; } + void updateRoutePolyline() { + // Modifica a polyline da rota + polylinesSignal.value['route'] = Polyline( + polylineId: const PolylineId("route"), + points: _routePolylineCoordinatesSignal.value, + width: 6, + color: primaryColor, + geodesic: true, + jointType: JointType.round, + ); + // Força o update das polylines da rota + polylinesSignal.value = {...polylinesSignal.value}; + } + RouteController isolatedCopy() { var copy = RouteController._(); copy._spotholesInRouteList.value = List.from(_spotholesInRouteList.value); diff --git a/spotholes_android/lib/controllers/spothole_info_window_controller.dart b/spotholes_android/lib/controllers/spothole_info_window_controller.dart index 2abea60..4c75a0b 100644 --- a/spotholes_android/lib/controllers/spothole_info_window_controller.dart +++ b/spotholes_android/lib/controllers/spothole_info_window_controller.dart @@ -4,6 +4,7 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:signals/signals.dart'; import 'package:spotholes_android/package/custom_info_window.dart'; +import '../main.dart'; import '../models/spothole.dart'; import '../services/service_locator.dart'; import '../utilities/constants.dart'; @@ -35,7 +36,8 @@ class SpotholeInfoWindowController { ); } - void addSpotholeMarker(context, Spothole spothole) { + void addSpotholeMarker(Spothole spothole) { + final context = MyApp.navigatorKey.currentContext; final marker = Marker( markerId: MarkerId(spothole.id!), icon: spothole.type == Type.deepHole @@ -68,7 +70,7 @@ class SpotholeInfoWindowController { spothole.category = riskCategory; spothole.type = type; spothole.id = spotholeId; - addSpotholeMarker(context, spothole); + addSpotholeMarker(spothole); _markersSignal.value = {..._markersSignal.value}; _markersSignal.value[spotholeId]!.onTap!(); updateCameraGoogleMapsController(spothole.position); diff --git a/spotholes_android/lib/main.dart b/spotholes_android/lib/main.dart index 14e377b..ca179da 100644 --- a/spotholes_android/lib/main.dart +++ b/spotholes_android/lib/main.dart @@ -28,9 +28,14 @@ void main() async { class MyApp extends StatelessWidget { const MyApp({super.key}); + // Cria uma chave global para o Navigator, permitindo a consulta do context global fora da árvore (solução para controllers) + static final GlobalKey navigatorKey = + GlobalKey(); + @override Widget build(BuildContext context) { return MaterialApp( + navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, title: 'Spotholes', theme: ThemeData( diff --git a/spotholes_android/lib/pages/base_map_page.dart b/spotholes_android/lib/pages/base_map_page.dart index cbc3fc0..5f44167 100644 --- a/spotholes_android/lib/pages/base_map_page.dart +++ b/spotholes_android/lib/pages/base_map_page.dart @@ -23,21 +23,17 @@ class BaseMapPageState extends State { late final _currentLocationSignal = _baseMapController.currentLocationSignal; late final _draggableScrollableSheetSignal = _baseMapController.draggableScrollableSheetSignal; + late final _isProgrammaticMove = _baseMapController.isProgrammaticMove; + late final _isTrackingLocation = _baseMapController.isTrackingLocation; void _onMapCreated(mapController) { - _baseMapController.onMapCreated(mapController, context); + _baseMapController.onMapCreated(mapController); } void _onLongPress(LatLng position) { _baseMapController.onLongPress(context, position); } - @override - void initState() { - super.initState(); - _baseMapController.loadCurrentLocation(); - } - @override void dispose() { _baseMapController.dispose(); @@ -74,9 +70,17 @@ class BaseMapPageState extends State { zoomControlsEnabled: false, onTap: (position) => _customInfoWindowControllerSignal .value.hideInfoWindow!(), - onCameraMove: (position) => - _customInfoWindowControllerSignal - .value.onCameraMove!(), + onCameraMove: (position) { + _customInfoWindowControllerSignal + .value.onCameraMove!(); + }, + onCameraMoveStarted: () { + if (!_isProgrammaticMove.value) { + _isTrackingLocation.value = false; + } else { + _isProgrammaticMove.value = false; + } + }, ), CustomInfoWindow( controller: _customInfoWindowControllerSignal.value, diff --git a/spotholes_android/lib/pages/navigation_page.dart b/spotholes_android/lib/pages/navigation_page.dart index 42fb5dd..1cc1ba0 100644 --- a/spotholes_android/lib/pages/navigation_page.dart +++ b/spotholes_android/lib/pages/navigation_page.dart @@ -43,6 +43,13 @@ class _NavigationPageState extends State { late final _customInfoWindowControllerSignal = _routeController.customInfoWindowControllerSignal; + late final _isTrackingLocation = _navigationController.isTrackingLocation; + late final _isProgrammaticMove = _navigationController.isProgrammaticMove; + + void trackLocation() { + _navigationController.trackLocation(); + } + @override void dispose() { NavigationController.dispose(); @@ -118,6 +125,13 @@ class _NavigationPageState extends State { onCameraMove: (position) => _customInfoWindowControllerSignal .value.onCameraMove!(), + onCameraMoveStarted: () { + if (!_isProgrammaticMove.value) { + _isTrackingLocation.value = false; + } else { + _isProgrammaticMove.value = false; + } + }, ), ), ); @@ -140,7 +154,7 @@ class _NavigationPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ CustomFloatingActionButton( - tooltip: "Start Mock Locaiton", + tooltip: "Start Mock Location", onPressed: () => {}, // _navigationController.startMockLocation(), icon: const Icon(Icons.play_arrow), @@ -155,9 +169,10 @@ class _NavigationPageState extends State { const SizedBox(height: 10), CustomFloatingActionButton( tooltip: "Centralizar a câmera", - onPressed: () => _navigationController - .centerCurrentLocation(), - icon: const Icon(Icons.location_searching), + onPressed: () => trackLocation(), + icon: _isTrackingLocation.value + ? const Icon(Icons.my_location) + : const Icon(Icons.location_searching), ), ], ), diff --git a/spotholes_android/lib/pages/route_page.dart b/spotholes_android/lib/pages/route_page.dart index 64262b6..7ee87f7 100644 --- a/spotholes_android/lib/pages/route_page.dart +++ b/spotholes_android/lib/pages/route_page.dart @@ -72,7 +72,7 @@ class _RoutePageState extends State { Future _loadRoute() async { await _routeController.loadRouteWithLegsAndSteps( - widget.sourceLocation, widget.destinationLocation, context); + widget.sourceLocation, widget.destinationLocation); setState(() { _isLoading = false; }); diff --git a/spotholes_android/lib/services/location_service.dart b/spotholes_android/lib/services/location_service.dart index 3724595..0ed8e7a 100644 --- a/spotholes_android/lib/services/location_service.dart +++ b/spotholes_android/lib/services/location_service.dart @@ -50,6 +50,7 @@ class LocationService { distanceFilter: 5, ); _currentLocationSignal.value = await _location.getLocation(); + //TODO Implementar Snackbar alertando que o GPS está fora. Pode ser um signal que exibe o snackbar na tela com o ícone de GPS fora _location.onLocationChanged.listen( (newLoc) { _currentLocationSignal.value = newLoc; diff --git a/spotholes_android/lib/services/navigation_service.dart b/spotholes_android/lib/services/navigation_service.dart index 389f863..12a15a3 100644 --- a/spotholes_android/lib/services/navigation_service.dart +++ b/spotholes_android/lib/services/navigation_service.dart @@ -17,39 +17,17 @@ class NavigationService { late final Signal> _routePolylineCoordinatesSignal = _routeController.routePolylineCoordinatesSignal; late final List _stepsIndexes = _routeController.stepsIndexes; - late final totalPointsOnRoute = _routePolylineCoordinatesSignal.value.length; - // late final Signal _pageController = - // _routeController.pageControllerSignal; int _discardedPointsCounter = 0; + int _countOutOfRoute = 0; + int _countRecalculatedRoute = 0; List routePointsMtk = []; Timer? _exitRouteTimer; - // final _location = Location.instance; - // Function? _dispose; - - // void _listenCurrentLocation() async { - // _location.onLocationChanged.listen( - // (newLoc) { - // _locationService.loadCurrentLocationMark( - // _routeController.markersSignal, - // _routeController.customInfoWindowControllerSignal, - // ); - // if (newLoc.accuracy != null && newLoc.accuracy! < 30.0) { - // _updateNavigationCamera(locationToLatLng(newLoc), newLoc.heading!); - // _updateRouteStatus(locationToMtkLatLng(newLoc)); - // } else { - // //TODO snackbar alertando que o GPS está fora. Pode ser um signal que exibe o snackbar na tela com o ícone de GPS fora - // } - // }, - // ); - // } - void startNavigation() { routePointsMtk = convertGmapsToMtkList(_routePolylineCoordinatesSignal.value); - // _listenCurrentLocation(); } void stopNavigation() { @@ -91,9 +69,8 @@ class NavigationService { } void updateRouteStatus(LatLng currentLocation) { - // TODO Criar verificação de destino alcançado, p.ex. se está a x metros do ponto final para limpar a rota // TODO Verificar se há outras situações de fim da rota - // Verifica se a rota está vazia, p.ex trajeto concluído. + // Verifica se a rota está vazia, por exemplo: trajeto concluído. if (_routeController.routeStepsLatLng.value.isEmpty) { return; } @@ -104,24 +81,14 @@ class NavigationService { debugPrint('----index on polyline: $index'); // Se a localização atual está na rota if (index > 0) { + _countOutOfRoute = 0; // Define a posição inicial para a localização atual routePointsMtk[index] = currentLocationMtk; // Remove os pontos iniciais até a posição atual routePointsMtk.removeRange(0, index + 1); _routePolylineCoordinatesSignal.value.removeRange(0, index + 1); - // Modifica a polyline da rota - _routeController.polylinesSignal.value['route'] = Polyline( - polylineId: const PolylineId("route"), - points: _routePolylineCoordinatesSignal.value, - width: 6, - color: primaryColor, - geodesic: true, - jointType: JointType.round, - ); - // Força o update da polyline da rota - _routeController.polylinesSignal.value = { - ..._routeController.polylinesSignal.value - }; + _routePolylineCoordinatesSignal.value[0] = currentLocation; + _routeController.updateRoutePolyline(); // Atualiza o contador de pontos descartados _discardedPointsCounter += index + 1; debugPrint('Pontos a descartar $_discardedPointsCounter'); @@ -129,6 +96,9 @@ class NavigationService { int currentStepIndex = verifyStep(); // Se o step avançou, o pageview é atualizado if (currentStepIndex > 0 && _pageController.hasClients) { + // Atualiza a polyline que representa a seta de manobra no mapa. Precisa ser feito antes de remover os steps + _routeController.plotManeuverPolyline(currentStepIndex, + updateCamera: false); // Remove steps que já passaram e faz update do signal _stepsIndexes.removeRange(0, currentStepIndex); _routeController.routeStepsLatLng.value @@ -136,8 +106,6 @@ class NavigationService { _routeController.routeStepsLatLng.value = [ ..._routeController.routeStepsLatLng.value ]; - // Atualiza a polyline que representa a seta de manobra no mapa - _routeController.plotManeuverPolyline(0, updateCamera: false); debugPrint('---pages: ${_pageController.page} $currentStepIndex'); // Verifica se está na pageView correspondente ao step atual, senão atualiza if (_pageController.page != currentStepIndex) { @@ -145,16 +113,15 @@ class NavigationService { _pageController.jumpToPage(currentStepIndex); } } - // Caso esteja entre a posição 0 e 1 da polyline, então a polyline é atualizada + // Caso esteja entre a posição 0 e 1 da polyline (index == 0), então a polyline é atualizada } else if (index == 0) { + _countOutOfRoute = 0; _routePolylineCoordinatesSignal.value[0] = currentLocation; - // Força o update da polyline da rota - _routeController.polylinesSignal.value = { - ..._routeController.polylinesSignal.value - }; + _routeController.updateRoutePolyline(); // Caso seja o último step e tenha menos que 3 pontos, então descarta o último step, pontos e conclui a rota } else if (_routeController.routeStepsLatLng.value.length == 1 && _routePolylineCoordinatesSignal.value.length < 3) { + _countOutOfRoute = 0; debugPrint('---Cheguei no final'); _stepsIndexes.clear(); _routeController.routeStepsLatLng.value.clear(); @@ -165,7 +132,16 @@ class NavigationService { ..._routeController.routeStepsLatLng.value ]; } else { - //TODO Criar ação para quando index = -1, ou seja, fora da rota. Inclusive com a possibildade de recálculo da rota; + _countOutOfRoute += 1; + // Recalcula a rota após 5 movimentos consecutivos fora da rota (considerando a margem de tolerâcia em metros) + // Apenas recalcula a rota 3 vezes de forma automática, evitando falhas que gerem muitos recálculos + // TODO Criar Snackbar para avisar que ultrapassou o limite de 3 vezes, perguntando se quer recalcular de forma manual, caso sim, mais 3 automáticos + // TODO Incluir possibilidade de recálculo manual, porém com limite de tempo para evitar uso indevido + if (_countOutOfRoute > 5 && _countRecalculatedRoute <= 3) { + _countOutOfRoute = 0; + recalculateRoute(currentLocation); + _countRecalculatedRoute += 1; + } } debugPrint( '----[Após descarte] points ${_routePolylineCoordinatesSignal.value.length} steps:${_routeController.routeStepsLatLng.value.length}'); @@ -174,9 +150,8 @@ class NavigationService { void recalculateRoute(LatLng currentPosition) { // Função mock, substitua com a lógica real para recalcular a rota usando a API de Directions do Google Maps // vou precisar do contexto atualizado para fazer isso! + LatLng destination = _routePolylineCoordinatesSignal.value.last; + _routeController.clearManeuverPolyline(); + _routeController.loadRouteWithLegsAndSteps(currentPosition, destination); } - - // dispose() { - // _dispose!(); - // } } diff --git a/spotholes_android/lib/services/spothole_service.dart b/spotholes_android/lib/services/spothole_service.dart index f9081fa..c21be6d 100644 --- a/spotholes_android/lib/services/spothole_service.dart +++ b/spotholes_android/lib/services/spothole_service.dart @@ -5,6 +5,7 @@ import 'package:signals/signals_flutter.dart'; import 'package:spotholes_android/controllers/spothole_info_window_controller.dart'; import 'package:spotholes_android/services/service_locator.dart'; +import '../main.dart'; import '../models/spothole.dart'; import '../package/custom_info_window.dart'; import '../utilities/point_on_route_haversine.dart'; @@ -22,7 +23,7 @@ class SpotholeService { SpotholeService(this._markersSignal, this._customInfoWindowControllerSignal); - void loadSpotholeMarkers(context) { + void loadSpotholeMarkers() { databaseReference.child('spotholes').once().then( (DatabaseEvent event) { final spotholesMap = event.snapshot.value as Map?; @@ -33,7 +34,6 @@ class SpotholeService { Spothole.fromJson(Map.from(value as Map)); spothole.id = key; _spotholeInfoWindowController.addSpotholeMarker( - context, spothole, ); }, @@ -44,8 +44,7 @@ class SpotholeService { ); } - List loadSpotholesInRoute(context, routePolylineCoordinates) { - List spotholesInRouteList = []; + void loadSpotholesInRoute(routePolylineCoordinates, spotholesInRouteList) { databaseReference.child('spotholes').once().then( (DatabaseEvent event) { final spotholesMap = event.snapshot.value as Map?; @@ -57,21 +56,24 @@ class SpotholeService { spothole.id = entry.key; return spothole; }).toList(); - spotholesInRouteList = checkPointsAndStoreAccumulatedDistances( + spotholesInRouteList.value = checkPointsAndStoreAccumulatedDistances( spotholeList, routePolylineCoordinates, ); - for (Spothole spothole in spotholesInRouteList) { - _spotholeInfoWindowController.addSpotholeMarker(context, spothole); - } - _markersSignal.value = {..._markersSignal.value}; + addSpotholeMarkers(spotholesInRouteList); } }, ); - return spotholesInRouteList; } - void registerSpothole(context, LatLng position, category, type) { + void addSpotholeMarkers(spotholesInRouteList) { + for (Spothole spothole in spotholesInRouteList.value) { + _spotholeInfoWindowController.addSpotholeMarker(spothole); + } + _markersSignal.value = {..._markersSignal.value}; + } + + void registerSpothole(LatLng position, category, type) { final newSpotHoleRef = dataBaseSpotholesRef.push(); final newSpothole = Spothole( DateTime.now().toUtc(), @@ -83,19 +85,20 @@ class SpotholeService { newSpotHoleRef.key, ); newSpotHoleRef.set(newSpothole.toJson()); - _spotholeInfoWindowController.addSpotholeMarker(context, newSpothole); + _spotholeInfoWindowController.addSpotholeMarker(newSpothole); _markersSignal.value = {..._markersSignal.value}; } - void registerSpotholeModal(context, position) { + void registerSpotholeModal(position) { + final context = MyApp.navigatorKey.currentContext; showModalBottomSheet( - context: context, + context: context!, builder: (builder) { return RegisterSpotholeModal( title: "Para alertar um risco, selecione:", textOnRegisterButton: "Adicionar", onRegister: (riskCategory, type) => - registerSpothole(context, position, riskCategory, type), + registerSpothole(position, riskCategory, type), ); }, ); diff --git a/spotholes_android/lib/utilities/constants.dart b/spotholes_android/lib/utilities/constants.dart index cf06394..d5825ba 100644 --- a/spotholes_android/lib/utilities/constants.dart +++ b/spotholes_android/lib/utilities/constants.dart @@ -6,6 +6,13 @@ const double defaultPadding = 16.0; //Google Maps const double defaultZoomMap = 18.5; const double defaultNavigationTilt = 90; +const String mapStyle2D = '''[ + { + "featureType": "landscape.man_made", + "elementType": "geometry", + "stylers": [{"visibility": "off"}] + } + ]'''; // Navigation -const double routeDeviationTolerance = 12; \ No newline at end of file +const double routeDeviationTolerance = 12; diff --git a/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart b/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart index a9cade0..2092058 100644 --- a/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart +++ b/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:spotholes_android/controllers/base_map_controller.dart'; import '../../utilities/custom_icons.dart'; @@ -7,6 +8,11 @@ import 'custom_floating_action_button.dart'; class CustomFloatingActionButtonList extends StatelessWidget { CustomFloatingActionButtonList({super.key}); final _baseMapController = BaseMapController.instance; + late final _isTrackingLocation = _baseMapController.isTrackingLocation; + + void trackingLocation() { + _baseMapController.trackLocation(); + } @override Widget build(BuildContext context) { @@ -21,20 +27,22 @@ class CustomFloatingActionButtonList extends StatelessWidget { children: [ CustomFloatingActionButton( tooltip: "Adicionar um risco", - onPressed: () => - _baseMapController.registerSpotholeModal(context), + onPressed: () => _baseMapController.registerSpotholeModal(), icon: CustomIcons.potholeAddIcon), const SizedBox(height: 10), CustomFloatingActionButton( tooltip: "Sincronizar os riscos", - onPressed: () => - _baseMapController.loadSpotholeMarkers(context), + onPressed: () => _baseMapController.loadSpotholeMarkers(), icon: const Icon(Icons.sync)), const SizedBox(height: 10), - CustomFloatingActionButton( - tooltip: "Centralizar a câmera", - onPressed: _baseMapController.centerView, - icon: const Icon(Icons.location_searching), + Watch( + (_) => CustomFloatingActionButton( + tooltip: "Centralizar a câmera", + onPressed: () => trackingLocation(), + icon: _isTrackingLocation.value + ? const Icon(Icons.my_location) + : const Icon(Icons.location_searching), + ), ), ], ), diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart index dcee7df..6f62068 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart @@ -50,7 +50,7 @@ class MainDraggableSheetState extends State { ), CustomButton( label: 'Alertar', - onPressed: () => _baseMapController.registerSpotholeModal(context), + onPressed: () => _baseMapController.registerSpotholeModal(), ), ]; } diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart index 2965dc2..9dd3f78 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart @@ -50,8 +50,8 @@ class PlaceDraggableSheetState extends State { ), CustomButton( label: 'Alertar', - onPressed: () => _baseMapController.registerSpotholeModal(context, - position: position), + onPressed: () => + _baseMapController.registerSpotholeModal(position: position), ), ]; } diff --git a/spotholes_android/lib/widgets/nav_route_steps_page_view.dart b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart index 3f703c1..f6a521d 100644 --- a/spotholes_android/lib/widgets/nav_route_steps_page_view.dart +++ b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart @@ -23,10 +23,10 @@ class NavRouteStepsPageView extends StatefulWidget { }); @override - RouteStepsStatePageView createState() => RouteStepsStatePageView(); + NavRouteStepsStatePageView createState() => NavRouteStepsStatePageView(); } -class RouteStepsStatePageView extends State { +class NavRouteStepsStatePageView extends State { late final _routeController = widget.routeController; late final _pageController = widget.pageController; int _currentPage = 0; @@ -41,6 +41,7 @@ class RouteStepsStatePageView extends State { WidgetsBinding.instance.addPostFrameCallback((_) { _animateOnce(); }); + _pageController.addListener( () { int newPage = _pageController.page!.round(); From a0062aa184de218118260475f13a027d4089210f Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:15:42 -0300 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20Implementa=20bot=C3=B5es=20na=20t?= =?UTF-8?q?ela=20de=20navega=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mais Informações: - Implementa a lógica de registro de riscos na tela de navegação por meio de um botão; - Implementa o botão de recálculo da rota na tela de navegação e refatora a lógica de recálculo da rota; - Refatora o tootip dos botões de rastreio da câmera. --- .../lib/controllers/base_map_controller.dart | 4 ++-- .../controllers/navigation_controller.dart | 21 +++++++++--------- .../lib/controllers/route_controller.dart | 10 +++++++++ .../lib/pages/navigation_page.dart | 22 ++++++++++--------- .../lib/services/navigation_service.dart | 10 +-------- .../custom_floating_action_button_list.dart | 4 +++- 6 files changed, 38 insertions(+), 33 deletions(-) diff --git a/spotholes_android/lib/controllers/base_map_controller.dart b/spotholes_android/lib/controllers/base_map_controller.dart index 6a3d442..cf22a74 100644 --- a/spotholes_android/lib/controllers/base_map_controller.dart +++ b/spotholes_android/lib/controllers/base_map_controller.dart @@ -162,8 +162,8 @@ class BaseMapController { } void registerSpotholeModal({LatLng? position}) { - final latLng = position ?? currentLocationLatLng; - spotholeService.registerSpotholeModal(latLng); + final registerPosition = position ?? currentLocationLatLng; + spotholeService.registerSpotholeModal(registerPosition); } void onLongPress(BuildContext context, LatLng position) async { diff --git a/spotholes_android/lib/controllers/navigation_controller.dart b/spotholes_android/lib/controllers/navigation_controller.dart index ef35ae1..b68c8a5 100644 --- a/spotholes_android/lib/controllers/navigation_controller.dart +++ b/spotholes_android/lib/controllers/navigation_controller.dart @@ -37,17 +37,6 @@ class NavigationController { } } - /*Tentativa de Mock do serviço de localização - final _locationService = LocationServiceMock.create(); - late final Signal _currentLocationSignal = - _locationService.currentLocationSignal; - void startMockLocation(){ - _locationService.startMockLocationMonitoring(); - } - void stopMockLocation(){ - _locationService.stopMockLocationMonitoring(); - } */ - void onMapCreated(mapController) { mapController.setMapStyle(mapStyle2D); _routeController.onMapCreated(mapController); @@ -97,6 +86,16 @@ class NavigationController { _routeController.updateCameraLatLng(_locationService.currentLocationLatLng); } + void registerSpotholeModal({LatLng? position}) { + LatLng registerPosition = + position ?? _locationService.currentLocationLatLng; + _routeController.registerSpotholeModal(registerPosition); + } + + void recalculateRoute() { + _routeController.recalculateRoute(_locationService.currentLocationLatLng); + } + static dispose() { // _navigationService.dispose(); _dispose!(); diff --git a/spotholes_android/lib/controllers/route_controller.dart b/spotholes_android/lib/controllers/route_controller.dart index 3c76959..bfa76d2 100644 --- a/spotholes_android/lib/controllers/route_controller.dart +++ b/spotholes_android/lib/controllers/route_controller.dart @@ -166,6 +166,12 @@ class RouteController { return RoutePointsData(routePoints, stepsIndexes); } + void recalculateRoute(LatLng currentLocation) { + LatLng destination = _routePolylineCoordinatesSignal.value.last; + clearManeuverPolyline(); + loadRouteWithLegsAndSteps(currentLocation, destination); + } + Future loadRouteWithLegsAndSteps( LatLng sourceLocation, LatLng destinationLocation) async { DirectionsService.init(EnvironmentConfig.googleApiKey!); @@ -337,6 +343,10 @@ class RouteController { polylinesSignal.value = {...polylinesSignal.value}; } + void registerSpotholeModal(LatLng registerPosition) { + _spotholeService.registerSpotholeModal(registerPosition); + } + RouteController isolatedCopy() { var copy = RouteController._(); copy._spotholesInRouteList.value = List.from(_spotholesInRouteList.value); diff --git a/spotholes_android/lib/pages/navigation_page.dart b/spotholes_android/lib/pages/navigation_page.dart index 1cc1ba0..5f574ba 100644 --- a/spotholes_android/lib/pages/navigation_page.dart +++ b/spotholes_android/lib/pages/navigation_page.dart @@ -7,6 +7,7 @@ import '../controllers/navigation_controller.dart'; import '../controllers/route_controller.dart'; import '../package/custom_info_window.dart'; import '../utilities/constants.dart'; +import '../utilities/custom_icons.dart'; import '../widgets/button/custom_floating_action_button.dart'; import '../widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart'; import '../widgets/nav_route_steps_page_view.dart'; @@ -154,21 +155,22 @@ class _NavigationPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ CustomFloatingActionButton( - tooltip: "Start Mock Location", - onPressed: () => {}, - // _navigationController.startMockLocation(), - icon: const Icon(Icons.play_arrow), - ), + tooltip: "Adicionar um risco", + onPressed: () => _navigationController + .registerSpotholeModal(), + icon: CustomIcons.potholeAddIcon), const SizedBox(height: 10), CustomFloatingActionButton( - tooltip: "Stop Mock Location", - onPressed: () => {}, - // _navigationController.stopMockLocation(), - icon: const Icon(Icons.pause), + tooltip: "Atualizar rota e marcadores", + onPressed: () => + _navigationController.recalculateRoute(), + icon: const Icon(Icons.autorenew), ), const SizedBox(height: 10), CustomFloatingActionButton( - tooltip: "Centralizar a câmera", + tooltip: _isTrackingLocation.value + ? "Desativar centralização de câmera" + : "Ativar centralização de câmera", onPressed: () => trackLocation(), icon: _isTrackingLocation.value ? const Icon(Icons.my_location) diff --git a/spotholes_android/lib/services/navigation_service.dart b/spotholes_android/lib/services/navigation_service.dart index 12a15a3..545d2d5 100644 --- a/spotholes_android/lib/services/navigation_service.dart +++ b/spotholes_android/lib/services/navigation_service.dart @@ -139,19 +139,11 @@ class NavigationService { // TODO Incluir possibilidade de recálculo manual, porém com limite de tempo para evitar uso indevido if (_countOutOfRoute > 5 && _countRecalculatedRoute <= 3) { _countOutOfRoute = 0; - recalculateRoute(currentLocation); + _routeController.recalculateRoute(currentLocation); _countRecalculatedRoute += 1; } } debugPrint( '----[Após descarte] points ${_routePolylineCoordinatesSignal.value.length} steps:${_routeController.routeStepsLatLng.value.length}'); } - - void recalculateRoute(LatLng currentPosition) { - // Função mock, substitua com a lógica real para recalcular a rota usando a API de Directions do Google Maps - // vou precisar do contexto atualizado para fazer isso! - LatLng destination = _routePolylineCoordinatesSignal.value.last; - _routeController.clearManeuverPolyline(); - _routeController.loadRouteWithLegsAndSteps(currentPosition, destination); - } } diff --git a/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart b/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart index 2092058..ca5daea 100644 --- a/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart +++ b/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart @@ -37,7 +37,9 @@ class CustomFloatingActionButtonList extends StatelessWidget { const SizedBox(height: 10), Watch( (_) => CustomFloatingActionButton( - tooltip: "Centralizar a câmera", + tooltip: _isTrackingLocation.value + ? "Desativar centralização de câmera" + : "Ativar centralização de câmera", onPressed: () => trackingLocation(), icon: _isTrackingLocation.value ? const Icon(Icons.my_location) From 6060b0e4b6092958095baa21cdaf3d0b75321762 Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Wed, 25 Dec 2024 00:13:16 -0300 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20Implementa=20melhorias=20na=20l?= =?UTF-8?q?=C3=B3gica=20de=20navega=C3=A7=C3=A3o=20e=20faz=20corre=C3=A7?= =?UTF-8?q?=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Funcionalidades: - Implementa a ação de movimento da câmera para o step (manobra) atual, baseada na posição do dispositivo ao clicar no botão de rastreio. - Implementa a ação de clique sobre os steps para centralizar a câmera da manobra no step clicado. - Implementa a possibilidade de acionar o movimento do pageView sem afetar a câmera do mapa. Sendo essencial pois a lógica atual de navegação se baseia na remoção dos steps ao progredir na rota e isso gerava o movimento no pageView e por consequência resultava em movimentos de câmera indesejados, em alguns casos. Coreções de bug: - Corrige a lógica de exibição do plot da polilyne de manobra durante a navegação quando há rastreio. - Corrige a atualização dos marcadores da rota ao recalcular a rota. Agora exibe instantaneamente a localização atual ao recalcular a rota, independentemente de haver atualizações na localização. - Corrige o plot da polyline nas setas que representam manobras de curva quando a manobra é o step atual. Antes parte da seta estava apontando para a origem da rota e ficando, por consequência, para fora da rota. Foi necessário criar uma lista auxiliar para os steps, pois durante a navegação os steps percorridos na rota são eliminados e o plot correto do step atual depende o step anterior. - Corrige a navegação sem rastreio de modo a evitar o movimento da câmera ao avançar entre os steps em razão do progresso da navegação na rota. Antes a câmera voltava para o step atual ao atualizá-lo. --- .../controllers/navigation_controller.dart | 25 ++++++++-- .../lib/controllers/route_controller.dart | 49 +++++++++++-------- .../lib/pages/navigation_page.dart | 9 ++-- .../lib/services/navigation_service.dart | 42 ++++++++++------ .../widgets/nav_route_steps_page_view.dart | 29 ++++++++--- 5 files changed, 104 insertions(+), 50 deletions(-) diff --git a/spotholes_android/lib/controllers/navigation_controller.dart b/spotholes_android/lib/controllers/navigation_controller.dart index b68c8a5..8841b21 100644 --- a/spotholes_android/lib/controllers/navigation_controller.dart +++ b/spotholes_android/lib/controllers/navigation_controller.dart @@ -21,18 +21,28 @@ class NavigationController { final _pageController = PageController(initialPage: 1); get pageController => _pageController; - late final _navigationService = - NavigationService(_routeController, _pageController); - get navigationService => _navigationService; - final isTrackingLocation = signal(true); final isProgrammaticMove = signal(true); + final isPageViewMoveCamera = signal(true); + + late final _navigationService = NavigationService(_routeController, + _pageController, isTrackingLocation, isPageViewMoveCamera); + get navigationService => _navigationService; - void trackLocation() { + void goToCurrentStepPageView() { + if (_pageController.page != 1) { + isPageViewMoveCamera.value = false; + _pageController.jumpToPage(1); + } + } + + void onTrackLocation() { + // Alterna a ação entre rastrear e não rastrear if (isTrackingLocation.value) { isTrackingLocation.value = false; } else { isTrackingLocation.value = true; + goToCurrentStepPageView(); centerCurrentLocation(); } } @@ -92,8 +102,13 @@ class NavigationController { _routeController.registerSpotholeModal(registerPosition); } + // TODO Limitar o número de requisições por minuto para evitar uso indevido, talvez um debaunce void recalculateRoute() { _routeController.recalculateRoute(_locationService.currentLocationLatLng); + _locationService.loadCurrentLocationMark( + _routeController.markersSignal, + _routeController.customInfoWindowControllerSignal, + ); } static dispose() { diff --git a/spotholes_android/lib/controllers/route_controller.dart b/spotholes_android/lib/controllers/route_controller.dart index bfa76d2..1a9ff78 100644 --- a/spotholes_android/lib/controllers/route_controller.dart +++ b/spotholes_android/lib/controllers/route_controller.dart @@ -62,6 +62,7 @@ class RouteController { final _directionResult = Signal(const DirectionsResult()); final Signal> _routeStepsLatLng = Signal>([]); Signal> get routeStepsLatLng => _routeStepsLatLng; + final Signal> _auxRouteStepsLatLng = Signal>([]); // Store steps' start indexes inside a route polyline List _stepsIndexes = []; @@ -128,9 +129,9 @@ class RouteController { newCameraLatLngBounds(_routePolylineCoordinatesSignal.value); } - void newCameraLatLngBoundsFromStep(Step step) { - final startLocationLatLng = geoCoordToLatLng(step.startLocation!); - final endLocationLatLng = geoCoordToLatLng(step.endLocation!); + void newCameraLatLngBoundsFromStep(Step currentStep) { + final startLocationLatLng = geoCoordToLatLng(currentStep.startLocation!); + final endLocationLatLng = geoCoordToLatLng(currentStep.endLocation!); newCameraLatLngBounds([startLocationLatLng, endLocationLatLng]); } @@ -156,8 +157,8 @@ class RouteController { List stepPoints = []; List stepsIndexes = []; int currentIndex = 0; - for (var step in steps) { - var polyline = step.polyline!.points; + for (var currentStep in steps) { + var polyline = currentStep.polyline!.points; stepPoints = decodePolyline(polyline!); routePoints.addAll(stepPoints); stepsIndexes.add(currentIndex); @@ -168,6 +169,7 @@ class RouteController { void recalculateRoute(LatLng currentLocation) { LatLng destination = _routePolylineCoordinatesSignal.value.last; + _markersSignal.value.clear(); clearManeuverPolyline(); loadRouteWithLegsAndSteps(currentLocation, destination); } @@ -191,6 +193,7 @@ class RouteController { if (status == DirectionsStatus.ok) { _directionResult.value = response; _routeStepsLatLng.value = response.routes!.first.legs!.first.steps!; + _auxRouteStepsLatLng.value = [..._routeStepsLatLng.value]; final routePointsData = decodePointsFromSteps(_routeStepsLatLng.value); _routePolylineCoordinatesSignal.value = routePointsData.points; @@ -264,18 +267,18 @@ class RouteController { void plotManeuverPolyline(int stepIndex, {bool updateCamera = true}) { final steps = _routeStepsLatLng.value; - final step = steps[stepIndex]; - final maneuver = step.maneuver ?? 'straight'; + final currentStep = steps[stepIndex]; + final maneuver = currentStep.maneuver ?? 'straight'; List maneuverPoints = []; List arrowPoints = []; int? midpointIndex; final sourceLocation = markersSignal.value['sourceRouteMarker']!.position; - maneuverPoints = decodePolyline(step.polyline!.points!); + maneuverPoints = decodePolyline(currentStep.polyline!.points!); if (maneuver == 'straight') { - if (updateCamera) newCameraLatLngBoundsFromStep(step); + if (updateCamera) newCameraLatLngBoundsFromStep(currentStep); polylinesSignal.value.addAll({ 'straightPath': Polyline( polylineId: const PolylineId('straightPath'), @@ -288,13 +291,16 @@ class RouteController { ) }); } else { - if (updateCamera) updateCameraGeoCoord(step.startLocation!); - if (stepIndex == 0) { + if (updateCamera) updateCameraGeoCoord(currentStep.startLocation!); + final previousStepIndex = + _auxRouteStepsLatLng.value.indexOf(currentStep) - 1; + if (previousStepIndex == 0) { maneuverPoints = [sourceLocation, ...maneuverPoints]; midpointIndex = 1; } else { + final previousStep = _auxRouteStepsLatLng.value[previousStepIndex]; final beforeManeuverPoints = - decodePolyline(steps[stepIndex - 1].polyline!.points!); + decodePolyline(previousStep.polyline!.points!); midpointIndex = beforeManeuverPoints.length; maneuverPoints.insertAll(0, beforeManeuverPoints); } @@ -373,6 +379,7 @@ class RouteController { copy._directionResult.value = _directionResult.value; copy._stepsIndexes = _stepsIndexes; copy._routeStepsLatLng.value = _routeStepsLatLng.value; + copy._auxRouteStepsLatLng.value = _auxRouteStepsLatLng.value; // Removi todos os casos de valores que são inicializados nulo no RouteController, assim eles são reinicializados e não retorna erro por duplicidade, como no caso de widgets que precisam de controladores únicos // copy._pageControllerSignal.value = PageController(initialPage: 1); // copy._routeAndStepsListSignal.value = @@ -423,15 +430,15 @@ class RouteController { // strSteps.add(strRoute); // //STEPS // List steps = response.routes![0].legs![0].steps!; - // for (Step step in steps) { - // strSteps.add('Distância: ${step.distance}\n' - // 'Duração: ${step.duration!.text}\n' - // 'Start Location: ${geoCoordToString(step.startLocation!)}\n' - // 'End Location: ${geoCoordToString(step.endLocation!)}\n' - // 'Instructions: ${step.instructions}\n' - // 'Maneuver: ${step.maneuver}\n' - // 'Transit: ${step.transit}\n' - // 'Travel Mode: ${step.travelMode}\n'); + // for (Step currentStep in steps) { + // strSteps.add('Distância: ${currentStep.distance}\n' + // 'Duração: ${currentStep.duration!.text}\n' + // 'Start Location: ${geoCoordToString(currentStep.startLocation!)}\n' + // 'End Location: ${geoCoordToString(currentStep.endLocation!)}\n' + // 'Instructions: ${currentStep.instructions}\n' + // 'Maneuver: ${currentStep.maneuver}\n' + // 'Transit: ${currentStep.transit}\n' + // 'Travel Mode: ${currentStep.travelMode}\n'); // } // //Update da string de ROTA e STEPS // _routeAndStepsListSignal.value = strSteps; diff --git a/spotholes_android/lib/pages/navigation_page.dart b/spotholes_android/lib/pages/navigation_page.dart index 5f574ba..024f65a 100644 --- a/spotholes_android/lib/pages/navigation_page.dart +++ b/spotholes_android/lib/pages/navigation_page.dart @@ -46,9 +46,11 @@ class _NavigationPageState extends State { late final _isTrackingLocation = _navigationController.isTrackingLocation; late final _isProgrammaticMove = _navigationController.isProgrammaticMove; + late final _isPageViewUpdateCamera = + _navigationController.isPageViewMoveCamera; - void trackLocation() { - _navigationController.trackLocation(); + void onTrackLocation() { + _navigationController.onTrackLocation(); } @override @@ -90,6 +92,7 @@ class _NavigationPageState extends State { routeController: _routeController, pageController: _navigationController.pageController, route: _routeSignal, + isPageViewUpdateCamera: _isPageViewUpdateCamera, ), ), Expanded( @@ -171,7 +174,7 @@ class _NavigationPageState extends State { tooltip: _isTrackingLocation.value ? "Desativar centralização de câmera" : "Ativar centralização de câmera", - onPressed: () => trackLocation(), + onPressed: () => onTrackLocation(), icon: _isTrackingLocation.value ? const Icon(Icons.my_location) : const Icon(Icons.location_searching), diff --git a/spotholes_android/lib/services/navigation_service.dart b/spotholes_android/lib/services/navigation_service.dart index 545d2d5..109425e 100644 --- a/spotholes_android/lib/services/navigation_service.dart +++ b/spotholes_android/lib/services/navigation_service.dart @@ -11,8 +11,15 @@ import '../utilities/constants.dart'; class NavigationService { final RouteController _routeController; final PageController _pageController; + final Signal _isTrackingLocation; + final Signal _isPageViewUpdateCamera; - NavigationService(this._routeController, this._pageController); + NavigationService( + this._routeController, + this._pageController, + this._isTrackingLocation, + this._isPageViewUpdateCamera, + ); late final Signal> _routePolylineCoordinatesSignal = _routeController.routePolylineCoordinatesSignal; @@ -68,7 +75,7 @@ class NavigationService { return -1; } - void updateRouteStatus(LatLng currentLocation) { + void updateRouteStatus(LatLng currentLocation) async { // TODO Verificar se há outras situações de fim da rota // Verifica se a rota está vazia, por exemplo: trajeto concluído. if (_routeController.routeStepsLatLng.value.isEmpty) { @@ -97,21 +104,29 @@ class NavigationService { // Se o step avançou, o pageview é atualizado if (currentStepIndex > 0 && _pageController.hasClients) { // Atualiza a polyline que representa a seta de manobra no mapa. Precisa ser feito antes de remover os steps - _routeController.plotManeuverPolyline(currentStepIndex, - updateCamera: false); - // Remove steps que já passaram e faz update do signal + if (_isTrackingLocation.value || _pageController.page == 1) { + _routeController.plotManeuverPolyline(currentStepIndex, + updateCamera: false); + } + // Remove os steps que já passaram _stepsIndexes.removeRange(0, currentStepIndex); _routeController.routeStepsLatLng.value .removeRange(0, currentStepIndex); - _routeController.routeStepsLatLng.value = [ - ..._routeController.routeStepsLatLng.value - ]; - debugPrint('---pages: ${_pageController.page} $currentStepIndex'); - // Verifica se está na pageView correspondente ao step atual, senão atualiza - if (_pageController.page != currentStepIndex) { - // TODO Criar uma segunda condição, talvez um boolean para chavear entre monitorar automaticamente ou com base na ação do usuário, incluindo movimento de câmera, pageview, etc. - _pageController.jumpToPage(currentStepIndex); + + final stepsLength = _routeController.routeStepsLatLng.value.length; + final totalRemovedSteps = currentStepIndex; + final page = _pageController.page!.toInt(); + // Verifica se o movimento de retorno do pageView termina no máximo na página 01 (step 0), se a página atual está entre a página 01 e a penúltima página (dentro da lista de steps) + if (totalRemovedSteps < page && page > 1 && page < stepsLength + 2) { + _isPageViewUpdateCamera.value = false; + _pageController.jumpToPage(page - totalRemovedSteps); + } else { + // É necessário atualizar para forçar a renderização do widget, porém assim evita duplicação do update porque o jumpToPage invoca um método que faz update da lista + _routeController.routeStepsLatLng.value = [ + ..._routeController.routeStepsLatLng.value + ]; } + debugPrint('---pages: ${_pageController.page} $currentStepIndex'); } // Caso esteja entre a posição 0 e 1 da polyline (index == 0), então a polyline é atualizada } else if (index == 0) { @@ -136,7 +151,6 @@ class NavigationService { // Recalcula a rota após 5 movimentos consecutivos fora da rota (considerando a margem de tolerâcia em metros) // Apenas recalcula a rota 3 vezes de forma automática, evitando falhas que gerem muitos recálculos // TODO Criar Snackbar para avisar que ultrapassou o limite de 3 vezes, perguntando se quer recalcular de forma manual, caso sim, mais 3 automáticos - // TODO Incluir possibilidade de recálculo manual, porém com limite de tempo para evitar uso indevido if (_countOutOfRoute > 5 && _countRecalculatedRoute <= 3) { _countOutOfRoute = 0; _routeController.recalculateRoute(currentLocation); diff --git a/spotholes_android/lib/widgets/nav_route_steps_page_view.dart b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart index f6a521d..a27dbb6 100644 --- a/spotholes_android/lib/widgets/nav_route_steps_page_view.dart +++ b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart @@ -14,12 +14,14 @@ class NavRouteStepsPageView extends StatefulWidget { final RouteController routeController; final PageController pageController; final Signal route; + final Signal isPageViewUpdateCamera; const NavRouteStepsPageView({ super.key, required this.routeController, required this.pageController, required this.route, + required this.isPageViewUpdateCamera, }); @override @@ -29,6 +31,8 @@ class NavRouteStepsPageView extends StatefulWidget { class NavRouteStepsStatePageView extends State { late final _routeController = widget.routeController; late final _pageController = widget.pageController; + late final _isPageViewUpdateCamera = widget.isPageViewUpdateCamera; + int _currentPage = 0; late final _route = widget.route; @@ -54,7 +58,15 @@ class NavRouteStepsStatePageView extends State { _routeController.updateCameraGeoCoord(_leg.value.endLocation!); } else { final stepIndex = _currentPage - 1; - _routeController.plotManeuverPolyline(stepIndex); + if (_isPageViewUpdateCamera.value) { + _routeController.plotManeuverPolyline(stepIndex, + updateCamera: true); + } else { + // Caso seja uma execução que não precise atualizar a câmera, volta para o estado padrão + _isPageViewUpdateCamera.value = true; + _routeController.plotManeuverPolyline(stepIndex, + updateCamera: false); + } } } }, @@ -119,7 +131,8 @@ class NavRouteStepsStatePageView extends State { child: CustomIcons.sourceIconAsset, ), onTap: () => { - // _routeController.updateCameraGeoCoord(_leg.startLocation!), + _routeController + .updateCameraGeoCoord(_leg.value.startLocation!), }, ), ), @@ -136,7 +149,8 @@ class NavRouteStepsStatePageView extends State { padding: const EdgeInsets.symmetric(vertical: 6), child: ListTile( onTap: () => { - // _routeController.updateCameraGeoCoord(_leg.endLocation!), + _routeController + .updateCameraGeoCoord(_leg.value.endLocation!), }, title: Text( 'Destino: ${_leg.value.endAddress!}', @@ -164,10 +178,11 @@ class NavRouteStepsStatePageView extends State { padding: const EdgeInsets.symmetric(vertical: 6), child: ListTile( onTap: () => { - // maneuver == 'straight' - // ? _routeController.newCameraLatLngBoundsFromStep(step) - // : _routeController - // .updateCameraGeoCoord(step.startLocation!), + maneuver == 'straight' + ? _routeController + .newCameraLatLngBoundsFromStep(step) + : _routeController + .updateCameraGeoCoord(step.startLocation!), }, leading: Icon( icon, From ccc3b15157f584170a3b6273891d6573d3dd755d Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Thu, 26 Dec 2024 20:46:04 -0300 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20Implementa=20o=20RouteObserver=20?= =?UTF-8?q?e=20faz=20diversas=20altera=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Funcionalidade: - Implementa o uso de RouteObserver e RouteAware para incluir gatilhos para as ações de navegação. Foi necessário incluir ações ao retornar da página de navegação para a página de rota para garantir o correto funcionamento do customInfoWindow ao acionar os marcadores do mapa. Correções de bug: - Corrige a reatividade de diversas variáveis signal que não estavam atualizando corretamente entre as páginas de navegação e rota. - Corrige a exceção ao tentar centralizar a rota quando não houver polylines na rota. Refatorações: - Refatora as variáveis signal do RouteController para o correto funcionamento e compartilhamento entre os widgets. Foram feitos ajustes diversos nas demais classes que utilizam as variáveis. - Refatora o uso do navigator para garantir consistência na aplicação. Agora todas as ações de push e pop do navigator são feitas através do uso do contexto global armazenada na variável MyApp.navigatorKey. - Refatora outros trechos de código reduzindo a complexidade da aplicação. --- .../lib/controllers/base_map_controller.dart | 1 - .../lib/controllers/route_controller.dart | 192 +++++---------- spotholes_android/lib/main.dart | 6 + .../lib/pages/navigation_page.dart | 16 +- spotholes_android/lib/pages/route_page.dart | 60 +++-- .../lib/services/navigation_service.dart | 28 +-- .../lib/utilities/app_navigator_observer.dart | 37 +++ .../draggable_scrollable_sheet_type.dart | 5 +- .../location_draggable_sheet.dart | 23 +- .../main_draggable_sheet.dart | 4 +- .../navigation_draggable_sheet.dart | 36 +-- .../place_draggable_sheet.dart | 12 +- .../route_draggable_sheet.dart | 59 +++-- .../widgets/nav_route_steps_page_view.dart | 9 +- .../lib/widgets/route_steps_page_view.dart | 231 +++++++++--------- .../lib/widgets/spotholes_page_view.dart | 14 +- 16 files changed, 339 insertions(+), 394 deletions(-) create mode 100644 spotholes_android/lib/utilities/app_navigator_observer.dart diff --git a/spotholes_android/lib/controllers/base_map_controller.dart b/spotholes_android/lib/controllers/base_map_controller.dart index cf22a74..2117de4 100644 --- a/spotholes_android/lib/controllers/base_map_controller.dart +++ b/spotholes_android/lib/controllers/base_map_controller.dart @@ -200,7 +200,6 @@ class BaseMapController { DraggableScrollableSheetTypes.location( position: position, formattedPlacemark: formattedPlacemark, - onRegister: () => spotholeService.registerSpotholeModal(position), ), ); } diff --git a/spotholes_android/lib/controllers/route_controller.dart b/spotholes_android/lib/controllers/route_controller.dart index 1a9ff78..24f3677 100644 --- a/spotholes_android/lib/controllers/route_controller.dart +++ b/spotholes_android/lib/controllers/route_controller.dart @@ -6,7 +6,6 @@ import 'package:flutter_polyline_points/flutter_polyline_points.dart' as fpp; import 'package:google_directions_api/google_directions_api.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:signals/signals_flutter.dart'; -import 'package:spotholes_android/package/extensions/directions_result_extension.dart'; import '../config/environment_config.dart'; import '../models/spothole.dart'; @@ -26,63 +25,76 @@ class RoutePointsData { } class RouteController { - RouteController._(); - static RouteController? _instance; - static RouteController get instance => _instance ??= RouteController._(); - - static void resetInstance() { - _instance = null; - } - final databaseReference = getIt(); late final dataBaseSpotholesRef = databaseReference.child('spotholes'); - final _spotholesInRouteList = Signal>([]); final _googleMapControllerCompleter = Completer(); - get getGoogleMapController async => + Future get getGoogleMapController async => await _googleMapControllerCompleter.future; final _customInfoWindowControllerSignal = Signal(CustomInfoWindowController()); - get customInfoWindowControllerSignal => _customInfoWindowControllerSignal; - - // Store all markers - final _markersSignal = Signal>({}); + Signal get customInfoWindowControllerSignal => + _customInfoWindowControllerSignal; late final _spotholeService = SpotholeService( _markersSignal, _customInfoWindowControllerSignal, ); - // Store all polyline points - final _routePolylineCoordinatesSignal = Signal>([]); - // Store all polylines - final _polylinesSignal = Signal>({}); - // Store a result from a request for a route on Google Directions API - final _directionResult = Signal(const DirectionsResult()); - final Signal> _routeStepsLatLng = Signal>([]); - Signal> get routeStepsLatLng => _routeStepsLatLng; - final Signal> _auxRouteStepsLatLng = Signal>([]); - - // Store steps' start indexes inside a route polyline - List _stepsIndexes = []; - get stepsIndexes => _stepsIndexes; + // Store all markers + var _markersSignal = Signal>({}); + Signal> get markersSignal => _markersSignal; + // Store all spotholes in route + var _spotholesInRouteList = Signal>([]); Signal> get spotholesInRouteList => _spotholesInRouteList; - Signal> get markersSignal => _markersSignal; + + // Store all polylines + var _polylinesSignal = Signal>({}); + Signal> get polylinesSignal => _polylinesSignal; + + // Store all route polyline points + var _routePolylineCoordinatesSignal = Signal>([]); Signal> get routePolylineCoordinatesSignal => _routePolylineCoordinatesSignal; - Signal> get polylinesSignal => _polylinesSignal; + + // Store a result from a request for a route on Google Directions API + var _directionResult = Signal(const DirectionsResult()); Signal get directionResult => _directionResult; + // Common computed signal route variables + late final _route = computed(() => _directionResult.value.routes![0]); + Computed get route => _route; + late final _leg = computed(() => _route.value.legs![0]); + Computed get leg => _leg; + late final _startLocation = computed(() => _leg.value.startLocation); + Computed get startLocation => _startLocation; + late final _endLocation = computed(() => _leg.value.endLocation); + Computed get endLocation => _endLocation; + + late final _startLocationLatLng = + computed(() => geoCoordToLatLng(_startLocation.value!)); + Computed get startLocationLatLng => _startLocationLatLng; + + // This variable cannot be computed because it needs to be reactive as a List + var _routeStepsLatLng = Signal>([]); + Signal> get routeStepsLatLng => _routeStepsLatLng; + // This variable is just an independent copy of the routeStepsLatLng to mantain the original data + var _auxRouteStepsLatLng = Signal>([]); + + // Store steps' start indexes inside a route polyline + var _stepsIndexes = []; + List get stepsIndexes => _stepsIndexes; + final _showStepsPageSignal = signal(false); - get showStepsPageSignal => _showStepsPageSignal; + Signal get showStepsPageSignal => _showStepsPageSignal; final _pageViewTypeSignal = signal(''); - get pageViewTypeSignal => _pageViewTypeSignal; + Signal get pageViewTypeSignal => _pageViewTypeSignal; final _pageControllerSignal = Signal(PageController()); - get pageControllerSignal => _pageControllerSignal; + Signal get pageControllerSignal => _pageControllerSignal; void onMapCreated(mapController) { _customInfoWindowControllerSignal.value.googleMapController = mapController; @@ -126,7 +138,10 @@ class RouteController { } void centerViewRoute() { - newCameraLatLngBounds(_routePolylineCoordinatesSignal.value); + // TODO criar snackbar ou desativar o botão quando houver menos de 2 pontos na polyline da rota + if (_routePolylineCoordinatesSignal.value.length > 1) { + newCameraLatLngBounds(_routePolylineCoordinatesSignal.value); + } } void newCameraLatLngBoundsFromStep(Step currentStep) { @@ -353,111 +368,26 @@ class RouteController { _spotholeService.registerSpotholeModal(registerPosition); } - RouteController isolatedCopy() { - var copy = RouteController._(); - copy._spotholesInRouteList.value = List.from(_spotholesInRouteList.value); - copy._markersSignal.value = Map.from(_markersSignal.value); - copy._polylinesSignal.value = Map.from(_polylinesSignal.value); - copy._routePolylineCoordinatesSignal.value = - List.from(_routePolylineCoordinatesSignal.value); - // Dica: Em tipos complexos, a nova instância do tipo pai, por si só não resolve pois as partes internas vão ser cópias por referência, a não ser que crie toda a estrutura interna até chegar nos objetos que precisa fazer a cópia por passagem, em vez de referência - copy._directionResult.value = _directionResult.value.copyWith(); - copy._stepsIndexes = List.of(_stepsIndexes); - copy._routeStepsLatLng.value = List.from(_routeStepsLatLng.value); - copy._pageControllerSignal.value = PageController(initialPage: 1); - // Removi todos os casos de valores que são inicializados nulo no RouteController, assim eles são reinicializados e não retorna erro por duplicidade, como no caso - return copy; - } - - RouteController copy() { - var copy = RouteController._(); - copy._spotholesInRouteList.value = _spotholesInRouteList.value; - copy._markersSignal.value = _markersSignal.value; - copy._polylinesSignal.value = _polylinesSignal.value; - copy._routePolylineCoordinatesSignal.value = - _routePolylineCoordinatesSignal.value; - copy._directionResult.value = _directionResult.value; + RouteController getCopy() { + var copy = RouteController(); + // Share the signals variables that need to be reactives between the RouteController instances + copy._markersSignal = _markersSignal; + copy._spotholesInRouteList = _spotholesInRouteList; + copy._polylinesSignal = _polylinesSignal; + copy._routePolylineCoordinatesSignal = _routePolylineCoordinatesSignal; + copy._directionResult = _directionResult; + copy._routeStepsLatLng = _routeStepsLatLng; + copy._auxRouteStepsLatLng = _auxRouteStepsLatLng; copy._stepsIndexes = _stepsIndexes; - copy._routeStepsLatLng.value = _routeStepsLatLng.value; - copy._auxRouteStepsLatLng.value = _auxRouteStepsLatLng.value; - // Removi todos os casos de valores que são inicializados nulo no RouteController, assim eles são reinicializados e não retorna erro por duplicidade, como no caso de widgets que precisam de controladores únicos + // Removi todos os casos de valores que são inicializados nulo no RouteController + // Principalmente controlladores, pois cada página precisa de um único por widget evitando erros por duplicidade. // copy._pageControllerSignal.value = PageController(initialPage: 1); - // copy._routeAndStepsListSignal.value = - // List.from(_routeAndStepsListSignal.value); // copy._googleMapController = null; // copy._customInfoWindowControllerSignal.value = // _customInfoWindowControllerSignal.value; // copy._spotholeService = _spotholeService; // copy._showStepsPageSignal.value = _showStepsPageSignal.value; // copy._pageViewTypeSignal.value = _pageViewTypeSignal.value; - // Torno null para não utilizar o msm controller em duas PageView diferentes return copy; } - - static RouteController getCopy() { - return instance.copy(); - } - - // // TODO just for dev tests! - // List routeToString(DirectionsResult response) { - // List strSteps = []; - // String strRoute = ''; - // LatLng northeastBound = - // geoCoordToLatLng(response.routes![0].bounds!.northeast); - // LatLng southwestBound = - // geoCoordToLatLng(response.routes![0].bounds!.southwest); - // //BOUNDS - // strRoute += 'northeastBound: ${latLngToString(northeastBound)}\n' - // 'southwestBound: ${latLngToString(southwestBound)}\n'; - // //SUMARY - // strRoute += 'summary: ${response.routes![0].summary}\n'; - // //OVERVIEW PATH - // List points = response.routes![0].overviewPath!; - // strRoute += 'overviewPath: '; - // for (GeoCoord geoCoord in points) { - // strRoute += '${geoCoordToString(geoCoord)}, '; - // } - // strRoute += '\n'; - // //WARNINGS - // final warnings = response.routes![0].warnings; - // strRoute += 'Warnings:'; - // if (warnings != null) { - // for (String? warning in warnings) { - // strRoute += '${warning!}, '; - // } - // } - // strRoute += '\n'; - // strSteps.add(strRoute); - // //STEPS - // List steps = response.routes![0].legs![0].steps!; - // for (Step currentStep in steps) { - // strSteps.add('Distância: ${currentStep.distance}\n' - // 'Duração: ${currentStep.duration!.text}\n' - // 'Start Location: ${geoCoordToString(currentStep.startLocation!)}\n' - // 'End Location: ${geoCoordToString(currentStep.endLocation!)}\n' - // 'Instructions: ${currentStep.instructions}\n' - // 'Maneuver: ${currentStep.maneuver}\n' - // 'Transit: ${currentStep.transit}\n' - // 'Travel Mode: ${currentStep.travelMode}\n'); - // } - // //Update da string de ROTA e STEPS - // _routeAndStepsListSignal.value = strSteps; - // return strSteps; - // } - - // // TODO just for dev tests! - // void showOverViewPathPoints(List points) { - // Map markers = {}; - // for (LatLng point in points) { - // String strPoint = latLngToString(point); - // markers[strPoint] = Marker( - // markerId: MarkerId(strPoint), - // position: point, - // ); - // } - // _markersSignal.value = { - // ..._markersSignal.value, - // ...markers, - // }; - // } } diff --git a/spotholes_android/lib/main.dart b/spotholes_android/lib/main.dart index ca179da..18eaeb4 100644 --- a/spotholes_android/lib/main.dart +++ b/spotholes_android/lib/main.dart @@ -32,9 +32,15 @@ class MyApp extends StatelessWidget { static final GlobalKey navigatorKey = GlobalKey(); + static final RouteObserver routeObserver = + RouteObserver(); + @override Widget build(BuildContext context) { return MaterialApp( + // Implements the AppNavigatorObserver to handle all route changes + // navigatorObservers: [AppNavigatorObserver()], + navigatorObservers: [routeObserver], navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, title: 'Spotholes', diff --git a/spotholes_android/lib/pages/navigation_page.dart b/spotholes_android/lib/pages/navigation_page.dart index 024f65a..e1b36b3 100644 --- a/spotholes_android/lib/pages/navigation_page.dart +++ b/spotholes_android/lib/pages/navigation_page.dart @@ -26,18 +26,8 @@ class NavigationPage extends StatefulWidget { class _NavigationPageState extends State { late final _routeController = widget.routeController; - late final _markers = _routeController.markersSignal; - - late final _directionResult = _routeController.directionResult; - late final _route = _directionResult.value.routes![0]; - late final _leg = _route.legs![0]; - late final _sourceLocation = - _routeController.geoCoordToLatLng(_leg.startLocation!); - late final _destinationLocation = - _routeController.geoCoordToLatLng(_leg.endLocation!); - - late final _routeSignal = signal(_route); + late final _startLocationLatLng = _routeController.startLocationLatLng; late final _navigationController = NavigationController(_routeController); @@ -91,7 +81,6 @@ class _NavigationPageState extends State { (_) => NavRouteStepsPageView( routeController: _routeController, pageController: _navigationController.pageController, - route: _routeSignal, isPageViewUpdateCamera: _isPageViewUpdateCamera, ), ), @@ -111,7 +100,7 @@ class _NavigationPageState extends State { .onMapCreated(controller); }, initialCameraPosition: CameraPosition( - target: _sourceLocation, + target: _startLocationLatLng.value, zoom: defaultZoomMap, tilt: 45, ), @@ -146,7 +135,6 @@ class _NavigationPageState extends State { ), NavigationDraggableSheet( routeController: _routeController, - destinationLocation: _destinationLocation, ), Positioned( bottom: 220, diff --git a/spotholes_android/lib/pages/route_page.dart b/spotholes_android/lib/pages/route_page.dart index 7ee87f7..8aa436c 100644 --- a/spotholes_android/lib/pages/route_page.dart +++ b/spotholes_android/lib/pages/route_page.dart @@ -4,6 +4,7 @@ import 'package:signals/signals_flutter.dart'; import 'package:spotholes_android/widgets/spotholes_page_view.dart'; import '../controllers/route_controller.dart'; +import '../main.dart'; import '../package/custom_info_window.dart'; import '../utilities/app_routes.dart'; import '../utilities/constants.dart'; @@ -26,37 +27,32 @@ class RoutePage extends StatefulWidget { State createState() => _RoutePageState(); } -class _RoutePageState extends State { - final _routeController = RouteController.instance; +class _RoutePageState extends State with RouteAware { + final _routeController = RouteController(); late final _markers = _routeController.markersSignal; - - late final _directionResult = _routeController.directionResult; - late final _route = _directionResult.value.routes![0]; - late final _leg = _route.legs![0]; + late final _route = _routeController.route; + late final _leg = _routeController.leg; late final _customInfoWindowControllerSignal = _routeController.customInfoWindowControllerSignal; - bool _isLoading = true; - + late final _pageControllerSignal = _routeController.pageControllerSignal; late final _pageViewTypeSignal = _routeController.pageViewTypeSignal; - late final _showStepsPageSignal = _routeController.showStepsPageSignal; - late final Signal _pageControllerSignal = - _routeController.pageControllerSignal; + bool _isLoading = true; void _toggleWidgets() { _showStepsPageSignal.value = !_showStepsPageSignal.value; } - List _horizontalListButtons(BuildContext context, position) { + List _horizontalListButtons() { return [ CustomButton( label: 'Iniciar viagem', bgColor: Colors.tealAccent.shade400, - onPressed: () => Navigator.of(context).pushNamed( + onPressed: () => MyApp.navigatorKey.currentState?.pushNamed( AppRoutes.navigation, - arguments: [RouteController.getCopy()], + arguments: [_routeController.getCopy()], ), ), CustomButton( @@ -84,9 +80,24 @@ class _RoutePageState extends State { _loadRoute(); } + @override + void didPopNext() { + _routeController.centerViewRoute(); + // Avoid exception when the user returns from the navigation page and try access the info window + _customInfoWindowControllerSignal.value.hideInfoWindow!(); + // Update markers when the user returns from the navigation page changing the customInfoWindowController with the new mapController + _routeController.updateAllRouteMarkers(); + } + + @override + void didChangeDependencies() { + MyApp.routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute); + super.didChangeDependencies(); + } + @override void dispose() { - RouteController.resetInstance(); + MyApp.routeObserver.unsubscribe(this); super.dispose(); } @@ -135,14 +146,14 @@ class _RoutePageState extends State { child: Column( children: [ Text( - 'Via: ${_route.summary!}', + 'Via: ${_route.value.summary!}', style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), Text( - '${_leg.distance!.text} (${_leg.duration!.text})', + '${_leg.value.distance!.text} (${_leg.value.duration!.text})', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), @@ -152,13 +163,13 @@ class _RoutePageState extends State { if (_showStepsPageSignal.value && _pageViewTypeSignal.value == 'route') RouteStepsPageView( - pageController: _pageControllerSignal.value, - route: _route, + routeController: _routeController, ), if (_showStepsPageSignal.value && _pageViewTypeSignal.value == 'spothole') SpotholesPageView( - pageController: _pageControllerSignal.value), + routeController: _routeController, + ), Expanded( child: Stack( children: [ @@ -206,9 +217,7 @@ class _RoutePageState extends State { visible: !_showStepsPageSignal.value, maintainState: true, child: RouteDraggableSheet( - controller: RouteDraggableSheetController(), - destinationLocation: - widget.destinationLocation, + routeController: _routeController, ), ), if (_showStepsPageSignal.value) @@ -273,10 +282,7 @@ class _RoutePageState extends State { height: 60.0, child: ListView( scrollDirection: Axis.horizontal, - children: _horizontalListButtons( - context, - widget.destinationLocation, - ), + children: _horizontalListButtons(), ), ), ), diff --git a/spotholes_android/lib/services/navigation_service.dart b/spotholes_android/lib/services/navigation_service.dart index 109425e..bc48e48 100644 --- a/spotholes_android/lib/services/navigation_service.dart +++ b/spotholes_android/lib/services/navigation_service.dart @@ -21,9 +21,10 @@ class NavigationService { this._isPageViewUpdateCamera, ); - late final Signal> _routePolylineCoordinatesSignal = + late final _routePolylineCoordinatesSignal = _routeController.routePolylineCoordinatesSignal; - late final List _stepsIndexes = _routeController.stepsIndexes; + late final _stepsIndexes = _routeController.stepsIndexes; + late final _routeStepsLatLng = _routeController.routeStepsLatLng; int _discardedPointsCounter = 0; int _countOutOfRoute = 0; @@ -78,7 +79,7 @@ class NavigationService { void updateRouteStatus(LatLng currentLocation) async { // TODO Verificar se há outras situações de fim da rota // Verifica se a rota está vazia, por exemplo: trajeto concluído. - if (_routeController.routeStepsLatLng.value.isEmpty) { + if (_routeStepsLatLng.value.isEmpty) { return; } routePointsMtk = @@ -110,10 +111,9 @@ class NavigationService { } // Remove os steps que já passaram _stepsIndexes.removeRange(0, currentStepIndex); - _routeController.routeStepsLatLng.value - .removeRange(0, currentStepIndex); + _routeStepsLatLng.value.removeRange(0, currentStepIndex); - final stepsLength = _routeController.routeStepsLatLng.value.length; + final stepsLength = _routeStepsLatLng.value.length; final totalRemovedSteps = currentStepIndex; final page = _pageController.page!.toInt(); // Verifica se o movimento de retorno do pageView termina no máximo na página 01 (step 0), se a página atual está entre a página 01 e a penúltima página (dentro da lista de steps) @@ -121,10 +121,8 @@ class NavigationService { _isPageViewUpdateCamera.value = false; _pageController.jumpToPage(page - totalRemovedSteps); } else { - // É necessário atualizar para forçar a renderização do widget, porém assim evita duplicação do update porque o jumpToPage invoca um método que faz update da lista - _routeController.routeStepsLatLng.value = [ - ..._routeController.routeStepsLatLng.value - ]; + // É necessário atualizar para forçar a renderização do widget, porém, assim evita duplicação do update porque o jumpToPage invoca um método que faz update da lista + _routeStepsLatLng.value = [..._routeStepsLatLng.value]; } debugPrint('---pages: ${_pageController.page} $currentStepIndex'); } @@ -134,18 +132,16 @@ class NavigationService { _routePolylineCoordinatesSignal.value[0] = currentLocation; _routeController.updateRoutePolyline(); // Caso seja o último step e tenha menos que 3 pontos, então descarta o último step, pontos e conclui a rota - } else if (_routeController.routeStepsLatLng.value.length == 1 && + } else if (_routeStepsLatLng.value.length == 1 && _routePolylineCoordinatesSignal.value.length < 3) { _countOutOfRoute = 0; debugPrint('---Cheguei no final'); _stepsIndexes.clear(); - _routeController.routeStepsLatLng.value.clear(); + _routeStepsLatLng.value.clear(); _routePolylineCoordinatesSignal.value.clear(); _routeController.clearManeuverPolyline(); // Força update dos steps - _routeController.routeStepsLatLng.value = [ - ..._routeController.routeStepsLatLng.value - ]; + _routeStepsLatLng.value = [..._routeStepsLatLng.value]; } else { _countOutOfRoute += 1; // Recalcula a rota após 5 movimentos consecutivos fora da rota (considerando a margem de tolerâcia em metros) @@ -158,6 +154,6 @@ class NavigationService { } } debugPrint( - '----[Após descarte] points ${_routePolylineCoordinatesSignal.value.length} steps:${_routeController.routeStepsLatLng.value.length}'); + '----[Após descarte] points ${_routePolylineCoordinatesSignal.value.length} steps:${_routeStepsLatLng.value.length}'); } } diff --git a/spotholes_android/lib/utilities/app_navigator_observer.dart b/spotholes_android/lib/utilities/app_navigator_observer.dart new file mode 100644 index 0000000..4adc9fd --- /dev/null +++ b/spotholes_android/lib/utilities/app_navigator_observer.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class AppNavigatorObserver extends RouteObserver> { + // void _sendScreenView(PageRoute route) { + // var screenName = route.settings.name; + // // do something with it, ie. send it to your analytics service collector + // } + + @override + void didPop(Route route, Route? previousRoute) { + super.didPop(route, previousRoute); + if (previousRoute is PageRoute && route is PageRoute) { + // _sendScreenView(previousRoute); + var routeName = route.settings.name; + var previousRouteName = previousRoute.settings.name; + if (routeName == '/navigation' && previousRouteName == '/route') { + debugPrint('---- didPop: $routeName, $previousRouteName'); + } + } + } + + // @override + // void didPush(Route route, Route previousRoute) { + // super.didPush(route, previousRoute); + // if (route is PageRoute) { + // _sendScreenView(route); + // } + // } + + // @override + // void didReplace({Route newRoute, Route oldRoute}) { + // super.didReplace(newRoute: newRoute, oldRoute: oldRoute); + // if (newRoute is PageRoute) { + // _sendScreenView(newRoute); + // } + // } +} diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart index 65bb497..76da10e 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart @@ -29,13 +29,10 @@ class DraggableScrollableSheetTypes { } static DraggableScrollableSheetType location( - {required Function onRegister, - required LatLng position, - required formattedPlacemark}) { + {required LatLng position, required formattedPlacemark}) { return DraggableScrollableSheetType( widget: LocationDraggableSheet( controller: LocationDraggableSheetController(), - onRegister: onRegister, position: position, formattedPlacemark: formattedPlacemark, ), diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart index 81100e9..c260c30 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart @@ -3,6 +3,7 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:spotholes_android/utilities/app_routes.dart'; import '../../controllers/base_map_controller.dart'; +import '../../main.dart'; import '../button/custom_button.dart'; class LocationDraggableSheetController { @@ -17,13 +18,11 @@ class LocationDraggableSheet extends StatefulWidget { const LocationDraggableSheet({ super.key, required this.controller, - required this.onRegister, required this.position, required this.formattedPlacemark, }); final LatLng position; - final Function onRegister; final LocationDraggableSheetController controller; final String formattedPlacemark; @@ -37,23 +36,24 @@ class LocationDraggableSheetState extends State { final _baseMapController = BaseMapController.instance; late final _canvasColor = Theme.of(context).canvasColor; - void _registerSpotholeModal(BuildContext context) { - widget.onRegister(); - } - - List _horizontalListButtons(BuildContext context, position) { + List _horizontalListButtons() { return [ CustomButton( label: 'Rota', bgColor: Colors.tealAccent.shade400, - onPressed: () => Navigator.of(context).pushNamed( + onPressed: () => MyApp.navigatorKey.currentState?.pushNamed( AppRoutes.route, - arguments: [_baseMapController.currentLocationLatLng, position], + arguments: [ + _baseMapController.currentLocationLatLng, + widget.position + ], ), ), CustomButton( label: 'Alertar', - onPressed: () => _registerSpotholeModal(context), + onPressed: () => _baseMapController.registerSpotholeModal( + position: widget.position, + ), ), ]; } @@ -143,8 +143,7 @@ class LocationDraggableSheetState extends State { height: 60.0, child: ListView( scrollDirection: Axis.horizontal, - children: _horizontalListButtons( - context, widget.position), + children: _horizontalListButtons(), ), ) ], diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart index 6f62068..eebdc02 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart @@ -40,7 +40,7 @@ class MainDraggableSheetState extends State { super.dispose(); } - List _horizontalListButtons(BuildContext context) { + List _horizontalListButtons() { return [ CustomButton( label: 'Buscar', @@ -113,7 +113,7 @@ class MainDraggableSheetState extends State { height: 60.0, child: ListView( scrollDirection: Axis.horizontal, - children: _horizontalListButtons(context), + children: _horizontalListButtons(), ), ) ], diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart index 5f2e239..b436a35 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/navigation_draggable_sheet.dart @@ -1,19 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:signals/signals_flutter.dart'; import 'package:spotholes_android/controllers/route_controller.dart'; import 'package:spotholes_android/widgets/bullet_list.dart'; +import '../../main.dart'; import '../button/custom_button.dart'; class NavigationDraggableSheet extends StatefulWidget { const NavigationDraggableSheet({ super.key, required this.routeController, - required this.destinationLocation, }); - final LatLng destinationLocation; final RouteController routeController; @override @@ -30,17 +28,11 @@ class NavigationDraggableSheetState extends State { final _minDraggableChildSize = 0.25; final _intermediateDraggableChildSize = 0.4; final _maxDraggableChildSize = 1.0; - // int? _selectedIndex; late final _canvasColor = Theme.of(context).canvasColor; - late final _directionResult = _routeController.directionResult.value; - late final _route = _directionResult.routes![0]; - late final _leg = _route.legs![0]; - // late final _steps = _leg.steps!; - - // late final _spotholesInRouteListSignal = - // _routeController.spotholesInRouteList; + late final _route = _routeController.route; + late final _leg = _routeController.leg; @override void initState() { @@ -53,7 +45,8 @@ class NavigationDraggableSheetState extends State { } } - bool _haveWarnings() => _route.warnings?.isNotEmpty == true ? true : false; + bool _haveWarnings() => + _route.value.warnings?.isNotEmpty == true ? true : false; void _showWarningsDialog() { showDialog( @@ -73,7 +66,7 @@ class NavigationDraggableSheetState extends State { ], ), content: BulletList( - items: _route.warnings!, + items: _route.value.warnings!, ), actions: [ TextButton( @@ -109,13 +102,8 @@ class NavigationDraggableSheetState extends State { super.dispose(); } - List _horizontalListButtons(BuildContext context, position) { + List _horizontalListButtons() { return [ - // CustomButton( - // label: 'Alertar Risco', - // bgColor: Colors.tealAccent.shade400, - // // onPressed: () => , - // ), CustomButton( label: 'Centralizar', bgColor: Colors.tealAccent.shade400, @@ -176,7 +164,7 @@ class NavigationDraggableSheetState extends State { ), SliverAppBar( title: Text( - '${_leg.distance!.text} (${_leg.duration!.text})', + '${_leg.value.distance!.text} (${_leg.value.duration!.text})', style: Theme.of(context).textTheme.titleLarge, ), primary: false, @@ -213,7 +201,8 @@ class NavigationDraggableSheetState extends State { ), IconButton( icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => + MyApp.navigatorKey.currentState?.pop(true), ), ], // bottom: PreferredSize( @@ -239,7 +228,7 @@ class NavigationDraggableSheetState extends State { child: Column( children: [ Text( - 'Via: ${_route.summary!}', + 'Via: ${_route.value.summary!}', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, maxLines: 2, @@ -262,8 +251,7 @@ class NavigationDraggableSheetState extends State { height: 60.0, child: ListView( scrollDirection: Axis.horizontal, - children: - _horizontalListButtons(context, widget.destinationLocation), + children: _horizontalListButtons(), ), ), ), diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart index 9dd3f78..19cd3b7 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart @@ -4,6 +4,7 @@ import 'package:spotholes_android/package/google_places_flutter/model/place_deta import 'package:spotholes_android/utilities/app_routes.dart'; import '../../controllers/base_map_controller.dart'; +import '../../main.dart'; import '../button/custom_button.dart'; class PlaceDraggableSheetController { @@ -35,23 +36,23 @@ class PlaceDraggableSheetState extends State { late final _canvasColor = Theme.of(context).canvasColor; _loadRoute(position) { - Navigator.of(context).pushNamed( + MyApp.navigatorKey.currentState?.pushNamed( AppRoutes.route, arguments: [_baseMapController.currentLocationLatLng, position], ); } - List _horizontalListButtons(BuildContext context, position) { + List _horizontalListButtons() { return [ CustomButton( label: 'Rota', bgColor: Colors.tealAccent.shade400, - onPressed: () => _loadRoute(position), + onPressed: () => _loadRoute(widget.position), ), CustomButton( label: 'Alertar', onPressed: () => - _baseMapController.registerSpotholeModal(position: position), + _baseMapController.registerSpotholeModal(position: widget.position), ), ]; } @@ -143,8 +144,7 @@ class PlaceDraggableSheetState extends State { height: 60.0, child: ListView( scrollDirection: Axis.horizontal, - children: _horizontalListButtons( - context, widget.position), + children: _horizontalListButtons(), ), ) ], diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/route_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/route_draggable_sheet.dart index e27cf0d..9fa41b5 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/route_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/route_draggable_sheet.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:signals/signals_flutter.dart'; import 'package:spotholes_android/controllers/route_controller.dart'; import 'package:spotholes_android/widgets/bullet_list.dart'; +import '../../main.dart'; import '../../models/spothole.dart'; import '../../utilities/app_routes.dart'; import '../../utilities/custom_icons.dart'; @@ -20,21 +20,19 @@ class RouteDraggableSheetController { } class RouteDraggableSheet extends StatefulWidget { + final RouteController routeController; + const RouteDraggableSheet({ super.key, - required this.controller, - required this.destinationLocation, + required this.routeController, }); - final RouteDraggableSheetController controller; - final LatLng destinationLocation; - @override RouteDraggableSheetState createState() => RouteDraggableSheetState(); } class RouteDraggableSheetState extends State { - final _routeController = RouteController.instance; + late final _routeController = widget.routeController; final scrollController = ScrollController(); final _draggableController = DraggableScrollableController(); @@ -46,10 +44,9 @@ class RouteDraggableSheetState extends State { late final _canvasColor = Theme.of(context).canvasColor; - late final _directionResult = _routeController.directionResult.value; - late final _route = _directionResult.routes![0]; - late final _leg = _route.legs![0]; - late final _steps = _leg.steps!; + late final _route = _routeController.route; + late final _leg = _routeController.leg; + late final _steps = _routeController.routeStepsLatLng; late final _spotholesInRouteListSignal = _routeController.spotholesInRouteList; @@ -65,7 +62,8 @@ class RouteDraggableSheetState extends State { } } - bool _haveWarnings() => _route.warnings?.isNotEmpty == true ? true : false; + bool _haveWarnings() => + _route.value.warnings?.isNotEmpty == true ? true : false; void _showWarningsDialog() { showDialog( @@ -85,7 +83,7 @@ class RouteDraggableSheetState extends State { ], ), content: BulletList( - items: _route.warnings!, + items: _route.value.warnings!, ), actions: [ TextButton( @@ -121,14 +119,14 @@ class RouteDraggableSheetState extends State { super.dispose(); } - List _horizontalListButtons(BuildContext context, position) { + List _horizontalListButtons() { return [ CustomButton( label: 'Iniciar viagem', bgColor: Colors.tealAccent.shade400, - onPressed: () => Navigator.of(context).pushNamed( + onPressed: () => MyApp.navigatorKey.currentState?.pushNamed( AppRoutes.navigation, - arguments: [RouteController.getCopy()], + arguments: [_routeController.getCopy()], ), ), CustomButton( @@ -246,7 +244,8 @@ class RouteDraggableSheetState extends State { ), IconButton( icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => + MyApp.navigatorKey.currentState?.pop(), ), ], bottom: TabBar( @@ -262,8 +261,8 @@ class RouteDraggableSheetState extends State { Container( padding: const EdgeInsets.all(6), child: Text( - _steps.length < 100 - ? '${_steps.length}' + _steps.value.length < 100 + ? '${_steps.value.length}' : '+99', ), ), @@ -304,14 +303,14 @@ class RouteDraggableSheetState extends State { (context) => ListView.separated( controller: scrollController, physics: const ClampingScrollPhysics(), - itemCount: _steps.length + 2, + itemCount: _steps.value.length + 2, separatorBuilder: (context, index) => const Divider(), itemBuilder: (context, index) { if (index == 0) { return ListTile( title: Text( - 'Partida: ${_leg.startAddress!}'), + 'Partida: ${_leg.value.startAddress!}'), leading: SizedBox( height: 35, width: 35, @@ -335,16 +334,17 @@ class RouteDraggableSheetState extends State { _intermediateDraggableChildSize), _routeController ..updateCameraGeoCoord( - _leg.startLocation!) + _leg.value.startLocation!) ..setupStepsPageView(0, 'route'), }, ); - } else if (index == _steps.length + 1) { + } else if (index == + _steps.value.length + 1) { return ListTile( selected: _selectedIndex == - _steps.length + 1, + _steps.value.length + 1, tileColor: _selectedIndex == - _steps.length + 1 + _steps.value.length + 1 ? Colors.amber : null, selectedColor: Theme.of(context) @@ -361,12 +361,12 @@ class RouteDraggableSheetState extends State { _intermediateDraggableChildSize), _routeController ..updateCameraGeoCoord( - _leg.endLocation!) + _leg.value.endLocation!) ..setupStepsPageView( index, 'route'), }, title: Text( - 'Destino: ${_leg.endAddress!}'), + 'Destino: ${_leg.value.endAddress!}'), leading: SizedBox( height: 35, width: 35, @@ -375,7 +375,7 @@ class RouteDraggableSheetState extends State { ), ); } else { - final step = _steps[index - 1]; + final step = _steps.value[index - 1]; final maneuver = step.maneuver ?? 'straight'; final icon = maneuverIcons[maneuver] ?? @@ -538,8 +538,7 @@ class RouteDraggableSheetState extends State { height: 60.0, child: ListView( scrollDirection: Axis.horizontal, - children: - _horizontalListButtons(context, widget.destinationLocation), + children: _horizontalListButtons(), ), ), ), diff --git a/spotholes_android/lib/widgets/nav_route_steps_page_view.dart b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart index a27dbb6..49e1c61 100644 --- a/spotholes_android/lib/widgets/nav_route_steps_page_view.dart +++ b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:expandable_page_view/expandable_page_view.dart'; import 'package:flutter/material.dart' hide Step; import 'package:flutter_html/flutter_html.dart' hide Marker; -import 'package:google_directions_api/google_directions_api.dart'; import 'package:signals/signals_flutter.dart'; import '../controllers/route_controller.dart'; @@ -13,14 +12,12 @@ import '../utilities/maneuver_icons.dart'; class NavRouteStepsPageView extends StatefulWidget { final RouteController routeController; final PageController pageController; - final Signal route; final Signal isPageViewUpdateCamera; const NavRouteStepsPageView({ super.key, required this.routeController, required this.pageController, - required this.route, required this.isPageViewUpdateCamera, }); @@ -32,11 +29,9 @@ class NavRouteStepsStatePageView extends State { late final _routeController = widget.routeController; late final _pageController = widget.pageController; late final _isPageViewUpdateCamera = widget.isPageViewUpdateCamera; - int _currentPage = 0; - late final _route = widget.route; - late final _leg = computed(() => _route.value.legs![0]); + late final _leg = _routeController.leg; late final _steps = _routeController.routeStepsLatLng; @override @@ -164,7 +159,7 @@ class NavRouteStepsStatePageView extends State { ), ), ); - // Demais steps que contém manobras, excluindo a origem e o destino + // Demais steps que contêm manobras, excluindo a origem e o destino } else if (index >= 1 && index <= _steps.value.length) { final step = _steps.value[index - 1]; final maneuver = step.maneuver ?? 'straight'; diff --git a/spotholes_android/lib/widgets/route_steps_page_view.dart b/spotholes_android/lib/widgets/route_steps_page_view.dart index 641f565..adf8e64 100644 --- a/spotholes_android/lib/widgets/route_steps_page_view.dart +++ b/spotholes_android/lib/widgets/route_steps_page_view.dart @@ -3,33 +3,34 @@ import 'dart:async'; import 'package:expandable_page_view/expandable_page_view.dart'; import 'package:flutter/material.dart' hide Step; import 'package:flutter_html/flutter_html.dart' hide Marker; -import 'package:google_directions_api/google_directions_api.dart'; +import 'package:signals/signals_flutter.dart'; import '../controllers/route_controller.dart'; import '../utilities/custom_icons.dart'; import '../utilities/maneuver_icons.dart'; class RouteStepsPageView extends StatefulWidget { + final RouteController routeController; + const RouteStepsPageView({ super.key, - required this.route, - required this.pageController, + required this.routeController, }); - final DirectionsRoute route; - final PageController pageController; - @override RouteStepsStatePageView createState() => RouteStepsStatePageView(); } class RouteStepsStatePageView extends State { - late final PageController _pageController = widget.pageController; - int _currentPage = 0; - - final _routeController = RouteController.instance; - late final _leg = widget.route.legs![0]; + late final _routeController = widget.routeController; + late final _leg = _routeController.leg; + late final _startLocation = _routeController.startLocation; + late final _endLocation = _routeController.endLocation; late final _steps = _routeController.routeStepsLatLng; + late final _polylinesSignal = _routeController.polylinesSignal; + + late final _pageController = _routeController.pageControllerSignal.value; + int _currentPage = 0; @override void initState() { @@ -44,9 +45,9 @@ class RouteStepsStatePageView extends State { _currentPage = newPage; _cleanPolylines(); if (_currentPage == 0) { - _routeController.updateCameraGeoCoord(_leg.startLocation!); + _routeController.updateCameraGeoCoord(_startLocation.value!); } else if (_currentPage == _steps.value.length + 1) { - _routeController.updateCameraGeoCoord(_leg.endLocation!); + _routeController.updateCameraGeoCoord(_endLocation.value!); } else { final stepIndex = _currentPage - 1; _routeController.plotManeuverPolyline(stepIndex); @@ -57,19 +58,17 @@ class RouteStepsStatePageView extends State { } void _cleanPolylines() { - _routeController.polylinesSignal.value.remove('maneuverArrow'); - _routeController.polylinesSignal.value.remove('straightPath'); - _routeController.polylinesSignal.value = { - ..._routeController.polylinesSignal.value - }; + _polylinesSignal.value.remove('maneuverArrow'); + _polylinesSignal.value.remove('straightPath'); + _polylinesSignal.value = {..._polylinesSignal.value}; } void _initialStepCamera() { final initialPage = _pageController.initialPage; if (initialPage == 0) { - _routeController.updateCameraGeoCoord(_leg.startLocation!); + _routeController.updateCameraGeoCoord(_startLocation.value!); } else if (initialPage == _steps.value.length + 1) { - _routeController.updateCameraGeoCoord(_leg.endLocation!); + _routeController.updateCameraGeoCoord(_endLocation.value!); } else { final step = _steps.value[initialPage - 1]; final maneuver = step.maneuver ?? 'straight'; @@ -112,106 +111,114 @@ class RouteStepsStatePageView extends State { @override Widget build(BuildContext context) { - return Material( - child: InkWell( - child: ExpandablePageView.builder( - controller: _pageController, - itemCount: _steps.value.length + 2, - itemBuilder: (context, index) { - if (index == 0) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(width: 0.5), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: ListTile( - title: Text( - 'Partida: ${_leg.startAddress!}', - ), - leading: SizedBox( - height: 35, - width: 35, - child: CustomIcons.sourceIconAsset, - ), - onTap: () => { - _routeController - .updateCameraGeoCoord(_leg.startLocation!), - }, + return Watch( + (context) => Material( + child: InkWell( + child: ExpandablePageView.builder( + controller: _pageController, + itemCount: _steps.value.length + 2, + itemBuilder: (context, index) { + if (index == 0) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(width: 0.5), ), - ), - ); - } else if (index == _steps.value.length + 1) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(width: 0.5), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: ListTile( - onTap: () => { - _routeController.updateCameraGeoCoord(_leg.endLocation!), - }, - title: Text( - 'Destino: ${_leg.endAddress!}', - ), - leading: SizedBox( - height: 35, - width: 35, - child: CustomIcons.destinationIconAsset, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: ListTile( + title: Text( + 'Partida: ${_leg.value.startAddress!}', + ), + leading: SizedBox( + height: 35, + width: 35, + child: CustomIcons.sourceIconAsset, + ), + onTap: () => { + _routeController + .updateCameraGeoCoord(_startLocation.value!), + }, ), ), - ), - ); - } else { - final step = _steps.value[index - 1]; - final maneuver = step.maneuver ?? 'straight'; - final icon = maneuverIcons[maneuver] ?? Icons.directions; - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(width: 0.5), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: ListTile( - onTap: () => { - maneuver == 'straight' - ? _routeController.newCameraLatLngBoundsFromStep(step) - : _routeController - .updateCameraGeoCoord(step.startLocation!), - }, - leading: Icon( - icon, - size: 35, - ), - title: Html( - data: step.instructions!, - style: { - "body": Style( - fontStyle: - Theme.of(context).textTheme.bodyLarge!.fontStyle, - margin: Margins.zero, - ), + ); + } else if (index == _steps.value.length + 1) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(width: 0.5), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: ListTile( + onTap: () => { + _routeController + .updateCameraGeoCoord(_endLocation.value!), }, + title: Text( + 'Destino: ${_leg.value.endAddress!}', + ), + leading: SizedBox( + height: 35, + width: 35, + child: CustomIcons.destinationIconAsset, + ), ), - subtitle: Html( - data: 'Distância: ${step.distance!.text}', - style: { - "body": Style( - fontStyle: - Theme.of(context).textTheme.bodyLarge!.fontStyle, - margin: Margins.zero, - ), + ), + ); + } else { + final step = _steps.value[index - 1]; + final maneuver = step.maneuver ?? 'straight'; + final icon = maneuverIcons[maneuver] ?? Icons.directions; + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(width: 0.5), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: ListTile( + onTap: () => { + maneuver == 'straight' + ? _routeController + .newCameraLatLngBoundsFromStep(step) + : _routeController + .updateCameraGeoCoord(step.startLocation!), }, + leading: Icon( + icon, + size: 35, + ), + title: Html( + data: step.instructions!, + style: { + "body": Style( + fontStyle: Theme.of(context) + .textTheme + .bodyLarge! + .fontStyle, + margin: Margins.zero, + ), + }, + ), + subtitle: Html( + data: 'Distância: ${step.distance!.text}', + style: { + "body": Style( + fontStyle: Theme.of(context) + .textTheme + .bodyLarge! + .fontStyle, + margin: Margins.zero, + ), + }, + ), ), ), - ), - ); - } - }, + ); + } + }, + ), ), ), ); diff --git a/spotholes_android/lib/widgets/spotholes_page_view.dart b/spotholes_android/lib/widgets/spotholes_page_view.dart index 61d3090..4c6538a 100644 --- a/spotholes_android/lib/widgets/spotholes_page_view.dart +++ b/spotholes_android/lib/widgets/spotholes_page_view.dart @@ -7,25 +7,23 @@ import '../controllers/route_controller.dart'; import '../models/spothole.dart'; class SpotholesPageView extends StatefulWidget { + final RouteController routeController; + const SpotholesPageView({ super.key, - required this.pageController, + required this.routeController, }); - final PageController pageController; - @override SpotholesStatePageView createState() => SpotholesStatePageView(); } class SpotholesStatePageView extends State { - late final PageController _pageController = widget.pageController; - int _currentPage = 0; - - final _routeController = RouteController.instance; - + late final _routeController = widget.routeController; late final _spotholesInRouteList = _routeController.spotholesInRouteList.value; + late final _pageController = _routeController.pageControllerSignal.value; + int _currentPage = 0; @override void initState() { From 0739ecdf00f151161cf9eab57614add093692498 Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Thu, 26 Dec 2024 22:50:54 -0300 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20remove=20o=20padr=C3=A3o=20Si?= =?UTF-8?q?ngleton=20do=20BaseMapController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mais informações: - Remove o padrão singleton do BaseMapController para melhorar a consistência da aplicação. Garantindo que o widget que precise do controller receba-o via construtor. - Modifica a lógica dos DraggableScrollableSheets para adequá-los à mudança; - Refatora alguns getters para retornar o tipo em vez de dynamic; - Padroniza os imports nos arquivos modificados. --- .../lib/controllers/base_map_controller.dart | 83 ++++++++++--------- .../lib/pages/base_map_page.dart | 10 ++- .../lib/services/location_service.dart | 9 +- .../custom_floating_action_button_list.dart | 15 ++-- .../draggable_scrollable_sheet_type.dart | 28 +++++-- .../location_draggable_sheet.dart | 16 ++-- .../main_draggable_sheet.dart | 11 ++- .../place_draggable_sheet.dart | 29 ++++--- spotholes_android/lib/widgets/search_bar.dart | 32 ++++--- 9 files changed, 137 insertions(+), 96 deletions(-) diff --git a/spotholes_android/lib/controllers/base_map_controller.dart b/spotholes_android/lib/controllers/base_map_controller.dart index 2117de4..8e1cba4 100644 --- a/spotholes_android/lib/controllers/base_map_controller.dart +++ b/spotholes_android/lib/controllers/base_map_controller.dart @@ -19,52 +19,51 @@ import '../widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.da import '../widgets/info_window/marker_info_window.dart'; class BaseMapController { - BaseMapController._(); - static BaseMapController _instance = BaseMapController._(); - static BaseMapController get instance => _instance; - - static void resetInstance() { - _instance = BaseMapController._(); - } - Function? _dispose; final LocationService _locationService = LocationService.instance; - late final Signal _currentLocationSignal = - _locationService.currentLocationSignal; - get currentLocationSignal => _currentLocationSignal; - get currentLocationLatLng => LatLng(_currentLocationSignal.value!.latitude!, + late final _currentLocationSignal = _locationService.currentLocationSignal; + Signal get currentLocationSignal => _currentLocationSignal; + + LatLng get currentLocationLatLng => LatLng( + _currentLocationSignal.value!.latitude!, _currentLocationSignal.value!.longitude!); final databaseReference = getIt(); late final dataBaseSpotholesRef = databaseReference.child('spotholes'); final _googleMapControllerCompleter = Completer(); - get getGoogleMapController async => + Future get getGoogleMapController async => await _googleMapControllerCompleter.future; final _customInfoWindowControllerSignal = Signal(CustomInfoWindowController()); - get customInfoWindowControllerSignal => _customInfoWindowControllerSignal; + Signal get customInfoWindowControllerSignal => + _customInfoWindowControllerSignal; final _markersSignal = Signal>({}); - get markersSignal => _markersSignal; + Signal> get markersSignal => _markersSignal; - late final spotholeService = SpotholeService( + late final _spotholeService = SpotholeService( _markersSignal, _customInfoWindowControllerSignal, ); final _textEditingController = TextEditingController(); - get textEditingController => _textEditingController; + TextEditingController get textEditingController => _textEditingController; final _searchBarFocusNode = FocusNode(); - get searchBarFocusNode => _searchBarFocusNode; + FocusNode get searchBarFocusNode => _searchBarFocusNode; + + late final _draggableScrollableSheetTypes = + DraggableScrollableSheetTypes(baseMapController: this); - late Signal draggableScrollableSheetSignal = signal( - DraggableScrollableSheetTypes.initial.widget, + late final _draggableScrollableSheetSignal = signal( + _draggableScrollableSheetTypes.initial.widget, ); + Signal get draggableScrollableSheetSignal => + _draggableScrollableSheetSignal; final _geocodingService = GeocodingService.instance; final isTrackingLocation = signal(true); @@ -74,7 +73,7 @@ class BaseMapController { _customInfoWindowControllerSignal.value.googleMapController = mapController; _googleMapControllerCompleter.complete(mapController); listenCurrentLocation(); - spotholeService.loadSpotholeMarkers(); + _spotholeService.loadSpotholeMarkers(); } void trackLocation() { @@ -129,7 +128,7 @@ class BaseMapController { void closeDraggableSheet(String key) { _customInfoWindowControllerSignal.value.hideInfoWindow!(); removeMarkerByKey(key); - changeDraggableSheet(DraggableScrollableSheetTypes.initial); + changeDraggableSheet(_draggableScrollableSheetTypes.initial); isTrackingLocation.value = true; centerView(); } @@ -142,15 +141,20 @@ class BaseMapController { icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure), position: position, onTap: () => _customInfoWindowControllerSignal.value.addInfoWindow!( - MarkerInfoWindow( - title: 'Resultado da Busca', - textContent: placeDetails.result!.name ?? - 'Latitude: ${placeLocation.lat!}\rLongitude: ${placeLocation.lng!}'), - position), + MarkerInfoWindow( + title: 'Resultado da Busca', + textContent: placeDetails.result!.name ?? + 'Latitude: ${placeLocation.lat!}\rLongitude: ${placeLocation.lng!}', + ), + position, + ), ); updateCameraGoogleMapsController(position); changeDraggableSheet(DraggableScrollableSheetTypes.place( - placeDetails: placeDetails, position: position)); + placeDetails: placeDetails, + position: position, + baseMapController: this, + )); } void removeMarkerByKey(key) { @@ -158,12 +162,12 @@ class BaseMapController { } void loadSpotholeMarkers() { - spotholeService.loadSpotholeMarkers(); + _spotholeService.loadSpotholeMarkers(); } void registerSpotholeModal({LatLng? position}) { final registerPosition = position ?? currentLocationLatLng; - spotholeService.registerSpotholeModal(registerPosition); + _spotholeService.registerSpotholeModal(registerPosition); } void onLongPress(BuildContext context, LatLng position) async { @@ -180,9 +184,10 @@ class BaseMapController { WidgetsBinding.instance.addPostFrameCallback( (_) { CustomSnackbar.show( - context: context, - message: - 'Não foi possível carregar informações, verifique a conexão com a internet'); + context: context, + message: + 'Não foi possível carregar informações, verifique a conexão com a internet', + ); }, ); } @@ -190,16 +195,18 @@ class BaseMapController { markerId: MarkerId(position.toString()), position: position, onTap: () => _customInfoWindowControllerSignal.value.addInfoWindow!( - MarkerInfoWindow( - title: 'Local Aproximado', - textContent: windowInfo, - ), - position), + MarkerInfoWindow( + title: 'Local Aproximado', + textContent: windowInfo, + ), + position, + ), ); changeDraggableSheet( DraggableScrollableSheetTypes.location( position: position, formattedPlacemark: formattedPlacemark, + baseMapController: this, ), ); } diff --git a/spotholes_android/lib/pages/base_map_page.dart b/spotholes_android/lib/pages/base_map_page.dart index 5f44167..c7c772f 100644 --- a/spotholes_android/lib/pages/base_map_page.dart +++ b/spotholes_android/lib/pages/base_map_page.dart @@ -16,7 +16,7 @@ class BaseMapPage extends StatefulWidget { } class BaseMapPageState extends State { - final _baseMapController = BaseMapController.instance; + final _baseMapController = BaseMapController(); late final _customInfoWindowControllerSignal = _baseMapController.customInfoWindowControllerSignal; late final _markersSignal = _baseMapController.markersSignal; @@ -85,8 +85,12 @@ class BaseMapPageState extends State { CustomInfoWindow( controller: _customInfoWindowControllerSignal.value, ), - CustomFloatingActionButtonList(), - const CustomHeader(), + CustomFloatingActionButtonList( + baseMapController: _baseMapController, + ), + CustomHeader( + baseMapController: _baseMapController, + ), _draggableScrollableSheetSignal.value, ], ), diff --git a/spotholes_android/lib/services/location_service.dart b/spotholes_android/lib/services/location_service.dart index 0ed8e7a..19330ed 100644 --- a/spotholes_android/lib/services/location_service.dart +++ b/spotholes_android/lib/services/location_service.dart @@ -16,9 +16,12 @@ class LocationService { final _location = Location(); final _currentLocationSignal = Signal(null); - get currentLocationSignal => _currentLocationSignal; - get currentLocationLatLng => LatLng(_currentLocationSignal.value!.latitude!, - _currentLocationSignal.value!.longitude!); + Signal get currentLocationSignal => _currentLocationSignal; + + LatLng get currentLocationLatLng => LatLng( + _currentLocationSignal.value!.latitude!, + _currentLocationSignal.value!.longitude!, + ); String currentLocationLatLngURLPattern() => "${_currentLocationSignal.value!.latitude!.toString()}" diff --git a/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart b/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart index ca5daea..ed1e25d 100644 --- a/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart +++ b/spotholes_android/lib/widgets/button/custom_floating_action_button_list.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; import 'package:signals/signals_flutter.dart'; -import 'package:spotholes_android/controllers/base_map_controller.dart'; +import '../../controllers/base_map_controller.dart'; import '../../utilities/custom_icons.dart'; import 'custom_floating_action_button.dart'; class CustomFloatingActionButtonList extends StatelessWidget { - CustomFloatingActionButtonList({super.key}); - final _baseMapController = BaseMapController.instance; - late final _isTrackingLocation = _baseMapController.isTrackingLocation; + final BaseMapController baseMapController; + late final _isTrackingLocation = baseMapController.isTrackingLocation; + + CustomFloatingActionButtonList({super.key, required this.baseMapController}); void trackingLocation() { - _baseMapController.trackLocation(); + baseMapController.trackLocation(); } @override @@ -27,12 +28,12 @@ class CustomFloatingActionButtonList extends StatelessWidget { children: [ CustomFloatingActionButton( tooltip: "Adicionar um risco", - onPressed: () => _baseMapController.registerSpotholeModal(), + onPressed: () => baseMapController.registerSpotholeModal(), icon: CustomIcons.potholeAddIcon), const SizedBox(height: 10), CustomFloatingActionButton( tooltip: "Sincronizar os riscos", - onPressed: () => _baseMapController.loadSpotholeMarkers(), + onPressed: () => baseMapController.loadSpotholeMarkers(), icon: const Icon(Icons.sync)), const SizedBox(height: 10), Watch( diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart index 76da10e..eee3c57 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet_type.dart @@ -1,40 +1,50 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:spotholes_android/package/google_places_flutter/model/place_details.dart'; +import '../../controllers/base_map_controller.dart'; +import '../../package/google_places_flutter/model/place_details.dart'; import 'location_draggable_sheet.dart'; import 'main_draggable_sheet.dart'; import 'place_draggable_sheet.dart'; class DraggableScrollableSheetType { final Widget widget; - const DraggableScrollableSheetType({required this.widget}); } class DraggableScrollableSheetTypes { - static final initial = DraggableScrollableSheetType( - widget: MainDraggableSheet(controller: MainDraggableSheetController()), - ); + final BaseMapController baseMapController; + DraggableScrollableSheetTypes({required this.baseMapController}); + + DraggableScrollableSheetType get initial => DraggableScrollableSheetType( + widget: MainDraggableSheet( + controller: MainDraggableSheetController(), + baseMapController: baseMapController, + ), + ); static DraggableScrollableSheetType place( - {required PlaceDetails placeDetails, required LatLng position}) { + {required PlaceDetails placeDetails, + required LatLng position, + required BaseMapController baseMapController}) { return DraggableScrollableSheetType( widget: PlaceDraggableSheet( placeDetails: placeDetails, - controller: PlaceDraggableSheetController(), position: position, + baseMapController: baseMapController, ), ); } static DraggableScrollableSheetType location( - {required LatLng position, required formattedPlacemark}) { + {required LatLng position, + required formattedPlacemark, + required BaseMapController baseMapController}) { return DraggableScrollableSheetType( widget: LocationDraggableSheet( - controller: LocationDraggableSheetController(), position: position, formattedPlacemark: formattedPlacemark, + baseMapController: baseMapController, ), ); } diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart index c260c30..588e4eb 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:spotholes_android/utilities/app_routes.dart'; import '../../controllers/base_map_controller.dart'; import '../../main.dart'; +import '../../utilities/app_routes.dart'; import '../button/custom_button.dart'; class LocationDraggableSheetController { @@ -15,25 +15,25 @@ class LocationDraggableSheetController { } class LocationDraggableSheet extends StatefulWidget { + final LatLng position; + final String formattedPlacemark; + final BaseMapController baseMapController; + const LocationDraggableSheet({ super.key, - required this.controller, required this.position, required this.formattedPlacemark, + required this.baseMapController, }); - final LatLng position; - final LocationDraggableSheetController controller; - final String formattedPlacemark; - @override LocationDraggableSheetState createState() => LocationDraggableSheetState(); } class LocationDraggableSheetState extends State { - late LocationDraggableSheetController controller; + // late LocationDraggableSheetController controller; final scrollController = ScrollController(); - final _baseMapController = BaseMapController.instance; + late final _baseMapController = widget.baseMapController; late final _canvasColor = Theme.of(context).canvasColor; List _horizontalListButtons() { diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart index eebdc02..94f5e88 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/main_draggable_sheet.dart @@ -12,9 +12,14 @@ class MainDraggableSheetController { } class MainDraggableSheet extends StatefulWidget { - const MainDraggableSheet({super.key, required this.controller}); - final MainDraggableSheetController controller; + final BaseMapController baseMapController; + + const MainDraggableSheet({ + super.key, + required this.controller, + required this.baseMapController, + }); @override MainDraggableSheetState createState() => MainDraggableSheetState(); @@ -23,7 +28,7 @@ class MainDraggableSheet extends StatefulWidget { class MainDraggableSheetState extends State { late ScrollController scrollController; late MainDraggableSheetController mainDraggableSheetController; - final _baseMapController = BaseMapController.instance; + late final _baseMapController = widget.baseMapController; late final _canvasColor = Theme.of(context).canvasColor; String _data = ""; diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart index 19cd3b7..85a57e0 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:spotholes_android/package/google_places_flutter/model/place_details.dart'; -import 'package:spotholes_android/utilities/app_routes.dart'; import '../../controllers/base_map_controller.dart'; import '../../main.dart'; +import '../../package/google_places_flutter/model/place_details.dart'; +import '../../utilities/app_routes.dart'; import '../button/custom_button.dart'; class PlaceDraggableSheetController { @@ -16,29 +16,32 @@ class PlaceDraggableSheetController { } class PlaceDraggableSheet extends StatefulWidget { - const PlaceDraggableSheet( - {super.key, - required this.controller, - required this.position, - required this.placeDetails}); - - final PlaceDraggableSheetController controller; final LatLng position; final PlaceDetails placeDetails; + final BaseMapController baseMapController; + + const PlaceDraggableSheet({ + super.key, + required this.position, + required this.placeDetails, + required this.baseMapController, + }); + @override PlaceDraggableSheetState createState() => PlaceDraggableSheetState(); } class PlaceDraggableSheetState extends State { final ScrollController scrollController = ScrollController(); - final _baseMapController = BaseMapController.instance; + late final baseMapController = widget.baseMapController; + late final _canvasColor = Theme.of(context).canvasColor; _loadRoute(position) { MyApp.navigatorKey.currentState?.pushNamed( AppRoutes.route, - arguments: [_baseMapController.currentLocationLatLng, position], + arguments: [baseMapController.currentLocationLatLng, position], ); } @@ -52,13 +55,13 @@ class PlaceDraggableSheetState extends State { CustomButton( label: 'Alertar', onPressed: () => - _baseMapController.registerSpotholeModal(position: widget.position), + baseMapController.registerSpotholeModal(position: widget.position), ), ]; } void closeDraggable() { - _baseMapController.closeDraggableSheet('selectedPlace'); + baseMapController.closeDraggableSheet('selectedPlace'); } @override diff --git a/spotholes_android/lib/widgets/search_bar.dart b/spotholes_android/lib/widgets/search_bar.dart index e9f839e..dc46d58 100644 --- a/spotholes_android/lib/widgets/search_bar.dart +++ b/spotholes_android/lib/widgets/search_bar.dart @@ -1,19 +1,23 @@ import 'package:flutter/material.dart'; -import 'package:spotholes_android/controllers/base_map_controller.dart'; import '../config/environment_config.dart'; +import '../controllers/base_map_controller.dart'; import '../package/google_places_flutter/google_places_flutter.dart'; import '../package/google_places_flutter/model/place_details.dart'; import '../package/google_places_flutter/model/prediction.dart'; import '../services/location_service.dart'; class CustomHeader extends StatelessWidget { - const CustomHeader({super.key}); + final BaseMapController baseMapController; + const CustomHeader({super.key, required this.baseMapController}); + @override Widget build(BuildContext context) { - return const Column( + return Column( children: [ - CustomSearchContainer(), + CustomSearchContainer( + baseMapController: baseMapController, + ), // CustomSearchCategories(), ], ); @@ -21,7 +25,8 @@ class CustomHeader extends StatelessWidget { } class CustomSearchContainer extends StatelessWidget { - const CustomSearchContainer({super.key}); + final BaseMapController baseMapController; + const CustomSearchContainer({super.key, required this.baseMapController}); @override Widget build(BuildContext context) { @@ -36,7 +41,9 @@ class CustomSearchContainer extends StatelessWidget { ), child: Row( children: [ - CustomTextField(), + CustomTextField( + baseMapController: baseMapController, + ), // const Icon(Icons.mic), // const SizedBox(width: 16), // const CustomUserAvatar(), @@ -49,11 +56,12 @@ class CustomSearchContainer extends StatelessWidget { } class CustomTextField extends StatelessWidget { - CustomTextField({super.key}); - final _baseMapController = BaseMapController.instance; + final BaseMapController baseMapController; final _locationService = LocationService.instance; - late final _textEditingController = _baseMapController.textEditingController; - late final searchBarfocusNode = _baseMapController.searchBarFocusNode; + late final _textEditingController = baseMapController.textEditingController; + late final _searchBarfocusNode = baseMapController.searchBarFocusNode; + + CustomTextField({super.key, required this.baseMapController}); @override Widget build(BuildContext context) { @@ -77,7 +85,7 @@ class CustomTextField extends StatelessWidget { countries: const ["br"], isLatLngRequired: true, getPlaceDetailWithLatLng: (PlaceDetails placeDetails) { - _baseMapController.loadPlaceLocation(context, placeDetails); + baseMapController.loadPlaceLocation(context, placeDetails); }, // this callback is called when isLatLngRequired is true itemClick: (Prediction prediction) { _textEditingController.text = prediction.description!; @@ -107,7 +115,7 @@ class CustomTextField extends StatelessWidget { ); }, textInputAction: TextInputAction.search, - focusNode: searchBarfocusNode, + focusNode: _searchBarfocusNode, // if you want to add seperator between list items seperatedBuilder: const Divider(), // want to show close icon From 70bcbfb610f7d9c2a39e725571466ccc6609030a Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Fri, 27 Dec 2024 00:44:23 -0300 Subject: [PATCH 10/13] =?UTF-8?q?feat:=20Implementa=20o=20marcador=20de=20?= =?UTF-8?q?localiza=C3=A7=C3=A3o=20na=20tela=20de=20rota?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mais informações: - Separa a lógica de atualização da navegação entre o RouteController e o NavigationController. - Agora o RouteController fica responsável por monitorar e atualizar o marcador de localização e o NavigationController monitora e atualiza apenas o progresso sobre a polyline da rota e o rastreio da câmera do mapa. --- .../controllers/navigation_controller.dart | 4 -- .../lib/controllers/route_controller.dart | 67 +++++++++++++------ .../lib/services/location_service.dart | 7 +- .../lib/services/spothole_service.dart | 4 +- 4 files changed, 55 insertions(+), 27 deletions(-) diff --git a/spotholes_android/lib/controllers/navigation_controller.dart b/spotholes_android/lib/controllers/navigation_controller.dart index 8841b21..b8290a4 100644 --- a/spotholes_android/lib/controllers/navigation_controller.dart +++ b/spotholes_android/lib/controllers/navigation_controller.dart @@ -62,10 +62,6 @@ class NavigationController { final double heading = _currentLocationSignal.value!.heading!; untracked( () { - _locationService.loadCurrentLocationMark( - _routeController.markersSignal, - _routeController.customInfoWindowControllerSignal, - ); _navigationService.updateRouteStatus(currentLocation); }, ); diff --git a/spotholes_android/lib/controllers/route_controller.dart b/spotholes_android/lib/controllers/route_controller.dart index 24f3677..aec3ce1 100644 --- a/spotholes_android/lib/controllers/route_controller.dart +++ b/spotholes_android/lib/controllers/route_controller.dart @@ -10,6 +10,7 @@ import 'package:signals/signals_flutter.dart'; import '../config/environment_config.dart'; import '../models/spothole.dart'; import '../package/custom_info_window.dart'; +import '../services/location_service.dart'; import '../services/service_locator.dart'; import '../services/spothole_service.dart'; import '../utilities/constants.dart'; @@ -76,6 +77,8 @@ class RouteController { late final _startLocationLatLng = computed(() => geoCoordToLatLng(_startLocation.value!)); Computed get startLocationLatLng => _startLocationLatLng; + late final _endLocationLatLng = + computed(() => geoCoordToLatLng(_endLocation.value!)); // This variable cannot be computed because it needs to be reactive as a List var _routeStepsLatLng = Signal>([]); @@ -96,9 +99,28 @@ class RouteController { final _pageControllerSignal = Signal(PageController()); Signal get pageControllerSignal => _pageControllerSignal; + final LocationService _locationService = LocationService.instance; + late final _currentLocationSignal = _locationService.currentLocationSignal; + + static Function? _dispose; + + void listenCurrentLocation() async { + _dispose = effect(() { + if (_currentLocationSignal.value != null) { + untracked(() { + _locationService.loadCurrentLocationMark( + markersSignal, + customInfoWindowControllerSignal, + ); + }); + } + }); + } + void onMapCreated(mapController) { _customInfoWindowControllerSignal.value.googleMapController = mapController; _googleMapControllerCompleter.complete(mapController); + listenCurrentLocation(); } GeoCoord latLngToGeoCoord(LatLng latLng) => @@ -145,9 +167,10 @@ class RouteController { } void newCameraLatLngBoundsFromStep(Step currentStep) { - final startLocationLatLng = geoCoordToLatLng(currentStep.startLocation!); - final endLocationLatLng = geoCoordToLatLng(currentStep.endLocation!); - newCameraLatLngBounds([startLocationLatLng, endLocationLatLng]); + final startLocationStepLatLng = + geoCoordToLatLng(currentStep.startLocation!); + final endLocationStepLatLng = geoCoordToLatLng(currentStep.endLocation!); + newCameraLatLngBounds([startLocationStepLatLng, endLocationStepLatLng]); } void newCameraLatLngBounds(List polylineCoordinates) async { @@ -233,29 +256,31 @@ class RouteController { ); } - void loadRouteMarkers(sourceLocation, destinationLocation) { + void loadRouteMarkers(LatLng startLocation, LatLng endLocation) { Marker sourceRouteMarker = Marker( markerId: const MarkerId("sourceRoute"), icon: CustomIcons.sourceIcon, - position: sourceLocation, + position: startLocation, onTap: () => _customInfoWindowControllerSignal.value.addInfoWindow!( - const MarkerInfoWindow( - title: 'Rota', - textContent: 'Início da Rota', - ), - sourceLocation), + const MarkerInfoWindow( + title: 'Rota', + textContent: 'Início da Rota', + ), + startLocation, + ), ); Marker destinationRouteMarker = Marker( markerId: const MarkerId("destinationRoute"), icon: CustomIcons.destinationIcon, - position: destinationLocation, + position: endLocation, onTap: () => _customInfoWindowControllerSignal.value.addInfoWindow!( - const MarkerInfoWindow( - title: 'Rota', - textContent: 'Destino da Rota', - ), - destinationLocation), + const MarkerInfoWindow( + title: 'Rota', + textContent: 'Destino da Rota', + ), + endLocation, + ), ); _markersSignal.value = { @@ -267,10 +292,8 @@ class RouteController { // Update context and controllers related to the markers void updateAllRouteMarkers() { - final sourceLocation = markersSignal.value['sourceRouteMarker']!.position; - final destinationLocation = - markersSignal.value['destinationRouteMarker']!.position; - loadRouteMarkers(sourceLocation, destinationLocation); + listenCurrentLocation(); + loadRouteMarkers(_startLocationLatLng.value, _endLocationLatLng.value); _spotholeService.addSpotholeMarkers(spotholesInRouteList); } @@ -390,4 +413,8 @@ class RouteController { // copy._pageViewTypeSignal.value = _pageViewTypeSignal.value; return copy; } + + static dispose() { + _dispose!(); + } } diff --git a/spotholes_android/lib/services/location_service.dart b/spotholes_android/lib/services/location_service.dart index 19330ed..0d49cf4 100644 --- a/spotholes_android/lib/services/location_service.dart +++ b/spotholes_android/lib/services/location_service.dart @@ -2,6 +2,7 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:location/location.dart'; import 'package:signals/signals_flutter.dart'; +import '../package/custom_info_window.dart'; import '../utilities/custom_icons.dart'; import '../widgets/info_window/marker_info_window.dart'; @@ -27,8 +28,10 @@ class LocationService { "${_currentLocationSignal.value!.latitude!.toString()}" "%2C${_currentLocationSignal.value!.longitude!.toString()}"; - void loadCurrentLocationMark(Signal> markersSignal, - final customInfoWindowControllerSignal) { + void loadCurrentLocationMark( + Signal> markersSignal, + Signal customInfoWindowControllerSignal, + ) { final newMarker = Marker( markerId: const MarkerId("currentLocationMarker"), icon: CustomIcons.currentLocationIcon, diff --git a/spotholes_android/lib/services/spothole_service.dart b/spotholes_android/lib/services/spothole_service.dart index c21be6d..ec87ce7 100644 --- a/spotholes_android/lib/services/spothole_service.dart +++ b/spotholes_android/lib/services/spothole_service.dart @@ -19,7 +19,9 @@ class SpotholeService { final Signal _customInfoWindowControllerSignal; late final _spotholeInfoWindowController = SpotholeInfoWindowController( - _customInfoWindowControllerSignal, _markersSignal); + _customInfoWindowControllerSignal, + _markersSignal, + ); SpotholeService(this._markersSignal, this._customInfoWindowControllerSignal); From 36f21ba6cb856cc69cdff612ec0a9d6a68ddbd20 Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Fri, 27 Dec 2024 18:19:28 -0300 Subject: [PATCH 11/13] =?UTF-8?q?feat:=20Implementa=20o=20alertDialog=20pa?= =?UTF-8?q?ra=20quando=20a=20rota=20=C3=A9=20conclu=C3=ADda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Funcionalidade: - Implementa o alertDialog para quando a rota é concluída, tento apenas a opção de encerrar a rota. Foi incluído um botão com timer de 5 segundos para encerrar a rota. Correção de bug: - Corrige o bug de plot da polyline de manobra quando é uma curva e está na posição 1 da lista de manobras da rota. Refatorações: - Refatora o uso de context em alguns controllers para usar a variável global de contexto; - Refatora alguns métodos incluindo o tipo de dado dos parâmetros em vez de apenas dynamic; - Refatora outras partes menores de código. --- .../lib/controllers/base_map_controller.dart | 2 +- .../lib/controllers/route_controller.dart | 34 +++++++++++++++++- .../spothole_info_window_controller.dart | 18 +++++----- spotholes_android/lib/pages/route_page.dart | 35 ++++++++++++------- .../lib/services/navigation_service.dart | 7 +--- .../lib/services/spothole_service.dart | 11 +++--- .../lib/widgets/button/auto_press_button.dart | 15 ++++---- .../location_draggable_sheet.dart | 2 +- .../place_draggable_sheet.dart | 2 +- .../modal/register_spothole_modal.dart | 2 +- .../widgets/route_finished_alert_dialog.dart | 23 ++++++++++++ 11 files changed, 107 insertions(+), 44 deletions(-) create mode 100644 spotholes_android/lib/widgets/route_finished_alert_dialog.dart diff --git a/spotholes_android/lib/controllers/base_map_controller.dart b/spotholes_android/lib/controllers/base_map_controller.dart index 8e1cba4..36bcf59 100644 --- a/spotholes_android/lib/controllers/base_map_controller.dart +++ b/spotholes_android/lib/controllers/base_map_controller.dart @@ -165,7 +165,7 @@ class BaseMapController { _spotholeService.loadSpotholeMarkers(); } - void registerSpotholeModal({LatLng? position}) { + void registerSpotholeModal([LatLng? position]) { final registerPosition = position ?? currentLocationLatLng; _spotholeService.registerSpotholeModal(registerPosition); } diff --git a/spotholes_android/lib/controllers/route_controller.dart b/spotholes_android/lib/controllers/route_controller.dart index aec3ce1..8de709a 100644 --- a/spotholes_android/lib/controllers/route_controller.dart +++ b/spotholes_android/lib/controllers/route_controller.dart @@ -8,6 +8,7 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:signals/signals_flutter.dart'; import '../config/environment_config.dart'; +import '../main.dart'; import '../models/spothole.dart'; import '../package/custom_info_window.dart'; import '../services/location_service.dart'; @@ -18,6 +19,7 @@ import '../utilities/custom_icons.dart'; import '../utilities/maneuver_arrow_polyline.dart'; import '../utilities/map_utils.dart'; import '../widgets/info_window/marker_info_window.dart'; +import '../widgets/route_finished_alert_dialog.dart'; class RoutePointsData { List points; @@ -332,7 +334,7 @@ class RouteController { if (updateCamera) updateCameraGeoCoord(currentStep.startLocation!); final previousStepIndex = _auxRouteStepsLatLng.value.indexOf(currentStep) - 1; - if (previousStepIndex == 0) { + if (previousStepIndex < 0) { maneuverPoints = [sourceLocation, ...maneuverPoints]; midpointIndex = 1; } else { @@ -391,6 +393,36 @@ class RouteController { _spotholeService.registerSpotholeModal(registerPosition); } + void routeFinishedShowDialog() => showDialog( + context: MyApp.navigatorKey.currentContext!, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return RouteFinishedAlertDialogs( + onConfirm: () { + MyApp.navigatorKey.currentState + ?.popUntil((route) => route.isFirst); + }, + ); + }, + ); + + void finishNavigationRoute() async { + // Clear essential route data + _polylinesSignal.value.clear(); + _routePolylineCoordinatesSignal.value.clear(); + _routeStepsLatLng.value.clear(); + _stepsIndexes.clear(); + clearManeuverPolyline(); + routeFinishedShowDialog(); + // _markersSignal.value.clear(); + // _auxRouteStepsLatLng.value.clear(); + // _showStepsPageSignal.value = false; + // _pageViewTypeSignal.value = ''; + // _pageControllerSignal.value.dispose(); + // Force update of steps + // _routeStepsLatLng.value = [..._routeStepsLatLng.value]; + } + RouteController getCopy() { var copy = RouteController(); // Share the signals variables that need to be reactives between the RouteController instances diff --git a/spotholes_android/lib/controllers/spothole_info_window_controller.dart b/spotholes_android/lib/controllers/spothole_info_window_controller.dart index 4c75a0b..ccf9cc7 100644 --- a/spotholes_android/lib/controllers/spothole_info_window_controller.dart +++ b/spotholes_android/lib/controllers/spothole_info_window_controller.dart @@ -37,7 +37,6 @@ class SpotholeInfoWindowController { } void addSpotholeMarker(Spothole spothole) { - final context = MyApp.navigatorKey.currentContext; final marker = Marker( markerId: MarkerId(spothole.id!), icon: spothole.type == Type.deepHole @@ -48,9 +47,9 @@ class SpotholeInfoWindowController { _customInfoWindowControllerSignal.value.addInfoWindow!( SpotholeInfoWindow( editSpothole: () => editSpotholeModal( - context, spothole.id!, spothole.category, spothole.type), + spothole.id!, spothole.category, spothole.type), showDeleteSpotholeAlertDialog: () => - showDeleteSpotholeAlertDialog(context, spothole.id!), + showDeleteSpotholeAlertDialog(spothole.id!), spothole: spothole, ), spothole.position, @@ -60,7 +59,7 @@ class SpotholeInfoWindowController { _markersSignal.value[spothole.id!] = marker; } - void editSpothole(context, spotholeId, riskCategory, type) async { + void editSpothole(String spotholeId, Category riskCategory, Type type) async { final dateOfUpdate = DateTime.now().toUtc(); final spotholeRef = databaseReference.ref.child('spotholes/$spotholeId'); final event = await spotholeRef.once(); @@ -77,7 +76,9 @@ class SpotholeInfoWindowController { spotholeRef.set(spothole.toJson()); } - void editSpotholeModal(context, spotholeId, riskCategory, riskType) { + void editSpotholeModal( + String spotholeId, Category riskCategory, Type riskType) { + final context = MyApp.navigatorKey.currentContext!; showModalBottomSheet( context: context, builder: (builder) { @@ -85,8 +86,8 @@ class SpotholeInfoWindowController { title: "Para editar um risco, selecione:", textOnRegisterButton: "Editar", isCountdown: false, - onRegister: (riskCategory, type) => - editSpothole(context, spotholeId, riskCategory, type), + onRegister: (Category riskCategory, Type type) => + editSpothole(spotholeId, riskCategory, type), riskCategory: riskCategory, riskType: riskType, ); @@ -94,7 +95,8 @@ class SpotholeInfoWindowController { ); } - void showDeleteSpotholeAlertDialog(context, String spotholeId) { + void showDeleteSpotholeAlertDialog(String spotholeId) { + final context = MyApp.navigatorKey.currentContext!; showDialog( context: context, builder: (BuildContext dialogContext) { diff --git a/spotholes_android/lib/pages/route_page.dart b/spotholes_android/lib/pages/route_page.dart index 8aa436c..7cd6dfb 100644 --- a/spotholes_android/lib/pages/route_page.dart +++ b/spotholes_android/lib/pages/route_page.dart @@ -126,7 +126,7 @@ class _RoutePageState extends State with RouteAware { centerTitle: true, title: !_showStepsPageSignal.value ? Text( - "Seu local ➞ Destino", + "Rota", style: Theme.of(context).textTheme.titleLarge, ) : Text( @@ -145,18 +145,27 @@ class _RoutePageState extends State with RouteAware { color: Theme.of(context).focusColor, child: Column( children: [ - Text( - 'Via: ${_route.value.summary!}', - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - Text( - '${_leg.value.distance!.text} (${_leg.value.duration!.text})', - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ), + if (_route.value.summary!.isNotEmpty) + Text( + 'Via: ${_route.value.summary!}', + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.two_wheeler), + const SizedBox(width: 8.0), + Text( + '${_leg.value.distance!.text} (${_leg.value.duration!.text})', + style: + Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ], + ) ], ), ), diff --git a/spotholes_android/lib/services/navigation_service.dart b/spotholes_android/lib/services/navigation_service.dart index bc48e48..0deb359 100644 --- a/spotholes_android/lib/services/navigation_service.dart +++ b/spotholes_android/lib/services/navigation_service.dart @@ -136,12 +136,7 @@ class NavigationService { _routePolylineCoordinatesSignal.value.length < 3) { _countOutOfRoute = 0; debugPrint('---Cheguei no final'); - _stepsIndexes.clear(); - _routeStepsLatLng.value.clear(); - _routePolylineCoordinatesSignal.value.clear(); - _routeController.clearManeuverPolyline(); - // Força update dos steps - _routeStepsLatLng.value = [..._routeStepsLatLng.value]; + _routeController.finishNavigationRoute(); } else { _countOutOfRoute += 1; // Recalcula a rota após 5 movimentos consecutivos fora da rota (considerando a margem de tolerâcia em metros) diff --git a/spotholes_android/lib/services/spothole_service.dart b/spotholes_android/lib/services/spothole_service.dart index ec87ce7..5b0b4f0 100644 --- a/spotholes_android/lib/services/spothole_service.dart +++ b/spotholes_android/lib/services/spothole_service.dart @@ -75,7 +75,7 @@ class SpotholeService { _markersSignal.value = {..._markersSignal.value}; } - void registerSpothole(LatLng position, category, type) { + void registerSpothole(LatLng position, Category category, Type type) { final newSpotHoleRef = dataBaseSpotholesRef.push(); final newSpothole = Spothole( DateTime.now().toUtc(), @@ -91,7 +91,7 @@ class SpotholeService { _markersSignal.value = {..._markersSignal.value}; } - void registerSpotholeModal(position) { + void registerSpotholeModal(LatLng position) { final context = MyApp.navigatorKey.currentContext; showModalBottomSheet( context: context!, @@ -99,8 +99,11 @@ class SpotholeService { return RegisterSpotholeModal( title: "Para alertar um risco, selecione:", textOnRegisterButton: "Adicionar", - onRegister: (riskCategory, type) => - registerSpothole(position, riskCategory, type), + onRegister: ( + [Category riskCategory = Category.unitary, + Type type = Type.pothole]) { + registerSpothole(position, riskCategory, type); + }, ); }, ); diff --git a/spotholes_android/lib/widgets/button/auto_press_button.dart b/spotholes_android/lib/widgets/button/auto_press_button.dart index 97212d2..abc61af 100644 --- a/spotholes_android/lib/widgets/button/auto_press_button.dart +++ b/spotholes_android/lib/widgets/button/auto_press_button.dart @@ -1,16 +1,14 @@ import 'package:flutter/material.dart'; -import '../../models/spothole.dart'; - class AutoPressButton extends StatefulWidget { final String textOnRegisterButton; final int timerInSeconds; - final Function onRegister; + final Function onPressButton; const AutoPressButton( {super.key, required this.textOnRegisterButton, - required this.onRegister, + required this.onPressButton, this.timerInSeconds = 10}); @override @@ -22,9 +20,10 @@ class AutoPressButtonState extends State late AnimationController _animationController; late Animation _animation; - autoRegisterSpothole() { - widget.onRegister(Category.unitary, Type.pothole); + pressButton() { + // Ther order of the next two lines is important Navigator.pop(context); + widget.onPressButton(); } @override @@ -40,7 +39,7 @@ class AutoPressButtonState extends State ); _animationController.addStatusListener((AnimationStatus status) { if (status == AnimationStatus.completed) { - autoRegisterSpothole(); + pressButton(); } }); _animationController.forward(); @@ -70,7 +69,7 @@ class AutoPressButtonState extends State ), GestureDetector( onTap: () { - autoRegisterSpothole(); + pressButton(); }, child: Container( width: 100, diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart index 588e4eb..8a622e3 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/location_draggable_sheet.dart @@ -52,7 +52,7 @@ class LocationDraggableSheetState extends State { CustomButton( label: 'Alertar', onPressed: () => _baseMapController.registerSpotholeModal( - position: widget.position, + widget.position, ), ), ]; diff --git a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart index 85a57e0..125634e 100644 --- a/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart +++ b/spotholes_android/lib/widgets/draggable_scrollable_sheet/place_draggable_sheet.dart @@ -55,7 +55,7 @@ class PlaceDraggableSheetState extends State { CustomButton( label: 'Alertar', onPressed: () => - baseMapController.registerSpotholeModal(position: widget.position), + baseMapController.registerSpotholeModal(widget.position), ), ]; } diff --git a/spotholes_android/lib/widgets/modal/register_spothole_modal.dart b/spotholes_android/lib/widgets/modal/register_spothole_modal.dart index b6131ee..4433413 100644 --- a/spotholes_android/lib/widgets/modal/register_spothole_modal.dart +++ b/spotholes_android/lib/widgets/modal/register_spothole_modal.dart @@ -121,7 +121,7 @@ class RegisterSpotholeModalState extends State { children: [ if (widget.isCountdown) AutoPressButton( - onRegister: widget.onRegister, + onPressButton: widget.onRegister, textOnRegisterButton: widget.textOnRegisterButton, timerInSeconds: widget.timerInSeconds, ) diff --git a/spotholes_android/lib/widgets/route_finished_alert_dialog.dart b/spotholes_android/lib/widgets/route_finished_alert_dialog.dart new file mode 100644 index 0000000..fb4c138 --- /dev/null +++ b/spotholes_android/lib/widgets/route_finished_alert_dialog.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'button/auto_press_button.dart'; + +class RouteFinishedAlertDialogs extends StatelessWidget { + final VoidCallback onConfirm; + + const RouteFinishedAlertDialogs({super.key, required this.onConfirm}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Confirmação'), + content: const Text('Trajeto concluído com sucesso!'), + actions: [ + AutoPressButton( + textOnRegisterButton: 'Concluir', + onPressButton: onConfirm, + ) + ], + ); + } +} From ba250dadab65e3444f9330a63f8d4c4c55d20ed7 Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Fri, 27 Dec 2024 22:23:49 -0300 Subject: [PATCH 12/13] =?UTF-8?q?Chore:=20Remove=20o=20pubspec.lock=20do?= =?UTF-8?q?=20controle=20de=20vers=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++++- spotholes_android/pubspec.lock | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8492ff2..4d87345 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ .idea # Environment -.env \ No newline at end of file +.env + +# Project related +pubspec.lock \ No newline at end of file diff --git a/spotholes_android/pubspec.lock b/spotholes_android/pubspec.lock index e9342ac..9423323 100644 --- a/spotholes_android/pubspec.lock +++ b/spotholes_android/pubspec.lock @@ -680,6 +680,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + maps_toolkit: + dependency: "direct main" + description: + name: maps_toolkit + sha256: "277877f9505208acacd2a0794ef190e836a5ffee58ebc8efc5b9ca8de50e3e2f" + url: "https://pub.dev" + source: hosted + version: "3.0.0" matcher: dependency: transitive description: From 6e8278e1e580f38be7695a7a1e35fe3229a0924d Mon Sep 17 00:00:00 2001 From: Diego Feitoza <19846327+DiFeitoza@users.noreply.github.com> Date: Fri, 27 Dec 2024 23:21:16 -0300 Subject: [PATCH 13/13] =?UTF-8?q?refator:=20Traduz=20coment=C3=A1rios=20pa?= =?UTF-8?q?ra=20o=20ingl=C3=AAs=20e=20aplica=20a=20nota=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20documenta=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/navigation_controller.dart | 4 +- .../lib/controllers/route_controller.dart | 49 ++++++++++++------- spotholes_android/lib/main.dart | 4 +- .../lib/pages/navigation_page.dart | 2 +- spotholes_android/lib/pages/route_page.dart | 4 +- .../lib/services/location_service.dart | 2 +- .../lib/services/navigation_service.dart | 41 ++++++++-------- .../lib/services/service_locator.dart | 2 +- .../lib/utilities/constants.dart | 2 +- .../lib/utilities/location_service_mock.dart | 2 +- .../utilities/maneuver_arrow_polyline.dart | 17 ++++--- .../utilities/point_on_route_haversine.dart | 17 ++++--- .../lib/widgets/button/auto_press_button.dart | 2 +- .../widgets/nav_route_steps_page_view.dart | 10 ++-- spotholes_android/lib/widgets/search_bar.dart | 6 +-- 15 files changed, 91 insertions(+), 73 deletions(-) diff --git a/spotholes_android/lib/controllers/navigation_controller.dart b/spotholes_android/lib/controllers/navigation_controller.dart index b8290a4..d01bb01 100644 --- a/spotholes_android/lib/controllers/navigation_controller.dart +++ b/spotholes_android/lib/controllers/navigation_controller.dart @@ -37,7 +37,7 @@ class NavigationController { } void onTrackLocation() { - // Alterna a ação entre rastrear e não rastrear + /// Alternates the action between tracking and not tracking if (isTrackingLocation.value) { isTrackingLocation.value = false; } else { @@ -98,7 +98,7 @@ class NavigationController { _routeController.registerSpotholeModal(registerPosition); } - // TODO Limitar o número de requisições por minuto para evitar uso indevido, talvez um debaunce + // TODO Limit the number of requests per minute to avoid misuse, maybe a debounce void recalculateRoute() { _routeController.recalculateRoute(_locationService.currentLocationLatLng); _locationService.loadCurrentLocationMark( diff --git a/spotholes_android/lib/controllers/route_controller.dart b/spotholes_android/lib/controllers/route_controller.dart index 8de709a..ef14abb 100644 --- a/spotholes_android/lib/controllers/route_controller.dart +++ b/spotholes_android/lib/controllers/route_controller.dart @@ -45,24 +45,24 @@ class RouteController { _customInfoWindowControllerSignal, ); - // Store all markers + /// Store all markers var _markersSignal = Signal>({}); Signal> get markersSignal => _markersSignal; - // Store all spotholes in route + /// Store all spotholes in route var _spotholesInRouteList = Signal>([]); Signal> get spotholesInRouteList => _spotholesInRouteList; - // Store all polylines + /// Store all polylines var _polylinesSignal = Signal>({}); Signal> get polylinesSignal => _polylinesSignal; - // Store all route polyline points + /// Store all route polyline points var _routePolylineCoordinatesSignal = Signal>([]); Signal> get routePolylineCoordinatesSignal => _routePolylineCoordinatesSignal; - // Store a result from a request for a route on Google Directions API + /// Store a result from a request for a route on Google Directions API var _directionResult = Signal(const DirectionsResult()); Signal get directionResult => _directionResult; @@ -82,13 +82,13 @@ class RouteController { late final _endLocationLatLng = computed(() => geoCoordToLatLng(_endLocation.value!)); - // This variable cannot be computed because it needs to be reactive as a List + /// This variable cannot be computed because it needs to be reactive as a List var _routeStepsLatLng = Signal>([]); Signal> get routeStepsLatLng => _routeStepsLatLng; - // This variable is just an independent copy of the routeStepsLatLng to mantain the original data + /// This variable is just an independent copy of the routeStepsLatLng to mantain the original data var _auxRouteStepsLatLng = Signal>([]); - // Store steps' start indexes inside a route polyline + /// Store steps' start indexes inside a route polyline var _stepsIndexes = []; List get stepsIndexes => _stepsIndexes; @@ -252,7 +252,7 @@ class RouteController { _spotholeService.loadSpotholesInRoute( routePolylineCoordinatesSignal.value, _spotholesInRouteList); } else { - // do something with error response + // TODO do something with error response } }, ); @@ -292,7 +292,7 @@ class RouteController { }; } - // Update context and controllers related to the markers + /// Update context and controllers related to the markers void updateAllRouteMarkers() { listenCurrentLocation(); loadRouteMarkers(_startLocationLatLng.value, _endLocationLatLng.value); @@ -376,7 +376,7 @@ class RouteController { } void updateRoutePolyline() { - // Modifica a polyline da rota + /// Modifies the polyline of the route polylinesSignal.value['route'] = Polyline( polylineId: const PolylineId("route"), points: _routePolylineCoordinatesSignal.value, @@ -385,7 +385,7 @@ class RouteController { geodesic: true, jointType: JointType.round, ); - // Força o update das polylines da rota + /// Forces the update of the route polylines polylinesSignal.value = {...polylinesSignal.value}; } @@ -407,7 +407,7 @@ class RouteController { ); void finishNavigationRoute() async { - // Clear essential route data + /// Clear essential route data _polylinesSignal.value.clear(); _routePolylineCoordinatesSignal.value.clear(); _routeStepsLatLng.value.clear(); @@ -419,13 +419,30 @@ class RouteController { // _showStepsPageSignal.value = false; // _pageViewTypeSignal.value = ''; // _pageControllerSignal.value.dispose(); - // Force update of steps + /// Force update of steps // _routeStepsLatLng.value = [..._routeStepsLatLng.value]; } + /// Creates a copy of the current `RouteController` instance. + /// + /// The copied instance shares the reactive signal variables with the original instance. + /// This includes: + /// - `_markersSignal`: Signal for markers. + /// - `_spotholesInRouteList`: List of spotholes in the route. + /// - `_polylinesSignal`: Signal for polylines. + /// - `_routePolylineCoordinatesSignal`: Signal for route polyline coordinates. + /// - `_directionResult`: Result of the direction. + /// - `_routeStepsLatLng`: LatLng coordinates of the route steps. + /// - `_auxRouteStepsLatLng`: Auxiliary LatLng coordinates of the route steps. + /// - `_stepsIndexes`: Indexes of the steps. + /// + /// Note: Controllers and other variables that are initialized as null in the `RouteController` + /// are not copied to avoid duplication errors. Each page requires a unique controller per widget. + /// + /// Returns: + /// A new `RouteController` instance with shared reactive signal variables. RouteController getCopy() { var copy = RouteController(); - // Share the signals variables that need to be reactives between the RouteController instances copy._markersSignal = _markersSignal; copy._spotholesInRouteList = _spotholesInRouteList; copy._polylinesSignal = _polylinesSignal; @@ -434,8 +451,6 @@ class RouteController { copy._routeStepsLatLng = _routeStepsLatLng; copy._auxRouteStepsLatLng = _auxRouteStepsLatLng; copy._stepsIndexes = _stepsIndexes; - // Removi todos os casos de valores que são inicializados nulo no RouteController - // Principalmente controlladores, pois cada página precisa de um único por widget evitando erros por duplicidade. // copy._pageControllerSignal.value = PageController(initialPage: 1); // copy._googleMapController = null; // copy._customInfoWindowControllerSignal.value = diff --git a/spotholes_android/lib/main.dart b/spotholes_android/lib/main.dart index 18eaeb4..3d7ea56 100644 --- a/spotholes_android/lib/main.dart +++ b/spotholes_android/lib/main.dart @@ -28,7 +28,7 @@ void main() async { class MyApp extends StatelessWidget { const MyApp({super.key}); - // Cria uma chave global para o Navigator, permitindo a consulta do context global fora da árvore (solução para controllers) + /// Creates a global key for the Navigator, allowing the global context to be accessed outside the widget tree (solution for controllers). static final GlobalKey navigatorKey = GlobalKey(); @@ -38,7 +38,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - // Implements the AppNavigatorObserver to handle all route changes + /// Implements the AppNavigatorObserver to handle all route changes // navigatorObservers: [AppNavigatorObserver()], navigatorObservers: [routeObserver], navigatorKey: navigatorKey, diff --git a/spotholes_android/lib/pages/navigation_page.dart b/spotholes_android/lib/pages/navigation_page.dart index e1b36b3..b33660e 100644 --- a/spotholes_android/lib/pages/navigation_page.dart +++ b/spotholes_android/lib/pages/navigation_page.dart @@ -53,7 +53,7 @@ class _NavigationPageState extends State { Widget build(BuildContext context) { return Watch( (context) => PopScope( - //TODO definir ação do popScope para gerar o alertDialog! + // TODO define PopScope action to generate the alertDialog! child: Scaffold( backgroundColor: context.isDarkMode ? Colors.black : Colors.white, body: SafeArea( diff --git a/spotholes_android/lib/pages/route_page.dart b/spotholes_android/lib/pages/route_page.dart index 7cd6dfb..b53c9aa 100644 --- a/spotholes_android/lib/pages/route_page.dart +++ b/spotholes_android/lib/pages/route_page.dart @@ -83,9 +83,9 @@ class _RoutePageState extends State with RouteAware { @override void didPopNext() { _routeController.centerViewRoute(); - // Avoid exception when the user returns from the navigation page and try access the info window + /// Avoid exception when the user returns from the navigation page and try access the info window _customInfoWindowControllerSignal.value.hideInfoWindow!(); - // Update markers when the user returns from the navigation page changing the customInfoWindowController with the new mapController + /// Update markers when the user returns from the navigation page changing the customInfoWindowController with the new mapController _routeController.updateAllRouteMarkers(); } diff --git a/spotholes_android/lib/services/location_service.dart b/spotholes_android/lib/services/location_service.dart index 0d49cf4..5e1f534 100644 --- a/spotholes_android/lib/services/location_service.dart +++ b/spotholes_android/lib/services/location_service.dart @@ -56,7 +56,7 @@ class LocationService { distanceFilter: 5, ); _currentLocationSignal.value = await _location.getLocation(); - //TODO Implementar Snackbar alertando que o GPS está fora. Pode ser um signal que exibe o snackbar na tela com o ícone de GPS fora + // TODO Implement Snackbar alerting that GPS is off. It can be a signal that displays the snackbar on the screen with the GPS off icon _location.onLocationChanged.listen( (newLoc) { _currentLocationSignal.value = newLoc; diff --git a/spotholes_android/lib/services/navigation_service.dart b/spotholes_android/lib/services/navigation_service.dart index 0deb359..09d8b34 100644 --- a/spotholes_android/lib/services/navigation_service.dart +++ b/spotholes_android/lib/services/navigation_service.dart @@ -77,8 +77,8 @@ class NavigationService { } void updateRouteStatus(LatLng currentLocation) async { - // TODO Verificar se há outras situações de fim da rota - // Verifica se a rota está vazia, por exemplo: trajeto concluído. + // TODO Check if there are other end-of-route situations + /// Checks if the route is empty, for example: route completed. if (_routeStepsLatLng.value.isEmpty) { return; } @@ -87,61 +87,62 @@ class NavigationService { mtk.LatLng currentLocationMtk = locationToMtkLatLng(currentLocation); int index = locationIndexOnPath(currentLocationMtk, routePointsMtk); debugPrint('----index on polyline: $index'); - // Se a localização atual está na rota + /// If the current location is on the route if (index > 0) { _countOutOfRoute = 0; - // Define a posição inicial para a localização atual + /// Set the initial position to the current location routePointsMtk[index] = currentLocationMtk; - // Remove os pontos iniciais até a posição atual + /// Remove the initial points up to the current position routePointsMtk.removeRange(0, index + 1); _routePolylineCoordinatesSignal.value.removeRange(0, index + 1); _routePolylineCoordinatesSignal.value[0] = currentLocation; _routeController.updateRoutePolyline(); - // Atualiza o contador de pontos descartados + /// Update the discarded points counter _discardedPointsCounter += index + 1; - debugPrint('Pontos a descartar $_discardedPointsCounter'); - // Verifica o step atual na rota + debugPrint('Points to discard $_discardedPointsCounter'); + /// Check the current step on the route int currentStepIndex = verifyStep(); - // Se o step avançou, o pageview é atualizado + /// If the step has advanced, the pageview is updated if (currentStepIndex > 0 && _pageController.hasClients) { - // Atualiza a polyline que representa a seta de manobra no mapa. Precisa ser feito antes de remover os steps + /// Update the polyline that represents the maneuver arrow on the map. Needs to be done before removing the steps if (_isTrackingLocation.value || _pageController.page == 1) { _routeController.plotManeuverPolyline(currentStepIndex, updateCamera: false); } - // Remove os steps que já passaram + /// Remove the steps that have passed _stepsIndexes.removeRange(0, currentStepIndex); _routeStepsLatLng.value.removeRange(0, currentStepIndex); final stepsLength = _routeStepsLatLng.value.length; final totalRemovedSteps = currentStepIndex; final page = _pageController.page!.toInt(); - // Verifica se o movimento de retorno do pageView termina no máximo na página 01 (step 0), se a página atual está entre a página 01 e a penúltima página (dentro da lista de steps) + /// Check if the pageView return movement ends at most on page 01 (step 0) + /// if the current page is between page 01 and the penultimate page (within the steps list) if (totalRemovedSteps < page && page > 1 && page < stepsLength + 2) { _isPageViewUpdateCamera.value = false; _pageController.jumpToPage(page - totalRemovedSteps); } else { - // É necessário atualizar para forçar a renderização do widget, porém, assim evita duplicação do update porque o jumpToPage invoca um método que faz update da lista + /// It is necessary to update to force the widget to render, however, this avoids duplication of the update because the jumpToPage invokes a method that updates the list _routeStepsLatLng.value = [..._routeStepsLatLng.value]; } debugPrint('---pages: ${_pageController.page} $currentStepIndex'); } - // Caso esteja entre a posição 0 e 1 da polyline (index == 0), então a polyline é atualizada + /// If it is between position 0 and 1 of the polyline (index == 0), then the polyline is updated } else if (index == 0) { _countOutOfRoute = 0; _routePolylineCoordinatesSignal.value[0] = currentLocation; _routeController.updateRoutePolyline(); - // Caso seja o último step e tenha menos que 3 pontos, então descarta o último step, pontos e conclui a rota + /// If it is the last step and has less than 3 points, then discard the last step, points and complete the route } else if (_routeStepsLatLng.value.length == 1 && _routePolylineCoordinatesSignal.value.length < 3) { _countOutOfRoute = 0; - debugPrint('---Cheguei no final'); + debugPrint('---Reached the end'); _routeController.finishNavigationRoute(); } else { _countOutOfRoute += 1; - // Recalcula a rota após 5 movimentos consecutivos fora da rota (considerando a margem de tolerâcia em metros) - // Apenas recalcula a rota 3 vezes de forma automática, evitando falhas que gerem muitos recálculos - // TODO Criar Snackbar para avisar que ultrapassou o limite de 3 vezes, perguntando se quer recalcular de forma manual, caso sim, mais 3 automáticos + /// Recalculate the route after 5 consecutive movements off the route (considering the tolerance margin in meters) + /// Only recalculate the route 3 times automatically, avoiding failures that generate many recalculations + // TODO Create Snackbar to warn that the limit of 3 times has been exceeded, asking if you want to recalculate manually, if yes, 3 more automatic recalculations if (_countOutOfRoute > 5 && _countRecalculatedRoute <= 3) { _countOutOfRoute = 0; _routeController.recalculateRoute(currentLocation); @@ -149,6 +150,6 @@ class NavigationService { } } debugPrint( - '----[Após descarte] points ${_routePolylineCoordinatesSignal.value.length} steps:${_routeStepsLatLng.value.length}'); + '----[After discard] points ${_routePolylineCoordinatesSignal.value.length} steps:${_routeStepsLatLng.value.length}'); } } diff --git a/spotholes_android/lib/services/service_locator.dart b/spotholes_android/lib/services/service_locator.dart index 5a838d8..dd2623e 100644 --- a/spotholes_android/lib/services/service_locator.dart +++ b/spotholes_android/lib/services/service_locator.dart @@ -4,7 +4,7 @@ import 'package:get_it/get_it.dart'; final getIt = GetIt.instance; void setupDependencies() { - // Setup Firebase + /// Setup Firebase getIt.registerLazySingleton(() { return FirebaseDatabase.instance.ref(); }); diff --git a/spotholes_android/lib/utilities/constants.dart b/spotholes_android/lib/utilities/constants.dart index d5825ba..7ed9935 100644 --- a/spotholes_android/lib/utilities/constants.dart +++ b/spotholes_android/lib/utilities/constants.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; const Color primaryColor = Color(0xFF7B61FF); const double defaultPadding = 16.0; -//Google Maps +// Google Maps const double defaultZoomMap = 18.5; const double defaultNavigationTilt = 90; const String mapStyle2D = '''[ diff --git a/spotholes_android/lib/utilities/location_service_mock.dart b/spotholes_android/lib/utilities/location_service_mock.dart index 66f31ad..31503d2 100644 --- a/spotholes_android/lib/utilities/location_service_mock.dart +++ b/spotholes_android/lib/utilities/location_service_mock.dart @@ -16,7 +16,7 @@ class LocationServiceMock { // final List> _simulatedPoints; - // Lista de pontos para simulação (em torno de casa) + /// List of points for simulation (around home) List> simulatedPoints = [ {"lat": -4.970759417, "lon": -39.018417578}, {"lat": -4.970149456, "lon": -39.018437396}, diff --git a/spotholes_android/lib/utilities/maneuver_arrow_polyline.dart b/spotholes_android/lib/utilities/maneuver_arrow_polyline.dart index 49cb6d3..594d5e8 100644 --- a/spotholes_android/lib/utilities/maneuver_arrow_polyline.dart +++ b/spotholes_android/lib/utilities/maneuver_arrow_polyline.dart @@ -2,7 +2,8 @@ import 'dart:math'; import 'package:google_maps_flutter/google_maps_flutter.dart'; double haversineDistance(LatLng point1, LatLng point2) { - const double earthRadius = 6371000; // Earth's radius in meters + /// Earth's radius in meters + const double earthRadius = 6371000; double dLat = (point2.latitude - point1.latitude) * pi / 180; double dLng = (point2.longitude - point1.longitude) * pi / 180; @@ -45,16 +46,16 @@ int findAndInsertMidpoint(List points) { double fraction = remainingDistance / segmentDistance; LatLng midpoint = interpolate(points[i], points[i + 1], fraction); - // Insert midpoint into list + /// Insert midpoint into list points.insert(i + 1, midpoint); - - // Return the midpoint index + ///Return the midpoint index return i + 1; } accumulatedDistance += segmentDistance; } - return points.length - 1; // If the accumulated distance is exactly half + /// If the accumulated distance is exactly half + return points.length - 1; } List getSegmentAroundMidpoint( @@ -78,7 +79,7 @@ List getSegmentAroundMidpoint( List segment = []; - // If the accumulated distance is exactly half + /// If the accumulated distance is exactly half double remainingDistanceBefore = distanceBefore; for (int i = midpointIndex; i > 0; i--) { double segmentDistance = haversineDistance(points[i], points[i - 1]); @@ -92,10 +93,10 @@ List getSegmentAroundMidpoint( } } - // Add the midpoint + /// Add the midpoint segment.add(midpoint); - // Calculate points after midpoint + /// Calculate points after midpoint double remainingDistanceAfter = distanceAfter; for (int i = midpointIndex + 1; i < points.length; i++) { double segmentDistance = haversineDistance(points[i - 1], points[i]); diff --git a/spotholes_android/lib/utilities/point_on_route_haversine.dart b/spotholes_android/lib/utilities/point_on_route_haversine.dart index ec7a80c..6234c2d 100644 --- a/spotholes_android/lib/utilities/point_on_route_haversine.dart +++ b/spotholes_android/lib/utilities/point_on_route_haversine.dart @@ -4,9 +4,10 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import '../models/spothole.dart'; -// Function to calculate the Haversine distance between two points +/// Function to calculate the Haversine distance between two points double haversine(LatLng point1, LatLng point2) { - const R = 6371000; // Earth radius in meters + /// Earth radius in meters + const R = 6371000; final phi1 = point1.latitude * pi / 180; final phi2 = point2.latitude * pi / 180; final deltaPhi = (point2.latitude - point1.latitude) * pi / 180; @@ -19,7 +20,7 @@ double haversine(LatLng point1, LatLng point2) { return R * c; } -// Function to calculate the distance from a point to a line segment +/// Function to calculate the distance from a point to a line segment double pointToSegmentDistance(LatLng point, LatLng start, LatLng end) { final A = [ point.latitude - start.latitude, @@ -44,7 +45,7 @@ double pointToSegmentDistance(LatLng point, LatLng start, LatLng end) { return haversine(point, projection); } -// Function to calculate the accumulated distance along the route +/// Function to calculate the accumulated distance along the route List calculateAccumulatedDistances(List route) { List accumulatedDistances = [0.0]; for (int i = 1; i < route.length; i++) { @@ -54,7 +55,7 @@ List calculateAccumulatedDistances(List route) { return accumulatedDistances; } -// Function to find the nearest point on the route and the accumulated distance to it +/// Function to find the nearest point on the route and the accumulated distance to it double findClosestPointDistance( LatLng point, List route, List accumulatedDistances) { double minDistance = double.infinity; @@ -71,7 +72,7 @@ double findClosestPointDistance( return closestDistance; } -// Function to verify and store accumulated distances from the points in relation to the route +/// Function to verify and store accumulated distances from the points in relation to the route List checkPointsAndStoreAccumulatedDistances( List points, List route, {double tolerance = 5.0}) { @@ -103,7 +104,7 @@ List checkPointsAndStoreAccumulatedDistances( return pointsWithinTolerance; } -// Function to check if a point is within tolerance with respect to a route +/// Function to check if a point is within tolerance with respect to a route bool isPointNearRoute(LatLng point, List route, {double tolerance = 5.0}) { for (int i = 0; i < route.length - 1; i++) { @@ -117,7 +118,7 @@ bool isPointNearRoute(LatLng point, List route, return false; } -// Function to check a list of points +/// Function to check a list of points List arePointsNearRoute(List points, List route, {double tolerance = 5.0}) { return points diff --git a/spotholes_android/lib/widgets/button/auto_press_button.dart b/spotholes_android/lib/widgets/button/auto_press_button.dart index abc61af..f56f20f 100644 --- a/spotholes_android/lib/widgets/button/auto_press_button.dart +++ b/spotholes_android/lib/widgets/button/auto_press_button.dart @@ -21,7 +21,7 @@ class AutoPressButtonState extends State late Animation _animation; pressButton() { - // Ther order of the next two lines is important + /// The order of the next two lines is important Navigator.pop(context); widget.onPressButton(); } diff --git a/spotholes_android/lib/widgets/nav_route_steps_page_view.dart b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart index 49e1c61..e49ea6d 100644 --- a/spotholes_android/lib/widgets/nav_route_steps_page_view.dart +++ b/spotholes_android/lib/widgets/nav_route_steps_page_view.dart @@ -57,7 +57,7 @@ class NavRouteStepsStatePageView extends State { _routeController.plotManeuverPolyline(stepIndex, updateCamera: true); } else { - // Caso seja uma execução que não precise atualizar a câmera, volta para o estado padrão + /// If it is an execution that does not need to update the camera, return to the default state _isPageViewUpdateCamera.value = true; _routeController.plotManeuverPolyline(stepIndex, updateCamera: false); @@ -107,7 +107,7 @@ class NavRouteStepsStatePageView extends State { controller: _pageController, itemCount: _steps.value.length + 2, itemBuilder: (context, index) { - // Se step inicial, ponto de partida + /// If initial step, starting point if (index == 0) { return Container( decoration: BoxDecoration( @@ -132,7 +132,7 @@ class NavRouteStepsStatePageView extends State { ), ), ); - // Caso seja o step do destino + /// If it is the destination step } else if (index == _steps.value.length + 1) { return Watch( (context) => Container( @@ -159,7 +159,7 @@ class NavRouteStepsStatePageView extends State { ), ), ); - // Demais steps que contêm manobras, excluindo a origem e o destino + /// Other steps that contain maneuvers, excluding origin and destination } else if (index >= 1 && index <= _steps.value.length) { final step = _steps.value[index - 1]; final maneuver = step.maneuver ?? 'straight'; @@ -210,7 +210,7 @@ class NavRouteStepsStatePageView extends State { ), ), ); - // TODO Testar: se fora do range retorna vazio, em vez de exceção. + // TODO Test: if out of range returns empty, instead of exception. } else { return const SizedBox.shrink(); } diff --git a/spotholes_android/lib/widgets/search_bar.dart b/spotholes_android/lib/widgets/search_bar.dart index dc46d58..0be0fe1 100644 --- a/spotholes_android/lib/widgets/search_bar.dart +++ b/spotholes_android/lib/widgets/search_bar.dart @@ -93,7 +93,7 @@ class CustomTextField extends StatelessWidget { TextPosition(offset: prediction.description!.length), ); }, - // if we want to make custom list item builder + /// if we want to make custom list item builder itemBuilder: (context, index, Prediction prediction) { return Container( padding: const EdgeInsets.all(10), @@ -116,9 +116,9 @@ class CustomTextField extends StatelessWidget { }, textInputAction: TextInputAction.search, focusNode: _searchBarfocusNode, - // if you want to add seperator between list items + /// if you want to add seperator between list items seperatedBuilder: const Divider(), - // want to show close icon + /// want to show close icon isCrossBtnShown: true, // place type // placeType: PlaceType.geocode,