เนื้อหานี้เป็นอีกบทความที่อยากแนะนำเพิ่ม และคิดว่าอาจจะ
มีประโยชน์ในการนำไปปรับประยุกต์ใช้งานเพิ่มเติมได้ สิ่งที่จะ
พูดถึงก็คือ Distance Matrix และ Direction Route โดย
ทั้งสองส่วนนี้เราเคยมีบทความเกี่ยวกับการใช้งานในรูปแบบเว็บไซต์
แล้วสามารถดูได้ที่บทความด้านล่าง
หาระยะทางจากตำแหน่งปัจจุบันกับสถานที่ใน google map ด้วย Distance Matrix http://niik.in/832
DirectionsService DirectionsRenderer ค้นหา และสร้าง เส้นทางใน google map http://niik.in/340
บทความนี้เราจะปรับแต่งประยุกต์จากบทความก่อนหน้า จะตัดในส่วนของการใช้งาน polygon
ออก ส่วน polyline ยังใช้งานบางส่วนในแผนที่
ทบทวนตอนที่แล้วได้ที่บทความ
การใช้งาน Geocoding หาพิกัดและซื่อสถานที่บนแผนที่ ใน Flutter http://niik.in/1130
ความแตกต่างของ Distance Matrix กับ การวัดระยะทางด้วย Geolocator
ก่อนจะลงรายละเอียดเกี่ยวกับโค้ด ขออธิบายอีกเล็กน้อย ในตอนที่ผ่านมา เราจะเห็นการใช้งาน
การหาระยะทางของเส้น polyline โดยใช้ geolocator แพ็กเก็จ นั่นคือการวัดความยาวในเส้นตรง
พูดง่ายๆ ก็คือเส้นตรงผ่านอากาศ ไม่ผ่านเส้นทาง หรือถนน ดังนั้น ถ้าเป็นจุดของเส้นในแนวเส้นตรง
ทั้งหมด ก็จะสามารถวัดได้ถูกต้อง หรือใช้วัดระยะห่างจะหว่างจุดพิกัดได้ แต่สำหรับ Distance Matrix
นั้นจะเป็นการใช้ข้อมูลเส้นทางในแผนที่ แล้ววัดระยะทางที่ดีที่สุดในขณะนั้นตามเส้นถนน ที่อาจจะมีเลี้ยว
ซ้ายขวาไปมา มาใช้งาน
สรุปความแตกต่าง
Geolocator: วัด ระยะทางเส้นตรง (จากพิกัด GPS) ไม่สนใจเส้นทางถนน
Distance Matrix API: วัด ระยะทางตามเส้นทางถนนจริง โดยคำนวณตามเส้นทางที่ Google แนะนำ รวมถึงพิจารณาสภาพการจราจร
เมื่อไหร่ควรใช้แต่ละวิธี
ถ้าต้องการ ระยะทางโดยประมาณ แบบเส้นตรงจากจุด A ไปยังจุด B โดยไม่สนใจเส้นทางถนน
(เช่น การคำนวณระยะทางในการบิน) ให้ใช้ Geolocator
ถ้าต้องการ ระยะทางตามเส้นทางถนน ที่ใช้ในการเดินทางจริง (พร้อมกับเวลาเดินทาง) ให้ใช้
Google Distance Matrix API
ดังนั้นในการประยุกต์เนื้อหาตอนนี้ เราจะใช้งาน Distance Matrix เพื่อหาระยะทางระหว่างจุดพิกัด
ซึ่งจะช่วยให้เรารู้ว่าสถานที่ใด ไกลหรือไกล้กว่ากัน ใช้เวลาเดินทางประมาณเท่าไหร่
ติดตั้ง package ที่จำเป็นเพิ่มเติม ตามรายการด้านล่าง
แพ็กเก็จที่จำเป็นต้องติดตั้งเพิ่มเติม สำหรับการทำงานมีดังนี้
google_maps_flutter: ^2.9.0 geolocator: ^13.0.1 permission_handler: ^11.3.1 geocoding: ^3.0.0 http: ^1.2.2
การใช้งาน Distance Matrix บนแผนที่ใน Flutter
ใน flutter ไม่มีแพ็กเก็จในการจัดการกับ Distance Matrix แต่เราสามารถเรียกใช้งานผ่าน
API ได้ดังนั้น จึงจำเป็นจะต้องใช้แพ็กเก็จ http เพื่อใช้สำหรับไปดึงข้อมูลจากบริการของ Google
มาใช้งาน โดยจะใช้ค่า apiKey ของแผนที่เป็นค่าเดียวกันกับที่เราได้มาและกำหนดไปก่อนหน้า
ตัวอย่างการเรียกใช้ Distance Matrix API
// ฟังก์ชั่นหาระยะทางของเส้นทางจาก DistanceMatrix http://niik.in/832 Future<void> getDistanceMatrix(LatLng origin, LatLng destination) async { String _origin = '${origin.latitude},${origin.longitude}'; String _destination = '${destination.latitude},${destination.longitude}'; String _apiKey = 'Google Map API Key'; final url = Uri.parse( 'https://maps.googleapis.com/maps/api/distancematrix/json' '?origins=$_origin' '&destinations=$_destination' '&key=$_apiKey', ); // สามารถกำหนด option อื่นๆ เพิ่มเติมได้ ดูจากลิ้งค์ด้านล่าง // https://developers.google.cn/maps/documentation/distance-matrix/distance-matrix?hl=th final response = await http.get(url); if (response.statusCode == 200) { final data = jsonDecode(response.body); /* ระยะทางระหว่างต้นทางและปลายทาง (ในหน่วยต่างๆ เช่น กิโลเมตร เมตร) เวลาที่ใช้ในการเดินทาง (ในหน่วยต่างๆ เช่น วินาที) */ // ข้อความที่แสดงระยะทางจะมีการปัดเศษโดยประมาณ (เช่น "12.7 เมตร") var distance = data['rows'][0]['elements'][0]['distance']['text']; // ข้อความที่แสดงเวลาเดินทางจะมีการปัดเศษโดยประมาณ (เช่น "13 นาที") var duration = data['rows'][0]['elements'][0]['duration']['text']; var distanceValue = data['rows'][0]['elements'][0]['distance']['value']; // ระยะทางในหน่วยเมตร var durationValue = data['rows'][0]['elements'][0]['duration']['value']; // เวลาในหน่วยวินาที print('Distance: $distance'); print('Duration: $duration'); print('Distance Value: $distanceValue'); print('Duration Value: $durationValue'); } else { // กรณีไม่มีเส้นทางในฐานข้อมูล Google Map throw Exception('Failed to load distance matrix'); } }
ในที่นี้เราจะประยุกต์ตัวอย่างคือ เราจะใช้ตัว marker หลักเป็นตัระบุตำแหน่งเริ่มต้น และ เราสามารถ
กดเพิ่มตัว marker ในโหมด marker เพื่อระบุพิกัดใหม่ลงไป โดยทันทีที่เพิ่มตัว marker เข้าไปก็จะ
ทำการเรียกใช้ฟังก์ชั่นหาระยะทางจากเส้นทางผ่าน distance matrix และแสดงผลลัพธ์ข้อมูลระยะทาง
และเวลาเดินทางโดยประมาณ เราสามารถขยับตำแหน่ง ตัว marker เข้าหาตำแหน่งปัจจุบันได้ เพื่อดู
ระยะทางที่เปลี่ยนแปลงไปได้
ไฟล์ map.dart
import 'dart:async'; import 'dart:ui' as ui; import 'dart:math'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:geolocator/geolocator.dart'; import 'package:geocoding/geocoding.dart'; import 'package:http/http.dart' as http; class Maps extends StatefulWidget { static const routeName = '/map'; const Maps({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _MapsState(); } } class _MapsState extends State<Maps> { // กำนหนดตัวควบคุมให้กับแผนที่ สังเกตว่า มีการใช้แบบ Completer ซึงในภาษา Dart // เป็นคลาสที่ใช้ในการควบคุมการทำงานของ Future ดังนั้น ตัว controller จะถูกสร้างก็เมื่อตัวแผนที่ // โหลดเรียบร้อยแล้วเท่านั้น เพื่อให้พร้อมใช้งาน final Completer<GoogleMapController> _controller = Completer<GoogleMapController>(); // การกำหนดตำแหน่งต่างๆ จะใช้เป็นข้อมูล CameraPosition // ในตัวอย่างเรากำหนดตัวแปรไว้ 2 ตัว CameraPosition? _initialPosition; // ตัวแปรสำหรับตำแหน่งเริ่มต้น ยังไม่กำหนดค่าใดๆ // ตัวอย่งการกำหนดตำแหน่งแบบตายตัว สมมติเป็นห้าง มาบุญครอง static const CameraPosition _place_mbk = CameraPosition( bearing:0.0, // องศาทิศทางของกล้องตามเข็มนาฬิกาจากทิศเหนือ (หมุนแกนนอน) target: LatLng(13.745324385325134, 100.52999353076734), // พิกัด Lat Long tilt: 0.0, // องศามุมที่กล้องหันตรงไปที่พื้นโลก (หมุนแกนตั้ง) zoom: 14.0 // ระดะับการขยาย ); // สร้าง Set ของ Marker final Set<Marker> _markers = {}; List<LatLng> _markerPoints = []; // สำกำหนดจุดพิกัดตัว marker BitmapDescriptor? _markerIcon; // กำหนดไอคอนจากรูป final MarkerId _markerId = const MarkerId('current_location'); LatLng? _activeMarkerPosition; // ตัวแปรสำหรับเก็บตำแหน่งของ Marker ที่ถูกลาก // กำหนดตัวแปรไว้นับจำนวน marker แลัวไว้สร้าง ค่าไม่ซ้ำไว้ใช้งาน int _markerCount = 0; // กำหนดส่วนของ polyline ที่จะใช้กับแผนที่ Set<Polyline> _polylines = {}; // สำหรับกำหนดโหมดใช้งาน bool _marker_mode = false; // สำหรับวางจุดเท่านั้น // เพิ่มส่วนจัดการเกี่ยวข้อมูลชื่อสถานที่ final TextEditingController _searchController = TextEditingController(); final FocusNode _focusNode = FocusNode(); // สร้าง FocusNode สำหรับ TextField @override void initState() { super.initState(); // ขอสิทธิการเข้าถึงพิกัดตำแหน่ง requestPermissions(); _getCurrentLocation(); // เตรียมไอคอน prepareIcons(); } // ขอ permission สำหรับจัดการตำแหน่ง void requestPermissions() async { await Permission.location.request(); } // สร้างไอคอนจากรูปใน assets void prepareIcons() async { _markerIcon = await BitmapDescriptor.asset( const ImageConfiguration(size: Size(41.5, 48)), // ขนาดของไอคอนที่ต้องการแสดง // const ImageConfiguration(), // ขนาดของไอคอนที่ต้องการแสดงใช้ตามขนาดรูป 'assets/icon/marker.png', // พาธของ asset ขนาดรูปตัวอย่างที่ทดสอบคือ 32x37 ); } // ฟังก์ชันเพื่อดึงตำแหน่งปัจจุบันของผู้ใช้ Future<void> _getCurrentLocation() async { bool serviceEnabled; LocationPermission permission; // ตรวจสอบว่าเปิด Location Services ไว้หรือไม่ serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // หาก Location Services ปิด แสดงข้อความหรือจัดการตามต้องการ return; } // ตรวจสอบ permission สำหรับตำแหน่งที่ตั้ง permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { // Permission ถูกปฏิเสธ return; } } if (permission == LocationPermission.deniedForever) { // Permission ถูกปฏิเสธอย่างถาวร return; } // หาก permission ได้รับการอนุญาต ดึงตำแหน่งที่ตั้งปัจจุบัน Position position = await Geolocator.getCurrentPosition(); // กำหนดตำแหน่งเริ่มต้นเป็นตำแหน่งของผู้ใช้ setState(() { // กำหนดค่าให้กับตำแหน่งเริ่มต้น แล้วสร้างรูปแบบพิกัดไว้ใช้งาน // ค่า bearing และ tilt หากไม่กำหนด ก็จะใช้เป็น 0 ตามค่าเริ่มต้น ในที่นี้เราใช้แค่ // ตำแหน่ง กับ การ zoom _initialPosition = CameraPosition( target: LatLng(position.latitude, position.longitude), zoom: 14.0, ); // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = LatLng(position.latitude, position.longitude); // เพิ่ม Marker สำหรับตำแหน่งปัจจุบัน _markers.add( Marker( markerId: _markerId, position: _activeMarkerPosition!, icon: _markerIcon!, // กำหนดไอคอนจาก asset draggable: true, // เปิดใช้งานการลาก marker infoWindow: InfoWindow( title: 'ตำแหน่งของคุณ', // ข้อความเพิ่มเติมที่จะแสดงใน InfoWindow snippet: '${_activeMarkerPosition!.latitude}, ${_activeMarkerPosition!.longitude}', onTap: () async { print("hide infowindow"); final GoogleMapController controller = await _controller.future; controller.hideMarkerInfoWindow(_markerId); } ), onTap: () async { }, onDragEnd: (LatLng newPosition) { print("Marker dropped at: ${newPosition.latitude}, ${newPosition.longitude}"); // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = LatLng(newPosition.latitude, newPosition.longitude); // สามารถจัดการตำแหน่งใหม่ได้ที่นี่ เช่น อัปเดตตำแหน่งในฐานข้อมูลหรือแสดงข้อมูลใหม่ CameraPosition _newPosition = CameraPosition( target: LatLng(newPosition.latitude, newPosition.longitude), zoom: 14.0, ); // เรียกใช้ ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป _goToPosition(_newPosition); // เรียกใช้งานฟังก์ชัน อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarkerSnippet(_markerId,_activeMarkerPosition!); }, ), ); }); // เรียกใช้ GoogleMapController เพื่อแสดง InfoWindow อัตโนมัติ final GoogleMapController controller = await _controller.future; controller.showMarkerInfoWindow(_markerId); } // ฟังก์ชั่น อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง void _updateMarkerSnippet(MarkerId markerId, LatLng newPosition) { // ตรวจสอบหาตัว markder เดิม ที่ไอดีตรงกับที่ส่งมา Marker? oldMarker = _markers.firstWhere( (marker) => marker.markerId == markerId, orElse: () => null as Marker, // ใช้ cast เป็น markder เพื่อเลี่ยง null safely ); if (oldMarker != null) { // ถ้าไม่ใช่ null // ลบ markder ตัวเดิม _markers.remove(oldMarker); // กำหนดข้อมูลที่แสดงใน infowindow ใหม่ เป็นค่าพิกัดใหม่ String newSnippet = '${newPosition.latitude}, ${newPosition.longitude}'; // เพิ่มตัว markder ตัวใหม่เข้าไป _markers.add( Marker( markerId: markerId, // ใช้ไอดีเดิม position: newPosition, // ตำแหน่งใหม่ icon: _markerIcon!, // กำหนดไอคอนจาก asset draggable: true, // เปิดใช้งานการลาก marker infoWindow: InfoWindow( title: oldMarker.infoWindow.title, // ใช้จากค่าเดิม snippet: newSnippet, // อัปเดทค่าใหม่ onTap: oldMarker.infoWindow.onTap, // ใช้จากค่าเดิม ), onTap: oldMarker.onTap, // ใช้จากค่าเดิม onDragEnd: oldMarker.onDragEnd, // ใช้จากค่าเดิม ), ); // rebuild เพื่ออัปเดทแผนที่ setState(() {}); } } // ฟังก์ชั่นเฉพาะ กรณีเราต้องการให้ไปยังตำแหน่ง ที่เจาะจง ในที่นี้คือไปตำแหน่ง ห้าง มาบุญครอง Future<void> _goToMBK() async { final GoogleMapController controller = await _controller.future; await controller.animateCamera(CameraUpdate.newCameraPosition(_place_mbk)); } // ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป Future<void> _goToPosition(CameraPosition postion) async { final GoogleMapController controller = await _controller.future; await controller.animateCamera(CameraUpdate.newCameraPosition(postion)); // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = postion.target!; // อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarkerSnippet(_markerId,postion.target!); // เรียกใช้ GoogleMapController เพื่อแสดง InfoWindow อัตโนมัติ controller.showMarkerInfoWindow(_markerId); } // ฟังก์ชั่นการกดที่แผนที่ แล้วไปยังตำแหน่งที่ต้องการ Future<void> _onMapTapped(LatLng position) async { // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = LatLng(position.latitude, position.longitude); // สามารถจัดการตำแหน่งใหม่ได้ที่นี่ เช่น อัปเดตตำแหน่งในฐานข้อมูลหรือแสดงข้อมูลใหม่ CameraPosition _newPosition = CameraPosition( target: LatLng(position.latitude, position.longitude), zoom: 14.0, ); // เรียกใช้ ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป _goToPosition(_newPosition); // เพิ่มการเรียกใช้ฟังก์ชั่น แสดงชื่อสถานที่จากพิกัด getPlaceFromCoordinates(position.latitude,position.longitude); } // ฟังก์ชั่น อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง Future<void> _updateMarker(MarkerId markerId, LatLng newPosition) async { // ตรวจสอบหาตัว markder เดิม ที่ไอดีตรงกับที่ส่งมา Marker? oldMarker = _markers.firstWhere( (marker) => marker.markerId == markerId, orElse: () => null as Marker, // ใช้ cast เป็น markder เพื่อเลี่ยง null safely ); if (oldMarker != null) { // ถ้าไม่ใช่ null // ลบ markder ตัวเดิม _markers.remove(oldMarker); // กำหนดข้อมูลที่แสดงใน infowindow ใหม่ เป็นค่าพิกัดใหม่ String newSnippet = '${newPosition.latitude}, ${newPosition.longitude}'; markerId = MarkerId(newPosition.toString()); // เพิ่มตัว markder ตัวใหม่เข้าไป _markers.add( Marker( markerId: markerId, // ใช้ไอดีเดิม position: newPosition, // ตำแหน่งใหม่ icon: oldMarker.icon, // กำหนดไอคอนจาก asset draggable: true, // เปิดใช้งานการลาก marker infoWindow: InfoWindow( title: oldMarker.infoWindow.title, // ใช้จากค่าเดิม snippet: newSnippet, // อัปเดทค่าใหม่ onTap: oldMarker.infoWindow.onTap, // ใช้จากค่าเดิม ), onTap: oldMarker.onTap, // ใช้จากค่าเดิม onDragEnd: (LatLng newPosition) { print("drop marker ${newPosition.toString()}"); // เรียกใช้งานฟังก์ชัน อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarker(markerId,newPosition); // เมื่อเพิ่ม marker เรียกใช้ฟังก์ชั่น หาระยะทางจากพิกัด หลัก กับพิกัดตัว marker ที่เพิ่ม getDistanceMatrix(_activeMarkerPosition!,newPosition); } ), ); // ค่าใหม่ที่จะแทนที่ LatLng newLocation = newPosition; // ค่าใหม่ที่จะแทนที่ // หาค่าเดิมใน List int index = _markerPoints.indexWhere((latLng) => latLng.latitude == oldMarker.position.latitude && latLng.longitude == oldMarker.position.longitude); if (index != -1) { // แทนที่ค่าเดิมด้วยค่าใหม่ ถ้ามี _markerPoints[index] = newLocation; } // rebuild เพื่ออัปเดทแผนที่ setState(() {}); } } // ฟังก์ชั่นสำหรับเพิ่มตัว marker ไปยังจุดที่กดแตะในแผนที่ กรณีใช้งานโหมด ปักหมุด polyline และ polygon Future<void> _addMarkerOnMap(LatLng tappedPosition) async { _markerCount++; // เพิ่มค่าสำหรับใช้กำหนด ข้อความ ที่เป็นตัวเลข ใน marker ไม่ให้ซ้ำกัน // นำค่าตัวเลขที่ไม่ซ้ำ ส่งเข้าไปในฟังก์ชั่นสร้างรูป ไอคอน สำหรับ marker final customMarker = await _createCustomMarker('$_markerCount'); setState(() { // เก็บค่าตำแหน่ง marker ต้วก่อนหน้า เพื่อใช้สำหรับการ เชื่อมเป็น polyline หรือ polygon LatLng lastMarkerPosition = _markers.last.position; MarkerId markerId = MarkerId(tappedPosition.toString()); // ถ้าใช้งานโหมด marker if(_marker_mode){ // เพิ่มตำแหน่งใน List สำหรับ marker _markerPoints.add(tappedPosition); // เพิ่มจุดที่ผู้ใช้แตะเพื่อสร้าง marker // ทำการเพิ่มตัว marker เมื่อกดแตะที่แผนที่ _markers.add( Marker( // ใช้ค่าพิกัดเป็น id ของ marker จะได้ไม่ซ้ำกัน ค่านี้จะขึ้นต้นด้วย LatLng markerId: markerId, position: tappedPosition, // เพิ่มตรงตำแหน่งที่แตะ icon: customMarker, // ใช้ไอคอนไม่ซ้ำรูปแบบกัน infoWindow: InfoWindow( // ส่วนแสดง info title: 'Marker $_markerCount', // Title for the marker snippet: '${tappedPosition.latitude}, ${tappedPosition.longitude}', ), draggable: true, onDragEnd: (LatLng newPosition) { print("drop marker ${tappedPosition.toString()}"); // เรียกใช้งานฟังก์ชัน อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarker(markerId,newPosition); // เมื่อเพิ่ม marker เรียกใช้ฟังก์ชั่น หาระยะทางจากพิกัด หลัก กับพิกัดตัว marker ที่เพิ่ม getDistanceMatrix(_activeMarkerPosition!,newPosition); } ), ); // เมื่อเพิ่ม marker เรียกใช้ฟังก์ชั่น หาระยะทางจากพิกัด หลัก กับพิกัดตัว marker ที่เพิ่ม getDistanceMatrix(_activeMarkerPosition!,tappedPosition); } }); } // สร้างฟังก์ชั่นสำหรับสร้างตัว marker Future<BitmapDescriptor> _createCustomMarker(String text) async { // ตัวจัดการการ render ภาพ final recorder = ui.PictureRecorder(); // ตัวจัดการพื้นที่วาดภาพ หรือสร้างรูปภาพ final canvas = Canvas(recorder); // กำหนดรูปแบบการวาด และขนาด final paint = Paint() ..color = Colors.green // สีพื้นหลัง ..style = PaintingStyle.fill; // ใช้แบบเติมสี final radius = 24.0; // กำหนดรัศมีของวงลม ที่จะทำ marker final pinHeight = 12.0; // ความสูงของสามเหลี่ยมที่เราจะทำเป็นปลายปักหมุด // วาดรูปวงกลม ส่วนหลัก เข้าไปก่อน canvas.drawCircle(Offset(radius, radius), radius, paint); // วาดจุดสำหรับสร้างรูป สามเหลี่ยม final trianglePath = Path(); // จุดเริ่มต้นของสามเหลี่ยม trianglePath.moveTo(radius - pinHeight, (radius * 2) - (pinHeight / 2)); // จุดที่สอง ลากไปทางขวา จากจัดแรก trianglePath.lineTo(radius + pinHeight, (radius * 2) - (pinHeight / 2)); // จัดที่สาม จุดตรงกลาง ยื่นลงมาเป็นจุด ปักหมด trianglePath.lineTo(radius, radius * 2 + pinHeight); // เปิดจุดการวาด จากจุดที่สาม ไปจุดเริ่มแรก เพื่อปิด trianglePath.close(); // วาดรูปสามเหลี่ยมลงในพื้นที่วาด ตามค่าที่กำหนด canvas.drawPath(trianglePath, paint); // การใส่ตัวเลขเข้าไปในภาพ จัดรูปแบบต่างๆ final textPainter = TextPainter( text: TextSpan( text: text, // ใช้การรับค่ามาแสดง style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr,// การเรียงซ้ายไปขวา ); // กำหนดส่วนจัดวางตัวเลขหรือข้อความ ในที่นี้ใช้ค่าเริ่มต้น textPainter.layout(); // วาดข้อความลงไปในพื้นที่วาด จัดตำแหน่งอ้างอิงจากค่าต่างๆ ที่เกี่ยวข้อง ในที่นี้คือไว้ตรงกลางรูปวงกลม textPainter.paint( canvas, Offset(radius - textPainter.width / 2, radius - textPainter.height / 2)); // ใช้ตัว render แปลงจาก canvas ไปเป็นรูปภาพ final picture = recorder.endRecording(); // สร้างรูปภาพ กำหนดขนาด ตามต้องการ ในที่นี้ใช้การคำนวณ กว้างเท้ากับขนาดของวงกลม // ส่วนความสูงให้เท่ากับขนาดของวงกลม บวกกับความสูงของ สามเหลี่ยม final img = await picture.toImage((radius * 2).toInt(), ((radius * 2) + pinHeight).toInt()); // เปลงเป็น binary data เพื่อนำไปใช้งาน final byteData = await img.toByteData(format: ui.ImageByteFormat.png); final pngBytes = byteData!.buffer.asUint8List(); // สร้าง BitmapDescriptor ที่ใช้สำหรับเป็นไอคอน จากข้อมูล binary ของรูปภาพ return BitmapDescriptor.bytes(pngBytes); } // ฟังก์ชั่นสำหรับเปลี่ยนโหมด Future<void> _changeMode({bool marker = false}) async{ setState(() { _marker_mode = marker; _markerPoints.clear(); // ล้างค่าจุดของ marker _markerCount = 0; // เริ่มนับค่าใหม่ _polylines.clear(); // ล้าง polyline ออกจากแผนที่ // ลบตัว marker ทั้งหมดที่ไอดี ขึ้นต้นด้วยคำว่า 'LatLng' _markers.removeWhere( (marker) => marker.markerId.value.startsWith('LatLng')); }); } // ส่วนของฟังก์ชั่นแสดงการแจ้งเตือน void _showResultSnackbar(String result) { // ปิด SnackBar ที่แสดงอยู่ก่อน ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Container( // height: 100, // ปรับขนาดความสูงของการแสดง ปิดการกำหนดตายตัว ให้ขยายขนาดตามข้อมูล child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( result, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18, ), ), ], ), ), // duration: Duration(seconds: 100), // กำหนดแสดงชั่วคราว duration: const Duration(days: 365), // กำหนดแสดงแบบถาวร behavior: SnackBarBehavior.floating, // floating / fixed backgroundColor: Colors.grey[850], action: SnackBarAction( label: 'Close', textColor: Colors.white, onPressed: () { // เมื่อกดปิด ScaffoldMessenger.of(context).hideCurrentSnackBar(); // Dismisses the SnackBar }, ), ), ); } // ฟังก์ชั่นสำหรับหาพิกัดจากซื้อสถานที่ Future<void> getCoordinatesFromPlace(String placeName) async { try { List<Location> locations = await locationFromAddress(placeName); if (locations.isNotEmpty) { // แสดงพิกัด latitude และ longitude double latitude = locations[0].latitude; double longitude = locations[0].longitude; // สามารถจัดการตำแหน่งใหม่ได้ที่นี่ CameraPosition _newPosition = CameraPosition( target: LatLng(latitude,longitude), zoom: 14.0, ); // เรียกใช้ ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป _goToPosition(_newPosition); print('พิกัดของ $placeName: $latitude, $longitude'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('พิกัดของ $placeName: $latitude, $longitude'); } } catch (e) { print('ไม่สามารถค้นหาพิกัดจากชื่อสถานที่ได้: $e'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('ไม่สามารถค้นหาพิกัดจากชื่อสถานที่ได้: $e'); } } // ฟังก์ชั่นหาชื่อสถานที่จากค่าพิกัด Future<void> getPlaceFromCoordinates(double latitude, double longitude) async { try { List<Placemark> placemarks = await placemarkFromCoordinates(latitude, longitude); if (placemarks.isNotEmpty) { // แสดงข้อมูลสถานที่ที่ค้นพบ Placemark place = placemarks[0]; print('สถานที่จากพิกัด: ${place.street}, ${place.locality}, ${place.country}'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('สถานที่จากพิกัด: ${place.street}, ${place.locality}, ${place.country}'); } } catch (e) { print('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); } } // ฟังก์ชั่นหาระยะทางของเส้นทางจาก DistanceMatrix http://niik.in/832 Future<void> getDistanceMatrix(LatLng origin, LatLng destination) async { String _origin = '${origin.latitude},${origin.longitude}'; String _destination = '${destination.latitude},${destination.longitude}'; String _apiKey = 'Google Map API Key'; final url = Uri.parse( 'https://maps.googleapis.com/maps/api/distancematrix/json' '?origins=$_origin' '&destinations=$_destination' '&language=th' '&key=$_apiKey', ); // สามารถกำหนด option อื่นๆ เพิ่มเติมได้ ดูจากลิ้งค์ด้านล่าง // https://developers.google.cn/maps/documentation/distance-matrix/distance-matrix?hl=th final response = await http.get(url); if (response.statusCode == 200) { final data = jsonDecode(response.body); /* ระยะทางระหว่างต้นทางและปลายทาง (ในหน่วยต่างๆ เช่น กิโลเมตร เมตร) เวลาที่ใช้ในการเดินทาง (ในหน่วยต่างๆ เช่น วินาที) */ // ข้อความที่แสดงระยะทางจะมีการปัดเศษโดยประมาณ (เช่น "12.7 เมตร") var distance = data['rows'][0]['elements'][0]['distance']['text']; // ข้อความที่แสดงเวลาเดินทางจะมีการปัดเศษโดยประมาณ (เช่น "13 นาที") var duration = data['rows'][0]['elements'][0]['duration']['text']; var distanceValue = data['rows'][0]['elements'][0]['distance']['value']; // ระยะทางในหน่วยเมตร var durationValue = data['rows'][0]['elements'][0]['duration']['value']; // เวลาในหน่วยวินาที print('Distance: $distance'); print('Duration: $duration'); print('Distance Value: $distanceValue'); print('Duration Value: $durationValue'); _showResultSnackbar( 'Distance: $distance\r\n' 'Duration: $duration\r\n' 'Distance Value: $distanceValue\r\n' 'Duration Value: $durationValue' ); } else { // กรณีไม่มีเส้นทางในฐานข้อมูล Google Map _showResultSnackbar('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); // throw Exception('Failed to load distance matrix'); } } @override void dispose() { // การล้างค่า เมื่อไม่ได้ใช้งานแล้ว _searchController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('แผนที่'), actions: [ IconButton( onPressed: (){ _changeMode(); }, icon: Image.asset('assets/icon/pan${(!_marker_mode) ? '_active' : ''}.png',), ), IconButton( onPressed: (){ _changeMode(marker: true); }, icon: Image.asset('assets/icon/mark${(_marker_mode) ? '_active' : ''}.png',), ), ], ), body: _initialPosition == null // กรณียังไม่ได้ตำแหน่ง แสดงตัว Loading ก่อน ? const Center(child: CircularProgressIndicator()) // Loading หรือจะใช้รูปแบบอื่นๆ แทนได้ถ้าต้องการ : Stack( // ใช้ stack ในการวางซ้อนแผนที่ รองรับการเพิ่มเมนูไอคอนต่างๆ children: [ GoogleMap( mapType: MapType.normal, // normal | terrain | satellite | hybrid initialCameraPosition: _initialPosition!, // ใช้ค่าเริ่มต้นจากตำแหน่งของเรา onMapCreated: (GoogleMapController controller) { // ทำให้ Future สำเร็จเมื่อแผนที่ถูกสร้างเสร็จแล้ว // ตัว controller พร้อมใช้งาน _controller.complete(controller); }, // ในที่นี้เราจtซ่อนตัวปุ่ม zoom และกำหนดเอง จากปุ่มที่เราสร้าง zoomControlsEnabled: false, // ซ่อนค่าเริ่มต้นปุ่ม zoom // ในที่นี้เราจะซ่อนตัวปุม ตำแหน่งปัจจุบัน และกำหนดเองจากปุ่มที่เราสร้าง myLocationButtonEnabled: false, // ซ่อนปุ่มตำแหน่งปัจจุบัน myLocationEnabled: false, // แสดงรูป จุด ของตำแหน่งปัจจุบัน markers: _markers, // เพิ่ม markers ใน GoogleMap polylines: _polylines, // เพิ่มการใช้งาน polylines onTap: (_marker_mode) // ถ้าอยู่ในโหมดใดโหมดหนึ่ง ? _addMarkerOnMap // จัดการเกี่ยวกับ marker polyline polygon : _onMapTapped, // เมื่อกดที่แผนที่ให้ทำคำสั่ง ย้ายตำแหน่งไปยังจุดที่ต้องการ ), // ช่องค้นหาชื่อสถานที่ Positioned( top: 16, left: 16, child: Container( width: 250, // ความกว้างของช่องค้นหา child: TextField( controller: _searchController, // ควบคุมค่าใน TextField focusNode: _focusNode, // เชื่อมต่อ FocusNode กับ TextField minLines: 1, // จำนวนบรรทัดขั้นต่ำ maxLines: null, // ให้ขยายได้ไม่จำกัดตามเนื้อหา decoration: InputDecoration( hintText: 'ค้นหาสถานที่...', hintStyle: TextStyle( color: Colors.grey[500], // สีอ่อนสำหรับ hintText ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), // ปรับให้ขอบมน borderSide: BorderSide( color: Colors.grey[100]!, // สีขอบอ่อน width: 1.0, // ความกว้างของขอบ ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), // ปรับให้ขอบมน borderSide: BorderSide( color: Colors.grey[100]!, // สีขอบอ่อน width: 1.0, // ความกว้างของขอบ ), ), filled: true, fillColor: Colors.white, prefixIcon: IconButton( // ปุ่มค้นหาที่ด้านซ้าย icon: const Icon(Icons.search), onPressed: () { // ทำการค้นหาสถานที่ตามชื่อที่กรอกในช่องค้นหา String searchValue = _searchController.text; if (searchValue.isNotEmpty) { print("Search for $searchValue"); getCoordinatesFromPlace(searchValue); FocusScope.of(context).unfocus(); // ยกเลิก focus } }, ), // แสดงปุ่ม clear เฉพาะเมื่อข้อความไม่ว่าง suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { // ล้างข้อความใน TextField _searchController.clear(); setState(() {}); // อัปเดต UI หลังจากลบข้อความ }, ) : null, contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), // ปรับ padding ของเนื้อหา ), onSubmitted: (value) { // ทำการค้นหาสถานที่ตามชื่อที่กรอก print("Enter ${value}"); if(value.isNotEmpty){ getCoordinatesFromPlace(value); FocusScope.of(context).unfocus(); // ยกเลิก focus } }, ), ), ), // ส่วนควบคุมการ Zoom ที่เราสร้างเอง Positioned( top: 16, right: 16, child: Column( children: [ FloatingActionButton( mini: true, onPressed: () async { // ควบคุมการ Zoom In final controller = await _controller.future; controller.animateCamera(CameraUpdate.zoomIn()); }, heroTag: 'zoomin', // Set a unique tag ต้องกำหนด child: const Icon(Icons.add), ), const SizedBox(height: 8), FloatingActionButton( mini: true, onPressed: () async { // ควบคุมการ Zoom Out final controller = await _controller.future; controller.animateCamera(CameraUpdate.zoomOut()); }, heroTag: 'zoomout', // Set a unique tag ต้องกำหนด child: const Icon(Icons.remove), ), ], ), ), // ส่วนควบคุมตำแหน่งปัจจุบัน ที่เราสร้างเอง Positioned( bottom: 16, right: 16, child: FloatingActionButton( onPressed: () { // ไปยังตำแหน่ง จากพิกัด _goToPosition(_initialPosition!); }, heroTag: 'mylocation', // Set a unique tag ต้องกำหนด child: const Icon(Icons.my_location), ), ), ], ), ); } }
ผลลัพธ์ที่ได้
จากตัวอย่าง เมื่อเรากดเพิ่มตัว marker ก็จะแสดงระยะทางและเวลาเดินทางจากตำแหน่งตัว marker
หลัก(ไอคอนสีเหลือง) โดยวัดจากเส้นทางใน google map ที่มีข้อมูล และทุกครั้งที่เราย้ายตำแหน่ง
ของตัว marker ก็จะให้ไปเรียกข้อมูลใหม่ จากพิกัดที่ได้เปลี่ยนแปลง
ในตัวอย่างเป็นการหาระยะทางของ 2 จุดพิกัดเท่านั้น ถ้าเราต้องการหาข้อมูลหลายๆ จุดพร้อมกันเช่น
สมมติเรากำหนด marker ไป 3 ตัว และอย่างรู้ว่าตัวไหน อยู่ใกล้ตำแหน่งหลักมาที่สุด หรืออยากให้เรียง
ลำดับแต่ละตัว marker จากใกล้สุดไปไกลสุด ดังนั้นเพื่อประยุกต์แนวทางนี้ เราจะไม่เรียกใช้งานคำสั่ง
api ทันทีที่กำหนดพิกัด แต่จะต้องกำหนดพิกัดให้เรียบร้อยแล้ว ครบจำนวนที่ต้องการ แล้วมีปุ่มด้านล่าง
ให้กดแสดงการทำงานว่า ตำแหน่งไหนอยู่ไกล หรือไกล้อย่างไร สามารถปรับได้ดังนี้
ไฟล์ map.dart (เรียงระยะทาง marker ใกล้สุดก่อน)
import 'dart:async'; import 'dart:ui' as ui; import 'dart:math'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:geolocator/geolocator.dart'; import 'package:geocoding/geocoding.dart'; import 'package:http/http.dart' as http; class Maps extends StatefulWidget { static const routeName = '/map'; const Maps({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _MapsState(); } } class _MapsState extends State<Maps> { // กำนหนดตัวควบคุมให้กับแผนที่ สังเกตว่า มีการใช้แบบ Completer ซึงในภาษา Dart // เป็นคลาสที่ใช้ในการควบคุมการทำงานของ Future ดังนั้น ตัว controller จะถูกสร้างก็เมื่อตัวแผนที่ // โหลดเรียบร้อยแล้วเท่านั้น เพื่อให้พร้อมใช้งาน final Completer<GoogleMapController> _controller = Completer<GoogleMapController>(); // การกำหนดตำแหน่งต่างๆ จะใช้เป็นข้อมูล CameraPosition // ในตัวอย่างเรากำหนดตัวแปรไว้ 2 ตัว CameraPosition? _initialPosition; // ตัวแปรสำหรับตำแหน่งเริ่มต้น ยังไม่กำหนดค่าใดๆ // ตัวอย่งการกำหนดตำแหน่งแบบตายตัว สมมติเป็นห้าง มาบุญครอง static const CameraPosition _place_mbk = CameraPosition( bearing:0.0, // องศาทิศทางของกล้องตามเข็มนาฬิกาจากทิศเหนือ (หมุนแกนนอน) target: LatLng(13.745324385325134, 100.52999353076734), // พิกัด Lat Long tilt: 0.0, // องศามุมที่กล้องหันตรงไปที่พื้นโลก (หมุนแกนตั้ง) zoom: 14.0 // ระดะับการขยาย ); // สร้าง Set ของ Marker final Set<Marker> _markers = {}; List<LatLng> _markerPoints = []; // สำกำหนดจุดพิกัดตัว marker BitmapDescriptor? _markerIcon; // กำหนดไอคอนจากรูป final MarkerId _markerId = const MarkerId('current_location'); LatLng? _activeMarkerPosition; // ตัวแปรสำหรับเก็บตำแหน่งของ Marker ที่ถูกลาก // กำหนดตัวแปรไว้นับจำนวน marker แลัวไว้สร้าง ค่าไม่ซ้ำไว้ใช้งาน int _markerCount = 0; // กำหนดส่วนของ polyline ที่จะใช้กับแผนที่ Set<Polyline> _polylines = {}; // สำหรับกำหนดโหมดใช้งาน bool _marker_mode = false; // สำหรับวางจุดเท่านั้น // เพิ่มส่วนจัดการเกี่ยวข้อมูลชื่อสถานที่ final TextEditingController _searchController = TextEditingController(); final FocusNode _focusNode = FocusNode(); // สร้าง FocusNode สำหรับ TextField @override void initState() { super.initState(); // ขอสิทธิการเข้าถึงพิกัดตำแหน่ง requestPermissions(); _getCurrentLocation(); // เตรียมไอคอน prepareIcons(); } // ขอ permission สำหรับจัดการตำแหน่ง void requestPermissions() async { await Permission.location.request(); } // สร้างไอคอนจากรูปใน assets void prepareIcons() async { _markerIcon = await BitmapDescriptor.asset( const ImageConfiguration(size: Size(41.5, 48)), // ขนาดของไอคอนที่ต้องการแสดง // const ImageConfiguration(), // ขนาดของไอคอนที่ต้องการแสดงใช้ตามขนาดรูป 'assets/icon/marker.png', // พาธของ asset ขนาดรูปตัวอย่างที่ทดสอบคือ 32x37 ); } // ฟังก์ชันเพื่อดึงตำแหน่งปัจจุบันของผู้ใช้ Future<void> _getCurrentLocation() async { bool serviceEnabled; LocationPermission permission; // ตรวจสอบว่าเปิด Location Services ไว้หรือไม่ serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // หาก Location Services ปิด แสดงข้อความหรือจัดการตามต้องการ return; } // ตรวจสอบ permission สำหรับตำแหน่งที่ตั้ง permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { // Permission ถูกปฏิเสธ return; } } if (permission == LocationPermission.deniedForever) { // Permission ถูกปฏิเสธอย่างถาวร return; } // หาก permission ได้รับการอนุญาต ดึงตำแหน่งที่ตั้งปัจจุบัน Position position = await Geolocator.getCurrentPosition(); // กำหนดตำแหน่งเริ่มต้นเป็นตำแหน่งของผู้ใช้ setState(() { // กำหนดค่าให้กับตำแหน่งเริ่มต้น แล้วสร้างรูปแบบพิกัดไว้ใช้งาน // ค่า bearing และ tilt หากไม่กำหนด ก็จะใช้เป็น 0 ตามค่าเริ่มต้น ในที่นี้เราใช้แค่ // ตำแหน่ง กับ การ zoom _initialPosition = CameraPosition( target: LatLng(position.latitude, position.longitude), zoom: 14.0, ); // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = LatLng(position.latitude, position.longitude); // เพิ่ม Marker สำหรับตำแหน่งปัจจุบัน _markers.add( Marker( markerId: _markerId, position: _activeMarkerPosition!, icon: _markerIcon!, // กำหนดไอคอนจาก asset draggable: true, // เปิดใช้งานการลาก marker infoWindow: InfoWindow( title: 'ตำแหน่งของคุณ', // ข้อความเพิ่มเติมที่จะแสดงใน InfoWindow snippet: '${_activeMarkerPosition!.latitude}, ${_activeMarkerPosition!.longitude}', onTap: () async { print("hide infowindow"); final GoogleMapController controller = await _controller.future; controller.hideMarkerInfoWindow(_markerId); } ), onTap: () async { }, onDragEnd: (LatLng newPosition) { print("Marker dropped at: ${newPosition.latitude}, ${newPosition.longitude}"); // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = LatLng(newPosition.latitude, newPosition.longitude); // สามารถจัดการตำแหน่งใหม่ได้ที่นี่ เช่น อัปเดตตำแหน่งในฐานข้อมูลหรือแสดงข้อมูลใหม่ CameraPosition _newPosition = CameraPosition( target: LatLng(newPosition.latitude, newPosition.longitude), zoom: 14.0, ); // เรียกใช้ ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป _goToPosition(_newPosition); // เรียกใช้งานฟังก์ชัน อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarkerSnippet(_markerId,_activeMarkerPosition!); }, ), ); }); // เรียกใช้ GoogleMapController เพื่อแสดง InfoWindow อัตโนมัติ final GoogleMapController controller = await _controller.future; controller.showMarkerInfoWindow(_markerId); } // ฟังก์ชั่น อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง void _updateMarkerSnippet(MarkerId markerId, LatLng newPosition) { // ตรวจสอบหาตัว markder เดิม ที่ไอดีตรงกับที่ส่งมา Marker? oldMarker = _markers.firstWhere( (marker) => marker.markerId == markerId, orElse: () => null as Marker, // ใช้ cast เป็น markder เพื่อเลี่ยง null safely ); if (oldMarker != null) { // ถ้าไม่ใช่ null // ลบ markder ตัวเดิม _markers.remove(oldMarker); // กำหนดข้อมูลที่แสดงใน infowindow ใหม่ เป็นค่าพิกัดใหม่ String newSnippet = '${newPosition.latitude}, ${newPosition.longitude}'; // เพิ่มตัว markder ตัวใหม่เข้าไป _markers.add( Marker( markerId: markerId, // ใช้ไอดีเดิม position: newPosition, // ตำแหน่งใหม่ icon: _markerIcon!, // กำหนดไอคอนจาก asset draggable: true, // เปิดใช้งานการลาก marker infoWindow: InfoWindow( title: oldMarker.infoWindow.title, // ใช้จากค่าเดิม snippet: newSnippet, // อัปเดทค่าใหม่ onTap: oldMarker.infoWindow.onTap, // ใช้จากค่าเดิม ), onTap: oldMarker.onTap, // ใช้จากค่าเดิม onDragEnd: oldMarker.onDragEnd, // ใช้จากค่าเดิม ), ); // rebuild เพื่ออัปเดทแผนที่ setState(() {}); } } // ฟังก์ชั่นเฉพาะ กรณีเราต้องการให้ไปยังตำแหน่ง ที่เจาะจง ในที่นี้คือไปตำแหน่ง ห้าง มาบุญครอง Future<void> _goToMBK() async { final GoogleMapController controller = await _controller.future; await controller.animateCamera(CameraUpdate.newCameraPosition(_place_mbk)); } // ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป Future<void> _goToPosition(CameraPosition postion) async { final GoogleMapController controller = await _controller.future; await controller.animateCamera(CameraUpdate.newCameraPosition(postion)); // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = postion.target!; // อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarkerSnippet(_markerId,postion.target!); // เรียกใช้ GoogleMapController เพื่อแสดง InfoWindow อัตโนมัติ controller.showMarkerInfoWindow(_markerId); } // ฟังก์ชั่นการกดที่แผนที่ แล้วไปยังตำแหน่งที่ต้องการ Future<void> _onMapTapped(LatLng position) async { // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = LatLng(position.latitude, position.longitude); // สามารถจัดการตำแหน่งใหม่ได้ที่นี่ เช่น อัปเดตตำแหน่งในฐานข้อมูลหรือแสดงข้อมูลใหม่ CameraPosition _newPosition = CameraPosition( target: LatLng(position.latitude, position.longitude), zoom: 14.0, ); // เรียกใช้ ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป _goToPosition(_newPosition); // เพิ่มการเรียกใช้ฟังก์ชั่น แสดงชื่อสถานที่จากพิกัด getPlaceFromCoordinates(position.latitude,position.longitude); } // ฟังก์ชั่น อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง Future<void> _updateMarker(MarkerId markerId, LatLng newPosition) async { // ตรวจสอบหาตัว markder เดิม ที่ไอดีตรงกับที่ส่งมา Marker? oldMarker = _markers.firstWhere( (marker) => marker.markerId == markerId, orElse: () => null as Marker, // ใช้ cast เป็น markder เพื่อเลี่ยง null safely ); if (oldMarker != null) { // ถ้าไม่ใช่ null // ลบ markder ตัวเดิม _markers.remove(oldMarker); // กำหนดข้อมูลที่แสดงใน infowindow ใหม่ เป็นค่าพิกัดใหม่ String newSnippet = '${newPosition.latitude}, ${newPosition.longitude}'; markerId = MarkerId(newPosition.toString()); // เพิ่มตัว markder ตัวใหม่เข้าไป _markers.add( Marker( markerId: markerId, // ใช้ไอดีเดิม position: newPosition, // ตำแหน่งใหม่ icon: oldMarker.icon, // กำหนดไอคอนจาก asset draggable: true, // เปิดใช้งานการลาก marker infoWindow: InfoWindow( title: oldMarker.infoWindow.title, // ใช้จากค่าเดิม snippet: newSnippet, // อัปเดทค่าใหม่ onTap: oldMarker.infoWindow.onTap, // ใช้จากค่าเดิม ), onTap: oldMarker.onTap, // ใช้จากค่าเดิม onDragEnd: (LatLng newPosition) { print("drop marker ${newPosition.toString()}"); // เรียกใช้งานฟังก์ชัน อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarker(markerId,newPosition); } ), ); // ค่าใหม่ที่จะแทนที่ LatLng newLocation = newPosition; // ค่าใหม่ที่จะแทนที่ // หาค่าเดิมใน List int index = _markerPoints.indexWhere((latLng) => latLng.latitude == oldMarker.position.latitude && latLng.longitude == oldMarker.position.longitude); if (index != -1) { // แทนที่ค่าเดิมด้วยค่าใหม่ ถ้ามี _markerPoints[index] = newLocation; } // rebuild เพื่ออัปเดทแผนที่ setState(() {}); } } // ฟังก์ชั่นสำหรับเพิ่มตัว marker ไปยังจุดที่กดแตะในแผนที่ กรณีใช้งานโหมด ปักหมุด polyline และ polygon Future<void> _addMarkerOnMap(LatLng tappedPosition) async { _markerCount++; // เพิ่มค่าสำหรับใช้กำหนด ข้อความ ที่เป็นตัวเลข ใน marker ไม่ให้ซ้ำกัน // นำค่าตัวเลขที่ไม่ซ้ำ ส่งเข้าไปในฟังก์ชั่นสร้างรูป ไอคอน สำหรับ marker final customMarker = await _createCustomMarker('$_markerCount'); setState(() { // เก็บค่าตำแหน่ง marker ต้วก่อนหน้า เพื่อใช้สำหรับการ เชื่อมเป็น polyline หรือ polygon LatLng lastMarkerPosition = _markers.last.position; MarkerId markerId = MarkerId(tappedPosition.toString()); // ถ้าใช้งานโหมด marker if(_marker_mode){ // เพิ่มตำแหน่งใน List สำหรับ marker _markerPoints.add(tappedPosition); // เพิ่มจุดที่ผู้ใช้แตะเพื่อสร้าง marker // ทำการเพิ่มตัว marker เมื่อกดแตะที่แผนที่ _markers.add( Marker( // ใช้ค่าพิกัดเป็น id ของ marker จะได้ไม่ซ้ำกัน ค่านี้จะขึ้นต้นด้วย LatLng markerId: markerId, position: tappedPosition, // เพิ่มตรงตำแหน่งที่แตะ icon: customMarker, // ใช้ไอคอนไม่ซ้ำรูปแบบกัน infoWindow: InfoWindow( // ส่วนแสดง info title: 'Marker $_markerCount', // Title for the marker snippet: '${tappedPosition.latitude}, ${tappedPosition.longitude}', ), draggable: true, onDragEnd: (LatLng newPosition) { print("drop marker ${tappedPosition.toString()}"); // เรียกใช้งานฟังก์ชัน อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarker(markerId,newPosition); } ), ); } }); } // สร้างฟังก์ชั่นสำหรับสร้างตัว marker Future<BitmapDescriptor> _createCustomMarker(String text) async { // ตัวจัดการการ render ภาพ final recorder = ui.PictureRecorder(); // ตัวจัดการพื้นที่วาดภาพ หรือสร้างรูปภาพ final canvas = Canvas(recorder); // กำหนดรูปแบบการวาด และขนาด final paint = Paint() ..color = Colors.green // สีพื้นหลัง ..style = PaintingStyle.fill; // ใช้แบบเติมสี final radius = 24.0; // กำหนดรัศมีของวงลม ที่จะทำ marker final pinHeight = 12.0; // ความสูงของสามเหลี่ยมที่เราจะทำเป็นปลายปักหมุด // วาดรูปวงกลม ส่วนหลัก เข้าไปก่อน canvas.drawCircle(Offset(radius, radius), radius, paint); // วาดจุดสำหรับสร้างรูป สามเหลี่ยม final trianglePath = Path(); // จุดเริ่มต้นของสามเหลี่ยม trianglePath.moveTo(radius - pinHeight, (radius * 2) - (pinHeight / 2)); // จุดที่สอง ลากไปทางขวา จากจัดแรก trianglePath.lineTo(radius + pinHeight, (radius * 2) - (pinHeight / 2)); // จัดที่สาม จุดตรงกลาง ยื่นลงมาเป็นจุด ปักหมด trianglePath.lineTo(radius, radius * 2 + pinHeight); // เปิดจุดการวาด จากจุดที่สาม ไปจุดเริ่มแรก เพื่อปิด trianglePath.close(); // วาดรูปสามเหลี่ยมลงในพื้นที่วาด ตามค่าที่กำหนด canvas.drawPath(trianglePath, paint); // การใส่ตัวเลขเข้าไปในภาพ จัดรูปแบบต่างๆ final textPainter = TextPainter( text: TextSpan( text: text, // ใช้การรับค่ามาแสดง style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr,// การเรียงซ้ายไปขวา ); // กำหนดส่วนจัดวางตัวเลขหรือข้อความ ในที่นี้ใช้ค่าเริ่มต้น textPainter.layout(); // วาดข้อความลงไปในพื้นที่วาด จัดตำแหน่งอ้างอิงจากค่าต่างๆ ที่เกี่ยวข้อง ในที่นี้คือไว้ตรงกลางรูปวงกลม textPainter.paint( canvas, Offset(radius - textPainter.width / 2, radius - textPainter.height / 2)); // ใช้ตัว render แปลงจาก canvas ไปเป็นรูปภาพ final picture = recorder.endRecording(); // สร้างรูปภาพ กำหนดขนาด ตามต้องการ ในที่นี้ใช้การคำนวณ กว้างเท้ากับขนาดของวงกลม // ส่วนความสูงให้เท่ากับขนาดของวงกลม บวกกับความสูงของ สามเหลี่ยม final img = await picture.toImage((radius * 2).toInt(), ((radius * 2) + pinHeight).toInt()); // เปลงเป็น binary data เพื่อนำไปใช้งาน final byteData = await img.toByteData(format: ui.ImageByteFormat.png); final pngBytes = byteData!.buffer.asUint8List(); // สร้าง BitmapDescriptor ที่ใช้สำหรับเป็นไอคอน จากข้อมูล binary ของรูปภาพ return BitmapDescriptor.bytes(pngBytes); } // ฟังก์ชั่นสำหรับเปลี่ยนโหมด Future<void> _changeMode({bool marker = false}) async{ setState(() { _marker_mode = marker; _markerPoints.clear(); // ล้างค่าจุดของ marker _markerCount = 0; // เริ่มนับค่าใหม่ _polylines.clear(); // ล้าง polyline ออกจากแผนที่ // ลบตัว marker ทั้งหมดที่ไอดี ขึ้นต้นด้วยคำว่า 'LatLng' _markers.removeWhere( (marker) => marker.markerId.value.startsWith('LatLng')); }); } // ส่วนของฟังก์ชั่นแสดงการแจ้งเตือน void _showResultSnackbar(String result) { // ปิด SnackBar ที่แสดงอยู่ก่อน ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Container( // height: 100, // ปรับขนาดความสูงของการแสดง ปิดการกำหนดตายตัว ให้ขยายขนาดตามข้อมูล child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( result, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18, ), ), ], ), ), // duration: Duration(seconds: 100), // กำหนดแสดงชั่วคราว duration: const Duration(days: 365), // กำหนดแสดงแบบถาวร behavior: SnackBarBehavior.floating, // floating / fixed backgroundColor: Colors.grey[850], action: SnackBarAction( label: 'Close', textColor: Colors.white, onPressed: () { // เมื่อกดปิด ScaffoldMessenger.of(context).hideCurrentSnackBar(); // Dismisses the SnackBar }, ), ), ); } // ฟังก์ชั่นสำหรับหาพิกัดจากซื้อสถานที่ Future<void> getCoordinatesFromPlace(String placeName) async { try { List<Location> locations = await locationFromAddress(placeName); if (locations.isNotEmpty) { // แสดงพิกัด latitude และ longitude double latitude = locations[0].latitude; double longitude = locations[0].longitude; // สามารถจัดการตำแหน่งใหม่ได้ที่นี่ CameraPosition _newPosition = CameraPosition( target: LatLng(latitude,longitude), zoom: 14.0, ); // เรียกใช้ ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป _goToPosition(_newPosition); print('พิกัดของ $placeName: $latitude, $longitude'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('พิกัดของ $placeName: $latitude, $longitude'); } } catch (e) { print('ไม่สามารถค้นหาพิกัดจากชื่อสถานที่ได้: $e'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('ไม่สามารถค้นหาพิกัดจากชื่อสถานที่ได้: $e'); } } // ฟังก์ชั่นหาชื่อสถานที่จากค่าพิกัด Future<void> getPlaceFromCoordinates(double latitude, double longitude) async { try { List<Placemark> placemarks = await placemarkFromCoordinates(latitude, longitude); if (placemarks.isNotEmpty) { // แสดงข้อมูลสถานที่ที่ค้นพบ Placemark place = placemarks[0]; print('สถานที่จากพิกัด: ${place.street}, ${place.locality}, ${place.country}'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('สถานที่จากพิกัด: ${place.street}, ${place.locality}, ${place.country}'); } } catch (e) { print('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); } } // ฟังก์ชั่นหาระยะทางของเส้นทางจาก DistanceMatrix http://niik.in/832 Future<void> getDistanceMatrix(LatLng origin, LatLng destination) async { String _origin = '${origin.latitude},${origin.longitude}'; String _destination = '${destination.latitude},${destination.longitude}'; String _apiKey = 'Google Map API Key'; final url = Uri.parse( 'https://maps.googleapis.com/maps/api/distancematrix/json' '?origins=$_origin' '&destinations=$_destination' '&language=th' '&key=$_apiKey', ); // สามารถกำหนด option อื่นๆ เพิ่มเติมได้ ดูจากลิ้งค์ด้านล่าง // https://developers.google.cn/maps/documentation/distance-matrix/distance-matrix?hl=th final response = await http.get(url); if (response.statusCode == 200) { final data = jsonDecode(response.body); /* ระยะทางระหว่างต้นทางและปลายทาง (ในหน่วยต่างๆ เช่น กิโลเมตร เมตร) เวลาที่ใช้ในการเดินทาง (ในหน่วยต่างๆ เช่น วินาที) */ // ข้อความที่แสดงระยะทางจะมีการปัดเศษโดยประมาณ (เช่น "12.7 เมตร") var distance = data['rows'][0]['elements'][0]['distance']['text']; // ข้อความที่แสดงเวลาเดินทางจะมีการปัดเศษโดยประมาณ (เช่น "13 นาที") var duration = data['rows'][0]['elements'][0]['duration']['text']; var distanceValue = data['rows'][0]['elements'][0]['distance']['value']; // ระยะทางในหน่วยเมตร var durationValue = data['rows'][0]['elements'][0]['duration']['value']; // เวลาในหน่วยวินาที print('Distance: $distance'); print('Duration: $duration'); print('Distance Value: $distanceValue'); print('Duration Value: $durationValue'); _showResultSnackbar( 'Distance: $distance\r\n' 'Duration: $duration\r\n' 'Distance Value: $distanceValue\r\n' 'Duration Value: $durationValue' ); } else { // กรณีไม่มีเส้นทางในฐานข้อมูล Google Map _showResultSnackbar('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); // throw Exception('Failed to load distance matrix'); } } // ฟังก์ชั่นหาระยะทางของเส้นทางจาก DistanceMatrix ต้นทางเดียวกัน หลายปลายทาง http://niik.in/832 Future<void> getDistanceMatrixCompare(LatLng origin, List<LatLng> destinations) async { // จัดรูปแบบปลายทางให้อยู่ในรูปแบบ พิกัด|พิกัด|พิกัด|พิกัด String formattedDestinations = destinations.map( (destination) => '${destination.latitude},${destination.longitude}').join('|'); String _origin = '${origin.latitude},${origin.longitude}'; String _destination = formattedDestinations; // ปลายทางแบบหลายพิกัด String _apiKey = 'Google Map API Key'; final url = Uri.parse( 'https://maps.googleapis.com/maps/api/distancematrix/json' '?origins=$_origin' '&destinations=$_destination' '&language=th' '&key=$_apiKey', ); // สามารถกำหนด option อื่นๆ เพิ่มเติมได้ ดูจากลิ้งค์ด้านล่าง // https://developers.google.cn/maps/documentation/distance-matrix/distance-matrix?hl=th final response = await http.get(url); if (response.statusCode == 200) { final data = jsonDecode(response.body); /* ระยะทางระหว่างต้นทางและปลายทาง (ในหน่วยต่างๆ เช่น กิโลเมตร เมตร) เวลาที่ใช้ในการเดินทาง (ในหน่วยต่างๆ เช่น วินาที) */ List<dynamic> rows = data['rows']; List<Map<dynamic, dynamic>> _distanceResult = []; // กำหนดตัวแปรเก็บค่าไว้เปรียบเทียบ var row = rows[0]; // วนลูปค่าระยะทางของแต่ละพิกัดที่ได้ผลลัพธ์กลับมา for (var elementIndex = 0; elementIndex < row['elements'].length; elementIndex++) { String _result = ""; var element = row['elements'][elementIndex]; var distance = element['distance']['text']; var duration = element['duration']['text']; var distanceValue = element['distance']['value']; // ในหน่วยเมตร var durationValue = element['duration']['value']; // ในหน่วยวินาที // จัดรูปแบบข้อความที่เราต้องการแสดง _result = '${elementIndex+1}: ระยะทาง: $distance, ใช้เวลา: $duration\r\n'; // เพิ่มข้อมูลไว้ในตัวแปรสำหรับเปรียบเทียบและแสดงผล ในที่นี้ให้ value เป็นระยะทาง เราจะใช้เปรียบเทียบใกล้ หรือไกลกว่ากัน _distanceResult.add({'id': elementIndex+1, 'value': distanceValue, 'text': _result}); } _distanceResult.sort((a, b) => a['value'].compareTo(b['value'])); // เรียงน้อยไปมาก // _distanceResult.sort((a, b) => b['value'].compareTo(a['value'])); // เรียงมากไปน้อย // วนลูปเอาเฉพาะส่วนของ text ข้อความผลลัพธ์ หลังจาก เรียงข้อมุลใหม่แล้วมาแสดง String result = _distanceResult.map((item) => item['text']).join('\r\n'); _showResultSnackbar(result); } else { // กรณีไม่มีเส้นทางในฐานข้อมูล Google Map _showResultSnackbar('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); // throw Exception('Failed to load distance matrix'); } } @override void dispose() { // การล้างค่า เมื่อไม่ได้ใช้งานแล้ว _searchController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('แผนที่'), actions: [ IconButton( onPressed: (){ _changeMode(); }, icon: Image.asset('assets/icon/pan${(!_marker_mode) ? '_active' : ''}.png',), ), IconButton( onPressed: (){ _changeMode(marker: true); }, icon: Image.asset('assets/icon/mark${(_marker_mode) ? '_active' : ''}.png',), ), ], ), body: _initialPosition == null // กรณียังไม่ได้ตำแหน่ง แสดงตัว Loading ก่อน ? const Center(child: CircularProgressIndicator()) // Loading หรือจะใช้รูปแบบอื่นๆ แทนได้ถ้าต้องการ : Stack( // ใช้ stack ในการวางซ้อนแผนที่ รองรับการเพิ่มเมนูไอคอนต่างๆ children: [ GoogleMap( mapType: MapType.normal, // normal | terrain | satellite | hybrid initialCameraPosition: _initialPosition!, // ใช้ค่าเริ่มต้นจากตำแหน่งของเรา onMapCreated: (GoogleMapController controller) { // ทำให้ Future สำเร็จเมื่อแผนที่ถูกสร้างเสร็จแล้ว // ตัว controller พร้อมใช้งาน _controller.complete(controller); }, // ในที่นี้เราจtซ่อนตัวปุ่ม zoom และกำหนดเอง จากปุ่มที่เราสร้าง zoomControlsEnabled: false, // ซ่อนค่าเริ่มต้นปุ่ม zoom // ในที่นี้เราจะซ่อนตัวปุม ตำแหน่งปัจจุบัน และกำหนดเองจากปุ่มที่เราสร้าง myLocationButtonEnabled: false, // ซ่อนปุ่มตำแหน่งปัจจุบัน myLocationEnabled: false, // แสดงรูป จุด ของตำแหน่งปัจจุบัน markers: _markers, // เพิ่ม markers ใน GoogleMap polylines: _polylines, // เพิ่มการใช้งาน polylines onTap: (_marker_mode) // ถ้าอยู่ในโหมดใดโหมดหนึ่ง ? _addMarkerOnMap // จัดการเกี่ยวกับ marker polyline polygon : _onMapTapped, // เมื่อกดที่แผนที่ให้ทำคำสั่ง ย้ายตำแหน่งไปยังจุดที่ต้องการ ), // ช่องค้นหาชื่อสถานที่ Positioned( top: 16, left: 16, child: Container( width: 250, // ความกว้างของช่องค้นหา child: TextField( controller: _searchController, // ควบคุมค่าใน TextField focusNode: _focusNode, // เชื่อมต่อ FocusNode กับ TextField minLines: 1, // จำนวนบรรทัดขั้นต่ำ maxLines: null, // ให้ขยายได้ไม่จำกัดตามเนื้อหา decoration: InputDecoration( hintText: 'ค้นหาสถานที่...', hintStyle: TextStyle( color: Colors.grey[500], // สีอ่อนสำหรับ hintText ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), // ปรับให้ขอบมน borderSide: BorderSide( color: Colors.grey[100]!, // สีขอบอ่อน width: 1.0, // ความกว้างของขอบ ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), // ปรับให้ขอบมน borderSide: BorderSide( color: Colors.grey[100]!, // สีขอบอ่อน width: 1.0, // ความกว้างของขอบ ), ), filled: true, fillColor: Colors.white, prefixIcon: IconButton( // ปุ่มค้นหาที่ด้านซ้าย icon: const Icon(Icons.search), onPressed: () { // ทำการค้นหาสถานที่ตามชื่อที่กรอกในช่องค้นหา String searchValue = _searchController.text; if (searchValue.isNotEmpty) { print("Search for $searchValue"); getCoordinatesFromPlace(searchValue); FocusScope.of(context).unfocus(); // ยกเลิก focus } }, ), // แสดงปุ่ม clear เฉพาะเมื่อข้อความไม่ว่าง suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { // ล้างข้อความใน TextField _searchController.clear(); setState(() {}); // อัปเดต UI หลังจากลบข้อความ }, ) : null, contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), // ปรับ padding ของเนื้อหา ), onSubmitted: (value) { // ทำการค้นหาสถานที่ตามชื่อที่กรอก print("Enter ${value}"); if(value.isNotEmpty){ getCoordinatesFromPlace(value); FocusScope.of(context).unfocus(); // ยกเลิก focus } }, ), ), ), // ส่วนควบคุมการ Zoom ที่เราสร้างเอง Positioned( top: 16, right: 16, child: Column( children: [ FloatingActionButton( mini: true, onPressed: () async { // ควบคุมการ Zoom In final controller = await _controller.future; controller.animateCamera(CameraUpdate.zoomIn()); }, heroTag: 'zoomin', // Set a unique tag ต้องกำหนด child: const Icon(Icons.add), ), const SizedBox(height: 8), FloatingActionButton( mini: true, onPressed: () async { // ควบคุมการ Zoom Out final controller = await _controller.future; controller.animateCamera(CameraUpdate.zoomOut()); }, heroTag: 'zoomout', // Set a unique tag ต้องกำหนด child: const Icon(Icons.remove), ), ], ), ), // ส่วนควบคุมตำแหน่งปัจจุบัน ที่เราสร้างเอง Positioned( bottom: 16, right: 16, child: FloatingActionButton( onPressed: () { // ไปยังตำแหน่ง จากพิกัด _goToPosition(_initialPosition!); }, heroTag: 'mylocation', // Set a unique tag ต้องกำหนด child: const Icon(Icons.my_location), ), ), // ส่วนปุ่มคำนวณระยะทาง if (_marker_mode) Positioned( bottom: 16, left: 16, child: ElevatedButton( onPressed: () async { if(_markerPoints.length < 1){ _showResultSnackbar('กำหนดจุดพิกัดอย่างน้อย 1 จุด'); }else{ // คำนวณระยะทางตำแหน่งปัจจุบันกับ ตัว marker หลายๆ จุด getDistanceMatrixCompare(_activeMarkerPosition!,_markerPoints); } }, child: Text('คำนวณระยะทาง', style: TextStyle( fontSize: 18, ),), ), ), ], ), ); } }
ผลลัพธ์ที่ได้
เรากำหนดจุดพิกัดไว้ทั้งหมด 4 จุด และเมื่อทำการเรียกใช้ api เพื่อหาระยะเส้นทาง ก็จะแจ้งสถานที่ แต่ละ
ตำแหน่งจากระยะทางจากใกล้สุดไปไกลสุด หรือค่า value ของระยะทาง ในตัวอย่าง ตำแหน่งที่ 2 อยู่ไกล
สุด จากน้้นเราลองปิดผลการแจ้งเตือน แล้วย้ายตำแหน่งตัว marker ที่ 2 มาไว้ใกล้ๆ กับพิกัดหลัก แล้ว
ทำการคำนวณหาระยะทางใหม่ ก็จะพบว่า ระบบก็จะดึงข้อมูลใหม่ และเรียงตำแหน่งที่ 2 เป็นลำดับแรก
เพราะเป็นลำดับตำแหน่งที่ใกล้ที่สุด แบบนี้เป็นต้น เราสามารถนำไปปรับประยุกต์เพิ่มเติมได้ตามต้องการ
อย่างไรก็ดีส่วนของ Distance Matrix API จะไม่รองรับการสร้างเส้นทางได้ เพราะไม่มีค่า Route
หรือพิกัดเส้นทางส่งกลับมา จะใช้เฉพาะการหาระยะทางและเวลาเดินทางเป็นหลัก
การใช้งาน Direction Service สร้าง Route เส้นทาง
การหาระยะทางด้วย distance matrix ที่ใช้ค่าระยะทางจากเส้นทางถนนที่สามารถผ่านได้ เราจะได้
แค่ค่าของระยะทางและเวลาการเดินทางเป็นหลัก แต่ถ้าเราต้องการทั้งระยะทาง เวลาเดินทาง และต้องการ
เส้นทางหรือ route เราจะต้องมาใช้งาน Direction Service โดยรูปแบบการใช้งานก็จะคล้ายๆ กับ
การใช้งาน Distance Matrix คือการส่งค่าไปดึงข้อมูลผ่าน API โดยใช้ตำแหน่งเริ่มต้น และตำแหน่ง
ปลายทาง แล้วตัว API จะเลือกเส้นทางที่ดีสุดกลับมาให้ โดยจะคำนึงถึง ระยะทาง สภาพจราจร และอื่นๆ
ที่เกี่ยวข้อง นั่นหมายความว่า อาจจะใช่ระยะทางที่ใกล้ที่สุดก็เป็นได้
ในที่นี้เราจะประยุกต์จากโค้ดด้านบนเล็กน้อย คือเพิ่มส่วนของฟังก์ชั่นการเรียก api เข้ามา และเรา
กำหนดให้ปักหมดได้แค่ 1 ตำแหน่งเพื่อกำหนดปลายทาง ทันทีที่ปักหมุด ก็จะทำการส่งค่าไปดึงค่า api
โดยจะได้ค่า route หรือตำแหน่งพิกัดเส้นทางหรือจุดผ่านของเส้นทางทั้งหมด จากนั้นเราก็นำมาสร้าง
เป็นเส้น polyline เพื่อเป็นเส้นทางในแผนที่ สามารถกดลากตัว marker เพื่อเปลี่ยนเป้าหมายและ
เส้นทางก็จะสร้างใหม่ ถ้ามีเส้นทางของพิกัดนั้น
ไฟล์ map.dart (สร้างเส้นทางด้วย polyline)
import 'dart:async'; import 'dart:ui' as ui; import 'dart:math'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:geolocator/geolocator.dart'; import 'package:geocoding/geocoding.dart'; import 'package:http/http.dart' as http; class Maps extends StatefulWidget { static const routeName = '/map'; const Maps({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _MapsState(); } } class _MapsState extends State<Maps> { // กำนหนดตัวควบคุมให้กับแผนที่ สังเกตว่า มีการใช้แบบ Completer ซึงในภาษา Dart // เป็นคลาสที่ใช้ในการควบคุมการทำงานของ Future ดังนั้น ตัว controller จะถูกสร้างก็เมื่อตัวแผนที่ // โหลดเรียบร้อยแล้วเท่านั้น เพื่อให้พร้อมใช้งาน final Completer<GoogleMapController> _controller = Completer<GoogleMapController>(); // การกำหนดตำแหน่งต่างๆ จะใช้เป็นข้อมูล CameraPosition // ในตัวอย่างเรากำหนดตัวแปรไว้ 2 ตัว CameraPosition? _initialPosition; // ตัวแปรสำหรับตำแหน่งเริ่มต้น ยังไม่กำหนดค่าใดๆ // ตัวอย่งการกำหนดตำแหน่งแบบตายตัว สมมติเป็นห้าง มาบุญครอง static const CameraPosition _place_mbk = CameraPosition( bearing:0.0, // องศาทิศทางของกล้องตามเข็มนาฬิกาจากทิศเหนือ (หมุนแกนนอน) target: LatLng(13.745324385325134, 100.52999353076734), // พิกัด Lat Long tilt: 0.0, // องศามุมที่กล้องหันตรงไปที่พื้นโลก (หมุนแกนตั้ง) zoom: 14.0 // ระดะับการขยาย ); // สร้าง Set ของ Marker final Set<Marker> _markers = {}; List<LatLng> _markerPoints = []; // สำกำหนดจุดพิกัดตัว marker BitmapDescriptor? _markerIcon; // กำหนดไอคอนจากรูป final MarkerId _markerId = const MarkerId('current_location'); LatLng? _activeMarkerPosition; // ตัวแปรสำหรับเก็บตำแหน่งของ Marker ที่ถูกลาก // กำหนดตัวแปรไว้นับจำนวน marker แลัวไว้สร้าง ค่าไม่ซ้ำไว้ใช้งาน int _markerCount = 0; // กำหนดส่วนของ polyline ที่จะใช้กับแผนที่ Set<Polyline> _polylines = {}; // สำหรับกำหนดโหมดใช้งาน bool _marker_mode = false; // สำหรับวางจุดเท่านั้น // เพิ่มส่วนจัดการเกี่ยวข้อมูลชื่อสถานที่ final TextEditingController _searchController = TextEditingController(); final FocusNode _focusNode = FocusNode(); // สร้าง FocusNode สำหรับ TextField @override void initState() { super.initState(); // ขอสิทธิการเข้าถึงพิกัดตำแหน่ง requestPermissions(); _getCurrentLocation(); // เตรียมไอคอน prepareIcons(); } // ขอ permission สำหรับจัดการตำแหน่ง void requestPermissions() async { await Permission.location.request(); } // สร้างไอคอนจากรูปใน assets void prepareIcons() async { _markerIcon = await BitmapDescriptor.asset( const ImageConfiguration(size: Size(41.5, 48)), // ขนาดของไอคอนที่ต้องการแสดง // const ImageConfiguration(), // ขนาดของไอคอนที่ต้องการแสดงใช้ตามขนาดรูป 'assets/icon/marker.png', // พาธของ asset ขนาดรูปตัวอย่างที่ทดสอบคือ 32x37 ); } // ฟังก์ชันเพื่อดึงตำแหน่งปัจจุบันของผู้ใช้ Future<void> _getCurrentLocation() async { bool serviceEnabled; LocationPermission permission; // ตรวจสอบว่าเปิด Location Services ไว้หรือไม่ serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // หาก Location Services ปิด แสดงข้อความหรือจัดการตามต้องการ return; } // ตรวจสอบ permission สำหรับตำแหน่งที่ตั้ง permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { // Permission ถูกปฏิเสธ return; } } if (permission == LocationPermission.deniedForever) { // Permission ถูกปฏิเสธอย่างถาวร return; } // หาก permission ได้รับการอนุญาต ดึงตำแหน่งที่ตั้งปัจจุบัน Position position = await Geolocator.getCurrentPosition(); // กำหนดตำแหน่งเริ่มต้นเป็นตำแหน่งของผู้ใช้ setState(() { // กำหนดค่าให้กับตำแหน่งเริ่มต้น แล้วสร้างรูปแบบพิกัดไว้ใช้งาน // ค่า bearing และ tilt หากไม่กำหนด ก็จะใช้เป็น 0 ตามค่าเริ่มต้น ในที่นี้เราใช้แค่ // ตำแหน่ง กับ การ zoom _initialPosition = CameraPosition( target: LatLng(position.latitude, position.longitude), zoom: 14.0, ); // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = LatLng(position.latitude, position.longitude); // เพิ่ม Marker สำหรับตำแหน่งปัจจุบัน _markers.add( Marker( markerId: _markerId, position: _activeMarkerPosition!, icon: _markerIcon!, // กำหนดไอคอนจาก asset draggable: true, // เปิดใช้งานการลาก marker infoWindow: InfoWindow( title: 'ตำแหน่งของคุณ', // ข้อความเพิ่มเติมที่จะแสดงใน InfoWindow snippet: '${_activeMarkerPosition!.latitude}, ${_activeMarkerPosition!.longitude}', onTap: () async { print("hide infowindow"); final GoogleMapController controller = await _controller.future; controller.hideMarkerInfoWindow(_markerId); } ), onTap: () async { }, onDragEnd: (LatLng newPosition) { print("Marker dropped at: ${newPosition.latitude}, ${newPosition.longitude}"); // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = LatLng(newPosition.latitude, newPosition.longitude); // สามารถจัดการตำแหน่งใหม่ได้ที่นี่ เช่น อัปเดตตำแหน่งในฐานข้อมูลหรือแสดงข้อมูลใหม่ CameraPosition _newPosition = CameraPosition( target: LatLng(newPosition.latitude, newPosition.longitude), zoom: 14.0, ); // เรียกใช้ ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป _goToPosition(_newPosition); // เรียกใช้งานฟังก์ชัน อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarkerSnippet(_markerId,_activeMarkerPosition!); }, ), ); }); // เรียกใช้ GoogleMapController เพื่อแสดง InfoWindow อัตโนมัติ final GoogleMapController controller = await _controller.future; controller.showMarkerInfoWindow(_markerId); } // ฟังก์ชั่น อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง void _updateMarkerSnippet(MarkerId markerId, LatLng newPosition) { // ตรวจสอบหาตัว markder เดิม ที่ไอดีตรงกับที่ส่งมา Marker? oldMarker = _markers.firstWhere( (marker) => marker.markerId == markerId, orElse: () => null as Marker, // ใช้ cast เป็น markder เพื่อเลี่ยง null safely ); if (oldMarker != null) { // ถ้าไม่ใช่ null // ลบ markder ตัวเดิม _markers.remove(oldMarker); // กำหนดข้อมูลที่แสดงใน infowindow ใหม่ เป็นค่าพิกัดใหม่ String newSnippet = '${newPosition.latitude}, ${newPosition.longitude}'; // เพิ่มตัว markder ตัวใหม่เข้าไป _markers.add( Marker( markerId: markerId, // ใช้ไอดีเดิม position: newPosition, // ตำแหน่งใหม่ icon: _markerIcon!, // กำหนดไอคอนจาก asset draggable: true, // เปิดใช้งานการลาก marker infoWindow: InfoWindow( title: oldMarker.infoWindow.title, // ใช้จากค่าเดิม snippet: newSnippet, // อัปเดทค่าใหม่ onTap: oldMarker.infoWindow.onTap, // ใช้จากค่าเดิม ), onTap: oldMarker.onTap, // ใช้จากค่าเดิม onDragEnd: oldMarker.onDragEnd, // ใช้จากค่าเดิม ), ); // rebuild เพื่ออัปเดทแผนที่ setState(() {}); } } // ฟังก์ชั่นเฉพาะ กรณีเราต้องการให้ไปยังตำแหน่ง ที่เจาะจง ในที่นี้คือไปตำแหน่ง ห้าง มาบุญครอง Future<void> _goToMBK() async { final GoogleMapController controller = await _controller.future; await controller.animateCamera(CameraUpdate.newCameraPosition(_place_mbk)); } // ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป Future<void> _goToPosition(CameraPosition postion) async { final GoogleMapController controller = await _controller.future; await controller.animateCamera(CameraUpdate.newCameraPosition(postion)); // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = postion.target!; // อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarkerSnippet(_markerId,postion.target!); // เรียกใช้ GoogleMapController เพื่อแสดง InfoWindow อัตโนมัติ controller.showMarkerInfoWindow(_markerId); } // ฟังก์ชั่นการกดที่แผนที่ แล้วไปยังตำแหน่งที่ต้องการ Future<void> _onMapTapped(LatLng position) async { // อัปเดตตำแหน่งของ active marker _activeMarkerPosition = LatLng(position.latitude, position.longitude); // สามารถจัดการตำแหน่งใหม่ได้ที่นี่ เช่น อัปเดตตำแหน่งในฐานข้อมูลหรือแสดงข้อมูลใหม่ CameraPosition _newPosition = CameraPosition( target: LatLng(position.latitude, position.longitude), zoom: 14.0, ); // เรียกใช้ ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป _goToPosition(_newPosition); // เพิ่มการเรียกใช้ฟังก์ชั่น แสดงชื่อสถานที่จากพิกัด getPlaceFromCoordinates(position.latitude,position.longitude); } // ฟังก์ชั่น อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง Future<void> _updateMarker(MarkerId markerId, LatLng newPosition) async { // ตรวจสอบหาตัว markder เดิม ที่ไอดีตรงกับที่ส่งมา Marker? oldMarker = _markers.firstWhere( (marker) => marker.markerId == markerId, orElse: () => null as Marker, // ใช้ cast เป็น markder เพื่อเลี่ยง null safely ); if (oldMarker != null) { // ถ้าไม่ใช่ null // ลบ markder ตัวเดิม _markers.remove(oldMarker); // กำหนดข้อมูลที่แสดงใน infowindow ใหม่ เป็นค่าพิกัดใหม่ String newSnippet = '${newPosition.latitude}, ${newPosition.longitude}'; markerId = MarkerId(newPosition.toString()); // เพิ่มตัว markder ตัวใหม่เข้าไป _markers.add( Marker( markerId: markerId, // ใช้ไอดีเดิม position: newPosition, // ตำแหน่งใหม่ icon: oldMarker.icon, // กำหนดไอคอนจาก asset draggable: true, // เปิดใช้งานการลาก marker infoWindow: InfoWindow( title: oldMarker.infoWindow.title, // ใช้จากค่าเดิม snippet: newSnippet, // อัปเดทค่าใหม่ onTap: oldMarker.infoWindow.onTap, // ใช้จากค่าเดิม ), onTap: oldMarker.onTap, // ใช้จากค่าเดิม onDragEnd: (LatLng newPosition) { print("drop marker ${newPosition.toString()}"); // เรียกใช้งานฟังก์ชัน อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarker(markerId,newPosition); // เรียกใช้ฟังก์ชั่นการสร้างเส้นทางด้วย polyline ใช้ค่าจาก route _createRouteLine(); } ), ); // ค่าใหม่ที่จะแทนที่ LatLng newLocation = newPosition; // ค่าใหม่ที่จะแทนที่ // หาค่าเดิมใน List int index = _markerPoints.indexWhere((latLng) => latLng.latitude == oldMarker.position.latitude && latLng.longitude == oldMarker.position.longitude); if (index != -1) { // แทนที่ค่าเดิมด้วยค่าใหม่ ถ้ามี _markerPoints[index] = newLocation; } // rebuild เพื่ออัปเดทแผนที่ setState(() {}); } } // ฟังก์ชั่นสำหรับเพิ่มตัว marker ไปยังจุดที่กดแตะในแผนที่ กรณีใช้งานโหมด ปักหมุด polyline และ polygon Future<void> _addMarkerOnMap(LatLng tappedPosition) async { if(_markerPoints.length>0){ return; } _markerCount++; // เพิ่มค่าสำหรับใช้กำหนด ข้อความ ที่เป็นตัวเลข ใน marker ไม่ให้ซ้ำกัน // นำค่าตัวเลขที่ไม่ซ้ำ ส่งเข้าไปในฟังก์ชั่นสร้างรูป ไอคอน สำหรับ marker final customMarker = await _createCustomMarker('$_markerCount'); setState(() { // เก็บค่าตำแหน่ง marker ต้วก่อนหน้า เพื่อใช้สำหรับการ เชื่อมเป็น polyline หรือ polygon LatLng lastMarkerPosition = _markers.last.position; MarkerId markerId = MarkerId(tappedPosition.toString()); // ถ้าใช้งานโหมด marker if(_marker_mode){ // เพิ่มตำแหน่งใน List สำหรับ marker _markerPoints.add(tappedPosition); // เพิ่มจุดที่ผู้ใช้แตะเพื่อสร้าง marker // ทำการเพิ่มตัว marker เมื่อกดแตะที่แผนที่ _markers.add( Marker( // ใช้ค่าพิกัดเป็น id ของ marker จะได้ไม่ซ้ำกัน ค่านี้จะขึ้นต้นด้วย LatLng markerId: markerId, position: tappedPosition, // เพิ่มตรงตำแหน่งที่แตะ icon: customMarker, // ใช้ไอคอนไม่ซ้ำรูปแบบกัน infoWindow: InfoWindow( // ส่วนแสดง info title: 'Marker $_markerCount', // Title for the marker snippet: '${tappedPosition.latitude}, ${tappedPosition.longitude}', ), draggable: true, onDragEnd: (LatLng newPosition) { print("drop marker ${tappedPosition.toString()}"); // เรียกใช้งานฟังก์ชัน อัปเดทตำแหน่งตัว markder และ ข้อความภายในที่แสดง _updateMarker(markerId,newPosition); // เรียกใช้ฟังก์ชั่นการสร้างเส้นทางด้วย polyline ใช้ค่าจาก route _createRouteLine(); } ), ); // เรียกใช้ฟังก์ชั่นการสร้างเส้นทางด้วย polyline ใช้ค่าจาก route _createRouteLine(); } }); } // สร้างฟังก์ชั่นสำหรับสร้างตัว marker Future<BitmapDescriptor> _createCustomMarker(String text) async { // ตัวจัดการการ render ภาพ final recorder = ui.PictureRecorder(); // ตัวจัดการพื้นที่วาดภาพ หรือสร้างรูปภาพ final canvas = Canvas(recorder); // กำหนดรูปแบบการวาด และขนาด final paint = Paint() ..color = Colors.green // สีพื้นหลัง ..style = PaintingStyle.fill; // ใช้แบบเติมสี final radius = 24.0; // กำหนดรัศมีของวงลม ที่จะทำ marker final pinHeight = 12.0; // ความสูงของสามเหลี่ยมที่เราจะทำเป็นปลายปักหมุด // วาดรูปวงกลม ส่วนหลัก เข้าไปก่อน canvas.drawCircle(Offset(radius, radius), radius, paint); // วาดจุดสำหรับสร้างรูป สามเหลี่ยม final trianglePath = Path(); // จุดเริ่มต้นของสามเหลี่ยม trianglePath.moveTo(radius - pinHeight, (radius * 2) - (pinHeight / 2)); // จุดที่สอง ลากไปทางขวา จากจัดแรก trianglePath.lineTo(radius + pinHeight, (radius * 2) - (pinHeight / 2)); // จัดที่สาม จุดตรงกลาง ยื่นลงมาเป็นจุด ปักหมด trianglePath.lineTo(radius, radius * 2 + pinHeight); // เปิดจุดการวาด จากจุดที่สาม ไปจุดเริ่มแรก เพื่อปิด trianglePath.close(); // วาดรูปสามเหลี่ยมลงในพื้นที่วาด ตามค่าที่กำหนด canvas.drawPath(trianglePath, paint); // การใส่ตัวเลขเข้าไปในภาพ จัดรูปแบบต่างๆ final textPainter = TextPainter( text: TextSpan( text: text, // ใช้การรับค่ามาแสดง style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr,// การเรียงซ้ายไปขวา ); // กำหนดส่วนจัดวางตัวเลขหรือข้อความ ในที่นี้ใช้ค่าเริ่มต้น textPainter.layout(); // วาดข้อความลงไปในพื้นที่วาด จัดตำแหน่งอ้างอิงจากค่าต่างๆ ที่เกี่ยวข้อง ในที่นี้คือไว้ตรงกลางรูปวงกลม textPainter.paint( canvas, Offset(radius - textPainter.width / 2, radius - textPainter.height / 2)); // ใช้ตัว render แปลงจาก canvas ไปเป็นรูปภาพ final picture = recorder.endRecording(); // สร้างรูปภาพ กำหนดขนาด ตามต้องการ ในที่นี้ใช้การคำนวณ กว้างเท้ากับขนาดของวงกลม // ส่วนความสูงให้เท่ากับขนาดของวงกลม บวกกับความสูงของ สามเหลี่ยม final img = await picture.toImage((radius * 2).toInt(), ((radius * 2) + pinHeight).toInt()); // เปลงเป็น binary data เพื่อนำไปใช้งาน final byteData = await img.toByteData(format: ui.ImageByteFormat.png); final pngBytes = byteData!.buffer.asUint8List(); // สร้าง BitmapDescriptor ที่ใช้สำหรับเป็นไอคอน จากข้อมูล binary ของรูปภาพ return BitmapDescriptor.bytes(pngBytes); } // ฟังก์ชั่นสำหรับเปลี่ยนโหมด Future<void> _changeMode({bool marker = false}) async{ setState(() { _marker_mode = marker; _markerPoints.clear(); // ล้างค่าจุดของ marker _markerCount = 0; // เริ่มนับค่าใหม่ _polylines.clear(); // ล้าง polyline ออกจากแผนที่ // ลบตัว marker ทั้งหมดที่ไอดี ขึ้นต้นด้วยคำว่า 'LatLng' _markers.removeWhere( (marker) => marker.markerId.value.startsWith('LatLng')); }); } // ส่วนของฟังก์ชั่นแสดงการแจ้งเตือน void _showResultSnackbar(String result) { // ปิด SnackBar ที่แสดงอยู่ก่อน ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Container( // height: 100, // ปรับขนาดความสูงของการแสดง ปิดการกำหนดตายตัว ให้ขยายขนาดตามข้อมูล child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( result, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18, ), ), ], ), ), // duration: Duration(seconds: 100), // กำหนดแสดงชั่วคราว duration: const Duration(days: 365), // กำหนดแสดงแบบถาวร behavior: SnackBarBehavior.floating, // floating / fixed backgroundColor: Colors.grey[850], action: SnackBarAction( label: 'Close', textColor: Colors.white, onPressed: () { // เมื่อกดปิด ScaffoldMessenger.of(context).hideCurrentSnackBar(); // Dismisses the SnackBar }, ), ), ); } // ฟังก์ชั่นสำหรับหาพิกัดจากซื้อสถานที่ Future<void> getCoordinatesFromPlace(String placeName) async { try { List<Location> locations = await locationFromAddress(placeName); if (locations.isNotEmpty) { // แสดงพิกัด latitude และ longitude double latitude = locations[0].latitude; double longitude = locations[0].longitude; // สามารถจัดการตำแหน่งใหม่ได้ที่นี่ CameraPosition _newPosition = CameraPosition( target: LatLng(latitude,longitude), zoom: 14.0, ); // เรียกใช้ ฟังก์ชั่นสำหรับ กรณีไปยังตำแหน่งใดๆ ที่ต้องการ ตามค่าที่ส่งเข้าไป _goToPosition(_newPosition); print('พิกัดของ $placeName: $latitude, $longitude'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('พิกัดของ $placeName: $latitude, $longitude'); } } catch (e) { print('ไม่สามารถค้นหาพิกัดจากชื่อสถานที่ได้: $e'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('ไม่สามารถค้นหาพิกัดจากชื่อสถานที่ได้: $e'); } } // ฟังก์ชั่นหาชื่อสถานที่จากค่าพิกัด Future<void> getPlaceFromCoordinates(double latitude, double longitude) async { try { List<Placemark> placemarks = await placemarkFromCoordinates(latitude, longitude); if (placemarks.isNotEmpty) { // แสดงข้อมูลสถานที่ที่ค้นพบ Placemark place = placemarks[0]; print('สถานที่จากพิกัด: ${place.street}, ${place.locality}, ${place.country}'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('สถานที่จากพิกัด: ${place.street}, ${place.locality}, ${place.country}'); } } catch (e) { print('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); // แสดงผลลัพธ์แจ้งเตือน _showResultSnackbar('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); } } // ฟังก์ชั่นหาระยะทางของเส้นทางจาก DistanceMatrix http://niik.in/832 Future<void> getDistanceMatrix(LatLng origin, LatLng destination) async { String _origin = '${origin.latitude},${origin.longitude}'; String _destination = '${destination.latitude},${destination.longitude}'; String _apiKey = 'Google Map API Key'; final url = Uri.parse( 'https://maps.googleapis.com/maps/api/distancematrix/json' '?origins=$_origin' '&destinations=$_destination' '&language=th' '&key=$_apiKey', ); // สามารถกำหนด option อื่นๆ เพิ่มเติมได้ ดูจากลิ้งค์ด้านล่าง // https://developers.google.cn/maps/documentation/distance-matrix/distance-matrix?hl=th final response = await http.get(url); if (response.statusCode == 200) { final data = jsonDecode(response.body); /* ระยะทางระหว่างต้นทางและปลายทาง (ในหน่วยต่างๆ เช่น กิโลเมตร เมตร) เวลาที่ใช้ในการเดินทาง (ในหน่วยต่างๆ เช่น วินาที) */ // ข้อความที่แสดงระยะทางจะมีการปัดเศษโดยประมาณ (เช่น "12.7 เมตร") var distance = data['rows'][0]['elements'][0]['distance']['text']; // ข้อความที่แสดงเวลาเดินทางจะมีการปัดเศษโดยประมาณ (เช่น "13 นาที") var duration = data['rows'][0]['elements'][0]['duration']['text']; var distanceValue = data['rows'][0]['elements'][0]['distance']['value']; // ระยะทางในหน่วยเมตร var durationValue = data['rows'][0]['elements'][0]['duration']['value']; // เวลาในหน่วยวินาที print('Distance: $distance'); print('Duration: $duration'); print('Distance Value: $distanceValue'); print('Duration Value: $durationValue'); _showResultSnackbar( 'Distance: $distance\r\n' 'Duration: $duration\r\n' 'Distance Value: $distanceValue\r\n' 'Duration Value: $durationValue' ); } else { // กรณีไม่มีเส้นทางในฐานข้อมูล Google Map _showResultSnackbar('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); // throw Exception('Failed to load distance matrix'); } } // ฟังก์ชั่นหาระยะทางของเส้นทางจาก DistanceMatrix ต้นทางเดียวกัน หลายปลายทาง http://niik.in/832 Future<void> getDistanceMatrixCompare(LatLng origin, List<LatLng> destinations) async { // จัดรูปแบบปลายทางให้อยู่ในรูปแบบ พิกัด|พิกัด|พิกัด|พิกัด String formattedDestinations = destinations.map( (destination) => '${destination.latitude},${destination.longitude}').join('|'); String _origin = '${origin.latitude},${origin.longitude}'; String _destination = formattedDestinations; // ปลายทางแบบหลายพิกัด String _apiKey = 'Google Map API Key'; final url = Uri.parse( 'https://maps.googleapis.com/maps/api/distancematrix/json' '?origins=$_origin' '&destinations=$_destination' '&language=th' '&key=$_apiKey', ); // สามารถกำหนด option อื่นๆ เพิ่มเติมได้ ดูจากลิ้งค์ด้านล่าง // https://developers.google.cn/maps/documentation/distance-matrix/distance-matrix?hl=th final response = await http.get(url); if (response.statusCode == 200) { final data = jsonDecode(response.body); /* ระยะทางระหว่างต้นทางและปลายทาง (ในหน่วยต่างๆ เช่น กิโลเมตร เมตร) เวลาที่ใช้ในการเดินทาง (ในหน่วยต่างๆ เช่น วินาที) */ List<dynamic> rows = data['rows']; List<Map<dynamic, dynamic>> _distanceResult = []; // กำหนดตัวแปรเก็บค่าไว้เปรียบเทียบ var row = rows[0]; // วนลูปค่าระยะทางของแต่ละพิกัดที่ได้ผลลัพธ์กลับมา for (var elementIndex = 0; elementIndex < row['elements'].length; elementIndex++) { String _result = ""; var element = row['elements'][elementIndex]; var distance = element['distance']['text']; var duration = element['duration']['text']; var distanceValue = element['distance']['value']; // ในหน่วยเมตร var durationValue = element['duration']['value']; // ในหน่วยวินาที // จัดรูปแบบข้อความที่เราต้องการแสดง _result = '${elementIndex+1}: ระยะทาง: $distance, ใช้เวลา: $duration\r\n'; // เพิ่มข้อมูลไว้ในตัวแปรสำหรับเปรียบเทียบและแสดงผล ในที่นี้ให้ value เป็นระยะทาง เราจะใช้เปรียบเทียบใกล้ หรือไกลกว่ากัน _distanceResult.add({'id': elementIndex+1, 'value': distanceValue, 'text': _result}); } _distanceResult.sort((a, b) => a['value'].compareTo(b['value'])); // เรียงน้อยไปมาก // _distanceResult.sort((a, b) => b['value'].compareTo(a['value'])); // เรียงมากไปน้อย // วนลูปเอาเฉพาะส่วนของ text ข้อความผลลัพธ์ หลังจาก เรียงข้อมุลใหม่แล้วมาแสดง String result = _distanceResult.map((item) => item['text']).join('\r\n'); _showResultSnackbar(result); } else { // กรณีไม่มีเส้นทางในฐานข้อมูล Google Map _showResultSnackbar('ไม่สามารถค้นหาสถานที่จากพิกัดได้: $e'); // throw Exception('Failed to load distance matrix'); } } // ฟังก์ชั่นเรียกข้อมูลพิกัด route ของเส้นทาง Future<List<List<double>>> getRouteCoordinates(LatLng origin, LatLng destination) async { String _origin = '${origin.latitude},${origin.longitude}'; String _destination = '${destination.latitude},${destination.longitude}'; String _apiKey = 'Google Map API Key'; final url = Uri.parse( 'https://maps.googleapis.com/maps/api/directions/json' '?origin=$_origin' '&destination=$_destination' '&key=$_apiKey', ); final response = await http.get(url); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['routes'].isNotEmpty) { // มีข้อมูลเส้นทาง final polyline = data['routes'][0]['overview_polyline']['points']; return decodePolyline(polyline); // ส่งค่าพิกัดจากข้อมูล polyline ของเส้นทาง route } } throw Exception('Failed to load directions'); } // ฟังก์ชันถอดรหัส polyline สร้างจุดพิกัดของ polyline จากข้อมูล List<List<double>> decodePolyline(String polyline) { List<List<double>> points = []; int index = 0, len = polyline.length; int lat = 0, lng = 0; while (index < len) { int b, shift = 0, result = 0; do { b = polyline.codeUnitAt(index++) - 63; result |= (b & 0x1F) << shift; shift += 5; } while (b >= 0x20); int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); lat += dlat; shift = 0; result = 0; do { b = polyline.codeUnitAt(index++) - 63; result |= (b & 0x1F) << shift; shift += 5; } while (b >= 0x20); int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); lng += dlng; points.add([(lat / 1E5), (lng / 1E5)]); } return points; } // ฟังก์ชั่นสำหรับสร้างเส้น route ด้วย polyline Future<void> _createRouteLine() async { // ใช้ค่าพิกัด route จากการเรียก api ของ direction service List<List<double>> route = await getRouteCoordinates(_activeMarkerPosition!,_markerPoints.last); print("$route"); setState(() { _polylines.clear(); // แก้ไข เพิ่มการล้างค่านี้ก่อน _polylines.add(Polyline( polylineId: PolylineId("route"), points: route.map((point) => LatLng(point[0], point[1])).toList(), color: Colors.green, width: 5, )); }); } @override void dispose() { // การล้างค่า เมื่อไม่ได้ใช้งานแล้ว _searchController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('แผนที่'), actions: [ IconButton( onPressed: (){ _changeMode(); }, icon: Image.asset('assets/icon/pan${(!_marker_mode) ? '_active' : ''}.png',), ), IconButton( onPressed: (){ _changeMode(marker: true); }, icon: Image.asset('assets/icon/mark${(_marker_mode) ? '_active' : ''}.png',), ), ], ), body: _initialPosition == null // กรณียังไม่ได้ตำแหน่ง แสดงตัว Loading ก่อน ? const Center(child: CircularProgressIndicator()) // Loading หรือจะใช้รูปแบบอื่นๆ แทนได้ถ้าต้องการ : Stack( // ใช้ stack ในการวางซ้อนแผนที่ รองรับการเพิ่มเมนูไอคอนต่างๆ children: [ GoogleMap( mapType: MapType.normal, // normal | terrain | satellite | hybrid initialCameraPosition: _initialPosition!, // ใช้ค่าเริ่มต้นจากตำแหน่งของเรา onMapCreated: (GoogleMapController controller) { // ทำให้ Future สำเร็จเมื่อแผนที่ถูกสร้างเสร็จแล้ว // ตัว controller พร้อมใช้งาน _controller.complete(controller); }, // ในที่นี้เราจtซ่อนตัวปุ่ม zoom และกำหนดเอง จากปุ่มที่เราสร้าง zoomControlsEnabled: false, // ซ่อนค่าเริ่มต้นปุ่ม zoom // ในที่นี้เราจะซ่อนตัวปุม ตำแหน่งปัจจุบัน และกำหนดเองจากปุ่มที่เราสร้าง myLocationButtonEnabled: false, // ซ่อนปุ่มตำแหน่งปัจจุบัน myLocationEnabled: false, // แสดงรูป จุด ของตำแหน่งปัจจุบัน markers: _markers, // เพิ่ม markers ใน GoogleMap polylines: _polylines, // เพิ่มการใช้งาน polylines onTap: (_marker_mode) // ถ้าอยู่ในโหมดใดโหมดหนึ่ง ? _addMarkerOnMap // จัดการเกี่ยวกับ marker polyline polygon : _onMapTapped, // เมื่อกดที่แผนที่ให้ทำคำสั่ง ย้ายตำแหน่งไปยังจุดที่ต้องการ ), // ช่องค้นหาชื่อสถานที่ Positioned( top: 16, left: 16, child: Container( width: 250, // ความกว้างของช่องค้นหา child: TextField( controller: _searchController, // ควบคุมค่าใน TextField focusNode: _focusNode, // เชื่อมต่อ FocusNode กับ TextField minLines: 1, // จำนวนบรรทัดขั้นต่ำ maxLines: null, // ให้ขยายได้ไม่จำกัดตามเนื้อหา decoration: InputDecoration( hintText: 'ค้นหาสถานที่...', hintStyle: TextStyle( color: Colors.grey[500], // สีอ่อนสำหรับ hintText ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), // ปรับให้ขอบมน borderSide: BorderSide( color: Colors.grey[100]!, // สีขอบอ่อน width: 1.0, // ความกว้างของขอบ ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30.0), // ปรับให้ขอบมน borderSide: BorderSide( color: Colors.grey[100]!, // สีขอบอ่อน width: 1.0, // ความกว้างของขอบ ), ), filled: true, fillColor: Colors.white, prefixIcon: IconButton( // ปุ่มค้นหาที่ด้านซ้าย icon: const Icon(Icons.search), onPressed: () { // ทำการค้นหาสถานที่ตามชื่อที่กรอกในช่องค้นหา String searchValue = _searchController.text; if (searchValue.isNotEmpty) { print("Search for $searchValue"); getCoordinatesFromPlace(searchValue); FocusScope.of(context).unfocus(); // ยกเลิก focus } }, ), // แสดงปุ่ม clear เฉพาะเมื่อข้อความไม่ว่าง suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { // ล้างข้อความใน TextField _searchController.clear(); setState(() {}); // อัปเดต UI หลังจากลบข้อความ }, ) : null, contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), // ปรับ padding ของเนื้อหา ), onSubmitted: (value) { // ทำการค้นหาสถานที่ตามชื่อที่กรอก print("Enter ${value}"); if(value.isNotEmpty){ getCoordinatesFromPlace(value); FocusScope.of(context).unfocus(); // ยกเลิก focus } }, ), ), ), // ส่วนควบคุมการ Zoom ที่เราสร้างเอง Positioned( top: 16, right: 16, child: Column( children: [ FloatingActionButton( mini: true, onPressed: () async { // ควบคุมการ Zoom In final controller = await _controller.future; controller.animateCamera(CameraUpdate.zoomIn()); }, heroTag: 'zoomin', // Set a unique tag ต้องกำหนด child: const Icon(Icons.add), ), const SizedBox(height: 8), FloatingActionButton( mini: true, onPressed: () async { // ควบคุมการ Zoom Out final controller = await _controller.future; controller.animateCamera(CameraUpdate.zoomOut()); }, heroTag: 'zoomout', // Set a unique tag ต้องกำหนด child: const Icon(Icons.remove), ), ], ), ), // ส่วนควบคุมตำแหน่งปัจจุบัน ที่เราสร้างเอง Positioned( bottom: 16, right: 16, child: FloatingActionButton( onPressed: () { // ไปยังตำแหน่ง จากพิกัด _goToPosition(_initialPosition!); }, heroTag: 'mylocation', // Set a unique tag ต้องกำหนด child: const Icon(Icons.my_location), ), ), // ส่วนปุ่มคำนวณระยะทาง if (_marker_mode) Positioned( bottom: 16, left: 16, child: ElevatedButton( onPressed: () async { if(_markerPoints.length < 1){ _showResultSnackbar('กำหนดจุดพิกัดอย่างน้อย 1 จุด'); }else{ // คำนวณระยะทางตำแหน่งปัจจุบันกับ ตัว marker หลายๆ จุด getDistanceMatrixCompare(_activeMarkerPosition!,_markerPoints); } }, child: Text('คำนวณระยะทาง', style: TextStyle( fontSize: 18, ),), ), ), ], ), ); } }
ผลลัพธ์ที่ได้
เรากดเลือกโหมด marker แล้วกำหนดจุดพิกัดปลายทาง กำหนดได้แค่จุดเดียว ระบบก็จะทำการสร้าง
เส้นทางถ้ามีด้วย polyline ลงในแผนที่ โดยจะเลือกระยะทางที่ดีที่สุดให้เรา เมื่อเรากดย้ายตำแหน่งของ
ตัว marker เส้นทางก็จะอัปเดทใหม่ทันที และถ้าเรากดที่ปุ่มคำนวณระยะทาง ก็จะแสดงระยะทางของเส้น
ทางจากฟังก์ชั่นก่อนหน้า
เรายังสามารถเลือกให้แสดงเส้นทางทั้งหมดที่เป็นทางเลือกได้ นั่นคือ ต้นทางกับปลายทางจุดหมาย
อาจจะมีได้หลายเส้นทาง เราสามารถดึงค่าหลายเส้นทาง แล้วเขียนลงแผนที่ได้ โดยใช้สีเข้มเป็นเส้นทาง
หลักที่ดีที่สุดและใช้สีเทาเป็นเส้นทางทางเลือก แบบนี้เป็นต้น โดยปรับแก้โค้ดเล็กน้อยดังนี้
เพิ่มค่า alternatives เท่ากับ true และกำหนดการส่งค่ากลับมา เข้าไปในฟังก์ชั่น เรียก api
สมมติเราใช้ชื่อเป็นอีกฟังก์ชั่นว่า getRoutes
// ฟังก์ชั่นเรียกข้อมูลพิกัด route ของเส้นทาง แบบรองรับหลายเส้นทางเลือก Future<List<List<List<double>>>> getRoutes(LatLng origin, LatLng destination) async { String _origin = '${origin.latitude},${origin.longitude}'; String _destination = '${destination.latitude},${destination.longitude}'; String _apiKey = 'Google Map API Key'; final url = Uri.parse( 'https://maps.googleapis.com/maps/api/directions/json' '?origin=$_origin' '&destination=$_destination' '&alternatives=true' // กำหนดให้รองรับเส้นทางเลือกถ้ามี '&key=$_apiKey', ); // สามารถกำหนด option อื่นๆ ได้ตามต้องการ // https://developers.google.com/maps/documentation/directions/get-directions?hl=th final response = await http.get(url); if (response.statusCode == 200) { final data = jsonDecode(response.body); List<List<List<double>>> routes = []; if (data['routes'].isNotEmpty) { // วนลูปรวบรวมพิกัดทั้งหมด แล้วคืนค่าเป็น List ของ route for (var route in data['routes']) { final polyline = route['overview_polyline']['points']; routes.add(decodePolyline(polyline)); } } return routes; } throw Exception('Failed to load directions'); }
จากนั้นในส่วนของการสร้างเส้น polyline ก็ปรับใหม่เป็นดังนี้
// ฟังก์ชั่นสำหรับสร้างเส้น route ด้วย polyline Future<void> _createRouteLine() async { // แบบรองรับหลายเส้นทาง List<List<List<double>>> routes = await getRoutes(_activeMarkerPosition!,_markerPoints.last); setState(() { _polylines.clear(); // แก้ไข เพิ่มการล้างค่านี้ก่อน for (int i = 0; i < routes.length; i++) { _polylines.add(Polyline( polylineId: PolylineId("route_$i"), points: routes[i].map((point) => LatLng(point[0], point[1])).toList(), color: (i == 0) ? Colors.green : Colors.grey, // สีที่ต่างกันสำหรับแต่ละเส้นทาง width: 5, )); } }); }
ผลัพธ์ที่ได้
จะเห็นว่าจะมีเส้นทางที่ดีที่สุดเป็นสีเขียว และมีเส้นทางทางเลือกเป็นสีเทา กรณีมีเส้นทางระหว่างสองจุด
พิกัดมากกว่า 1 เส้นทาง เราสามารถนำไปปรับประยุกต์เพิ่มเติมได้
เนื้อหาโดยภาพรวมเกี่ยวกับการใช้งานแผนที่ใน Flutter ด้วย google_map_flutter ก็จะขอจบ
ไว้เพียงเท่านี้ หวังว่าจะเป็นประโยชน์ในการไปปรับประยุกต์ใช้งานต่อไปไม่มากก็น้อย สำหรับเนื้อหาใน
ตอนหน้าจะเกี่ยวกับอะไร รอติดตาม