前言
在完成如何利用Leaflet插件进行点线面要素的添加与编辑后,紧接着的问题就是如何保存并再一次初始化时加载这些点线面要素,这里借助另一专栏中 面向对象的TypeScript-序列化与反序列化(1) 一文提到相关序列化与反序列化方法,将空间数据保存到MongoDB。
Code:https://github.com/shengzheng1981/learn-ts-oop
Demo:https://shengzheng1981.github.io/learn-ts-oop/chapter6
下图是本文完成效果:
改进路线
由于本文是在上一篇以及上述提及的序列化一文,这两篇基础上的改进,故有关如何序列化对象以及Leaflet插件Leaflet-Editable如何调用及工作的内容,此处不再赘述。本文重点关注:
1.当前工具标记
2.编辑事件的侦听与抛出
3.对象与空间要素的连接
做好这三点就可以完成对上一篇Simple Editor的改进。
当前工具标记
如果做过ArcGIS Engine开发的同仁,应该知道Engine是通过currentTool,即当前工具标记来告知地图点击响应事件逻辑该做什么,例如默认情况下地图单击事件也许是进行空间要素选择,但在设置为添加点时,单击事件的逻辑应该要走添加点的分支。当你需要做复杂的编辑工具条时,就需要来标记当前正在使用什么工具(Tool)。
题外话:顺带一提ArcGIS Engine的ICommand和ITool,这两个接口的区分其实很简单,ICommand是即时发生的命令,不会监听地图鼠标相关事件,例如全图(FullExtent),上一视图、下一视图、固定比例的放大;ITool则会监听地图鼠标相关事件,例如漫游(Pan)、框选放大、选择与查询(Select Identify)。
本篇中当前工具标记的用途:
//选择
select() {
if (this.map.editTools.drawing()) {
this.map.editTools.stopDrawing();
}
this.currentTool = "Select";
}
//画点
startMarker() {
this.currentTool = "Marker";
const marker = this.map.editTools.startMarker(undefined, {
icon: new Icon({
iconUrl: "assets/img/marker/" + (this.option.style.icon || "marker.svg"),
iconSize: [28, 28],
iconAnchor: [14, 14]
})
});
marker.on('dblclick', (event) => {
this.editable && marker.toggleEdit();
});
}
目的一目了然,区别在与地图交互时,是走选择逻辑还是走画点逻辑。
编辑事件的侦听与抛出
做编辑器Editor最难的三点,1.序列化与反序列化(即Load&Save),2.重做与撤销(即Redo与Undo),3.事件的侦听与抛出(Angular的On&Emit)。
这里先说明下,空间要素编辑器做到前端本身就较为复杂,原因上一篇已做说明,而在Leaflet下做更为难受,为什么?这里说说Leaflet这个轻量级API最大的问题:这个API各个方面都让人感觉最初的设计人不是GIS科班的,而是个PhotoShop制图出身。GIS里Layer的概念定义非常重要,可以参见我在简述GIS中空间数据组织的基本思想? 的回答,分层很重要,图层Layer是同一类具有相同属性要素的集合。而Leaflet里的Layer是一个什么概念呢,几乎和PhotoShop里是一个概念,Layer承担了GIS里Feature的概念,一个Marker是一个Layer?一个Polyline是一个Layer?一个Polygon是一个Layer?这种设计不是说有错,但在逻辑上以及后期一些功能的实现上带来了诸多不便。
牢骚过后,回到正题。受限与采用的Leaflet-Editable插件的事件机制,本篇的事件侦听与抛出,只能点到为止,即为实现功能而实现功能,却并非为最佳设计。代码如下:
this.map.editTools.on("editable:drawing:commit", (e) => {
if (this.currentTool == "Marker" || this.currentTool == "Polyline" || this.currentTool == "Polygon") {
e.layer.disableEdit();
e.layer.created = true;
this.currentTool = "Select";
this.edited = true;
}
});
this.map.editTools.on("editable:editing", (e) => {
e.layer.updated = true;
this.edited = true;
});
参考该插件的API:http://leaflet.github.io/Leaflet.Editable/doc/api.html
editable:drawing:commit发生在Fired when user finish drawing a feature.
editable:editing发生在Fired as soon as any change is made to the feature geometry.
editable:drawing:commit事件用于新增点线面事件的侦听,而editable:editing用于编辑已有要素的侦听。同时editable:editing事件是一个连续型事件,即鼠标编辑时一有移动就会激发,故此中逻辑一定要简之又简。
此外,本文设计两种抛出事件模式:1.单个要素编辑后抛出,2.所有编辑要素打包一起抛出。
save() {
this.map.editTools.featuresLayer.eachLayer((layer) => {
if(layer.created) {
this.onCreated.emit(layer);
} else if (layer.updated) {
this.onUpdated.emit(layer);
}
layer.disableEdit();
});
this.deleteArray.forEach((layer) => {
this.onDeleted.emit(layer);
});
this.edited = false;
}
save2() {
const changedArray = [];
this.map.editTools.featuresLayer.eachLayer((layer) => {
if(layer.created) {
changedArray.push(layer);
} else if (layer.updated) {
changedArray.push(layer);
}
layer.disableEdit();
});
this.deleteArray.forEach((layer) => {
changedArray.push(layer);
});
this.onSave.emit(changedArray);
this.edited = false;
}
无论什么方式,在Web端进行空间要素的编辑与保存,都或多或少存在问题,如果只进行一些简单Marker的维护,没问题可以;但如果说要类似桌面端的编辑,那就非常困难了。
对象与空间要素的连接
序列化对象与Leaflet中空间要素(Layer)的关联,办法有很多,这里利用js这门动态语言的优势,做了个最简单id关联。id关联的作用就是在抛出事件后,保存时找到对应的对象。
//加载要素到地图(来自反序列化)
loadFeatures(features){
Array.isArray(features) && features.forEach(feature => {
if (feature.geometry) {
if(feature.geometry.type === 'Point'){
const point = feature.geometry.coordinates;
const marker = new Marker([point[1],point[0]],{
icon: new Icon({
iconUrl: "assets/img/marker/" + (this.option.style.icon || "marker.svg"),
iconSize: [28, 28],
iconAnchor: [14, 14]
})
});
marker.on('dblclick', (event) => {
this.editable && marker.toggleEdit();
});
//id关联
marker._id = feature._id;
this.map.editTools.featuresLayer.addLayer(marker);
}
if(feature.geometry.type === 'LineString'){
const swap = feature.geometry.coordinates.map( point => [point[1],point[0]] );
const polyline = new Polyline(swap, {
color: this.option.style.color || '#ff0000',
opacity: this.option.style.fillOpacity || 1,
weight: 4,
clickTolerance: 6
});
polyline.on('dblclick', (event) => {
this.editable && polyline.toggleEdit();
});
//id关联
polyline._id = feature._id;
this.map.editTools.featuresLayer.addLayer(polyline);
}
if(feature.geometry.type === 'Polygon'){
const swap = feature.geometry.coordinates.map( ring => ring.map( point => [point[1],point[0]])) ;
const polygon = new Polygon(swap, {
color: this.option.style.color || '#ff0000',
fillColor : this.option.style.fillColor || '#ff0000',
fillOpacity : this.option.style.fillOpacity || 1
});
polygon.on('dblclick', (event) => {
if (this.editable) {
polygon.toggleEdit();
}
});
//id关联
polygon._id = feature._id;
this.map.editTools.featuresLayer.addLayer(polygon);
}
}
});
}
题外话:很多人说JavaScript相比较与Java以及.Net一族的静态语言,动态语言是劣等语言,是要淘汰的,在设计时根本无法发现大量错误,尤其当TypeScript出现后,用JavaScript编程的空间再度被压缩。但是这样说话,本人举双手双脚反对,动态语言有劣势很明显,但动态语言的优点也显而易见。因为我是从.Net桌面端和后端开发出身,所以一直从事.Net面向对象的编程工作,当我一开始转到JavaScript前端开发时,是极度不适应的,觉得动态脚本语言简直就是个玩具,这TM能用于开发,加上当时JavaScript模块化一团糟,三个框架都未诞生。随着模块化、框架、Nodejs的迅猛发展,逐步地,我习惯了函数响应式编程,习惯了一些动态语言的trick。有时,说实话,动态语言更加灵活,比如,对象的一些界面化状态,其实从面向对象的角度来说应该不是该对象的属性,如Checked、Expand、Collapsed等等,而这些用动态语言,你是不用像静态语言那样为此而去添加定义的。
再题外,TypeScript的出现,我会用面向对象的思想重构我的前端,但绝不是全部。有人以为用了Angular框架,框架下的编程就是TypeScript了。这TM简直就是掩耳盗铃,自欺欺人,TypeScript是一个超集,所以它会包容你原先的JS,但你如果不在一些编程思想进行转变,而是说:”看我的后缀名是TS“。我想Angular,会被你气吐血吧。
结语
综上,一句话,在前端做空间要素的编辑,很困难,很复杂,目前可以做简单点线面编辑,更多地适用于简单Marker的编辑与维护。
题外-插件Leaflet-Editable的扩展
由于该插件在Marker双击编辑时,编辑状态的交互不友好,以下做了一些改进,可找到源文件对应处进行覆盖:
// namespace Editable; class MarkerEditor; aka L.Editable.MarkerEditor
// inherits BaseEditor
// Editor for Marker.
L.Editable.MarkerEditor = L.Editable.BaseEditor.extend({
vertex: null,
initialize: function (map, feature, options) {
L.Editable.BaseEditor.prototype.initialize.call(this, map, feature, options);
},
addHooks: function () {
L.Editable.BaseEditor.prototype.addHooks.call(this);
if (this.feature) this.initVertexMarker();
return this;
},
onFeatureAdd: function () {
this.tools.editLayer.addLayer(this.editLayer);
//if (this.feature.dragging) this.feature.dragging.enable();
},
initVertexMarker: function () {
if (!this.enabled()) return;
const latlng = this.feature._latlng;
this.vertex = new this.tools.options.vertexMarkerClass(latlng, [latlng], this);
this.vertex.on('dblclick', () => {
this.disable();
});
},
reset: function () {
this.editLayer.clearLayers();
this.initVertexMarker();
},
onDrawingMouseMove: function (e) {
L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e);
if (this._drawing) {
this.feature.setLatLng(e.latlng);
}
},
refresh: function () {
this.feature.setLatLng(this.vertex.latlng);
this.onEditing();
},
onVertexMarkerClick: function (e) {
this.fireAndForward('editable:vertex:click', e);
},
onVertexMarkerMouseDown: function (e) {
// namespace Editable
// section Vertex events
// event editable:vertex:mousedown: VertexEvent
// Fired when user `mousedown` a vertex.
this.fireAndForward('editable:vertex:mousedown', e);
},
onVertexMarkerMouseOver: function (e) {
// namespace Editable
// section Vertex events
// event editable:vertex:mouseover: VertexEvent
// Fired when a user's mouse enters the vertex
this.fireAndForward('editable:vertex:mouseover', e);
},
onVertexMarkerMouseOut: function (e) {
// namespace Editable
// section Vertex events
// event editable:vertex:mouseout: VertexEvent
// Fired when a user's mouse leaves the vertex
this.fireAndForward('editable:vertex:mouseout', e);
},
onVertexMarkerDrag: function (e) {
this.onMove(e);
// namespace Editable
// section Vertex events
// event editable:vertex:drag: VertexEvent
// Fired when a vertex is dragged by user.
this.fireAndForward('editable:vertex:drag', e);
},
onVertexMarkerDragStart: function (e) {
// namespace Editable
// section Vertex events
// event editable:vertex:dragstart: VertexEvent
// Fired before a vertex is dragged by user.
this.fireAndForward('editable:vertex:dragstart', e);
},
onVertexMarkerDragEnd: function (e) {
// namespace Editable
// section Vertex events
// event editable:vertex:dragend: VertexEvent
// Fired after a vertex is dragged by user.
this.fireAndForward('editable:vertex:dragend', e);
this.commitDrawing(e);
},
processDrawingClick: function (e) {
// namespace Editable
// section Drawing events
// event editable:drawing:clicked: Event
// Fired when user `click` while drawing, after all internal actions.
this.fireAndForward('editable:drawing:clicked', e);
this.commitDrawing(e);
},
connect: function (e) {
// On touch, the latlng has not been updated because there is
// no mousemove.
if (e) {
this.feature._latlng = e.latlng;
}
L.Editable.BaseEditor.prototype.connect.call(this, e);
}
});