2017年12月17日日曜日

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

 これは,OpenLayers 3 を使ってみよう(その21:Highcharts でルートの標高グラフを表示)の続きというか改良版になる。 OpenLayers 3 を使ってみよう(その0:はじめに:地理院地図を表示)に目次がある。 その13までのページでは OpenLayers v3.7.0 で書いてきたが,ここでは OpenLayers 3.20.1 で書かれている。
 前回(その21:Highcharts でルートの標高グラフを表示)は,Highcharts を使って標高図を追加してみた。 ただ,前回は少し無駄なことをしていたので,少しだけ改良してみた,というのが今回のお話。

 前回,Highcharts を使って標高図を描いたのだが,データとして KML 形式のデータにその他のデータを追加した特殊なデータ形式を使っていた。これは標高図の横軸に経路の距離(沿面距離)を使いたかったからであり,予め沿面距離をデータとして作っておいて,それを読み込んで表示していた。 今回はその作戦はやめて,KML のデータとして読み込んだ,緯度,経度,高度,の情報からスクリプト内で沿面距離を計算して標高図を作る作戦に変更してみた。

 まず,web ページのソースを載せよう。 このソースは前回(その21:Highcharts でルートの標高グラフを表示)とほぼ同じであり,JavaScript の読み込みで,「ol3ex21.js」を「ol3ex22.js」に変えただけである。
 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="ol3ex22.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>
以下,前回(その21:Highcharts でルートの標高グラフを表示)の説明と同じだが,そのまま載せておこう。
 この web ページのソースの多くはその20のものとほぼ同じなので,そちらも見てほしい。 今回の Highcharts による標高表示に関連した部分は赤色太字部分になっている。 ここでは,
 (1) 標高図用の <div>(id = "map_canvas2")を用意した。
 (2) 標高図の表示ボタンと表示域の拡大縮小用のスライダーを用意した。
の2点である。
 また経路の距離を求めるルーチンは省略している。 それ以外はその20 と同じとなっている(はず…)。

 以下に JavaScript を載せよう。
ここは前回(その21:Highcharts でルートの標高グラフを表示)と少し変えてあるが,大まかには前回と同じ構成となっている。
 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 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; // 不透明度の最小値
// -------------------------------------------------------------------
// ヒュベニの公式で緯度・経度から距離を求めるための定数
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() の中で
    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 による標高図の設定
// Highcharts でグラフを描く。mouseOver で地図上の geoMarker を動かす
        alt_array = getAltArray(); // get data
        altArray  = [].concat(alt_array); // copy

        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 }] // 線の情報
        });
// .........................
    }); // 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() {
    let coords = [];
    let point0 = [routeCoords[0][0],routeCoords[0][1]]; //
    let height0 = routeCoords[0][2];
    point0 = ol.proj.transform(point0, "EPSG:3857", "EPSG:4326"); // メルカトル --> WGS84
    let lon0 = point0[0];
    let lat0 = point0[1];
    coords.push([0, height0, 0]); // 1点目

    let c_dist = 0;
    for (i = 1; i < routeCoords.length; i++) {
        let point1 = [routeCoords[i][0],routeCoords[i][1]];
        let height1 = routeCoords[i][2];
        point1 = ol.proj.transform(point1, "EPSG:3857", "EPSG:4326");  // メルカトル --> WGS84
        let lon1 = point1[0];
        let lat1 = point1[1];
        let f_dist = dist_2pts(lon0, lat0, lon1, lat1);
        c_dist = c_dist + Math.sqrt(f_dist*f_dist + (height1-height0)*(height1-height0));
        coords.push([Math.round(c_dist/10)/100, height1,i]); // creep distance (km), height (m)
        lat0 = lat1;
        lon0 = lon1;
        height0 = height1;
    }
    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(); }
}
// *******************************************************************
// ヒュベニの公式を使った距離計算
// 2点間の距離 (in meter), lon, lat は WGS84
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 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
}
// *******************************************************************

 ここも大まかには前回(その21:Highcharts でルートの標高グラフを表示)と同じなので,前回の説明を修正する形で説明しよう。
 ちなみに,前回の説明は,前々回からの変更点を示すように書かれている。

標高図に関連する部分(赤の太字部分)を上から見ていくと,
(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]) を取り出して出力配列に入れている。


 ここが一番変わった部分である。 今回は,KML 形式で読み込んだ経路データ(routeCoords 配列)からデータを取り出し,ヒュベニの公式を使って平面距離を求め,そこから高度情報を使って沿面距離を計算している。 i 番目の routeCoords 配列データは routeCoords[i] で得られるが,それら自体が,経度(Longitude),緯度(Latitude),高度(Altitude)をデータとする配列となっている。 そのため,i 番目の経路点の経度は routeCoords[i][0],緯度は routeCoords[i][1],高度は routeCoords[i][2] で取得できる。 ただし,緯度と経度は EPSG:3857(球面メルカトル図法)による座標なので,通常の緯度経度(WGS84)に変換してある。 これを距離計算のサブルーチンに送って平面距離を出し,平面距離と高度差からピタゴラスの定理で沿面距離を出して,積算して標高図の横軸にしている。 ちなみに沿面距離は km 単位(小数点以下第1位まで)にするために 10 で割った時点で四捨五入し,さらに 100 で割っている。

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

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

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

0 件のコメント: