OpenLayers 6 + webpack 通过源码分析来实现一个简单的自定义控件
晚上鼓捣webpack环境的时候,忽然心血来潮,想试试实现一个自定义的地图控件。其实网络上已经有很多相关的教程,实现思路大多是采用jQuery做一个UI组件外挂上去。不过这种方式实现的控件可重用性不是很高。
为了有别于其他人的方案,达到可重用的要求,我采用的是通过继承ol.control类的方式来实现。
基本思路:
通过阅读OpenLayers的源码,仿制一个点击之后可以弹出一个警告框的控件。
实现步骤:
先看一看OpenLayers的源码。为了简化问题,我找了最简单的FullScreen的控件源码来参考:
import Control from './Control.js';
import {CLASS_CONTROL, CLASS_UNSELECTABLE, CLASS_UNSUPPORTED} from '../css.js';
import {replaceNode} from '../dom.js';
import {listen} from '../events.js';
import EventType from '../events/EventType.js';
const events = ['fullscreenchange', 'webkitfullscreenchange', 'MSFullscreenChange'];
/**
* @typedef {Object} Options
* @property {string} [className='ol-full-screen'] CSS class name.
* @property {string|Text} [label='\u2922'] Text label to use for the button.
* Instead of text, also an element (e.g. a `span` element) can be used.
* @property {string|Text} [labelActive='\u00d7'] Text label to use for the
* button when full-screen is active.
* Instead of text, also an element (e.g. a `span` element) can be used.
* @property {string} [tipLabel='Toggle full-screen'] Text label to use for the button tip.
* @property {boolean} [keys=false] Full keyboard access.
* @property {HTMLElement|string} [target] Specify a target if you want the
* control to be rendered outside of the map's viewport.
* @property {HTMLElement|string} [source] The element to be displayed
* fullscreen. When not provided, the element containing the map viewport will
* be displayed fullscreen.
*/
class FullScreen extends Control {
/**
* @param {Options=} opt_options Options.
*/
constructor(opt_options) {
const options = opt_options ? opt_options : {};
super({
element: document.createElement('div'),
target: options.target
});
/**
* @private
* @type {string}
*/
this.cssClassName_ = options.className !== undefined ? options.className :
'ol-full-screen';
const label = options.label !== undefined ? options.label : '\u2922';
/**
* @private
* @type {Text}
*/
this.labelNode_ = typeof label === 'string' ?
document.createTextNode(label) : label;
const labelActive = options.labelActive !== undefined ? options.labelActive : '\u00d7';
/**
* @private
* @type {Text}
*/
this.labelActiveNode_ = typeof labelActive === 'string' ?
document.createTextNode(labelActive) : labelActive;
/**
* @private
* @type {HTMLElement}
*/
this.button_ = document.createElement('button');
const tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen';
this.setClassName_(this.button_, isFullScreen());
this.button_.setAttribute('type', 'button');
this.button_.title = tipLabel;
this.button_.appendChild(this.labelNode_);
this.button_.addEventListener(EventType.CLICK, this.handleClick_.bind(this), false);
const cssClasses = this.cssClassName_ + ' ' + CLASS_UNSELECTABLE +
' ' + CLASS_CONTROL + ' ' +
(!isFullScreenSupported() ? CLASS_UNSUPPORTED : '');
const element = this.element;
element.className = cssClasses;
element.appendChild(this.button_);
/**
* @private
* @type {boolean}
*/
this.keys_ = options.keys !== undefined ? options.keys : false;
/**
* @private
* @type {HTMLElement|string|undefined}
*/
this.source_ = options.source;
}
/**
* @param {MouseEvent} event The event to handle
* @private
*/
handleClick_(event) {
event.preventDefault();
this.handleFullScreen_();
}
……
直接看一下构造函数:这里是对初始化的时候所用的配置项的处理,以及对父类中的成员进行初始化的过程,可以不用动。
constructor(opt_options) {
const options = opt_options ? opt_options : {};
super({
element: document.createElement('div'),
target: options.target
});
接下来是定义控件的css样式,也不用动:
/**
* @private
* @type {string}
*/
this.cssClassName_ = options.className !== undefined ? options.className :
'ol-full-screen';
接下来是定义按钮上的文字标签,原来的FullScreen控件定义了两个,对应是否全屏的两个状态,我们后面只需要定义一个就行了:
const label = options.label !== undefined ? options.label : '\u2922';
/**
* @private
* @type {Text}
*/
this.labelNode_ = typeof label === 'string' ?
document.createTextNode(label) : label;
const labelActive = options.labelActive !== undefined ? options.labelActive : '\u00d7';
/**
* @private
* @type {Text}
*/
this.labelActiveNode_ = typeof labelActive === 'string' ?
document.createTextNode(labelActive) : labelActive;
下面几行代码功能比较密集,在代码注释里面加以说明:
/**
* @private
* @type {HTMLElement}
*/
//创建控件的HTML控件,是一个按钮
this.button_ = document.createElement('button');
//为这个按钮初始化hint文字
const tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen';
//根据全屏状态设置按钮的样式名称
this.setClassName_(this.button_, isFullScreen());
//为控件添加一个button的类型属性
this.button_.setAttribute('type', 'button');
//为这个按钮添加hint文字
this.button_.title = tipLabel;
//将按钮文字标签添加到按钮上
this.button_.appendChild(this.labelNode_);
//为HTML按钮绑定点击事件和回调函数
this.button_.addEventListener(EventType.CLICK, this.handleClick_.bind(this), false);
//构造css类名
const cssClasses = this.cssClassName_ + ' ' + CLASS_UNSELECTABLE +
' ' + CLASS_CONTROL + ' ' +
(!isFullScreenSupported() ? CLASS_UNSUPPORTED : '');
//获取本控件的DOM对象句柄,并将按钮添加为这个DOM对象的子节点
const element = this.element;
element.className = cssClasses;
element.appendChild(this.button_);
//初始化另外两个属性
/**
* @private
* @type {boolean}
*/
this.keys_ = options.keys !== undefined ? options.keys : false;
/**
* @private
* @type {HTMLElement|string|undefined}
*/
this.source_ = options.source;
}
接下来是处理点击事件的回调函数:
/**
* @param {MouseEvent} event The event to handle
* @private
*/
handleClick_(event) {
event.preventDefault();
this.handleFullScreen_();
}
到这里,我们所需要的东西就基本上具备了,接下来就可以仿照写一个自己的MyControl控件了。
import Control from 'ol/control/Control';
import {CLASS_CONTROL, CLASS_UNSELECTABLE, CLASS_UNSUPPORTED} from 'ol/css';
import {listen} from 'ol/events';
import EventType from 'ol/events/EventType';
class MyControl extends Control {
constructor(opt_options) {
const options = opt_options ? opt_options : {};
super({
element: document.createElement('div'),
target: options.target
});
this.cssClassName_ = options.className !== undefined ? options.className :
'ol-full-screen';
const label = options.label !== undefined ? options.label : '\u00f7';
this.labelNode_ = typeof label === 'string' ?
document.createTextNode(label) : label;
this.button_ = document.createElement('button');
const tipLabel = options.tipLabel ? options.tipLabel : '点我';
this.button_.setAttribute('type', 'button');
this.button_.title = tipLabel;
this.button_.appendChild(this.labelNode_);
listen(this.button_, EventType.CLICK,
this.handleClick_, this);
const element = this.element;
const cssClasses = this.cssClassName_ + ' ' + CLASS_UNSELECTABLE +
' ' + CLASS_CONTROL;
element.className = cssClasses;
element.appendChild(this.button_);
}
handleClick_(event) {
event.preventDefault();
alert('Your control is online!');
}
}
使用的时候,做如下调用(下面的代码段省略了各种import):
let map = new Map({
controls: defaultControls().extend([
new MyControl()
]),
target: 'map',
layers: [
new TileLayer({
source: new XYZ({
url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png'
})
})
],
view: new View({
center: [0, 0],
zoom: 2
})
});
和原生风格完全一样,并且可重用性绝对OK。