-
Notifications
You must be signed in to change notification settings - Fork 507
Create New Layer Type
Layer is the core of maptalks. You can create a new Layer to visualize spatial data, create complicated interactions and load data of customized format.
This doc is a step-by-step tutorial of creating a new Layer. To run the examples, you need to use a browser supports ES6 grammar.
- The simplest layer
- Layer renderer
- Add texts
- Draw texts
- Advanced techniques
- Transpile to ES5
- WebGL and dom
Declare a new class, let it extend maptalks.Layer
, you get a simplest layer now.
class HelloLayer extends maptalks.Layer {
}
Althought it does nothing, we can try to add it to the map and see what happens.
class HelloLayer extends maptalks.Layer {
}
// An id must be given, as required by maptalks.Layer
const layer = new HelloLayer('hello');
layer.addTo(map);
Try to run it in browser, sadly an error will be thrown: 'Uncaught Error: Invalid renderer for Layer(hello):canvas'.
To fix it, a layer renderer must be implemented.
A renderer is a class taking care of layer's drawing, interaction and event listenings. It can be implemented by any tech you love such as Canvas, WebGL, SVG or HTML. A layer can have more than one renderer, e.g. TileLayer has 2 renderers: gl and canvas, which to use is decided by layer.options.renderer.
The default and the most common renderer is canvas renderer. A canvas renderer has a private canvas element for drawing, and map will draw layer's canvas on map's main canvas when drawing completes.
How to create a canvas renderer?
Declare a child class extending maptalks.renderer.CanvasRenderer
, add a new method named draw
and that's it.
/*
class HelloLayer extends maptalks.Layer {
}
*/
class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
/**
* A required method for drawing when map is not interacting
*/
draw() { }
}
// register HelloLayerRenderer as HelloLayer's canvas renderer
HelloLayer.registerRenderer('canvas', HelloLayerRenderer);
/*
const layer = new HelloLayer('hello');
layer.addTo(map);
*/
Run it again, no errors now.
Now let's draw some texts on HelloLayer.
At first, we need to define layer's data format:
[
{
'coord' : [x, y], //coordinate
'text' : 'Hello World' //text
},
{
'coord' : [x, y],
'text' : 'Hello World'
},
...
]
Then let's add some necessary method to:
- Get or update data
- Define default options: font, color
const options = {
// color
'color' : 'Red',
// font
'font' : '30px san-serif';
};
class HelloLayer extends maptalks.Layer {
// constructor
constructor(id, data, options) {
super(id, options);
this.data = data;
}
setData(data) {
this.data = data;
return this;
}
getData() {
return this.data;
}
}
//Merge options into HelloLayer
HelloLayer.mergeOptions(options);
/*
class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
draw() { }
}
HelloLayer.registerRenderer('canvas', HelloLayerRenderer);
const layer = new HelloLayer('hello');
layer.addTo(map);
*/
Let's add some texts to the layer.
var layer = new HelloLayer('hello');
layer.setData([
{
'coord' : map.getCenter().toArray(),
'text' : 'Hello World'
},
{
'coord' : map.getCenter().add(0.01, 0.01).toArray(),
'text' : 'Hello World 2'
}
]);
layer.addTo(map);
Map is still blank, we will do some drawing in the next section.
Let's draw the texts in the HelloLayerRenderer
.
It's easy and straight: in draw
method, iterate the texts and draw.
/*
const options = {
// color
'color' : 'Red',
// font
'font' : '30px san-serif'
};
class HelloLayer extends maptalks.Layer {
constructor(id, data, options) {
super(id, options);
this.data = data;
}
setData(data) {
this.data = data;
return this;
}
getData() {
return this.data;
}
}
//Merge options into HelloLayer
HelloLayer.mergeOptions(options);
*/
class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
draw() {
const drawn = this._drawData(this.layer.getData(), this.layer.options.color);
//Record data drawn
this._drawnData = drawn;
//completeRenderer is a method defined in CanvasRenderer
//
// 1. Fires events like "renderend"
// 2. Mark renderer's status as "canvas updated", which tells map to redraw the main canvas and draw layer canvas on it.
this.completeRender();
}
/**
* Draw texts
*/
_drawData(data, color) {
if (!Array.isArray(data)) {
return;
}
const map = this.getMap();
//prepareCanvas is a method defined in CanvasRenderer to prepare layer canvas
//If canvas doesn't exist, create it.
//If canvas is created, clear it for drawing
this.prepareCanvas();
//this.context is layer canvas's CanvasRenderingContext2D
const ctx = this.context;
//set color and font
ctx.fillStyle = color;
ctx.font = this.layer.options['font'];
const containerExtent = map.getContainerExtent();
const drawn = [];
data.forEach(d => {
//convert text's coordinate to containerPoint
//containerPoint is the screen position from container's top left.
const point = map.coordinateToContainerPoint(new maptalks.Coordinate(d.coord));
//If point is not within map's container extent, ignore it to improve performance.
if (!containerExtent.contains(point)) {
return;
}
const text = d.text;
const len = ctx.measureText(text);
ctx.fillText(text, point.x - len.width / 2, point.y);
drawn.push(d);
});
return drawn;
}
}
/*
HelloLayer.registerRenderer('canvas', HelloLayerRenderer);
const layer = new HelloLayer('hello');
layer.setData([
{
'coord' : map.getCenter().toArray(),
'text' : 'Hello World'
},
{
'coord' : map.getCenter().add(0.01, 0.01).toArray(),
'text' : 'Hello World 2'
}
]);
layer.addTo(map);
*/
Hah! Now some red "Hello World" texts are drawn on the map, you can add more data to draw more texts.
You can check out the example and play by yourself.
As we know, external images need to be preloaded before draw images on canvas. To preload external images, we can define a method named checkResources
to return urls of external images. Layer will wait for loading complete and draws afterwards.
Data format of checkResources
's result:
[[url1, width1, height1], [url2, width2, height2]]
Width and height is needed when converting SVG to canvas or png, it's not necessary for normal images (png/jpg).
class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
checkResources() {
//HelloLayer doesn't have any external image to load
//Just returns an empty array
return [];
}
draw() {
....
}
}
drawOnInteracting
is a method for drawing when map is interacting(moving, zooming, dragRotating).
With drawOnInteracting, you can redraw every frame during map interaction for better user experience.
For HelloLayer, in drawOnInteracting
, we can redraw data recorded by draw
method instead of all the data in every frame to gain better performance.
/*
class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
draw() {
const drawn = this._drawData(this.layer.getData(), this.layer.options.color);
this._drawnData = drawn;
this.completeRender();
}
*/
drawOnInteracting(evtParam) {
if (!this._drawnData || this._drawnData.length === 0) {
return;
}
this._drawData(this._drawnData, this.layer.options.color);
}
//call back when drawOnIntearcting is skipped by map's fps control
onSkipDrawOnInteracting() { }
/*
_drawData(data, color) {
if (!Array.isArray(data)) {
return;
}
const map = this.getMap();
this.prepareCanvas();
//..........
return drawn;
}
}
HelloLayer.registerRenderer('canvas', HelloLayerRenderer);
*/
Now texts will be redrawn smoothly with map when zooming.
You can check out the example, and play by yourself.
ATTENTION
When fps is low when interacting, to maintain the fps, map may skip calling some layer's drawOnInteracting
and call onSkipDrawOnInteracting
instead.
Thus drawOnInteracting
should always keep performance in mind and balance with user experience.
Map has an internal requestAnimationFrame loop running constantly. If layer is an animating one, map will call its renderer's draw
or drawOnInteracting
in every frame.
It's easy to animate a layer, add a new method needToRedraw
in renderer, and let it return true to indicate that it will always be redrawn in the next frame.
Let's do some animation and change text's color by time:
- Add
animation
in options to control the animation - Change options.color to a color array to fetch color during animation
- Override
needToRedraw
, let it return true when options.animation is true - Update
draw
/drawOnInteracting
to redraw texts with color by time
const options = {
// color array
'color' : ['Red', 'Green', 'Yellow'],
'font' : '30px san-serif',
// animation control
'animation' : true
};
/*
class HelloLayer extends maptalks.Layer {
constructor(id, data, options) {
super(id, options);
this.data = data;
}
setData(data) {
this.data = data;
return this;
}
getData() {
return this.data;
}
}
HelloLayer.mergeOptions(options);
class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
*/
draw() {
const colors = this.layer.options.color;
const now = Date.now();
const rndIdx = Math.round(now / 300 % colors.length),
color = colors[rndIdx];
const drawn = this._drawData(this.layer.getData(), color);
this._drawnData = drawn;
this.completeRender();
}
drawOnInteracting(evtParam) {
if (!this._drawnData || this._drawnData.length === 0) {
return;
}
const colors = this.layer.options.color;
const now = Date.now();
const rndIdx = Math.round(now / 300 % colors.length),
color = colors[rndIdx];
this._drawData(this._drawnData, color);
}
onSkipDrawOnInteracting() { }
//Return true when layer.options.animation is true
//Layer will be redrawn in the next frame
needToRedraw() {
if (this.layer.options['animation']) {
return true;
}
return super.needToRedraw();
}
/*
_drawData(data) {
if (!Array.isArray(data)) {
return;
}
const map = this.getMap();
this.prepareCanvas();
//..........
return drawn;
}
}
HelloLayer.registerRenderer('canvas', HelloLayerRenderer);
*/
Now HelloLayer's texts' color changes every 300 ms, you can check it out and play by yourself.
CanvasRenderer defines some default event callback methods, you can override them for your own event logics.
onZoomStart(e) { super.onZoomStart(e); }
onZooming(e) { super.onZooming(e); }
onZoomEnd(e) { super.onZoomEnd(e); }
onResize(e) { super.onResize(e); }
onMoveStart(e) { super.onMoveStart(e); }
onMoving(e) { super.onMoving(e); }
onMoveEnd(e) { super.onMoveEnd(e); }
onDragRotateStart(e) { super.onDragRotateStart(e); }
onDragRotating(e) { super.onDragRotating(e); }
onDragRotateEnd(e) { super.onDragRotateEnd(e); }
onSpatialReferenceChange(e) { super.onSpatialReferenceChange(e); }
Below are some useful properties and methods defined in CanvasRenderer, You can use them or override them whenever necessary:
-
this.canvas
property, renderer's private canvas element
-
this.context
property, canvas's CanvasRenderingContext2D
-
onAdd
method, callback when layer is added to map
-
onRemove
method, callback when layer is removed from map
-
setToRedraw()
method, mark layer should be redrawn in the next frame, map will call renderer's
draw
/drawOnInteracting
and redraw it on map's main canvas. -
setCanvasUpdated()
method, mark layer's canvas updated and ask map to redraw it on map's main canvas without calling of
draw
/drawOnInteracting
-
getCanvasImage()
method, return layer renderer's canvas image, the format:
{
image : // canvas,
layer : // layer,
point : // containerPoint of canvas's left top,
size : // canvas's size
}
-
createCanvas()
method, create private canvas and initialize it. A
canvascreate
event will be fired. -
onCanvasCreate()
A callback method that will be called once layer's canvas is created.
-
prepareCanvas()
method, prepare layer's canvas:
- Clear canvas
- If layer has a mask, clip the canvas as the mask
-
clearCanvas()
method, clear the canvas
-
resizeCanvas(size)
method, resize the canvas as the given size (or map's size in default)
-
completeRender()
method, helper method to complete render
- Fires events like "renderend"
- Mark renderer's status as "canvas updated", which tells map to redraw the main canvas and draw layer canvas on it.
Please refer to CanvasRenderer's API for information of other methods.
We write layer(plugin)'s codes in ES6 grammar, so we need to transpile codes to ES5 for older browsers like IE9/10.
Please refer to Begin Plugin Develop for more details.
This example is using Canvas 2D, but we can also implement layer's renderer by WebGL and HTML DOM.
Please refer to the source codes of maptalks plugins if you are interested.
For demonstration of WebGL renderer, a good example is TileLayer's gl renderer.