Fetch APIのStream機能を使ってデータを読み込みながら地図を描画する
地理空間データはどうしてもデータサイズが大きくなりがちです。
通常のデータ読み込みでは、読み込みが終わるまで地図の描画を始めることができないのですが、Fetch APIのReadableStreamを使うことで、「一部データを読み込んでは地図の一部分を描画する」という分割したレンダリングを実装することができます。
サンプルコード
約50MBのポリゴンデータを読み込みながら逐次描画しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
//地理データをstreamを使って読み込む fetch("city.txt").then((response) => { const reader = response.body.getReader(); const stream = new ReadableStream({ start(controller) { function push() { reader.read().then(({ done, value }) => { if (done) { controller.close(); draw(""); console.log("Stream End") return; } const chunk = new TextDecoder("utf-8").decode(value); draw(chunk); controller.enqueue(value); push(); }); }; push(); } }); return new Response(stream, { headers: { "Content-Type": "text/html" } }); }); //chankを溜めとくバッファ let buffer = ""; //地図を描画するキャンバス const canvas = d3.select("canvas") const ctx = canvas.node().getContext('2d'); //投影法の設定 const projection = d3.geoMercator() .scale(4000) .translate([960,500*2]) .center([139.0032936, 36.3219088]); //パスジェネレーターを作成 const geoPath = d3.geoPath(projection) .context(ctx); //地図描画 function draw(chunk){ buffer += chunk; //改行コードが見つからない場合は次に回す if(buffer.indexOf("n") < 1) return; //改行コードを元にバッファを分割 const block = buffer.split("n"); //分割したデータの末尾を取り出し。 const last = block.pop(); //分割したバッファをオブジェクトに変換 const jsons = block.map(JSON.parse); //末尾データに改行コードが含まれているかチェック if(last.indexOf("n") < 0){ buffer = last; //改行コードがない場合は次に回す }else{ if(last) jsons.push(JSON.parse(last)); //改行コードが含まれている場合はオブジェクトに変換 } //変換したJデータを使ってcanvasに地図を描画する jsons.forEach(d => { ctx.beginPath(); geoPath(d); ctx.stroke(); ctx.closePath(); }); } |
ブラウザ対応
推奨環境
- 最新版 Chrome
- 最新版 Safari Mobiled
Fetch API - Web API インターフェイス | MDN
とりあえず、IE、Firefoxでは動きません。IEはそもそもFetch APIに未対応、FirefoxはReadableStreamのResponse bodyが未サポートのためです。
* 最新版のFirefoxでは動作するようになりました。
chromeは問題無し。また、MDNのページではSafari, Safari Mobileが未サポートとなっていますが、手元の環境(Safari 11.1 / iOS 11.3.1)で確認したところどちらも問題なく動作しました。
一部解説
GeoJSONファイルの分割
ストリームで使いやすいように、GeoJSONをfeatures単位で分割し一行ごとのJSONとしてテキストファイルに保存しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const fs = require('fs'); const converter = require('json-2-csv'); const file = process.argv[2]; const geojson = JSON.parse(fs.readFileSync(file, 'utf8')); let features = []; const len = geojson.features.length; for(let i=0; i < len; i++){ features.push(JSON.stringify(geojson.features[i])) } console.log(features.join("n")); |
1 |
node geojson2txt.js japan_ver81.geojson > japan.txt |
Streeming APIで読み込む
Streamから受けっとたchunkをbufferに溜め、改行コードを目印に分割してJSONを読み込み元のオブジェクトに変換しています。
ReadableStream - Web APIs | MDN
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
fetch("japan.txt").then(response => { const reader = response.body.getReader(); const stream = new ReadableStream({ start(controller) { function push() { reader.read().then(({ done, value }) => { if (done) { controller.close(); draw(""); console.log("Stream End") return; } //文字列に変換 const chunk = new TextDecoder("utf-8").decode(value); draw(chunk); controller.enqueue(value); push(); }); }; push(); } }); return new Response(stream, { headers: { "Content-Type": "text/html" } }); }); |
地図を描画する(SVG)
chunkから取り出したJSONをD3で描画するコードです。
しかしデータ量が多いとSVGエレメント数も比例して多くなるので、この方法は非推奨です。
詳しくは「地図を描画する(Canvas)」を参照してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
let buffer = ""; const projection = d3.geoMercator() .center([138, 35]) .scale(2000) const geoPath = d3.geoPath(projection); const map = d3.select("svg").append("g"); map.attr("transform", "translate(300, 300)"); function draw(chunk){ buffer += chunk; //改行コードが含まれているかチェック if(buffer.indexOf("n") < 1) return; //改行コードで文字列を分割する const block = buffer.split("n"); //文字列の末尾を取り出し const last = block.pop(); //末尾以外をオブジェクトに変換 const jsons = block.map(JSON.parse); //末尾に改行コードが含まれているかチェク if(last.indexOf("n") < 0){ buffer = last; //改行コードがない場合は次のchankを待つ }else{ if(last) jsons.push(JSON.parse(last)); //改行コードが含まれている場合はオブジェクトに変換して追加 } //svgで地図を描画 jsons.forEach(d => { map.append("path").attr("d", geoPath(d)); }); } |
地図を描画する(canvas)
chunkから取り出したJSONをD3でcanvasに描画するコードです。
この方法だと、データ量の多い地理情報もある程度まで描画することができます。
これ以上のサイズになるとWebGLなどを利用する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
const canvas = d3.select("canvas") const ctx = canvas.node().getContext('2d'); const projection = d3.geoMercator() //投影法の指定 .scale(4000) //スケール(ズーム)の指定 .translate([960,500*2]) //表示位置調整 .center([139.0032936, 36.3219088]); //中心の座標を指定 const geoPath = d3.geoPath(projection) .context(ctx); function draw(chunk){ buffer += chunk; if(buffer.indexOf("n") < 1) return; const block = buffer.split("n"); const last = block.pop(); const jsons = block.map(JSON.parse); if(last.indexOf("n") < 0){ buffer = last; }else{ if(last) jsons.push(JSON.parse(last)); } jsons.forEach(d => { ctx.beginPath(); geoPath(d); ctx.stroke(); ctx.closePath(); }); } |