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:
- raster - RasterLayerImplementationConstructor
- vector - VectorLayerImplementationConstructor
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