socib / Leaflet.TimeDimension

Add time dimension capabilities on a Leaflet map.
MIT License
433 stars 138 forks source link

TimeDimension with clustered data #189

Open francescocretti opened 4 years ago

francescocretti commented 4 years ago

Hi everyone.

Somebody have ever managed to use TimeDimension with clustered data? For example I'm currently using Supercluster plugin to render big amount of data, and I need to add a timeline feature.

This is a snippet from my code:

const clustersIndex = new Supercluster();
clustersIndex.load(myData);

const clustersLayer = L.geoJSON(null, {
  pointToLayer  : (feature, latlng) => generateIcon(options, feature, latlng)
});

const bounds = window.map.getBounds();
const bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
const zoom = window.map.getZoom();
const clusters = clustersIndex.getClusters(bbox, zoom);

clustersLayer.clearLayers();
clustersLayer.addData(clusters);

const timedClusters = L.timeDimension.layer.timedGeojson(clustersLayer, {
  addlastPoint: false,
  updateTimeDimension: true,
  updateTimeDimensionMode: 'intersect'
});

timedClusters.addTo(myMap);

The clusters appear correctly on the map but they're not affected by the time control.

I suppose it depends on how Supercluster processes the data, probably hiding the time information from TimeDimension.

Before starting to dig deeper I opened the issue to figure if someone has been through this before :)

Thanks.

francescocretti commented 4 years ago

Ok I figured out a solution: I worte a custom TimeDimension Layer that calls my API for each timestamp requested by the user with an action on the control timeline. Then when I get the data I perform the clusers computation and refresh the view.

Something like this:

L.TimeDimension.Layer.SuperClusterLayer = L.TimeDimension.Layer.extend({

  initialize: function(options) {
    // options
    this._baseURL   = options.baseURL || null;

    this._clustersIndex = new Supercluster({
      radius: 90
    });

    const clustersLayer = L.geoJSON(null, {
      pointToLayer  : (feature, latlng) => createClusterIcon(feature, latlng) // choose marker icon
    });

    L.TimeDimension.Layer.prototype.initialize.call(this, clustersLayer, options);

    this._currentLoadedTime = 0;
    this._currentTimeData   = [];
  },

  onAdd: function(map) {

    L.TimeDimension.Layer.prototype.onAdd.call(this, map);

    map.addLayer(this._baseLayer);

    // update clusters on map movements
    map.on('moveend', e => {
      this._updateClusters();
    });

    if (this._timeDimension) {
      this._getDataForTime(this._timeDimension.getCurrentTime());
    }
  },

  _onNewTimeLoading: function(ev) {
    this._getDataForTime(ev.time);
    return;
  },

  isReady: function(time) {
    return (this._currentLoadedTime == time);
  },

  _update: function() {

    // load new data
    this._clustersIndex.load(this._currentTimeData);
    // perform clustering
    this._updateClusters();

    return true;
  },

  _getDataForTime: function(time) {
    if (!this._baseURL || !this._map || !this._mapId) {
      return;
    }

    const url = `${this._baseURL}?timestamp=${time}`;

    // get data
    $.getJSON(url, json => {

      this._currentTimeData = json;
      this._currentLoadedTime = time;

      if (this._timeDimension && time == this._timeDimension.getCurrentTime() && !this._timeDimension.isLoading()) {
        this._update();
      }

      this.fire('timeload', { time });

    }).fail(err => console.warn('Error getting data', url, err));
  },

  _updateClusters: function() {
    const bounds = this._map.getBounds();
    const bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];  
    const zoom = this._map.getZoom();
    const clusters = this._clustersIndex.getClusters(bbox, zoom); 
    this._baseLayer.clearLayers();
    this._baseLayer.addData(clusters);
   }
});

L.timeDimension.layer.clusteredLayer = options => new L.TimeDimension.Layer.SuperClusterLayer(options);

// instance example
const timedClusters = L.timeDimension.layer.clusteredLayer({
  baseURL   : '/my/api/url'
});

timedClusters.addTo(myMap);

If needed, a basic Supercluster example can be found here: https://github.com/mapbox/supercluster/blob/master/demo/index.js