2014年9月12日金曜日

OpenLayers 3 を使ってみよう(その8:ol.interaction.Draw 修正可能バージョンで表示領域の大きさの調整を行う)

これはOpenLayers 3 を使ってみよう(その7:ol.interaction.Modify を使った修正可能バージョン)からの続きである。
OpenLayers 3 を使ってみよう(その0:はじめに:地理院地図を表示)に目次がある。
ここでは OpenLayers 3.7.0 を使っている。
 前回(その7:ol.interaction.Modify を使った修正可能バージョン)では, ol.interaction.Modify を使って,マウスで経路線を修正する方法に挑戦してみた。
今回は,点の数を変えたり,文字データを読み込んで線を描画する際に,地図の表示範囲を自動で変更できるようにしてみた。

 地図の表示範囲の自動調整はその5:テキストデータから折線データ読込みでも書いた。 基本的には同じことをさせている。 ただし,座標データがメルカトル座標になっているなど微妙な違いはあるが,本質的には同じことをさせている。 そこでは,ヒュベニの公式を用いた距離計算を行い,経路が収まるように地図の zoom を自動で調整している。 その5:テキストデータから折線データ読込みでも書いたが, ol.geom.LineString クラスの変数を作って,その長さを求める関数 getLength() を使う方法は正確な距離を出さないので,ここでは利用していない。

 さっそく,いつものように web ページのソースを載せよう。 今回も前回と全く同じものであるが,念の為に載せておこう。 web ページのソース部分は,赤色部分が JavaScript 関連の部分であり,灰色部分は web の基本的な要素である。 紫色の部分は JavaScript を呼び出すボタン類であり, オレンジ色の部分は JavaScript の出力(表示)関連,緑色部分は不透明度変更に関連した部分となっている。 青はタイトルで,その他が黒色となっている。 ここのあるのは前回同様これまでに出てきたものばかりなので,説明は割愛したい。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta http-equiv="content-style-type" content="text/css">
<meta http-equiv="content-script-type" content="text/javascript">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="http://openlayers.org/en/v3.7.0/css/ol.css" type="text/css">
<script src="http://openlayers.org/en/v3.7.0/build/ol.js" type="text/javascript"></script>
<style type="text/css">
   div.fill {width: 100%; height: 100%;}
   body {padding: 0; margin: 0}
   html, body, #map {height: 100%; width: 100%;}

   .ol-attribution {
     padding: 3px;  position: absolute;  background-color:#ffffff;
     background-color:rgba(230,255,255,0.7);
     right: 3px;  bottom:5px;  font-size:12px;
   }
   .ol-attribution ul { padding: 0px;  line-height: 14px;  margin: 0px; }
   .ol-attribution li { line-height: inherit;  display: inline;  list-style: none outside none; }

   .ol-zoom .ol-zoom-out { margin-top: 202px; }
   .ol-zoomslider { background-color: transparent; top: 2.3em; }
   .ol-touch .ol-zoom .ol-zoom-out { margin-top: 212px; }
   .ol-touch .ol-zoomslider { top: 2.75em; }

   button.boldblack { color:black; font-weight:bold; }
   button.red { color:red; }
   button.boldred { color:red; font-weight:bold; }
   button.blue { color:blue; }
   button.boldblue { color:blue; font-weight:bold; }
</style>
<title>OpenLayers 3 Example: Draw Interaction with Modification and Auto Scaling</title>
<script src="ol3ex8.js" type="text/javascript"></script>
</head>

<body onload="init_map()">
  <div id="map_canvas" style="float:left; width:76%; height:100%;"></div>
  <div id="control_panel" style="float:right;width:24%;text-align:left;padding-top:10px;font-size:85%">
 (1) クリックの度に点が増え,ダブルクリックで終端<br>
 (2) 文字データからの入力が可能<br>
 (3) 座標配列表示後に文字から入力すると,<br>
   複数経路は直前の経路の末尾に結合<br>
 (4) 描画後にも通過点は移動可能<br>
 (5) 座標表示後に文字ベースで移動させて,<br>
   文字から入力すると始点終点変更可<br>
 (6) Shift + Click で通過点の削除可能<br>
  <hr size="1" color="#808080">
    <div style="font-size:100%">
      &nbsp;<b>不透明度 Δ=±0.2:
      <a title="decrease opacity" href="javascript: changeOpacity(-0.2);">&lt;&lt;</a>
      <span id="opacity_control">0.5</span>
      <a title="increase opacity" href="javascript: changeOpacity(0.2);">&gt;&gt;</a></b>
      <button id="clearAllLines" onclick="clearAllLines();" class="boldred">Clear All Lines</button><br>
       <button id="getGeometry" onclick="getGeometryFromFeature();" class="boldblack">Display Line Data</button>
       <button id="getLineFromDataList" onclick="getLineFromDataList();" class="boldblue">Get Line Data Pts from Text</button><br>
      <br>
     <button id="deleteLastPoint" onclick="removeLastPoint();" class="red">Delete Last Point</button>
      <button id="deleteLastLine" onclick="removeLastLine();" class="boldred">Delete Last Line</button>
      <br>
     <textarea cols="46" rows="45" id="latlng_display" style="font-size:7.5pt;"></textarea><br>
      &nbsp;<span id="outStr" style="font-size:9pt;"></span><br>
      &nbsp;<span id="outStr2" style="font-size:9pt;"></span><br>
      &nbsp;<span id="outStr3" style="font-size:9pt;"></span>
    </div>
  </div>
</body>
</html>

 次に JavaScript を載せよう。 ここでも色の付け方は前回と同様にしている。 灰色部分はグローバルな変数の定義であり, 赤色部分が今回の距離の計算と zoom の自動調整に関連するである。 青色部分オレンジ色の部分紫色部分水色部分は,経路の作製,削除や文字データからの追加などのルーチンである。 青色部分は FeatureOverlay という名前の ol.layer.Vector タイプの変数の定義など要の部分であり, オレンジ色の部分は FeatureOverlay 変数内座標のテキストへの出力部分, 紫色部分は線や点の削除に関する部分, 水色部分はテキストデータから逆に線データを作る部分である。 その他の緑色部分は不透明度変更に関連した部分となっている。 その他の部分は黒色となっている。 項目事の説明は下に書こう。
// ===================================================================
var map = null;      // 全体の地図用の変数
var view = null;     // 地図の表示用変数
var cyberJ = null;   // 地理院地図用の変数

var featureOverlay = null;
// -------------------------------------------------------------------
var lineColor = '#ff0000'; // red

var center_lon = 135.100303888; // 中心の経度(須磨浦公園)
var center_lat = 34.637674639; // 中心の緯度(須磨浦公園)

var initZoom = 10; // ズームの初期値
var MinZoom  = 6;   // ズームの最小値(最も広い範囲)
var MaxZoom  = 17;  // ズームの最大値(最も狭い範囲)

var initPrecision = 8; // 座標表示の小数点以下の桁数の初期値
var initOpacity = 1.0; // 不透明度の初期値
var gMaxOpacity = 1.0; // 不透明度の最大値
var gMinOpacity = 0.0; // 不透明度の最小値
// -------------------------------------------------------------------
// ヒュベニの公式で緯度・経度から距離を求めるための定数
var long_r = 6378137.000;     // [m] 長半径
var short_r = 6356752.314245; // [m] 短半径
var rishin = Math.sqrt((long_r * long_r - short_r * short_r)/(long_r * long_r)); // 第一離心率
var a_e_2 = long_r * (1-rishin * rishin);  // a(1-e^2)
var pi = 3.14159265358979;    // Pi
// *******************************************************************
function init_map() {
// テキストエリアのクリア
    document.getElementById("latlng_display").value = '';

// 表示用の view 変数の定義
    view = new ol.View({ projection: "EPSG:3857", maxZoom: MaxZoom, minZoom: MinZoom })

// cyberJ の opacity をいじるために,cyberJ という変数に入れている。
    cyberJ = new ol.layer.Tile({
        opacity: initOpacity,
        source: new ol.source.XYZ({
            attributions: [ new ol.Attribution({ html: "<a href='http://maps.gsi.go.jp/development/ichiran.html' target='_blank'>国土地理院</a>" }) ],
            url: "http://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png", projection: "EPSG:3857"
        })
    })

// 地図変数 (map 変数) の定義。地理院地図を表示するように指定している。
    map = new ol.Map({
        target: 'map_canvas',
        layers: [cyberJ],
        view: view,
        renderer: ['canvas', 'dom'],
        controls: ol.control.defaults().extend([new ol.control.ScaleLine()]),
        interactions: ol.interaction.defaults()
    });

// zoom slider の追加
    map.addControl(new ol.control.ZoomSlider());
// 中心の指定。view に対して指定。transform を忘れないこと。
    view.setCenter(ol.proj.transform([center_lon, center_lat], "EPSG:4326", "EPSG:3857"));
// zoom の指定。view に対して指定する。
    view.setZoom(initZoom);

// ol.FeatureOverlay の追加
     var collections = new ol.Collection();
    featureOverlay = new ol.layer.Vector({
        source: new ol.source.Vector({
            features: collections  // ol.Collection でないといけない。
        }),
        style: new ol.style.Style({
            stroke: new ol.style.Stroke({ color: '#ff0000', width: 2 })
        }),
    });
    featureOverlay.setMap(map);

    var modify = new ol.interaction.Modify({
        features: featureOverlay.getSource().getFeaturesCollection(),
        // the SHIFT key must be pressed to delete vertices, so
        // that new vertices can be drawn at the same position
        // of existing vertices
        deleteCondition: function(event) {
            return ol.events.condition.shiftKeyOnly(event) &&
                ol.events.condition.singleClick(event);
        }
    });
    map.addInteraction(modify);

// ol.interaction.Draw の追加
    var draw = new ol.interaction.Draw({
        features: featureOverlay.getSource().getFeaturesCollection(),
        type: 'LineString'
    });
    map.addInteraction(draw);

// span opacity_control (地理院地図の不透明度) に初期値(実数)を入れる。
    document.getElementById('opacity_control').innerHTML = initOpacity.toFixed(1);
} // function init_map()
// ===================================================================
// ヒュベニの公式を使った距離計算
// 2点間の距離
function dist_2pts(lon0, lat0, lon1, lat1) {
    lon0 = lon0 * pi / 180;  lat0 = lat0 * pi / 180; // in radian
    lon1 = lon1 * pi / 180;  lat1 = lat1 * pi / 180; // in radian
    var d_lon = lon1 - lon0;
    var d_lat = lat1 - lat0;
    var ave_lat = (lat1+lat0)/2;
    var Wx = Math.sqrt(1-rishin * rishin * Math.sin(ave_lat) * Math.sin(ave_lat));
    var Mx = a_e_2 /Wx/Wx/Wx;
    var Nx = long_r /Wx;
    var dum = (d_lat * Mx)*(d_lat * Mx) + (d_lon* Nx * Math.cos(ave_lat)) * (d_lon* Nx * Math.cos(ave_lat)); // square of distance
    return Math.sqrt(dum);
}
// ===================================================================
// 地理院地図 (var cyberJ) の opacity(不透明度) を変える
function changeOpacity(opacity) {
    var newOpacity = (parseFloat(document.getElementById('opacity_control').innerHTML) + opacity).toFixed(1); // 新しい opacity の値を求める
    newOpacity = Math.min(gMaxOpacity, Math.max(gMinOpacity, newOpacity)); // 最大値と最小値の範囲を超えないように
    cyberJ.setOpacity(newOpacity); // 地理院地図の opacity の変更
    document.getElementById('opacity_control').innerHTML = newOpacity.toFixed(1); // opacity の数字の表示書き換え
}
function directSetOpacity(opacity) {
    cyberJ.setOpacity(opacity);
    document.getElementById('opacity_control').innerHTML = opacity.toFixed(1);
}
// *******************************************************************
// 文字データの表示ルーチン:メルカトル座標系から WGS84 へ変換してテキスト表示している。
function writeData(coordArray) {
    var outstr = document.getElementById("latlng_display").value;
    for(var i=0; i < coordArray.length; i++) { outstr = outstr + ol.proj.transform(coordArray[i],"EPSG:3857", "EPSG:4326").toString() + "\n"; }
    outstr = outstr + "\n"; // 最後に改行を追加しておく
    document.getElementById("latlng_display").value = outstr; // 文字列を表示し直す
}
// *******************************************************************
function setCenterZoom() {
// feature が1個以上ある時に,以下の処理をする
    var features = featureOverlay.getSource().getFeaturesCollection(); // array ではなく ol.Collection なので,注意必要
    var lengf = features.getLength();// length of ol.Collectioin
    document.getElementById("outStr2").innerHTML = ""; // 文字列のクリア
// feature が1個以上,feature[0] が2点以上あれば,以下の処理を行う
    if ((lengf > 0) && ((features.getArray())[0].getGeometry().getCoordinates().length >1)) {
        var coordArray = new Array();
        for (var j=0; j < lengf; j++) {
            coordArray = coordArray.concat(features.getArray()[j].getGeometry().getCoordinates()); // 各 feature に含まれる線分座標の配列を追加
        }
        var lineStrings = new ol.geom.LineString(coordArray);
        view.fit(lineStrings, map.getSize());
    }
}
// ===================================================================
// 経路の長さを求めて表示する
function length_line() {
    var lineLength = 0;
    var features = featureOverlay.getSource().getFeaturesCollection(); // array ではなく ol.Collection なので,注意必要
    var lengf = features.getLength();// length of ol.Collectioin
    for (var j=0; j < lengf; j++) {
        var geometry = (features.getArray())[j].getGeometry(); // featureOverlay の中のgeometry(例:ol.geom.LineString )
        var coordArray = geometry.getCoordinates(); // geometry 変数の中の座標を配列へ(メルカトル座標系)
        for (var i=0; i < coordArray.length; i++) { coordArray[i] = ol.proj.transform(coordArray[i],"EPSG:3857", "EPSG:4326"); }
        for (var i=0; i < (coordArray.length-1); i++) { lineLength = lineLength + dist_2pts(coordArray[i][0],coordArray[i][1],coordArray[i+1][0],coordArray[i+1][1]); }
    }
    document.getElementById("outStr").innerHTML = "&nbsp;&nbsp;L = "+(Math.floor(lineLength)/1000)+" [km]";
}
// -------------------------------------------------------------------
// 得られた線分の座標の表示
function getGeometryFromFeature() {
    var features = featureOverlay.getSource().getFeaturesCollection(); // array ではなく ol.Collection なので,注意必要
    var lengf = features.getLength();// length of ol.Collectioin
    document.getElementById("latlng_display").value = ''; // 文字列を表示し直す
    for(var j=0; j < lengf; j++) {
        var geometry = (features.getArray())[j].getGeometry(); // featureOverlay の中のgeometry(例:ol.geom.LineString )
        var coordArray = geometry.getCoordinates(); // geometry 変数の中の座標を配列へ(メルカトル座標系)
        writeData(coordArray); // 座標配列を WGS84 に変換してテキスト表示している
    }
    length_line();
}
// *******************************************************************
// 全ての点を削除するルーチン
function clearAllLines() {
    document.getElementById("latlng_display").value = ''; // 文字データもクリア
    featureOverlay.getSource().getFeaturesCollection().clear(); // データをクリア
    length_line();
}
// -------------------------------------------------------------------
// 最後のラインを削除するルーチン
function removeLastLine() {
// feature が1個以上の場合に処理をする
    if (featureOverlay.getSource().getFeaturesCollection().getLength() >0) {
        featureOverlay.getSource().getFeaturesCollection().pop(); // 最後の feature のみクリア
// テキストデータの再表示
        getGeometryFromFeature();
    }
}
// -------------------------------------------------------------------
// 最後のラインの最後の一点を削除するルーチン
function removeLastPoint() {
    var features = featureOverlay.getSource().getFeaturesCollection(); // array ではなく ol.Collection なので,注意必要
    var lengf = features.getLength();// length of ol.Collectioin
// feature が1個以上,feature[0] が2点以上あれば,以下の処理を行う
    if ((lengf > 0) && ((features.getArray())[0].getGeometry().getCoordinates().length >0)) {
        var geometry = (features.getArray())[lengf-1].getGeometry(); // 最後の feature の geometry を取り出す
        var coordArray = geometry.getCoordinates(); // geometry 変数の中の座標を配列へ(メルカトル座標系)
        coordArray.pop(); // 配列の最後の点を削除
        geometry = new ol.geom.LineString(coordArray); // featureOverlay の中のgeometry(例:ol.geom.LineString )
        features.pop(); // 最後の feature のみクリア
        if (coordArray.length>0) { // 1点でも残っていれば線にデータを追加
            featureOverlay.getSource().addFeature(new ol.Feature({ geometry: geometry })); // featureOverlay に線のデータを追加
        }
// テキストデータの再表示
        getGeometryFromFeature();
    }
}
// -------------------------------------------------------------------
// 文字列データから読み込むルーチン
function getLineFromDataList() {
    featureOverlay.getSource().getFeaturesCollection().clear(); // データをクリア
// テキストデータから座標配列へ入れる
    var coordArray = new Array;
    var lineData = document.getElementById("latlng_display").value; // 文字列データを読み込む
    var singleLines = lineData.split("\n"); // 改行マークで切って配列に入れる
    for (i in singleLines) {
        if (singleLines[i] != "") { // 空行は飛ばす
            var yy = singleLines[i].split(","); // コンマでデータを分割
            coordArray.push(ol.proj.transform([parseFloat(yy[0]), parseFloat(yy[1])], "EPSG:4326", "EPSG:3857")); // メルカトル座標系に変換してから代入
        }
    }

    if (coordArray.length > 0) {
// 新しい経路の作成
        var geometry = new ol.geom.LineString(coordArray); // featureOverlay の中のgeometry(例:ol.geom.LineString )
        featureOverlay.getSource().addFeature(new ol.Feature({ geometry: geometry })); // featureOverlay に線のデータを追加
// テキストデータの再表示
        getGeometryFromFeature();
        setCenterZoom();
    }
}
// *******************************************************************
 まず,色がついていない OpenLayers 3 の基本的な部分だが,これまでにも書いているので,細かい点は省略しよう。 わからない時は,OpenLayers 3 を使ってみよう(その0:はじめに:地理院地図を表示)にある目次から該当するものを探して説明を見てみて欲しい。 おおまかには,地図として地理院地図を持ちており,'map_canvas' という id を持つ web 要素に地図が描かれる。 controls(スイッチみたいなもの)や interaction(地図や地図上の物体へのなんらかのアクション(動作))もとりあえず基本的な物がデフォルトで設定されている。

 青色部分は ol.interaction.Draw, ol.interaction.Modify を使った図形の描画関連の部分である。 これは前回に説明しているが,念の為に少し説明を載せておこう。 ここでは「ol.layer.Vector」クラスの変数(インスタンス)featureOverlay を定義して,map に追加している。

 次に「ol.interaction.Modify」クラスの変数(動作なので関数の方がいいかも)modify を定義している。 この modify は図形に対する動作の一種であり,図形を修正するという動作(interaction)を定義している。 対象となるのは上記で定義した featureOverlay 変数の中の source の中にある features(図形を特徴付ける変数の配列)であり, 図形を修正(変形)する動作を定義している。 これにより,図形を修正できるようになる。 さらに,Shift キーと一緒にクリックされたら点を削除するように定義されている。

 その下で「draw」変数が定義されて,map.addInteraction(draw); として map に追加されている。 これも featureOverlay の中の source の中にある features(図形を特徴付ける変数の配列)に対して, 'LineString'(折れ線図形)を描画する動作(interaction)を定義している。 つまり,この interaction によって,地図上に折れ線を描くことができ,その描かれた折れ線のデータが featureOverlay の中の source の中の feature に入ることになる。

 赤色部分は距離の計算や zoom に対して自動調整を行っている部分である。 dist_2pts() 関数は WGS84 測地系で与えられた2点間の距離を求めている。

 少し離れた下の方で定義してあるsetCenterZoom() 関数は,中心と zoom 自動調整の部分である。 ここでは featureOverlay の中にある各 feature(図形を特徴づける変数)から点の座標データを取り出し,すべてを一つの配列にして, そこから lineString という線分を表す変数を作って fit() 関数を使っている。

 作図された経路線の情報は featureOverlay 変数の中に最初から含まれる「feature」に入ってる。 featureOverlay の中の feature は,図形1個に対して1個の feature が割り当てられるため, 一般には複数の feature(結局は複数の図形)が含まれる。 そこで,「var features = featureOverlay.getSource().getFeaturesCollection();」で feature の配列を取り出している。 だたし,この配列は通常の JavaScript の配列ではなく,ol.Collection という OpenLayers 3 で定義された「ある種の配列」になっている。 通常の JavaScript の配列と異なるため,例えば,長さを得るのは「features.length」ではなく,「features.getLength()」としなければならない。 また,feature を配列として取り出すには「features.getArray() 関数」を使う。 そのため,具体的な線要素(ol.geom.LineString タイプの geometry 要素)を取り出すのに, 前回のような「var geometry = features[j].getGeometry();ではなく, 「var geometry = features.getArray()[j].getGeometry();としなくてはならない。 (上記の例では,配列を取り出したというのが分かり易いように余分なカッコをつけている:「var geometry = (features.getArray())[j].getGeometry();

 そして,ol.geom.LineString クラスの変数(インスタンス)geometry に「getCoordinate()」という関数を作用させると,「点の配列」(coordArray)が得られる。 この coordArray は,経度と緯度を要素に持つ「座標(ol.Coordinate)」タイプのデータの配列であり, 通常の数値の配列(2次元配列になっている)なので,表示するのは普通の JavaScript で配列を表示すればいいことになる。

 緑色部分は不透明度変更関連であり,ここでは説明は省略したい。 説明はその2:地図の不透明度を変えるを見て欲しい。

 オレンジ色の部分は,作図した経路線のデータを出力する部分である。 上から見ていくと,まず「writeData」というのがある。 これは,通常の配列に入れられた座標データを,文字列として出力する関数である。 データが球面メルカトル座標系(EPSG:3857)で入っているので,WGS84(EPSG:4326)に変換してから文字列として出力している。

 「length_line()」関数では,描かれている経路の長さの合計を計算している。 折れ線の各点間の距離はそれぞれヒュベニの公式を用いて計算され,それらの和として経路長を計算している。 ただし,異なる線分の距離はそれぞれ独立して計算されて,それらの合計を全体の距離としている。

 「getGeometryFromFeature()」関数では,作図された経路線の変数から,経路点の情報を取り出して,上記の writeData 関数に送っている。 ここでは複数の feature(図形)の全てから点の配列を取り出してテキストとして表示させている。 ちなみに,異なる feature に対応する配列間に空行を表示するようにしている。

 紫色の部分は,点や線を削除するための処理である。 「clearAllLines()」関数では,featureOverlay 中の全ての feature をクリアすることで,全ての線を削除している。 「removeLastLine()」関数では,featureOverlay 中の最後の feature を削除することで,最後の線のみを削除している。 「removeLastPoint()」関数は,最後の線の feature から点配列を数値データの配列として取り出し, coordArray.pop(); で最後の線の最後の一点を削除している。 そして,featureOverlay から,最後の feature(最後の線)を消し(pop()関数),新しく数値データ配列から feature を作って featureOverlay に付け加えている。 今回は removeLastLine() 関数と removeLastPoint() 関数では, 点が無くなった時や,線がなくなった時に余計な削除処理をしないように改良を加えておいた

 最後に水色の部分は,テキストデータから図形(feature)を作って featureOverlay に代入している。 別々の feature に入っていたデータは全て一つにまとめられ(一個前の線の終端と次の線の始点が線でつながってしまう), 一度 coordArray という数値データ配列に入れられた後で,featureOverlay に代入される。 わかってしまえば確かにそうなのだが,最初はどの変数に図形の情報が入っているかがわからずに,しばらく悩んでしまった…。 (ここでも点が無い時に余計な削除処理をしないように改良を加えておいた

 今回で ol.interaction.Draw と ol.interaction.Modify の例は終わり(となるはず)。 この例では,折れ線を引っ張ったり伸ばしたりして,変形させる事ができ,また,テキストからのデータの読み込みを選択すると中心や zoom の自動調整を行ってくれる。 そのため,折れ線で経路線を作るのが大変ラクになると思っている。
 次回は趣きを変えて,KML データの表示の話をしよう。

 「その8」のサンプルを具体的な web ページとして用意したので,具体的な表示を見てみて欲しい。(ちなみにサンプルページはアクセスログを取るルーチンを組み込んでいます)

その9:KML データで経路を描画するに続く

0 件のコメント: