Skip to content

Commit

Permalink
Update to use sfomuseum/swift-pmtiles package (#5)
Browse files Browse the repository at this point in the history
* update to use PMTilesReader

* update swift-pmtiles

* update to reflect changes to swift-pmtiles

* docs

---------

Co-authored-by: sfomuseumbot <sfomuseumbot@localhost>
  • Loading branch information
thisisaaronland and sfomuseumbot authored Jan 4, 2025
1 parent 8a14e22 commit 0da66c4
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 109 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
"version" : "1.6.2"
}
},
{
"identity" : "swift-pmtiles",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sfomuseum/swift-pmtiles.git",
"state" : {
"branch" : "main",
"revision" : "ff61be142df4e469f4c7b1403f959d058c91de1c"
}
},
{
"identity" : "swifter",
"kind" : "remoteSourceControl",
Expand Down
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import PackageDescription

let package = Package(
name: "swifter-protomaps",
platforms: [
.iOS(.v14),
.macOS(.v11)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(name: "SwifterProtomaps", targets: ["SwifterProtomaps"]),
Expand All @@ -13,6 +17,7 @@ let package = Package(
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/sfomuseum/swifter.git", branch:"main"),
.package(url: "https://github.com/sfomuseum/swift-pmtiles.git", branch:"main"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
],
Expand All @@ -23,6 +28,7 @@ let package = Package(
name: "SwifterProtomaps",
dependencies: [
.product(name: "Swifter", package: "swifter"),
.product(name: "PMTiles", package: "swift-pmtiles"),
.product(name: "Logging", package: "swift-log")
]),
.testTarget(
Expand All @@ -34,6 +40,7 @@ let package = Package(
"SwifterProtomaps",
.product(name: "Swifter", package: "swifter"),
.product(name: "Logging", package: "swift-log"),
.product(name: "PMTiles", package: "swift-pmtiles"),
.product(name: "ArgumentParser", package: "swift-argument-parser")
],
path: "Scripts"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This package provides a simple `ServeProtomapsTiles` helper method to serve one

It was designed for use with an iOS application built around `WKWebKitView` views whose HTML/JavaScript code need to load and render local (on device) Protomaps tiles.

It is not designed to be a general purpose function for serving files using HTTP `Range` requests.
It is not designed to be a general purpose function for serving files using HTTP `Range` requests. It uses the [swift-pmtiles](https://github.com/sfomuseum/swift-pmtiles) package under the hood.

For a longer version detailing why we did this please see the [Serving map tiles to yourself using Protomaps and iOS](https://millsfield.sfomuseum.org/blog/2022/03/30/swifter-protomaps/) blog post.

Expand Down Expand Up @@ -167,6 +167,7 @@ This package requires:

## See also

* https://github.com/sfomuseum/swift-pmtiles
* https://github.com/sfomuseum/swifter-protomaps-example
* https://github.com/sfomuseum/swifter
* https://github.com/protomaps/
146 changes: 38 additions & 108 deletions Sources/SwifterProtomaps/SwifterProtomaps.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Swifter
import Foundation
import Logging
import System
import PMTiles

/// ServeProtomapsOptions defines runtime options for serving Protomaps tiles
public struct ServeProtomapsOptions {
Expand All @@ -28,8 +28,6 @@ public struct ServeProtomapsOptions {
}

/// ServeProtomapsTiles will serve HTTP range requests for zero or more Protomaps tile databases in a directory.
@available(iOS 14.0, *)
@available(macOS 11.0, *)
public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest) -> HttpResponse) {

return { r in
Expand All @@ -45,42 +43,7 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
if opts.StripPrefix != "" {
rel_path = rel_path.replacingOccurrences(of: opts.StripPrefix, with: "")
}

let uri = opts.Root.appendingPathComponent(rel_path)
let path = uri.absoluteString

// https://developer.apple.com/documentation/foundation/filehandle

var fh: FileHandle?
var fd: FileDescriptor?

do {

if opts.UseFileDescriptor {
let fp = FilePath(uri.absoluteString.replacingOccurrences(of: "file://", with: ""))
fd = try FileDescriptor.open(fp, .readOnly)
} else {
fh = try FileHandle(forReadingFrom: uri)
}

} catch {
opts.Logger?.error("Failed to open path (\(path)) for reading \(error)")
return .raw(404, "Not found", rsp_headers, {_ in })
}

defer {
do {
if opts.UseFileDescriptor {
try fd?.close()
} else {
try fh?.close()
}

} catch (let error) {
opts.Logger?.warning("Failed to close \(path), \(error)")
}
}

guard var range_h = r.headers["range"] else {
rsp_headers["Access-Control-Allow-Origin"] = opts.AllowOrigins
rsp_headers["Access-Control-Allow-Headers"] = opts.AllowHeaders
Expand All @@ -107,7 +70,7 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
return .raw(400, "Bad Request", rsp_headers, {_ in })
}

guard let stop = Int(positions[1]) else {
guard let stop = UInt64(positions[1]) else {
rsp_headers["X-Error"] = "Invalid stopping range"
return .raw(400, "Bad Request", rsp_headers, {_ in })
}
Expand All @@ -117,74 +80,59 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
return .raw(400, "Bad Request", rsp_headers, {_ in })
}

opts.Logger?.debug("Read data from \(path) start: \(start) stop: \(stop)")

let next = stop + 1

let body: Data!
// Fetch data

let db_url = opts.Root.appendingPathComponent(rel_path)

var pmtiles_reader: PMTilesReader

do {

opts.Logger?.debug("Seek \(path) to \(start)")
var reader_opts = PMTilesReaderOptions(db_url, use_file_descriptor: opts.UseFileDescriptor)
reader_opts.Logger = opts.Logger

if opts.UseFileDescriptor {
try fd?.seek(offset: Int64(start), from: FileDescriptor.SeekOrigin.start)
} else {
fh?.seek(toFileOffset: start)
}
pmtiles_reader = try PMTilesReader(reader_opts)

} catch {
opts.Logger?.error("Failed to seek to \(start) for \(path), \(error)")
rsp_headers["X-Error"] = "Failed to read from Protomaps tile"
opts.Logger?.error("Failed to instantiate PMTiles reader \(error)")
rsp_headers["X-Error"] = "Failed to instantiate PMTiles reader"
return .raw(500, "Internal Server Error", rsp_headers, {_ in })
}

opts.Logger?.debug("Read data from \(path) to \(next)")

if opts.UseFileDescriptor {

let read_len = Int(UInt64(next) - start)
opts.Logger?.debug("Read \(read_len) bytes from \(path)")

guard let data = readData(from: fd!.rawValue, length: Int(read_len)) else {
opts.Logger?.error("Failed to read to \(next) for \(path)")
rsp_headers["X-Error"] = "Failed to read from Protomaps tile"
return .raw(500, "Internal Server Error", rsp_headers, {_ in })
defer {
if case .failure(let error) = pmtiles_reader.Close() {
opts.Logger?.error("Failed to close PMTiles reader \(error)")
}

}

opts.Logger?.debug("Read data from \(db_url.absoluteString) start: \(start) stop: \(stop)")

let body: Data!

let read_rsp = pmtiles_reader.Read(from: start, to: stop)

switch read_rsp {
case .success(let data):
body = data

} else {

do {
body = try fh?.read(upToCount: next)
} catch (let error){
opts.Logger?.error("Failed to read to \(next) for \(path), \(error)")
rsp_headers["X-Error"] = "Failed to read from Protomaps tile"
return .raw(500, "Internal Server Error", rsp_headers, {_ in })
}

case .failure(let error):
opts.Logger?.error("Failed to read data from PMTiles reader, \(error)")
rsp_headers["X-Error"] = "Failed to read from Protomaps tile"
return .raw(500, "Internal Server Error", rsp_headers, {_ in })
}

// https://httpwg.org/specs/rfc7233.html#header.accept-ranges

var filesize = "*"

do {
if opts.UseFileDescriptor {

let size = try fd!.seek(offset: 0, from: FileDescriptor.SeekOrigin.end)
filesize = String(size)

opts.Logger?.debug("filesize \(filesize)")

} else {

let size = try fh!.seekToEnd()
filesize = String(size)
}
} catch (let error){
opts.Logger?.warning("Failed to determine filesize for \(path), \(error)")
let size_rsp = pmtiles_reader.Size()

switch size_rsp {
case .success(let sz):
filesize = String(sz)
case .failure(let error):
opts.Logger?.warning("Failed to determine size from PMTiles reader \(error)")
}

let length = UInt64(next) - start
Expand All @@ -198,6 +146,8 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
rsp_headers["Content-Range"] = content_range
rsp_headers["Accept-Ranges"] = "bytes"

opts.Logger?.debug("Return 206 \(content_range)")

return .raw(206, "Partial Content", rsp_headers, { writer in

do {
Expand All @@ -208,23 +158,3 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
})
}
}

internal func readData(from fileDescriptor: Int32, length: Int) -> Data? {
// Create a Data buffer of the desired length
var data = Data(count: length)

// Read the data into the Data buffer
let bytesRead = data.withUnsafeMutableBytes { buffer -> Int in
guard let baseAddress = buffer.baseAddress else { return -1 }
return read(fileDescriptor, baseAddress, length)
}

// Handle errors or end-of-file
guard bytesRead > 0 else {
return nil // Return nil if no bytes were read
}

// Resize the Data object to the actual number of bytes read
data.removeSubrange(bytesRead..<data.count)
return data
}

0 comments on commit 0da66c4

Please sign in to comment.