3

My iOS app allows users to reproject rasters to different spatial reference systems. This is implemented with GDAL Utils (C API, in a Swift wrapper) to run a GDALWarp(). Sometimes these operations can take a long time for large rasters.

My users would like to have a 'Cancel' button but I'm not sure of the best way to actually cancel a GDALWarp() while it is in progress. The GDALWarp() is run within a function that is within a separate Operation (thread-handling object), and in theory I could easily cancel the Operation object and dismiss the progress dialogue in the GUI. But this would leave the warp running in the background, consuming power, CPU, RAM and storage, (and quite a lot of all four), so this would not be acceptable. I still need the cancellation to actually kill the GDALWarp().

Here's how I currently run the warp (this is Swift code utilising GDAL Utils C API):

    func reprojectTo(_ url: URL, driver: Driver, srs: SRS) -> (Bool, Int32?) {
        guard let destSRSWKT = srs.wkt() else { return (false, nil) }
        let opts = ["-t_srs", destSRSWKT, "-r", "cubic", "-dstalpha", "-of", driver.nameShort()]
        
        return withMutableArrayOfMutableCStrings(opts) { (cOpts) in
            var optionalSourceDSRef: GDALDatasetH? = _ds
            return withUnsafeMutablePointer(to: &optionalSourceDSRef) { (dsPointer) in
                let warpOpts = GDALWarpAppOptionsNew(cOpts, nil)
                var err: Int32 = 0
                let destDSRef = GDALWarp(url.path, nil, 1, dsPointer, warpOpts, &err)
                GDALWarpAppOptionsFree(warpOpts)
                
                if destDSRef == nil {
                    logger.error("FAILED to warp with driver \(driver.nameShort()) to SRS \(srs.authorityCode() ?? "(?)") at:\n\(url)")
                    return (false, err)
                } else {
                    GDALClose(destDSRef)
                    return (true, nil)
                }
            }
        }
    }

What's the correct way to completely kill a running GDALWarp()?

1 Answer 1

0

I tripped over the answer to this completely by accident while trying to figure out how to implement a more meaningful linnear progress indicator (instead of just the spinning activity indicator that I had been using).

It turns out that if a progress callback function is passed to the warp options (to do something with the progress value, such as update a progress indicator, or to print/log the progress percent), the return value of the progress callback is used to determine if the progress should continue (1/true) or to cancel (0/false).

It works well, and on returning 0 from the callback, GDAL's warp even logs a sensible, ERROR 9: User terminated message.

In brief, use the GDALWarpAppOptionsSetProgress() function to pass a progress callback function to the warp options before running the warp. This callback function should usually return 1 to continue progressing or 0 to cancel.

The callback must comply with GDALProgressFunc() type. Ie,

typedef int (*GDALProgressFunc)(double dfComplete, const char *pszMessage, void *pProgressArg)

Here's how it works in the context of my code above, using a Swift closure as the callback function:

    func reprojectTo(_ url: URL, driver: Driver, srs: SRS) -> (Bool, Int32?) {
        guard let destSRSWKT = srs.wkt() else { return (false, nil) }
        let opts = ["-t_srs", destSRSWKT, "-r", "cubic", "-dstalpha", "-of", driver.nameShort()]

        return withMutableArrayOfMutableCStrings(opts) { (cOpts) in
            var optionalSourceDSRef: GDALDatasetH? = _ds
            return withUnsafeMutablePointer(to: &optionalSourceDSRef) { (dsPointer) in
                let warpOpts = GDALWarpAppOptionsNew(cOpts, nil)
                GDALWarpAppOptionsSetProgress(warpOpts, { (progress, cMessage, nil) in
                    let message = cMessage == nil ? "" : String(cString: cMessage!)
                    logger.log("\(progress * 100) % \(message)")
                    if userHasCancelled() {
                        return 0
                    }
                    return 1
                }, nil)
                var err: Int32 = 0
                let destDSRef = GDALWarp(url.path, nil, 1, dsPointer, warpOpts, &err)
                GDALWarpAppOptionsFree(warpOpts)
                
                if destDSRef == nil {
                    logger.error("FAILED to warp with driver \(driver.nameShort()) to SRS \(srs.authorityCode() ?? "(?)") at:\n\(url)")
                    return (false, err)
                } else {
                    GDALClose(destDSRef)
                    return (true, nil)
                }
            }
        }
    }

The userHasCancelled() function just needs to check if the 'Cancel' button has been tapped, which is fairly straightforward (the 'Cancel' button just needs to set a isCancelled boolean to true).

(Just for the sake of completeness, I should note that my actual live code is somewhat more complicated than the above example due to Swift not being able to pass a closure as a C function if the closure includes any context (ie, any access to anything not actually passed directly into the function). This results in the need for some rather messy conversion of objects to void * to pass into the closure and then converting back to Swift objects again within the closure. I've omitted this from the example above because it makes it more difficult to read, and would not apply to pure C, anyhow.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.