OpenLayers – Creating a Heatmap From Vector Tiles

heat mapopenlayersvector-tiles

I know that is possible to make a heatmap using ol/layer/Heatmap, but this component accept as source only ol/source/Vector component.

Someone have build a heatmap using VectorTile as source? Can you suggest me a workaround?

Best Answer

What @Mike proposed, to load vector tile features into normal vector heatmap layer is probably the only solution. Problem is that with vector tiles number of features can change greatly with zoom change, so with zoom change previous features in normal vector layer have to be cleared and new ones added.

One possible solution for this would be to use vector tile source method getFeaturesInExtent after all tiles are loaded, to get all features in visible extent and select the ones to be added to the heatmap vector layer. Vector tile features/geometries cannot be used directly, since they have different structure that normal vector features. Features for normal vector layer have to be created from coordinates of vector features.

Code below is more a proof of concept than bulletproof solution. Whole idea is to wait for map moveend events and vector tile layer postrender events. When moveend is detected, set a flag, and when a postrender event is detected and moveend flag is set, do inventory of vector tile layer features with the getFeaturesInExtent method, clear heatmap vector layer source and add desired features (points) from vector tile layer inventory.

Code uses ArcGIS World_Basemap_v2 vector tile layer as a source. For heatmap points cities are filtered in. Since cities in the layer have no property to be used as weight, each city gets the same value 0.8.

var defaultStyles = new ol.layer.Vector().getStyleFunction()();

var styleFunction = function(feature, resolution) {
  if (feature.get('layer') == 'Coastline') {
    return(defaultStyles);
  }
};

var vectorLayer = new ol.layer.VectorTile({
  source: new ol.source.VectorTile({
    format: new ol.format.MVT(),
    url: 'https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer/tile/{z}/{y}/{x}.pbf',
    wrapX: false
  }),
  style: styleFunction
});

var heatmapSource = new ol.source.Vector();

var heatmapLayer = new ol.layer.Heatmap({
  source: heatmapSource,
  weight: function(feature) {
    return(0.8);
  }
});

var map = new ol.Map({
  target: 'map',
  view: new ol.View({
    center: [0, 0],
    zoom: 0,
    extent: vectorLayer.getSource().getTileGrid().getExtent()
  }),
  layers: [vectorLayer, heatmapLayer]
});
  
var moveEnd = false;
var firstTime = true;
map.on('moveend', function() {
  moveEnd = true;
  if (firstTime) {
    firstTime = false;
    setTimeout(function() {
      refreshHeatmap();
    }, 250);
  }
});

vectorLayer.on('postrender', function(evt) {
  if (moveEnd) {
    moveEnd = false;
    setTimeout(function() {
      refreshHeatmap();
    }, 0);
  }  
});

function refreshHeatmap() {
  var geometry, extent, source, features;
  source = vectorLayer.getSource();
  features = source.getFeaturesInExtent(map.getView().calculateExtent());
  heatmapSource.clear();
  for (var i = 0; i < features.length; i++) {
    geometry = features[i].getGeometry();
    if ((geometry.getType() == 'Point') && (features[i].get('layer').substring(0, 4) == 'City')  && features[i].get('_name_en') )  {
      extent = geometry.getExtent();
      heatmapSource.addFeature(new ol.Feature({geometry: new ol.geom.Point([extent[0], extent[1]])}));
    }
  }
}

Here is working JSFiddle: https://jsfiddle.net/TomazicM/y1xus3m0/ and two screenshots at different zooms:

enter image description here

enter image description here