Hibok
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

408 lines
12 KiB

  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:chat/generated/i18n.dart';
  4. import 'package:chat/map/auto_comp_iete_item.dart';
  5. import 'package:chat/map/location_provider.dart';
  6. import 'package:chat/map/location_result.dart';
  7. import 'package:chat/map/map.dart';
  8. import 'package:chat/map/nearby_place.dart';
  9. import 'package:chat/map/rich_suggestion.dart';
  10. import 'package:chat/map/search_input.dart';
  11. import 'package:chat/utils/CustomUI.dart';
  12. import 'package:chat/utils/uuid.dart';
  13. import 'package:flutter/material.dart';
  14. import 'package:google_maps_flutter/google_maps_flutter.dart';
  15. import 'package:http/http.dart' as http;
  16. import 'package:provider/provider.dart';
  17. class LocationPicker extends StatefulWidget {
  18. LocationPicker(
  19. this.apiKey, {
  20. Key key,
  21. this.initialCenter,
  22. this.requiredGPS = true,
  23. });
  24. final String apiKey;
  25. final LatLng initialCenter;
  26. final bool requiredGPS;
  27. @override
  28. LocationPickerState createState() => LocationPickerState();
  29. /// Returns a [LatLng] object of the location that was picked.
  30. ///
  31. /// The [apiKey] argument API key generated from Google Cloud Console.
  32. /// You can get an API key [here](https://cloud.google.com/maps-platform/)
  33. ///
  34. /// [initialCenter] The geographical location that the camera is pointing at.
  35. ///
  36. static Future<LocationResult> pickLocation(
  37. BuildContext context,
  38. String apiKey, {
  39. LatLng initialCenter = const LatLng(45.521563, -122.677433),
  40. bool requiredGPS = true,
  41. }) async {
  42. var results = await Navigator.of(context).push(
  43. MaterialPageRoute<dynamic>(
  44. builder: (BuildContext context) {
  45. return LocationPicker(
  46. apiKey,
  47. initialCenter: initialCenter,
  48. requiredGPS: requiredGPS,
  49. );
  50. },
  51. ),
  52. );
  53. if (results != null && results.containsKey('location')) {
  54. return results['location'];
  55. } else {
  56. return null;
  57. }
  58. }
  59. }
  60. class LocationPickerState extends State<LocationPicker> {
  61. /// Result returned after user completes selection
  62. LocationResult locationResult;
  63. /// Overlay to display autocomplete suggestions
  64. OverlayEntry overlayEntry;
  65. List<NearbyPlace> nearbyPlaces = List();
  66. /// Session token required for autocomplete API call
  67. String sessionToken = Uuid().generateV4();
  68. var mapKey = GlobalKey<MapPickerState>();
  69. var appBarKey = GlobalKey();
  70. var searchInputKey = GlobalKey<SearchInputState>();
  71. bool hasSearchTerm = false;
  72. /// Hides the autocomplete overlay
  73. void clearOverlay() {
  74. if (overlayEntry != null) {
  75. overlayEntry.remove();
  76. overlayEntry = null;
  77. }
  78. }
  79. /// Begins the search process by displaying a "wait" overlay then
  80. /// proceeds to fetch the autocomplete list. The bottom "dialog"
  81. /// is hidden so as to give more room and better experience for the
  82. /// autocomplete list overlay.
  83. void searchPlace(String place) {
  84. if (context == null) return;
  85. clearOverlay();
  86. setState(() => hasSearchTerm = place.length > 0);
  87. if (place.length < 1) return;
  88. final RenderBox renderBox = context.findRenderObject();
  89. Size size = renderBox.size;
  90. final RenderBox appBarBox = appBarKey.currentContext.findRenderObject();
  91. overlayEntry = OverlayEntry(
  92. builder: (context) => Positioned(
  93. top: appBarBox.size.height,
  94. width: size.width,
  95. child: Material(
  96. elevation: 1,
  97. child: Container(
  98. padding: EdgeInsets.symmetric(
  99. vertical: 16,
  100. horizontal: 24,
  101. ),
  102. color: Colors.white,
  103. child: Row(
  104. children: <Widget>[
  105. SizedBox(
  106. height: 24,
  107. width: 24,
  108. child: CircularProgressIndicator(
  109. strokeWidth: 3,
  110. ),
  111. ),
  112. SizedBox(
  113. width: 24,
  114. ),
  115. Expanded(
  116. child: Text(
  117. "${I18n.of(context).finding_place}...",
  118. style: TextStyle(
  119. fontSize: 16,
  120. ),
  121. ),
  122. )
  123. ],
  124. ),
  125. ),
  126. ),
  127. ),
  128. );
  129. Overlay.of(context).insert(overlayEntry);
  130. autoCompleteSearch(place);
  131. }
  132. /// Fetches the place autocomplete list with the query [place].
  133. void autoCompleteSearch(String place) {
  134. place = place.replaceAll(" ", "+");
  135. var endpoint =
  136. "https://maps.googleapis.com/maps/api/place/autocomplete/json?" +
  137. "key=${widget.apiKey}&" +
  138. "input={$place}&sessiontoken=$sessionToken";
  139. if (locationResult != null) {
  140. endpoint += "&location=${locationResult.latLng.latitude}," +
  141. "${locationResult.latLng.longitude}";
  142. }
  143. http.get(endpoint).then((response) {
  144. if (response.statusCode == 200) {
  145. Map<String, dynamic> data = jsonDecode(response.body);
  146. List<dynamic> predictions = data['predictions'];
  147. List<RichSuggestion> suggestions = [];
  148. if (predictions.isEmpty) {
  149. AutoCompleteItem aci = AutoCompleteItem();
  150. aci.text = "No result found";
  151. aci.offset = 0;
  152. aci.length = 0;
  153. suggestions.add(RichSuggestion(aci, () {}));
  154. } else {
  155. for (dynamic t in predictions) {
  156. AutoCompleteItem aci = AutoCompleteItem();
  157. aci.id = t['place_id'];
  158. aci.text = t['description'];
  159. aci.offset = t['matched_substrings'][0]['offset'];
  160. aci.length = t['matched_substrings'][0]['length'];
  161. suggestions.add(RichSuggestion(aci, () {
  162. decodeAndSelectPlace(aci.id);
  163. }));
  164. }
  165. }
  166. displayAutoCompleteSuggestions(suggestions);
  167. }
  168. }).catchError((error) {
  169. print(error);
  170. });
  171. }
  172. /// To navigate to the selected place from the autocomplete list to the map,
  173. /// the lat,lng is required. This method fetches the lat,lng of the place and
  174. /// proceeds to moving the map to that location.
  175. void decodeAndSelectPlace(String placeId) {
  176. clearOverlay();
  177. String endpoint =
  178. "https://maps.googleapis.com/maps/api/place/details/json?key=${widget.apiKey}" +
  179. "&placeid=$placeId";
  180. http.get(endpoint).then((response) {
  181. if (response.statusCode == 200) {
  182. Map<String, dynamic> location =
  183. jsonDecode(response.body)['result']['geometry']['location'];
  184. LatLng latLng = LatLng(location['lat'], location['lng']);
  185. moveToLocation(latLng);
  186. }
  187. }).catchError((error) {
  188. print(error);
  189. });
  190. }
  191. /// Display autocomplete suggestions with the overlay.
  192. void displayAutoCompleteSuggestions(List<RichSuggestion> suggestions) {
  193. final RenderBox renderBox = context.findRenderObject();
  194. Size size = renderBox.size;
  195. final RenderBox appBarBox = appBarKey.currentContext.findRenderObject();
  196. clearOverlay();
  197. overlayEntry = OverlayEntry(
  198. builder: (context) => Positioned(
  199. width: size.width,
  200. top: appBarBox.size.height,
  201. child: Material(
  202. elevation: 1,
  203. color: Colors.white,
  204. child: Column(
  205. children: suggestions,
  206. ),
  207. ),
  208. ),
  209. );
  210. Overlay.of(context).insert(overlayEntry);
  211. }
  212. /// Utility function to get clean readable name of a location. First checks
  213. /// for a human-readable name from the nearby list. This helps in the cases
  214. /// that the user selects from the nearby list (and expects to see that as a
  215. /// result, instead of road name). If no name is found from the nearby list,
  216. /// then the road name returned is used instead.
  217. // String getLocationName() {
  218. // if (locationResult == null) {
  219. // return "Unnamed location";
  220. // }
  221. //
  222. // for (NearbyPlace np in nearbyPlaces) {
  223. // if (np.latLng == locationResult.latLng) {
  224. // locationResult.name = np.name;
  225. // return np.name;
  226. // }
  227. // }
  228. //
  229. // return "${locationResult.name}, ${locationResult.locality}";
  230. // }
  231. /// Fetches and updates the nearby places to the provided lat,lng
  232. void getNearbyPlaces(LatLng latLng) {
  233. http
  234. .get("https://maps.googleapis.com/maps/api/place/nearbysearch/json?" +
  235. "key=${widget.apiKey}&" +
  236. "location=${latLng.latitude},${latLng.longitude}&radius=150")
  237. .then((response) {
  238. if (response.statusCode == 200) {
  239. nearbyPlaces.clear();
  240. for (Map<String, dynamic> item
  241. in jsonDecode(response.body)['results']) {
  242. NearbyPlace nearbyPlace = NearbyPlace();
  243. nearbyPlace.name = item['name'];
  244. nearbyPlace.icon = item['icon'];
  245. double latitude = item['geometry']['location']['lat'];
  246. double longitude = item['geometry']['location']['lng'];
  247. LatLng _latLng = LatLng(latitude, longitude);
  248. nearbyPlace.latLng = _latLng;
  249. nearbyPlaces.add(nearbyPlace);
  250. }
  251. }
  252. // to update the nearby places
  253. setState(() {
  254. // this is to require the result to show
  255. hasSearchTerm = false;
  256. });
  257. }).catchError((error) {});
  258. }
  259. /// This method gets the human readable name of the location. Mostly appears
  260. /// to be the road name and the locality.
  261. Future reverseGeocodeLatLng(LatLng latLng) async {
  262. /*
  263. var placeMarks = await Geolocator()
  264. .placemarkFromCoordinates(latLng.latitude, latLng.longitude);
  265. if (placeMarks == null) {
  266. return;
  267. }
  268. Placemark place = placeMarks.first;
  269. print('~~~~~~~~~~~~~~~~~~~~~~~~');
  270. print(place.toString());
  271. setState(() {
  272. locationResult = LocationResult();
  273. locationResult.address = place.name;
  274. locationResult.latLng =
  275. LatLng(place.position.latitude, place.position.longitude);
  276. });
  277. */
  278. var response = await http.get(
  279. "https://maps.googleapis.com/maps/api/geocode/json?latlng=${latLng.latitude},${latLng.longitude}"
  280. "&key=${widget.apiKey}");
  281. if (response.statusCode == 200) {
  282. Map<String, dynamic> responseJson = jsonDecode(response.body);
  283. String road =
  284. responseJson['results'][0]['address_components'][0]['short_name'];
  285. setState(() {
  286. locationResult = LocationResult();
  287. locationResult.address = road;
  288. locationResult.latLng = latLng;
  289. });
  290. }
  291. }
  292. /// Moves the camera to the provided location and updates other UI features to
  293. /// match the location.
  294. void moveToLocation(LatLng latLng) {
  295. mapKey.currentState.mapController.future.then((controller) {
  296. controller.animateCamera(
  297. CameraUpdate.newCameraPosition(
  298. CameraPosition(
  299. target: latLng,
  300. zoom: 18.0,
  301. ),
  302. ),
  303. );
  304. });
  305. reverseGeocodeLatLng(latLng);
  306. getNearbyPlaces(latLng);
  307. }
  308. @override
  309. void dispose() {
  310. mapKey = null;
  311. appBarKey = null;
  312. clearOverlay();
  313. super.dispose();
  314. }
  315. @override
  316. Widget build(BuildContext context) {
  317. return MultiProvider(
  318. providers: [
  319. ChangeNotifierProvider(create: (_) => LocationProvider()),
  320. ],
  321. child: Builder(builder: (context) {
  322. return Scaffold(
  323. appBar: AppBar(
  324. backgroundColor: Colors.white,
  325. iconTheme: IconThemeData(color: Colors.black),
  326. key: appBarKey,
  327. title: SearchInput(
  328. (input) => searchPlace(input),
  329. key: searchInputKey,
  330. ),
  331. leading: CustomUI.buildCustomLeading(context),
  332. ),
  333. body: MapPicker(
  334. initialCenter: widget.initialCenter,
  335. key: mapKey,
  336. apiKey: widget.apiKey,
  337. requiredGPS: widget.requiredGPS,
  338. ),
  339. );
  340. }),
  341. );
  342. }
  343. }