在 OpenLayers 3 中,负责交互的模块中,有一个负责绘制图形的交互模块,这个交互子模块是 ol.interaction.Draw。该模块允许用户通过鼠标点击(PC浏览器环境)或者手指触摸( 触屏手机浏览器环境)在地图上绘制点、线 和 面,上一篇文章 中主要介绍了绘制功能的用法,这次我们看看其底层实现及其原理。
注:绘制完成之前的要素图形我们称之为草图(sketch),绘制完成的图形称为要素(feature)。
绘制交互是通过鼠标或手指的点击添加点的坐标的,OpenLayers 会自动将 屏幕坐标转换为 地图坐标,点击绘制完成后,所有的点初始化一个 geometry 对象,然后利用这个 geometry 初始化一个 feature。在上一篇介绍绘制用法时,我们知道初始化一个绘制交互,要传一个 features 参数或者 source 参数,如果是 features,那么勾绘完成的 feature 就会添加到 features 集合中,而 features 会属于一个 source,而 source 又属于 layer,layer 属于一个 map,这样,勾绘的要素(feature)就会显示到地图中。这个过程比较麻烦,估计会绕晕,我们用一张图进行说明:
勾绘交互原理流程图该类中,有几个私有变量,维护着绘制过程中的坐标和子要素或者绘制状态等内容,主要有以下几个:
ol.interaction.Draw 类中,私有的辅助函数包括:
ol.interaction.Draw.getMode_,获得绘制要素的 geometry 类型,包含四种:Point、LineString、Circle和Polygon。
ol.interaction.Draw.prototype.handlePointerMove_,处理鼠标移动事件;
我们看他的实现就会看得非常明白:
/** * Redraw the sketch features. * @private */
ol.interaction.Draw.prototype.updateSketchFeatures_ = function() {
var sketchFeatures = [];
if (this.sketchFeature_) {
sketchFeatures.push(this.sketchFeature_);
}
if (this.sketchLine_) {
sketchFeatures.push(this.sketchLine_);
}
if (this.sketchPoint_) {
sketchFeatures.push(this.sketchPoint_);
}
var overlaySource = this.overlay_.getSource();
overlaySource.clear(true);
overlaySource.addFeatures(sketchFeatures);
};
/** * @private */
ol.interaction.Draw.prototype.updateState_ = function() {
var map = this.getMap();
var active = this.getActive();
if (!map || !active) {
this.abortDrawing_();
}
this.overlay_.setMap(active ? map : null);
};
纵观所有这些辅助的私有函数,我们可以看到,基本的思路就是根据发生的事件,获取事件对象,获取对象包含的坐标,根据具体情况对以上提到的保存数据的六个私有变量进行修改,来达到目的。
对外的 API 函数有以下几个,这些对外的公有函数,也无外乎使用辅助的私有函数或者私有变量来完成一些具体的操作。如下,我们一一来进行介绍:
/** * Handles the {@link ol.MapBrowserEvent map browser event} and may actually * draw or finish the drawing. * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event. * @return {boolean} `false` to stop event propagation. * @this {ol.interaction.Draw} * @api */
ol.interaction.Draw.handleEvent = function(mapBrowserEvent) {
var pass = !this.freehand_;
if (this.freehand_ &&
mapBrowserEvent.type === ol.MapBrowserEvent.EventType.POINTERDRAG) {
this.addToDrawing_(mapBrowserEvent);
pass = false;
} else if (mapBrowserEvent.type ===
ol.MapBrowserEvent.EventType.POINTERMOVE) {
pass = this.handlePointerMove_(mapBrowserEvent);
} else if (mapBrowserEvent.type === ol.MapBrowserEvent.EventType.DBLCLICK) {
pass = false;
}
return ol.interaction.Pointer.handleEvent.call(this, mapBrowserEvent) && pass;
};
this.freehand_
默认是 false
,这里将 pass 赋值为 true
。首先检查是否设置了 freehand_ 模式,且检查发生的事件是否是鼠标拖拽事件,如果同时满足两个条件,就将事件对象中包含的坐标添加到当前正在绘制的多边形(因为只有多边形才有 freehand_ 模式),同时将 pass 设置为 false;如果不满足第一个条件,那么检查传来的事件是否是鼠标移动事件,如果是,调用的函数始终返回 true,但是会判断用户是否已经点击了最后一个点(finishCoordinate_),如果点击了,就将事件对象传递给 modifyDrawing_,如果没有定义,那么就将事件对象传递给 createOrUpdateSketchPoint_ ;如果前面两个条件都不满足,那么检查事件是否是鼠标双击事件,那么就将 pass 赋值为 false。
/** * Create a `geometryFunction` for `mode: 'Circle'` that will create a regular * polygon with a user specified number of sides and start angle instead of an * `ol.geom.Circle` geometry. * @param {number=} opt_sides Number of sides of the regular polygon. Default is * 32. * @param {number=} opt_angle Angle of the first point in radians. 0 means East. * Default is the angle defined by the heading from the center of the * regular polygon to the current pointer position. * @return {ol.interaction.DrawGeometryFunctionType} Function that draws a * polygon. * @api */
ol.interaction.Draw.createRegularPolygon = function(opt_sides, opt_angle) {
return (
/** * @param {ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>} coordinates * @param {ol.geom.SimpleGeometry=} opt_geometry * @return {ol.geom.SimpleGeometry} */
function(coordinates, opt_geometry) {
var center = coordinates[0];
var end = coordinates[1];
var radius = Math.sqrt(
ol.coordinate.squaredDistance(center, end));
var geometry = opt_geometry ? opt_geometry :
ol.geom.Polygon.fromCircle(new ol.geom.Circle(center), opt_sides);
goog.asserts.assertInstanceof(geometry, ol.geom.Polygon,
'geometry must be a polygon');
var angle = opt_angle ? opt_angle :
Math.atan((end[1] - center[1]) / (end[0] - center[0]));
ol.geom.Polygon.makeRegular(geometry, center, radius, angle);
return geometry;
}
);
};
从上面的定义可以看出,该函数返回的 geometry 是由两个点坐标构成的,分别是 center 和 end,之所以将end 赋值为 coordinate[1],是因为这是基于使用 Circle 模式为前提的,在这种模式下,只需要单击两次鼠标就可以完成绘制,所以 coordinate 数组中只有两个坐标,由于是 circle 模式,其半径是两点之间的距离;返回的函数中的第二个参数是 绘制的类型,必须是多边形;角度如果没有指定的话,那么就将两个坐标点的连线与水平(也就是东西走向)所成的角度。最后,将这些所需的参数都确定之后,利用这些参数调用了 ol.geom.polygon.makeRegular 函数,这个函数的主要作用是将 polygon 类型修改为一个规则多边形。
上面的解释比较难懂,我们举个例子来说明,我们首先初始化一个矢量图层和一个地图对象,并将该图层添加到地图中:
var vector = new ol.layer.Vector({
source: new ol.source.Vector()
});
var map = new ol.Map({
target: 'map',
layers: [ vector ],
view: new ol.View({
center: ol.proj.transform([120.896637,31.381207], 'EPSG:4326', 'EPSG:3857'),
zoom: 15
})
});
然后初始化一个 draw interaction,将其绘制模式设置为 Circle,source 设置为矢量图层的 source(vector.getSource( )),geometryFunction 设置为我们这里介绍的函数创建(ol.interaction.Draw.createRegularPolygon(3)),并将其加入到地图中:
var draw = new ol.interaction.Draw({
type: 'Circle',
source: vector.getSource(),
geometryFunction: ol.interaction.Draw.createRegularPolygon(3)
});
map.addInteraction(draw);
然后我们运行这个例子,效果如下:
边数为3(ol.interaction.Draw.createRegularPolygon(3))这里我们创建的是三条边的规则多边形(ol.interaction.Draw.createRegularPolygon(3)),自然就是三角形,我们将边数 3 换为 4 或者 5,效果如下:
边数为4(ol.interaction.Draw.createRegularPolygon(4)) 边数为5(ol.interaction.Draw.createRegularPolygon(5))如果继续增大边数,绘制的形状会越来越接近圆形。
/** * Remove last point of the feature currently being drawn. * @api */
ol.interaction.Draw.prototype.removeLastPoint = function() {
var geometry = this.sketchFeature_.getGeometry();
goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
'geometry must be an ol.geom.SimpleGeometry');
var coordinates, sketchLineGeom;
if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
coordinates = this.sketchCoords_;
coordinates.splice(-2, 1);
this.geometryFunction_(coordinates, geometry);
} else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
coordinates = this.sketchCoords_[0];
coordinates.splice(-2, 1);
sketchLineGeom = this.sketchLine_.getGeometry();
goog.asserts.assertInstanceof(sketchLineGeom, ol.geom.LineString,
'sketchLineGeom must be an ol.geom.LineString');
sketchLineGeom.setCoordinates(coordinates);
this.geometryFunction_(this.sketchCoords_, geometry);
}
if (coordinates.length === 0) {
this.finishCoordinate_ = null;
}
this.updateSketchFeatures_();
};
在管理绘制的这个类中,sketchFeature_ 是该类的私有变量,保存的是正在绘制的要素(feature)。该函数首先获取当前要素的 geometry 子对象,然后获取当前绘制的坐标数组,删除数组中的倒数第二个点(因为倒数第一个点还没有点定,是当前悬挂的点),然后重新构造 geometry 返回给当前绘制的要素(feature),并更新当前绘制的要素。
同样,我们举例说明该函数的效果。首先我们更改draw 对象的定义:
var draw = new ol.interaction.Draw({
type: 'Polygon',
source: vector.getSource(),
//geometryFunction: ol.interaction.Draw.createRegularPolygon(5)
});
然后我们在地图上添加一个按钮,用于触发该函数,如下:
$('#undo').bind('click', function(){
draw.removeLastPoint();
});
添加触发该函数的图标按钮
然后我们绘制一个多边形,然后点击按钮,取消绘制的前一个点,效果如下:
正在编辑的要素 点击按钮移除一个点 移除第二个点 移除第三个点。。。一直到把点全部取消:
移除倒数第二个点 移除最后一个点该函数可以用作 回退(undo) 操作。
/** * Stop drawing and add the sketch feature to the target layer. * The {@link ol.interaction.DrawEventType.DRAWEND} event is dispatched before * inserting the feature. * @api */
ol.interaction.Draw.prototype.finishDrawing = function() {
var sketchFeature = this.abortDrawing_();
goog.asserts.assert(!goog.isNull(sketchFeature),
'sketchFeature should not be null');
var coordinates = this.sketchCoords_;
var geometry = sketchFeature.getGeometry();
goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
'geometry must be an ol.geom.SimpleGeometry');
if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
// remove the redundant last point
coordinates.pop();
this.geometryFunction_(coordinates, geometry);
} else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
// When we finish drawing a polygon on the last point,
// the last coordinate is duplicated as for LineString
// we force the replacement by the first point
coordinates[0].pop();
coordinates[0].push(coordinates[0][0]);
this.geometryFunction_(coordinates, geometry);
}
// cast multi-part geometries
if (this.type_ === ol.geom.GeometryType.MULTI_POINT) {
sketchFeature.setGeometry(new ol.geom.MultiPoint([coordinates]));
} else if (this.type_ === ol.geom.GeometryType.MULTI_LINE_STRING) {
sketchFeature.setGeometry(new ol.geom.MultiLineString([coordinates]));
} else if (this.type_ === ol.geom.GeometryType.MULTI_POLYGON) {
sketchFeature.setGeometry(new ol.geom.MultiPolygon([coordinates]));
}
// First dispatch event to allow full set up of feature
this.dispatchEvent(new ol.interaction.DrawEvent(
ol.interaction.DrawEventType.DRAWEND, sketchFeature));
// Then insert feature
if (!goog.isNull(this.features_)) {
this.features_.push(sketchFeature);
}
if (!goog.isNull(this.source_)) {
this.source_.addFeature(sketchFeature);
}
};
该函数和上面的函数类似,首先获得当前绘制的要素(var sketchFeature = this.abortDrawing_();
)、geometry对象和坐标,然后删除最后一个悬挂的点,重新返回新的 geometry,并传给 feature,以完成绘制。同样,我们举例说明,这个例子和上面的例子几乎一模一样,所以不再详述实现,对勾图标对应调用该函数。实现效果如下:
总结一下,不仅仅是这个类,还包括其他的类都是这个思路,总体分为三层:私有变量、私有辅助函数和对外的 API 函数,其功能如下图:
类设计分层图不仅仅是 OpenLayers 的设计,其他库的设计也大致是这样;不仅仅是 JavaScript ,很多其他的语言的类设计也是这样。其实这就是面向对象的思想的体现 - 封装。
这篇文章中我们深入分析了 ol.interaction.Draw 的实现原理、实现细节,从上面的分析可以看到,大部分实现是依靠几个私有变量记录绘制的数据,并对其做一些操作的。除了这些功能,我们当然可以根据其私有变量 sketchFeature_, sketchCoords_, sketchLine_, sketchLineCoords_ 等,进行定制化的函数开发,做一些定制性的需求或者对其进行扩展。
看完有什么疑问可以对文章进行评论 或者 发邮件我 [email protected]