0

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)); // ??
Jaa-c
  • 5,017
  • 4
  • 34
  • 64
  • 2
    That will scale but patterns will not anchored relative to features. For the pattern to stay fixed to the feature you would need a custom renderer similar to https://openlayers.org/en/latest/examples/style-renderer.html – Mike Feb 24 '23 at 13:46

1 Answers1

1

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();
Jaa-c
  • 5,017
  • 4
  • 34
  • 64
Mike
  • 16,042
  • 2
  • 14
  • 30
  • Thank you, the provided example helped me a lot. I'll award the bounty as soon as I can. I've also edited the answer to contain the code for the future visitors. – Jaa-c Feb 27 '23 at 16:22