Skip to content

Commit

Permalink
docs: add VersaTiles stack specification v2.0
Browse files Browse the repository at this point in the history
docs: improve spec

docs: reorganize container spec

docs: improve container spec

ci: check markdown links

docs: fix links and format

docs: improve container spec format

rewrite?
  • Loading branch information
MichaelKreil committed Mar 31, 2024
1 parent 3b50661 commit 7581c21
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 99 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Check Markdown links

on:
push:
workflow_dispatch:
schedule:
- cron: "0 3 1 * *"

jobs:
markdown-link-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@main
- uses: michaelkreil/check-markdown-links@main
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Versions

- [**v2.0**](v02/readme.md) <- current version
- [v1.0](v01)
- [v1.0](v01/container/readme.md)

## Implementations

Expand Down
6 changes: 3 additions & 3 deletions v01/container/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The file is composed of several parts:
- **index** of these tiles
4. **index** of all blocks

<p align="center"><img src="file_format.svg?raw=true"></p>
<p align="center"><img src="file_format.svg"></p>

## file

Expand Down Expand Up @@ -79,7 +79,7 @@ The file is composed of several parts:
- Each `block` contains data of up to 256x256 (= 65536) `tile`s.
- Levels 0-8 can be stored with one `block` each. level 9 might contain 512x512 `tile`s so 4 `block`s are necessary.

<p align="center"><img src="level_blocks.svg?raw=true"></p>
<p align="center"><img src="level_blocks.svg"></p>

- Each `block` contains the concatenated `tile` blobs and ends with a `tile_index`.
- Neither the order of `block`s in the `file` nor the order of `tile`s in a `block` matters as long as their indexes are correct.
Expand All @@ -96,7 +96,7 @@ The file is composed of several parts:
- `tile`s are read horizontally then vertically
- `j = (row - row_min)*(col_max - col_min + 1) + (col - col_min)`

<p align="center"><img src="block_tiles.svg?raw=true"></p>
<p align="center"><img src="block_tiles.svg"></p>

- identical `tile`s can be stored once and referenced multiple times to save storage space
- if a `tile` does not exist, the length of `tile` is zero
Expand Down
231 changes: 136 additions & 95 deletions v02/container/readme.md
Original file line number Diff line number Diff line change
@@ -1,140 +1,181 @@
# Versatiles container format specification v02
# Versatiles Container Format Specification v2.0

## Overview
## Contents
- [Versatiles Container Format Specification v2.0](#versatiles-container-format-specification-v20)
- [Contents](#contents)
- [1. Introduction](#1-introduction)
- [2. General Guidelines](#2-general-guidelines)
- [3. File structure](#3-file-structure)
- [3.1. File Header](#31-file-header)
- [3.1.1. Value `tile_format`](#311-value-tile_format)
- [3.1.2. Value `precompression`](#312-value-precompression)
- [3.2. Metadata Chunk](#32-metadata-chunk)
- [3.3. Blocks](#33-blocks)
- [3.3.1. Tile Blobs](#331-tile-blobs)
- [3.3.2. Tile Index](#332-tile-index)
- [3.4. Block Index](#34-block-index)
- [4. Glossary](#4-glossary)

- All numbers are stored in big endian byte order.
- Tiles are organised in the XYZ scheme, and not the TMS scheme. So tiles with x=0,y=0 are in the top left corner.
## 1. Introduction

The file is composed of four parts:
1. starting with a [**`file_header`**](#file_header)
2. followed by compressed [**`metadata`**](#metadata)
3. followed by several [**`block`s**](#block), where each block consists of:
- concatenated [**`tile_blobs`**](#tile_blobs)
- followed by [**`tile_index`**](#tile_index) as an index of these tiles
4. followed by [**`block_index`**](#block_index) as an index of all blocks
This document defines the Versatiles Container Format v2.0, which describes the structure and encoding mechanisms for efficiently storing large numbers of map tiles.

| File Format |
## 2. General Guidelines

- **Byte order:** All numeric values are encoded in big-endian byte order.
- **Tile organization:** Tiles are organised according to the XYZ scheme, with the origin (x=0, y=0) located at the top-left (northwest) corner.

## 3. File structure

The Versatiles Container format consists of four main components:

1. **[File Header](#31-file-header):** Introduces the file, details global properties, and indicates the locations of [Metadata](#32-metadata-chunk) and [Block Index](#34-block-index).
2. **[Metadata](#32-metadata-chunk):** Provides detailed information about the tileset, including attribution and layer definitions.
3. **[Blocks](#33-blocks):** Aggregates tiles into larger units (Blocks) for efficient storage and access, each containing [**Tile Blobs**](#331-tile-blobs) and [**Tile Index**](#332-tile-index).
4. **[Block Index](#34-block-index):** Acts as a parent directory for all blocks within the file.

| File Format |
|:-------------------------------:|
| ![File Format](file_format.svg) |

## `file_header`

- has a length of 66 bytes
- all offsets are relative to start of the file

| offset | length | type | description |
|--------|--------|--------|-------------------------|
| 0 | 14 | string | `"versatiles_v02"` |
| 14 | 1 | u8 | `tile_format` |
| 15 | 1 | u8 | `tile_precompression` |
| 16 | 1 | u8 | min zoom level |
| 17 | 1 | u8 | max zoom level |
| 18 | 4 | i32 | bbox min x (10⁷ × lon) |
| 22 | 4 | i32 | bbox min y (10⁷ × lat) |
| 26 | 4 | i32 | bbox max x (10⁷ × lon) |
| 30 | 4 | i32 | bbox max y (10⁷ × lat) |
| 34 | 8 | u64 | offset of `metadata` |
| 42 | 8 | u64 | length of `metadata` |
| 50 | 8 | u64 | offset of `block_index` |
| 58 | 8 | u64 | length of `block_index` |
### 3.1. File Header

- **Length:** 66 bytes.
- **Location:** At the start of the file.
- **Purpose:** Outlines essential file properties and indicates subsequent section locations.
- all offsets are relative to start of the file

### `tile_format` values:
| Offset | Length | Type | Description |
|-------:|-------:|:------:|------------------------------------------|
| 0 | 14 | string | File identifier (`"versatiles_v02"`) |
| 14 | 1 | u8 | `tile_format` value |
| 15 | 1 | u8 | `precompression` value |
| 16 | 1 | u8 | minimum zoom level |
| 17 | 1 | u8 | maximum zoom level |
| 18 | 4 | i32 | bbox min x (10⁷ × lon) |
| 22 | 4 | i32 | bbox min y (10⁷ × lat) |
| 26 | 4 | i32 | bbox max x (10⁷ × lon) |
| 30 | 4 | i32 | bbox max y (10⁷ × lat) |
| 34 | 8 | u64 | offset of [Metadata](#32-metadata-chunk) |
| 42 | 8 | u64 | length of [Metadata](#32-metadata-chunk) |
| 50 | 8 | u64 | offset of [Block Index](#34-block-index) |
| 58 | 8 | u64 | length of [Block Index](#34-block-index) |

| value | type | mime |
|--------|----------|----------------------------|
| `0x00` | bin | *application/octet-stream* |
| `0x10` | png | *image/png* |
| `0x11` | jpg | *image/jpeg* |
| `0x12` | webp | *image/webp* |
| `0x13` | avif | *image/avif* |
| `0x14` | svg | *image/svg+xml* |
| `0x20` | pbf | *application/x-protobuf* |
| `0x21` | geojson | *application/geo+json* |
| `0x22` | topojson | *application/topo+json* |
| `0x23` | json | *application/json* |


### `tile_precompression`
#### 3.1.1. Value `tile_format`

allowed values:
- `0`: uncompressed
- `1`: gzip compressed
- `2`: brotli compressed
| Value hex | Value dec | Type | Mime |
|----------:|----------:|:--------:|----------------------------|
| `0x00` | `0` | bin | *application/octet-stream* |
| `0x10` | `8` | png | *image/png* |
| `0x11` | `9` | jpg | *image/jpeg* |
| `0x12` | `10` | webp | *image/webp* |
| `0x13` | `11` | avif | *image/avif* |
| `0x14` | `12` | svg | *image/svg+xml* |
| `0x20` | `16` | pbf | *application/x-protobuf* |
| `0x21` | `17` | geojson | *application/geo+json* |
| `0x22` | `18` | topojson | *application/topo+json* |
| `0x23` | `19` | json | *application/json* |


## `metadata`

Content of `tiles.json`. Encoded in UTF-8, compressed with `$tile_precompression`.
If no metadata is specified, offset and length must be `0`.
#### 3.1.2. Value `precompression`

[Metadata](#32-metadata-chunk) and all [Tile Blobs](#331-tile-blobs) are pre-compressed with:

## Blocks
| Value | Method |
|-------|--------------|
| `0` | Uncompressed |
| `1` | gzip |
| `2` | Brotli |

### `block_index`

- Brotli compressed data structure
- Empty `block`s are not stored
- For each block, `block_index` contains a 33 bytes long record:

| offset | length | type | description |
|-----------|--------|------|---------------------------|
| 0 + 33*i | 1 | u8 | `level` |
| 1 + 33*i | 4 | u32 | `column`/256 |
| 5 + 33*i | 4 | u32 | `row`/256 |
| 9 + 33*i | 1 | u8 | `col_min` (0..255) |
| 10 + 33*i | 1 | u8 | `row_min` (0..255) |
| 11 + 33*i | 1 | u8 | `col_max` (0..255) |
| 12 + 33*i | 1 | u8 | `row_max` (0..255) |
| 13 + 33*i | 8 | u64 | offset of `block` in file |
| 21 + 33*i | 8 | u64 | length of `tile_blobs` |
| 29 + 33*i | 4 | u32 | length of `tile_index` |
### 3.2. Metadata Chunk

- Since a `block` only consists of `tile_blobs` appended by `tile_index`, the length of `block` must be the sum of the lengths of `tile_blobs` and `tile_index`.
- Note: To efficiently find the `block` that contains the `tile` you are looking for, use a data structure such as a "map", "dictionary" or "associative array" and fill it with the data from the `block_index`.
- **Content:** Encapsulates `tiles.json`, detailing tileset metadata.
- **Encoding:** UTF-8.
- **Compression:** Defined by the [`precompression`](#312-value-precompression) flag in the [File Header](#31-file-header).
- **Note:** The absence of Metadata is indicated by zero offsets and lengths in the [File Header](#31-file-header).


### `block`

- Each `block` is like a "super tile" and contains data of up to 256×256 (= 65536) `tile`s.
- Levels 0-8 can be stored with one `block` each. Level 9 can contain up to 512×512 `tile`s so up to 4 `block`s are necessary.
- Number of Blocks: `max(1, pow(2, (level-7))`
### 3.3. Blocks

- **Structure:** Blocks act as aggregators for up to 256×256 tiles.
- **Zoom Levels:** Single Blocks can span entire zoom levels (0-8). Higher zoom levels (>8) may require multiple Blocks.
- Maximum number of Blocks per zoom level: `pow(4, max(0, level - 8))`.

| `block`s per level |
| Blocks per level |
|:---------------------------------:|
| ![Level Blocks](level_blocks.svg) |

- Each Block contains concatenated [Tile Blobs](#331-tile-blobs) and ends with a [Tile Index](#332-tile-index).
- Neither the [Tile Blobs](#331-tile-blobs) in a Block nor Blocks in the file need to follow any particular order.


- Each `block` contains the concatenated `tile` blobs and ends with a `tile_index`.
- Neither `tile`s in a `block` nor `block`s in a `file` have to be sorted in any kind of order, as long as their indexes are correct.
- But it is recommended to sort the `tile`s in a `block` in an ascending order (e.g. to improve caching efficiency when reading/converting the whole file).

#### 3.3.1. Tile Blobs

## Tiles
- Tile Blobs are concatenated binary data, each containing one tile. All tiles have the same format and are pre-compressed.
- **Format:** Each Tile Blob has the same file format, determined by the [`tile_format`](#311-value-tile_format) code in the [File Header](#31-file-header).
- **Compression:** Each Tile Blob is compressed as specified by the [`precompression`](#312-value-precompression) flag in the [File Header](#31-file-header).

### `tile_index`

- Brotli compressed data structure
- `tile`s are read horizontally then vertically
- `index = (row - row_min)*(col_max - col_min + 1) + (col - col_min)`
- (`col_min`, `row_min`, `col_max`, `row_max` are specified in `block_index`)
- identical `tile`s can be stored once and referenced multiple times to save storage space
- if a `tile` does not exist, the length of `tile` is `0`
- offsets of `tile_blob`s are relative to the beginning of the `block`. So the offset of the first `tile_blob` should always be `0`.

| offset | length | type | description |
|--------|--------|------|----------------------------------|
| 12*i | 8 | u64 | offset of `tile_blob` in `block` |
| 12*i+8 | 4 | u32 | length of `tile_blob` |
#### 3.3.2. Tile Index

- **Compression:** Brotli.
- **Purpose:** Maps coordinates of tiles within a block to their respective binary position and length.
- Tiles are ordered horizontally then vertically
- `index = (row - row_min) * (col_max - col_min + 1) + (col - col_min)`
- (`col_min`, `row_min`, `col_max`, `row_max` are specified in [Block Index](#34-block-index))
- identical [Tile Blobs](#331-tile-blobs) can be stored once and referenced multiple times to save storage space
- if a tile does not exist, the length of Tile Blob is `0`
- offsets of [Tile Blobs](#331-tile-blobs) are relative to the beginning of the Block. So the offset of the first Tile Blob should always be `0`.

| index of `tile_blob`s |
| Offset | Length | Type | Description |
|--------|-------:|:----:|------------------------------|
| 12*i | 8 | u64 | offset of Tile Blob in Block |
| 12*i+8 | 4 | u32 | length of Tile Blob |

| index of Tile Blobs |
|:-------------------------------:|
| ![Block Tiles](block_tiles.svg) |


### `tile_blob`

- each tile is a PNG/PBF/... file as data blob
- compressed with `$tile_precompression`
### 3.4. Block Index

- **Compression:** Brotli.
- **Function:** Provides a directory for locating [Blocks](#33-blocks) within the container file.
- Empty Blocks are not stored.
- Each 33-byte entry within the Block Index is structured as follows:

| Offset | Length | Type | Description |
|----------:|-------:|:----:|-----------------------------------------|
| 0 + 33*i | 1 | u8 | `level` |
| 1 + 33*i | 4 | u32 | `column`/256 |
| 5 + 33*i | 4 | u32 | `row`/256 |
| 9 + 33*i | 1 | u8 | `col_min` (0..255) |
| 10 + 33*i | 1 | u8 | `row_min` (0..255) |
| 11 + 33*i | 1 | u8 | `col_max` (0..255) |
| 12 + 33*i | 1 | u8 | `row_max` (0..255) |
| 13 + 33*i | 8 | u64 | offset of Block in file |
| 21 + 33*i | 8 | u64 | length of [Tile Blobs](#331-tile-blobs) |
| 29 + 33*i | 4 | u32 | length of [Tile Index](#332-tile-index) |

- Since a Block consists only of [Tile Blobs](#331-tile-blobs) appended by the [Tile Index](#332-tile-index), the length of Block must be the sum of the lengths of the [Tile Blobs](#331-tile-blobs) and the [Tile Index](#332-tile-index).
- Note: To efficiently find the Block containing the tile you are looking for, use a data structure such as a "map", "dictionary" or "associative array" and fill it with the data from the Block Index.



## 4. Glossary

- **Blob:** A chunk of binary data. [Object storage on Wikipedia](https://en.wikipedia.org/wiki/Object_storage)
- **Block:** A composite unit containing up to 256×256 tiles.
- **Brotli:** A compression algorithm known for its efficiency and performance. It offers better compression than gzip. [Brotli on Wikipedia](https://en.wikipedia.org/wiki/Brotli)
- **Tile:** A square geographic area at a specified zoom level, containing map information as an image or as vector data.
Loading

0 comments on commit 7581c21

Please sign in to comment.