2016年7月6日水曜日

OpenLayers 3 を使ってみよう(その19:その9からその18までのまとめ)

これはOpenLayers 3 を使ってみよう(その18:Marker アニメーション)からの続きである。
OpenLayers 3 を使ってみよう(その0:はじめに:地理院地図を表示)に目次がある。
 その13までのページでは OpenLayers v3.7.0 で書いてきたが,ここでは OpenLayers 3.16.0 で書かれている。
 前回(その18:Marker アニメーション)では,OpenLayers の Example に載っていた Marker を使ったアニメーション Marker Animation について書いた。 今回はその9:KML データで経路を描画するからその18:Marker アニメーションまでをまとめたものを示しておこう。 ただし,単にまとめただけではなく,マーカー表示の表示・非表示やセンターマークの表示・非表示などの動作をトグルスイッチに変えてみた。 また,アニメーションになった際には,カメラマークのマーカーは強制的に非表示にし,逆に KML 経路は強制的に表示するようにしてみた。 さらに,拡大・縮小のための関数を用意し,アニメーションが始まると,少し拡大して表示するようにした。 少し拡大して表示すると,点が移動していくと表示範囲からはみ出してしまう。 それを追いかけるために,点が表示範囲からはみ出すと,表示範囲を移動するようにもしてみた。

 まず,web ページのソースを載せよう。 web ページのソース部分は,灰色部分は web の基本的な要素であり,赤色部分が JavaScript 関連の部分である。 青い部分は,センターマークや凡例と 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.16.0/css/ol.css" type="text/css">
<script src="http://openlayers.org/en/v3.16.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%;}

   .center-marker { position: absolute; top: 50%; left: 50%; }
   .center-marker a { display: block;
       height: 32px; width: 32px;
       margin-top: -16px;  margin-left: -16px;
       background-image:url(../cj_map/crosshairs.png);
   }
   .title-fig { position: absolute; top: 0px; left: 40px; }
   .title-fig a { display: block;
       height: 21px; width: 240px;
       margin-top: 0px;  margin-left: 0px;
       background-image:url(./o3cjmap_title.png);
   }
   .symbols { position: absolute; top: 100%; left: 100%; }
   .symbols a { display: block;
       height: 144px; width: 120px;
       margin-top: -180px;  margin-left: -120px;
       background-image:url(./o3cjmap_icons.png);
   }

   .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; }

   .ol-popup { display: none; position: absolute; background-color: white;
     -moz-box-shadow: 0 1px 4px rgba(0,0,0,0.2);
     -webkit-filter: drop-shadow(0 1px 4px rgba(0,0,0,0.2));
     filter: drop-shadow(0 1px 4px rgba(0,0,0,0.2));
     padding: 5px; border-radius: 10px; border: 1px solid #cccccc; bottom: 24px; left: -51px; }
   .ol-popup:after, .ol-popup:before { top: 100%; border: solid transparent; content: " ";
                                       height: 0; width: 0; position: absolute; pointer-events: none; }
   .ol-popup:after  { border-top-color: white;   border-width: 10px; left: 48px; margin-left: -10px; }
   .ol-popup:before { border-top-color: #cccccc; border-width: 11px; left: 48px; margin-left: -11px; }
   .ol-popup-closer { text-decoration: none; position: absolute; top: 2px; right: 8px; }
   .ol-popup-closer:after { content: " ✖ "; }

   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: Marker Animation 2</title>
<script src="ol3ex19.js" type="text/javascript"></script>
<script src="../jquery.min.js" type="text/javascript"></script>
</head>

<body onload="init_map()">
<div id="map_canvas" style="width: 100%; height: 97%; position:absolute; top:30px; left:0px; font-size:100%;">
  <div id="popup" class="ol-popup">
    <a href="#" id="popup-closer" class="ol-popup-closer"></a>
    <div id="popup-content"></div>
  </div>
</div>

<div style="font-size:85%">
  <b>&nbsp;不透明度・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="kml_vector_visible" onclick="kml_vector_visible();" class="blue">隠す</button>
   マーカー<button id="wp_vector_visible" onclick="wp_vector_visible();" class="blue">隠す</button>
   センター<button id="centerMarker_visible" onclick="centerMarker_visible();" class="blue">隠す</button>
   Title/凡例<button id="titleSymbol_visible" onclick="titleSymbol_visible();" class="blue">隠す</button>
   アニメ<button id="start_animation" class="boldred">Start</button>
  <label for="speed">
    speed:&nbsp;
    <input id="speed" type="range" min="10" max="999" step="10" value="50">
  </label>
  
   Zoom <button id="autoZoomButton" onclick="setCenterZoom();" class="boldblack">Fit</button>
    <button id="expandZoomButton" onclick="expandZoom(2, view.getCenter());" class="red">Expand</button>
    <button id="contractZoomButton" onclick="expandZoom(0.5, view.getCenter());" class="blue">Contract</button>
   <button id="getLengthButton" onclick="length_line();" class="boldblack">経路長</button>
  <span id="outStr"></span>
</div>
</body>
</html>
 上から順に説明していこう。 まず,先頭の灰色部分だが,ここは web page の基本的な項目なので,ここでは説明は省略しよう。

 次に赤色部分があるが,これは OpenLayers の css ファイルと JavaScript ファイルを読み込んでいる。
ここでは v3.16.0 を読み込んでいるのがわかる。

 その下にいろいろな style を記述している。 最初の黒い部分は web の基本項目である。 それに続く青い部分は,センターマークや凡例や web のタイトルを表示する時のスタイルや,画像が入っているファイルを指定している。
 青い部分の少し下に茶色の部分があるが,ここは zoom 関連,特に zoom slider を表示するためのスタイルを指定している。
その下の水色の部分はカメラマーカーをクリックした際に表示される吹き出し(popup)関連のスタイルである。
ここで吹き出しの形や横幅などが指定されている。

 スタイル指定が終わると,web ページのタイトルがあった後で,このページで使う ol3ex19.js という JavaScript ファイルを読み込んでいる。 そのファイルの内容はこの下の方に記載している。 その中で,このページの地図の指定などを行っている。 また,JavaScript の中で使いたいので jQuery (jquery.min.js) を読み込ませている。 ここでは jQuery はサーバーにダウンロードしてきたものを読み込ませている。

 ヘッダー部が終わると <body> パートが続く。 ここでは <body> が読み込まれると init_map() という関数が呼ばれる。 その関数は少し上で読み込まれた ol3ex19.js という JavaScript ファイルに記載されており,主に初期設定が書かれている。

 <body> パートでは,まず地図を表示するための id = "map_canvas" という id を持つ <div> が定義されている。 この <div> はボタンなどを置くために,最上部に少し隙間を設けている。 そして,この <div> の中には,さらに吹き出し(popup)のための <div> が指定してある。 地図の具体的な記載はないが,この "map_canvas" という id を持つ <div> パートに,JavaScript を使って地図を表示している。 その具体的な処理はすべて ol3ex19.jss という JavaScript ファイルの中に記述がある。

 地図表示領域が終わると,ボタン類の定義が書いてある。 緑色部分はベースとなる地理院地図の不透明度(opacity)を変えるためのものである。 不透明度(opacity)を下げると,徐々に半透明になっていき,KML 経路などが目立つようになる。 デフォルトでは不透明度は最大(opacity = 1.0)と指定している。

 その下にある青い部分はルート(KML 経路),マーカー,センターマーク,地図タイトルと凡例,の表示・非表示を切り替えるためのボタン群である。 このブログの以前の回では,「表示ボタン」と「非表示ボタン」を別々に用意していたが,今回は「表示・非表示切り替えボタン」として,1個ずつにしている。 各ボタンは,独自の id を持たせてあり,JavaScript の中から各ボタンを指定して処理できるが, ここでは,各ボタンの定義の中に「onclick="XXXX"」としてボタンがクリックされた際の処理を書いた関数を指定している。 その関数は,表示の時も,消去の時も同じ関数を呼び出し,ボタンが押された時の状況(表示になっているか,非表示か)に応じて, ボタンの名前も変わるし(「表示」と「隠す」の2通り),処理も別々の処理を行う。 そのため,JavaScript の中にそれぞれのボタンごとに現在の表示・非表示の情報がわかるような boolean 型の変数を用意している。

 更に下にある紫色の部分はアニメーション関連であり,アニメーションのスタート/ストップボタンと,アニメーションのスピードを決めるスライドバーが定義されている。 スタート/ストップボタンは,マーカーなどの表示・非表示切り替えボタンと同じく,一つのボタンで Start/Stop の両方の仕事をする。 そのため,専用の boolean 変数を持ち,ボタンが押された時の状況によって,アニメーションをスタートさせたり,逆にアニメーションを止めたりする。

 最後の茶色の部分オレンジ色の部分は,それぞれ拡大・縮小関連と距離を求めるためのボタンである。 茶色の部分では,fit, expand, contract の3つのボタンを定義している。 それぞれ,fit は kml_vector の範囲に合わせて表示範囲をフィットさせる,expand は縦横それぞれ2倍に拡大表示(中心は固定),contract は縦横共に1/2に縮小表示(中心は固定)となっている。 経路長ボタンは,経路の長さを計算して表示してくれる。 各ボタンが押された時の処理はすべて JavaScript に記載があるので,次に JavaScript ファイルである ol3ex19.js について見てみよう。

 以下に JavaScript を載せよう。 色は上記の web ページとは少しずれているが,ご容赦願いたい。 できるだけ同じ色を使うように努力はしているが…。
 灰色部分が変数や定数の定義であり, 青い部分は,センターマークや凡例に関連した部分, 茶色の部分はズーム関連である。 水色の部分はマーカーの吹出し関連であり, 緑色部分は不透明度変更に関連した部分となっている。 紫色の部分はアニメーション関連であり, オレンジ色の部分は距離計算に関連する部分である。 赤色の部分はマーカーやセンターマークなどの表示・非表示に関連する部分である。 その他が黒色となっている。 説明は web ページのソースと同じくこの下に書いておく。 ちなみに,ルートは六甲全山縦走のコースにしてある。
// ===================================================================
// Define a namespace for the application.
window.app = {};
var app = window.app;  // 中心マークや凡例の表示用
var map = null; // map 変数
var view = null;       // view 変数
var cyberJ = null;     // 地理院地図用の変数
// -------------------------------------------------------------------
var kml_vector = null; // KML ファイル用変数
var wp_vector = null;  // waypoint KML ファイル用変数
var kml_extent = null; // KML ファイルの領域範囲用変数
var current_extent = null; // 現在表示している extent
// -------------------------------------------------------------------
var kml_vector_visible_flag = true;
var wp_vector_visible_flag = true;
var centerMarker_visible_flag = true;
var titleSymbol_visible_flag = true;
// -------------------------------------------------------------------
var kmlVectorVisibleButton = null;
var wpVectorVisibleButton = null;
var centerMarkerVisibleButton = null;
var titleSymbolVisibleButton = null;
// -------------------------------------------------------------------
// for animation demo
var styles = null;       // styles for geoMarker
var animating = false;   // flag animating or not
var speed, now;
var speedInput = null;   // speed control Element
var startButton = null;  // start button Element
// -------------------------------------------------------------------
var routeCoords = null;  // route の座標配列
var routeLength = null;  // route の長さ
var geoMarker = null;    // 移動する点のマーカー
var vectorLayer = null;  // geoMarker を入れる vector Layer
var moveFeature = null;  // アニメーション用の関数用の変数
// -------------------------------------------------------------------
var center_lon = 135.083273333333; // 表示中心の経度(デフォルトは始点の経度)
var center_lat = 34.63374; // 表示中心の緯度(デフォルトは始点の緯度)
var kml_url  = "work/map_data20160430.kml";
var kml_url2 = "work/wp_data20160430.kml";
// -------------------------------------------------------------------
var initZoom = 15;  // ズームの初期値
var MinZoom  = 6;   // ズームの最小値(最も広い範囲)
var MaxZoom  = 21;  // ズームの最大値(最も狭い範囲)
var MinResolution  = 40075016.68557849/256/Math.pow(2, MaxZoom); // 最小解像度
var MaxResolution  = 40075016.68557849/256/Math.pow(2, MinZoom); // 最大解像度
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() {
// 以下の DOM の定義は,init_map() の中に入れないとだめだった。
    var container = document.getElementById('popup');
    var content   = document.getElementById('popup-content');
    var closer    = document.getElementById('popup-closer');
// -------------------------------------------------------------------
// for animation 以下の変数は init_map() 内で指定しないと値が入らない。
    speedInput = document.getElementById('speed');
    startButton = document.getElementById('start_animation');
    kmlVectorVisibleButton = document.getElementById('kml_vector_visible');
    wpVectorVisibleButton = document.getElementById('wp_vector_visible');
    centerMarkerVisibleButton = document.getElementById('centerMarker_visible');
    titleSymbolVisibleButton = document.getElementById('titleSymbol_visible');
// -------------------------------------------------------------------
// 表示用の view 変数の定義
// projection はデフォルトの EPSG:3857(球面メルカトル図法)となっている。
    view = new ol.View({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"
        })
    });
// 経路の KML データ
    kml_vector = new ol.layer.Vector({ source: new ol.source.Vector({ url: kml_url, format: new ol.format.KML({ showPointNames: false }) }) });
// マーカーの KML データ
    wp_vector = new ol.layer.Vector({ source: new ol.source.Vector({ url: kml_url2, format: new ol.format.KML({ showPointNames: false }) }) });
// -------------------------------------------------------------------
// 中央にセンターマーカー
    app.addCenterMarker = function(opt_options) {
        var options = opt_options || {};
        var anchor = document.createElement('a');
        var element = document.createElement('div');
        element.className = 'center-marker ol-unselectable';
        element.id = 'centerMarker';
        element.appendChild(anchor);
        ol.control.Control.call(this, { element: element, target: options.target });
    };
    ol.inherits(app.addCenterMarker, ol.control.Control);
// -------------------------------------------------------------------
// 左上にタイトル
    app.addTitleFig = function(opt_options) {
        var options = opt_options || {};
        var anchor = document.createElement('a');
        var element = document.createElement('div');
        element.className = 'title-fig ol-unselectable';
        element.id = 'title-fig';
        element.appendChild(anchor);
        ol.control.Control.call(this, { element: element, target: options.target });
    };
    ol.inherits(app.addTitleFig, ol.control.Control);
// -------------------------------------------------------------------
// 右下に凡例
    app.addSymbols = function(opt_options) {
        var options = opt_options || {};
        var anchor = document.createElement('a');
        var element = document.createElement('div');
        element.className = 'symbols ol-unselectable';
        element.id = 'symbols';
        element.appendChild(anchor);
        ol.control.Control.call(this, { element: element, target: options.target });
    };
    ol.inherits(app.addSymbols, ol.control.Control);
// -------------------------------------------------------------------
// 地図をクリックした際に,停留点の情報を表示するための overlay 変数(popup 用)
    var overlay = new ol.Overlay({ element: container });
// -------------------------------------------------------------------
// 地図変数 (map 変数) の定義。地理院地図を表示するように指定している
    map = new ol.Map({
        target: document.getElementById('map_canvas'),
        loadTilesWhileAnimating: true,
        layers: [cyberJ, kml_vector, wp_vector],
        view: view,
        overlays: [overlay],
        renderer: ['canvas', 'dom'],
        controls: ol.control.defaults().extend([new ol.control.ScaleLine()]),
        interactions: ol.interaction.defaults()
    });
// -------------------------------------------------------------------
    function displayFeatureInfo(pixel, coordinate) {
        var features = [];
        map.forEachFeatureAtPixel(pixel, function(feature, wp_layer) {
            features.push(feature);
        });
        if (features.length > 0) {
            var info = [];
            info.push('<div id="wp_desc" style="font-size:12px; width:215px">'+features[0].get('name')+features[0].get('description')+'</div>');
            overlay.setPosition(coordinate);
            content.innerHTML = info[0];
            container.style.display = 'block';
        } else {
            content.innerHTML = '';
            container.style.display = 'none';
        }
    };
    map.on('click', function(evt) { displayFeatureInfo(evt.pixel, evt.coordinate); });

// マーカー上でアイコンの表示を変更するイベントハンドラー, jQuery が必要
    $(map.getViewport()).on('mousemove', function(e) {
        var pixel = map.getEventPixel(e.originalEvent);
        var hit = map.forEachFeatureAtPixel(pixel, function(feature, layer) { return true; });
        if (hit) { map.getTarget().style.cursor = 'pointer'; }
            else { map.getTarget().style.cursor = ''; }
    });
// popup を閉じるためのイベントハンドラー
    closer.onclick = function() {
        container.style.display = 'none';
        closer.blur();
        return false;
    };
    map.addControl(new ol.control.ZoomSlider());
    map.addControl(new app.addCenterMarker());
    map.addControl(new app.addTitleFig());
    map.addControl(new app.addSymbols());

    view.setCenter(ol.proj.transform([center_lon, center_lat], "EPSG:4326", "EPSG:3857"));
    view.setZoom(initZoom);
    document.getElementById('opacity_control').innerHTML = initOpacity.toFixed(1);
// -------------------------------------------------------------------
// kml_vector に対する処理は,以下のようにイベント処理に入れないと,データが読まれずにうまくいかない。
    kml_vector.once('change',function() {
        kml_extent = kml_vector.getSource().getExtent();
        view.fit(kml_extent, map.getSize());
        current_extent = ol.extent.buffer(kml_extent,0); // duplicate kml_extent to current_extent

        var features = kml_vector.getSource().getFeatures(); // 通常の JavaScript の array
        for (var j=0; j < features.length; j++) {
            var coordArray = features[j].getGeometry().getCoordinates(); // geometry 変数の中の座標を配列へ(メルカトル座標系)
            if (j == 0) { routeCoords = coordArray; }
                   else { routeCoords = routeCoords.concat(coordArray); } // 全ルートの座標配列を結合
        }
        routeLength = routeCoords.length;

        geoMarker = new ol.Feature({ type: 'geoMarker', geometry: new ol.geom.Point(routeCoords[0]) });
        styles = { 'geoMarker': new ol.style.Style({
            image: new ol.style.Circle({ radius: 7, snapToPixel: false, fill: new ol.style.Fill({color: 'black'}), stroke: new ol.style.Stroke({ color: 'white', width: 2 }) })
            })
        };
        vectorLayer = new ol.layer.Vector({
            source: new ol.source.Vector({ features: [geoMarker] }),
            style: function(feature) {
                // hide geoMarker if animation is active
                if (animating && feature.get('type') === 'geoMarker') { return null; }
                return styles[feature.get('type')];
            }
        });
        map.addLayer(vectorLayer);
    }); // kml_vector.once('change',function()
// -------------------------------------------------------------------
    moveFeature = function(event) {
        var vectorContext = event.vectorContext;
        var frameState = event.frameState;

        if (animating) {
            var elapsedTime = frameState.time - now; // here the trick to increase speed is to jump some indexes on lineString coordinates
            var index = Math.round(speed / 1000 * elapsedTime / 2000 * routeLength); // 2000 --> 2sec で全行程
            if (index >= routeLength) { stopAnimation(true); return; }
            var currentPoint = new ol.geom.Point(routeCoords[index]);
            var feature = new ol.Feature(currentPoint);
            vectorContext.drawFeature(feature, styles.geoMarker);
// ----------------
            if (!(ol.extent.containsCoordinate(current_extent, routeCoords[index]))) { expandZoom(1,routeCoords[index]); }
            speed = speedInput.value; // update speed
// ----------------
        }
        map.render(); // tell OL3 to continue the postcompose animation
    };
// -------------------------------------------------------------------
    startButton.addEventListener('click', startAnimation, false);
} // 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 kml_vector_set_invisible() {
    kml_vector_visible_flag = false;
    kml_vector.setVisible(false);
    kmlVectorVisibleButton.textContent = "表示";
    kmlVectorVisibleButton.setAttribute("class","red");
}
function kml_vector_set_visible() {
    kml_vector_visible_flag = true;
    kml_vector.setVisible(true);
    kmlVectorVisibleButton.textContent = "隠す";
    kmlVectorVisibleButton.setAttribute("class","blue");
}
function kml_vector_visible() {
    if (kml_vector_visible_flag) { kml_vector_set_invisible(); }
                            else { kml_vector_set_visible(); }
}

function wp_vector_set_invisible() {
    wp_vector_visible_flag = false;
    wp_vector.setVisible(false);
    wpVectorVisibleButton.textContent = "表示";
    wpVectorVisibleButton.setAttribute("class","red");
}
function wp_vector_set_visible() {
    wp_vector_visible_flag = true;
    wp_vector.setVisible(true);
    wpVectorVisibleButton.textContent = "隠す";
    wpVectorVisibleButton.setAttribute("class","blue");
}
function wp_vector_visible() {
    if (wp_vector_visible_flag) { wp_vector_set_invisible(); }
                           else { wp_vector_set_visible(); }
}

function centerMarker_set_invisible() {
    centerMarker_visible_flag = false;
    document.getElementById('centerMarker').style.display = "none";
    centerMarkerVisibleButton.textContent = "表示";
    centerMarkerVisibleButton.setAttribute("class","red");
}
function centerMarker_set_visible() {
    centerMarker_visible_flag = true;
    document.getElementById('centerMarker').style.display = "block";
    centerMarkerVisibleButton.textContent = "隠す";
    centerMarkerVisibleButton.setAttribute("class","blue");
}
function centerMarker_visible() {
    if (centerMarker_visible_flag) { centerMarker_set_invisible(); }
                              else { centerMarker_set_visible(); }
}

function titleSymbol_set_invisible() {
    titleSymbol_visible_flag = false;
    document.getElementById('title-fig').style.display = "none";
    document.getElementById('symbols').style.display = "none";
    titleSymbolVisibleButton.textContent = "表示";
    titleSymbolVisibleButton.setAttribute("class","red");
}
function titleSymbol_set_visible() {
    titleSymbol_visible_flag = true;
    document.getElementById('title-fig').style.display = "block";
    document.getElementById('symbols').style.display = "block";
    titleSymbolVisibleButton.textContent = "隠す";
    titleSymbolVisibleButton.setAttribute("class","blue");
}
function titleSymbol_visible() {
    if (titleSymbol_visible_flag) { titleSymbol_set_invisible(); }
                             else { titleSymbol_set_visible(); }
}
// *******************************************************************
// ヒュベニの公式を使った距離計算
// 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() {
    var wgs84Sphere = new ol.Sphere(6378137);
    var measureLength = 0;
    var lineLength = 0;
    var features = kml_vector.getSource().getFeatures(); // 通常の JavaScript の array
    for (var j=0; j < features.length; j++) {
        var coordArray = features[j].getGeometry().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]); }
        for (var i=0; i < (coordArray.length-1); i++) { measureLength = measureLength + wgs84Sphere.haversineDistance(coordArray[i], coordArray[i+1]); }
    }
    var outstr;
    outstr = (Math.floor(measureLength)/1000)+" [km]";
    document.getElementById("outStr").innerHTML = "  L = "+(Math.floor(lineLength)/1000)+" [km]"+" (ol.haversine "+outstr+")";
}
// *******************************************************************
function setCenterZoom() {
    view.fit(kml_extent, map.getSize());
    current_extent = ol.extent.buffer(kml_extent,0); // duplicate kml_extent to current_extent
}
// *******************************************************************
function expandZoom(factor, center) {
    var cur_reso = view.getResolution();
    var new_reso = cur_reso/factor;
    new_reso = Math.min(new_reso, MaxResolution);
    new_reso = Math.max(new_reso, MinResolution);
    view.setCenter(center);
    view.setResolution(new_reso);
    current_extent = view.calculateExtent(map.getSize());
}
// *******************************************************************
function startAnimation() {
// ボタンはトグルになっているので,if (animating) が必要になる。
    if (animating) {
        stopAnimation(false);
    } else {
        animating = true;
        now = new Date().getTime();
        speed = speedInput.value; // max = 1000
        startButton.textContent = 'Cancel';
        startButton.setAttribute("class","boldblue");
        geoMarker.setStyle(null); // hide geoMarker
// ----------------
        kml_vector_set_visible();
        wp_vector_set_invisible();
        setCenterZoom(); // set extent to default
        expandZoom(2,routeCoords[0]); // set start point to the center of extent
// ----------------
        map.on('postcompose', moveFeature);
        map.render();
    }
}
// *******************************************************************
function stopAnimation(ended) {
    animating = false;
    startButton.textContent = 'Start';
    startButton.setAttribute("class","boldred");
    // if animation cancelled set the marker at the beginning
    var coord = ended ? routeCoords[routeLength - 1] : routeCoords[0];
    /** @type {ol.geom.Point} */ (geoMarker.getGeometry()).setCoordinates(coord);
    if (! ended) { setCenterZoom(); }
    map.un('postcompose', moveFeature); //remove listener
}
// *******************************************************************

 上から順に見ていこう。
まず,青色部分があるが,中心マークなどを表示するために,ウィンドウ情報を変数に入れている。 それに引き続き,しばらく global 変数の定義が並んでいる。 これらは複数の関数内で使われるため,global として定義されている。

 まずは map や view などの global 変数が定義されている。 定義されていると言っても,null としているので,global として場所を確保した,程度の定義である。 具体的な定義はずっと下の方に書いてある(はず)。 ここで,map は地図を表す変数であり,ol.Map クラスのインスタンスである。 view は地図の表示方法(範囲とか,投影法などを指定する)のための変数である。 cyberJ は地理院地図のための ol.layer.Tile クラスの変数である。 kml_vector, wp_vector は共に ol.layer.Vector クラスの変数であり,それぞれ KML 経路とマーカーの情報を持っている。

 次に茶色で kml_extent と current_extent が定義されている。 これらは,表示範囲(extent)のための変数であり,x軸,y軸の最小値と最大値を持っている。 kml_extent は KML 経路が含まれる範囲を示し,current_extent はある瞬間に画面表示されている範囲(正確にはこの範囲がすべて収まるように画面表示されているので,画面の全体よりも少し狭い範囲のことも多い)である。

 その下の赤色の部分は,マーカーやセンターマークなどの表示・非表示関連の変数である。 前半部分の xxxx_flag = ture; というのは,KML 経路,マーカー,センターマーク,Title/凡例,の状態を表す変数群であり, 全部 true になっているのは,すべてがデフォルトで「表示状態にある」ことを示している。 これらの変数が false にセットされると,該当する項目が非表示になったことを意味する(実際に非表示にするには, ここで false にするだけではなく,非表示にするための具体的な処理が必要)。 赤色の後半部分は,表示・非表示切り替えボタンを表す要素のための変数(DOM 変数)であり,とりあえず null(空っぽ)として定義されている。 要素(Element)の指定は,init_map() 関数の中で行う。

 次に紫色の部分で,アニメーション関連の変数が定義されている。 多くは global に定義したいのでここで定義されているが,とりあえず場所だけ確保,という感じで,空っぽで指定されているものが多い。 style は,アニメーションで動く黒丸のスタイルをいれるための変数である。 animating はアニメーションが動いているか動いてないか,を表す boolean 型の変数である。 speed はアニメーションを動かす速度(0~1000),now はアニメーションを動かし始めた時刻(1970/1/1, 00:00:00が起点)用の変数である。 speedInput と startButton はそれぞれスピード指定用のスライドバーとアニメーション開始ボタンを表す要素を入れるための変数である。
 後半は,routeCoords が経路の座標の配列であり,routeLength はその配列の大きさ(長さ)を表す。 geoMarker はアニメーションで移動する黒丸自体を表す変数であり,vectorLayer はgeoMarker を表示するための ol.layer.Vector である。 moveFeature はアニメーションで点の移動の処理を行う関数を表す変数である。

 アニメーション関連の後にある黒い部分は,表示領域の中心座標と KML データを示す URL である。 ここでは,始点は六甲全山縦走路の起点である須磨浦公園駅になっている。 その下には,ズームの初期値や,ズームの範囲指定,解像度の範囲指定,数字の表示の桁数,不透明度の初期値と取りうる範囲が指定されている。 解像度の範囲の指定には,ol.View の option にある minResolution の記載内容を参考にした。 最後に2点間の距離を求める際に使うヒュベニの公式で出てくる定数が定義されている。

------------------
 その下で水色の部分がある直前から init_map() という関数の定義が長々と書かれている。 init_map() 関数は,web の <body> が読み込まれる際に実行される関数であり,地図の初期設定が書かれている。 まずは水色の3行だが,これは吹き出し(popup)のための web 要素を指定している。 次に紫色赤色が並んでいるが, これらもそれぞれ,アニメーション開始ボタンとスピード用スライドバーマーカーやセンターマークなどの表示・非表示切り替えボタン,を示す web 要素が代入されている。

 その下の黒い部分では,まず地図の表示範囲などを決める ol.View クラスの変数 view が定義されている。 ここでは,ズームの最大値と最小値を指定している。 中心点やズームの設定は map 変数を定義したあとで設定している。 それに引き続き cyberJ という ol.layer.Tile クラスの変数を定義している。 これは地理院地図を表示するための変数である。 中身は opacity(不透明度)と source の指定である。 source は ol.source.XYZ クラスのものが指定されている。 地理院地図を使うにはこのように記述する。

 cyberJ につづいて,kml_vector と wp_vector という2つの ol.layer.Vector が指定されている。 中身は KML フォーマット(ol.format.KML)を持つ ol.layer.Vector クラスのインスタンスと定義されている。 注意点として,kml_vector と wp_vector はここで定義されているが,定義された時点ですぐに読み込まれるわけではなさそう,という点があげられる。 例えば,kml_vector の点のデータを使いたいと思って,この定義式のすぐ後で
kml_vector.getSource().getFeatures().getGeometry().getCoordinates();
としても,データを取得することはできないことが多い。 どうやら JavaScript の仕様のようなのだが,kml_vector 内のデータを使うにはイベント処理を使うという工夫が必要になっている。

 kml_vector,wp_vector に引き続いて青い部分がある。 ここでは,センターマーク,タイトル,凡例の表示のための関数, app.addCenterMarker, app.addTitleFig, app.addSymbols,の設定を行っている。 基本的には web 要素を作り,web ページのヘッダー部分で定義したどの設定を使うか,などの指定を行っている。 実際に地図上に表示するためにはこれらの関数を呼び出さないといけないが,それは map の定義の後で行っている。

 次に来る水色の部分吹き出し(popup)用の overlay の定義である。 ここでは container という web 要素に入れなさい,というぐらいしか情報は入っていない。

 そして,やっと ol.Map クラスの map という一番重要な変数の定義がある。 これが地図の本体を表している。 その中では,web のどの要素(ここでは <div>)に地図を表示するのか,表示する Layer(層)は何があるのか,などが書かれている。 ここで target が地図を表示する web 要素を表している。指定は map_canvas という id を持つ要素になっている。 その次の「loadTilesWhileAnimating」だが,これはアニメーション動作時に Tile 形式のデータを読み込むかどうか,を示している。 この値の真偽(true か false か)の違いがいまいちわからないのだが,これがないとアニメーション動作時に移動先の地図が読み込まれないのではないかと思っている。 layer としては,地理院地図を表す cyberJ と2個の KML データが指定してある。 layer などいくつかの項目に関しては,この map の定義の後で,map.addLayer() のようにして追加することも可能である。

 他にも overlay,renderer,controls,interactions などが定義されている。 overlay は地図に重ねて描く要素で,配置は地図上の座標(緯度や経度)で指定される(と思っている)。 一方 controls は画面操作などの要素のためのもので,配置は画面(地図 window)のどこか,で決まり,地図上の場所を移動しても画面の同じ場所から動かない。 配置としては,画面の左上とか右下という指定の仕方ができる。 ここではデフォルトの controls にスケールラインを加えている。 ちなみにデフォルトの controls は
  ol.control.Zoom
  ol.control.Rotate
  ol.control.Attribution
の3つであり,他に,FullScreen,MousePosition,OverviewMap,ScaleLine,ZoomSlider,ZoomToExtent,があるらしい。

interactions は controls やマウスの操作を行った際に地図をどう変形させるか,のようなものが入る。 拡大や縮小,回転,線を描く,マウスのホイールで拡大する,などがある。 ここではデフォルトを指定しており,以下のものが含まれる。
  ol.interaction.DragRotate
  ol.interaction.DoubleClickZoom
  ol.interaction.DragPan
  ol.interaction.PinchRotate
  ol.interaction.PinchZoom
  ol.interaction.KeyboardPan
  ol.interaction.KeyboardZoom
  ol.interaction.MouseWheelZoom
  ol.interaction.DragZoom

 map の定義に続く水色の部分吹き出し(popup)を表示するための具体的な処理が書かれている。 最初の displayFeatureInfo 関数では,吹き出しに表示する位置などの情報を指定している。 その際,引数として,画面上の座標と地図上の座標(緯度,経度)の両方が必要となっている。 吹き出し(popup)の表示は,map.on() というイベント処理の関数で定義されている。 ここでは map 上でマウスクリックされると,その点(evt)の座標と緯度・経度を引数として displayFeatureInfo を呼び出せ,となっている。

 2個目のグループでは,$(map.getViewport()).on として,マウスがマーカー上に来た時に,矢印から指先マークに変えるための処理である。 そして closer.onclick として,吹き出しの右上の×じるしをクリックした際の処理について書かれている。

 水色の領域に引き続いて,地図に幾つかの要素が加えられている。 まずは茶色で,control としてズームスライダーが加えられている。 また,センターマーク,タイトル,凡例も control として加えられている。 これらは control として加えられているので,地図上で移動しても,いつも画面の同じ位置に表示されている。

 そして view.setCenter と view.setZoom で,表示の中心座標(緯度・経度)と拡大値(zoom)が指定されている。

 まだ init_map() 関数は続く。

 次は kml_vector 内の要素を取り出した処理が書かれている。 そこでは,
kml_vector.once('change',function() {
として,イベント処理が使われている。 これは kml_vector の定義の所でも書いたが,定義してもすぐには kml_vector にデータが読み込まれない。 そのため,データが読み込まれてから(変更が加えられてから)処理をしたいので,変化があった(change)なら,以下の処理をしなさい,と定義されている。

 kml_vector へのイベント処理の最初は,茶色で書かれた表示範囲の自動設定である。 そこでは,
kml_extent = kml_vector.getSource().getExtent();
として,kml_vector のデータのある範囲(矩形)を読み取り,view.fit() 関数でフィットさせている。 また,現時点ので表示範囲(current_extent)に kml_extent の値を入れている。 ただし,current_extent = kml_extent としてはいけない。 もし current_extent = kml_extent とすると,current_extent と kml_extent が同じものになり,一方を変更すると,他方も一緒に変更されてしまう。 ここでは kml_exent はいつも kml_vector の広がっている範囲を示してほしいので,ol.extent.buffer 関数を使って内容のみをコピーしている。

 その下にある紫色の部分はアニメーション関係である。 ここでは,kml_vector の経路に沿って黒丸を動かしている。 そのために kml_vector の経路点の座標(緯度・経度)の配列を取り出している。 kml_vector の中に経路が複数含まれる場合もあるので,複数の feature があると仮定して処理が書かれている。

 さらに続く紫色の部分で,geoMarker と vectorLayer が定義され,地図に加えられている(map.addLayer(vectorLayer);)。 ここで geoMarker は移動していく黒丸を表す変数であり,その geoMarker は vectorLayer という名の layer(層)内で定義されている。

 kml_vector.once('change',function() の後にも紫色の部分があるが, そこでは moveFeature として,アニメーションの際に実際に黒丸を動かす処理が書かれている。 ここでは,アニメーションスタート時の時刻を now に入れ,この moveFeature 関数が呼びだされた時刻(frame.State.time)から now を引くことで, アニメーションの経過時間(elapsedTime)を求めている。 そして,経過時間に speed を含む適度な数値をかけることで,その時点で黒丸がいるべきポイント(点の番号を index で指定)を得ている。 ここでは,最高速度(speed の値が 1000 の時)で 2 sec でアニメーションが終わるように設定している。

 また,「// ----------------」で挟まれた所では,表示位置の調整を行っている。 もし黒丸の位置が表示範囲を出ていたら,中心座標を移動させる処理を行っている。

 最後に startButton がクリックされた際のイベント処理を map に加えて init_map() 関数が終わる。

------------------
 init_map() 関数に続いていくつかの関数が定義されている。 まず,緑色で書かれた部分では,地理院地図の不透明度(opacity)を変更するためのルーチンが書かれている。 web 上の矢印に応じて,現在の不透明度に,引数 opacity の値を加え,その結果を cyberJ に当てはめている。 もし範囲を越えていれば,範囲内の収まるようにしている。

 次に赤色で,マーカーやセンターマークなどの表示・非表示の切り替えボタンの処理が書かれている。 4つのボタンに対して,それぞれ,対象となる要素(マーカーやセンターマークなど)を非表示にする関数と,逆に表示する関数が定義されて, 最後に,ボタンが押された際に実行される関数が定義されている。 非表示にする関数では,表示・非表示状態を表す変数に false を入れ,対象となる要素を見えなくし(setVisible(false);), ボタンの文字列を赤色の「表示」に変更している。
 表示する関数では,逆に,表示・非表示状態を表す変数に trueを入れ,対象となる要素を見えるようにし(setVisible(true);), ボタンの文字列を青色の「隠す」に変更している。
 ボタンが押された際に実行される関数は,表示・非表示状態を表す変数が表示状態(true)なら非表示関数を, 非表示状態(false)なら表示する関数を選ぶようにしている。

 次のオレンジ色の部分は経路の長さを求める関数が書かれている。 具体的には,dist_2pts() という関数と,length_line() という2つの関数が書かれている。 初めの dist_2pts() という関数は,ヒュベニの公式を使って,2つの点の座標(緯度・経度)から2点間の距離を求めている。
 2個目の length_line() という関数では,2つの方法で経路の距離を求めている。 一つ目の方法は上記のヒュベニの公式を用いるものである。 もう一つはOpenLayers の ol.Sphere に対して定義された haversineDistance() という関数を使う方法である。 具体的には,kml_vector 内の全経路(全 feature)から経路点の座標(経度・緯度)を配列として取り出し,投影法を球面メルカトル図法から通常の緯度・経度による表示法(WGS 84)に変換している。 そして,上記の2つの方法で各2点間の距離を求めて足し上げることで経路全体の距離を求めている。 ヒュベニの公式とhaversineDistance() 関数の比較については,その17:OpenLayers v3 で 経路の距離を測るを見て欲しい。

 距離計測に続いては,茶色で書かれた zoom 関連の関数である。 ここでは setCenterZoom() という関数と,expandZoom() という2個の関数が書かれている。 setCenterZoom() 関数は,init_map() の中で得られた kml_extent という範囲に表示をフィットさせ(KML 経路全体が表示されるように表示範囲をフィット), 現在の表示範囲(current_extent)に kml_extent の値をコピーしている。
 expandZoom() 関数は,引数に拡大倍率と中心座標(球面メルカトル座標)を持つ。 処理は,現在の解像度を取得し,それを拡大倍率で割り,それが解像度の範囲指定内になるようにしている。 そして,中心位置と解像度を設定している。 また,移動する黒丸が表示範囲を出たかどうかを判定できるように,current_extent にも値を入れている。

 最後に紫色で書かれたアニメーション関係の関数が2個書かれている。 それらは startAnimation と stopAnimation である。 startAnimation はアニメーション開始ボタンが押された時に行う処理が書かれている。 処理はアニメーションが動いている最中かそうでないか,によって,条件分けがなされている。 アニメーションが動いている最中の場合は,アニメーションを途中で止める事になるので,false という引数を持って stopAnimation を呼び出している。 アニメーションが動いていない場合は,アニメーションを動かすための処理が書かれている。 具体的には,animating という変数を true にし,開示時刻を now 変数に入れ,speed の値を取得している。 また,スタートボタンの表示を赤色の「Cancel」に変え,geoMarker を見えなくしている。 そして,KML 経路の線を表示させ,カメラマーカーを非表示にし,表示範囲を kml_extent の半分の領域にして(面積では1/4),経路の始点を表示画面の中心に持ってきている。 そして,最後に map.on('postcompose', moveFeature); として,moveFeature 関数を呼び出している。
 stopAnimation では,スタートボタンの表示をもとに戻し,moveFeature 関数を呼び出すイベント処理をキャンセルしている。 また,表示範囲は,アニメーションが最後まで行って止まった場合(ended == true)は拡大したままにし, 途中でキャンセルされた場合は,KML データが全部含まれるように表示範囲を変えている。

以下に具体例を載せてあるので,具体的な表示を見てみて欲しい。(ちなみにサンプルページはアクセスログを取るルーチンを組み込んでいます)
 「その19」のサンプル
サンプルでは,ルート,マーカー,センター,Title/凡例の各ボタンを押して,それぞれの動作を確認してみて欲しい。 さらに,ルートを非表示にしてから,アニメの Start ボタンを押して欲しい。すると,画面が拡大されて,黒丸が動き始めるのがわかる。 この際,強制的にルートが表示状態になり,カメラマークが強制的に非表示になっているのがわかると思う。 そのままアニメーションを続けると時間がかかるので,一度 Cancel ボタンを押してみよう。 すると,表示は KML 経路全体が見える範囲に変わるはずである。
 今度はスピードのスライドバーを右の方に寄せてから,アニメの Start ボタンを押して欲しい。 かなりの速度で黒丸が動き,あっという間に終点に着くと思う。 終点に着いても,表示は同じ解像度のままになっているはずである。

その20:Open Street Map を表示させるに続く

0 件のコメント: