Build Vector Tiles and Customize Their Rendering in the Web Browser

Featured image

Mapbox GL JS is a web mapping library based on WebGL.

Using Mapbox GL JS for serving tilesets is an interesting option especially if rendering a map on the fly in the client web browser is what you are looking for. This ability allows you to render efficiently data-driven map.

The below instructions detail the process for a RHEL/CentOS 7 Linux distribution.

Prerequisites

Install proj.4

PROJ is a generic coordinate transformation software, that transforms coordinates from one coordinate reference system (CRS) to another. This includes cartographic projections as well as geodetic transformations.

proj.4 installation from source

git clone https://github.com/OSGeo/proj.4
cd proj.4
./autogen.sh
./configure
make
sudo make install

Install Expat and SQLite

  • Expat is a C library for parsing XML, started by James Clark in 1997
  • Development tools for the sqlite3 embeddable SQL database engine
sudo yum install expat sqlite-devel

Install GDAL and TippeCanoe

GDAL

GDAL is a translator library for raster and vector geospatial data formats that is released under an X/MIT style Open Source license by the Open Source Geospatial Foundation.

(GDAL must be installed with SQLite and Expat support.)

Download last GDAL from: http://trac.osgeo.org/gdal/wiki/DownloadSource

Install GDAL

./configure
make
sudo make install

Make GDAL tools accessible by adding /usr/local/lib64 to LD_LIBRARY_PATH

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib64

Check ogr2ogr gdal tool version

ogr2ogr --version
GDAL 2.3.1, released 2018/06/22

Tippecanoe

Tippecanoe enable to build vector tilesets from large collections of GeoJSON features.

Install Tippecanoe

git clone https://github.com/mapbox/tippecanoe
cd tippecanoe
make
sudo make install

Generate Your Own Vector Tiles

Download any OpenStreetMap .pbf file you would like to render.

wget https://download.geofabrik.de/europe/great-britain/england/greater-london-latest.osm.pbf

The .pbf file itself can be thought of as a database. GDAL (ogr2ogr) will automatically view the .pbf as having 5 tables: points, lines, multipolygons, multilinestrings (routes), other_relations (osm relations). The osmconf.ini file tells GDAL what columns you want in your tables. Columns are the key name from key=value tag pairs in OSM. (Source: User:Bgirardot/How To Convert osm .pbf files to Esri Shapefiles)

My osmconf.ini file define the [lines] information that way:

[lines]
# common attributes
osm_id=yes
osm_version=no
osm_timestamp=no
osm_uid=no
osm_user=no
osm_changeset=no

# keys to report as OGR fields
attributes=name,highway,waterway,aerialway,barrier,man_made

# type of attribute 'foo' can be changed with something like
#foo_type=Integer/Real/String/DateTime

# keys that should NOT be reported in the "other_tags" field
ignore=created_by,converted_by,source,time,ele,note,openGeoDB:,fixme,FIXME
# uncomment to avoid creation of "other_tags" field
#other_tags=no
# uncomment to create "all_tags" field. "all_tags" and "other_tags" are exclusive
#all_tags=yes

#computed_attributes must appear before the keywords _type and _sql
computed_attributes=z_order
z_order_type=Integer
# Formula based on https://github.com/openstreetmap/osm2pgsql/blob/master/style.lua#L13
# [foo] is substituted by value of tag foo. When substitution is not wished, the [ character can be escaped with \[ in literals
# Note for GDAL developers: if we change the below formula, make sure to edit ogrosmlayer.cpp since it has a hardcoded optimization for this very precise formula
z_order_sql="SELECT (CASE [highway] WHEN 'minor' THEN 3 WHEN 'road' THEN 3 WHEN 'unclassified' THEN 3 WHEN 'residential' THEN 3 WHEN 'tertiary_link' THEN 4 WHEN 'tertiary' THEN 4 WHEN 'secondary_link' THEN 6 WHEN 'secondary' THEN 6 WHEN 'primary_link' THEN 7 WHEN 'primary' THEN 7 WHEN 'trunk_link' THEN 8 WHEN 'trunk' THEN 8 WHEN 'motorway_link' THEN 9 WHEN 'motorway' THEN 9 ELSE 0 END) + (CASE WHEN [bridge] IN ('yes', 'true', '1') THEN 10 ELSE 0 END) + (CASE WHEN [tunnel] IN ('yes', 'true', '1') THEN -10 ELSE 0 END) + (CASE WHEN [railway] IS NOT NULL THEN 5 ELSE 0 END) + (CASE WHEN [layer] IS NOT NULL THEN 10 * CAST([layer] AS INTEGER) ELSE 0 END)"

Convert .osm.pbf data to GeoJSON format specifying the layer of data to extract, here: lines

ogr2ogr -f 'GeoJSON' -s_srs 'EPSG:4326' -t_srs 'EPSG:4326' 'Greater London.json' 'Greater_London.osm.pbf' lines

Having our data in a GeoJSON file, we can generate tiles that way:

tippecanoe --no-feature-limit --no-tile-size-limit --include={"osm_id","highway"} --maximum-zoom=16 --output-to-directory "Greater_London" 'Greater_London.json'

Check how your data has been encoded inside a tile (having a tile/a file: ./Greater_London/8/127/84.pbf, I want to read the first 500 characters)

tippecanoe-decode ./Greater_London/8/127/84.pbf 8 127 82 | head -c 500

I get (The output below has been formatted after the command)

{
   "type":"FeatureCollection",
   "properties":{
      "zoom":8,
      "x":127,
      "y":82
   },
   "features":[
      {
         "type":"FeatureCollection",
         "properties":{
            "layer":"Greater_London",
            "version":2,
            "extent":4096
         },
         "features":[
            {
               "type":"Feature",
               "properties":{
                  "osm_id":"3702620",
                  "highway":"residential"
               },
               "geometry":{
                  "type":"LineString",
                  "coordinates":[[-0.499535,53.343583],[-0.501251,53.344813],[-0.502625,53.347272],[-0.504341,53.347887]]
               }
            },
            {
               "type":"Feature",
               "properties":

Render your tiles with Mapbox GL JavaScript library

Under your web server directory create a new directory

mkdir serving_custom_tiles_demo

In it edit an index.html file

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Map</title>
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.50.0/mapbox-gl.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.50.0/mapbox-gl.css' rel='stylesheet' />
  <style>
    body { margin:0; padding:0; }
    #map { position:absolute; top:0; bottom:0; width:100%; }
  </style>
</head>
<body>
  <div id="map"></div>
  <script>
    mapboxgl.accessToken = "not-needed-unless-using-mapbox-styles";
    var map = new mapboxgl.Map({
        container: "map",
        style: "base_style.json",
        center: [0.1278, 51.5074], // center of London
        minZoom: 5,
        zoom: 13,
        maxZoom: 16
    });
    map.addControl(new mapboxgl.NavigationControl());
  </script>
</body>
</html>

Move your tiles previously generated under your web server directory, making your tiles reachable in the browser from: http://localhost/tiles/Greater_London/{z}/{x}/{y}.pbf

Edit also a Javascript file base_style.json defining your custom rendering style:

{
  "version": 8,
  "name": "Custom",
  "metadata": {
    "mapbox:autocomposite": true
  },
  "sources": {
    "composite": {
      "type": "vector",      
      "tiles": [
        "http://localhost/tiles/Greater_London/{z}/{x}/{y}.pbf"
      ],
      "minzoom": 0,
      "maxzoom": 15
    }
  },
 "layers": [
    {
      "id": "background",
      "type": "background",
      "paint": {
        "background-color": "#e3decb"
      }
    },
  {
            "interactive": true,
            "layout": {
                "line-cap": "butt",
                "line-join": "miter"
            },
            "filter": [
                "all",
                [
                    "==",
                    "$type",
                    "LineString"
                ],
                [
                    "all",
                    [
                        "in",
                        "highway",
                        "motorway",
                        "motorway_link",
                        "primary",
                        "primary_link ",
                        "secondary",
                        "secondary_link ",
                        "residential",
                        "tertiary",
                        "tertiary_link ",
                        "trunk",
                        "trunk_link",
                        "street",
                        "street_limited",
                        "service",
                        "track",
                        "pedestrian",
                        "path",
                        "link"
                    ]
                ]
            ],
            "type": "line",
            "source": "composite",
            "id": "tunnel_minor",
            "paint": {
                "line-color": "#efefef",
                "line-width": {
                    "base": 1.55,
                    /***
                    *  Zoom functions allow the appearance of a map feature to change with map’s zoom level. 
                    *  Zoom functions can be used to create the illusion of depth and control data density. 
                    *  Each stop is an array with two elements: the first is a zoom level and the second is 
                    *  a function output value.
                    ***/
                    "stops": [
                    	// zoom is 4 -> the line width will be 0.25px
                        [4, 0.25],
                        // zoom is 20 -> the line width will be 30px
                        [20, 30]
                    ]
                }
            },
            "source-layer": "Greater_London"
        }
  ]
}

Check Mapbox Style Specification to know more about the above properties used.

Notice that the source-layer property should correspond to the layer property seen before in a .pbf tile.

If Apache can read your tiles you should be able to see a rendering of it in a web browser at: http://localhost/serving_custom_tiles_demo

See also

Monitor SSIS job and package executions

date_range 02/09/2020

Featured image

How to monitor SSIS job and package executions.

Enable network connectivity between Docker containers on CentOS 8

date_range 15/08/2020

Featured image

Enable a network connectivity between Docker containers on CentOS 8.

Setup a GitHub repository to serve your Sphinx documentation

date_range 07/04/2020

Featured image

Sphinx and GitHub provide an efficient and free way to publish your documentation online. Here we describe how to do so.