|  | import Foundation | 
        
          |  | import Mapbox | 
        
          |  | import SQLite | 
        
          |  |  | 
        
          |  | // MARK: MbtilesSource | 
        
          |  |  | 
        
          |  | enum MBTilesSourceError: Error { | 
        
          |  | case CouldNotReadFileError | 
        
          |  | case UnknownFormatError | 
        
          |  | case UnsupportedFormatError | 
        
          |  | } | 
        
          |  |  | 
        
          |  | class MBTilesSource: NSObject { | 
        
          |  |  | 
        
          |  | // MBTiles spec, including details about vector tiles: | 
        
          |  | // https://github.com/mapbox/mbtiles-spec/ | 
        
          |  |  | 
        
          |  | // MARK: Properties | 
        
          |  |  | 
        
          |  | var filePath: String | 
        
          |  | fileprivate var isVector = false | 
        
          |  | fileprivate var tileSize: Int? | 
        
          |  | fileprivate var layersJson: String? | 
        
          |  | fileprivate var attribution: String? | 
        
          |  | var minZoom: Float? | 
        
          |  | var maxZoom: Float? | 
        
          |  | var bounds: MGLCoordinateBounds? | 
        
          |  |  | 
        
          |  | fileprivate var mglTileSource: MGLTileSource? | 
        
          |  | fileprivate var mglStyleLayer: MGLForegroundStyleLayer? | 
        
          |  | var isVisible: Bool { | 
        
          |  | get { | 
        
          |  | return mglStyleLayer == nil ? false : mglStyleLayer!.isVisible | 
        
          |  | } | 
        
          |  | set { | 
        
          |  | mglStyleLayer?.isVisible = newValue | 
        
          |  | } | 
        
          |  | } | 
        
          |  |  | 
        
          |  | private var database: Connection? | 
        
          |  | private let validRasterFormats = ["jpg", "png"] | 
        
          |  | private let validVectorFormats = ["pbf", "mvt"] | 
        
          |  |  | 
        
          |  | // MARK: Functions | 
        
          |  |  | 
        
          |  | init(filePath: String) throws { | 
        
          |  | self.filePath = filePath | 
        
          |  | super.init() | 
        
          |  |  | 
        
          |  | do { | 
        
          |  | self.database = try Connection(filePath, readonly: true) | 
        
          |  |  | 
        
          |  | guard let anyTile = try database?.scalar("SELECT tile_data FROM tiles LIMIT 1") as? Blob else { | 
        
          |  | throw MBTilesSourceError.CouldNotReadFileError | 
        
          |  | } | 
        
          |  | let tileData = Data(bytes: anyTile.bytes) | 
        
          |  |  | 
        
          |  | // https://stackoverflow.com/a/42104538 | 
        
          |  | var format: String? | 
        
          |  | let headerData = [UInt8](tileData)[0] | 
        
          |  | if headerData == 0x89 { | 
        
          |  | format = "png" | 
        
          |  | } else if headerData == 0xFF { | 
        
          |  | format = "jpg" | 
        
          |  | } else { | 
        
          |  | format = getMetadata(fieldName: "format") | 
        
          |  | } | 
        
          |  |  | 
        
          |  | if format == nil { | 
        
          |  | throw MBTilesSourceError.UnknownFormatError | 
        
          |  | } else if validRasterFormats.contains(format!) { | 
        
          |  | isVector = false | 
        
          |  | } else if validVectorFormats.contains(format!) { | 
        
          |  | isVector = true | 
        
          |  | } else { | 
        
          |  | throw MBTilesSourceError.UnsupportedFormatError | 
        
          |  | } | 
        
          |  |  | 
        
          |  | if let tileImage = UIImage(data: tileData) { | 
        
          |  | let screenScale = UIScreen.main.scale > 1 ? 2 : 1 | 
        
          |  | self.tileSize = Int(tileImage.size.height / CGFloat(screenScale)) | 
        
          |  | } | 
        
          |  |  | 
        
          |  | if let minString = getMetadata(fieldName: "minzoom") { | 
        
          |  | minZoom = Float(minString) | 
        
          |  | } | 
        
          |  | if let maxString = getMetadata(fieldName: "maxzoom") { | 
        
          |  | maxZoom = Float(maxString) | 
        
          |  | } | 
        
          |  | if let boundsString = getMetadata(fieldName: "bounds") { | 
        
          |  | let coordinates = boundsString.split(separator: ",") | 
        
          |  | if let west  = CLLocationDegrees(coordinates[0]), | 
        
          |  | let south = CLLocationDegrees(coordinates[1]), | 
        
          |  | let east = CLLocationDegrees(coordinates[2]), | 
        
          |  | let north  = CLLocationDegrees(coordinates[3]) { | 
        
          |  | bounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake(south, west), | 
        
          |  | CLLocationCoordinate2DMake(north, east)) | 
        
          |  | } | 
        
          |  | } | 
        
          |  | attribution = getMetadata(fieldName: "attribution") | 
        
          |  | layersJson = getMetadata(fieldName: "json") | 
        
          |  | } catch { | 
        
          |  | throw MBTilesSourceError.CouldNotReadFileError | 
        
          |  | } | 
        
          |  | } | 
        
          |  |  | 
        
          |  | deinit { | 
        
          |  | database = nil | 
        
          |  | } | 
        
          |  |  | 
        
          |  | private func getMetadata(fieldName: String) -> String? { | 
        
          |  | let query = "SELECT value FROM metadata WHERE name=\"\(fieldName)\"" | 
        
          |  | if let binding = try? database?.scalar(query) { | 
        
          |  | return binding as? String | 
        
          |  | } | 
        
          |  | return nil | 
        
          |  | } | 
        
          |  |  | 
        
          |  | fileprivate func getTile(x: Int, y: Int, z: Int) -> Data? { | 
        
          |  | let query = "SELECT tile_data FROM tiles WHERE tile_column=\(x) AND tile_row=\(y) AND zoom_level=\(z)" | 
        
          |  | if let binding = try? database?.scalar(query), | 
        
          |  | let blob = binding as? Blob { | 
        
          |  | return Data(bytes: blob.bytes) | 
        
          |  | } | 
        
          |  | return nil | 
        
          |  | } | 
        
          |  | } | 
        
          |  |  | 
        
          |  | // MARK: - MGLMapView Extension | 
        
          |  |  | 
        
          |  | extension MGLMapView { | 
        
          |  | func insertMBTilesSource(_ source: MBTilesSource, at index: UInt, tileSize: Int? = nil) throws { | 
        
          |  |  | 
        
          |  | remove(MBTilesSource: source) | 
        
          |  |  | 
        
          |  | let port: UInt = 54321 | 
        
          |  |  | 
        
          |  | MbtilesServer.shared.start(port: port) | 
        
          |  | MbtilesServer.shared.sources[source.filePath] = source | 
        
          |  |  | 
        
          |  | let filePath = source.filePath as NSString | 
        
          |  |  | 
        
          |  | guard let escapedPath = filePath.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlPathAllowed) else { | 
        
          |  | return | 
        
          |  | } | 
        
          |  |  | 
        
          |  | let id = filePath.lastPathComponent | 
        
          |  | let urlTemplate = "http://localhost:\(port)\(escapedPath)?x={x}&y={y}&z={z}" | 
        
          |  | var options = [MGLTileSourceOption : Any]() | 
        
          |  | options[MGLTileSourceOption.tileCoordinateSystem] = MGLTileCoordinateSystem.TMS.rawValue | 
        
          |  | options[MGLTileSourceOption.tileSize] = tileSize ?? source.tileSize | 
        
          |  | options[MGLTileSourceOption.minimumZoomLevel] = source.minZoom | 
        
          |  | options[MGLTileSourceOption.maximumZoomLevel] = source.maxZoom | 
        
          |  | if let bounds = source.bounds { | 
        
          |  | options[MGLTileSourceOption.coordinateBounds] = NSValue(mglCoordinateBounds: bounds) | 
        
          |  | } | 
        
          |  | if let attributionString = source.attribution { | 
        
          |  | options[MGLTileSourceOption.attributionInfos] = [MGLAttributionInfo(title: NSAttributedString(string: attributionString) , url: nil)] | 
        
          |  | } | 
        
          |  |  | 
        
          |  | if source.isVector { | 
        
          |  | // Only raster data (jpg, png) is currently supported. | 
        
          |  | // Raster data (i.e. pbf) is not (yet) supported. | 
        
          |  | throw MBTilesSourceError.UnsupportedFormatError | 
        
          |  | } else { | 
        
          |  | source.mglTileSource = MGLRasterTileSource(identifier: id, tileURLTemplates: [urlTemplate], options: options) | 
        
          |  | source.mglStyleLayer = MGLRasterStyleLayer(identifier: id, source: source.mglTileSource!) | 
        
          |  | } | 
        
          |  |  | 
        
          |  | if source.minZoom != nil { | 
        
          |  | source.mglStyleLayer!.minimumZoomLevel = source.minZoom! | 
        
          |  | } | 
        
          |  |  | 
        
          |  | if source.maxZoom != nil { | 
        
          |  | source.mglStyleLayer!.maximumZoomLevel = source.maxZoom! | 
        
          |  | } | 
        
          |  |  | 
        
          |  | DispatchQueue.main.async { [weak self] in | 
        
          |  | self?.style?.addSource(source.mglTileSource!) | 
        
          |  | self?.style?.insertLayer(source.mglStyleLayer!, at: index) | 
        
          |  | } | 
        
          |  | } | 
        
          |  |  | 
        
          |  | func remove(MBTilesSource source: MBTilesSource) { | 
        
          |  | let filePath = source.filePath | 
        
          |  | let id = (filePath as NSString).lastPathComponent | 
        
          |  | DispatchQueue.main.async { [weak self] in | 
        
          |  | if let layer = self?.style?.layer(withIdentifier: id) { | 
        
          |  | self?.style?.removeLayer(layer) | 
        
          |  | } | 
        
          |  | if let source = self?.style?.source(withIdentifier: id) { | 
        
          |  | self?.style?.removeSource(source) | 
        
          |  | } | 
        
          |  | } | 
        
          |  | MbtilesServer.shared.sources[filePath] = nil | 
        
          |  | } | 
        
          |  | } | 
        
          |  |  | 
        
          |  | // MARK: - Server | 
        
          |  |  | 
        
          |  | fileprivate class MbtilesServer: NSObject { | 
        
          |  |  | 
        
          |  | // MARK: Properties | 
        
          |  |  | 
        
          |  | static let shared = MbtilesServer() | 
        
          |  | var sources = [String : MBTilesSource]() | 
        
          |  |  | 
        
          |  | private var webServer: GCDWebServer? = nil | 
        
          |  |  | 
        
          |  | // MARK: Functions | 
        
          |  |  | 
        
          |  | func start(port: UInt) { | 
        
          |  | guard Thread.isMainThread else { | 
        
          |  | DispatchQueue.main.sync { [weak self] in | 
        
          |  | self?.start(port: port) | 
        
          |  | } | 
        
          |  | return | 
        
          |  | } | 
        
          |  |  | 
        
          |  | if webServer == nil { | 
        
          |  | webServer = GCDWebServer() | 
        
          |  | } | 
        
          |  | guard !webServer!.isRunning else { | 
        
          |  | return | 
        
          |  | } | 
        
          |  | GCDWebServer.setLogLevel(3) | 
        
          |  | webServer?.addDefaultHandler(forMethod: "GET", request: GCDWebServerRequest.self, processBlock: { [weak self] (request: GCDWebServerRequest) -> GCDWebServerResponse? in | 
        
          |  | return self?.handleGetRequest(request) | 
        
          |  | }) | 
        
          |  | webServer?.start(withPort: port, bonjourName: nil) | 
        
          |  | } | 
        
          |  |  | 
        
          |  | func stop() { | 
        
          |  | guard Thread.isMainThread else { | 
        
          |  | DispatchQueue.main.sync { [weak self] in | 
        
          |  | self?.stop() | 
        
          |  | } | 
        
          |  | return | 
        
          |  | } | 
        
          |  |  | 
        
          |  | webServer?.stop() | 
        
          |  | webServer?.removeAllHandlers() | 
        
          |  | } | 
        
          |  |  | 
        
          |  | private func handleGetRequest(_ request: GCDWebServerRequest) -> GCDWebServerResponse? { | 
        
          |  | if (request.path as NSString).pathExtension == "mbtiles", | 
        
          |  | let source = sources[request.path], | 
        
          |  | let query = request.query, | 
        
          |  | let xString = query["x"] as? String, | 
        
          |  | let yString = query["y"] as? String, | 
        
          |  | let zString = query["z"] as? String, | 
        
          |  | let x = Int(xString), | 
        
          |  | let y = Int(yString), | 
        
          |  | let z = Int(zString), | 
        
          |  | let tileData = source.getTile(x: x, y: y, z: z) { | 
        
          |  |  | 
        
          |  | return GCDWebServerDataResponse(data: tileData, contentType: "") | 
        
          |  | } else { | 
        
          |  | return GCDWebServerResponse(statusCode: 404) | 
        
          |  | } | 
        
          |  | } | 
        
          |  | } | 
  
I am trying this solution to show mbtiles on my iPad app.
GCDWebServer started the local server correctly but iPad app doesn't trigger this url
"http://localhost:\(port)\(escapedPath)?x={x}&y={y}&z={z}"
Which code line must trigger this url?
Actually self?.style?.insertLayer(source.mglStyleLayer!, at: index) make the app crashes so I changed this line as follows
self?.style?.insertLayer(source.mglStyleLayer!, at: 0)
Then app doesn't crashed but mbtiles are not showing up. Is this why the localhost url doesn't triggered?
I am trying to show mbtiles for several days but still no luck.
I would be appreciate any help. Thanks in advance,