เนื้อหาตอนต่อไปนี้ จริงๆ ก็ยังอยู่ในบทความเกี่ยวกับการใช้งาน
แผนที่ แต่เนื่องจากเป็นการประยุกต์แล้ว จึงไม่ต่อเป็นตอนที่ 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 ช่วนให้เราจัดการการทำงานของสอง
ส่วนนี้ได้ง่ายขึ้น สามารถนำไปต่อยอดอีกได้
หวังว่าบทความสั้นๆ นี้จะมีประโยชน์ ไม่มากก็น้อย สำหรับเนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม