การใช้งาน Geocoding หาพิกัดและซื่อสถานที่บนแผนที่ ใน Flutter

บทความใหม่ เดือนที่แล้ว โดย Ninenik Narkdee
flutter google map flutter google map geolocator geocoding

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ flutter google map flutter google map geolocator geocoding

ดูแล้ว 211 ครั้ง


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


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ



ทบทวนบทความที่แล้ว









เนื้อหาที่เกี่ยวข้อง






เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน เพื่ออ่านเนื้อหาบทความ

ยังไม่เป็นสมาชิก

สมาชิกล็อกอิน



( หรือ เข้าใช้งานผ่าน Social Login )




URL สำหรับอ้างอิง





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


    ( หรือ เข้าใช้งานผ่าน Social Login )







เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ