ionic-team / capacitor-google-maps

16 stars 24 forks source link

Cannot customize marker with base64 png url #49

Open yiannis-spyridakis opened 1 month ago

yiannis-spyridakis commented 1 month ago

Bug Report

I am adding markers with code like this:

const marker: Marker = {
    coordinate: gasStation.location,
    iconUrl: 'data:image/png;base64,...', // Base64 png url, tested and working with the js api, cordova plugin
    iconSize: {
        width: 38,
        height: 48
    },
}
const markerId = await nativeMap.addMarker(marker);

The image is ignored, I just get the standard marker icon.

Is this a known missing feature? By investigating, I was able to find some discussions regarding base64 svg icons but no direct confirmation that data urls are unsupported.

Can you please confirm?

Plugin(s)

@capacitor/google-maps

Capacitor Version

Latest Dependencies:

  @capacitor/cli: 6.1.2
  @capacitor/core: 6.1.2
  @capacitor/android: 6.1.2
  @capacitor/ios: 6.1.2

Installed Dependencies:

  @capacitor/cli: 6.1.2
  @capacitor/core: 6.1.2
  @capacitor/android: 6.1.2
  @capacitor/ios: 6.1.2

[success] iOS looking great! πŸ‘Œ
[success] Android looking great! πŸ‘Œ

Platform(s)

Android IOS? (only tested on Android so far)

Current Behavior

Data urls are ignored when adding Markers.

Expected Behavior

A workaround or confirmation that this is expected.

Other Technical Details

The data url is exported from an HTML canvas like so:

// Export to base64 string
const ret = canvas.toDataURL('image/png');
yiannis-spyridakis commented 1 month ago

Hi, For Android I have confirmed that the plugin currently doesn't support data urls. It either opens a url starting with https:// or it looks for a sub-path under public:

(code in CapacitorGoogleMap.buildMarker)

  var stream: InputStream? = null
  if (marker.iconUrl!!.startsWith("https:")) {
      stream = URL(marker.iconUrl).openConnection().getInputStream()
  } else {
      stream = this.delegate.context.assets.open("public/${marker.iconUrl}")
  }
  var bitmap = BitmapFactory.decodeStream(stream)
  this.markerIcons[marker.iconUrl!!] = bitmap
  markerOptions.icon(getResizedIcon(bitmap, marker))

This works for me:

  var stream: InputStream? = null
  if (marker.iconUrl!!.startsWith("data:")) {
      // Extract the base64 part for the data URL
      val base64Data = marker.iconUrl!!.split(",")[1]

      // Decode the base64 string into a byte array
      val decodedBytes = Base64.decode(base64Data, Base64.DEFAULT)

      // Convert the byte array to an InputStream
      stream = ByteArrayInputStream(decodedBytes)

  }
  else if (marker.iconUrl!!.startsWith("https:")) {
      stream = URL(marker.iconUrl).openConnection().getInputStream()
  } else {
      stream = this.delegate.context.assets.open("public/${marker.iconUrl}")
  }
  val bitmap = BitmapFactory.decodeStream(stream)
  this.markerIcons[marker.iconUrl!!] = bitmap
  markerOptions.icon(getResizedIcon(bitmap, marker))

Please let me know if you are interested in accepting contributions. Is the plugin actively maintained?

yiannis-spyridakis commented 1 month ago

For completeness, as expected the behavior is the same on IOS.

The relevant function is Map.buildMarker

The relevant code section is here:

        // cache and reuse marker icon uiimages
        if let iconUrl = marker.iconUrl {
            if let iconImage = self.markerIcons[iconUrl] {
                newMarker.icon = getResizedIcon(iconImage, marker)
            } else {
                if iconUrl.starts(with: "https:") {
                    if let url = URL(string: iconUrl) {
                        URLSession.shared.dataTask(with: url) { (data, _, _) in
                            DispatchQueue.main.async {
                                if let data = data, let iconImage = UIImage(data: data) {
                                    self.markerIcons[iconUrl] = iconImage
                                    newMarker.icon = getResizedIcon(iconImage, marker)
                                }
                            }
                        }.resume()
                    }
                } else if let iconImage = UIImage(named: "public/\(iconUrl)") {
                    self.markerIcons[iconUrl] = iconImage
                    newMarker.icon = getResizedIcon(iconImage, marker)

And this fix works for me:

       if let iconUrl = marker.iconUrl {
            if let iconImage = self.markerIcons[iconUrl] {
                newMarker.icon = getResizedIcon(iconImage, marker)
            } else {
                if iconUrl.starts(with: "data") {
                  // Handle base64 encoded image
                  if let dataRange = iconUrl.range(of: "base64,") {
                    let base64String = String(iconUrl[dataRange.upperBound...])
                    if let imageData = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters),
                      let iconImage = UIImage(data: imageData) {
                        self.markerIcons[iconUrl] = iconImage
                        newMarker.icon = getResizedIcon(iconImage, marker)
                    } else {
                      print("CapacitorGoogleMaps Wanring: could not decode base64 image. Using default marker icon.")
                    }
                  } else {
                    print("CapacitorGoogleMaps Warning: invalid data URL format. Using default marker icon.")
                  }
                } else if iconUrl.starts(with: "https:") {
                    if let url = URL(string: iconUrl) {
                        URLSession.shared.dataTask(with: url) { (data, _, _) in
                            DispatchQueue.main.async {
                                if let data = data, let iconImage = UIImage(data: data) {
                                    self.markerIcons[iconUrl] = iconImage
                                    newMarker.icon = getResizedIcon(iconImage, marker)
                                }
                            }
                        }.resume()
                    }
                } else if let iconImage = UIImage(named: "public/\(iconUrl)") {
                    self.markerIcons[iconUrl] = iconImage
                    newMarker.icon = getResizedIcon(iconImage, marker)
                }