本系列文章已出版實體書籍:
「你的地圖會說話?WebGIS 與 JavaScript 的情感交織」(博碩文化)
WebGIS啟蒙首選✖五家地圖API✖近百個程式範例✖實用簡易口訣✖學習難度分級✖補充ES6小知識
本篇文章請搭配
[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;
}
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,繪圖開始。
這裡有幾個重點
配接器模式用以解決兩個介面不相容的問題,就好像是萬用插座一樣,不論是兩孔、三孔還是八字型插頭都可以輕鬆轉接。而建立這個配接器(轉接頭),首先要先定義通用的介面。
// 以物件陣列方式儲存點座標
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就能輕鬆轉接。
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();
},
};
建立好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)
})
↓ 畫圓
↓ 結果可以看到callback function回傳的是Leaflet的物件,就還需額外處理這個物件取出我們要的資訊。
2. 使用adaper的方法呼叫
drawing.StartWithAdapter(adaptee.LCircleToObject, function () {
console.log(arguments)
});
↓ 結果回傳結果為我們定義的介面,有經度、緯度以及半徑。
既然昨天介紹了ES6 Promise物件,那我們就改用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物件。
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));
↓ 畫長方形
↓ 結果可以看到Promise物件會依照.then依序進行後續動作;
沒有使用adapter回傳的是Leaflet的物件,還需額外處理這個物件取出我們要的資訊。
2. 使用adaper的方法呼叫
↓ 一行解決,乾淨俐落
drawing.StartWithAdapter(adaptee.LRectangleToGeojson).then(res => console.log(res));
↓ 結果
回傳為我們指定的Geojson格式。
今天簡單介紹了配接器模式,
配接器模式最著名的其實是WebRTC,
大家有空可以去 github 拜讀一下大神們寫的Source Code。
明天繼續努力!