เนื้อหาตอนต่อไปนี้ จริงๆ ก็ยังอยู่ในบทความเกี่ยวกับการใช้งาน
แผนที่ แต่เนื่องจากเป็นการประยุกต์แล้ว จึงไม่ต่อเป็นตอนที่ 4 แต่
แยกเป็บทความหัวข้อใหม่ ในตอนที่แล้ว เราได้ประยุกต์หลายๆ อย่าง
และก็ยังมีส่วนที่ยังสามารถจัดการได้ในแผนที่ ก็คือ การที่เราจะหาชื่อ
จากพิกัด ซึ่งเป็นชื่อที่มีอยู่แล้วในระบบของ google map แต่ไม่สามารถ
ดึงมาใช้งานได้ผ่าน google_map_flutter และ geolocator แพ็กเก็จ
ซึ่งถ้าต้องการใช้งานส่วนนี้ เราต้องใช้แพ็กเก็จอีกตัวเพิ่มเข้ามา ที่ชื่อว่า
geocoding ใช้เพื่อแปลงข้อมูลตำแหน่งที่อยู่เป็นพิกัดทางภูมิศาสตร์ (latitude และ
longitude) หรือแปลงพิกัดทางภูมิศาสตร์กลับมาเป็นข้อมูลตำแหน่งที่อยู่ได้ (reverse
geocoding) ซึ่งมีประโยชน์ในหลาย ๆ แอปพลิเคชัน
เนื้อหานี้ต่อเนื่องจากตอนที่แล้ว ทบทวนได้ที่
การใช้งาน Polyline และ Polygon บนแผนที่ใน Flutter ตอนที่ 3 http://niik.in/1129
ติดตั้ง package ที่จำเป็นเพิ่มเติม ตามรายการด้านล่าง
แพ็กเก็จที่จำเป็นต้องติดตั้งเพิ่มเติม สำหรับการทำงานมีดังนี้
google_maps_flutter: ^2.9.0 geolocator: ^13.0.1 permission_handler: ^11.3.1 geocoding: ^3.0.0
ก่อนใช้งานให้ตรวจสอบข้อกำหนด และเงื่อนไขเพิ่มเติมได้ที่หน้าแพ็กเก็จ โปรเจ็ค flutter ใหม่ๆ
ส่วนใหญ่จะรองรับการใช้งานนี้โดยไม่ต้องตั้งค่าอะไรมาก
การใช้งาน Geocoding จัดการพิกัดและซื่อสถานที่
เราสามารถค้นหาตำแหน่งที่อยู่ โดยเมื่อผู้ใช้ป้อนที่อยู่ลงในแอป ก็สามารถแปลงที่อยู่เป็นพิกัดเพื่อ
นำไปใช้กับแผนที่ได้ และแสดงตำแหน่งที่อยู่จากพิกัด เมื่อได้พิกัด (latitude, longitude) จาก GPS
หรือบริการตำแหน่ง เราก็สามารถใช้ reverse geocoding เพื่อแปลงเป็นที่อยู่ที่ชัดเจน เช่น ชื่อถนน
เมือง หรือประเทศ เพื่อแสดงตำแหน่งพิกัด และระบุในแผนที่ได้
สิ่งที่เราจะทำเพิ่มจากโค้ดในตอนที่แล้วก็คือ เราจะเพิ่มส่วนของการค้นหา เข้าไปในแผนที่ ส่วนนี้เราจะ
ใช้สำหรับ กรอกชื่อสถานที่ แล้วทำการค้นหา โดยจะเรียกใช้งานฟังก์ชั่นการแปลงซื้อสถานที่เป็นพิกัด
กรณีสามารถระบุพิกัดได้ ก็จะแจ้งเตือนโดย snackBar และย้ายตำแหน่งตัว marker แรกไปยังตำแหน่ง
นั้นๆ แต่ถ้าไม่สามารถระบุพิกัดได้ ก็แจ้งเตือนเช่นกัน และส่วนที่สองจากเดิมในโหมดปกติ พอเรากดที่
แผนที่ตำแหน่งใดๆ ก็จะย้ายตัว marker ไปจุดนั้น พร้อมระบุพิกัดตรง infowindow เราก็จะเพิ่มให้
แจ้งชื่อสถานที่ด้วย snackBar จากพิกัดที่ปักหมุดได้ ว่าตำแหน่งนั้นๆ เป็นที่ไหน ซึ่งรองรับการแสดง
ชื่อสถานที่เป็นภาษาไทย คำอธิบายแสดงในโค้ด
ไฟล์ map.dart (ใช้งาน geocoding)
import 'dart:async'; import 'dart:ui' as ui; import 'dart:math'; 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'; 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 = {}; BitmapDescriptor? _markerIcon; // กำหนดไอคอนจากรูป final MarkerId _markerId = const MarkerId('current_location'); LatLng? _activeMarkerPosition; // ตัวแปรสำหรับเก็บตำแหน่งของ Marker ที่ถูกลาก // กำหนดตัวแปรไว้นับจำนวน marker แลัวไว้สร้าง ค่าไม่ซ้ำไว้ใช้งาน int _markerCount = 0; // กำหนดส่วนของ polyline ที่จะใช้กับแผนที่ Set<Polyline> _polylines = {}; List<LatLng> _polylinePoints = []; // กำหนดส่วนของ polyline ที่จะใช้กับแผนที่ รวมทั้งจุดด้วย List<LatLng> _polygonPoints = []; Set<Polygon> _polygons = {}; // สำหรับกำหนดโหมดใช้งาน bool _marker_mode = false; // สำหรับวางจุดเท่านั้น bool _polyline_mode = false; // สำหรับวางจุด และสากเส้น bool _polygon_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)); // อัปเดทตำแหน่งตัว 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 mode กรณีมีการย้ายจุด if(_polyline_mode){ // ค่าใหม่ที่จะแทนที่ LatLng newLocation = newPosition; // ค่าใหม่ที่จะแทนที่ // หาค่าเดิมใน List int index = _polylinePoints.indexWhere((latLng) => latLng.latitude == oldMarker.position.latitude && latLng.longitude == oldMarker.position.longitude); if (index != -1) { // แทนที่ค่าเดิมด้วยค่าใหม่ ถ้ามี _polylinePoints[index] = newLocation; } // ตรวจสอบว่ามีจุดมากกว่า 1 จุดหรือไม่ เพื่อสร้าง polyline if (_polylinePoints.length > 1) { // ทำการเพิ่ม polyline เข้าไปในแผนที่ _polygons.clear(); // ล้าง polyline เก่าออกก่อน (ถ้าต้องการอัปเดต polyline ใหม่) _polylines.add( Polyline( polylineId: PolylineId('line$_markerCount'), // กำหนดไอดีไม่ซ้ำ points: _polylinePoints, // จุดเชื่อมทั้งหมดที่สร้าง polyline color: Colors.blue, // กำหนดสีเส้น width: 5, // กำหนดความกว้างหรือหนาของเส้น ), ); } } // ถ้าใช้งาน polygon mode กรณีมีการย้ายจุด if(_polygon_mode){ // ค่าใหม่ที่จะแทนที่ LatLng newLocation = newPosition; // ค่าใหม่ที่จะแทนที่ // หาค่าเดิมใน List int index = _polygonPoints.indexWhere((latLng) => latLng.latitude == oldMarker.position.latitude && latLng.longitude == oldMarker.position.longitude); if (index != -1) { // แทนที่ค่าเดิมด้วยค่าใหม่ ถ้ามี _polygonPoints[index] = newLocation; } // ตรวจสอบว่ามีจุดมากกว่า 3 จุดเพื่อสร้าง polygon ได้ if (_polygonPoints.length >= 3) { _polygons.clear(); // ล้าง polygon เก่าออกก่อน (ถ้าต้องการอัปเดต polygon ใหม่) _polygons.add( Polygon( polygonId: PolygonId('polygon$_markerCount'), points: _polygonPoints, // จุดทั้งหมดที่สร้าง polygon strokeColor: Colors.red, // สีขอบ strokeWidth: 3, // ความหนาของขอบ fillColor: Colors.red.withOpacity(0.3), // สีพื้นพร้อมความโปร่งใส ), ); } } // 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()); // ถ้าใช้งานโหมดใดโหมดหนึ่งของทั้งสามโหมด if(_marker_mode || _polyline_mode || _polygon_mode){ // ทำการเพิ่มตัว 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 mode if(_polyline_mode){ // เพิ่มตำแหน่งใน List สำหรับ polyline _polylinePoints.add(tappedPosition); // เพิ่มจุดที่ผู้ใช้แตะเพื่อสร้าง polyline // ตรวจสอบว่ามีจุดมากกว่า 1 จุดหรือไม่ เพื่อสร้าง polyline if (_polylinePoints.length > 1) { // ตัวใช้ทดสอบดูค่า ตำแหน่งสองจุด print("add polyline ${lastMarkerPosition.latitude}, ${lastMarkerPosition.longitude}"); print("add polyline ${tappedPosition.latitude}, ${tappedPosition.longitude}"); // ทำการเพิ่ม polyline เข้าไปในแผนที่ _polygons.clear(); // ล้าง polyline เก่าออกก่อน (ถ้าต้องการอัปเดต polyline ใหม่) _polylines.add( Polyline( polylineId: PolylineId('line$_markerCount'), // กำหนดไอดีไม่ซ้ำ points: _polylinePoints, // จุดเชื่อมทั้งหมดที่สร้าง polyline color: Colors.blue, // กำหนดสีเส้น width: 5, // กำหนดความกว้างหรือหนาของเส้น ), ); } } // ถ้าใช้งาน polygon mode if(_polygon_mode){ // เพิ่มตำแหน่งใน List สำหรับ polygon _polygonPoints.add(tappedPosition); // เพิ่มจุดที่ผู้ใช้แตะเพื่อสร้าง polygon // ตรวจสอบว่ามีจุดมากกว่า 3 จุดเพื่อสร้าง polygon ได้ if (_polygonPoints.length >= 3) { _polygons.clear(); // ล้าง polygon เก่าออกก่อน (ถ้าต้องการอัปเดต polygon ใหม่) _polygons.add( Polygon( polygonId: PolygonId('polygon$_markerCount'), points: _polygonPoints, // จุดทั้งหมดที่สร้าง polygon strokeColor: Colors.red, // สีขอบ strokeWidth: 3, // ความหนาของขอบ fillColor: Colors.red.withOpacity(0.3), // สีพื้นพร้อมความโปร่งใส ), ); } } }); } // สร้างฟังก์ชั่นสำหรับสร้างตัว 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, bool polyline = false, bool polygon = false}) async{ setState(() { _marker_mode = marker; _polyline_mode = polyline; _polygon_mode = polygon; _markerCount = 0; // เริ่มนับค่าใหม่ _polylines.clear(); // ล้าง polyline ออกจากแผนที่ _polylinePoints.clear(); // ล้างค่าจุดของ polyline _polygonPoints.clear(); // ล้างค่าจุดของ polygon _polygons.clear(); // ล้าง polygon ออกจากแผนที่ // ลบตัว marker ทั้งหมดที่ไอดี ขึ้นต้นด้วยคำว่า 'LatLng' _markers.removeWhere( (marker) => marker.markerId.value.startsWith('LatLng')); }); } // ฟังก์ชั่นการคำนวณระยะทางของ polyline จาก พิกัด double calculatePolylineDistance(List<LatLng> polylinePoints) { // ประกาศตัวแปรสำหรับเก็บค่าระยะทางรวมทั้งหมด เริ่มต้นที่ 0.0 double totalDistance = 0.0; // วนลูปผ่านแต่ละจุดใน Polyline โดยไม่รวมนับจุดสุดท้าย for (int i = 0; i < polylinePoints.length - 1; i++) { // คำนวณระยะทางระหว่างจุดที่ i และจุดถัดไป (i+1) โดยใช้ฟังก์ชัน Geolocator.distanceBetween // ซึ่งจะคำนวณระยะทางระหว่างจุดทั้งสองโดยพิจารณาค่าพิกัดละติจูดและลองจิจูด totalDistance += Geolocator.distanceBetween( polylinePoints[i].latitude, // พิกัดละติจูดของจุดที่ i polylinePoints[i].longitude, // พิกัดลองจิจูดของจุดที่ i polylinePoints[i + 1].latitude, // พิกัดละติจูดของจุดถัดไป (i+1) polylinePoints[i + 1].longitude, // พิกัดลองจิจูดของจุดถัดไป (i+1) ); } // หลังจากวนลูปครบแล้ว จะส่งคืนค่าระยะทางรวมทั้งหมด return totalDistance; } /// ฟังก์ชันคำนวณพื้นที่ของ Polygon จากพิกัด double geodesicArea(List<LatLng> polygonPoints) { if (polygonPoints.length < 3) return 0.0; /// ค่าคงที่รัศมีของโลก (หน่วยเป็นเมตร) const double earthRadius = 6371000; double totalArea = 0.0; for (int i = 0; i < polygonPoints.length; i++) { LatLng p1 = polygonPoints[i]; LatLng p2 = polygonPoints[(i + 1) % polygonPoints.length]; // Convert latitude and longitude to radians double lat1 = p1.latitude * pi / 180; double lat2 = p2.latitude * pi / 180; double lon1 = p1.longitude * pi / 180; double lon2 = p2.longitude * pi / 180; // ใช้สูตรการคำนวณพื้นที่เชิงทรงกลม totalArea += (lon2 - lon1) * (2 + sin(lat1) + sin(lat2)); } // พื้นที่รวมในหน่วยตารางเมตร totalArea = totalArea * earthRadius * earthRadius / 2.0; // คืนค่าพื้นที่ที่เป็นบวก return totalArea.abs(); } // ส่วนของฟังก์ชั่นแสดงการแจ้งเตือน 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'); } } @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 && !_polyline_mode && !_polygon_mode) ? '_active' : ''}.png',), ), IconButton( onPressed: (){ _changeMode(marker: true); }, icon: Image.asset('assets/icon/mark${(_marker_mode) ? '_active' : ''}.png',), ), IconButton( onPressed: (){ _changeMode(polyline: true); }, icon: Image.asset('assets/icon/polyline${(_polyline_mode) ? '_active' : ''}.png',), ), IconButton( onPressed: (){ _changeMode(polygon: true); }, icon: Image.asset('assets/icon/polygon${(_polygon_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 polygons: _polygons, // เพิ่มการใช้งาน polygon onTap: (_marker_mode || _polyline_mode || _polygon_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), ), ), // ส่วนปุ่มคำนวณระยะทางและพื้นที่ เมื่อยู่ในโหมด polyline และ polygon if (_polyline_mode || _polygon_mode) Positioned( bottom: 16, left: 16, child: ElevatedButton( onPressed: (){ if(_polyline_mode){ if(_polylinePoints.length < 2){ _showResultSnackbar('กำหนดจุดพิกัดอย่างน้อย 2 จุด'); }else{ double distance = calculatePolylineDistance(_polylinePoints); print('ระยะทางทั้งหมด: $distance เมตร'); _showResultSnackbar('ระยะทางทั้งหมด: $distance เมตร'); } } if(_polygon_mode){ if(_polygonPoints.length < 3){ _showResultSnackbar('กำหนดจุดพิกัดอย่างน้อย 3 จุด'); }else{ double area = geodesicArea(_polygonPoints); print('พื้นที่ของ Polygon: $area ตารางเมตร'); _showResultSnackbar('พื้นที่ของ Polygon: $area ตารางเมตร'); } } }, child: Text( _polyline_mode ? 'คำนวณระยะทาง' : 'คำนวณพื้นที่' ), ), ), ], ), ); } }
ผลลัพธ์ที่ได้
จะเห็นว่าได้เพิ่มส่วนของช่องค้นหาเข้าไปตรงมุมซ้ายของแผนที่ รูปที่สองเราใช้งานทำงานเมื่อกดที่ใดๆ
ในแผนที่ ก็จะย้ายตัว marker ไปยังพิกัดน้้น แล้วเราเรียกใช้ฟังก์ชั่นค้นหาสถานที่จากพิกัด ถ้ามีข้อมูล
ก็จะแสดงด้วย snackBar ตามรูป และส่วนที่สาม เราทำการค้นหาคำว่า เชียงใหม่ จังหวัดทางภาคเหนือ
ของไทย เมื่อเรียกใช้ฟังก์ชั่นค้นหาพิกัดจากชื่อสถานที่แล้วพบข้อมูล ก็จะส่งพิกัดมา แล้วเราก็นำไปใช้งาน
โดยย้ายตัว marker ไปยังพิกัดนั้นๆ จะเห็นว่าตัว geocoding ช่วนให้เราจัดการการทำงานของสอง
ส่วนนี้ได้ง่ายขึ้น สามารถนำไปต่อยอดอีกได้
หวังว่าบทความสั้นๆ นี้จะมีประโยชน์ ไม่มากก็น้อย สำหรับเนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม