outdooractive/mvt-tools
)](#command-line-tool)
Table of Contents
- Read MVT - Read MLT - Read GeoJSON - Write MVT - Write MLT - Read GPX - Write GPX - Read CSV - Write CSV - Read Shapefile - Write Shapefile - Read GeoPackage - Write GeoPackage - Write GeoJSON - Layer management - Merge tiles - Tile info - Export options - Playground
- Value access - Comparisons - String values - Set membership - Boolean conditions - Spatial predicates - Complete examples - Using the query API programmatically
- mvt dump - mvt info - mvt query - mvt merge - mvt export - mvt import - mvt load - mvt rezoom - mvt reproject - Projections
Features
- Read & write MVT (Mapbox Vector Tile 2.1, including version-1 layers) and MLT (MapLibre Tiles) from/to disk or
Data(gzipped input is detected automatically). - GeoJSON import/export — convert between MVT/MLT and GeoJSON, including gzipped GeoJSON output/input.
- GPX import/export — convert between MVT and GPX (GPS eXchange) formats; waypoints, routes and tracks are automatically split into separate layers.
- CSV import/export — convert between MVT and CSV formats; a header row is required and geometry is never guessed. Works directly with Postgres/Postgis CSV exports (
COPY TO) - Shapefile import/export — convert between MVT and ESRI Shapefile format; supports single-file and per-layer directory export with mixed-geometry detection.
- GeoPackage import/export — convert between MVT and OGC GeoPackage format; supports single-table and per-layer export, preserving layer information in the
"gpkg_layer"property. - Export options — gzip compression, buffering (pixels or extents), geometry simplification (meters or extents).
- Configurable MLT encoder — string and shared dictionaries, FSST, FastPFOR, Hilbert/Morton vertex dictionaries and delta/RLE integer streams can be toggled per export (
ExportOptions.mlt). The defaults favour encode speed over the last few percent of size, andMLTEncodingOptions.plaindisables all optional encodings. - Projections — EPSG:4326 (WGS84), EPSG:3857 (Web Mercator), EPSG:4978 (ECEF),
noSRID(raw tile coordinates). - Spatial queries — R-Tree indexed or linear scan; bounding-box search, center+radius proximity (
near), bounding-box containment (within), bounding-box intersection (intersects). - Property queries — powerful RPN-based query DSL with comparisons, string operators, regex, set membership, boolean logic, and existence checks.
- Layer management — extract, merge, remove, or filter layers and features.
- Tile metadata — per-layer feature counts, geometry-type breakdowns, property histograms.
- Command-line tool —
mvtwith subcommands:dump,info,query,merge,import,export,load,rezoom,reproject.
Requirements
This package requires Swift 6.3 or higher (Xcode 26 or newer), and compiles on iOS (\>= iOS 15), macOS (\>= macOS 15), tvOS (\>= tvOS 15), watchOS (\>= watchOS 8) as well as Linux.
Installation with Swift Package Manager
dependencies: [
.package(url: "https://github.com/Outdooractive/mvt-tools", from: "3.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "MVTTools", package: "mvt-tools"),
]),
]This package requires gis-tools 3.0.1 or newer and the gis-tools-* format packages 3.0 or newer (within the 3.x series). It is used by the mvt-postgis library; please have a look at that project as well.
Format traits
GeoJSON is always available. MVT and the remaining coders are package traits:
| Format | Trait | Notes | | --- | --- | --- | | MVT | EnableMVT | Mapbox Vector Tile 2.1 import/export. Pulls in swift-protobuf. | | MLT | EnableMLT | Pure Swift decoder/encoder. No C++ toolchain or C++ interoperability required. | | CSV | EnableCSV | | | GPX | EnableGPX | | | FIT | EnableFIT | | | Shapefile | EnableShapefile | | | GeoPackage | EnableGeoPackage | |
Enable every format in a build with swift build --enable-all-traits or swift test --enable-all-traits. The released mvt binary ships with every non-C++ coder; to build the CLI locally with all of them use swift build --enable-all-traits --product mvt. In a consuming package, declare the trait on the product dependency or enable it from the command line. To use the pure Swift MLT implementation, enable only EnableMLT:
swift build --traits EnableMLTUsage
See the API documentation (via Swift Package Index).
Read MVT
MVT support is behind the EnableMVT package trait:
import MVTTools
let mvtData = try Data(contentsOf: URL(fileURLWithPath: "14_8716_8015.vector.mvt"))
let tile = try VectorTile(mvtData: mvtData, x: 8716, y: 8015, z: 14, indexed: .hilbert)
print(tile.isIndexed) // true
print(tile.layerNames.sorted()) // ["admin", "aeroway", "airport_label", …]
// Export as GeoJSON
let geoJsonData: Data? = tile.toGeoJson(prettyPrinted: true)
// Spatial query
let results = tile.query(
at: Coordinate3D(latitude: 3.87, longitude: 11.52),
tolerance: 100.0)Read MLT
MLT support is behind the EnableMLT package trait. The decoder is pure Swift and reads the tag 0x01 format, including gzipped input, dictionaries, FSST, FastPFOR and Hilbert/Morton vertex streams:
let mltData = try Data(contentsOf: URL(fileURLWithPath: "14_8716_8015.mlt"))
let tile = try VectorTile(mltData: mltData, x: 8716, y: 8015, z: 14, indexed: .hilbert)
print(tile.layerNames.sorted())Read GeoJSON
let geoJsonData = try Data(contentsOf: URL(fileURLWithPath: "features.geojson"))
let tile = try VectorTile(geoJsonData: geoJsonData, layerProperty: "vt_layer")
// The tile can now be exported as MVT
let mvtData = tile.mvtData()Write MVT
MVT support is behind the EnableMVT package trait:
var tile = try VectorTile(x: 8716, y: 8015, z: 14)
var feature = Feature(Point(Coordinate3D(latitude: 3.870163, longitude: 11.518585)))
feature.properties = [
"name": "Test",
"value": 42,
]
tile.setFeatures([feature], for: "my_layer")
// Write with compression and buffering options
let options = VectorTile.ExportOptions(
bufferSize: .pixel(4),
compression: .default,
simplifyFeatures: .meters(1.0))
let mvtData = tile.mvtData(options: options)
try mvtData?.write(to: URL(fileURLWithPath: "output.mvt"))
// Or write directly
tile.writeMVT(to: URL(fileURLWithPath: "output.mvt"), options: options)Write MLT
var tile = try VectorTile(x: 8716, y: 8015, z: 14)
let feature = Feature(
Point(Coordinate3D(latitude: 3.870163, longitude: 11.518585)),
properties: ["name": "Test"])
tile.setFeatures([feature], for: "my_layer")
// Defaults: string/shared dictionaries, FSST and delta/RLE are enabled;
// FastPFOR and vertex dictionaries are opt-in.
var options = VectorTile.ExportOptions(compression: .default)
options.mlt.useVertexDictionaries = true
options.mlt.useFastPFOR = true
let mltData = tile.mltData(options: options)
try mltData?.write(to: URL(fileURLWithPath: "output.mlt"))
// Or write directly
tile.writeMLT(to: URL(fileURLWithPath: "output.mlt"), options: options)See the MLT guide for the supported streams, encoder feature switches and experimental-feature policy.
Read GPX
let gpxData = try Data(contentsOf: URL(fileURLWithPath: "track.gpx"))
let tile = try VectorTile(gpxData: gpxData)
// Waypoints, routes and tracks are split into separate layers.
print(tile.layerNames.sorted()) // ["rte", "trk", "wpt"]
// Export as MVT
let mvtData = tile.mvtData()Write GPX
let gpxData = tile.toGpxData()
try gpxData?.write(to: URL(fileURLWithPath: "output.gpx"))
// Or write directly
tile.writeGPX(to: URL(fileURLWithPath: "output.gpx"))Read CSV
let csvData = try Data(contentsOf: URL(fileURLWithPath: "points.csv"))
let tile = try VectorTile(csvData: csvData)
// A header row is required; geometry is never guessed.
// Latitude/longitude columns (or a WKT "geometry" column) are used.
print(tile.layerNames) // ["Layer-0"]
// With a non-default delimiter:
let tile = try VectorTile(
csvData: csvData,
readOptions: CSVReadOptions(delimiter: ";"))
// Projected data uses x/y (or longitude/latitude) and an explicit CRS.
let projected = try VectorTile(
csvData: csvData,
readOptions: CSVReadOptions(projection: .epsg3857))A projection, crs, or srid column can declare the CRS directly. Non-empty values must be consistent across the file.
Write CSV
let csvData = tile.toCsvData()
try csvData?.write(to: URL(fileURLWithPath: "output.csv"))
// Or write directly
tile.writeCSV(to: URL(fileURLWithPath: "output.csv"))Read Shapefile
let tile = try VectorTile(shapefile: URL(fileURLWithPath: "roads.shp"))
// Layer name defaults to the filename.
print(tile.layerNames) // ["roads"]Write Shapefile
// Single file export (all layers merged, throws on mixed geometry)
try tile.writeShapefile(to: URL(fileURLWithPath: "output.shp"))
// Per-layer export to a directory
try tile.writeShapefiles(to: URL(fileURLWithPath: "/tmp/layers"))
// Creates /tmp/layers/roads.shp, /tmp/layers/buildings.shp, …Read GeoPackage
// Load all feature tables (each becomes a layer)
let tile = try await VectorTile(geopackage: URL(fileURLWithPath: "data.gpkg"))
// Load a single table
let tile = try await VectorTile(
geopackage: URL(fileURLWithPath: "data.gpkg"),
table: "roads")Write GeoPackage
// Single table (all layers merged)
try await tile.writeGeoPackage(
to: URL(fileURLWithPath: "output.gpkg"),
table: "combined")
// One table per layer
try await tile.writeGeoPackage(to: URL(fileURLWithPath: "output.gpkg"))Write GeoJSON
let geoJsonData = tile.toGeoJson(
layerNames: ["road", "building"],
prettyPrinted: true,
layerProperty: "vt_layer")
try geoJsonData?.write(to: URL(fileURLWithPath: "output.geojson"))
// Or write directly
tile.writeGeoJson(to: URL(fileURLWithPath: "output.geojson"), prettyPrinted: true)Layer management
// Add features to a layer
tile.appendFeatures([feature1, feature2], to: "roads")
// Replace a layer
tile.setFeatures([feature3], for: "buildings")
// Remove features matching a predicate
tile.removeFeatures(fromLayer: "roads") { $0.properties["class"] as? String == "footway" }
// Remove an entire layer
tile.removeLayer("temporary_layer")
// Extract layers into a new tile
let subset = tile.extract(layerNames: ["road", "building"])
// Merge tiles
tile.merge(anotherTile)Merge tiles
let tile1 = try VectorTile(mvtData: mvtData1, x: 5, y: 13, z: 4)
let tile2 = try VectorTile(mvtData: mvtData2, x: 5, y: 13, z: 4)
tile1.merge(tile2)Tile info
// Layer names
let names = VectorTile.layerNames(from: mvtData) // ["road", "building", …]
// Feature statistics
for layer in tile.tileInfo() {
print(layer.name,
layer.features, // total feature count
layer.pointFeatures, // point count
layer.linestringFeatures,
layer.polygonFeatures)
}
// Static info (no VectorTile instance needed)
if let info = VectorTile.tileInfo(from: mvtData) {
print(info.count)
}Export options
VectorTile.ExportOptions(
bufferSize: .extent(512), // buffer in tile-extent units
// or: .pixel(4), .no
compression: .level(9), // gzip compression 0-9
// or: .default, .no
simplifyFeatures: .meters(1.0) // simplify geometry to 1m tolerance
// or: .extent(10), .no
)Playground
On macOS you can use a Swift Playground to inspect the MVTTools API such as layerNames and projection.
- Load a tile using MVTTools
- Inspect the properties of the
VectorTile
Concurrency
VectorTile is a Sendable value type without internal locking. Copies can be read concurrently, but sharing and mutating one instance from multiple tasks requires synchronization. Use an actor that owns the tile, or a lock-backed wrapper for synchronous callers. See the DocC Concurrency article for examples and the list of mutating APIs.
Query language
The query language is used by the mvt query CLI command and by the programmatic tile.query(term:) API. It filters features by evaluating an expression against each feature's properties and geometry. The language uses a Reverse Polish Notation (RPN) internally but the query syntax follows a natural infix style.
Value access
Properties are accessed by prefixing the key with .:
| Query | Meaning | |-------|---------| | .name | Property name exists and is truthy | | .foo.bar | Nested property foo → bar | | ."foo.bar" | Property whose key contains a dot | | .foo.[0] | First element of array property foo | | .some.0 | Same as above, shorthand |
Assuming features with this structure:
{
"properties": {
"foo": {"bar": 1, "baz": 10},
"some": ["a", "b"],
"value": 1,
"name": "Some name"
}
}.foo → true (exists)
.foo.bar → true (1 is truthy)
.foo.x → false (key not found)
.some.[0] → true ("a" exists)
.some.2 → false (out of bounds)Comparisons
| Operator | Meaning | Example | |----------|---------|---------| | == | Equal | .value == 1 | | != | Not equal | .value != 2 | | > | Greater than | .value > 0 | | >= | Greater or equal | .value >= 1 | | < | Less than | .value < 2 | | <= | Less or equal | .value <= 1 | | =~ | Regex match | .name =~ /^Some/i | | = | String contains (case-insensitive) | .name = "ome" | | =^ | String starts with (case-insensitive) | .name =^ "some" | | =$ | String ends with (case-insensitive) | .name =$ "name" |
Cross-type numeric comparisons work automatically (e.g. Int vs Double, UInt8 vs Int).
.value == 1 → true
.value != 1 → false
.value > 0 → true
.value <= 1 → true
.name =~ /^Some/ → true (regex, case-sensitive)
.name =~ /^some/i → true (regex, case-insensitive)
.name =* "ome" → true (contains, case-insensitive)
.name =^ "Some" → true (starts with, case-insensitive)
.name =$ "name" → true (ends with, case-insensitive)String values
Strings can be quoted with single or double quotes:
.name == 'Main Street'
.name == "Main Street"
.name =~ "Main.*"Set membership
.class in ["primary", "secondary"] → true if .class matches either value
.value in [1, 3, 5] → integer sets
.name in ['Alice', 'Bob'] → string setsCommas inside quoted strings are preserved:
.tags in ["tag, with, comma", "other"]Boolean conditions
| Operator | Meaning | Example | |----------|---------|---------| | and | Logical AND | .a == 1 and .b == 2 | | or | Logical OR | .a == 1 or .b == 1 | | not | Logical NOT (postfix) | .a not | | exists | Truthy check | .a exists |
.foo.bar == 1 and .value == 1 → true
.foo == 1 or .bar == 2 → false
.foo not → false (foo exists, so !true)
.foo.bar not → false (bar exists in foo)
.nonexistent not → true (property absent)
.foo exists → true (non-nil)
.nonexistent exists → false (nil)exists can be combined with other conditions:
.bridge exists and .tunnel exists → true
.nonexistent exists not → trueSpatial predicates
| Predicate | Syntax | Meaning | |-----------|--------|---------| | near | near(lat, lon, tolerance) | Feature centroid is within tolerance meters of the given point | | within | within(minLon, minLat, maxLon, maxLat) | Feature's bounding box is fully inside the rectangle | | intersects | intersects(minLon, minLat, maxLon, maxLat) | Feature's geometry intersects the rectangle |
near(3.87, 11.52, 1000) → features within 1 km
.area > 40000 and within(11.5, 3.8, 11.6, 3.9) → large features in area
.highway == primary and intersects(11.5, 3.8, 11.6, 3.9) → roads crossing the areaNote: within checks bbox containment, intersects does a precise geometry-level intersection test (via GISTools' Feature.intersects(BoundingBox) which uses a two-phase check: bbox coarse filter + precise geometry intersection).
Complete examples
# Features with area > 20000 classified as hospital
.area > 20000 and .class == 'hospital'
# Features named "Hopital" (case-insensitive) near a coordinate
.name =~ /hopital/i and near(3.87324, 11.53731, 1000)
# Roads or buildings that intersect a bounding box
.class in ["road", "building"] and intersects(11.5, 3.8, 11.6, 3.9)
# Features that exist and have a name starting with "Lac" or "Lake"
(.name =^ "Lac" or .name =^ "Lake") — note: parentheses not supported in RPN,
use the natural evaluation order instead:
.name =^ "Lac" or .name =^ "Lake"
# Features with no area property
.area not
# Features with a bridge tag
.bridge existsUsing the query API programmatically
// Text search (falls back to full-text search if query isn't recognized)
let results = tile.query(term: "école")
let results = tile.query(term: ".class == 'hospital' and .area > 1000")
// Direct use of QueryParser
let parser = QueryParser(string: ".highway in [\"primary\", \"secondary\"] and .name =* \"Main\"")!
let matches = parser.evaluate(on: someFeature)Command line tool
You can install the command line tool mvt either
- with homebrew:
brew install Outdooractive/homebrew-tap/mvt-tools - or locally to
/usr/local/binwith./install_mvt.sh
Despite its name, mvt is not limited to vector tiles: every command accepts MVT, MLT, GeoJSON, GPX, FIT, CSV, Shapefile and GeoPackage files from local disk or a web server, and merge, import and export convert between all of them.
GeoJSONs can contain a layer name in their Feature properties (default name is vt_layer), and any resulting GeoJSON will automatically include this property. This can be controlled with the options --property-name (or -P), --disable-input-layer-property (or -Di) and --disable-output-layer-property (or -Do). Some commands allow limiting the result to certain layers with --layer (or -l), which can be repeated for as many layers as necessary.
CSV input and output are controlled with the --csv-* options (--csv-delimiter, --csv-null-handling, --csv-omit-null, --csv-linestring, --csv-geometry-format, …). With --csv-linestring (or -cLS), the coordinates of all CSV rows are concatenated into a single LineString feature — useful for GPS track exports.
Use --input-projection (-ip) and --output-projection (-op) to select input and output CRS independently. CSV also honors projection/crs/srid columns.
# mvt -h
OVERVIEW: Inspect, query, and convert vector tiles (MVT/MLT), GeoJSON, GPX, FIT, CSV, Shapefile, and GeoPackage files.
Despite its name, mvt is not limited to vector tiles: every command accepts any supported format, and merge, import and export convert between MVT, MLT, GeoJSON, GPX, FIT, CSV, Shapefile and GeoPackage.
A x/y/z tile coordinate is needed for encoding/decoding MVT/MLT tiles.
This tile coordinate can be extracted from the file path/URL if it's either in the form '/z/x/y' or 'z_x_y'.
Tile coordinates are not necessary for GeoJSON, GPX, FIT, CSV, Shapefile, and GeoPackage files (a tile coordinate will be calculated from the bounding box).
Examples:
- Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt
- https://demotiles.maplibre.org/tiles/2/2/1.pbf
USAGE: mvt <subcommand>
OPTIONS:
--version Show the version.
-h, --help Show help information.
SUBCOMMANDS:
dump (default) Print the input file (MVT, MLT, GeoJSON, GPX, FIT, CSV, Shapefile, or GeoPackage) as pretty-printed GeoJSON to the console
info Print information about the input file (MLT, MVT, GeoJSON, GPX, FIT, CSV, Shapefile, or GeoPackage)
query Query the features in the input file (MLT, MVT, GeoJSON, GPX, FIT, CSV, Shapefile, or GeoPackage)
merge Merge any number of input files into a single file of any supported format
import Alias for 'merge'. Import data into a file of any supported format.
export Alias for 'merge'. Export a file as GeoJSON.
load Download all tiles within a bounding box from a tile URL template
rezoom Overzoom or underzoom source tiles into a single output tile at a target zoom level
reproject Reproject an MVT, MLT, GeoJSON, CSV, Shapefile, GeoPackage, GPX, or FIT file
See 'mvt help <subcommand>' for detailed help.mvt dump
Print any supported input file as pretty-printed GeoJSON.
mvt dump Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt
{
"type" : "FeatureCollection",
"features" : [
{
"bbox" : [
11.516327261924731,
3.8807821163834175,
11.516590118408191,
3.8815421167424793
],
"properties" : {
"oneway" : 1,
"vt_layer" : "tunnel",
"class" : "motorway"
},
"geometry" : {
"coordinates" : [
...
],
"type" : "LineString"
},
"id" : 1,
"type" : "Feature"
},
...
}mvt info
Print some informations about the input file:
- The number of features, points, linestrings, polygons per layer
- The properties for each layer
- Counts of specific properties
Example 1: Print information about a vector tile.
mvt info Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt
Name | Features | Points | LineStrings | Polygons | Unknown | Version
--------------------+----------+--------+-------------+----------+---------+--------
area_label | 55 | 55 | 0 | 0 | 0 | 2
barrier_line | 4219 | 0 | 4219 | 0 | 0 | 2
bridge | 14 | 0 | 14 | 0 | 0 | 2
building | 5414 | 0 | 0 | 5414 | 0 | 2
...
road | 502 | 1 | 497 | 4 | 0 | 2Example 2: Inspect a remote MapLibre tile.
mvt info https://demotiles.maplibre.org/tiles/2/2/1.pbf
Name | Features | Points | LineStrings | Polygons | Unknown | Version
-----------+----------+--------+-------------+----------+---------+--------
centroids | 104 | 104 | 0 | 0 | 0 | 2
countries | 113 | 0 | 0 | 113 | 0 | 2
geolines | 4 | 0 | 4 | 0 | 0 | 2Example 3: Print property counts per layer.
mvt info Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt
Property | area | class | group | layer | ldir | len | name | ...
--------------------+------+-------+-------+-------+------+-----+------+-----
airport_label | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ...
area_label | 55 | 55 | 0 | 0 | 0 | 0 | 55 | ...
...Example 4: Count values for a specific property.
mvt info -p class Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt
Property | cemetery | driveway | fence | hedge | hospital | industrial | ...
----------+----------+----------+-------+-------+----------+------------+-----
class | 4 | 36 | 3895 | 324 | 9 | 2 | ...mvt query
The mvt query command uses the query language described above.
Example 1: Full-text search.
mvt query Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt "École"Example 2: Spatial query by coordinate.
mvt query Tests/MVTToolsTests/TestData/14_8716_8015.geojson "3.87324,11.53731,1000"For a negative latitude or longitude, separate the positional argument from the options with -- so it is not parsed as an option:
mvt query path/to/tile.mvt -- "-33.86,151.2,1000"Example 3: Property query with comparisons.
mvt query -p Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt ".area > 40000 and .class == 'hospital'"Example 4: Regex, string operators, and set membership.
# Case-insensitive regex
mvt query -p 14_8716_8015.vector.mvt ".name =~ /hopital/i"
# String starts with / ends with
mvt query -p 14_8716_8015.vector.mvt ".name =^ 'Main'"
mvt query -p 14_8716_8015.vector.mvt ".name =$ 'Street'"
# Set membership
mvt query -p 14_8716_8015.vector.mvt ".class in ['hospital', 'school']"Example 5: Spatial predicates.
# Features near a point
mvt query -p 14_8716_8015.vector.mvt "near(3.87324,11.53731,1000)"
# Features within a bounding box
mvt query -p 14_8716_8015.vector.mvt "within(11.5, 3.8, 11.6, 3.9)"
# Features intersecting a bounding box
mvt query -p 14_8716_8015.vector.mvt ".class == 'road' and intersects(11.5, 3.8, 11.6, 3.9)"Example 6: Combined queries.
# Name + spatial
mvt query -p 14_8716_8015.vector.mvt ".name =~ /^lac/i and near(3.87324,11.53731,10000)"
# Existence + comparison
mvt query -p 14_8716_8015.vector.mvt ".bridge exists and .tunnel not"mvt merge
Merge any number of input files in any combination. Output format is auto-detected from the output file extension.
# Merge vector tiles:
mvt merge --output merged.mvt path/to/first.mvt path/to/second.mvt
# Merge GeoJSON files:
mvt merge --output merged.geojson path/to/first.geojson path/to/second.geojson
# Merge GeoJSON files into a vector tile:
mvt merge --output merged.mvt path/to/first.geojson path/to/second.geojson
# Merge vector tiles into a GeoJSON file:
mvt merge --output merged.geojson path/to/first.mvt path/to/second.mvt
# All supported formats can be mixed:
mvt merge --output merged.gpkg data.geojson tracks.gpx points.csv roads.shp tile.mvtWhen writing MLT, optional encoder features can be controlled with --mlt-enable <features> / --mlt-disable <features> (comma-separated, repeatable) or the --mlt-plain preset. Use --verbose to see the effective set.
# Favour encode speed over the last few percent of size
mvt merge --output merged.mlt --mlt-disable fsst first.mvt second.mvt
# Opt in to the expensive encodings
mvt merge --output merged.mlt --mlt-enable fastpfor,vertex-dictionaries first.mvt second.mvtmvt export
Alias for merge — write any supported input file to GeoJSON (or any other output format selected by --output).
mvt export --output dumped.geojson --pretty-print Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvtmvt import
Alias for merge — import data from any supported format into a tile.
mvt import --output new.mvt -x 8716 -y 8015 -z 14 data.geojson
mvt import --output new.mvt tracks.gpx roads.shp
mvt import --output combined.gpkg data.geojson tracks.gpxmvt load
Download all tiles covering a bounding box from a tile URL template into a local directory. The URL template must contain {x} and {y} placeholders (case-insensitive). The zoom level is inferred from a numeric path segment in the URL, or can be supplied with --zoom.
Tiles can be laid out in a flat directory (z_x_y.ext, the default) or a tree hierarchy (z/x/y.ext via --layout tree). --zoom overrides a literal zoom in the URL template. Bounding boxes whose minimum longitude is greater than their maximum cross the antimeridian (for example 170,-10,-170,10).
# Simple download with flat layout
mvt load --bbox "11.0,3.0,12.0,4.0" --output-dir ./tiles \
https://example.com/tiles/14/{x}/{y}.pbf
# Tree layout with explicit zoom level and concurrency control
mvt load --bbox "11.0,3.0,12.0,4.0" --output-dir ./tiles --layout tree \
--zoom 12 --concurrency 4 https://example.com/tiles/{z}/{x}/{y}.pbf
# Overwrite existing tiles
mvt load --bbox "11.0,3.0,12.0,4.0" --output-dir ./tiles --overwrite-existing \
https://example.com/tiles/14/{x}/{y}.pbfmvt rezoom
Overzoom or underzoom one or more source tiles into a single output tile at a target zoom level. Each source tile must be an ancestor or descendant of the target tile. Source tiles that are not related to the target are silently skipped, unless --strict is used.
Target tile coordinates can be specified with --target-x, --target-y, --target-z, or inferred from the --output file path.
# Infer target from output path
mvt rezoom --output ./tiles/3_2_2.mvt source_2_1_1.mvt
# Explicit target coordinates
mvt rezoom --target-z 3 --target-x 2 --target-y 2 \
--output out.mvt source.mvt another_source.mvt
# Abort if any source tile is unrelated to the target
mvt rezoom --output out.mvt --strict source.mvt
# Filter layers
mvt rezoom --output out.mvt --layer roads --layer buildings source.mvt
# Strip specific layers
mvt rezoom --output out.mvt --drop-layer temporary source.mvtmvt reproject
Reproject any supported input file into a target CRS.
--output-projection (or -op) is required. --input-projection (or -ip) overrides the source CRS; this is useful for formats without CRS metadata, or when a file's embedded CRS is wrong. The output format is inferred from the --output file extension and can be forced with --output-format.
# Web Mercator tile -> WGS84 GeoJSON
mvt reproject -ip 3857 -op 4326 --output out.geojson 14/8716/8015.mvt
# WGS84 GeoJSON -> Web Mercator tile
mvt reproject -ip 4326 -op 3857 --output out.mvt input.geojson
# Reproject to ECEF and overwrite an existing file
mvt reproject -op 4978 --output out.mvt --force-overwrite input.geojsonmvt reproject accepts the same layer filters (--layer, --drop-layer), simplification (-oSm, -oSe), buffering (-oBp, -oBe), compression (-oC), MLT encoder features (--mlt-enable, --mlt-disable, --mlt-plain), and CSV options (--csv-*) as mvt merge. Reprojecting a tile changes the CRS of the stored coordinates, not the tile coordinate itself.
Projections
Built-in CRSs are EPSG:4326 (WGS84, latitude/longitude), EPSG:3857 (Web Mercator), EPSG:4978 (ECEF), and noSRID (raw tile coordinates). Custom projections registered with GISTools are supported as well.
dump, query, merge, import, export, rezoom, and reproject accept separate input and output CRS options:
| Option | Short | Description | | --- | --- | --- | | --input-projection | -ip | Input CRS: auto (default), an SRID, EPSG:n, noSRID, or a registered WKT string | | --output-projection | -op | Output CRS: auto (default), an SRID, EPSG:n, noSRID, or a registered WKT string |
MVT/MLT, CSV, GeoJSON, GPX, and FIT are read as EPSG:4326; Shapefile and GeoPackage use their embedded CRS. --input-projection overrides that default by reinterpreting the raw coordinates — for GeoJSON and CSV in the selected CRS (not reprojected from EPSG:4326), and for Shapefile/GeoPackage to replace wrong or missing metadata. GPX and FIT reject an input projection other than EPSG:4326. Use --output-projection (or mvt reproject) for an actual coordinate transformation.
Programmatically, VectorTile.projected(to:) transforms the coordinates into another CRS, while VectorTile.retagged(to:) only relabels them:
let mercator = try tile.projected(to: .epsg3857)
let raw = try tile.retagged(to: .noSRID)Development
See DEVELOPMENT.md for local setup, build, test, coverage and documentation instructions.
Contributing
Please create an issue or open a pull request.
See DEVELOPMENT.md for local build and test setup.
License
MIT
Package Metadata
Repository: outdooractive/mvt-tools
Default branch: main
README: README.md