2014年9月12日金曜日

OpenLayers 3 を使ってみよう(その7:ol.interaction.Modify を使った修正可能バージョン)

これはOpenLayers 3 を使ってみよう(その6:ol.interaction.Draw を使った例)からの続きである。
OpenLayers 3 を使ってみよう(その0:はじめに:地理院地図を表示)に目次がある。
ここでは OpenLayers 3.7.0 を使っている。
 前回(その6:ol.interaction.Draw を使った例)では, ol.interaction.Draw を使って,マウスで経路線を描く方法に挑戦してみた。 そこでは ol.layer.Vector を使って線を描いたが,一度描いた線を地図上で引っ張ったり伸ばしたりして修正することができなかった。
今回は ol.interaction.Modify を用いて,地図上で修正可能なバージョンに挑戦してみた。

 今回は OpenLayers 3 の Examples のページの中の, Draw and modify features example (draw-and-modify-features.html) を参考にしている。 これは作図した後に,図形を変形できるものである。 これを元に,作られた図形の情報を文字データとして表示させたり,逆に文字列データから図形情報を作って,地図上に表示させるとともに,その図形も変形させることができる状態にしてみた。

 さっそく,いつものように 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</title>
<script src="ol3ex7.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 を載せよう。 ここでも色の付け方は前回と同様にしている。 灰色部分はグローバルな変数の定義であり, 赤色部分が OpenLayers 3 の基本的な部分(これまでに出てきている)である。 青色部分オレンジ色の部分紫色部分水色部分が今回の重要な部分である。 青色部分は Line データを保持する FeatureOverlay という名前の変数の定義など要の部分であり, オレンジ色の部分は 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; // 不透明度の最小値
// *******************************************************************
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()
// ===================================================================
// 地理院地図 (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 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 に変換してテキスト表示している
    }
}
// *******************************************************************
// 全ての点を削除するルーチン
function clearAllLines() {
    document.getElementById("latlng_display").value = ''; // 文字データもクリア
    featureOverlay.getSource().getFeaturesCollection().clear(); // データをクリア
}
// -------------------------------------------------------------------
// 最後のラインを削除するルーチン
function removeLastLine() {
    featureOverlay.getSource().getFeaturesCollection().pop(); // 最後の feature のみクリア
// テキストデータの再表示
    getGeometryFromFeature();
}
// -------------------------------------------------------------------
// 最後のラインの最後の一点を削除するルーチン
function removeLastPoint() {
    var features = featureOverlay.getSource().getFeaturesCollection(); // array ではなく ol.Collection なので,注意必要
    var lengf = features.getLength();// length of ol.Collectioin
    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 のみクリア
    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")); // メルカトル座標系に変換してから代入
        }
    }
// 新しい経路の作成
    var geometry = new ol.geom.LineString(coordArray); // featureOverlay の中のgeometry(例:ol.geom.LineString )
    featureOverlay.getSource().addFeature(new ol.Feature({ geometry: geometry })); // featureOverlay に線のデータを追加
// テキストデータの再表示
    getGeometryFromFeature();
}
// *******************************************************************
 まず,赤色部分で書かれている OpenLayers 3 の基本的な部分だが,これまでにも書いているので,細かい点は割愛したい。 わからない時は,OpenLayers 3 を使ってみよう(その0:はじめに:地理院地図を表示)にある目次から該当するものを探して説明を見てみて欲しい。 おおまかには,地図として地理院地図を持ちており,'map_canvas' という id を持つ web 要素に地図が描かれる。 controls(スイッチみたいなもの)や interactions(地図や地図上の物体へのなんらかのアクション(動作))もとりあえず基本的な物がデフォルトで設定されている。

 青色部分からが今回の肝心な部分となっている。 今回も「ol.layer.Vector」クラスの変数(インスタンス)を定義している (変数名は featureOverlay)。 そして,この featureOverlay を map に追加している。 変数名は,OpenLayers 3.6.0 以前では ol.layer.Vector の代わりに ol.featureOverlay クラスの変数を使っていたことに由来している。

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

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

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

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

 「getGeometryFromFeature()」関数では,作図された経路線の変数から,経路点の情報を取り出して,上記の writeData 関数に送っている。 この部分は前回同様に苦労して理解した部分である。 少し詳しく見ていこう。 まず,作図された経路線の情報は featureOverlay 変数の中の source に含まれる「feature」に入ってる。 featureOverlay の中の source の中の 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 で配列を表示すればいいことになる。 今回は複数の feature(図形)の全てに対して,上記の操作を行い,点の配列をテキストとして表示させている。 ちなみに,異なる feature に対応する配列間に空行を表示するようにしている。

 紫色の部分は,点や線を削除するための処理である。 「clearAllLines()」関数では,featureOverlay 中の全ての feature をクリアすることで,全ての線を削除している。 「removeLastLine()」関数では,featureOverlay 中の最後の feature を削除することで,最後の線のみを削除している。 「removeLastPoint()」関数は,最後の線の feature から点配列を数値データの配列として取り出し, coordArray.pop(); で最後の線の最後の一点を削除している。 そして,featureOverlay から,最後の feature(最後の線)を消し(pop()関数),新しく数値データ配列から feature を作って featureOverlay に付け加えている。 (このままだと要素がカラになっても削除できてしまうなど不備があるので,次の投稿で訂正しておいた

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

 今回は,ol.interaction.Modify の例に挑戦してみた。 この例では,折れ線を引っ張ったり伸ばしたりして,変形させる事ができるため, 折れ線で経路線を作るのが大変ラクになると思っている。 しかし,まだテキストデータで与えられた経路に合わせて,地図の中心や zoom を変更してないので,そのルーチンを組み込んでみたいと思っている。

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

その8:ol.interaction.Draw 修正可能バージョンで表示領域の大きさの調整を行うに続く

0 件のコメント: