-
Notifications
You must be signed in to change notification settings - Fork 509
创建新图层
图层(Layer)是maptalks的核心, 你可以创建自己的图层, 来可视化数据, 实现复杂的交互, 载入自定义格式数据等.
本文介绍如何创建一个新的图层, 文中的示例需使用支持ES6语法的浏览器.
声明一个新的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负责图层的绘制, 交互和事件监听等. 你可以使用任何心仪的图形技术来实现图层渲染器, 如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);
*/
哈! 现在地图上出现了红色的"Hello World", 如果你愿意, 可以添加更多数据, 绘制更多的文字.
你可以点击这里查看完整示例.
我们都知道, 在canvas绘制前, 需要先载入外部图片. 可以在renderer中添加checkResources
方法返回外部图片的url, 地图会待图片载入完成后, 再调用draw方法进行绘制.
checkResources
返回的外部图片格式:
[[url1, width1, height1], [url2, width2, height2]]
提供高宽是因为在某些情况下, svg矢量图片需按给定高宽转化为canvas或者png, 故对普通图片, 高宽不是必须的.
外部图片会被缓存在renderer
的this.resources
中,其提供了下列方法用来操作外部图片:
- getImage(url, width, height) 返回缓存的图片
- isResourceLoaded(url) 外部图片是否已缓存
具体代码如下:
class HelloLayerRenderer extends maptalks.renderer.CanvasRenderer {
checkResources() {
//HelloLayer只是绘制文字, 没有外部图片, 所以返回空数组
return [];
}
/**
* 必须实现的方法
* 用来在地图没有交互时绘制图层
*/
draw() {
....
}
}
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);
*/
这样文字就会跟随地图缩放一起平滑移动, 而不再是默认的图层整体缩放效果.
你可以点击这里查看完整示例.
注意
当地图在交互且帧率(fps)较低时, 为维持帧率, 地图会略过部分图层renderer的drawOnInteracting, 并调用onSkipDrawOnInteracting回调函数.
所以drawOnInteracting应在尽量保证执行效率的前提下, 取得与绘制效果的平衡.
地图中有个内置的requestAnimationFrame循环. 如果有图层动画, 则地图会在每一帧(frame)调用renderer的draw
或drawOnInteracting
方法.
添加图层动画很简单, 在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);
*/
现在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文档
文中示例是用ES6语法写的, 正式开发时需把代码编译为ES5语法, 以在IE等老浏览器上使用.
具体请参考开始插件开发文档.
除了基于Canvas 2D技术, 我们也能基于WebGL和html dom (CSS 3)技术创建图层.
具体请参阅其他插件的源代码.
对于WebGL renderer, TileLayer的gl renderer是一个很好的示例.