1

GDAL includes a comprehensive low-level API in C and C++. It also includes some high-level APIs that effectively run the equivalent of some of the GDAL command-line utilities. (See: https://gdal.org/development/rfc/rfc59.1_utilities_as_a_library.html ).

The supported command-line utilities that can be effectively run directly in C/C++ using this API include gdalwarp, gdalinfo and gdal_translate. However, gdal2tiles is not available this way (presumably because it's a Python script, rather than a C/C++ utility program). Additionally, because I'm runnning on iOS, I do not have access to simply run the actual gdal2tiles utility itself (eg, with a system call).

Using the GDAL C APIs, what is the best way to run the equivalent of gdal2tiles?

Or is my only option to look up the actual source code of the gdal2tiles utility itself, and translate the relevant portions into C code? (Would be waaaaay overkill if there is any other way!)

My goal is to produce a TMS directory, including TMS metadata, from a user-supplied image file and user-supplied (interactively generated) georeferencing information. Or something similar that can be managed by GDAL and read by OpenLayers. (I did briefly consider MBTiles instead. However, the GDAL driver for MBTiles specifies that it only works for pseudo-mercator and I need to work with all SRSs.)

(I did something similar without GDAL a few years ago, including pyramid tiles. But was hoping to come up with a more standardised, flexible and maintainable method. I may have to look up my old Objective-C code and see if it is applicable to my current situation, and maybe translate it into Swift.)

1
  • Tiling a raster using GDAL in C++ while not trivial isn't as hard as it sounds especially if you already know the data type and number of bands, a lot of the GDAL2tiles.py is working out exactly what you're dealing with... however as you have GDAL_Translate all you need to do is calculate your boxes, iterate over them and supply as -srcwin providing a different output name to avoid overwriting. Mar 15, 2021 at 0:47

1 Answer 1

0

Below is how I ended up implementing this in the Swift language.

(Although my original question requested an answer for the GDAL C API, my app actually uses my own Swift language wrapper around the GDAL C functions. This abstracts the C functionality away, and makes the Swift programming much simpler. I need to add a few bits and pieces to my GDAL Swift wrapper to provide the required functionality, but for the actual raster tiling and pyramid work, the heavy lifting is all done in the Swift function below.)

Because this is written in Swift, and the GDAL calls are abstracted by higher-level wrappers, it should be treated by other readers as pseudo-code.

Note that this function includes a few extras required for this particular app's case:

  • feeds progress information to a passed-in progress handler function
  • can be cancelled by an external thread setting the isCancelled property
  • writes out meta-data to a .plist file so that when displaying the layer later, the app knows how to interpret it (this app uses OpenLayers to display the layer on the map as an XYZ source)
    func tileRaster(_ ds: RasterDataset, baseDestURL: URL, imageFormat: ImageTileFormat = .png, tileSize: Int = 256, progressHandler: ((_ totalCount: Int, _ processedCount: Int, _ errorCount: Int) -> Void)? = nil) -> Bool {
        let imageRect = ds.rasterRect()
        guard let geoTransform = GeoTransform(dataset: ds) else { return false }
        guard let layerSRSCode = ds.srs()?.authorityCode() else { return false }
        //  Inverted Y for GDAL rasters and OpenLayers XYZ tiles (from image perspective)
        let layerBottomLeft = geoTransform.transformedPoint(CGPoint(x: 0, y: imageRect.height))
        let layerTopRight = geoTransform.transformedPoint(CGPoint(x: imageRect.width, y: 0))
        let layerExtent = [Double(layerBottomLeft.x), Double(layerBottomLeft.y), Double(layerTopRight.x), Double(layerTopRight.y)]
        //  Use the greater image dimension to calculate resolution (likely to be more accurate that way)
        var resolution = imageRect.width > imageRect.height ? ( layerExtent[2] - layerExtent[0] ) / Double(imageRect.width) : ( layerExtent[3] - layerExtent[1] ) / Double(imageRect.height)
        var maxZoom = 0
        var tileSizes: Array<Array<Int>> = []
        var resolutions: Array<Double> = []
        var origins: Array<Array<Double>> = []
        var totalTiles = 0
        var processedTiles = 0
        var errorTiles = 0
        
        //  Count total tiles required (all zoom levels) and count zoom levels required
        var srcTileSize = CGFloat(tileSize)
        var z = -1
        while srcTileSize < 2 * max(imageRect.width, imageRect.height) || z < 0 {
            z += 1
            let xTiles = Int(ceil(imageRect.width / srcTileSize))
            let yTiles = Int(ceil(imageRect.height / srcTileSize))
            totalTiles += xTiles * yTiles

            srcTileSize *= 2
        }
        
        maxZoom = z

        //  Reset source tiles size after counting tiles and this time count zoom levels down in reverse
        //  maxZoom is at natural image resolution (level "0" is zoomed out with image scaled down to size of 1 tile)
        srcTileSize = CGFloat(tileSize)
        for z in (0...maxZoom).reversed() {
            do {
                try FileManager.default.createDirectory(at: baseDestURL.appendingPathComponent("\(z)"), withIntermediateDirectories: false, attributes: [:])
            } catch {
                return false
            }
            tileSizes.append([tileSize, tileSize])
            resolutions.insert(resolution, at: 0)
            origins.append([layerExtent[0], layerExtent[3]])
            
            let xTiles = Int(ceil(imageRect.size.width / srcTileSize))
            let yTiles = Int(ceil(imageRect.size.height / srcTileSize))
        
            for x in 0..<xTiles {
                do {
                    try FileManager.default.createDirectory(at: baseDestURL.appendingPathComponent("\(z)").appendingPathComponent("\(x)"), withIntermediateDirectories: false, attributes: [:])
                } catch {
                    return false
                }
                for y in 0..<yTiles {
                    if isCancelled { return false }
                    
                    let tileRect = CGRect(x: srcTileSize * CGFloat(x), y: srcTileSize * CGFloat(y),  width: srcTileSize, height: srcTileSize)
                    let tileURL = baseDestURL.appendingPathComponent("\(z)").appendingPathComponent("\(x)").appendingPathComponent("\(y).\(imageFormat.rawValue)")
                    
                    if !ds.writeImageForRect(tileRect, toURL: tileURL, outTileSize: tileSize, as: imageFormat) {
                        errorTiles += 1
                    }
                    processedTiles += 1
                    
                    progressHandler?(totalTiles, processedTiles, errorTiles)
                }
            }
            srcTileSize *= 2
            resolution *= 2
        }
        
        if errorTiles > 0 || processedTiles == 0 {
            return false
        }
        
        let tileGrid: Dictionary<String,Any> = [
            "minZoom": 0,
            "maxZoom": maxZoom,
            "tileSizes": tileSizes,
            "resolutions": resolutions,
            "origins": origins,
            "extent": layerExtent,
        ]
        
        guard let jsonData = try? JSONSerialization.data(withJSONObject: tileGrid, options: []), let jsonString = String(data: jsonData, encoding: .utf8) else { return false }
        let newLayerName = baseDestURL.lastPathComponent
        let manifest: Dictionary<String,Any> = [
            "title": newLayerName,
            "dsName": newLayerName,
            "type": RasterServiceType.tileGrid.rawValue,
            "urlTileFormat": "{z}/{x}/{y}.\(imageFormat.rawValue)",
            "onlineOffline": "offline",
            "srsAuth": layerSRSCode,
            "tileGrid": jsonString,
            //  "url" not used - documents dir URL changes with each launch; use docsDir, "dsName" and "urlTileFormat" instead to construct a URL on the fly
        ]
        
        do {
            try (manifest as NSDictionary).write(to: baseDestURL.appendingPathComponent("manifest.plist"))
        } catch {
            return false
        }

        return true
    }

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.