leaflet加载geojson数据_进阶-Leaflet-点线面要素的序列化与反序列化(三)

本文详细介绍了如何在Leaflet中使用Leaflet-Editable插件进行点线面要素的编辑,并探讨了如何保存和重新加载这些要素。文章关注了当前工具标记、编辑事件的侦听与抛出以及对象与空间要素的连接,还分享了针对Leaflet-Editable插件的改进,以优化编辑过程中的用户体验。同时,文章讨论了JavaScript与静态语言的差异,强调了动态语言在特定场景下的优势。
摘要由CSDN通过智能技术生成

3fe087ffbc4aa7473504f3a3a0e98c18.png

前言

在完成如何利用Leaflet插件进行点线面要素的添加与编辑后,紧接着的问题就是如何保存并再一次初始化时加载这些点线面要素,这里借助另一专栏中 面向对象的TypeScript-序列化与反序列化(1) 一文提到相关序列化与反序列化方法,将空间数据保存到MongoDB。

Code:https://github.com/shengzheng1981/learn-ts-oop

Demo:https://shengzheng1981.github.io/learn-ts-oop/chapter6

下图是本文完成效果:

a2fd17f63dd4a83639e2586a4689ae5a.png

改进路线

由于本文是在上一篇以及上述提及的序列化一文,这两篇基础上的改进,故有关如何序列化对象以及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);
        }

    });
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值