TÍCH HỢP MAPBOX TRÊN NỀN BẢN ĐỒ GOONG TRONG FLUTTER

TỔNG QUAN

Map SDK trong Flutter cho phép bạn tích hợp bản đồ vào ứng dụng di động của mình. Có nhiều plugin khác nhau và những plugin này hỗ trợ tùy chỉnh bản đồ, phong cách và xử lý sự kiện liên quan.

Goong Map Flutter cung cấp Map SDK cho cả thiết bị Android và IOS, cho phép tùy chỉnh bản đồ với nội dung để hiển thị trên các thiết bị di động.
Goong Map plugin dựa trên cơ chế của Flutter để bổ sung các màn hình hiển thị cho Android và IOS.

Tài liệu dưới đây trình bày cách tính hợp Mapbox trên nền bản đồ của Goong, và sử dụng các dịch vụ cơ bản, bao gồm:

  • Hiện các kiểu bản đồ: cơ bản, vệ tinh, tối, sáng,… gắn marker, vẽ vòng tròn bao quanh marker.
  • Tìm kiếm: nhập tên địa chỉ, hiển thị các gợi ý liên quan tới tên địa chỉ nhập, sau khi chọn thì nhảy ra điểm đó trên bản đồ (sử dụng autocomplete tìm kiếm gợi ý, rồi dùng place-detail để lấy thông tin tọa độ về địa chỉ đó).
  • Dẫn đường: nhập tọa độ điểm đầu và cuối, hiển thị đường dẫn trên bản đó, có thông tin về khoảng cách và thời gian di chuyển (direction với phương tiên di chuyển: car, taxi,..).

CÁCH TÍCH HỢP MAPBOX TRÊN NỀN BẢN ĐỒ GOONG TRONG FLUTTER

Các tham số cần thiết:

Cần có:

  • map key, api key: vào trang đang ký tài khoản và tạo key, xem hướng dẫn tạo key tại đây.

Gán Mapbox 

Vào https://account.mapbox.com/

để đăng ký tài khoản mapbox để cấp MAPBOX_ACCESS_TOKEN

  • sau khi đăng ký tài khoản bạn vào mục Token 

  • tiếp sau đó chọn Create token

  • bạn chọn theo mẫu sau

lưu ý : bắt buộc phải chọn DOWNLOADS:READ

  • ấn Create Token để tạo token

lưu ý: token sẽ có dạng sk. …

Cấu hình Android

Tìm hoặc tạo tệp properties trong thư mục người dùng Gradle của bạn. Thư mục này nằm tại «USER_HOME»/.gradle. Sau khi bạn tìm thấy hoặc tạo tệp, đường dẫn của nó sẽ là «USER_HOME»/.gradle/gradle.properties. Thông tin chi tiết về các thuộc tính của Gradle có thể được tìm thấy trong tài liệu chính thức của Gradle.

Thêm mã token của bạn vào tệp gradle.properties:
SDK_REGISTRY_TOKEN=YOUR_SECRET_MAPBOX_ACCESS_TOKEN

Cấu hình IOS

  •  Xác minh .netrc
machine api.mapbox.com

login mapbox

password sk.ey...
  • Cấu hình token

Bạn có thể truyền token của mình cho môi trường khi xây dựng, chạy hoặc khởi động ứng dụng Flutter.

Bạn có thể truyền token truy cập công khai bằng cách sử dụng –dart-define khi chạy lệnh flutter build hoặc flutter run trên dòng lệnh:

$ flutter build <platform> --dart-define ACCESS_TOKEN=YOUR_PUBLIC_MAPBOX_ACCESS_TOKEN
$ flutter run --dart-define ACCESS_TOKEN=pk.eyJ1I...

Nếu bạn đang sử dụng Visual Studio Code, bạn có thể cấu hình tệp launch.json để thêm tham số –dart-define, và mỗi khi ứng dụng được khởi động, tham số này sẽ được áp dụng.

{   
 "configurations": [    
    {     
       "name": "Flutter",      
      "request": "launch",   
         "type": "dart",  
          "program": "lib/main.dart",   
         "args": [  
            "--dart-define",   
             "ACCESS_TOKEN=YOUR_PUBLIC_MAPBOX_ACCESS_TOKEN"  
          ], 
       }  
  ]
}

Sau đó, lấy token từ môi trường trong ứng dụng và thiết lập nó thông qua MapboxOptions:
ở file main.dart

String ACCESS_TOKEN = String.fromEnvironment("ACCESS_TOKEN");
MapboxOptions.setAccessToken(ACCESS_TOKEN);...
  • Thêm dependency

Để sử dụng Maps SDK cho Flutter, hãy thêm phụ thuộc git vào tệp pubspec.yaml:

…dependencies:

mapbox_maps_flutter: ^2.0.0

  • Trường hợp muốn thêm bản đồ về tinh
https://tiles.goong.io/assets/goong_satellite.json?api_key=<MAP_TILE_KEY>
  • Bỏ logo của Mapbox
attributionButtonPosition: null
  • Chỉnh camera vào vị trí đang đứng
CameraPosition(
  target: LatLng(21.03357551700003, 105.81911236900004),
  zoom: 14.0,
)
  • Thêm marker vào bản đồ
void _addMarkerAtCurrentPosition() async {
  if (mapController == null) {
    print("Map controller is not initialized");
    return;
  }

  const initialLatitude = 21.03357551700003;
  const initialLongitude = 105.81911236900004;

  try {
    // Thêm hình tròn vào vị trí hiện tại
    mapController?.addCircle(
      CircleOptions(
        geometry: LatLng(initialLatitude, initialLongitude),
        circleRadius: 100.0,
        circleColor: "#848484",
        circleOpacity: 0.3,
        circleStrokeWidth: 2,
        circleStrokeColor: "#848484",
      ),
    );

    // Thêm marker lên trên hình tròn
    mapController?.addSymbol(
      SymbolOptions(
        geometry: LatLng(initialLatitude, initialLongitude),
        iconImage: 'location',
        iconSize: 0.3,
        zIndex: 1, // Đảm bảo marker ở trên hình tròn
      ),
    );

    print("Initial marker added at ($initialLatitude, $initialLongitude)");
  } catch (e) {
    print("Error adding initial marker: $e");
  }
}
  • Trong hàm trên thực hiện 2 việc: gắn marker lên bản đồ, vẽ vòng tròn.
  • Khi gắn marker thì chỉ cần truyền 2 tham số latitude, longitude
  • Khi vẽ đường tròn: bản chất là vẽ 1 lớp layer có vòng tròn được tô màu, rồi sử dụng addCircle để hiển thị nó lên bản đồ.

Tìm kiếm địa điểm

Với bất kỳ tên địa chỉ nào người dùng nhập vào, hiển thị các gợi ý cho người dùng chọn.

Future<void> _fetchData(String input) async {
  try {
    final url = Uri.parse(
      'https://rsapi.goong.io/Place/AutoComplete?location=21.013715429594125%2C%20105.79829597455202&input=$input&api_key=<API_KEY>'
    );

    var response = await http.get(url);
    final jsonResponse = jsonDecode(response.body);
    print(jsonResponse);

    setState(() {
      final jsonResponse = jsonDecode(response.body);
      places = jsonResponse['predictions'] as List<dynamic>;
      print('url $url, size: ${places.length}');

      // _circleAnnotationManager?.deleteAll();
      isShow = true;
      isHidden = true;
    });
  } catch (e) {
    // ignore: avoid_print
    print('$e');
  }
}

Khi người dùng nhập tên địa chỉ, thì hàm trên sẽ gọi dịch vụ Autocomplete của Goong để trả về các gợi ý về tên địa chỉ ứng với từ mà người dùng nhập, kèm theo đó là place_id. Sau đó hiển thị những gợi ý lên cho người dùng chọn, khi chọn thì gọi tiếp dịch vụ place detail bằng tham số place_id, thì sẽ lấy được tọa độ của điểm này:

Future<void> _fetchDataPlaceDetail() async {
  final url = Uri.parse(
    'https://rsapi.goong.io/place/detail?place_id=${coordinate['place_id']}&api_key=<API_KEY>'
  );

  var response = await http.get(url);
  final jsonResponse = jsonDecode(response.body);
  details = jsonResponse['result'];

  setState(() {
    _destinationPoint = LatLng(
      details['geometry']['location']['lat'],
      details['geometry']['location']['lng'],
    );
    _addMarkerAtDestinationPoint(); // Thêm marker sau khi cập nhật _destinationPoint
  });

  _searchController.text = coordinate['description'];
  mainText = coordinate['structured_formatting']['main_text'];
  secondText = coordinate['structured_formatting']['secondary_text'];
}

Với tọa đồ của điểm mà người dùng đã chọn, ta sẽ gán marker và view camera sẽ dùng _addMarkerAtDestinationPoint:

void _addMarkerAtDestinationPoint() async {
  if (mapController == null) {
    print("Map controller is not initialized");
    return;
  }

  if (_destinationPoint == null) {
    print("Destination point is not set");
    return;
  }

  try {
    // Xóa marker hiện tại nếu có
    if (_currentMarker != null) {
      await mapController!.removeSymbol(_currentMarker!);
    }

    // Thêm marker mới
    _currentMarker = await mapController!.addSymbol(
      SymbolOptions(
        geometry: _destinationPoint!,
        iconImage: 'locationEnd',
        iconSize: 0.3,
      ),
    );

    // Di chuyển camera đến vị trí mới
    mapController!.animateCamera(CameraUpdate.newLatLng(_destinationPoint!));
  } catch (e) {
    print("Error adding marker: $e");
  }
}

Lưu ý: Số lần gọi autocomplete thì cần phải tối ưu, tùy theo nhu cầu của ứng dụng, ví dụ theo kiểu sau 2,3 ký tự mới đc gọi hoặc khách nhập nhưng chỉ gọi sau 3s không nhập gì. Vì Goong sẽ tính phí mỗi lần gọi autocomplete này

  • Xoá marker
await mapController!.removeSymbol(_currentMarker!);

Dẫn đường

Dẫn đường sử dụng dịch vụ direction, gửi kèm tọa độ điểm đầu, điểm cuối, phương tiện di chuyển, nhận về mã đường đi, khoảng cách 2 điểm, thời gian đi dự kiến,… Sau khi khách hàng nhập tọa độ điểm đầu và cuối, gọi dịch vụ direction của bên Goong sau đó sẽ giải mã đường đi và hiển thị đường đi đó lên bản đồ.

  • Hàm _fetchDataDirection gọi dịch vụ của Goong
Future<void> _fetchDataDirection() async {
  if (_currentPosition != null && _destinationPoint != null) {
    final url = Uri.parse(
      'https://rsapi.goong.io/Direction?origin=${_currentPosition!.latitude},${_currentPosition!.longitude}&destination=${_destinationPoint!.latitude},${_destinationPoint!.longitude}&vehicle=bike&api_key=<API_KEY>'
    );

    var response = await http.get(url);
    final jsonResponse = jsonDecode(response.body);
    var route = jsonResponse['routes'][0]['overview_polyline']['points'];

    List<PointLatLng> result = polylinePoints.decodePolyline(route);
    List<List<double>> coordinates = result.map((point) => [point.longitude, point.latitude]).toList();

    _drawLine(coordinates);
  }
}

Hàm này sẽ gọi dịch vụ direction của Goong, bằng cách truyền tham số tọa độ điểm đầu và cuối, phương tiên di chuyển (ở đây lấy car), và sẽ lấy ra được đường (route), khoảng cách 2 điểm (distance), thời gian đi dự kiến (time),…

  • Sau đó cần giải mã đường đi (route)
List<PointLatLng> result = polylinePoints.decodePolyline(route);
List<List<double>> coordinates = result.map((point) => [point.longitude, point.latitude]).toList();

Giải mã ra được 1 mảng các tọa độ điểm mà đường sẽ đi qua.

  • Hiển thị đường đó lên bản đồ
void _drawLine(List<List<double>> coordinates) {
  mapController?.removeLayer("line_layer");
  mapController?.removeSource("line_source");

  final geoJsonData = {
    "type": "FeatureCollection",
    "features": [
      {
        "type": "Feature",
        "geometry": {
          "type": "LineString",
          "coordinates": coordinates,
        },
      },
    ],
  };

  mapController?.addSource(
    "line_source",
    GeojsonSourceProperties(
      data: geoJsonData,
    ),
  );

  mapController?.addLineLayer(
    "line_source",
    "line_layer",
    LineLayerProperties(
      lineColor: "#0000FF",
      lineWidth: 10,
      lineCap: "round",
      lineJoin: "round",
    ),
  );
}

Hàm trên thực hiện vẽ đường từ điểm hiện tại đến điểm đến

  • Để xoá dẫn đường dùng
mapController?.removeLayer("line_layer");
mapController?.removeSource("line_source");