เนื้อหานี้เป็นตอนที่สามแล้ว เกี่ยวกับการใช้งานแผนที่ใน flutter
เราจะมาดูต่อเกี่ยวกับการจัดการตัว marker เช่น การเพิ่มจุดพิกัดต่างๆ
ลงไปในแผนที่ การวาดเส้นหรือ polyline ระหว่างจุด marker และการ
สร้างกำหนดพื้นที่หลายเหลี่ยมหรือ polygon เหล่านี้ เราจะได้เรียนรู้
ในตอนต่อไปนี้ โดยเนื้อหาจะเป็นการปรับเพิ่มเติมจากตอนที่แล้ว เพื่อให้
เราเข้าใจภาพรวมการทำงานได้ชัดเจนขึ้น และสามารถนำไปปรับประยุกต์
ในรูปแบบที่ต้องการ
ทบทวนเนื้อหาตอนที่แล้วได้ที่บทความ
การแสดงแผนที่ Google Map ใน Flutter ตอนที่ 2 http://niik.in/1128
เตรียมพร้อมสำหรับการแสดงแผนที่ด้วย Google Map Flutter ตอนที่ 1 http://niik.in/1127
การใช้งาน Polyline และ Polygon บนแผน
การใช้งานทั้งสามส่วน ไม่ว่าจะเป็น marker polyline และ polygon ก็จะมีความคล้ายคลึงเกี่ยว
เนื่องกัน ดังนั้น เราก็จะรวบการใช้งาน และวิธีการใช้มาไว้ในโค้ดเดียวกัน เราจะสร้างรูปแบบแผนที่ที่
มีเครื่องมือที่เราสามารถ เลือกได้ว่าต้องการเพิ่มจุด marker ในแผนที่ หรือเพิ่มเส้น polyline หรือ
เพิ่มพื้นที่ในแผนที่ polygon โดยจะแยกเป็นทั้งหมด 3 โหมด ซึ่งโหมดปกติ นอกเหนือจากทั้งสามแบบ
ก็จะเป็นการ ย้ายตำแหน่งพิกัดของตัว marker หลัก
ให้เข้าใจรูปแบบโค้ดเบื้องต้นดังนี้ โค้ดของเรา เดิมคือ เราจะมีตัว marker ตัวหนึ่งเป็นตัวหลักไว้
สำหรับระบุตำแหน่งปัจจุบัน เป็นรูปไอคอนคน สามารถลากย้ายตำแหน่งไปตรงไหนก็ได้ หรือกดที่แผนที่
ก็จะย้ายไปจุดนั้นๆ ได้ ต่อมา เรามีโหมด marker คือ ถ้าเราอยู่ในโหมดนี้ เราจะเปลี่ยนจากการแตะ
ที่แผนที่เพื่อย้ายตำแหน่งตัว marker หลัก ไปเป็นการเพิ่ม ตัว marker ใหม่เข้ามา นั่นคือ แต่ละ
จุดที่เรากดในแผนที่ จะเรียกคำสั่ง เพิ่มตัว marker เข้าไป โดย marker เหล่านี้จะแยกจาก marker
หลักตัวแรก มีไอดีเฉพาะขึ้นต้นด้วย LatLng กดที่ตำแหน่งใด ก็สร้างตัว marker เพิ่มขึ้นมีตัวเลข
กำกับจำนวนการเพิ่ม ต่อไปในโหมดที่สอง โหมดการเพิ่ม polyline เราก็ใช้งานประยุกต์เพิ่มจากการ
เพิ่มตัว marker แต่ละตัว ถ้ามีมากกว่าสอง (ไม่รวม marker หลัก) ก็จะสามารถสร้างเส้นจากสองจุด
ได้ ดังนั้นเมื่ออยู่ในโหมดนี้ ก็คือจะสร้างเส้นระหว่าง marker ตามลำดับไปเรื่อยๆ ต่อไปส่วนสุดท้าย
โหลดที่สาม การทำงานของ polygon เราก็ประยุกต์คล้ายกับ polyline แต่แทนที่จะเป็นการเพิ่มเส้น
ก็เป็นการวาดกรอบพื้นที่หลายเหลี่ยนในแผนที่ โดยการที่จะเริ่มสร้างกรอบพื้นที่ได้ ก็ต้องมีจุด polypoint
อย่างน้อย 3 จุด ก็จะเริ่มสร้างพื้นที่ได้
แนวทางในตัวอย่าง สามารถใช้งานรูปจากด้านล้างนำไปใช้งานได้
ผลลัพธ์หน้าตาการทำงาน
ในส่วนนี้เรายังไม่ลงไปในส่วนของโค้ดการประยุกต์เมื่อสร้างแล้วจะทำอะไรต่อ เพื่อไม่ให้โค้ดยาวเกิน
ไป และเพื่อจะได้ไปประยุกต์ในแบบอื่นๆ ได้ จริงๆ แล้วเราสามารถทำสิ่งเหล่านี้ได้ เช่น ถ้าเป็นการกำหนด
จุด marker เราสามารถประยุกต์ นำค่าแต่ละจุดที่กำหนด ไปบันทืกพิกัดลงฐานข้อมูลไว้ใช้งานได้ ส่วน
polyline เราสามารถ คำนวณหาระยะทางอย่างง่ายจากเส้นทางระหว่างจุดได้ และส่วน polygon เรา
สามารถคำนวณหาขนาดพื้นที่โดยประมาณของพื้นที่ที่กำหนดได้ ซึ่งเนื้อหานี้เรามีตัวอย่างในโค้ดเกี่ยวกับ
แผนที่ที่ใช้บนเว็บไซต์ ตามตัวอย่างบทความด้านล่าง
การใช้งาน polyline และ polygon ใน google map api v3 http://niik.in/380
ไฟล์ map.dart (การใช้งาน polyline และ polygon)
import 'dart:async'; import 'dart:ui' as ui; 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'; 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; // สำหรับวางจุดและ วาดพื้นที่ @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); } // ฟังก์ชั่นสำหรับเพิ่มตัว 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}', ), ), ); } // ถ้าใช้งาน 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')); }); } @override void 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, // เมื่อกดที่แผนที่ให้ทำคำสั่ง ย้ายตำแหน่งไปยังจุดที่ต้องการ ), // ส่วนควบคุมการ 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 จุดต่างๆ ส่วนที่สามเป็นส่วนการกำหนด polyline และส่วนที่สี่เป็นส่วนกำหนด polygon
โค้ดตัวอย่างข้างต้น เรากำหนดให้ไม่สามารถเลื่อนตำแหน่ง marker หรือจุดแต่ละจุดได้ ซึ่งหากต้องการ
กำหนดไหม่ก็แค่ล้างค่า หรือกดไปโหมดปกติ แล้วกำหนดจุดใหม่ตามต้องการ
หากเราต้องการประยุกต์ ให้ทำการลากเปลี่ยนเส้นได้ ก็จะต้องปรับโค้ดตาม ซึ่งจะยุ่งยากขึ้นมาหน่อย
เพราะตัว maker จะเป็นแบบไม่สามารถเปลี่ยนแปลงได้ ซึ่งเมื่อตำแหน่งมีการเปลี่ยน พิกัด และข้อความ
แจ้งก็ต้องเปลี่ยนไปด้วย และด้วยที่ไม่สามารถปรับเปลี่ยนได้ง่ายเหมือนในเว็บไซต์ ทำให้เวลาเปลี่ยน ก็จะ
เหมือนเราต้องทำการลบตัวเก่า และสร้างขึ้นมาใหม่แทน ตามที่เคยอธิบายไปแล้วในตอนที่ผ่านมา เช่น
เดียวกับกรณีของ polyline และ polygon เมื่อประยุกต์ ก็ต้องให้มีการวาดเส้นตามจุดปักหมุดใหม่
การประยุกต์ย้ายตำแหน่งพิกัดของตัว marker
ตามที่ได้อธิบายไปด้านบน ถ้าเราต้องการให้ตัว marker สามารถย้ายพิกัดได้ และแน่นอนจะมีผลกับ
การสร้าง polyline และ polygon ดังนั้น เพื่อให้สามารถนำไปปรับใช้ได้ เราจะประยุกต์เพิ่มเติมอีก
เล็กน้อย เพื่อให้รองรับให้ส่วนนี้ นั่นคือเพื่อการทำงานให้สามารถลากตัว marker ได้ และให้ทำการ
อัปเดทการวาดของ polyline กับ polygon ได้ โดยปรับโค้ดเพียงเล็กน้อยดังนี้
หมายเหตุ: การอัปเดทตำแหน่ง อาจจะไม่สมบูรณ์เสียทีเดียว เพราะมีการจัดการหลายอย่าง นั่นคือ
ตำแหน่งเวลาลาก อาจจะอัปเดทช้า หรือไม่ก็ต้องกดเลื่อนแผนที่เพื่อให้ค่าเปลี่ยนแปลง แต่โดยรวมใช้งาน
ได้ปกติ ไม่มีปัญหา ไม่เลื่อนตำแหน่งแล้ววางเร็วเกินไป หรือมือนิ่งก่อนค่อยวาง เพื่อให้การ render
เป็นไปอย่างถูกต้อง
ไฟล์ map.dart (รองรับอัปเดทเมื่อเลื่อนตำแหน่งพิกัด)
import 'dart:async'; import 'dart:ui' as ui; 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'; 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; // สำหรับวางจุดและ วาดพื้นที่ @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); } // ฟังก์ชั่น อัปเดทตำแหน่งตัว 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')); }); } @override void 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, // เมื่อกดที่แผนที่ให้ทำคำสั่ง ย้ายตำแหน่งไปยังจุดที่ต้องการ ), // ส่วนควบคุมการ 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 ได้และรองรับการสร้าง polyline และ polygon
ใหม่ได้ ทำให้สะดวกเวลาเรานำไปประยุกต์ เช่น ต้องการกำหนดพื้นที่ใหม่ ก็แค่เลื่อนจุดให้ได้ตามต้องการ
การคำนวณหาระยะทางและพื้นที่ของ Polyline และ Polygon
จากที่กล่าวไปแล้วข้างต้น ว่าเราสามารถประยุกต์ต่อเพิ่มเติมได้ ในกรณีการหาระยะทางของ polyline
ซึ่งส่วนนี้จะใช้แพ็กเก็จที่ชื่อ geolocator ที่เราติดตั้งไปแล้วในตอนต้น ส่วนพื้นที่ของ polygon นั้น
เราจะจะมีสูตรการคำนวณสำหรับใช้งาน ดังนั้น โค้ดประยุกต์นี้ก็จะมี 4 ส่วนที่เราแก้ไข นั่นคือ
1. เพิ่มฟังก์ชั่นในการคำนวณหาระยะทาง
2. เพิ่มฟังก์ชั่นในการคำนวณหาพื้นที่
3. เพิ่มฟังก์ชั่นในส่วนการแจ้งเตือนการแสดงข้อมูล
4. เพิ่มส่วนของปุ่มควบคุมการคำนวณ
โดยหลักคือ เราจะเพิ่มปุ่มเข้าไปในแผนที่ไว้ตรงมุมล่างซ้าย ปุ่มนี้จะแสดงก็ต่อเมื่ออยู่ในโหมด polyline
หรือ polygon ข้อความและการทำงานจะขึ้นเงื่อนไขของโหมดนั้นๆ หาก เรายังไม่กำหนดจุดในแผนที่
แต่กดปุ่มก่อน ก็จะมีข้อความแจ้งให้กำหนดพิกัด หลังกำหนดพิกัดเรียบร้อยแล้ว เราก็จะสามารถคำนวณ
ค่าต่างๆ ได้ และแสดงขึ้นตรงหน้าจอ ในที่นี้จะใช้หน่วย เมตรทั้งหมด คือ เมตร และตารางเมตร หากจะไป
ปรับแต่งเพิ่มเติมก็สามารถสร้างฟังก์ชั่นประยุกต์ต่อได้ เช่น แปลงเป็น งาน ตารางวา ไร่ เป็นต้น
ไฟล์ map.dart (เพิ่มการคำนวณระยะทาง และ พื้นที่)
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'; 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; // สำหรับวางจุดและ วาดพื้นที่ @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); } // ฟังก์ชั่น อัปเดทตำแหน่งตัว 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) { 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 }, ), ), ); } @override void 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, // เมื่อกดที่แผนที่ให้ทำคำสั่ง ย้ายตำแหน่งไปยังจุดที่ต้องการ ), // ส่วนควบคุมการ 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 ? 'คำนวณระยะทาง' : 'คำนวณพื้นที่' ), ), ), ], ), ); } }
ผลลัพธ์ที่ได้
ตอนนี้เราสามารถใช้งานและใช้ประโยชน์จากแผนที่ google map ที่แสดงในแอปมากขึ้น
เนื้อหาตอนที่ 3 นี้ก็ขอจบเพียงเท่านี้ เพื่อไม่ให้บทความยาวเกินไป หวังว่าจะเป็นประโยชน์ในการไปปรับ
ประยุกต์ใช้งานต่อไปได้ไม่มากก็น้อย สำหรับบทความหน้าจะเป็นอะไร จะยังเกี่ยวกับแผนที่อยู่หรือไม่
รอติดตามในตอนหน้า