diff --git a/ios/Classes/SwiftMapLauncherPlugin.swift b/ios/Classes/SwiftMapLauncherPlugin.swift index 91f8e67..22f38f1 100644 --- a/ios/Classes/SwiftMapLauncherPlugin.swift +++ b/ios/Classes/SwiftMapLauncherPlugin.swift @@ -111,9 +111,8 @@ private func getDirectionsMode(directionsMode: String?) -> String { } } -private func showMarker(mapType: MapType, url: String, title: String, latitude: String, longitude: String) { - switch mapType { - case MapType.apple: +private func showMarker(mapType: MapType, url: String, title: String, latitude: String, longitude: String, perfectUseMapKit: Bool) { + if (mapType == MapType.apple && perfectUseMapKit) { let coordinate = CLLocationCoordinate2DMake(Double(latitude)!, Double(longitude)!) let region = MKCoordinateRegion(center: coordinate, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.02)) let placemark = MKPlacemark(coordinate: coordinate, addressDictionary: nil) @@ -123,15 +122,13 @@ private func showMarker(mapType: MapType, url: String, title: String, latitude: MKLaunchOptionsMapSpanKey: NSValue(mkCoordinateSpan: region.span)] mapItem.name = title mapItem.openInMaps(launchOptions: options) - default: + } else { UIApplication.shared.open(URL(string:url)!, options: [:], completionHandler: nil) - } } -private func showDirections(mapType: MapType, url: String, destinationTitle: String?, destinationLatitude: String, destinationLongitude: String, originTitle: String?, originLatitude: String?, originLongitude: String?, directionsMode: String?, waypoints: [[String: String?]]?) { - switch mapType { - case MapType.apple: +private func showDirections(mapType: MapType, url: String, destinationTitle: String?, destinationLatitude: String, destinationLongitude: String, originTitle: String?, originLatitude: String?, originLongitude: String?, directionsMode: String?, waypoints: [[String: String?]]?, perfectUseMapKit: Bool) { + if (mapType == MapType.apple && perfectUseMapKit) { let destinationMapItem = getMapItem(latitude: destinationLatitude, longitude: destinationLongitude, name: destinationTitle ?? "Destination"); @@ -150,9 +147,8 @@ private func showDirections(mapType: MapType, url: String, destinationTitle: Str with: [originMapItem] + waypointMapItems + [destinationMapItem], launchOptions: [MKLaunchOptionsDirectionsModeKey: getDirectionsMode(directionsMode: directionsMode)] ) - default: + } else { UIApplication.shared.open(URL(string:url)!, options: [:], completionHandler: nil) - } } @@ -188,6 +184,7 @@ public class SwiftMapLauncherPlugin: NSObject, FlutterPlugin { let title = args["title"] as! String let latitude = args["latitude"] as! String let longitude = args["longitude"] as! String + let perfectUseMapKit = (args["perfectUseMapKit"] as? NSNumber)?.boolValue ?? true let map = getMapByRawMapType(type: mapType) if (!isMapAvailable(map: map)) { @@ -195,13 +192,21 @@ public class SwiftMapLauncherPlugin: NSObject, FlutterPlugin { return; } - showMarker(mapType: MapType(rawValue: mapType)!, url: url, title: title, latitude: latitude, longitude: longitude) + showMarker( + mapType: MapType(rawValue: mapType)!, + url: url, + title: title, + latitude: latitude, + longitude: longitude, + perfectUseMapKit: perfectUseMapKit + ) result(nil) case "showDirections": let args = call.arguments as! NSDictionary let mapType = args["mapType"] as! String let url = args["url"] as! String + let perfectUseMapKit = (args["perfectUseMapKit"] as? NSNumber)?.boolValue ?? true let destinationTitle = args["destinationTitle"] as? String let destinationLatitude = args["destinationLatitude"] as! String @@ -231,7 +236,8 @@ public class SwiftMapLauncherPlugin: NSObject, FlutterPlugin { originLatitude: originLatitude, originLongitude: originLongitude, directionsMode: directionsMode, - waypoints: waypoints + waypoints: waypoints, + perfectUseMapKit: perfectUseMapKit ) result(nil) diff --git a/lib/src/directions_url.dart b/lib/src/directions_url.dart index d727739..36f4916 100644 --- a/lib/src/directions_url.dart +++ b/lib/src/directions_url.dart @@ -6,7 +6,7 @@ import 'package:map_launcher/src/utils.dart'; /// Returns a url that is used by [showDirections] String getMapDirectionsUrl({ required MapType mapType, - required Coords destination, + required Coords? destination, String? destinationTitle, Coords? origin, String? originTitle, @@ -15,30 +15,16 @@ String getMapDirectionsUrl({ Map? extraParams, }) { switch (mapType) { - case MapType.google: - return Utils.buildUrl( - url: 'https://www.google.com/maps/dir/', - queryParams: { - 'api': '1', - 'destination': '${destination.latitude},${destination.longitude}', - 'origin': Utils.nullOrValue( - origin, - '${origin?.latitude},${origin?.longitude}', - ), - 'waypoints': waypoints - ?.map((waypoint) => '${waypoint.latitude},${waypoint.longitude}') - .join('|'), - 'travelmode': Utils.enumToString(directionsMode), - ...(extraParams ?? {}), - }, - ); - case MapType.googleGo: + case MapType.google: + // https://developers.google.com/maps/documentation/urls/get-started#directions-action return Utils.buildUrl( url: 'https://www.google.com/maps/dir/', queryParams: { 'api': '1', - 'destination': '${destination.latitude},${destination.longitude}', + 'destination': destination != null + ? '${destination.latitude},${destination.longitude}' + : destinationTitle, 'origin': Utils.nullOrValue( origin, '${origin?.latitude},${origin?.longitude}', @@ -50,23 +36,32 @@ String getMapDirectionsUrl({ ...(extraParams ?? {}), }, ); - case MapType.apple: + // https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/MapLinks/MapLinks.html + // https://itnext.io/apple-maps-url-schemes-e1d3ac7340af return Utils.buildUrl( - url: 'http://maps.apple.com/maps', + url: Platform.isIOS ? 'maps://' : 'http://maps.apple.com/maps', queryParams: { - 'daddr': '${destination.latitude},${destination.longitude}', + 'dirflg': Utils.getAppleDirectionsMode(directionsMode), + 'saddr': origin != null + ? '${origin.latitude},${origin.longitude}' + : originTitle, + 'daddr': destination != null + ? '${destination.latitude},${destination.longitude}' + : destinationTitle, ...(extraParams ?? {}), }, ); case MapType.amap: + // Android: https://lbs.amap.com/api/amap-mobile/guide/android/route + // iOS: https://lbs.amap.com/api/amap-mobile/guide/ios/route return Utils.buildUrl( url: Platform.isIOS ? 'iosamap://path' : 'amapuri://route/plan/', queryParams: { 'sourceApplication': 'applicationName', - 'dlat': '${destination.latitude}', - 'dlon': '${destination.longitude}', + 'dlat': destination?.latitude.toString(), + 'dlon': destination?.longitude.toString(), 'dname': destinationTitle, 'slat': Utils.nullOrValue(origin, '${origin?.latitude}'), 'slon': Utils.nullOrValue(origin, '${origin?.longitude}'), @@ -78,15 +73,20 @@ String getMapDirectionsUrl({ ); case MapType.baidu: + // Android: https://lbsyun.baidu.com/index.php?title=uri/api/android#:~:text=URL%E6%8E%A5%E5%8F%A3%EF%BC%9A-,baidumap%3A//map/direction,-%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E + // iOS: https://lbsyun.baidu.com/index.php?title=uri/api/ios#:~:text=%E6%9C%8D%E5%8A%A1%E5%9C%B0%E5%9D%80-,baidumap%3A//map/direction,-//%20iOS%E6%9C%8D%E5%8A%A1%E5%9C%B0%E5%9D%80 + String? loc(Coords? coords, String? title) => + title != null && coords != null + ? 'name:$title|latlng:${coords.latitude},${coords.longitude}' + : coords != null + ? '${coords.latitude},${coords.longitude}' + : title; + return Utils.buildUrl( url: 'baidumap://map/direction', queryParams: { - 'destination': - 'name: ${destinationTitle ?? 'Destination'}|latlng:${destination.latitude},${destination.longitude}', - 'origin': Utils.nullOrValue( - origin, - 'name: ${originTitle ?? 'Origin'}|latlng:${origin?.latitude},${origin?.longitude}', - ), + 'destination': loc(destination, destinationTitle), + 'origin': loc(origin, originTitle), 'coord_type': 'gcj02', 'mode': Utils.getBaiduDirectionsMode(directionsMode), 'src': 'com.map_launcher', @@ -95,6 +95,7 @@ String getMapDirectionsUrl({ ); case MapType.waze: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'waze://', queryParams: { @@ -106,6 +107,7 @@ String getMapDirectionsUrl({ ); case MapType.citymapper: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl(url: 'citymapper://directions', queryParams: { 'endcoord': '${destination.latitude},${destination.longitude}', 'endname': destinationTitle, @@ -119,6 +121,7 @@ String getMapDirectionsUrl({ case MapType.osmand: case MapType.osmandplus: + destination ??= Coords.zero; // Not tested, fallback to zero. if (Platform.isIOS) { return Utils.buildUrl( url: 'osmandmaps://navigate', @@ -148,6 +151,7 @@ String getMapDirectionsUrl({ // 'type': Utils.getMapsMeDirectionsMode(directionsMode), // }, // ); + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'mapsme://map', queryParams: { @@ -159,6 +163,7 @@ String getMapDirectionsUrl({ ); case MapType.yandexMaps: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'yandexmaps://maps.yandex.com/', queryParams: { @@ -170,6 +175,7 @@ String getMapDirectionsUrl({ ); case MapType.yandexNavi: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'yandexnavi://build_route_on_map', queryParams: { @@ -181,6 +187,7 @@ String getMapDirectionsUrl({ ); case MapType.doubleGis: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'dgis://2gis.ru/routeSearch/rsType/${Utils.getDoubleGisDirectionsMode(directionsMode)}/${origin == null ? '' : 'from/${origin.longitude},${origin.latitude}/'}to/${destination.longitude},${destination.latitude}', @@ -190,19 +197,26 @@ String getMapDirectionsUrl({ ); case MapType.tencent: + // When destination is null, you need to manually click the input box to search. + // https://lbs.qq.com/webApi/uriV1/uriGuide/uriMobileRoute return Utils.buildUrl( url: 'qqmap://map/routeplan', queryParams: { 'from': originTitle, - 'fromcoord': '${origin?.latitude},${origin?.longitude}', + 'fromcoord': origin != null // + ? '${origin.latitude},${origin.longitude}' + : null, 'to': destinationTitle, - 'tocoord': '${destination.latitude},${destination.longitude}', + 'tocoord': destination != null + ? '${destination.latitude},${destination.longitude}' + : null, 'type': Utils.getTencentDirectionsMode(directionsMode), ...(extraParams ?? {}), }, ); case MapType.here: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'https://share.here.com/r/${origin?.latitude},${origin?.longitude},$originTitle/${destination.latitude},${destination.longitude}', @@ -213,6 +227,7 @@ String getMapDirectionsUrl({ ); case MapType.petal: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl(url: 'petalmaps://route', queryParams: { 'daddr': '${destination.latitude},${destination.longitude} (${destinationTitle ?? 'Destination'})', @@ -225,6 +240,7 @@ String getMapDirectionsUrl({ }); case MapType.tomtomgo: + destination ??= Coords.zero; // Not tested, fallback to zero. if (Platform.isIOS) { return Utils.buildUrl( url: 'tomtomgo://x-callback-url/navigate', @@ -246,6 +262,7 @@ String getMapDirectionsUrl({ case MapType.copilot: // Documentation: // https://developer.trimblemaps.com/copilot-navigation/v10-19/feature-guide/advanced-features/url-launch/ + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'copilot://mydestination', queryParams: { @@ -259,6 +276,7 @@ String getMapDirectionsUrl({ ); case MapType.tomtomgofleet: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'google.navigation:', queryParams: { @@ -270,6 +288,7 @@ String getMapDirectionsUrl({ case MapType.sygicTruck: // Documentation: // https://www.sygic.com/developers/professional-navigation-sdk/introduction + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'com.sygic.aura://coordinate|${destination.longitude}|${destination.latitude}|drive', @@ -279,6 +298,7 @@ String getMapDirectionsUrl({ ); case MapType.flitsmeister: + destination ??= Coords.zero; // Not tested, fallback to zero. if (Platform.isIOS) { return Utils.buildUrl( url: 'flitsmeister://', @@ -294,6 +314,7 @@ String getMapDirectionsUrl({ ); case MapType.truckmeister: + destination ??= Coords.zero; // Not tested, fallback to zero. if (Platform.isIOS) { return Utils.buildUrl( url: 'truckmeister://', @@ -312,6 +333,7 @@ String getMapDirectionsUrl({ ); case MapType.naver: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'nmap://route/car', queryParams: { @@ -326,6 +348,7 @@ String getMapDirectionsUrl({ ); case MapType.kakao: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'kakaomap://route', queryParams: { @@ -337,6 +360,7 @@ String getMapDirectionsUrl({ ); case MapType.tmap: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'tmap://route', queryParams: { @@ -352,6 +376,7 @@ String getMapDirectionsUrl({ ); case MapType.mapyCz: + destination ??= Coords.zero; // Not tested, fallback to zero. return Utils.buildUrl( url: 'https://mapy.cz/zakladni', queryParams: { @@ -361,6 +386,7 @@ String getMapDirectionsUrl({ ); case MapType.mappls: + destination ??= Coords.zero; // Not tested, fallback to zero. var query = ''; if (origin != null) { query = diff --git a/lib/src/map_launcher.dart b/lib/src/map_launcher.dart index 41deb16..f89f005 100644 --- a/lib/src/map_launcher.dart +++ b/lib/src/map_launcher.dart @@ -49,9 +49,20 @@ class MapLauncher { /// Opens map app specified in [mapType] /// and shows directions to [destination] + /// + /// [destination], the coordinates of the destination. When it is `null`, use + /// [destinationTitle] to search destination, currently only some maps support it. + /// + /// - [MapType.apple], supported + /// - [MapType.google], supported + /// - [MapType.googleGo], supported + /// - [MapType.amap], supported + /// - [MapType.baidu], supported + /// - [MapType.tencent], partially supported, you need to manually click the input box to search. + /// - others map not tested, fallback to [Coords.zero] static Future showDirections({ required MapType mapType, - required Coords destination, + required Coords? destination, String? destinationTitle, Coords? origin, String? originTitle, @@ -73,10 +84,11 @@ class MapLauncher { final Map args = { 'mapType': Utils.enumToString(mapType), 'url': Uri.encodeFull(url), + // On iOS, MapKit does not support destination being null, use `UIApplication.shared.open()` instead. + 'perfectUseMapKit': destination != null, 'destinationTitle': destinationTitle, - 'destinationLatitude': destination.latitude.toString(), - 'destinationLongitude': destination.longitude.toString(), - 'destinationtitle': destinationTitle, + 'destinationLatitude': (destination?.latitude ?? 0.0).toString(), + 'destinationLongitude': (destination?.longitude ?? 0.0).toString(), 'originLatitude': origin?.latitude.toString(), 'originLongitude': origin?.longitude.toString(), 'originTitle': originTitle, diff --git a/lib/src/models.dart b/lib/src/models.dart index a99c1c5..d3cf46a 100644 --- a/lib/src/models.dart +++ b/lib/src/models.dart @@ -101,10 +101,12 @@ enum DirectionsMode { /// Class that holds latitude and longitude coordinates class Coords { + static const zero = Coords(0.0, 0.0); + final double latitude; final double longitude; - Coords(this.latitude, this.longitude); + const Coords(this.latitude, this.longitude); } /// Class that holds lat/lng coordinates and optional title @@ -166,7 +168,7 @@ class AvailableMap { /// Launches current map and shows directions to `destination` Future showDirections({ - required Coords destination, + required Coords? destination, String? destinationTitle, Coords? origin, String? originTitle, diff --git a/lib/src/utils.dart b/lib/src/utils.dart index c8911dd..06854af 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -140,4 +140,19 @@ class Utils { return 'd'; } } + + /// Returns [DirectionsMode] for [MapType.apple] + static String? getAppleDirectionsMode(DirectionsMode? directionsMode) { + switch (directionsMode) { + case DirectionsMode.driving: + return 'd'; + case DirectionsMode.walking: + return 'w'; + case DirectionsMode.transit: + return 'r'; + case DirectionsMode.bicycling: + case null: + return null; + } + } }