Custom layer implementations

Alert.

Custom layer implementations are an experimental feature. To enable it, please contact us.

In addition to the built-in layers, the JS API enables you to create custom implementations for vector and raster layers.
You can use them to customize the display of layers display and building the map in any you want.

Alert.

Vector and raster layers are not interchangeable. They have different constructor signatures and a different operation principle.

In general, a custom implementation is set as follows:

const {MMap, MMapLayer} = mappable;
const map = new MMap(document.getElementById('app'), {location: LOCATION});
map.addChild(
  new MMapLayer({
    id: 'custom-layer-id',
    type: 'custom',
    zIndex: 10000, // Allows to determine the layer order

    /**
     * Allows to determine layer sorting, in the layer group with id=`${mappable.MMapDefaultSchemeLayer.defaultProps.source}:buildings`
     * For more information about the layer order, see below
     */
    grouppedWith: `${mappable.MMapDefaultSchemeLayer.defaultProps.source}:buildings`,
    implementation: ({source, type, effectiveMode}) => {
      /**
       * Depending on the combinations of mode(raster or vector), source: string, and type: string, you can,
       * fully redefine the behaviour of standard layers or declare custom implementations
       */

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

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

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

More about the layer order

Alert.

Please note that implementation returns the constructor, not its instance.

For the corresponding mode, implementation must return a class constructor implementing the type:

If implementation is not specified or did not return the constructor, the built-in JS API implementations are used instead.

Custom raster layers

The constructor of a custom raster layer, must implement the RasterLayerImplementationConstructor interface.

import type {PixelCoordinates, Camera, WorldOptions, Projection} from '@mappable-world/mappable-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 MMapLayer({
    // ...
    implementation: ({source, type, effectiveMode}) => {
      if (type === 'custom' && effectiveMode === 'vector') {
        return CustomLayer;
      }
    }
  })
);

The implementation adds a layer to the map where you fully control the content of the HTML element.
It can display any content: HTMLCanvasElement, SVG, React app, and others.
In the examples, you will find an implementation on Canvas

Searching for objects under the cursor

Layer implementation can support searching for objects under the cursor. For that, you need to implement the findObjectInPosition method:

import {MMapHotspot} from '@mappable-world/mappable-types';

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

For example:

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

export class CustomSVGLayer {
  private __elm: HTMLElement;

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

  findObjectInPosition({
    worldCoordinates,
    screenCoordinates
  }: {
    worldCoordinates: WorldCoordinates;
    screenCoordinates: PixelCoordinates;
  }) {
    // Searching for an element under the cursor
    const elm = document.elementFromPoint(screenCoordinates.x, screenCoordinates.y);

    // Check that the element is within the layer
    if (elm && this.__elm.contains(elm) && elm.id) {
      return new mappable.MMapHotspot(
        {
          type: 'Point',
          coordinates: this.__projection.fromWorldCoordinates(worldCoordinates)
        },
        {
          id: elm.id,
          category: elm.getAttribute('class')
        }
      );
    }

    return null;
  }

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

  destroy() {
    // ...
  }
}

Then you can "catch" this object in MMapListener:

map.addChild(
  new mappable.MMapListener({
    onMouseEnter(entity) {
      if (entity instanceof mappable.MMapHotspot) {
        // For example, change the element class
        document.getElementById(entity.id).classList.toggle('hovered', true);
      }
    }
  })
);

Custom vector layers

Since the vector engine operates 3D graphics, your implementation should be able to use WebGLRenderingContext for rendering.
The construct of a custom vector layer, must implement the VectorLayerImplementationConstructor interface

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 MMapLayer({
    // ...
    implementation: ({source, type, effectiveMode}) => {
      if (effectiveMode === 'vector') {
        return Custom3dLayer;
      }
    }
  })
);

The task is to implement the render method that must return two WebGLTexture textures.
To do this, you can use any available WebGL framework.
In the examples, you will find an implementation on Deck.GL

Layer order

By default, layers are sorted in the order of addition, but this order is not guaranteed in React
or when using nesting.

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

In the tree, JSX elements are positioned in the following order: layer1, layer2, layer3, but since layer2 is added later, the layer order in the JS API will be as follows:

layer1
layer3
layer2

To fix this, add the zIndex setting to each layer

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

Then no matter in which order you add layers, they will always be positioned as follows:

layer1
layer2
layer3

You may sometimes need to position a layer under a particular built-in layer.
For example, when displaying a 3D model, it should be rendered right after the building layer (in reality, they are rendered simultaneously, but that is a topic for another article).

To enable such sorting, use the grouppedWith option:

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

layer6 will be sorted in the layer2/layer4/layer5/layer6 group by "zIndex" field.

In this case, the layer order will be as follows:

layer1
layer6
layer2
layer4
layer5
layer3