Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

MGLAnnotationImage with scale greater than 1.0 causes crash. #2198

Closed
petrmanek opened this issue Aug 28, 2015 · 14 comments · Fixed by #3561
Closed

MGLAnnotationImage with scale greater than 1.0 causes crash. #2198

petrmanek opened this issue Aug 28, 2015 · 14 comments · Fixed by #3561
Assignees
Labels
crash iOS Mapbox Maps SDK for iOS

Comments

@petrmanek
Copy link

Hello, I'm using MapboxGL 0.5 in my iOS map. When trying to annotate map with images of markers on retina displays, I got this error:

libc++abi.dylib: terminating with uncaught exception of type mbgl::util::SpriteImageException: Sprite image pixel count mismatch

Here's my setup:

class MyViewController: UIViewController, MGLMapViewDelegate {
    var mapView: MGLMapView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Create the map view.
        mapView = MGLMapView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
        mapView.delegate = self
        view.addSubview(mapView)

        // Add a single marker.
        let point = MGLPointAnnotation()
        point.coordinate = ...
        mapView.addAnnotation(point)
        mapView.setCenterCoordinate(...)
    }

    func mapView(mapView: MGLMapView, imageForAnnotation annotation: MGLAnnotation) -> MGLAnnotationImage? {
        // Memory Efficiency: If a marker is available from the reusable queue, make use of it.
        if let pin = mapView.dequeueReusableAnnotationImageWithIdentifier("customPin") {
            return pin
        }

        // Scale of the image below is automatically selected based on the display's pixel density.
        // Both @1x and @2x version is available. However, only @2x version causes the crash.
        let image = UIImage(named: "myCustomImage")!
        return MGLAnnotationImage(image: image, reuseIdentifier: "customPin")
    }
}

As I said in the comments, the crash occurs only when image.scale > 1.0. I've attempted to use the @1x version of the image only but that looks blurry and ugly on the retina display.

To confirm that the error isn't caused by a malformed image file, I've also tried several other @2x images. All of them crashed the app.

I have even managed to prove that this bug is caused explicitly by the scale of the image. The following code works because I've manually tuned down the scale of the @2x image from 2.0 to 1.0. The annotation displays on the map view correctly but has double the size of the original image. You can try and substitute any other scale in the let scaledUpImage... line to see my point. If you use scale greater than 1.0, you get a crash.

    func mapView(mapView: MGLMapView, imageForAnnotation annotation: MGLAnnotation) -> MGLAnnotationImage? {
        if let pin = mapView.dequeueReusableAnnotationImageWithIdentifier("customPin") {
            return pin
        }

        // This image will have scale 2.0 on retina displays.
        let image = UIImage(named: "myCustomImage")!

        // Use it to create a new image with scale 1.0 and double size.
        let scaledUpImage = UIImage(CGImage: image.CGImage, scale: 1.0, orientation: .Up)!
        return MGLAnnotationImage(image: scaledUpImage, reuseIdentifier: "customPin")
    }

Are there any workarounds?

@incanus incanus added bug iOS Mapbox Maps SDK for iOS labels Aug 28, 2015
@friedbunny
Copy link
Contributor

Great bug report, thanks for that. So far as we've seen (such as #1936), this error+crash happens when the image resources are not exactly sized in relation to each other. Check that the dimensions of your @2x resource are an exact doubling of your @1x.

@picciano
Copy link

IMHO, app should not crash on size discrepancy. At worst, log a warning to console. Can the images just be padded with transparent pixels if they aren't exactly @2x?

@1ec5 1ec5 added crash and removed bug labels Aug 28, 2015
@1ec5 1ec5 added this to the ios-v2.2.0 milestone Aug 28, 2015
@1ec5
Copy link
Contributor

1ec5 commented Aug 28, 2015

We should either pad or scale, but not crash.

@petrmanek
Copy link
Author

@friedbunny You are correct. Our graphics guy has designed the images in 2x and didn't notice their sizes were odd numbers. When generating the fallback 1x version, his software has automatically rounded their dimensions down.

Padding all images with transparent pixels fixed the crash.

FIY Here are all versions of the map pin which has caused the crash. Their sizes are:

  • 1x: 32x41 pixels

    1x version

  • 2x: 63x81 pixels (should be 64x82)

    2x version

  • 3x: 95x122 pixels (should be 96x123)

    3x version

On the behavior of your library, I agree with @picciano and @1ec5 . Crashing the app with a mysterious C++ message is too brutal for me and it might cost others a lot of time and coffee to find this thread. Not to mention that other Apple components deal with similar issues a lot more often and manage to handle them gracefully.

IMHO your library should pad (or crop) the image to the correct size since this is the behavior of Apple components (thus other developers might expect it from your map view as well). If that approach means a significant slowdown in your rendering engine, you can always print a console warning that the performance might be affected by the additional processing.

@friedbunny
Copy link
Contributor

Agreed, this should not crash and we should accommodate mismatched image dimensions. I'll reopen this so we can track a fix.

@clearbrian
Copy link

Also if you generate the UIImage from CoreGraphics I had to make sure the UIImage CGSize for all pins was the same else it would crash. I wanted the annotation images to be text labels which resized to the name of the location. I got around it by having a large but fixed size UIImage.frame but with clear Background then filling an inner rect which resized to sizeWithAttributes

@clearbrian
Copy link

func mapView(mapView: MGLMapView, imageForAnnotation annotation: MGLAnnotation) -> MGLAnnotationImage? {
        var annotationImage : MGLAnnotationImage? = nil     //self.mapView.dequeueReusableAnnotationImageWithIdentifier("map_pin_red")


    //seems to be a double optional! String??

    var title = ""
    var subTitle = ""
    //--------------------------------------------------
    //TITLE
    //--------------------------------------------------

    if let titleOpt = annotation.title{
        if let title_ = titleOpt{
            title = title_

        }else{
            appDelegate.log.error("titleOpt is nil")
        }
    }else{
        appDelegate.log.error("title is nil")
    }
    //--------------------------------------------------
    //SUBTITLE
    //--------------------------------------------------
    if let subtitleOpt = annotation.subtitle{
        if let subtitle_ = subtitleOpt{
            subTitle = subtitle_

        }else{
            appDelegate.log.error("subtitleOpt is nil")
        }
    }else{
        appDelegate.log.error("<#EXPR#> is nil")
    }
    //---------------------------------------------------------------------
    if title == "" {

    }else{
        if subTitle == "VESSEL" {
            let imageOut = self.textToImage(title , iconColor: UIColor.appColorFlat_TahitiGold_Orange())
            annotationImage = MGLAnnotationImage(image: imageOut, reuseIdentifier: title)
        }
        else if subTitle == "LOCATION" {
            let imageOut = self.textToImage(title , iconColor: UIColor.appColorButtonPink())
            annotationImage = MGLAnnotationImage(image: imageOut, reuseIdentifier: title)
        }
        else{

            let imageOut = self.textToImage(title ,iconColor: UIColor.appColorCYAN())
            annotationImage = MGLAnnotationImage(image: imageOut, reuseIdentifier: title)
        }
    }
    //---------------------------------------------------------------------

    return annotationImage
}

/*
Generate a map pin as UIImage.
We draw the text is a specific rectangle.
The size of the rect is calculated using sizeWithAttributes
to find the largest frame needed to display this text string at this font

ISSUES: Crash in Mapbox is all UIImages used are different sizes. 
So I cheat and keep the UIImage at a fixed size.
I display names of ports so found the largest one possible
"Ginsheim-Gustavsburg"
found "Ginsheim-Gustavsburg".sizeWithAttributes
//    CGSize
//    - width : 135.826171875
//    - height : 14.3203125
So my UIImage had to be at least 135x14 so I chose 
let sizeLabel:CGSize = CGSizeMake(200, 20)

To prevent lots of gaps I cheat and have an outer and inner rectangle fill then drawText over inner fill to mimic a UILabel
UIImage - CGSizeMake(200, 20)
    CGFILL Background color : clear - rect (200,20)

    INNER LABEL - Note not UILabel
        CGFill -
        drawText inner text
                will vary depending on length of port name
                "Ginsheim-Gustavsburg".sizeWithAttributes  =  CGSize(width : 135.826171875, height : 14.3203125)
                                "Hull".sizeWithAttributes  =  CGSize(width : 23.267578125, height : 14.3203125)


*/
func textToImage(textTitle: String, iconColor : UIColor) -> UIImage{


    let textTitleNSString = textTitle as NSString
    //---------------------------------------------------------------------
    let textColor: UIColor = UIColor.whiteColor()
    //---------------------------------------------------------------------
    //THE ONLY TWO THINGS YOU NEED TO ADJUST TO RESIZE THE LABEL
    //Strings to test

    //South California Lightering Area
    //Grytviken - if you include zone - 

    //ISSUE: https://github.com/mapbox/mapbox-gl-native/issues/2198
    //All map icon images used should be same size, can be very large - it has clear background
    //if text is chopped of then increase this - it has clear background so wont be seen
    let imageWidthCGFloat : CGFloat = 300.0

    //center the  visible lable in center of whole label
    //in fact have LHS of visible lable on center line of outer label 
    //this way the square pin seems to be over the location
    let centerXCGFloat : CGFloat = imageWidthCGFloat/3

    //set the font before we

    let borderDouble = 2.0

    //text is drawn into exact rect but need gap on LHS and RHS
    let textPadding : CGFloat = 5.0

    //let textFontTitle: UIFont = UIFont.boldSystemFontOfSize(12.0)
    let textFontTitle: UIFont = UIFont.systemFontOfSize(12.0)

    //---------------------------------------------------------------------
    //find the largest frame needed to display this text string at this font
    //This is the size of the inner label
    var sizeTitle:CGSize = textTitleNSString.sizeWithAttributes([NSFontAttributeName: textFontTitle])
    //add extra line to handle smaller lettter g drop below baseline
    sizeTitle.height = sizeTitle.height + 1
    //---------------------------------------------------------------------
    //Calc the UIImage rect from the sizeWithAttributes for the set font
    //Lets you tweak the font and all labels text generated ok
    //UIFont.boldSystemFontOfSize(12.0) >> 14.3203125
    //Round Up
    //CGFloat >> Double[14.3333] >> Int [14]
    let sizeTitleHeightFloat = Float(sizeTitle.height)

    var sizeTitleHeightInt:Int = Int(sizeTitleHeightFloat)
    //round up 14.333 >> 14 >> 15
    sizeTitleHeightInt = sizeTitleHeightInt + 1
    //15 >> 15.00
    let sizeTitleHeightDouble = Double(sizeTitleHeightInt)
    let sizeTitleHeightCGFloat = CGFloat(sizeTitleHeightDouble)
    //---------------------------------------------------------------------
    //white border around the text

    let borderCGFloat = CGFloat(borderDouble)
    //---------------------------------------------------------------------
    //HEIGHT oF UIMAGE
    let imageHeightDouble : Double = sizeTitleHeightDouble +  /* height of the text for current font - rounded up 14.3333 > 15 */
                                     (borderDouble * 4.0)          /* border above and below text - with white background plus border to edge of UIIMage*/

    let imageHeightCGFloat = CGFloat(imageHeightDouble)
    //---------------------------------------------------------------------
    //HEIGHT oF WHITE BORDER Rect
    let borderRectHeightDouble : Double = sizeTitleHeightDouble +  /* height of the text for current font - rounded up 14.3333 > 15 */
                                            (borderDouble * 2.0)          /* border above and below text - with white background plus border to edge of UIIMage*/

    let borderRectHeightCGFloat = CGFloat(borderRectHeightDouble)
    //---------------------------------------------------------------------


    let sizeUIImageReturned:CGSize = CGSizeMake(imageWidthCGFloat, imageHeightCGFloat)

    let sizeBorderRect:CGSize = CGSizeMake(borderCGFloat + sizeTitleHeightCGFloat + borderCGFloat + textPadding + sizeTitle.width + textPadding + borderCGFloat,
                                           borderRectHeightCGFloat)
    //---------------------------------------------------------------------

    //let offsetTitle: CGPoint = CGPoint(x: 5, y: 5)

    //WHOLE LABEL iS WHITE RECTANGLE
    UIGraphicsBeginImageContextWithOptions(sizeUIImageReturned, false, 0)

    let context = UIGraphicsGetCurrentContext()

    //FILL THE WHOLE UIIMAGE with clear
    //comment in to see outer UIImage fill
    //CGContextSetFillColorWithColor(context, UIColor.redColor().CGColor)
    CGContextSetFillColorWithColor(context, UIColor.clearColor().CGColor)
    CGContextFillRect(context, CGRect(origin: CGPoint(x: 0, y: 0), size: sizeUIImageReturned))

    //---------------------------------------------------------------------
    //WHITE BORDER - actually one large rect behind all others
    CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor)
    CGContextFillRect(context, CGRect(origin: CGPoint(x: centerXCGFloat + borderCGFloat, y: borderCGFloat), size: sizeBorderRect))

    //SQUARE
    CGContextSetFillColorWithColor(context, iconColor.CGColor)
    CGContextFillRect(context,
        CGRect(origin: CGPoint(x: centerXCGFloat + (borderCGFloat * 2.0), y: borderCGFloat * 2.0),
            size: CGSizeMake(sizeTitle.height, sizeTitle.height)))




    //------------------------------------------------------------------------------------------------
    //SET TEXT
    //------------------------------------------------------------------------------------------------
    let style = NSMutableParagraphStyle()
    style.alignment = .Left

    let attrTitle = [NSFontAttributeName:textFontTitle, NSForegroundColorAttributeName:textColor, NSParagraphStyleAttributeName:style]



    let rectTitle = CGRect(    x: centerXCGFloat + textPadding + borderCGFloat + sizeTitle.height + borderCGFloat + borderCGFloat,
                               y: borderCGFloat + borderCGFloat,
                           width: sizeTitle.width,
                          height: sizeTitle.height)
    //---------------------------------------------------------------------
    //TEXT BACKGROUND RECT
    CGContextSetFillColorWithColor(context, UIColor.appColorCYAN().CGColor)
    let sizeTextBackground : CGSize = CGSize(width: textPadding + sizeTitle.width + textPadding ,
                                            height: sizeTitle.height)


    CGContextFillRect(context, CGRect(origin: CGPoint(x: centerXCGFloat + borderCGFloat + sizeTitle.height + borderCGFloat + borderCGFloat,
                                                      y: borderCGFloat + borderCGFloat),
                                                   size: sizeTextBackground))
    //---------------------------------------------------------------------
    //DRAW TEXT over TEXT BACKGROUND RECT with padding LHS and RHS so its no flush at 0,0
    textTitleNSString.drawInRect(rectTitle, withAttributes: attrTitle)

    //GENERATE THE UIImage to use as map icon
    //Must be same size for all icons on map due to bug in MapBox
    let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()

    // End the context now that we have the image we need
    UIGraphicsEndImageContext()
    return newImage
}

@clearbrian
Copy link

simulatorscreensnapz025

@1ec5
Copy link
Contributor

1ec5 commented Nov 12, 2015

#3008 should fix both examples above. @clearbrian, the issue is not that the annotation images had different sizes: rather, if you make textToImage() calculate an image width that fits the text, the width would often be non-integral.

1ec5 added a commit that referenced this issue Nov 12, 2015
On HiDPI screens, the logical dimensions of a sprite image can be non-integral.

Fixes #2198.
@1ec5 1ec5 removed this from the ios-v3.0.0 milestone Nov 17, 2015
@1ec5
Copy link
Contributor

1ec5 commented Nov 17, 2015

Removing from the 3.0.0 milestone because there isn’t consensus on #3008. The workaround is to ensure that your sprite image’s width and height are both whole numbers.

@ansis
Copy link
Contributor

ansis commented Jan 13, 2016

#3530 which implements #3164 should fix this. @1ec5 could you confirm that it's fixed?

@1ec5
Copy link
Contributor

1ec5 commented Jan 14, 2016

@ansis, no, the same crash still reproduces. However, does #3530 make #3008 viable as a fix for this crash?

ansis added a commit that referenced this issue Jan 15, 2016
ref #3031
ref #2198

For example, an icon that has:
- a pixel width of 10
- a pixel ratio of 3
- a scaled with of 3.333
is now supported.
@ansis ansis self-assigned this Jan 15, 2016
ansis added a commit that referenced this issue Jan 16, 2016
ref #3031
ref #2198

For example, an icon that has:
- a pixel width of 10
- a pixel ratio of 3
- a scaled with of 3.333
is now supported.
ansis added a commit that referenced this issue Jan 20, 2016
ref #3031
ref #2198

For example, an icon that has:
- a pixel width of 10
- a pixel ratio of 3
- a scaled with of 3.333
is now supported.
@ansis ansis removed the in progress label Jan 20, 2016
@lucacorti
Copy link

Still getting this is 3.0.1:

libc++abi.dylib: terminating with uncaught exception of type mbgl::util::SpriteImageException: Sprite image pixel count mismatch

@1ec5
Copy link
Contributor

1ec5 commented Jan 24, 2016

@lucacorti, the fix is only in version 3.1.0-pre.1 of the Mapbox iOS SDK. It is not fixed in version 3.0.1.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
crash iOS Mapbox Maps SDK for iOS
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants