javascriptgisopenlayers

OpenLayers: optimize rendering on no-GPU platforms by skipping projection and allowing for a custom feature format?


I have an array of trajectories. Each trajectory represents a moving vehicle, and is an object consisting of a unique ID and a set of coordinates which are equally-spaced samples of the vehicle's positions over time. For example,

[
  {
    id: 1,
    p: [[100, 200], [300, 400]] // Array<[x, y]>
  },
  {
    id: 2,
    p: [[300, 400], [500, 600], [700, 800]] // Array<[x, y]>
  }
]

For example, these trajectories may represent such a movement (of 2 vehicles). (The image is unrelated to the data above, rather just another (visual) example).

enter image description here

The difficult part here is that there may be up to 15000 trajectories, and I need to animate all these moving vehicles simultaneously.

Currently, if I take a GeoJSON (with coordinates as LonLat pairs) of 15000 points and try to project them to the map (with WebMercator projection), the whole operation takes about 350ms. Given that I'm aiming for a smooth 60fps animation, I know that ideally the whole calculation part can't take more than 16.7ms. Moreover, the painting is also performed by the CPU (there's no hardware acceleration in my case), so these 16.7ms should be enough for both the data preparation and painting. Again, currently it all takes about 350ms, so I need to optimize certain parts of the algorithm.

The most crucial optimization, is that the trajectories' points' coordinates are already calculated to be canvas-bound. That it, if there's a coordinate [100, 200], it means that the point needs to be rendered at the canvas's x = 100 and y = 200, not Lon 100 and Lat 200 (or WebMercator coordinates 100 and 200). This should skip the projection step freeing some CPU time.

However, I still want to be able to benefit from all the built-in functionality OpenLayers provides for Vector Layers and Sources. Namely, the styling. Also, other features like interaction and selection. That means that I can't just blindly get the Canvas's 2D rendering context and draw some arcs there.

As I understand, I still need to create Features.

// Create features with screen coordinates
const points = trajectory.p.map(([x, y]) => {
  const geometry = new Point([x, y]);
  return new Feature(geometry);
});

However in this case the coordinates are treated as WebMercator coordinates (the features are projected to the map).

The question is, how can I make the features skip this part with projecting themselves on the map when they are rendered and instead treat their coordinates and absolute coordinates relative to the canvas where the map is rendered?


A bonus question.

Converting these coordinates into Feature object is a pretty costly operation as well. And I'm wondering, whether there's a way to skip it, but still benefit from all the styling and interactivity possibilities that OL provides for its features?

I though about creating a custom Source based on VectorSource, but that would still involve converting into Features. If I extend the base Source instead, then it's impossible to use that other source type with VectorLayer, as they only work with VectorSources. If I extend the base Layer with a custom CustomLayer to work with a CustomSource and give it a CanvasCustomLayerRenderer, then I lose all the default vector layer's styling possibilities.

What's the best I can do in my case to preserve all that OpenLayers provides out of the box, but at the same time to improve the performace by removing the steps I'm not interested in?


Solution

  • Example of OpenLayers style, pixel coordinates, no Feature

      <html>
    <head>
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v9.2.4/ol.css">
      <style>
        html, body, .map {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
        }
      </style>
      <script src="https://cdn.jsdelivr.net/npm/ol@v9.2.4/dist/ol.js"></script>
    </head>
    <body>
      <div id="map" class="map"></div>
      <script>
    
        const style = new ol.style.Style({
          image: new ol.style.Circle({
            radius: 10,
            fill: new ol.style.Fill({
              color: 'blue',
            }),
          }),
        });
    
        const base = new ol.layer.Tile({
          source:  new ol.source.OSM(),
        });
    
        base.on('postrender', e => {
          const vectorContext = ol.render.toContext(e.context);
          vectorContext.setStyle(style);
          vectorContext.drawGeometry(
            new ol.geom.Point([200, 100])
          );
        });
    
        const map = new ol.Map({
          target: 'map',
          pixelRatio: 1,
          layers: [base],
          view: new ol.View({
            center: [0, 0],
            zoom: 5,
          }),
        });
    
      </script>
    </body>
      </html>

    Using a custom layer:

    <html>
    <head>
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v9.2.4/ol.css">
      <style>
        html, body, .map {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
        }
      </style>
      <script src="https://cdn.jsdelivr.net/npm/ol@v9.2.4/dist/ol.js"></script>
    </head>
    <body>
      <div id="map" class="map"></div>
      <script>
    
        const style = new ol.style.Style({
          image: new ol.style.Circle({
            radius: 10,
            fill: new ol.style.Fill({
              color: 'blue',
            }),
          }),
        });
    
        const base = new ol.layer.Tile({
          source:  new ol.source.OSM(),
        });
    
        const canvas = document.createElement('canvas');
        const custom = new ol.layer.Layer({
          render: function (frameState) {
            const [width, height] = frameState.size;
            canvas.style.position = 'absolute';
            canvas.width = width;
            canvas.height = height;
            const vectorContext = ol.render.toContext(
              canvas.getContext('2d'),
            );
            vectorContext.setStyle(style);
            vectorContext.drawGeometry(
              new ol.geom.Point([200, 100]),
            );
            return canvas;
          },
        });
    
        const map = new ol.Map({
          target: 'map',
          pixelRatio: 1,
          layers: [base, custom],
          view: new ol.View({
            center: [0, 0],
            zoom: 5,
          }),
        });
    
      </script>
    </body>
    </html>