Static Vector Tiles II: Openlayers with Custom Projection

Building off the work from my previous post on vector tiles, I wanted to develop a second process for use in Openlayers. There are many tutorials out there demonstrating the use of vector tiles with predefined styles and tutorials explaining how to build a fully vector tile map online. My aim instead is to demonstrate the processes to build:

  1. A vector tile set up overlaying a raster tile cache
  2. How to integrate a custom style.json
  3. How to do all of this in a custom projection using Openlayers

As always, there is more than one method to do any of this. What is shown below can definitely be improved upon. Please take this and make it better.

Github Respository is here.

Website example is here.

This example uses Openlayers6. The map is served in the NZTM projection (EPSG:2193). No effort has been made in ordering the labels. The labels display as the Openlayers decluttering orders them. Included in the JS code is a basic pop-up window demonstrating how to get information from the vector tile.

The project is built as a static set up. The vector tile cache is built directly into the website. THIS IS NOT OPTIMAL, but does demonstrate the principle. Ideally, you would have a location like AWS S3, to serve your tile cache from.

In order to use a custom projection, you will need to build an XYZ tile cache. MBTiles do not handle projections other than Web Mercator (EPSG:3857).

Basic steps

  1. Download or reproject the shapefile in NZTM
  2. Upload shapefile to PostgreSQL database with PostGIS extensions
  3. Tile PostgreSQL table into NZTM (EPSG:2193) XYZ tile cache using TRex
  4. Construct Openlayers6 JS for tile consuption

Sample Data

https://data.linz.govt.nz/layer/50280-nz-geographic-names-topo-150k/

Note:

The Geographic Names layer is clipped and filtered for this example. I clipped only to the Wellington Region and filtered the data only to use:

desc_code = BAY, METR, LOC, POP, TOWN, SBRB

Upload to PostgreSQL

shp2pgsql -s 2193 /data/wellyRegion_townBay.shp public.wellyRegion_townBay_nztm | psql -h localhost -d <yourDatabaseHere> -U <youUserNameHere>

TRex Tiling

TRex will create an XYZ tile cache in the projection of your choosing. You will need to know the resolutions and bounding box of your projection in order to make this work. I was fortunate to have this information at hand thanks to a great tutorial from LINZ.

TRex uses a config file for tiling. The config used in this example is here

The command used to run TREX:

t_rex generate --progress true --maxzoom=14 --minzoom=0 --extent=174.627603,-41.613839,176.259896,-40.737190  --config /configpsql_points.toml

TRex will generate gzip pfb’s. If you prefer to unzip them:

find . -type f | xargs -n1 -P 1 -t -I % gzip -d -r -S .pbf %
find . -type f | xargs -n1 -P 1 -t -I % % %.pbf

Openlayers JS

The Openlayers for this is version 6.  <script> tags needed are:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/css/ol.css" type="text/css">

<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/build/ol.js"></script>

<script src="//cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.15/proj4.js"></script>

<script src="https://unpkg.com/ol-mapbox-style@6.3.2/dist/olms.js" type="text/javascript"></script>

For the full JS example

NZTM Construct in Openlayers

Building the projection for Openlayers

// set NZTM projection extent so OL can determine zoom level 0 extents.
// Define NZTM projection
proj4.defs("EPSG:2193","+proj=tmerc +lat_0=0 +lon_0=173 +k=0.9996 +x_0=1600000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs");

// Register projection with OpenLayers
ol.proj.proj4.register(proj4);

// Create new OpenLayers projection
var proj2193 = new ol.proj.Projection({
	code: 'EPSG:2193',
	units: 'm',
	extent: [827933.23, 3729820.29, 3195373.59, 7039943.58]
});

// NZTM tile matrix origin, resolution and matrixId definitions.
var origin = [-1000000, 10000000];
var resolutions = [ 8960, 4480, 2240, 1120, 560, 280, 140, 70, 28, 14, 7, 2.8, 1.4, 0.7, 0.28, 0.14, 0.07 ];
var matrixIds = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];

Applying to Raster and Vector Tiles

Raster Tiles

Another great tutorial from LINZ regarding the set up of an XYZ for raster tiles.

// Tile Services Map
var urlTemplate =
  "https://tiles.maps.linz.io/nz_colour_basemap/NZTM/{z}/{x}/{y}.png";

// Set raster layer
var layer = new ol.layer.Tile({
  source: new ol.source.XYZ({
    url: urlTemplate,
    projection: proj2193,
    attributions: ['<a href="http://data.linz.govt.nz">Data from LINZ. CC BY 4.0</a>'],
    tileGrid: new ol.tilegrid.TileGrid({
      origin: origin,
      resolutions: resolutions,
      matrixIds: matrixIds,
      extent: [827933.23, 3729820.29, 3195373.59, 7039943.58]
    })
  })
});
Vector Tiles

Set up the vector layer to use custom projection:

// Set vector layer
var placeSource = new ol.source.VectorTile({
  cacheSize: 0,
  overlaps: true,
  tilePixelRatio: 1, // oversampling when > 1
  tileGrid: new ol.tilegrid.TileGrid({ 
    origin: [-1000000, 10000000],
    maxZoom: 16,
    tileSize: 4096,
    extent: [827933.23, 3729820.29, 3195373.59, 7039943.58],
    resolutions: resolutions,
  }),
  extent: [827933.23, 3729820.29, 3195373.59, 7039943.58],
  format: new ol.format.MVT(),
  projection: ol.proj.get('EPSG:2193'),
  url: 'https://xycarto.github.io/static.vector.tiles.openlayers.nztm/tiles/wellyRegion_townBay_nztm/{z}/{x}/{y}.pbf'
});

var vectorMap = new ol.layer.VectorTile({
  declutter: true,
  source: placeSource,
  renderMode: 'vector',
  zIndex: 10
  })

Styling

For the style file example

  1. The method in this example is loading the vector tile and overaying it on a raster tile cache. In order to accomplish this, a vector tile cache must be loaded first to the map, THEN the rules from the style JOSN are applied using:
fetch('./styleText.json').then(function(response) {
  response.json().then(function(glStyle) {
    olms.applyStyle(vectorMap, glStyle, 'wellyRegion_townBay_wgs');
  });
});
  1. The above uses olms.applyStyle. To access this function you will need to add the scipt tag to your HTML:
<script src="https://unpkg.com/ol-mapbox-style@6.3.2/dist/olms.js" type="text/javascript"></script>

Notes

  1. Not fully complete. Working example only
  2. Runs slow in Safari
  3. This is in no way the only way to do this. My hope is someone takes this and make it better.
  4. Still needs label work for use on mobile devices.

Static Vector Tiles for Small Projects

Contours Wellington

Update 04/05/2021: This method has been improved and found here. https://xycarto.com/2021/04/30/static-vector-tiles-ii-openlayers-with-custom-projection/

The following covers a method for visualizing a complex vector tile dataset using a static server.  This particular blog is more for beginners and reduces the technical terms as much as possible. I wouldn’t recommend this method for large complex vector tile data sets, but instead for quick one-off sites that are looking to incorporate a small number of vector tiles where the GeoJSON might be too large/complex to handle.

If you just want to skip to the map and code, visit the Github page created for this blog.

Here is the big reveal at the beginning. If you want to serve a basic vector tile locally or with a static server:

  1. Create an tile cache with uncompressed .pbf files
  2. Place that tile cache on something like GitPages, e.g. embed the tile cache directly along with your website or embed the cache on your localhost along with the other site files
  3. In Leaflet, access the tile cache using ‘L.vectorGrid.protobuf()’

If you’d like to read more in depth:

I work on a Ubuntu OS; however, the methods below will just as easily work with iOS and Windows environments. The parts for building the site, e.g. the HTML, JS, CSS and GitPages, are OS agnostic.

If you want to follow the steps outlined you will need to have installed:

  • GDAL: I am only including this since I used GDAL to extract the contours from the original elevation file.
  • OGR2OGR: Easy tool for converting your shapefile to GeoJSON.
  • Tippecanoe: Excellent tool for generating your tile caches in Web Mercator.
  • T-Rex: Necessary for building your tile cache in a custom projection like NZTM. I tried a number of methods and T-Rex seemed to work best. Sadly, as of Nov 2020, ogr2ogr cannot currently create an MVT in NZTM projection, but it can do projections where the resolutions are halved at each zoom level.
  • Leaflet: Your friendly JS library for building your map in a web environment.
  • SimpleHTTPServer: if you’d like to do localhost testing, you can run your site with this.

You will also need a Github account and a basic knowledge of building a webpage .

Here are the two stacks depending on your needs:

  • Web Mercator: GDAL, OGR2OGR, Tippecanoe, Leaflet, GitPages
  • NZTM: GDAL, OGR2OGR, PostGIS, T-Rex, Leaflet, GitPages

The basic premise is this: with your vector file, you need to construct a tile cache with uncompressed PBF files. The tile cache is the same as any XYZ tile cache you would use for raster tiles, except you are filling it with .pbf files. That tile cache will need to reside in a location accessible by your Leaflet application. In this example we are using GitPages as our server and embedding our tiles directly along with the website we built. Technically, Gitpages is acting as the server. If you are testing and serving on localhost, just embed your tile cache with your web files.

I am going to keep the code light for this blog and instead layout the steps. You can find full code examples in my Github repository here.

Building Your Own Contours

Download Elevation in NZTM
https://data.linz.govt.nz/layer/53621-wellington-lidar-1m-dem-2013/

If you download the entire dataset, you can create the contour lines from the VRT. This saves a lot of time by not creating a mosaic.

Build VRT

gdalbuildvrt dem.vrt *.tif

Contour (@ 50m intervals)

gdal_contour -a elev -i 50 dem.vrt wellyDEMContour.shp

Web Mercator Tile Cache

If you are building a Web Mercator site, you can use Tippecanoe to render the tile cache. You need to create a GeoJSON of your shapefile first

Shape to GeoJSON

ogr2ogr -f GeoJSON -a_srs EPSG:2193 -t_srs EPSG:3857 wellyDEMContour.json wellyDEMContour.shp

Tile JSON

tippecanoe --no-tile-compression --projection=EPSG:3857 --minimum-zoom=12 --maximum-zoom=16 --output-to-directory "static.vector.tiles/contoursWebmer" wellyDEMContour.json

NZTM Tile Cache

If you are building an NZTM site you will need to use T-Rex to generate the NZTM tile cache for the vector tiles. T-Rex likes best if you can give it a PostGIS table to work from. I’d also recommend simplifying complex contour data sets. Your tile generation will be much faster.

Upload to PostgreSQL (you can upload the original NZTM shapefile)

shp2pgsql -s 2193 wellyDEMContour_NZTM.shp public.contournztm | psql -h localhost -d dbName -U userName

T-Rex Config

See full config here

Note the tile size you are setting. You will need this later for your Leaflet application

[grid.user]
width = 4096
height = 4096
extent = { minx = -1000000, miny = 3087000, maxx = 3327000, maxy = 10000000 }
srid = 2193
units = "m"
resolutions = [8960.0,4480.0,2240.0,1120.0,560.0,280.0,140.0,70.0,28.0,14.0,7.0,2.8,1.4,0.7,0.28,0.14,0.07]
origin = "TopLeft"

T-Rex Tile Cache

t_rex generate --progress true --maxzoom=12 --minzoom=9 --extent=160.6,-55.95,-171.2,-25.88 --config /static.vector.tiles/trexConfig/configpsql_contour.toml

Decompress PBF Files

find . -type f | xargs -n1 -P 1 -t -I % gzip -d -r -S .pbf %
find . -type f | xargs -n1 -P 1 -t -I % % %.pbf

Important bit for Leaflet

See the GitPages site for how to set up the Leaflet JS in NZTM


// Access the tile cache
var vector = L.vectorGrid.protobuf(vectorURL, styles);

Have fun!