Skip to content

Create New Layer Type

Fu Zhen edited this page Dec 29, 2017 · 2 revisions

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.

Index

  1. The simplest layer
  2. Layer renderer
  3. Add texts
  4. Draw texts
  5. Advanced techniques
  6. Transpile to ES5
  7. WebGL and dom

The simplest layer

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.

Layer renderer

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.

Add texts

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.

Draw the texts

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);
*/

image

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.

Advanced techniques

Load external images

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

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.

trip

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.

Layer animation

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);
*/

trip

Now HelloLayer's texts' color changes every 300 ms, you can check it out and play by yourself.

Event listening

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); }

Other useful methods

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:

    1. Clear canvas
    2. 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

    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.

Please refer to CanvasRenderer's API for information of other methods.

Transpile to ES5

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.

WebGL and DOM

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.