Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support to open map navigation/route guidance in app. #46

Open
patelsandeep opened this issue Nov 24, 2023 · 3 comments
Open

Add Support to open map navigation/route guidance in app. #46

patelsandeep opened this issue Nov 24, 2023 · 3 comments

Comments

@patelsandeep
Copy link

patelsandeep commented Nov 24, 2023

Hello @oguzhnatly , I am working in flutter app which requires to show direction and route guidance between two points.

Sharing screenshot for reference.

Screenshot 2023-11-29 at 5 46 23 PM

Can we add this functionality in the map showing above (CPPointOfInterestTemplate)?

I glanced to the native integration also which explained here https://developer.apple.com/documentation/carplay/integrating_carplay_with_your_navigation_app

And also go through CPPointOfInterestTemplate and flutter_carplay limitations.

I am newbie to flutter. Could you please explain possibilities I have mentioned.

Thanks.

@AhmedTitef
Copy link

i am having the same issue.

@ragul-steamA
Copy link

ragul-steamA commented Dec 30, 2024

Hi there, I am facing the same issue but i fixed this by editing some plugin code.

FlutterCarPlaySceneDelegate.swift


//  FlutterCarPlayPluginsSceneDelegate.swift
//  flutter_carplay
//
//  Created by Oğuzhan Atalay on 21.08.2021.
//

import CarPlay

@available(iOS 14.0, *)
class FlutterCarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
  static public var interfaceController: CPInterfaceController?
  static public var carplayScene: CPTemplateApplicationScene?

  static public func forceUpdateRootTemplate() {
    let rootTemplate = SwiftFlutterCarplayPlugin.rootTemplate
    let animated = SwiftFlutterCarplayPlugin.animated

    self.interfaceController?.setRootTemplate(rootTemplate!, animated: animated)
  }

  // Fired when just before the carplay become active
  func sceneDidBecomeActive(_ scene: UIScene) {
    SwiftFlutterCarplayPlugin.onCarplayConnectionChange(status: FCPConnectionTypes.connected)
  }

  // Fired when carplay entered background
  func sceneDidEnterBackground(_ scene: UIScene) {
    SwiftFlutterCarplayPlugin.onCarplayConnectionChange(status: FCPConnectionTypes.background)
  }

  static public func pop(animated: Bool) {
    self.interfaceController?.popTemplate(animated: animated)
  }

  static public func popToRootTemplate(animated: Bool) {
    self.interfaceController?.popToRootTemplate(animated: animated)
  }

  static public func push(template: CPTemplate, animated: Bool) {
    self.interfaceController?.pushTemplate(template, animated: animated)
  }

  static public func closePresent(animated: Bool) {
    self.interfaceController?.dismissTemplate(animated: animated)
  }

  static public func presentTemplate(
    template: CPTemplate, animated: Bool,
    onPresent: @escaping (_ completed: Bool) -> Void
  ) {
    self.interfaceController?.presentTemplate(
      template, animated: animated,
      completion: { completed, error in
        guard error != nil else {
          onPresent(false)
          return
        }
        onPresent(completed)
      })
  }

  func templateApplicationScene(
    _ templateApplicationScene: CPTemplateApplicationScene,
    didConnect interfaceController: CPInterfaceController
  ) {
    FlutterCarPlaySceneDelegate.interfaceController = interfaceController
    FlutterCarPlaySceneDelegate.carplayScene = templateApplicationScene

    SwiftFlutterCarplayPlugin.onCarplayConnectionChange(status: FCPConnectionTypes.connected)
    let rootTemplate = SwiftFlutterCarplayPlugin.rootTemplate

    if rootTemplate != nil {
      FlutterCarPlaySceneDelegate.interfaceController?.setRootTemplate(
        rootTemplate!, animated: SwiftFlutterCarplayPlugin.animated, completion: nil)
    }
  }

  static public func launchMapsApp(
    longitude: Double?, latitude: Double?, fromLongitude: Double? = nil, fromLatitude: Double? = nil
  ) {
    guard let destinationLongitude = longitude, let destinationLatitude = latitude else {
      NSLog("Destination coordinates are missing")
      return
    }

    let destination = "\(destinationLatitude),\(destinationLongitude)"
    let source =
      fromLongitude != nil && fromLatitude != nil
      ? "\(fromLatitude!),\(fromLongitude!)"
      : "current"

    let urlString = "maps://?daddr=\(destination)&saddr=\(source)"

    guard let url = URL(string: urlString) else {
      NSLog("Failed to create valid URL for Maps app")
      return
    }

    carplayScene?.open(url, options: nil) { success in
      if success {
        NSLog("Maps app launched successfully with route")
      } else {
        NSLog("Failed to launch Maps app")
      }
    }
  }

  static public func launchMaps(longitude: Double?, latitude: Double?) {
    guard let destinationLongitude = longitude, let destinationLatitude = latitude else {
      NSLog("Destination coordinates are missing")
      return
    }

    let googleMapsScheme = "comgooglemaps://"
    let webFallbackURL =
      "https://www.google.com/maps/dir/?api=1&destination=\(destinationLatitude),\(destinationLongitude)&travelmode=driving"

    // Construct the URL for Google Maps app
    if let url = URL(
      string:
        "\(googleMapsScheme)?saddr=current&daddr=\(destinationLatitude),\(destinationLongitude)&directionsmode=driving"
    ) {
      if UIApplication.shared.canOpenURL(url) {
        carplayScene?.open(url, options: nil) { success in
          if success {
            NSLog("Google Maps launched successfully with route")
          } else {
            NSLog("Failed to launch Google Maps app")
          }
        }
        return
      }
    }

    // Fallback to web browser if app is not installed
    if let appleMapURL = URL(string: "maps://?ll=\(destinationLatitude),\(destinationLongitude)") {
      carplayScene?.open(appleMapURL, options: nil) { success in
        if success {
          NSLog("Opened location in Apple Maps")
        } else {
          NSLog("Failed to open Apple Maps")
        }
      }
    } else {
      NSLog("Failed to create valid URL for Apple Maps")
    }
  }

  func templateApplicationScene(
    _ templateApplicationScene: CPTemplateApplicationScene,
    didDisconnect interfaceController: CPInterfaceController, from window: CPWindow
  ) {
    SwiftFlutterCarplayPlugin.onCarplayConnectionChange(status: FCPConnectionTypes.disconnected)

    //FlutterCarPlaySceneDelegate.interfaceController = nil
  }

  func templateApplicationScene(
    _ templateApplicationScene: CPTemplateApplicationScene,
    didDisconnectInterfaceController interfaceController: CPInterfaceController
  ) {
    SwiftFlutterCarplayPlugin.onCarplayConnectionChange(status: FCPConnectionTypes.disconnected)

    //FlutterCarPlaySceneDelegate.interfaceController = nil
  }
}

added a function launchMaps which should be called to redirect.

FCPPointOfInterest.swift

//
//  FCPPointOfInterest.swift
//  Runner
//
//  Created by Olaf Schneider on 15.02.22.
//

import CarPlay

@available(iOS 14.0, *)
class FCPPointOfInterest {
    private(set) var _super: CPPointOfInterest?
    private(set) var elementId: String
    private var latitude: Double
    private var longitude: Double
    private var title: String
    private var subtitle: String?
    private var summary: String?
    private var detailTitle: String?
    private var detailSubtitle: String?
    private var detailSummary: String?
    private var image: String?

    private var primaryButton: CPTextButton?
    private var objcPrimaryButton: FCPTextButton?

    private var secondaryButton: CPTextButton?
    private var objcSecondaryButton: FCPTextButton?

    static let maxPinImageSize: CGFloat = 40

    private var scene: CPTemplateApplicationScene?

    init(obj: [String: Any]) {
        self.elementId = obj["_elementId"] as! String

        let lat = obj["latitude"] as! NSNumber
        self.latitude = lat.doubleValue

        let lng = obj["longitude"] as! NSNumber
        self.longitude = lng.doubleValue
        self.title = obj["title"] as! String
        self.subtitle = obj["subtitle"] as? String
        self.summary = obj["summary"] as? String
        self.detailTitle = obj["detailTitle"] as? String
        self.detailSubtitle = obj["detailSubtitle"] as? String
        self.detailSummary = obj["detailSummary"] as? String
        self.image = obj["image"] as? String
        self.scene = FlutterCarPlaySceneDelegate.carplayScene as? CPTemplateApplicationScene

        let primaryButtonData = obj["primaryButton"] as? [String: Any]
        if primaryButtonData != nil {
            self.objcPrimaryButton = FCPTextButton(obj: primaryButtonData!)

            // Create a new CPTextButton with all required parameters
            let button = CPTextButton(
                title: self.objcPrimaryButton?.get.title ?? "",
                textStyle: .normal,  // You can change to .confirm if needed
                handler: { [weak self] _ in
                    FlutterCarPlaySceneDelegate.launchMaps(longitude: self?.longitude, latitude: self?.latitude)
                }
            )

            self.primaryButton = button
        }
        let secondaryButtonData = obj["secondaryButton"] as? [String: Any]
        if secondaryButtonData != nil {
            self.objcSecondaryButton = FCPTextButton(obj: secondaryButtonData!)
            self.secondaryButton = self.objcSecondaryButton?.get
        }
    }

    

    var get: CPPointOfInterest {

        let location = MKMapItem(
            placemark: MKPlacemark(
                coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude)))
        var pinImage: UIImage? = nil

        if let image = self.image {
            let key = SwiftFlutterCarplayPlugin.registrar?.lookupKey(forAsset: image)

            pinImage = UIImage(named: key!)
            if let pImage = pinImage {
                if pImage.size.height > FCPPointOfInterest.maxPinImageSize
                    || pImage.size.width > FCPPointOfInterest.maxPinImageSize
                {
                    pinImage = pImage.resizeImageTo(
                        size: CGSize(
                            width: FCPPointOfInterest.maxPinImageSize,
                            height: FCPPointOfInterest.maxPinImageSize))
                }
            }
        }
        let poi = CPPointOfInterest(
            location: location, title: title, subtitle: subtitle, summary: summary,
            detailTitle: detailTitle, detailSubtitle: detailSubtitle,
            detailSummary: detailSummary, pinImage: pinImage)

        if let primaryButton = self.primaryButton {
            poi.primaryButton = primaryButton

        }
        if let secondaryButton = self.secondaryButton {
            poi.secondaryButton = secondaryButton
        }

        self._super = poi
        return poi
    }
}

I had overiden the primary button functionality to call the launch function.

in Flutter:

CPPointOfInterest(
            latitude:latitude,
            longitude:longitude,
            title: 'title',
            subtitle: 'subtitle',
            summary: 'summary',
            primaryButton: CPTextButton(
              title: 'Navigate',
              onPress: () async {},
            ),
          )

Use the POI template like mentioned here.

@ragul-steamA
Copy link

Use a real device with carplay simulator for testing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants