Protocolbuffer Binary Format (PBF) Documentation or Examples?

07-17-2019 09:10 AM
New Contributor III

ArcGIS Enterprise added support for the PBF output format in 10.7.1. We are having difficulty finding any documentation or examples on Esri's implementation of PBF in either the Developer's documentation or Esri's Github outside of Vector Tiles. We would like to look at using this format with some larger datasets. Any pointers would be appreciated? 

Tags (1)
31 Replies
New Contributor II

I don't know what PBF-formatted data ESRI support other than vector tiles and fontstacks. We implemented PBF tile generation by following the MapBox spec ( and and I believe (I didn't implement it myself) taking inspiration from The actual implementation was done in C++ using Google's Protocol Buffers library (

New Contributor

Is there full documentation on how to de-serialize a vector tile pbf file yet? I hope to take a ESRI vector tile pbf and convert it into geojson. I tried to use the Mapbox spec (  but it only transformed the data into another format that I do not know how to handle.

@mgeorge Do you have any update?

0 Kudos
Esri Contributor

Hi @JINANYI, we release the FeatureTile Pbf spec here: The VTL encoding we use is the same as mapbox/vector-tile-spec: Mapbox Vector Tile specification (

Basically what you will want to do is pull in some pbf parsing library which will then parse the format verbatim. After that, take a look at vector-tile-spec/ at master · mapbox/vector-tile-spec ( to see what the properties of the tile mean. I.e., the geometry encoding for mapbox uses command integers that you need to decode what operation is happening.

0 Kudos
New Contributor

I read the references you provided and also this article: FAQ: What is a root tile and how are they used to make a vector tile package with a local coordinate system?

Using all the info, I tried to decode the a vector tile myself, using World Topographic Map ( I downloaded this particular tile in pbf,, and start decoding in R language (sorry it's the only language I know).

However, as it turned out, for some unknown reason, the origin of the selected tile seems to be the bottom left corner (x axis positive to the right, y axis positive upward), instead of top left corner. It is shown in the following codes:




#the st_read() function automatically uses `MVT' driver, and convert the geometry data into tile coordinates
test <- st_read("17.pbf", layer = "City small scale")

#function that convert tile coordinates into the real mapping coordinates (EPSG: 3857)
shift_tile_coord <- function(sf, level, row, column, crs = 3857) {
    if(crs != 3857) stop("currently only supports EPSG:3857 Web Mercator projection")

    origin_x <- -20037508.342787
    origin_y <- 20037508.342787
    maxscale <- 2.958287637957775E8
    tile_size <- 512 #pixels of a row/ column
    dpi <- 96
    inch2mapUnit <- 39.3700787  #1 meter = 39.3700787 inch
    tile_extent <- 4096

    root_tile_width <- maxscale * (tile_size/(dpi * inch2mapUnit))
    selected_tile_width <- root_tile_width/2^level
    sf_shifted <- sf
    #x = tile_x_coor*selected_tile_width/tile_extent + origin_x + selected_tile_width*column
    #y = tile_y_coor*selected_tile_width/tile_extent + origin_y - selected_tile_width*(row + 1)
    sf_shifted$geometry <- sf$geometry * matrix(c(selected_tile_width/tile_extent, selected_tile_width/tile_extent), ncol = 2)
    sf_shifted$geometry <- sf_shifted$geometry + matrix(c(origin_x + selected_tile_width * column, origin_y - selected_tile_width * (row + 1)), ncol = 2)
    sf_shifted <- st_set_crs(sf_shifted, crs)

#visulaize the data
test_shifted <- shift_tile_coord(test, level = 6, row = 24, column = 17)

ggplot(data = test_shifted) + 
    geom_sf_label(aes(label = X_name_en)) +
    geom_sf() +
    coord_sf(datum = st_crs(3857))




Test DataTest Data


If you use map viewer to check (, the coordinates (in Web Mercator, EPSG: 3857) are correct. However, the conversion of a y coordinate = tile_y_coor*selected_tile_width/tile_extent + origin_y - selected_tile_width*(row + 1). This means a y coordinate first starts at the bottom of the tile (thus (row + 1)), and moves upward (positive sign in tile_y_coor*selected_tile_width/tile_extent). This seems to be against the documentation you provided. Is it designed to be this way, or is there something wrong?


0 Kudos
Esri Contributor

@JINANYI not sure that I understand your repro. The coordinates after you convert them back to map space have positive y up? That would be expected. Positive y being downward is only in tile coordinate space (0-4096 for VectorTiles). 

0 Kudos
New Contributor

@mgeorge I am saying the tile coordinate space of the selected tile (level = 6, row = 24, column = 7), has positive y upward, but I expect positive y downward

0 Kudos
New Contributor

@mgeorge y coordinate (in Web Mercader projection) = tile_y_coor (0-4096 for tile coordinate) *selected_tile_width/tile_extent + origin_y (in Web Mercader projection) - selected_tile_width*(row + 1)

selected_tile_width, tile_extent are in meter (Web Mercader projection length unit)

row is the row index, 0-63 for level 6 tiles.

Here is the demonstration image (sorry, I tried my best). As you can see, for the selected tile, the origin is at the bottom left corner 

tile coordinate conversion.png

0 Kudos
Esri Contributor

Ok I think I see what you are asking. Basically you are wondering why you don't need to flip the y coordinate of the geometries parsed by the st_read method of the R library you are using?

0 Kudos
New Contributor

@mgeorge Yes, to be precise, I would expect (be aware of the negative sign and changing from "row+1" to "row"):

 y coordinate = - tile_y_coor*selected_tile_width/tile_extent + origin_y - selected_tile_width*row

Instead, it is:

 y coordinate = tile_y_coor*selected_tile_width/tile_extent + origin_y - selected_tile_width*(row + 1)

Behind the scene, st_read is using the MVT driver from GDAL: . 

0 Kudos