Custom layer implementations

Important

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

In addition to the built-in layers, the JS API allows you to create your own implementations for vector and raster layers.
In them, you can completely take over the display of the layer and thus customize the map as you wish.

Important

Vector and raster layers are not interchangeable. They have different constructor signatures and different operating principles.

In general, your own implementation is specified 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 you to define the layer order

    /**
     * Allows you to define the sorting of layers, in the layer group with id=`${mappable.MMapDefaultSchemeLayer.defaultProps.source}:buildings`
     * Read more about layer order 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
       * both completely override the behavior of standard layers and declare your own implementations
       */

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

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

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

More about layer order.

Important

Note that implementation returns the constructor, not an instance of it.

For appropriate mode implementations must return the class constructor's implementing type:

If implementation is not specified, or the constructor does not return, then the implementations built into the JS API are used.

Raster custom layers

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

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 in which you have full control over the content of the HTML element.
You can display any content in it: HTMLCanvasElement, SVG, React` application, etc.
In the examples, you will find implementation of snow on Canvas

Finding objects under the cursor

The layer implementation can support searching for objects under the cursor. To do this, 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;
  }) {
    // Finding an element under the cursor
    const elm = document.elementFromPoint(screenCoordinates.x, screenCoordinates.y);

    // Checking that the element is inside our 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 in MMapListener it will be possible to “catch” this object:

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

Vector custom layers

Since the vector engine operates on 3D graphics, your implementation must be able to use WebGLRenderingContext for rendering.
The constructor of a custom vector layer must implement the interface 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 MMapLayer({
    // ...
    implementation: ({source, type, effectiveMode}) => {
      if (effectiveMode === 'vector') {
        return Custom3dLayer;
      }
    }
  })
);

The task comes down to implementing the render method, which should return two WebGLTexture textures.
You can use any WebGL framework available to you for this.
In the examples you will implementation on ThreeJS and DeckGL.

Layer order

By default, layers are sorted in the order they were added, but this order is not guaranteed in any way 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 a JSX tree, elements are arranged in the order layer1, layer2, layer3. But since layer2 will be added later, the order of layers in the JS API will be as follows:

layer1
layer3
layer2

To correct the situation, each layer can be assigned the zIndex setting:

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 what order you add layers, they will always remain in this arrangement:

layer1
layer2
layer3

Sometimes, a layer needs to be positioned directly below a specific inline layer.
For example, when displaying a 3D model, it must be drawn immediately behind the building layer (In reality, they are drawn simultaneously, but this is a topic for another article).

To achieve this 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>
  );
}

The layer layer6 will be sorted in the group layer 2/layer 4/layer 5/layer6 by the zIndex field.

The order of layers in this case will be:

layer1
layer6
layer2
layer4
layer5
layer3