2017年4月22日土曜日

OpenLayers 3 を使ってみよう(その21:Highcharts でルートの標高グラフを表示)

 これは,OpenLayers 3 を使ってみよう(その20:Open Street Map を表示させる)からの続きになる。 OpenLayers 3 を使ってみよう(その0:はじめに:地理院地図を表示)に目次がある。 その13までのページでは OpenLayers v3.7.0 で書いてきたが,ここでは OpenLayers 3.20.1 で書かれている。
 前回(その20:Open Street Map を表示させる)は,その19:その9からその18までのまとめに Open Street Map の表示を追加してみた。 今回は,Highcharts というグラフ表示のスクリプトを使って,経路の標高を表示させよう。

 Highcharts は,株式の価格の時間変化などを表示させるために作られたような感じがするグラフ表示のスクリプトである。 Highstock デモには株式の時間変化を表示させるデモが載っている。 一方,普通のグラフの表示のデモも Highcharts デモにある。 Highcharts デモを見るといろいろなグラフの例を見ることができる。 今回はその基本的なルーチンを使って,経路の標高図を表示させたいと思った。

 標高図を表示するには標高のデータが必要となる。 どのように与えるか迷ったが,今回は KML ファイルの各点のデータに標高データを追加したを使うことにした。 ただし,標高データ以外に始点からの距離のデータも加えているため,もはや KML データとは呼べない形式になってしまったが…。

 以下に標高データの例を書いておこう。
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
  <Folder>
    <name>Tracks</name>
    <Placemark>
      <LineString>
        <coordinates>
          135.09789,34.6369083333333,31,0,0,0,0
          135.09787,34.6369033333333,31,1.91584195605755,1.91584195605755,0,0
          135.097751666667,34.6368966666667,31.4,12.7909527115746,12.7983064721845,0.399999999999999,0
          135.097728333333,34.6369483333333,31.7,18.908813559768,18.9235184160978,0.699999999999999,0
          ......
        </coordinates>
      </LineString>
    </Placemark>
  </Folder>
</Document>
</kml>
ここで,<coordinates> が経路を表している。 左から,経度,緯度,高度,水平距離,沿面距離,累積上昇高度,累積下降高度となっている。 正しい KML ファイルなら,経度,緯度,高度までにしておかないといけないはず。 このデータと,正しい KML データと,写真を撮ったりした停留点用の KML ファイルを使って地図を描こう。

 まず,web ページのソースを載せよう。 web ページのソース部分は,灰色部分は web の基本的な要素であり,赤色太字部分が Highcharts 関連の部分である。 赤色部分は JavaScript 関連の部分である。 青い部分は,センターマークや凡例と web のタイトルであり,茶色の部分はズーム関連である。 水色の部分はマーカーの吹出し関連であり,緑色部分は不透明度変更に関連した部分, 紫色の部分はアニメーション関連であり, オレンジ色の部分は Open Street Map に関連する部分である。 その他が黒色となっている。 説明はソースコードに下に書こう。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<!-- Written by Matsup 2017/04/09 -->
<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" type="text/css" href="http://openlayers.org/en/v3.20.1/css/ol.css">
<script src="http://openlayers.org/en/v3.20.1/build/ol.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></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: 36px; width: 36px;
       margin-top: -18px;  margin-left: -18px;
       z-index: 100000;
       background: url(../../cj_map/icons/mapCenterIcon2.svg) 50% 50% no-repeat;
   }
   .title-fig { position: absolute; top: 0px; left: 40px; }
   .title-fig a { display: block;
       height: 21px; width: 307px;
       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: with height figure</title>
<script src="//code.jquery.com/jquery-3.2.0.min.js" type="text/javascript"></script>
<script src="ol3ex21.js" type="text/javascript"></script>
</head>

<body onload="init_map()">
<div id="map_canvas" style="width: 100%; height: 97%; position:absolute; top:29px; 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 id="map_canvas2" style="width: 100%; height: 0%; position:absolute; bottom:0px; left:0px; font-size:100%;"></div>

<div style="font-size:85%">
  <b>&nbsp;不透明度:
  <a title="decrease opacity" href="javascript: changeOpacity(-0.2);">&lt;&lt;</a>
  <span id="opacity_control">1.0</span>
  <a title="increase opacity" href="javascript: changeOpacity(0.2);">&gt;&gt;</a></b>

  <input type="radio" name="select_map" id="cyberjMap" value="0" checked>地理院地図
  <input type="radio" name="select_map" id="osmMap" value="1">OpenStreetMap

  <button id="altitude_visible" onclick="altitude_visible();" class="red">標高図表示</button>
  <input id="alt_area" type="range" min="17" max="80" step="1" value="24" onInput="area_slider();">

  <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>
  <button id="titleSymbol_visible" onclick="titleSymbol_visible();" class="blue">Title/凡例隠す</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>
</div>
</body>
</html>
この web ページのソースの多くはその20のものとほぼ同じなので,そちらも見てほしい。 今回の Highcharts による標高表示に関連した部分は赤色太字部分になっている。 ここでは,
 (1) 標高図用の <div>(id = "map_canvas2")を用意した。
 (2) 標高図の表示ボタンと表示域の拡大縮小用のスライダーを用意した。
の2点である。
 また経路の距離を求めるルーチンは省略している。 それ以外はその20 と同じとなっている(はず…)。

 以下に JavaScript を載せよう。 こちらもその20のものとほぼ同じで,Highcharts による標高表示に関連した部分を赤色太字にしている。
他の色は,灰色部分が変数や定数の定義であり, 青い部分は,センターマークや凡例に関連した部分, 茶色の部分はズーム関連である。 水色の部分はマーカーの吹出し関連であり, 緑色部分は不透明度変更に関連した部分となっている。 紫色の部分はアニメーション関連であり, オレンジ色の部分は Open Street Map に関連する部分である。 赤色の部分はマーカーやセンターマークなどの表示・非表示に関連する部分である。 その他が黒色となっている。 説明は 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 osm = null;        // Open Street Map 用の変数
// -------------------------------------------------------------------
var chart = null;      // 標高図変数
var alt_array = null;  // 高度表示用配列
var altArray = null;   // 高度表示用配列
// -------------------------------------------------------------------
var kml_vector = null; // KML ファイル用変数
var wp_vector = null;  // waypoint KML ファイル用変数
var kml_extent = null; // KML ファイルの領域範囲用変数
var current_extent = null; // 現在表示している extent
// -------------------------------------------------------------------
var altitude_visible_flag = false;
var kml_vector_visible_flag = true;
var wp_vector_visible_flag = true;
var centerMarker_visible_flag = true;
var titleSymbol_visible_flag = true;
// -------------------------------------------------------------------
var mapCanvasDiv = null;   // 地図表示 div
var mapCanvas2Div = null;  // 標高図表示 div
var altitudeVisibleButton = null;     // 標高ボタン
var areaInput = null; // 標高図の高さ用スライダー
var kmlVectorVisibleButton = null;    // ルートボタン
var wpVectorVisibleButton = null;     // マーカーボタン
var centerMarkerVisibleButton = null; // 中心マーカーボタン
var titleSymbolVisibleButton = null;  // 凡例表示ボタン
var cyberjMapButton = null; // 地理院地図表示ボタン
var osmMapButton = null;    // OSM 表示ボタン
// -------------------------------------------------------------------
// for animation demo
var styles = null;       // styles for geoMarker
var animating = false;   // flag for animating or not
var speed, now;          // animation speed
var speedInput = null;   // animation speed control Element
var startButton = null;  // animation 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.09789; // 表示中心の経度(デフォルトは始点の経度)
var center_lat = 34.6369083333333; // 表示中心の緯度(デフォルトは始点の緯度)
var kml_url   = "work/o3cjmap_data_20161123.kml"; // ルートのkml
var kml_url_w = "work/o3cjmap_wpdata_20161123.kml"; // waypointのkml
var kml_url_a = "work/o3cjmap_data_20161123.kml.ele"; // 標高用kml
// -------------------------------------------------------------------
var initZoom = 15;   // ズームの初期値
const MinZoom  = 5;  // ズームの最小値(最も広い範囲)
const MaxZoom  = 19; // ズームの最大値(最も狭い範囲)
const MinResolution  = 40075016.68557849/256/Math.pow(2, MaxZoom); // 最小解像度
const MaxResolution  = 40075016.68557849/256/Math.pow(2, MinZoom); // 最大解像度
var initPrecision = 8;   // 座標表示の小数点以下の桁数の初期値
var initOpacity = 1.0;   // 不透明度の初期値
const gMaxOpacity = 1.0; // 不透明度の最大値
const gMinOpacity = 0.0; // 不透明度の最小値
// *******************************************************************
function init_map() {
// 以下の DOM の定義は,init_map() の中で
    let container = document.getElementById('popup');
    let content   = document.getElementById('popup-content');
    let 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');
    cyberjMapButton = document.getElementById('cyberjMap');
    osmMapButton = document.getElementById('osmMap');
    altitudeVisibleButton = document.getElementById('altitude_visible');
    mapCanvasDiv  = document.getElementById('map_canvas');
    mapCanvas2Div = document.getElementById('map_canvas2');
    areaInput = document.getElementById('alt_area');
    areaInput.style.display = "none";
// -------------------------------------------------------------------
// 表示用の view 変数の定義
// projection はデフォルトの EPSG:3857(球面メルカトル図法)となっている。
    view = new ol.View({maxZoom: MaxZoom, minZoom:MinZoom});
// Open Street Map の変数
    osm = new ol.layer.Tile({
        source: new ol.source.OSM()
    });
// cyberJ の opacity をいじるために,cyberJ という変数に入れている。
    cyberJ = new ol.layer.Tile({
        opacity: initOpacity,
        source: new ol.source.XYZ({
            attributions: [ new ol.Attribution({ html: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>国土地理院</a>" }) ],
            url: "https://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_url_w, format: new ol.format.KML({ showPointNames: false }) }) });
// -------------------------------------------------------------------
// 中央にセンターマーカー
    app.addCenterMarker = function(opt_options) {
        let options = opt_options || {};
        let anchor = document.createElement('a');
        let 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) {
        let options = opt_options || {};
        let anchor = document.createElement('a');
        let 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) {
        let options = opt_options || {};
        let anchor = document.createElement('a');
        let 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 用)
    let overlay = new ol.Overlay({ element: container });
// -------------------------------------------------------------------
// 地図変数 (map 変数) の定義。地理院地図を表示するように指定している
    map = new ol.Map({
        target: document.getElementById('map_canvas'),
        loadTilesWhileAnimating: true,
        layers: [osm, 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) {
        let features = [];
        map.forEachFeatureAtPixel(pixel, function(feature) {
            features.push(feature);
        });
        if (features.length > 0) {
            let 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) {
        let pixel = map.getEventPixel(e.originalEvent);
        let 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());
    osm.setVisible(false);

    view.setCenter(ol.proj.transform([center_lon, center_lat], "EPSG:4326", "EPSG:3857"));
    view.setZoom(initZoom);
    document.getElementById('opacity_control').innerHTML = initOpacity.toFixed(1);
// -------------------------------------------------------------------
// tile Layer の切り替え
    cyberjMapButton.onclick = function() { cyberJ.setVisible(true); osm.setVisible(false); changeOpacity(0); };
    osmMapButton.onclick    = function() { cyberJ.setVisible(false); osm.setVisible(true); changeOpacity(0); };
// -------------------------------------------------------------------
// 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

        let features = kml_vector.getSource().getFeatures(); // 通常の JavaScript の array

        for (let j=0; j < features.length; j++) {
            let 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);
// .........................
// Highcharts による標高図の設定
    let req = new XMLHttpRequest(); // HTTPでファイルを読み込むためのXMLHttpRrequestオブジェクトを生成
    req.open('GET', kml_url_a, true);
    req.setRequestHeader('Accept', 'application/vnd.google-earth.kml+xml');
    try {
        req.overrideMimeType('text/xml'); // unsupported by IE
    } catch (e) { }
// 標高データの読み込みとグラフの設定
    req.onreadystatechange = function () {
        if (req.readyState !== 4) return;
        if (req.status === 200) {
            alt_array = getAltArray(req.responseXML); // 準備ができたら読み込む
            altArray  = getAltArray(req.responseXML); // chart 表示用とカーソル移動用を別にしないとアニメの後でalt_array 消える
        }
// Highcharts でグラフを描く。mouseOver で地図上の geoMarker を動かす
        chart = Highcharts.chart('map_canvas2', {
            chart: { type: 'line', spacingBottom: 8, spacingTop: 8 },
            legend: { enabled: false }, // 凡例非表示
            title: { text: '', floating: true },
            xAxis: { crosshair: true, title: { text: 'creep distance (km)' }, min: 0 }, // crosshair は縦線
            yAxis: { title: { text: 'altitude (m)' } },
            tooltip: {
                positioner: function () {
                    return { x: Math.round(this.chart.chartWidth/20), y: -1 } // x: left aligned, y: align to title
                },
                borderWidth: 0,
                backgroundColor: 'none', // tooltip の透明化
                shadow: false,           // 指し棒と枠が消える
                headerFormat: '<b>{series.name}</b><br>',
                pointFormat: '{point.x:.2f} km: {point.y:.2f} m'
            },
            plotOptions: {
                line: {marker: { enabled: false }}, // 点にマーカーなし
                series: {
                    marker: {
                        states: {
                            select: { fillColor: 'red', radius: 6, lineWidth: 0 }
                        }
                    },
                    point: {
                        events: {
                            mouseOver: function () {
                                let ii=0;  do { ii++; } while (this.x > altArray[ii][0]);  // seriesのdata定義用と別にしないと,chart.series[0].data[i].select();で消える
                                geoMarker.setGeometry(new ol.geom.Point(routeCoords[ii])); // geoMarker を動かす
                            },
                            mouseOut: function () {
//                              geoMarker.setGeometry(new ol.geom.Point(routeCoords[0])); // マウスを外すと始点へ(コメントアウトしてある)
                            }
                        }
                    }
                }
            },
            series: [{ name: 'Altitude', color: "#FF0000", data: alt_array }] // 線の情報
        });
    }; // req.onreadystatechange = function...
    req.send(null);
// .........................
    }); // kml_vector.once('change',function()
// -------------------------------------------------------------------
    startButton.addEventListener('click', startAnimation, false);
// -------------------------------------------------------------------
} // init_map()
// *******************************************************************
moveFeature = function(event) {
    let vectorContext = event.vectorContext;
    let frameState = event.frameState;

    if (animating) {
        let elapsedTime = frameState.time - now; // here the trick to increase speed is to jump some indexes on lineString coordinates
        let index = Math.round(speed / 1000 * elapsedTime / 2000 * routeLength); // 2000 --> 2sec で全行程
        if (index >= routeLength) { stopAnimation(true); return; }
        let currentPoint = new ol.geom.Point(routeCoords[index]);
        let feature = new ol.Feature(currentPoint);
        vectorContext.drawFeature(feature, styles.geoMarker);
        if (index > 0) { chart.series[0].data[(index-1)].select(false); }
        chart.series[0].data[index].select(true);
// ----------------
        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
};
// *******************************************************************
// 標高データの読み込み (from xxx.kml.ele)
function getAltArray(xml) {
    let inline = '';
    let inlines = [];
    let coords = [];
    let el = xml.getElementsByTagName('Folder'); // 入れ子構造になっている
    for (i=0; i < el.length; i++) {
        let el1 = el[i].getElementsByTagName('Placemark');
        for (j=0; j < el1.length; j++) {
            let el2 = el1[j].getElementsByTagName('LineString');
            for (k=0; k < el2.length; k++) {
                let el3 = el2[k].getElementsByTagName('coordinates'); // 値を読み取る部分
                for (l=0; l < el3.length; l++) {
                    for (m=0; m < el3[l].childNodes.length; m++) {
                        inline = inline + el3[l].childNodes[m].nodeValue; // 一度長い一つの文にする
                    }
                }
            }
        }
    }
    inlines = inline.split(/[\s\n]+/); // 改行で分割
    for (i = 0; i < inlines.length; i++) {
        let ll = inlines[i].split(','); // コンマで分割
        if (ll.length < 2) { continue; }
        coords.push([Math.round(parseFloat(ll[4])/10)/100, parseFloat(ll[2]),i]); // creep distance を使う。[3] なら flat dist.
    }
    return coords;
}
// *******************************************************************
// 地理院地図 (var cyberJ) の opacity(この場合は不透明度) を変える
function changeOpacity(opacity) {
    let newOpacity = (parseFloat(document.getElementById('opacity_control').innerHTML) + opacity).toFixed(1); // 新しい opacity の値を求める
    newOpacity = Math.min(gMaxOpacity, Math.max(gMinOpacity, newOpacity)); // 最大値と最小値の範囲を超えないように
    if (document.getElementById("cyberjMap").checked) { cyberJ.setOpacity(newOpacity); } else { osm.setOpacity(newOpacity); }
    document.getElementById('opacity_control').innerHTML = newOpacity.toFixed(1); // opacity の数字の表示書き換え
}
// *******************************************************************
function area_slider() {
    mapCanvasDiv.style.height = (97 - areaInput.value)+"%";
    mapCanvas2Div.style.height = areaInput.value+"%";
    map.updateSize();
    chart.reflow();
    setCenterZoom(); // set extent to default
}
// *******************************************************************
function altitude_set_invisible() {
    mapCanvasDiv.style.height="97%";
    mapCanvas2Div.style.height="0%";
    areaInput.style.display="none";
    map.updateSize();
    chart.reflow();
    altitude_visible_flag = false;
    altitudeVisibleButton.textContent = "標高図表示";
    altitudeVisibleButton.setAttribute("class","red");
}
function altitude_set_visible() {
    mapCanvasDiv.style.height="72%";
    mapCanvas2Div.style.height="24%";
    areaInput.style.display="inline";
    areaInput.value=24;
    map.updateSize();
    chart.reflow();
    altitude_visible_flag = true;
    altitudeVisibleButton.textContent = "標高図隠す";
    altitudeVisibleButton.setAttribute("class","blue");
}
function altitude_visible() {
    if (altitude_visible_flag) { altitude_set_invisible(); }
                          else { altitude_set_visible(); }
}
// *******************************************************************
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 = "Title/凡例表示";
    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 = "Title/凡例隠す";
    titleSymbolVisibleButton.setAttribute("class","blue");
}
function titleSymbol_visible() {
    if (titleSymbol_visible_flag) { titleSymbol_set_invisible(); }
                             else { titleSymbol_set_visible(); }
}
// *******************************************************************
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) {
    let cur_reso = view.getResolution();
    let 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
    let coord = ended ? routeCoords[routeLength - 1] : routeCoords[0];
    /** @type {ol.geom.Point} */ (geoMarker.getGeometry()).setCoordinates(coord);
    if (! ended) { setCenterZoom(); }
    map.un('postcompose', moveFeature); //remove listener
}
// *******************************************************************

標高図に関連する部分(赤の太字部分)を上から見ていくと,
(1) chart, alt_array, altArray を定義している。
 これは標高図を表示するための chart 変数と,標高データ用の2個の配列変数を用意している。
 配列が2個あるのは,下記の(4)を見てほしい。

(2) mapCanvasDiv, mapCanvas2Div, altitudeVisibleButton, areaInput を定義している。
 これらは表示・非表示を切り替えれるように変数として定義している。

(3) init_map() の中で altitudeVisibleButton, mapCanvasDiv 等に値を入れている。
 これらは init_map() の中で代入しないとうまくいかなかった。

(4) 「// Highcharts による標高図の設定」から始まる部分で標高データを読み込み,Highcharts でグラフにしている。
 ここではまずファイル読み込みのために「req」という変数を XMLHttpRequest() として定義している。
次に「req.open」でファイルをオープンし,「req.setRequestHeader」でデータを要求している。
その次の try 関数の中で「req.overrideMimeTiyp」としている部分はいまいちわかってない。

 次に「req.onreadystatechange」の中でデータを読み出している。
より具体的にはもっと下で定義している「getAltArray() 関数」を呼び出して,alt_array 配列と altArray 配列にデータを入れている。
alt_array 配列は標高図のグラフ用 (series) に用意している。altArray 配列はマウスを近づけた時に各点にマーカー(赤丸)を表示するために使っている。
ここで altArray 配列を別に用意したのは,グラフ表示用とカーソル移動用を別にしないとアニメーション後に alt_array 配列が消えてしまうから,である。
なぜそうなるのかはわかっていない。誰か教えて〜

 その後,「chart = Highcharts.chart() 関数」で図を定義している。
オプションとして以下のように定義している。 それぞれに関する Document は Highcharts API reference に載っている。
 ・chart オプション:グラフのタイプは line で,上下に 8 ピクセルの隙間を開けている。
 ・legend オプション:凡例は非表示である。
 ・title オプション:タイトルテキストはなしにしている。
 ・xAxis オプション:最小値はゼロとし,軸タイトルを「creep distance (km)」としている。
     また,カーソルを持っていった際に縦に細い線を表示する(crosshair)と指定している。
 ・yAxis オプション:軸タイトルのみ定義している。
 ・tooltip オプション:グラフの線上にカーソルを持っていった際に,吹き出しの形で値を表示してくれる。
     ここでは,画面の左上に固定して線の名称(series.name (今回は Altitude))と,x 座標,y 座標を表示させている。
     枠や指し棒はなしで,吹き出しの背景色は透明にしている。
 ・plotOptions オプション:グラフ表示のオプションの指定
     ラインやマーカーの指定をしている。
     line 上の各点にマーカーは表示していない(線のみの表示)。
     「series:」として,各点にカーソルが近づいた際の処理(マーカーの表示)を定義している。
     マーカーは,半径 6 pixel,赤色,枠線なしであり,イベント処理として mouseOver するとマーカーをカーソル位置の点に移動させている。
     さらにイベント処理の中に地図上のマーカー(黒丸)を動かす処理も書いている。
     また逆に,アニメーション用の moveFeature 関数で標高図マーカー(赤丸)を動かす処理を書いている。
 ・series<line> オプション:グラフの線に関するオプション
     グラフの線は,名称が Altitude で色は赤,データは alt_array のデータを表示させている。

(5) getAltArray() 関数の定義
 getAltArray() 関数は,xml 形式のデータファイル(KML もどきの形式)からデータを取り出している。
注意点としては,欲しいデータは <coordinates> の中にあるが,それが <Folder> の中の <Placemark> の中の <LineString> の中にある。 そのため,何度も「getElementsByTagName」で要素を取り出している。 そして,最後に「childNodes[ii].nodeValue」で <coordinate> 内のデータを1行ずつ取り出し,その後一つの大きなテキストファイルにしている。
 取り出したテキストファイルは,改行で行ごとに分割したのち,コンマごとに区切ってデータを配列に入れている。 その中で5個目の沿面距離 (ll[4]) と3個目の高度データ (ll[2]) を取り出して出力配列に入れている。

(6) area_slider() 関数の定義
 「area_slider()」関数では,aeraInput スライダーの数値に合わせて,地図と標高図の表示領域の大きさを指定している。

(7) altitude_set_invisible() と altitude_set_visible(),altitude_visible() の定義
 これらは標高図表示ボタンの表示切り替え用の関数である。 標高図が未表示の時には「表示」ボタンとし,表示中は「非表示」ボタンとなる。

「その21」のサンプルに具体例を載せているので, 具体的な表示を見てみて欲しい。(ちなみにサンプルページはアクセスログを取るルーチンを組み込んでいます)
 サンプルでは,左上に「標高図表示」のボタンがある。 そのボタンを押すと画面の下の方に標高図が現れる。 実際にはすでに地図の下の方に標高図はあるのだが,表示範囲からはみ出しているので見えないだけである。 スクロールバーで地図の下の方を見ると標高図があるのがわかる。
 標高図の上にマウスを持っていくと標高図と地図の経路上に点が現れる。 標高図上は赤丸で地図上は黒丸にしている。 また,アニメボタンを押すと黒丸が地図上を動いていくが,同時に標高図上の赤丸も動いていく。 他のボタンも押してみてほしい。
その22:Highcharts でルートの標高グラフの改良に続く

0 件のコメント: