iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 14
0
Modern Web

《你的地圖會說話? WebGIS與JavaScript的情感交織》系列 第 14

[5-3] 點線面的接口 - 以配接器模式 Adapter Design Pattern 重構

本篇文章請搭配
[5-1] 環域與繪圖工具 - 以Leaflet Draw實現
[5-2] Callback & Promise - 解決request非同步的四種解法


讓我們回顧前天講到的繪圖工具,根據畫不同的圖形,
而有不同後續處理的方式,並且用if...else...去判斷。

        LMap.on(L.Draw.Event.CREATED, function (e) {
            var layer = e.layer;
            var type = e.layerType;
            drawItem.addLayer(layer);
            //console.log(type)
            //console.log(arguments)

            if (type === 'circle') {
                var center = layer.getLatLng();
                var radius = layer.getRadius();
                console.log(`經度: ${center.lng}, 緯度: ${center.lat}`);
                console.log(`半徑: ${radius} (m)`);
                
            } else if (type === 'marker') {
                var point = layer.getLatLng();
                console.log(`經度: ${point.lng}, 緯度: ${point.lat}`);
                
            } else if (type === 'rectangle') {
                var str = "";
                var arr = layer.getLatLngs();
                arr = arr[0].forEach(function (item, index) {
                    str += `${index} => 經度: ${item.lng}, 緯度: ${item.lat}`
                });
                console.log(e.layer.toGeoJSON());
                console.log(str);

            } else if (type === 'polygon') {
                var str = "";
                var arr = layer.getLatLngs();
                arr = arr[0].map(function (item, index) {
                    return {
                        x: item.lng,
                        y: item.lat
                    }
                });

                console.log(arr);
            }
        });

如果後續要做的事情很多,
昨天有教大家callback或ES6 Promise的方法,將程式分離出去。
可是根據不同的幾何圖形,每次都要重新看一次callback的arguments,
並且從中找到可以使用的資料。
如果今天API突然改版,資料回傳的格式變了呢?
如果今天老闆說畫圓形要用這個API,可是要用另一個API提供的畫扇形,
要怎麼整合並因應不同API資料儲存格式的差異?
就讓我們用Adapter Design Pattern試著來重構吧!

繪圖器物件

↓ 首先先建立一個繪圖器類別,由它建立的物件可以拿來繪圖。

        function Drawing(map, drawingMode, option = {}) {
            if (!(this instanceof Drawing)) {  // 如果沒有用new呼叫,幫他new
                return new Drawing(map, drawingMode, option);
            }
            this.map = map;
            this.drawingMode = drawingMode;
            this.option = option;
        }
  • map 決定繪圖的地圖物件
  • drawingMode 決定要畫什麼樣的圖形(Ex: "Circle"、"Marker"、"Polygon"、"Polyline"、"Rectangle")
  • option 決定繪圖的設定
        Drawing.prototype.Start = function (completeCallback) {
            const el = this;

            if (L.Draw[this.drawingMode] instanceof Function) {
                this.drawing = new L.Draw[this.drawingMode](this.map, this.option);

                this.map.off('draw:created').on('draw:created', function (e) {
                    e.layer.addTo(el.map);
                    completeCallback(e);
                });
                this.drawing.enable();

            } else {
                console.log(new Error('invalid drawingMode.'));
            }
        }

↑ 建立一個原型方法Start,繪圖開始。

這裡有幾個重點

  • 用L.Draw[this.drawingMode]的方法來新建Leaflet Draw的繪圖器,並且先用L.Draw[this.drawingMode] instanceof Function來判斷drawingMode是否合理。
  • 建立'draw:created'事件前要先off事件,不然每次呼叫都會建立一個事件,事件就會被觸發很多次。
  • 用this.drawing.enable()開始繪圖!
  • 繪圖完畢時以completeCallback來做後續的動作

Adapter Design Pattern

配接器模式用以解決兩個介面不相容的問題,就好像是萬用插座一樣,不論是兩孔、三孔還是八字型插頭都可以輕鬆轉接。而建立這個配接器(轉接頭),首先要先定義通用的介面。

  • 通用介面

// 以物件陣列方式儲存點座標
var pointList = [{ x: 121.5, y: 24 }, { x: 121.2, y: 23.8 }, { x: 121, y: 23.5 }];

// 以物件的方式儲存中心點及圓心
var obj = { x: 121, y: 23, radius: 1000 };

// Geojson

定義完通用介面後,接者寫轉接的方式,針對不同的介面進行轉接,轉成通用介面。
這些轉接的方法稱為Adaptee,未來有新的介面出現時,只要寫新的Adaptee就能輕鬆轉接。

  • Adaptee

        var adaptee = {  // 配接器,轉換不同型式介面
            // TGOS.TGLine 轉為 pointList
            TGLineToPointList: function (e) {  
                console.log(e.overlay.getPath());
                var pointList = e.overlay.getPath().path.map(function (item) {
                    return {
                        x: item.x,
                        y: item.y
                    }
                });

                return pointList;
            },
            // TGOS.TGPolygon 轉為 pointList
            TGPolygonToPointList: function (e) {  
                console.log(e.overlay.getPath().rings_[0].linestring);
                var pointList = e.overlay.getPath().rings_[0]
                        .linestring.path.map(function (item) {
                    return {
                        x: item.x,
                        y: item.y
                    }
                });

                return pointList;
            },
            // TGOS.TGCircle 轉為 Object
            TGCircleToObject: function (e) {  
                var tgCircle = e.overlay.getPath();
                return {
                    x: tgCircle.getCenter().x,
                    y: tgCircle.getCenter().y,
                    radius: tgCircle.getRadius()
                };
            },
            // Leaflet Circle 轉為 Object
            LCircleToObject: function (e) {  
                var layer = e.layer;
                return {
                    x: layer.getLatLng().lng,
                    y: layer.getLatLng().lat,
                    radius: layer.getRadius()
                };
            },
            // Leaflet Polygon 轉為 pointList
            LPolygonToPointList: function (e) {  
                var pointList = e.layer.getLatLngs();
                pointList = pointList[0].map(function (item, index) {
                    return {
                        x: item.lng,
                        y: item.lat
                    }
                });

                return pointList;
            },
            // Leaflet marker 轉為 Object
            LMarkerToObject: function (e) {  
                return {
                    x: e.layer.getLatLng().lng,
                    y: e.layer.getLatLng().lat,
                };
            },
            // Leaflet Rectangle 轉為 pointList
            LRectangleToPointList: function (e) {  
                var pointList = e.layer.getLatLngs();
                pointList = pointList[0].map(function (item, index) {
                    return {
                        x: item.lng,
                        y: item.lat
                    }
                });

                return pointList;
            },
            // Leaflet Rectangle 轉為 Geojson
            LRectangleToGeojson: function (e) {  
                return e.layer.toGeoJSON();
            },
        };
  • Adapter

建立好Adaptee後,接著新增共用的轉接方法來使用。

        Drawing.prototype.StartWithAdapter = function (adapter, completeCallback) {
            adapter = adapter instanceof Function ? 
                        adapter : function (e) {
                console.log(new TypeError('adapter is not a Function.'));
                return e;
            }
            this.Start(function (e) {
                completeCallback(adapter(e));
            });
        }

建立Drawing的原型方法StartWithAdapter

  • 先判斷adaptee是否為Function,如果是才繼續動作。

  • 呼叫this.Start(),會呼叫到剛剛建立的原型方法Start,並且把completeCallback函式傳入。

  • 呼叫

1. 先用沒有adaper的方法呼叫

        var drawing = new Drawing(LMap, 'Circle', {});
        drawing.Start(function () {
            console.log(arguments)
        })

↓ 畫圓
https://ithelp.ithome.com.tw/upload/images/20200929/20130604nGabUWYIcn.jpg

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20200929/201306042Iby1Ot6Un.jpg
可以看到callback function回傳的是Leaflet的物件,就還需額外處理這個物件取出我們要的資訊。

2. 使用adaper的方法呼叫

        drawing.StartWithAdapter(adaptee.LCircleToObject, function () {
            console.log(arguments)
        });

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20200929/201306048PV0mI05v9.jpg
回傳結果為我們定義的介面,有經度、緯度以及半徑。

Adapter with Promise

既然昨天介紹了ES6 Promise物件,那我們就改用Promise的方式來改寫配接器模式吧!

  • Start方法回傳Promise物件

↓ Adaptee不變,首先先來改寫Start方法。

        Drawing.prototype.Start = function () {
            const el = this;
            return new Promise(function (resolve, reject) {

                if (L.Draw[el.drawingMode] instanceof Function) {
                    el.drawing = new L.Draw[el.drawingMode](el.map, el.option);
                    el.map.off('draw:created').on('draw:created', function (e) {
                        e.layer.addTo(el.map);
                        resolve(e);
                    });
                    el.drawing.enable();

                } else {
                    console.log(new Error('invalid drawingMode.'));
                    reject(new Error('invalid drawingMode.'));
                }
            });
        }

讓Drawing的原型方法Start改為return一個Promise物件,並且加入resolve(e)、reject('error')來處理成功以及失敗的回調函式。注意!這邊要用const el = this;先把this寫入一個變數,否則在Promise內的this會指向Promise物件。

  • 改寫Adapter方法

        Drawing.prototype.StartWithAdapter = function (adapter) {
            const el = this;
            return new Promise(function (resolve, reject) {
                adapter = adapter instanceof Function ? adapter : function (e) {
                    console.log(new TypeError('adapter is not a Function.'));
                    return e;
                }
                el.Start().then((e) => resolve(adapter(e)));
            });
        }

Drawing的原型方法StartWithAdapter也return一個Promise物件,並在裡面呼叫原型方法Start,並用.then接續進行執行adapter的回呼。

  • 呼叫

1. 首先先用沒有adaper的方法呼叫

        var drawing = new Drawing(LMap, 'Rectangle', {});

        drawing.Start().then(res => {
            console.log(1);
            return res;
        }).then(res => {
            console.log(2);
            return res;
        }).then(res => console.log(res));

↓ 畫長方形
https://ithelp.ithome.com.tw/upload/images/20200929/20130604ZHOeSgGK2u.jpg

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20200929/20130604aKavQeh0Ol.jpg
可以看到Promise物件會依照.then依序進行後續動作;
沒有使用adapter回傳的是Leaflet的物件,還需額外處理這個物件取出我們要的資訊。

2. 使用adaper的方法呼叫

↓ 一行解決,乾淨俐落

drawing.StartWithAdapter(adaptee.LRectangleToGeojson).then(res => console.log(res));

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20200929/2013060433O8LYnhHt.jpg
回傳為我們指定的Geojson格式。


今天簡單介紹了配接器模式,
配接器模式最著名的其實是WebRTC,
大家有空可以去 github 拜讀一下大神們寫的Source Code。
明天繼續努力!/images/emoticon/emoticon12.gif


上一篇
[5-2] Callback & Promise - 解決request非同步的四種解法
下一篇
[5-4] 環域查詢 - 完結篇
系列文
《你的地圖會說話? WebGIS與JavaScript的情感交織》30

尚未有邦友留言

立即登入留言