Кастомные рендереры

Архитектура визуализации в OpenLayers построена на разделении уровня данных и уровня отрисовки. Геометрии, источники и слои не привязаны к конкретному способу рендеринга, что позволяет переключать стратегии отображения без изменения бизнес-логики.

Ключевые рендереры:

  • Canvas renderer — классическая 2D отрисовка через <canvas>
  • WebGL renderer — GPU-ускоренная отрисовка больших наборов данных
  • DOM renderer (Overlay) — HTML-элементы поверх карты
  • Vector image rendering — промежуточный режим растровой буферизации векторных данных

На уровне слоя выбор стратегии определяется параметрами renderMode, типом слоя и внутренними реализациями LayerRenderer.


Canvas-рендеринг через postrender

Наиболее гибкий способ кастомизации без переопределения внутренних классов — использование события postrender. Оно позволяет выполнять произвольную отрисовку поверх уже готового слоя.

Базовая структура кастомного рисования

import Map from 'ol/Map.js';
import View from 'ol/View.js';
import VectorLayer from 'ol/layer/Vector.js';
import VectorSource from 'ol/source/Vector.js';
import {fromLonLat} from 'ol/proj.js';

const layer = new VectorLayer({
  source: new VectorSource()
});

layer.on('postrender', (event) => {
  const context = event.context;
  const size = event.frameState.size;

  context.save();
  context.fillStyle = 'rgba(255, 0, 0, 0.2)';
  context.fillRect(0, 0, size[0], size[1]);
  context.restore();
});

Доступ к системе координат

Ключевой момент — преобразование координат карты в пиксели:

import {toLonLat} from 'ol/proj.js';

layer.on('postrender', (event) => {
  const ctx = event.context;
  const frameState = event.frameState;
  const pixelRatio = frameState.pixelRatio;

  const coordinate = fromLonLat([37.6173, 55.7558]);

  const pixel = event.map.getPixelFromCoordinate(coordinate);

  ctx.beginPath();
  ctx.arc(pixel[0] * pixelRatio, pixel[1] * pixelRatio, 8, 0, Math.PI * 2);
  ctx.fillStyle = 'blue';
  ctx.fill();
});

Использование transform контекста

OpenLayers применяет матрицы трансформации для синхронизации мира и экрана:

layer.on('postrender', (event) => {
  const ctx = event.context;
  const transform = event.inversePixelTransform;

  ctx.save();
  ctx.setTransform(...transform);

  ctx.fillStyle = 'rgba(0, 128, 255, 0.5)';
  ctx.fillRect(0, 0, 1000000, 1000000);

  ctx.restore();
});

VectorLayer и режимы отрисовки

VectorLayer поддерживает три режима:

  • vector — полная отрисовка векторных объектов
  • image — предварительный рендеринг в изображение
  • hybrid — смешанный режим
const layer = new VectorLayer({
  source: new VectorSource(),
  renderMode: 'image'
});

Поведение image-режима

В режиме image происходит:

  • рендеринг всех объектов в offscreen canvas
  • кэширование результата
  • повторное использование при панорамировании

Это снижает нагрузку при больших наборах данных, но усложняет кастомный рендеринг, так как доступ к отдельным feature ограничивается итоговым изображением.


Создание кастомного LayerRenderer

Более низкоуровневый подход — создание собственного рендерера слоя. OpenLayers использует внутреннюю систему классов LayerRenderer, которую можно расширять.

Концепция кастомного рендерера

Рендерер отвечает за:

  • подготовку данных слоя
  • управление контекстом отрисовки
  • взаимодействие с frameState
  • обработку обновлений геометрий

Пример структуры кастомного рендерера

import BaseVectorLayer from 'ol/layer/BaseVector.js';

class CustomLayer extends BaseVectorLayer {
  createRenderer() {
    return new CustomRenderer(this);
  }
}
class CustomRenderer {
  constructor(layer) {
    this.layer = layer;
  }

  prepareFrame(frameState) {
    this.frameState = frameState;
    return true;
  }

  renderFrame(frameState, target) {
    const ctx = target.getContext('2d');

    ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
    ctx.fillRect(0, 0, frameState.size[0], frameState.size[1]);

    return target;
  }
}

Управление жизненным циклом

Рендерер участвует в следующих этапах:

  • prepareFrame — подготовка данных
  • renderFrame — отрисовка кадра
  • dispose — очистка ресурсов

Такой подход используется для сложных визуализаций, включая тепловые карты и кастомные символические слои.


WebGL-рендеринг и кастомные шейдеры

OpenLayers предоставляет WebGL-слои для высокопроизводительной визуализации больших массивов точек и линий.

Основной класс:

  • WebGLPointsLayer
  • WebGLVectorLayer (в некоторых версиях)

Пример WebGLPointsLayer

import WebGLPointsLayer from 'ol/layer/WebGLPoints.js';

const layer = new WebGLPointsLayer({
  source: vectorSource,
  style: {
    'circle-radius': 6,
    'circle-fill-color': 'rgba(255,0,0,0.6)'
  }
});

Кастомизация через style variables

WebGL-слои используют декларативную модель:

style: {
  'circle-radius': ['interpolate', ['linear'], ['get', 'value'], 0, 3, 100, 12],
  'circle-fill-color': [
    'case',
    ['>', ['get', 'value'], 50],
    'red',
    'green'
  ]
}

Продвинутый уровень: кастомные шейдеры

При необходимости полного контроля подключаются GLSL-шейдеры:

style: {
  symbol: {
    vertexShader: `
      attribute vec2 a_position;
      void main() {
        gl_Position = vec4(a_position, 0.0, 1.0);
      }
    `,
    fragmentShader: `
      precision mediump float;
      void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 0.7);
      }
    `
  }
}

Такой подход используется для:

  • визуализации потоков
  • heatmap GPU уровня
  • больших кластеров (сотни тысяч объектов)

Хит-тестинг в кастомных рендерах

При кастомной отрисовке стандартная система выбора объектов может не работать автоматически.

Ручная реализация hit detection

map.on('click', (event) => {
  const pixel = event.pixel;

  layer.getFeaturesAtPixel(pixel, (feature) => {
    console.log(feature);
  });
});

При кастомном canvas-рендеринге требуется самостоятельная проверка:

layer.on('postrender', (event) => {
  const ctx = event.context;

  // отрисовка
});

И отдельная логика проверки попадания:

function isInsideCircle(pixel, center, radius) {
  const dx = pixel[0] - center[0];
  const dy = pixel[1] - center[1];
  return dx * dx + dy * dy <= radius * radius;
}

Оптимизация кастомного рендеринга

При работе с большими сценами критичны следующие механизмы:

Использование pixelRatio

const pr = event.frameState.pixelRatio;
ctx.scale(pr, pr);

Игнорирование этого параметра приводит к размытию на Retina-дисплеях.


Минимизация операций в postrender

  • избегается создание объектов внутри цикла отрисовки
  • предварительный расчет геометрии
  • кэширование экранных координат

Разделение слоев

  • один слой — одна визуальная задача
  • кастомные оверлеи не смешиваются с векторной логикой
  • WebGL используется для плотных данных

Паттерны комбинирования рендереров

Canvas + Vector source

Используется для гибридной визуализации:

  • данные хранятся как features
  • отрисовка полностью кастомная

WebGL + Canvas overlay

  • WebGL отвечает за массовую отрисовку
  • Canvas добавляет подписи и интерактив

Offscreen rendering

Подготовка изображения вне основного цикла:

const offscreen = document.createElement('canvas');
const ctx = offscreen.getContext('2d');

ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, 100, 100);

Управление перерисовкой

OpenLayers использует систему frameState, которая контролирует:

  • центр карты
  • масштаб
  • размер viewport
  • состояние анимации

Принудительная перерисовка:

layer.changed();
map.render();

Интеграция кастомных рендереров с взаимодействием карты

Кастомные рендереры должны синхронизироваться с:

  • pan/zoom событиями
  • изменением resolution
  • rotation карты
map.on('moveend', () => {
  layer.changed();
});

Управление порядком отрисовки

Слои рендерятся по z-index:

layer.setZIndex(10);

При кастомных рендерах порядок может быть критичен, особенно при использовании postrender, где отрисовка происходит поверх всех стандартных слоёв.