TỔNG QUAN
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 của Goong, 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ÁC BƯỚC TÍCH HỢP
-
Các tham số cần thiết
Cần có: map key, api key và mapbox access token
(https://account.goong.io/keys)
-
Gán Mapbox vào ios
- Đầu tiên vào .netrc copy nội dung dưới đây vào :
machine api.mapbox.com
login mapbox
password YOUR_SECRET_MAPBOX_ACCESS_TOKEN
Lưu ý : YOUR_SECRET_MAPBOX_ACCESS_TOKEN là key của mapbox bên Goong sẽ cung cấp.
- Tiếp theo File> Add Packages…. copy https://github.com/mapbox/mapbox-maps-ios.git vào search package
Lưu ý : trong Dependency Rule chọn Up to Next Major Version để version 10.15.0 - Vào plist thêm MBXAccessToken với key YOUR_SECRET_MAPBOX_ACCESS_TOKEN
-
Thêm Mapview của goong
– Khởi tạo bản đồ:
guard let styleURL = URL(string: “https://tiles.goong.io/assets/goong_map_web.json?api_key=\(GoongConstants.API_KEY)”) else {
print(“Invalid URL”)
return
}
let options = MapInitOptions(styleURI: StyleURI(url: styleURL))
mapView = MapView(frame: view.bounds, mapInitOptions: options)
// Add the map.
self.view.addSubview(mapView)
– Trường hợp muốn thêm bản đồ về tinh:
guard let styleURLSatellite = URL(string: “https://tiles.goong.io/assets/goong_satellite.json?api_key=\(GoongConstants.API_KEY)”) else {
print(“Invalid URL”)
return
}
– Bỏ logo của mapbox
mapView.ornaments.logoView.isHidden = true
mapView.ornaments.attributionButton.isHidden = true
mapView.ornaments.compassView.isHidden = true
mapView.ornaments.scaleBarView.isHidden = true
– Chỉnh camera vào vị trí đang đứng
func mapViewCameraCurrent() {
self.mapView.mapboxMap.setCamera(
to: CameraOptions(
center: CLLocationCoordinate2D(
latitude: self.locationManager.lastLocation?.coordinate.latitude ?? Constants.latitudeSimulator ,
longitude: self.locationManager.lastLocation?.coordinate.longitude ?? Constants.longitudeSimulator
),
padding: UIEdgeInsets(top: 150, left: 10 , bottom: 0.0, right: 10),
zoom: 15,
pitch: 12
)
)
}
Có thể lấy location hiện tại của mình để gán vào vị trí mình đang đứng bằng cách sử dụng LocationManager
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
@Published var locationStatus: CLAuthorizationStatus?
@Published var lastLocation: CLLocation?
@Published var speedLocation:CLLocationSpeedAccuracy?
@Published var heading: Double
{
willSet {
objectWillChange.send()
}
}
var speed: CLLocationSpeed = CLLocationSpeed()
var bearing: CLLocationDirection = CLLocationDirection()
var didUpdateLocations:(()->Void)?
override init() {
heading = 0
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
locationManager.startUpdatingHeading()
}
func requestLocation(){
locationManager.requestWhenInUseAuthorization()
}
var statusString: String {
guard let status = locationStatus else {
return “unknown”
}
switch status {
case .notDetermined: return “notDetermined”
case .authorizedWhenInUse: return “authorizedWhenInUse”
case .authorizedAlways: return “authorizedAlways”
case .restricted: return “restricted”
case .denied: return “denied”
default: return “unknown”
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
locationStatus = status
// print(#function, statusString)
}
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
self.heading = Double(round(1 * newHeading.trueHeading) / 1)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
lastLocation = location
didUpdateLocations?()
}
}
– Thêm marker vào bản đồ
func addCurrentMarker(latitude:Double,longitude:Double) {
let style = mapView.mapboxMap.style
try? style.addImage(UIImage(named: Constants.ic_current_point)!, id: Constants.blue_icon_id)
var features = [Feature]()
var feature = Feature(geometry: Point(LocationCoordinate2D(latitude: latitude, longitude: longitude)))
feature.properties = [Constants.icon_key:.string(Constants.blue_marker_property)]
features.append(feature)
var source = GeoJSONSource()
source.data = .featureCollection(FeatureCollection(features: features))
try? style.addSource(source, id: Constants.source_id)
var layerCurrent = SymbolLayer(id: Constants.layer_id)
layerCurrent.source = Constants.source_id
layerCurrent.iconImage = .constant(.name(“blue“))
layerCurrent.iconRotate = .expression(Exp(.get) {
“bearing”
})
layerCurrent.iconAnchor = .constant(.bottom)
layerCurrent.iconAllowOverlap = .constant(false)
layerCurrent.iconSize = .constant(0.5)//.constant(0.1)
var circleAnnotation = CircleAnnotation(centerCoordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude))
circleAnnotation.circleColor = StyleColor(.gray)
circleAnnotation.circleRadius = 64
circleAnnotation.circleOpacity = 0.5
let circleAnnotationManager = mapView.annotations.makeCircleAnnotationManager()
// Add the annotation to the manager.
circleAnnotationManager.annotations = [circleAnnotation]
try? style.addLayer(layerCurrent)
}
- 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 CircleAnnotation để hiển thị nó lên bản đồ. Hàm vẽ vòng tròn circleAnnotation
-
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. SearchViewModel sẽ gọi thực hiện điều đó
final class SearchViewModel: ObservableObject {
@Published var searchText: String = “”
@Published var searchFavourite: String = “”
@Published var predictions: AutoComplete?
@Published var statusSearch = false
private var inputText: String = “”
private var url: String = “”
func ConvertURL(currentLocation: CLLocationCoordinate2D, searchText:String){ inputText = searchText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
url = “\(GoongConstants.GOONG_API_URL)/Place/AutoComplete?api_key=\(GoongConstants.GOONG_API_KEY)&location=\(currentLocation.latitude),\(currentLocation.longitude)&input=\(inputText)&origin=\(currentLocation.latitude),\(currentLocation.longitude)”
if (searchText.count > 3) {
fetchAPISearch()
}
}
func fetchAPISearch(){
weak var task: DispatchWorkItem?
DispatchQueue.global().async {
var request = URLRequest(url: URL(string: self.url)!)
request.httpMethod = “GET”
request.addValue(“application/json“, forHTTPHeaderField: “Content-Type“)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {[weak self] data, response, error -> Void in
if let response = response as? HTTPURLResponse {
do {
let json = try JSONDecoder().decode(AutoComplete.self, from: data!)
DispatchQueue.main.async {
self?.predictions = json
task?.cancel()
}
} catch {
print(“error“,error)
}
}
})
task.resume()
}
}
}
– 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. PlaceDetailViewModel:
final class PlaceDetailViewModel: ObservableObject {
var onLocationUpdate: (()->Void)?
@Published var locationDetail: LocationReponseDto?{
didSet {
self.onLocationUpdate?()
}
}
private var url: String = “”
func ConvertURL(placeID:String){
url = “\(GoongConstants.GOONG_API_URL)/Place/Detail?place_id=\(placeID)&api_key=\(GoongConstants.GOONG_API_KEY)”
fetchPlaceDetail()
}
func fetchPlaceDetail(){
weak var task: DispatchWorkItem?
DispatchQueue.global().async {
var request = URLRequest(url: URL(string: self.url)!)
request.httpMethod = “GET”
request.addValue(“application/json“, forHTTPHeaderField: “Content-Type“)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {[weak self] data, response, error -> Void in
do {
let json = try JSONDecoder().decode(LocationReponseDto.self, from: data!)
DispatchQueue.main.async {
self?.locationDetail = json
task?.cancel()
}
} catch {
print(“Error: \(error)”)
}
})
task.resume()
}
}
}
– 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 2 hàm mapViewCameraDestination và addDestination
func mapViewCameraDestination () {
self.mapView.mapboxMap.setCamera(
to: CameraOptions(
center: CLLocationCoordinate2D(
latitude: (self.placeDetailViewModel.locationDetail?.result.geometry.location.lat)! ,
longitude: (self.placeDetailViewModel.locationDetail?.result.geometry.location.lng)!
),
zoom: 15.0,
bearing: locationManager.heading,
pitch: 0
)
)
}
– Truyền latitude, longitude để lấy điểm giữa của camera
func addDestinationMarker(latitude:Double,longitude:Double) {
let style = mapView.mapboxMap.style
try? style.addImage(UIImage(named: Constants.destination_marker)!, id: Constants.red_icon_id)
var features = [Feature]()
var feature = Feature(geometry: Point(LocationCoordinate2D(latitude: latitude, longitude: longitude)))
feature.properties = [Constants.icon_key_des: .string(Constants.red_marker_property)]
features.append(feature)
var source = GeoJSONSource()
source.data = .featureCollection(FeatureCollection(features: features))
try? style.addSource(source, id: Constants.source_id_destination)
var layer = SymbolLayer(id: Constants.layer_id_des)
layer.source = Constants.source_id_destination
layer.iconImage = .constant(.name(“red“))
layer.iconAnchor = .constant(.bottom)
layer.iconAllowOverlap = .constant(false)
layer.iconSize = .constant(0.1)
try? style.addLayer(layer)
}
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
func removeAnnotationPoint (){
do{
let style = mapView.mapboxMap.style
try style.removeLayer(withId: Constants.layer_id_des)
try style.removeSource(withId: Constants.source_id_destination)
}catch{
print(“xoa marker loi roi \(error)”)
}
}
-
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ụ directiopn của bên Goong sau đó sẽ giải mã đường đi và hiển thị đường đi đó lên bản đồ.
Hàm DirectionViewModel gọi dịch vụ của Goong:
final class DirectionViewModel: ObservableObject {
var onDirectionUpdate: (()->Void)?
@Published var direction: DirectionReponseDto!
{
didSet {
self.onDirectionUpdate?()
}
}
var onStatusLineUpdate: (()->Void)?
@Published var status:Bool!{
didSet{
self.onStatusLineUpdate?()
}
}
var url: String = “”
func ConvertURL(origin:String, destination:String ){
url = “\(GoongConstants.GOONG_API_URL)/Direction?vehicle=car&origin=\(origin)&destination=\(destination)&alternatives=true&api_key=\(GoongConstants.GOONG_API_KEY)”
getDirection()
}
func getDirection(){
weak var task: DispatchWorkItem?
DispatchQueue.global().async {
var request = URLRequest(url: URL(string: self.url)!)
request.httpMethod = “GET”
request.addValue(“application/json“, forHTTPHeaderField: “Content-Type“)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {[weak self] data, response, error -> Void in
do {
let json = try JSONDecoder().decode(DirectionReponseDto.self, from: data!)
DispatchQueue.main.async {
self?.direction = json
task?.cancel()
}
} catch {
print(“Error: \(error)”)
}
})
task.resume()
}
}
}
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), hàm decodePolyline:
func decodeGeoJSON(from geoJsonString: String) throws -> FeatureCollection? {
var featureCollection: FeatureCollection?
do {
guard let data = geoJsonString.data(using: .utf8) else {
return nil
}
featureCollection = try JSONDecoder().decode(FeatureCollection.self, from: data)
} catch {
print(“Error parsing data: \(error)”)
}
return featureCollection
}
=> 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 đồ, hàm addLine
func addLine(coordinates:[Any],decodedCoordinates:[CLLocationCoordinate2D]?) {
if ((try? mapView.mapboxMap.style.source(withId: Constants.GEO_JSON_ID) != nil) != nil) {
let geoJSON = GeoJSONObject.geometry(.lineString(.init(decodedCoordinates!)))
try? mapView.mapboxMap.style.updateGeoJSONSource(withId: Constants.GEO_JSON_ID, geoJSON:geoJSON)
}else{
let geoJSON:String = “””
{
“type”: “FeatureCollection”,
“features”: [{
“type”: “Feature”,
“properties”: {“lineMetrics”: true},
“geometry”: {
“coordinates”: \(coordinates),
“type”: “LineString”
}
}]
}
“””
let featureCollection = try? decodeGeoJSON(from: geoJSON)
// Create a GeoJSON data source.
geoJSONSource.data = .featureCollection(featureCollection!)
// Create a line layer
var lineLayer = LineLayer(id: Constants.LINE_LAYER)
lineLayer.source = Constants.GEO_JSON_ID
lineLayer.lineColor = .constant(StyleColor(.blue))
let lowZoomWidth = Constants.lowZoomWidth
let highZoomWidth = Constants.highZoomWidth
// Use an expression to define the line width at different zoom extents
lineLayer.lineWidth = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
Constants.lowZoom
lowZoomWidth
Constants.highZoom
highZoomWidth
}
)
lineLayer.lineCap = .constant(.round)
lineLayer.lineJoin = .constant(.round)
lineLayer.lineOpacity = .constant(Constants.opacityNumber)
// Create a line border
var lineborder = LineLayer(id:Constants.LINE_BORDER)
lineborder.source = Constants.GEO_JSON_ID
lineborder.lineColor = .constant(StyleColor(.white))
lineborder.lineGapWidth = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
Constants.lowZoom
lowZoomWidth
Constants.highZoom
highZoomWidth
}
)
lineborder.lineWidth = .constant(2)
lineborder.lineCap = .constant(.round)
lineborder.lineJoin = .constant(.round)
// Add the lineLayer to the map.
do{
try mapView.mapboxMap.style.addSource(geoJSONSource, id: Constants.GEO_JSON_ID)
try mapView.mapboxMap.style.addLayer(lineLayer, layerPosition: .below(Constants.belowLayer)) //road-label
try mapView.mapboxMap.style.addLayer(lineborder,layerPosition: .below(Constants.belowLayer)) //road-label
}catch{
print(“lay geojson loi roi \(error)”)
}
}
}
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 hàm removeLineLayer()
func removeLineLayer (){
do{
try mapView.mapboxMap.style.removeLayer(withId: Constants.LINE_LAYER)
try mapView.mapboxMap.style.removeLayer(withId: Constants.LINE_BORDER)
try mapView.mapboxMap.style.removeLayer(withId: Constants.imageID)
// try mapView.mapboxMap.style.removeLayer(withId: “LINE_LAYER_ROUTE_ARROW_\(stepsBannner.indexStep)“)
}catch{
print(“xoa line loi roi \(error)”)
}
}