2014年9月10日水曜日

OpenLayers 3 を使ってみよう(その5:テキストデータから折線データ読込み)

これはOpenLayers 3 を使ってみよう(その4:マウスクリックで地図上に経路を描画する)からの続きである。
OpenLayers 3 を使ってみよう(その0:はじめに:地理院地図を表示)に目次がある。
ここでは OpenLayers 3.7.0 を使っている。
 前回(その4:マウスクリックで地図上に経路を描画する)では, マウスクリックによって地図上に折線を描画する方法を記述した。
今回はテキストデータから折線を描画させようと思う。

 テキストデータは,前回折線の座標を表示させていた領域にあるデータから,逆に経路データを取得して,経路を描画させるものである。 具体的には,文字列を1行ずつ読み込んで緯度と経度に分け,数値に変換したのち点の配列にデータを入れて描画させている。

 ここでは,与えた経路の全長も計算させている。 長さを求める方法として,経路の点を与える配列から ol.geom.LineString クラスの変数を作って,その長さを求める関数 getLength() を使う方法がある。 しかし,その方法を試してみると,札幌-鹿児島の距離が約 2012km となってしまった。実際には約 1592 km なので,あまりに差が大きい。 どのような計算をしているかがわからないのでなんとも言えないがこれでは使い物にならない。 そこで,仕方なくヒュベニの公式と呼ばれるもので各点間の距離を算出して全体の和として全長を求めている。

 さらに,経路の大きさに合わせて,地図の中心と zoom を設定しなおしている。 これは ol.View に対する関数 fitGeometry() を使っている。 2014/9/24 現在,fitGeometry() 関数の説明は ol.View で右上にある「Stable Only」のチェックをはずさないと現れない。 これは ol.View に対する関数 fit() を使っている。 fit() 関数はまだ「Experimental」扱いであり,その説明は ol.View で右上にある「Stable Only」のチェックがない状態の時に表示される。(2015/8 v3.7.0 用に修正)

 まず,いつものように以下に web ページのソースを載せる。 ここでは前回からの変更箇所の色を変えている。 説明はソースコードに下に書こう。
<!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; }
</style>
<title>OpenLayers 3 Example: Draw a Line from Text</title>
<script src="ol3ex5.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%">
    <div style="font-size:100%">
      &nbsp;不透明度:<a title="decrease opacity" href="javascript: directSetOpacity(0.1);">0.1</a> 
      <a title="decrease opacity" href="javascript: directSetOpacity(0.5);">0.5</a> 
      <a title="decrease opacity" href="javascript: directSetOpacity(1.0); ">1.0</a><br>
      &nbsp;<b>不透明度 Δ=±0.2:
      <a title="decrease opacity" href="javascript: changeOpacity(-0.2);">&lt;&lt;</a>
      <span id="opacity_control"></span>
      <a title="increase opacity" href="javascript: changeOpacity(0.2);">&gt;&gt;</a></b>
      <button id="clearAllPoints" onclick="clearAllPoints();">Clear All Pts</button><br>
     <button id="getLineFromDataList" onclick="getLineFromDataList();">Get Line Data Pts from Text</button><br>
      <br>
     <button id="deleteLastPoint" onclick="removeLastPoint();">Delete Last Point</button>
      &nbsp;<span id="outStr"></span>
      &nbsp;<span id="outStr3"></span>
      <br>
     <textarea cols="46" rows="45" id="latlng_display" style="font-size:7.5pt;"></textarea><br>
      &nbsp;<span id="outStr2" style="font-size:9pt;"></span><br>
    </div>
  </div>
</body>
</html>
 今回の web ページ上の変更点は,「テキストからデータを読み込む」ボタンを用意したのと,テキスト出力領域を追加しただけある。

 次に JavaScript を載せよう。ここでも変更箇所の色を変え,説明は web ページのソースと同じくこの下に書いておく。
// ===================================================================
var map = null;      // 全体の地図用の変数
var view = null;     // 地図の表示用変数
var cyberJ = null;   // 地理院地図用の変数

var lineVector = null;                             // vector layer variable
var vectorSource = null;                           // variable for source
var vectorFeature = null;                          // variable for feature
var lineStrings = new ol.geom.MultiLineString([]); // line instance of the path
var lineStrArray = new Array();                    // line data array as lineString format [[pt0,pt1],[pt1,pt2],[pt2,pt3],....]
var coordArray = new Array();                      // line data array [pt0, pt1, pt2,...], eath point is pt0=[lon0,lat0]
var coord = new Array();                           // クリックした点の座標を渡す変数
var lineLength = 0;                                // 経路 (coordArray) の長さを入れる
// -------------------------------------------------------------------
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() {
// 表示用の view 変数の定義。
    view = new ol.View({ projection: "EPSG:3857",
        maxZoom: MaxZoom,
        minZoom: MinZoom
   })

// 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: document.getElementById('map_canvas'),
        layers: [cyberJ],
        view: view,
        renderer: ['canvas', 'dom'],
        controls: ol.control.defaults().extend([new ol.control.ScaleLine()]),
        interactions: ol.interaction.defaults()
    });

// 地図をクリックしたら点を追加し線を再描画させる。小数点以下の桁数は initPrecision で指定。メルカトル座標 (EPSG:3857) を WGS84 (EPSG:4326) に変換している。
    map.on('click', function(evt) {
        var coordinate = evt.coordinate;
        var stringifyFunc = ol.coordinate.createStringXY(initPrecision);
        coord = ol.proj.transform(coordinate, "EPSG:3857", "EPSG:4326");
        addPoint(coord);
    });

// 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);

// span opacity_control (地理院地図の不透明度) に初期値(実数)を入れる。
    document.getElementById('opacity_control').innerHTML = initOpacity.toFixed(1);

} // function init_map()

// ===================================================================
// 地理院地図 (var cyberJ) の opacity(不透明度) を変える
// DOM の指定で,document.getElementById('opacity_control').innerHTML とすると,うまくいかず。jQuery と干渉してのかな?
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);
}
// ===================================================================
// ヒュベニの公式を使った距離計算
// 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);
}
// ===================================================================
// 経路の長さを求めて表示する
function length_line() {
    lineLength = 0;
    for (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("outStr3").innerHTML = "L = "+(Math.floor(lineLength)/1000)+" [km]";
}
// ===================================================================
// 文字データの表示ルーチン
// 一度クリアしてから書き直す
function writeData() {
    var outstr = '';
    document.getElementById("latlng_display").value = ''; // 一度文字表示をクリア
    for (i=0; i< coordArray.length; i++) { outstr = outstr+coordArray[i].toString()+"\n"; }
    document.getElementById("latlng_display").value = outstr; // 文字列を表示し直す
}
// ===================================================================
// 点データの再表示とラインの再描画サブルーチン
// 再描画は2点以上ないと行わない。
function drawLine_sub() {
    document.getElementById("outStr").innerHTML = coordArray.length+" pts";
    writeData(); // 文字列データを表示しておく

// 点が2点以上あれば,経路線用の vector データを作る(作り直す)
    if (coordArray.length > 1) {
        lineStrArray.length = 0;  // 経路線用の配列を一度クリア(これをしないと変に画像が残る)
        for (i=0; i<(coordArray.length-1); i++) { lineStrArray.push([coordArray[i], coordArray[i+1]]); }
        lineStrings.setCoordinates(lineStrArray);
        vectorFeature = new ol.Feature(lineStrings.transform('EPSG:4326', 'EPSG:3857'));
    }
}
// -------------------------------------------------------------------
// ラインの初期描画サブルーチン
// coordArray の長さが 2 以上でないと呼び出してはいけない(線が描けないため)
function init_drawLine_sub() {
// 経路線用の vector データを作る
    drawLine_sub();
    vectorSource  = new ol.source.Vector({ features: [vectorFeature] }); // vector layer 用のソースの作成
// 経路用の vector layer の作成
    lineVector = new ol.layer.Vector({
        source: vectorSource,
        style: new ol.style.Style({
            stroke: new ol.style.Stroke({ color: lineColor, width: 2 })
        })
    });
    // vector layer の追加
    map.addLayer(lineVector);
}
// ===================================================================
// 点の追加ルーチン
// vectorFeature を書き換えると,自動で画像が追加される。coordArray.push([136.1720, 34.2250]);
function addPoint(coord) {
    coordArray.push(coord); // 配列に追加
// 経路点配列が2になれば,vector layer を新たに作る。それ以外は vectorFeature を更新すれば再描画される
    if (coordArray.length == 2) { init_drawLine_sub(); } else { drawLine_sub(); }
    length_line();
}
// -------------------------------------------------------------------
// 最後の点の削除ルーチン
function removeLastPoint() {
// 点が1点以上あれば,点の削除処理を行う
    if ( coordArray.length > 0) {
        coordArray.pop(); // 配列の最後の点を削除
// 点が1点に減れば,vector layer を消す
        if (coordArray.length == 1) { map.removeLayer(lineVector); }
        drawLine_sub();
        length_line();
    }
}
// -------------------------------------------------------------------
// 全ての点を削除するルーチン
function clearAllPoints() {
    coordArray.length = 0;    // 最初に経路点用の配列をクリア
    lineStrArray.length = 0;  // 経路線用の配列もクリア(これをしないと変に画像が残る)
    document.getElementById("latlng_display").value = ''; // 文字データもクリア
    document.getElementById("outStr").innerHTML = "0 pts"; // 点数もゼロにセット
    map.removeLayer(lineVector); // vector layer も消しておく
    length_line();
}
// =======================================================
// 文字列データから読み込むルーチン
function getLineFromDataList() {
    coordArray.length = 0;     // 最初に経路点用の配列をクリア
    lineStrArray.length = 0;;  // 経路線用の配列もクリア

    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([parseFloat(yy[0]), parseFloat(yy[1])]); // 数値への変換をサボっていはいけない
        }
    }

// 2点以上ある時に,以下の処理をする
    if (coordArray.length > 1) {
        map.removeLayer(lineVector);
        init_drawLine_sub();
        length_line();

//        view.fitGeometry(lineStrings, map.getSize());  // for v3.0.0
        view.fit(lineStrings, map.getSize());            // for v3.7.0
    }
}
// *******************************************************************
 今回も JavaScript の部分はかなり書き直されている。 テキストデータから線を描画する部分や,折線の領域の大きさを計算して,表示の中心や zoom を設定するルーチンが加わっており, かなり変更が加えられている。 以下に変更点について説明しよう。

 (1) 変数定義の所で「view」が定義されている。
   これは,zoom や中心点の変更する際に必要なので,グローバルな変数として定義している。
   それに伴って init_map() の中の view の設定文から「var」が消されている。
   また,経路の長さを表す変数 lineLength を定義している。

 (2) さらに,ヒュベニの公式を用いて,2点間の距離を求めるために,そこで必要となる定数が定義されている。

 (3) (1) にも書いたが,init_map() の中の「view」の定義文から「var」が消されている。

 (4) オレンジで書かれた dist_2pts() ルーチンだが,
   これらはヒュベニの公式を用いて,2点間の距離を求めている。
   ポイントとしては,座標をラジアンに変換しているのと,横方向の距離に cos(ave(lat)) を掛けている,などがある。
   ここで ave(lat) は緯度の平均値である。詳しくはヒュベニの公式を調べてみて欲しい。

 (5) 緑で書かれた length_line() は,ヒュベニの公式を使って経路の長さを求めている。
   具体的には,経路の各点の間の距離を dist_2pts() で求めて和をとっている。

 (6) addPoint() や removeLastPoint(),clearAllPoints() で,length_line() を呼び出し,
   「outStr2」という id を持つ web 要素の中を空白にしている。
   「outStr2」には,経路の占めている矩形の大きさなどを表示している。これはテキストからデータを取り込んだ際に記載されるものである。

 (7) 紫で書かれた getLineFromDataList() がテキストから経路点のデータを読み込み,線を描画するルーチンである。
   まず,coordArray と lineStrArray という2つの配列をクリアし,文字列データから経路点のデータを取り込んで,描画している。
   ここでのポイントは,配列に入れる際に parseFloat 関数を用いて,文字列から数値に変換をしないといけない点である。
   JavaScript では多少型が違っていても構わないが,ここではちゃんと変換しておかないと,うまくいかなかった。

   さらに,テキストデータとして与えられた経路に合わせて地図の中心や zoom を設定しなおしている。
   具体的には, ol.View に対する fitGeomerty() 関数を用いている。
   具体的には, ol.View に対する fit() 関数を用いている。(2015/8 v3.7.0 用に修正)
   view.fit(lineStrings, map.getSize()); の部分だが,最初の引数が対象となる図形,2個めが合わせるべき画面領域のサイズを示している。

 さて,これにより,マウスのクリックを繰り返すことで経路を作る web ページができた。 点を打ち間違えたら「最後の点のみ削除」もできるし,作り直したければ「全削除」も可能である。 また,「テキストから経路データを読み込む」とすると,データの読み込みのみならず,経路の長さや,表示範囲も自動で設定してくれる。 ここでは地理院地図を対象にしているので,登山の経路の計画や,たどった道を再現するのに使えるのではないかと思っている。

 ここでの例では,一度作った経路を微修正するのは面倒である。 テキストデータとして表示しておいて,該当する行を修正すればいいのだが,どの点がどの数値か,などはわかりにくいので,このままではいまいち使い勝手が悪い。 OpenLayers 3 では ol.interaction.Drawol.interaction.Modify というクラス(変数というよりは関数を定義している感じ)がある。 これらのクラスを用いれば,ol.interaction.Draw で経路を作り,ol.interaction.Modify によって途中の点の修正が可能となる。 そこで,次回で ol.interaction.Draw という動作を使って経路を作ろう。 そして次々回でその経路データを取り出したり,逆に数値データから経路を作ったりする処理についてみていきたい。

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

その6:ol.interaction.Draw を使った例に続く

0 件のコメント: