Skip to content

创建新图层

Fu Zhen edited this page Jan 7, 2018 · 14 revisions

图层(Layer)是maptalks的核心, 你可以创建自己的图层, 来可视化数据, 实现复杂的交互, 载入自定义格式数据等.

本文介绍如何创建一个新的图层, 文中的示例需使用支持ES6语法的浏览器.

目录

  1. 最简单的图层
  2. 创建渲染器(renderer)
  3. 添加要绘制的文字
  4. 绘制文字
  5. 高级技巧
  6. 编译为ES5
  7. WebGL和dom

最简单的图层

声明一个新的class, 继承maptalks.Layer, 就创建了一个最简单的图层类

class HelloLayer extends maptalks.Layer {

}

虽然它什么都没有做, 我们还是可以试着把它添加到地图上.

class HelloLayer extends maptalks.Layer {

}

// 根据Layer要求, 构造函数必须提供图层id
const layer = new HelloLayer('hello');
layer.addTo(map);

试着执行它, 很不幸页面会出现错误, 错误信息: 'Uncaught Error: Invalid renderer for Layer(hello):canvas'.

这是因为每个Layer必须要有一个渲染器(renderer).

创建渲染器(renderer)

renderer负责图层的绘制, 交互和事件监听等. 你可以使用任何心仪的图形技术来实现图层渲染器, 如Canvas 2D, WebGL, SVG 或HTML + CSS. 一个图层可以有多个渲染器, 例如TileLayer有gl和canvas(默认)两个渲染器, 使用哪个渲染器由图层的options.renderer来决定.

canvas是默认的渲染器, 它有自己的独立Canvas画布供绘制, 绘制结束后, map会加载canvas并呈现到地图上.

如何创建一个renderer?

声明一个继承maptalks.renderer.CanvasRenderer的class, 添加一个draw方法, 就创建了一个最简单的canvas渲染器类

/*
class HelloLayer extends maptalks.Layer {

}
*/

class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
    
  /**
  * 必须实现的方法
  * 用来在地图没有交互时绘制图层
  */
  draw() { }
}

//将`HelloLayerRenderer`注册为`HelloLayer`默认的`canvas`渲染器.
HelloLayer.registerRenderer('canvas', HelloLayerRenderer);

/*
const layer = new HelloLayer('hello');
layer.addTo(map);
*/

虽然它还是什么都没做, 但程序已不再报错.

添加要绘制的文字

现在我们来让HelloLayer做点什么, 比如在指定的坐标上绘制文字.

首先我们定义一下图层的数据格式:

[
  {
    'coord' : [x, y],       //坐标
    'text'  : 'Hello World' //文字
  },
  {
    'coord' : [x, y],
    'text'  : 'Hello World'
  },
  ...
]

然后我们为HelloLayer添加一些必要的方法, 用来:

  • 获取或更新数据
  • 定义默认配置: 字体, 文字颜色
const options = {
  // 默认颜色
  'color' : 'Red',
  // 默认字体
  '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;
  }
}

//定义默认的图层配置属性
HelloLayer.mergeOptions(options);

/*
class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
  draw() { }
}

HelloLayer.registerRenderer('canvas', HelloLayerRenderer);

const layer = new HelloLayer('hello');
layer.addTo(map);
*/

这样, 我们就能在图层上添加要绘制的文字了:

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

此时地图上还是一片空白, 接下来我们在renderer中实现文字的绘制逻辑.

绘制文字

接下来, 在之前定义的HelloLayerRenderer中实现文字绘制.

只需在draw方法里遍历图层中的数据并绘制.

/*
const options = {
  // 默认颜色
  'color' : 'Red',
  // 默认字体
  '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;
  }
}

//定义默认的图层配置属性
HelloLayer.mergeOptions(options);
*/

class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
  draw() {
    const drawn = this._drawData(this.layer.getData(), this.layer.options.color);
    //记录下绘制过的数据
    this._drawnData = drawn;
    //结束绘制:
    // 1. 触发必要的事件
    // 2. 将渲染器的canvas设为更新状态, map会加载canvas并呈现在地图上
    this.completeRender();
  }

  /**
  * 绘制数据
  */
  _drawData(data, color) {
    if (!Array.isArray(data)) {
      return;
    }
    const map = this.getMap();    
    //prepareCanvas是父类CanvasRenderer中的方法
    //用于准备canvas画布
    //如果canvas不存在时, 则创建它
    //如果canvas已存在, 则清空画布
    this.prepareCanvas();
    //this.context是渲染器canvas的CanvasRenderingContext2D
    const ctx = this.context;
    //设置样式
    ctx.fillStyle = color;
    ctx.font = this.layer.options['font'];

    const containerExtent = map.getContainerExtent();
    const drawn = [];
    data.forEach(d => {
      //将中心点经纬度坐标转化为containerPoint
      //containerPoint是指相对地图容器左上角的像素坐标.
      const point = map.coordinateToContainerPoint(new maptalks.Coordinate(d.coord));
      //如果绘制的点不在屏幕范围内, 则不做绘制以提高性能
      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

哈! 现在地图上出现了红色的"Hello World", 如果你愿意, 可以添加更多数据, 绘制更多的文字.

你可以点击这里查看完整示例.

高级技巧

载入外部图片

我们都知道, 在canvas绘制前, 需要先载入外部图片. 可以在renderer中添加checkResources方法返回外部图片的url, 地图会待图片载入完成后, 再调用draw方法进行绘制.

checkResources返回的外部图片格式:

[[url1, width1, height1], [url2, width2, height2]]

提供高宽是因为在某些情况下, svg矢量图片需按给定高宽转化为canvas或者png, 故对普通图片, 高宽不是必须的.

外部图片会被缓存在rendererthis.resources中,其提供了下列方法用来操作外部图片:

  • getImage(url, width, height) 返回缓存的图片
  • isResourceLoaded(url) 外部图片是否已缓存

具体代码如下:

class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
  
  checkResources() {
    //HelloLayer只是绘制文字, 没有外部图片, 所以返回空数组
    return [];
  }

  /**
  * 必须实现的方法
  * 用来在地图没有交互时绘制图层
  */
  draw() { 
    ....
  }
}

交互绘制(drawOnInteracting)

drawOnInteracting是在地图交互(moving, zooming, dragRotating)时调用的绘制方法.

也许你注意到, 地图缩放过程中, 图层只是被简单的整体缩放. 我们可以通过drawOnInteracting对缩放动画过程中的每一帧都进行重绘, 以获得更好的交互体验.

对于HelloLayer, 我们在drawOnInteracting中重绘上次draw方法中绘制的数据, 减少数据量来提高效率.

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

  //drawOnIntearcting被略过时的回调函数
  onSkipDrawOnInteracting() { }

/*
  _drawData(data, color) {
    if (!Array.isArray(data)) {
      return;
    }
    const map = this.getMap();
    this.prepareCanvas();

    //..........
    
    return drawn;
  }
}

HelloLayer.registerRenderer('canvas', HelloLayerRenderer);
*/

这样文字就会跟随地图缩放一起平滑移动, 而不再是默认的图层整体缩放效果.

trip

你可以点击这里查看完整示例.

注意

当地图在交互且帧率(fps)较低时, 为维持帧率, 地图会略过部分图层renderer的drawOnInteracting, 并调用onSkipDrawOnInteracting回调函数.

所以drawOnInteracting应在尽量保证执行效率的前提下, 取得与绘制效果的平衡.

图层动画

地图中有个内置的requestAnimationFrame循环. 如果有图层动画, 则地图会在每一帧(frame)调用renderer的drawdrawOnInteracting方法.

添加图层动画很简单, 在renderer中重写needToRedraw方法, 让它返回true即可.

下面我们为HelloLayer增加动画, 来让文字随时间变色:

  • 在options中增加animation配置, 控制图层是否动画.
  • 把options.color改为颜色数组, 动画时从中随时间取值
  • 重写needToRedraw方法, 在animation为true时返回trues实现图层动画
  • 改写draw/drawOnInteracting, 随时间从color中取值, 实现变色动画
const options = {
  // 颜色数组
  'color' : ['Red', 'Green', 'Yellow'],  
  'font' : '30px san-serif',
  // 是否动画
  '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);
  }

  //drawOnIntearcting被略过时的回调函数
  onSkipDrawOnInteracting() { }

  //当animation为true时是动画图层, 返回true
  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

现在HelloLayer上的文字开始按照color里的颜色随机闪烁了, 虽然看上去比较傻里傻气, 但的确是一个货真价实的动画图层 ^_^, 可以点击这里查看实际运行效果和完整代码.

事件监听

CanvasRenderer中提供了默认的事件回调函数, 你可以通过重写它们来添加事件的处理逻辑.

  // 各种事件回调, 可以根据需要选择实现
  onZoomStart(e) { super.onZoomStart(e); }
  onZoomEnd(e) { super.onZoomEnd(e); }
  onResize(e) { super.onResize(e); }
  onMoveStart(e) { super.onMoveStart(e); }
  onMoveEnd(e) { super.onMoveEnd(e); }
  onDragRotateStart(e) { super.onDragRotateStart(e); }
  onDragRotateEnd(e) { super.onDragRotateEnd(e); }
  onSpatialReferenceChange(e) { super.onSpatialReferenceChange(e); }

其他重要方法

CanvasRenderer中常用的属性及方法, 你可以在绘制逻辑中灵活的使用它们, 也可以对他们进行重写来实现自己的逻辑:

  • this.canvas

    成员变量, 渲染器的canvas画布对象

  • this.context

    成员变量, 渲染器canvas画布的CanvasRenderingContext2D

  • onAdd

    可选实现的回调函数, 图层第一次加载绘制时调用

  • onRemove

    可选实现的回调函数, 图层从map移除时调用, 可以用来释放本地创建的资源

  • setToRedraw()

    设置CanvasRenderer为重绘状态, 请求map调用draw/drawOnInteracting重绘, 并重画图层的canvas

  • setCanvasUpdated()

    设置CanvasRenderer的Canvas为更新状态, 请求map重画图层的canvas, 但不会调用draw/drawOnInteracting重绘

  • getCanvasImage()

    获取图层的Canvas图像, 返回的对象格式:

    { image : canvas画布, layer : 图层对象, point : 左上角containerPoint, size : 画布大小 }
  • createCanvas()

    创建Canvas画布, 并进行必要的设置

  • onCanvasCreate()

    图层Canvas画布创建后的回调函数

  • prepareCanvas()

    绘制前, 预备Canvas: 1. 清除Canvas, 2. 如果图层有mask, 则调用clip方法设置Canvas遮罩

  • clearCanvas()

    清除Canvas

  • resizeCanvas(size)

    按照参数size, 设置Canvas的高宽 (默认使用地图的高宽)

  • completeRender()

    绘制结束后的调用方法, 触发必要的事件, 并调用setCanvasUpdated请求重画图层的canvas

更多方法可以参考CanvasRenderer的API文档

编译为ES5

文中示例是用ES6语法写的, 正式开发时需把代码编译为ES5语法, 以在IE等老浏览器上使用.

具体请参考开始插件开发文档.

WebGL和dom

除了基于Canvas 2D技术, 我们也能基于WebGL和html dom (CSS 3)技术创建图层.

具体请参阅其他插件的源代码.

对于WebGL renderer, TileLayer的gl renderer是一个很好的示例.