Собственные реализации слоев

Важно.

Собственная реализации слоев это экспериментальная опция. Для ее включения напишите нам.

Помимо встроенных слоев, JS API повзоляет создавать собственные реализации для векторных и растровых слоев.
В них вы можете полностью взять на себя отображение слоя и кастомизировать таким образом карту, как угодно.

Важно.

Векторные и растровые слои не взаимозаменяемы. У них разные сигнатуры конструкторов и разный принцип работы.

В общем случае собственная имплементация задается таким образом:

const {YMap, YMapLayer} = ymaps3;
const map = new YMap(document.getElementById('app'), {location: LOCATION});
map.addChild(
  new YMapLayer({
    id: 'custom-layer-id',
    type: 'custom',
    zIndex: 10000, // Позволяет определить порядок слоя

    /**
     * Позволяет определить сортировку слоев, в группе слоя с id=`${ymaps3.YMapDefaultSchemeLayer.defaultProps.source}:buildings`
     * Подробнее о порядке слоев читайте ниже
     */
    grouppedWith: `${ymaps3.YMapDefaultSchemeLayer.defaultProps.source}:buildings`,
    implementation: ({source, type, effectiveMode}) => {
      /**
       * В зависимости от комбинаций mode(raster или vector), source: string и type: string вы можете,
       * как полностью переопределить поведение стандартных слоев, так и объявить свои реализации
       */

      if (effectiveMode === 'raster') {
        if (type === 'markers') {
          return class {
            constructor({}) {}
          };
        }

        return class {
          constructor({}) {}
        };
      }

      if (effectiveMode === 'vector') {
        return class {
          constructor({}) {}
        };
      }
    }
  })
);

Подробнее про порядок слоев

Важно.

Обратите внимание, implementation возвращает конструктор, а не его экземпляр.

Для соотвествующих mode implementation должен вернуть конструктор класса реализующий тип:

Если implementation не задан, или не вернул конструктор, то используются встроенные в JS API реализации.

Растровые пользовательские слои

Конструктор кастомного растрового слоя, должен реализовать интерфейс RasterLayerImplementationConstructor

import type {PixelCoordinates, Camera, WorldOptions, Projection} from '@yandex/ymaps3-types/common/types';

class CustomLayer {
  private element: HTMLElement;
  private size: PixelCoordinates;
  private camera: Camera;
  private worldOptions: WorldOptions;
  private projection: Projection;

  constructor(options) {
    this.requestRender = options.requestRender;
    this.element = options.options;
    this.size = options.size;
    this.camera = options.camera;
    this.worldOptions = options.worldOptions;
    this.projection = options.projection;
  }

  destroy(): void {
    // clean or smth
  }

  render(props): void {
    // Render data
    this.element.innerText = new Date.toString();
    this.requestRender();
  }
}

map.addChild(
  new YMapLayer({
    // ...
    implementation: ({source, type, effectiveMode}) => {
      if (type === 'custom' && effectiveMode === 'vector') {
        return CustomLayer;
      }
    }
  })
);

Реализация добавляет в карту слой в котором вы полностью контролируете содержимое HTML элемента.
В нем можно отобразить любое содержимое: HTMLCanvasElement, SVG, React` приложение и т.д.
В примерах, вы найдете реализацию снега на Canvas

Поиск объектов под курсором

Реализация слоя может поддерживать поиск объектов под курсором. Для этого необходимо реализовать метод findObjectInPosition:

import {YMapHotspot} from '@yandex/ymaps3-types';

interface CustomLayer {
  findObjectInPosition({
    worldCoordinates,
    screenCoordinates
  }: {
    worldCoordinates: WorldCoordinates;
    screenCoordinates: PixelCoordinates;
  }): YMapHotspot | null;
}

К примеру:

import type {
  PixelCoordinates,
  RasterLayerImplementationConstructorProps,
  RasterLayerImplementationRenderProps,
  WorldCoordinates
} from '@yandex/ymaps3-types';

export class CustomSVGLayer {
  private __elm: HTMLElement;

  constructor({projection, element}: RasterLayerImplementationConstructorProps) {
    this.__elm = element;
    // ...
  }

  findObjectInPosition({
    worldCoordinates,
    screenCoordinates
  }: {
    worldCoordinates: WorldCoordinates;
    screenCoordinates: PixelCoordinates;
  }) {
    // Поиск элемента под курсором
    const elm = document.elementFromPoint(screenCoordinates.x, screenCoordinates.y);

    // Проверяем, что элемент находится внутри нашего слоя
    if (elm && this.__elm.contains(elm) && elm.id) {
      return new ymaps3.YMapHotspot(
        {
          type: 'Point',
          coordinates: this.__projection.fromWorldCoordinates(worldCoordinates)
        },
        {
          id: elm.id,
          category: elm.getAttribute('class')
        }
      );
    }

    return null;
  }

  render({camera}: RasterLayerImplementationRenderProps) {
    // ...
  }

  destroy() {
    // ...
  }
}

Тогда в YMapListener можно будет "поймать" этот объект:

map.addChild(
  new ymaps3.YMapListener({
    onMouseEnter(entity) {
      if (entity instanceof ymaps3.YMapHotspot) {
        // К примеру меняем класс элемента
        document.getElementById(entity.id).classList.toggle('hovered', true);
      }
    }
  })
);

Векторные кастомные слои

Так как векторный движок оперирует 3d графикой, то и ваша реализация должна уметь использовать WebGLRenderingContext для отрисовки.
Конструктор кастомного векторного слоя, должен реализовать интерфейс VectorLayerImplementationConstructor

abstract class Custom3dLayer {
  constructor(
    gl: WebGLRenderingContext,
    options: {
      requestRender: () => void;
    }
  ) {
    this.gl = gl;
    this.options = options;
  }

  abstract render(arg: {
    size: {
      width: number;
      height: number;
    };
    worlds: {
      lookAt: Vec2;
      viewProjMatrix: Matrix4;
    }[];
  }): {
    color: WebGLTexture;
    depth?: WebGLTexture;
  };

  destroy(): void {}
}

map.addChild(
  new YMapLayer({
    // ...
    implementation: ({source, type, effectiveMode}) => {
      if (effectiveMode === 'vector') {
        return Custom3dLayer;
      }
    }
  })
);

Задача сводится к реализации метода render, который должен вернуть две WebGLTexture текстуры.
Вы можете использовать для этого любой доступный вам WebGL фреймворк.
В примерах вы найдете реализацию на ThreeJS

Порядок слоев

По умолчанию слои сортируются в порядке добавления, но этот порядок никак не гарантируется в React
или при использовании вложенности.

function App() {
  const {showLayer2, toggleLayer2} = useState(false);
  return (
    <>
      <button onClick={() => toggleLayer2(true)}>Show layer2</button>
      <YMap>
        <YMapLayer id={'layer1'} />
        {showLayer2 && <YMapLayer id={'layer2'} />}
        <YMapLayer id={'layer3'} />
      </YMap>
    </>
  );
}

В дереве JSX элементы рсположены в порядке layer1, layer2, layer3. Но так как layer2 будет добавлен позднее, то в JS API порядок слоев будет таким:

layer1
layer3
layer2

Чтобы исправить ситуацию, каждому слою можно прописать настройку zIndex

function App() {
  const {showLayer2, toggleLayer2} = useState(false);
  return (
    <>
      <button onClick={() => toggleLayer2(true)}>Show layer2</button>
      <YMap>
        <YMapLayer zIndex={1000} id={'layer1'} />
        {showLayer2 && <YMapLayer zIndex={1001} id={'layer2'} />}
        <YMapLayer zIndex={1002} id={'layer3'} />
      </YMap>
    </>
  );
}

Тогда в каком бы порядке вы не добавляли слои, они всегда остануться в таком расположении:

layer1
layer2
layer3

Иногда, слой необходимо расположить прямо под определенным встроенным слоем.
К примеру, при отображении 3d модели, ее необходимо рисовать сразу за слоем зданий (В реальности они рисуются одновременно, но это тема другой статьи).

Чтобы добится такой сортировки, ипользуется опция grouppedWith:

function App() {
  return (
    <YMap>
      <YMapLayer zIndex={1000} id={'layer1'} />
      <YMapLayer zIndex={1001} id={'layer2'} />
      <YMapLayer zIndex={1002} id={'layer3'} />
      <YMapLayer zIndex={1003} grouppedWith={'layer2'} id={'layer4'} />
      <YMapLayer zIndex={1004} grouppedWith={'layer2'} id={'layer5'} />
      <YMapLayer zIndex={1000} grouppedWith={'layer2'} id={'layer6'} />
    </YMap>
  );
}

Слой layer6 будет сортирован в группе layer2/layer4/layer5/layer6 по полю zIndex.

Порядок слоев в током случае, будет:

layer1
layer6
layer2
layer4
layer5
layer3