import 'dart:async'; import 'dart:convert'; import 'package:chat/generated/i18n.dart'; import 'package:chat/map/auto_comp_iete_item.dart'; import 'package:chat/map/location_provider.dart'; import 'package:chat/map/location_result.dart'; import 'package:chat/map/map.dart'; import 'package:chat/map/nearby_place.dart'; import 'package:chat/map/rich_suggestion.dart'; import 'package:chat/map/search_input.dart'; import 'package:chat/utils/CustomUI.dart'; import 'package:chat/utils/uuid.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:http/http.dart' as http; import 'package:provider/provider.dart'; class LocationPicker extends StatefulWidget { LocationPicker( this.apiKey, { Key key, this.initialCenter, this.requiredGPS = true, }); final String apiKey; final LatLng initialCenter; final bool requiredGPS; @override LocationPickerState createState() => LocationPickerState(); /// Returns a [LatLng] object of the location that was picked. /// /// The [apiKey] argument API key generated from Google Cloud Console. /// You can get an API key [here](https://cloud.google.com/maps-platform/) /// /// [initialCenter] The geographical location that the camera is pointing at. /// static Future pickLocation( BuildContext context, String apiKey, { LatLng initialCenter = const LatLng(45.521563, -122.677433), bool requiredGPS = true, }) async { var results = await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { return LocationPicker( apiKey, initialCenter: initialCenter, requiredGPS: requiredGPS, ); }, ), ); if (results != null && results.containsKey('location')) { return results['location']; } else { return null; } } } class LocationPickerState extends State { /// Result returned after user completes selection LocationResult locationResult; /// Overlay to display autocomplete suggestions OverlayEntry overlayEntry; List nearbyPlaces = List(); /// Session token required for autocomplete API call String sessionToken = Uuid().generateV4(); var mapKey = GlobalKey(); var appBarKey = GlobalKey(); var searchInputKey = GlobalKey(); bool hasSearchTerm = false; /// Hides the autocomplete overlay void clearOverlay() { if (overlayEntry != null) { overlayEntry.remove(); overlayEntry = null; } } /// Begins the search process by displaying a "wait" overlay then /// proceeds to fetch the autocomplete list. The bottom "dialog" /// is hidden so as to give more room and better experience for the /// autocomplete list overlay. void searchPlace(String place) { if (context == null) return; clearOverlay(); setState(() => hasSearchTerm = place.length > 0); if (place.length < 1) return; final RenderBox renderBox = context.findRenderObject(); Size size = renderBox.size; final RenderBox appBarBox = appBarKey.currentContext.findRenderObject(); overlayEntry = OverlayEntry( builder: (context) => Positioned( top: appBarBox.size.height, width: size.width, child: Material( elevation: 1, child: Container( padding: EdgeInsets.symmetric( vertical: 16, horizontal: 24, ), color: Colors.white, child: Row( children: [ SizedBox( height: 24, width: 24, child: CircularProgressIndicator( strokeWidth: 3, ), ), SizedBox( width: 24, ), Expanded( child: Text( "${I18n.of(context).finding_place}...", textScaleFactor: 1.0, style: TextStyle( fontSize: 16, ), ), ) ], ), ), ), ), ); Overlay.of(context).insert(overlayEntry); autoCompleteSearch(place); } /// Fetches the place autocomplete list with the query [place]. void autoCompleteSearch(String place) { place = place.replaceAll(" ", "+"); var endpoint = "https://maps.googleapis.com/maps/api/place/autocomplete/json?" + "key=${widget.apiKey}&" + "input={$place}&sessiontoken=$sessionToken"; if (locationResult != null) { endpoint += "&location=${locationResult.latLng.latitude}," + "${locationResult.latLng.longitude}"; } http.get(endpoint).then((response) { if (response.statusCode == 200) { Map data = jsonDecode(response.body); List predictions = data['predictions']; List suggestions = []; if (predictions.isEmpty) { AutoCompleteItem aci = AutoCompleteItem(); aci.text = "No result found"; aci.offset = 0; aci.length = 0; suggestions.add(RichSuggestion(aci, () {})); } else { for (dynamic t in predictions) { AutoCompleteItem aci = AutoCompleteItem(); aci.id = t['place_id']; aci.text = t['description']; aci.offset = t['matched_substrings'][0]['offset']; aci.length = t['matched_substrings'][0]['length']; suggestions.add(RichSuggestion(aci, () { decodeAndSelectPlace(aci.id); })); } } displayAutoCompleteSuggestions(suggestions); } }).catchError((error) { print(error); }); } /// To navigate to the selected place from the autocomplete list to the map, /// the lat,lng is required. This method fetches the lat,lng of the place and /// proceeds to moving the map to that location. void decodeAndSelectPlace(String placeId) { clearOverlay(); String endpoint = "https://maps.googleapis.com/maps/api/place/details/json?key=${widget.apiKey}" + "&placeid=$placeId"; http.get(endpoint).then((response) { if (response.statusCode == 200) { Map location = jsonDecode(response.body)['result']['geometry']['location']; LatLng latLng = LatLng(location['lat'], location['lng']); moveToLocation(latLng); } }).catchError((error) { print(error); }); } /// Display autocomplete suggestions with the overlay. void displayAutoCompleteSuggestions(List suggestions) { final RenderBox renderBox = context.findRenderObject(); Size size = renderBox.size; final RenderBox appBarBox = appBarKey.currentContext.findRenderObject(); clearOverlay(); overlayEntry = OverlayEntry( builder: (context) => Positioned( width: size.width, top: appBarBox.size.height, child: Material( elevation: 1, color: Colors.white, child: Column( children: suggestions, ), ), ), ); Overlay.of(context).insert(overlayEntry); } /// Utility function to get clean readable name of a location. First checks /// for a human-readable name from the nearby list. This helps in the cases /// that the user selects from the nearby list (and expects to see that as a /// result, instead of road name). If no name is found from the nearby list, /// then the road name returned is used instead. // String getLocationName() { // if (locationResult == null) { // return "Unnamed location"; // } // // for (NearbyPlace np in nearbyPlaces) { // if (np.latLng == locationResult.latLng) { // locationResult.name = np.name; // return np.name; // } // } // // return "${locationResult.name}, ${locationResult.locality}"; // } /// Fetches and updates the nearby places to the provided lat,lng void getNearbyPlaces(LatLng latLng) { http .get("https://maps.googleapis.com/maps/api/place/nearbysearch/json?" + "key=${widget.apiKey}&" + "location=${latLng.latitude},${latLng.longitude}&radius=150") .then((response) { if (response.statusCode == 200) { nearbyPlaces.clear(); for (Map item in jsonDecode(response.body)['results']) { NearbyPlace nearbyPlace = NearbyPlace(); nearbyPlace.name = item['name']; nearbyPlace.icon = item['icon']; double latitude = item['geometry']['location']['lat']; double longitude = item['geometry']['location']['lng']; LatLng _latLng = LatLng(latitude, longitude); nearbyPlace.latLng = _latLng; nearbyPlaces.add(nearbyPlace); } } // to update the nearby places setState(() { // this is to require the result to show hasSearchTerm = false; }); }).catchError((error) {}); } /// This method gets the human readable name of the location. Mostly appears /// to be the road name and the locality. Future reverseGeocodeLatLng(LatLng latLng) async { /* var placeMarks = await Geolocator() .placemarkFromCoordinates(latLng.latitude, latLng.longitude); if (placeMarks == null) { return; } Placemark place = placeMarks.first; print('~~~~~~~~~~~~~~~~~~~~~~~~'); print(place.toString()); setState(() { locationResult = LocationResult(); locationResult.address = place.name; locationResult.latLng = LatLng(place.position.latitude, place.position.longitude); }); */ var response = await http.get( "https://maps.googleapis.com/maps/api/geocode/json?latlng=${latLng.latitude},${latLng.longitude}" "&key=${widget.apiKey}"); if (response.statusCode == 200) { Map responseJson = jsonDecode(response.body); String road = responseJson['results'][0]['address_components'][0]['short_name']; setState(() { locationResult = LocationResult(); locationResult.address = road; locationResult.latLng = latLng; }); } } /// Moves the camera to the provided location and updates other UI features to /// match the location. void moveToLocation(LatLng latLng) { mapKey.currentState.mapController.future.then((controller) { controller.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( target: latLng, zoom: 18.0, ), ), ); }); reverseGeocodeLatLng(latLng); getNearbyPlaces(latLng); } @override void dispose() { mapKey = null; appBarKey = null; clearOverlay(); super.dispose(); } @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => LocationProvider()), ], child: Builder(builder: (context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.white, iconTheme: IconThemeData(color: Colors.black), key: appBarKey, title: SearchInput( (input) => searchPlace(input), key: searchInputKey, ), leading: CustomUI.buildCustomLeading(context), ), body: MapPicker( initialCenter: widget.initialCenter, key: mapKey, apiKey: widget.apiKey, requiredGPS: widget.requiredGPS, ), ); }), ); } }