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