Skip to content

Commit 2cb2cfe

Browse files
committed
Refactor to cleanup and prepare for directory caching
* there is no point in returning type and compression with every `get_tile` -- PMTiles keeps this data in the header, so it is constant, and if the user needs it, they can get it directly. Returns `bytes` now. * add `Entry::is_leaf` helper * add `Header::get_bounds` and `Header::get_center` (tilejson structs) * no need for `AsyncPmTilesReader<B: ...>` type - it has to be specified in the impl anyway * no need for the `backend::read_initial_bytes` - it is only used once, has default implementation anyway. Inlined. * inlined `read_directory_with_backend` - used once and tiny * split up the `find_tile_entry` into two functions - I will need this later to add caching -- the root entry is permanently cached as part of the main struct, but the other ones are not, so needs a different code path. * added `cargo test` for default features
1 parent c7dc1c4 commit 2cb2cfe

File tree

8 files changed

+86
-119
lines changed

8 files changed

+86
-119
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ jobs:
2929
- run: cargo test --features http-async
3030
- run: cargo test --features mmap-async-tokio
3131
- run: cargo test --features tilejson
32+
- run: cargo test

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pmtiles"
3-
version = "0.3.1"
3+
version = "0.4.0"
44
edition = "2021"
55
authors = ["Luke Seelenbinder <[email protected]>"]
66
license = "MIT OR Apache-2.0"
@@ -36,10 +36,10 @@ tokio = { version = "1", default-features = false, features = ["io-util"], optio
3636
varint-rs = "2"
3737

3838
[dev-dependencies]
39+
flate2 = "1"
3940
fmmap = { version = "0.3", features = ["tokio-async"] }
4041
reqwest = { version = "0.11", features = ["rustls-tls-webpki-roots"] }
4142
tokio = { version = "1", features = ["test-util", "macros", "rt"] }
42-
flate2 = "1"
4343

4444
[package.metadata.docs.rs]
4545
all-features = true

justfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ test:
1414
cargo test --features http-async
1515
cargo test --features mmap-async-tokio
1616
cargo test --features tilejson
17+
cargo test
1718
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps
1819

1920
# Run cargo fmt and cargo clippy
@@ -25,7 +26,7 @@ fmt:
2526

2627
# Run cargo clippy
2728
clippy:
28-
cargo clippy --workspace --all-targets --bins --tests --lib --benches -- -D warnings
29+
cargo clippy --workspace --all-targets --all-features --bins --tests --lib --benches -- -D warnings
2930

3031
# Build and open code documentation
3132
docs:

src/async_reader.rs

Lines changed: 53 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ use crate::header::{HEADER_SIZE, MAX_INITIAL_BYTES};
1616
use crate::http::HttpBackend;
1717
#[cfg(feature = "mmap-async-tokio")]
1818
use crate::mmap::MmapBackend;
19-
use crate::tile::{tile_id, Tile};
19+
use crate::tile::tile_id;
2020
use crate::{Compression, Header};
2121

22-
pub struct AsyncPmTilesReader<B: AsyncBackend> {
23-
pub header: Header,
22+
pub struct AsyncPmTilesReader<B> {
2423
backend: B,
24+
header: Header,
2525
root_directory: Directory,
2626
}
2727

@@ -30,11 +30,13 @@ impl<B: AsyncBackend + Sync + Send> AsyncPmTilesReader<B> {
3030
///
3131
/// Note: Prefer using new_with_* methods.
3232
pub async fn try_from_source(backend: B) -> Result<Self, Error> {
33-
let mut initial_bytes = backend.read_initial_bytes().await?;
34-
35-
let header_bytes = initial_bytes.split_to(HEADER_SIZE);
33+
// Read the first 127 and up to 16,384 bytes to ensure we can initialize the header and root directory.
34+
let mut initial_bytes = backend.read(0, MAX_INITIAL_BYTES).await?;
35+
if initial_bytes.len() < HEADER_SIZE {
36+
return Err(Error::InvalidHeader);
37+
}
3638

37-
let header = Header::try_from_bytes(header_bytes)?;
39+
let header = Header::try_from_bytes(initial_bytes.split_to(HEADER_SIZE))?;
3840

3941
let directory_bytes = initial_bytes
4042
.split_off((header.root_offset as usize) - HEADER_SIZE)
@@ -44,45 +46,32 @@ impl<B: AsyncBackend + Sync + Send> AsyncPmTilesReader<B> {
4446
Self::read_compressed_directory(header.internal_compression, directory_bytes).await?;
4547

4648
Ok(Self {
47-
header,
4849
backend,
50+
header,
4951
root_directory,
5052
})
5153
}
5254

53-
/// Fetches a [Tile] from the archive.
54-
pub async fn get_tile(&self, z: u8, x: u64, y: u64) -> Option<Tile> {
55+
/// Fetches tile bytes from the archive.
56+
pub async fn get_tile(&self, z: u8, x: u64, y: u64) -> Option<Bytes> {
5557
let tile_id = tile_id(z, x, y);
56-
let entry = self.find_tile_entry(tile_id, None, 0).await?;
57-
58-
let data = self
59-
.backend
60-
.read_exact(
61-
(self.header.data_offset + entry.offset) as _,
62-
entry.length as _,
63-
)
64-
.await
65-
.ok()?;
58+
let entry = self.find_tile_entry(tile_id).await?;
6659

67-
Some(Tile {
68-
data,
69-
tile_type: self.header.tile_type,
70-
tile_compression: self.header.tile_compression,
71-
})
60+
let offset = (self.header.data_offset + entry.offset) as _;
61+
let length = entry.length as _;
62+
let data = self.backend.read_exact(offset, length).await.ok()?;
63+
64+
Some(data)
7265
}
7366

7467
/// Gets metadata from the archive.
7568
///
7669
/// Note: by spec, this should be valid JSON. This method currently returns a [String].
7770
/// This may change in the future.
7871
pub async fn get_metadata(&self) -> Result<String, Error> {
79-
let metadata = self
80-
.backend
81-
.read_exact(
82-
self.header.metadata_offset as _,
83-
self.header.metadata_length as _,
84-
)
85-
.await?;
72+
let offset = self.header.metadata_offset as _;
73+
let length = self.header.metadata_length as _;
74+
let metadata = self.backend.read_exact(offset, length).await?;
8675

8776
let decompressed_metadata =
8877
Self::decompress(self.header.internal_compression, metadata).await?;
@@ -132,71 +121,52 @@ impl<B: AsyncBackend + Sync + Send> AsyncPmTilesReader<B> {
132121
Ok(tj)
133122
}
134123

135-
#[async_recursion]
136-
async fn find_tile_entry(
137-
&self,
138-
tile_id: u64,
139-
next_dir: Option<Directory>,
140-
depth: u8,
141-
) -> Option<Entry> {
142-
// Max recursion...
143-
if depth >= 4 {
144-
return None;
124+
/// Recursively locates a tile in the archive.
125+
async fn find_tile_entry(&self, tile_id: u64) -> Option<Entry> {
126+
let entry = self.root_directory.find_tile_id(tile_id);
127+
if let Some(entry) = entry {
128+
if entry.is_leaf() {
129+
return self.find_entry_rec(tile_id, entry, 0).await;
130+
}
145131
}
132+
entry.cloned()
133+
}
146134

147-
let next_dir = next_dir.as_ref().unwrap_or(&self.root_directory);
148-
149-
match next_dir.find_tile_id(tile_id) {
150-
None => None,
151-
Some(needle) => {
152-
if needle.run_length == 0 {
153-
// Leaf directory
154-
let next_dir = self
155-
.read_directory(
156-
(self.header.leaf_offset + needle.offset) as _,
157-
needle.length as _,
158-
)
159-
.await
160-
.ok()?;
161-
self.find_tile_entry(tile_id, Some(next_dir), depth + 1)
162-
.await
135+
#[async_recursion]
136+
async fn find_entry_rec(&self, tile_id: u64, entry: &Entry, depth: u8) -> Option<Entry> {
137+
// the recursion is done as two functions because it is a bit cleaner,
138+
// and it allows directory to be cached later without cloning it first.
139+
let offset = (self.header.leaf_offset + entry.offset) as _;
140+
let length = entry.length as _;
141+
let dir = self.read_directory(offset, length).await.ok()?;
142+
let entry = dir.find_tile_id(tile_id);
143+
144+
if let Some(entry) = entry {
145+
if entry.is_leaf() {
146+
return if depth <= 4 {
147+
self.find_entry_rec(tile_id, entry, depth + 1).await
163148
} else {
164-
Some(needle.clone())
165-
}
149+
None
150+
};
166151
}
167152
}
153+
154+
entry.cloned()
168155
}
169156

170157
async fn read_directory(&self, offset: usize, length: usize) -> Result<Directory, Error> {
171-
Self::read_directory_with_backend(
172-
&self.backend,
173-
self.header.internal_compression,
174-
offset,
175-
length,
176-
)
177-
.await
158+
let data = self.backend.read_exact(offset, length).await?;
159+
Self::read_compressed_directory(self.header.internal_compression, data).await
178160
}
179161

180162
async fn read_compressed_directory(
181163
compression: Compression,
182164
bytes: Bytes,
183165
) -> Result<Directory, Error> {
184166
let decompressed_bytes = Self::decompress(compression, bytes).await?;
185-
186167
Directory::try_from(decompressed_bytes)
187168
}
188169

189-
async fn read_directory_with_backend(
190-
backend: &B,
191-
compression: Compression,
192-
offset: usize,
193-
length: usize,
194-
) -> Result<Directory, Error> {
195-
let directory_bytes = backend.read_exact(offset, length).await?;
196-
197-
Self::read_compressed_directory(compression, directory_bytes).await
198-
}
199-
200170
async fn decompress(compression: Compression, bytes: Bytes) -> Result<Bytes, Error> {
201171
let mut decompressed_bytes = Vec::with_capacity(bytes.len() * 2);
202172
match compression {
@@ -229,8 +199,8 @@ impl AsyncPmTilesReader<MmapBackend> {
229199
/// Creates a new PMTiles reader from a file path using the async mmap backend.
230200
///
231201
/// Fails if [p] does not exist or is an invalid archive.
232-
pub async fn new_with_path<P: AsRef<Path>>(p: P) -> Result<Self, Error> {
233-
let backend = MmapBackend::try_from(p).await?;
202+
pub async fn new_with_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
203+
let backend = MmapBackend::try_from(path).await?;
234204

235205
Self::try_from_source(backend).await
236206
}
@@ -243,16 +213,6 @@ pub trait AsyncBackend {
243213

244214
/// Reads up to `length` bytes starting at `offset`.
245215
async fn read(&self, offset: usize, length: usize) -> Result<Bytes, Error>;
246-
247-
/// Read the first 127 and up to 16,384 bytes to ensure we can initialize the header and root directory.
248-
async fn read_initial_bytes(&self) -> Result<Bytes, Error> {
249-
let bytes = self.read(0, MAX_INITIAL_BYTES).await?;
250-
if bytes.len() < HEADER_SIZE {
251-
return Err(Error::InvalidHeader);
252-
}
253-
254-
Ok(bytes)
255-
}
256216
}
257217

258218
#[cfg(test)]
@@ -274,11 +234,11 @@ mod tests {
274234
let tile = tiles.get_tile(z, x, y).await.unwrap();
275235

276236
assert_eq!(
277-
tile.data.len(),
237+
tile.len(),
278238
fixture_bytes.len(),
279239
"Expected tile length to match."
280240
);
281-
assert_eq!(tile.data, fixture_bytes, "Expected tile to match fixture.");
241+
assert_eq!(tile, fixture_bytes, "Expected tile to match fixture.");
282242
}
283243

284244
#[tokio::test]

src/directory.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl Directory {
2525
// https://github.com/protomaps/PMTiles/blob/9c7f298fb42290354b8ed0a9b2f50e5c0d270c40/js/index.ts#L210
2626
if next_id > 0 {
2727
let previous_tile = self.entries.get(next_id - 1)?;
28-
if previous_tile.run_length == 0
28+
if previous_tile.is_leaf()
2929
|| tile_id - previous_tile.tile_id < previous_tile.run_length as u64
3030
{
3131
return Some(previous_tile);
@@ -88,6 +88,13 @@ pub(crate) struct Entry {
8888
pub(crate) run_length: u32,
8989
}
9090

91+
#[cfg(any(feature = "http-async", feature = "mmap-async-tokio"))]
92+
impl Entry {
93+
pub fn is_leaf(&self) -> bool {
94+
self.run_length == 0
95+
}
96+
}
97+
9198
#[cfg(test)]
9299
mod tests {
93100
use std::io::{BufReader, Read, Write};

src/header.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,27 @@ impl Header {
8080
tiles: sources,
8181
minzoom: self.min_zoom,
8282
maxzoom: self.max_zoom,
83-
bounds: tilejson::Bounds::new(
84-
self.min_longitude as f64,
85-
self.min_latitude as f64,
86-
self.max_longitude as f64,
87-
self.max_latitude as f64,
88-
),
89-
center: tilejson::Center::new(
90-
self.center_longitude as f64,
91-
self.center_latitude as f64,
92-
self.center_zoom,
93-
),
83+
bounds: self.get_bounds(),
84+
center: self.get_center(),
9485
}
9586
}
87+
88+
pub fn get_bounds(&self) -> tilejson::Bounds {
89+
tilejson::Bounds::new(
90+
self.min_longitude as f64,
91+
self.min_latitude as f64,
92+
self.max_longitude as f64,
93+
self.max_latitude as f64,
94+
)
95+
}
96+
97+
pub fn get_center(&self) -> tilejson::Center {
98+
tilejson::Center::new(
99+
self.center_longitude as f64,
100+
self.center_latitude as f64,
101+
self.center_zoom,
102+
)
103+
}
96104
}
97105

98106
#[derive(Debug, Eq, PartialEq, Copy, Clone)]

src/http.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,6 @@ mod tests {
6767
let client = reqwest::Client::builder().use_rustls_tls().build().unwrap();
6868
let backend = HttpBackend::try_from(client, TEST_URL).unwrap();
6969

70-
let _tiles = AsyncPmTilesReader::try_from_source(backend).await.unwrap();
70+
AsyncPmTilesReader::try_from_source(backend).await.unwrap();
7171
}
7272
}

src/tile.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
use bytes::Bytes;
2-
3-
use crate::{Compression, TileType};
4-
51
#[cfg(any(feature = "http-async", feature = "mmap-async-tokio", test))]
62
pub(crate) fn tile_id(z: u8, x: u64, y: u64) -> u64 {
73
if z == 0 {
@@ -15,12 +11,6 @@ pub(crate) fn tile_id(z: u8, x: u64, y: u64) -> u64 {
1511
base_id + tile_id
1612
}
1713

18-
pub struct Tile {
19-
pub data: Bytes,
20-
pub tile_type: TileType,
21-
pub tile_compression: Compression,
22-
}
23-
2414
#[cfg(test)]
2515
mod test {
2616
use super::tile_id;

0 commit comments

Comments
 (0)