typescriptopenlayersangular-openlayers

How to scale background pattern with the map when zooming in open layers?


I'm drawing a rectangular feature onto a map layer. I have some image pattern I want to display as a background of the feature and I want the pattern to scale with the map when zooming, meaning the pattern has always the same looking background, no matter the zoom level.

What I have right now displays the background for the feature, but it doesn't scale with the feature when zooming:

const imageData = "..."; // some base64 encoded image pattern

const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const img = new Image();
img.onload = function () {
  var pattern = context.createPattern(img, 'repeat');
  const style = new Style({
    fill: new Fill({
      color: pattern,
    }),
  });
  feature.setStyle(style);
};
img.src = imageData;

I'm adding the features to a VectorLayer to display them on the map:

const vectorLayer = new VectorLayer({
  source: new VectorSource({
    features: features,
  }),
  updateWhileAnimating: true,
});

What I tried to do is wrapping the code with reaction on resolution change and scaling the pattern, but I don't really know how to scale it properly, so that the background just stays the same relative to the pattern?

map.getView().on('change:resolution', function() {
    const resolution = this.getResolution();
    ....
    pattern.setTransform(new DOMMatrix().scale(1 / resolution, 1 / resolution)); // ??

Solution

  • Very much like the Style Render example https://openlayers.org/en/latest/examples/style-renderer.html except that instead of using flag images you would create a canvas image (big enough for the largest feature) filled with your pattern and use that. In the example the flags can be seen to move relative to the country outlines as they are panned partially out of the view. That can be prevented by setting a large renderBuffer on the layer. https://codesandbox.io/s/style-renderer-forked-tvnq7r?file=/main.js

    Working example:

    import GeoJSON from 'ol/format/GeoJSON.js';
    import Map from 'ol/Map.js';
    import VectorLayer from 'ol/layer/Vector.js';
    import VectorSource from 'ol/source/Vector.js';
    import View from 'ol/View.js';
    import {Fill, Stroke, Style} from 'ol/style.js';
    import {fromLonLat} from 'ol/proj.js';
    import {getBottomLeft, getHeight, getWidth} from 'ol/extent.js';
    import {toContext} from 'ol/render.js';
    
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    
    const pattern = (function () {
      canvas.width = 8;
      canvas.height = 8;
      // white background
      context.fillStyle = 'white';
      context.fillRect(0, 0, canvas.width, canvas.height);
      // outer circle
      context.fillStyle = 'rgba(102, 0, 102, 0.5)';
      context.beginPath();
      context.arc(4, 4, 3, 0, 2 * Math.PI);
      context.fill();
      // inner circle
      context.fillStyle = 'rgb(55, 0, 170)';
      context.beginPath();
      context.arc(4, 4, 1.5, 0, 2 * Math.PI);
      context.fill();
      return context.createPattern(canvas, 'repeat');
    })();
    
    const flag = document.createElement('canvas');
    const flagContext = flag.getContext('2d');
    flag.width = 2000;
    flag.height = 2000;
    flagContext.fillStyle = pattern;
    flagContext.fillRect(0, 0, flag.width, flag.height);
    
    const fill = new Fill();
    const stroke = new Stroke({
      color: 'black',
      width: 2,
    });
    let scale;
    const style = new Style({
      renderer: function (pixelCoordinates, state) {
        const context = state.context;
        const geometry = state.geometry.clone();
        geometry.setCoordinates(pixelCoordinates);
        const extent = geometry.getExtent();
        const width = getWidth(extent);
        const height = getHeight(extent);
        if (height < 1 || width < 1) {
          return;
        }
    
        // Stitch out country shape from the blue canvas
        context.save();
        const renderContext = toContext(context, {
          pixelRatio: 1,
        });
        renderContext.setFillStrokeStyle(fill, stroke);
        renderContext.drawGeometry(geometry);
        context.clip();
    
        // Fill transparent country with the flag image
        const bottomLeft = getBottomLeft(extent);
        const left = bottomLeft[0];
        const bottom = bottomLeft[1];
        context.imageSmoothingEnabled = false;
        context.drawImage(
          flag,
          0,
          0,
          width * scale,
          height * scale,
          left,
          bottom,
          width,
          height
        );
        context.restore();
      },
    });
    
    const vectorLayer = new VectorLayer({
      source: new VectorSource({
        url: 'https://openlayers.org/data/vector/us-states.json',
        format: new GeoJSON(),
      }),
      style: style,
      renderBuffer: 1e10,
    });
    
    const map = new Map({
      layers: [vectorLayer],
      target: 'map',
      view: new View({
        center: fromLonLat([-100, 38.5]),
        zoom: 4,
      }),
    });
    
    const view = map.getView();
    
    const setScale = function () {
      scale = view.getResolution() * 0.00008;
    };
    
    view.on('change:resolution', setScale);
    setScale();