Source: Viewer.js

import Camera from "./Camera";
import GLEnv from "./GLEnv";
import RenderStage, { PickStage } from "./RenderStage";
import StandardImageProvider from "./StandardImageProvider";
import StandardDemProvider from "./StandardDemProvider";
import LayerCollection from "./LayerCollection";
import Globe from "./Globe";
import PointCloudCollection from "./PointCloudCollection";
import TileTextureCache from "./TileTextureCache";
import NullRenderCallback from "./NullRenderCallback";
import GeoMath from "./GeoMath";
import Scene from "./Scene";
import SceneLoader from "./SceneLoader";
import EasyBindingBlock from "./animation/EasyBindingBlock";

// マウス・Attribution開発
import LogoController from "./LogoController";
import AttributionController from "./AttributionController";
import ContainerController from "./ContainerController";

/**
 * @summary 表示管理
 * @classdesc
 * <p>mapray の表示を管理するクラスである。</p>
 * @memberof mapray
 */
class Viewer {

    /**
     * @param {string|Element}                  container                           コンテナ (ID または要素)
     * @param {object}                          [options]                           生成オプション
     * @param {mapray.DemProvider}              [options.dem_provider]              DEM プロバイダ
     * @param {mapray.ImageProvider}            [options.image_provider]            画像プロバイダ
     * @param {array}                           [options.layers]                    地図レイヤー情報の配列
     * @param {boolean}                         [options.ground_visibility=true]    地表の可視性
     * @param {boolean}                         [options.entity_visibility=true]    エンティティの可視性
     * @param {mapray.RenderCallback}           [options.render_callback]           レンダリングコールバック
     * @param {mapray.Viewer.RenderMode}        [options.render_mode]               レンダリングモード
     * @param {mapray.DebugStats}               [options.debug_stats]               デバッグ統計オブジェクト
     * @param {mapray.LogoController}           [options.logo_controller]           ロゴ表示制御オブジェクト
     * @param {mapray.AttributionController}    [options.attribution_controller]    著作権表示制御オブジェクト
     */
    constructor( container, options )
    {
        var container_element;
        if ( typeof container == "string" ) {
            // コンテナを ID 指定したとき
            container_element = document.getElementById( container );
        }
        else {
            // コンテナを直接要素で指定のとき
            container_element = container;
        }

        var canvas = this._createCanvas( container_element );

        // インスタンス変数
        this._container_element  = container_element;
        this._canvas_element     = canvas;
        this._glenv              = new GLEnv( canvas );
        this._camera             = new Camera( canvas );
        this._animation          = this._createAnimationBindingBlock();
        this._dem_provider       = this._createDemProvider( options );
        this._image_provider     = this._createImageProvider( options );
        this._layers             = this._createLayerCollection( options );
        this._globe              = new Globe( this._glenv, this._dem_provider );
        this._tile_texture_cache = new TileTextureCache( this._glenv, this._image_provider );
        this._scene              = new Scene( this, this._glenv );
        this._ground_visibility  = Viewer._getBoolOption( options, "ground_visibility", true );
        this._entity_visibility  = Viewer._getBoolOption( options, "entity_visibility", true );
        this._render_mode        = (options && options.render_mode) || RenderMode.SURFACE;
        this._debug_stats        = (options && options.debug_stats) || null;
        this._point_cloud_collection = this._createPointCloudCollection( options );
        this._render_callback    = this._createRenderCallback( options );
        this._frame_req_id       = 0;
        this._previous_time      = undefined;
        this._is_destroyed       = false;

        // マウス・Attribution開発
        this._logo_controller = ( options && options.logo_controller ) || new LogoController( this._container_element );
        this._attribution_controller = ( options && options.attribution_controller ) || new AttributionController( this._container_element );

        // ロゴ・著作権表示用コンテナの作成
        this._createLogoAttributionContainer()

        this._logo_controller.createContainer();
        this._attribution_controller.createContainer();

        // 最初のフレームの準備
        this._requestNextFrame();
        this._updateCanvasSize();
    }


    /**
     * @summary インスタンスを破棄
     *
     * @desc
     * <p>次の順番で処理を行い、インスタンスを破棄する。</p>
     *
     * <ol>
     *   <li>アニメーションフレームを止める。(this.{@link mapray.Viewer#render_callback render_callback} の {@link mapray.RenderCallback#onUpdateFrame onUpdateFrame()} が呼び出されなくなる)</li>
     *   <li>this.{@link mapray.Viewer#render_callback render_callback} の {@link mapray.RenderCallback#onStop onStop()} を呼び出す。({@link mapray.RenderCallback#onStart onStart()} がすでに呼び出されている場合)</li>
     *   <li>{@link mapray.RenderCallback} インスタンスを this から切り離す。({@link mapray.RenderCallback#viewer} プロパティは null を返すようになる)</li>
     *   <li>this.{@link mapray.Viewer#canvas_element canvas_element} を this.{@link mapray.Viewer#container_element container_element} から取り外す。(キャンバスは表示されなくなる)</li>
     *   <li>データプロバイダのリクエスト、シーンデータのロードの取り消しを試みる。</li>
     * </ol>
     *
     * <p>このメソッドを呼び出した後は this に直接的または間接的にアクセスすることはできない。ただし {@link mapray.Viewer#destroy destroy()} の呼び出しは除く。</p>
     *
     * <p>このメソッドは {@link mapray.RenderCallback} のメソッドから呼び出してはならない。</p>
     */
    destroy()
    {
        if ( this._is_destroyed ) {
            // すでに this は破棄済み
            return;
        }

        // フレームを止める
        if ( this._frame_req_id != 0 ) {
            window.maprayCancelAnimationFrame( this._frame_req_id );
            this._frame_req_id = 0;
        }

        // RenderCallback の取り外し
        this._render_callback.detach();
        this._render_callback = this._createRenderCallback();  // NullRenderCallback

        // キャンバスをコンテナから外す
        this._container_element.removeChild( this._canvas_element );

        // DemProvider のリクエストを取り消す
        this._globe.cancel();

        // ImageProvider のリクエストを取り消す
        this._tile_texture_cache.cancel();

        // 各レイヤーの のリクエストを取り消す
        this._layers.cancel();

        // 各 SceneLoader の読み込みを取り消す
        this._scene.cancelLoaders();

        // マウス・Attribution開発
        this._logo_controller._destroy();
        this._attribution_controller._destroy();
        this._attribution_controller = null;

        // ロゴ・著作権用コンテナの削除
        this._deleteLogoAttributionContainer();

        // 破棄確定
        this._is_destroyed = true;
    }


    /**
     * キャンバス要素を生成
     * @param  {Element}           container
     * @return {HTMLCanvasElement}
     * @private
     */
    _createCanvas( container )
    {
        var canvas = document.createElement( "canvas" );
        canvas.className = "mapray-canvas";
        canvas.style.width = "100%";
        canvas.style.height = "100%";
        container.appendChild( canvas );
        return canvas;
    }


    /**
     * DemProvider を生成
     * @private
     */
    _createDemProvider( options )
    {
        if ( options && options.dem_provider )
            return options.dem_provider;
        else
            return new StandardDemProvider( "/dem/", ".bin" );
    }


    /**
     * animation.BindingBlock を生成
     * @private
     */
    _createAnimationBindingBlock()
    {
        let abb = new EasyBindingBlock();
        abb.addDescendantUnbinder( () => { this._unbindDescendantAnimations(); } );
        return abb;
    }


    /**
     * ImageProvider を生成
     * @private
     */
    _createImageProvider( options )
    {
        if ( options && options.image_provider )
            return options.image_provider;
        else
            return new StandardImageProvider( "http://cyberjapandata.gsi.go.jp/xyz/std/", ".png", 256, 0, 18 );
    }


    /**
     * LayerCollection を生成
     * @private
     */
    _createLayerCollection( options )
    {
        var layers = (options && options.layers) ? options.layers : {};
        return new LayerCollection( this._glenv, layers );
    }


    /**
     * PointCloudCollection を生成
     * @private
     */
    _createPointCloudCollection( options )
    {
        const point_cloud_providers = (options && options.point_cloud_providers) ? options.point_cloud_providers : {};
        return new PointCloudCollection( this._scene, point_cloud_providers );
    }

    /**
     * RenderCallback を生成
     * @private
     */
    _createRenderCallback( options )
    {
        var callback;
        if ( options && options.render_callback )
            callback = options.render_callback;
        else
            callback = new NullRenderCallback();

        callback.attach( this );

        return callback;
    }

    /**
     * @summary ロゴ・著作権表示用コンテナの作成
     *
     * @memberof Viewer
     */
    _createLogoAttributionContainer()
    {
        for ( var position of Viewer._positions )
        {
            var container = document.createElement( "div" );
            container.className = position
            this._container_element.appendChild( container );
        }
    }

    /**
     * @summary ロゴ・著作権表示用コンテナの削除
     *
     * @memberof Viewer
     */
    _deleteLogoAttributionContainer()
    {
        for ( var position of Viewer._positions )
        {
            var container = document.getElementById( position );

            if ( container ) { this._container_element.removeChild( position ); }
        }
    }

    /**
     * ブール値のオプションを取得
     * @private
     */
    static
    _getBoolOption( options, name, defaultValue )
    {
        return (options && (options[name] !== undefined)) ? options[name] : defaultValue;
    }


    /**
     * @summary コンテナ要素 (キャンバス要素を保有する)
     * @type {Element}
     * @readonly
     */
    get container_element() { return this._container_element; }


    /**
     * @summary キャンバス要素
     * @type {Element}
     * @readonly
     */
    get canvas_element() { return this._canvas_element; }


    /**
     * @summary アニメーションパラメータ設定
     * @type {mapray.animation.BindingBlock}
     * @readonly
     */
    get animation() { return this._animation; }


    /**
     * DEM データプロバイダ
     * @type {mapray.DemProvider}
     * @readonly
     */
    get dem_provider() { return this._dem_provider; }


    /**
     * @summary 画像プロバイダ
     * @type {mapray.ImageProvider}
     * @readonly
     */
    get image_provider() { return this._image_provider; }


    /**
     * @summary 地図レイヤー管理
     * @type {mapray.LayerCollection}
     * @readonly
     */
    get layers() { return this._layers; }


    /**
     * @summary 点群管理
     * @type {mapray.PointCloudCollection}
     * @readonly
     */
    get point_cloud_collection() { return this._point_cloud_collection; }


    /**
     * @summary レンダリングコールバック
     * @type {mapray.RenderCallback}
     * @readonly
     */
    get render_callback() { return this._render_callback; }


    /**
     * @summary レンダリングモード
     * @type {mapray.RenderMode}
     * @readonly
     */
    get render_mode() { return this._render_mode; }


    /**
     * @summary レンダリングモードを設定
     * @type {mapray.RenderMode}
     */
    set render_mode( val ) { this._render_mode = val; }


    /**
     * @summary デバッグ統計オブジェクト
     * @type {?mapray.DebugStats}
     * @readonly
     */
    get debug_stats() { return this._debug_stats; }


    /**
     * @summary カメラ
     * @type {mapray.Camera}
     * @readonly
     */
    get camera() { return this._camera; }


    /**
     * @summary モデルシーン
     * @type {mapray.Scene}
     * @readonly
     */
    get scene() { return this._scene; }


    /**
     * 内部的に実装で使用される WebGL レンダリングコンテキスト情報
     * @type {mapray.GLEnv}
     * @readonly
     * @package
     */
    get glenv() { return this._glenv; }


    /**
     * @type {mapray.Globe}
     * @readonly
     * @package
     */
    get globe() { return this._globe; }


    /**
     * 内部的に実装で使用される地図画像タイル管理
     * @type {mapray.TileTextureCache}
     * @readonly
     * @package
     */
    get tile_texture_cache() { return this._tile_texture_cache; }

    /**
     *
     * @type {mapray.LogoController}
     * @readonly
     * @memberof Viewer
     */
    get logo_controller() { return this._logo_controller; }

    /**
     *
     * @type {mapray.AttributionController}
     * @readonly
     * @memberof Viewer
     */
    get attribution_controller() { return this._attribution_controller; }    

    /**
     * @summary 可視性を設定
     * @desc
     * <p>target に属するオブジェクトを表示するかどうかを指定する。</p>
     * <p>可視性は Viewer の構築子の ground_visibility と entity_visibility オプションでも指定することができる。</p>
     *
     * @param {mapray.Viewer.Category} target      表示対象
     * @param {boolean}                visibility  表示するとき true, 表示しないとき false
     *
     * @see {@link mapray.Viewer#getVisibility}
     */
    setVisibility( target, visibility )
    {
        switch ( target ) {
        case Category.GROUND:
            this._ground_visibility = visibility;
            break;
        case Category.ENTITY:
            this._entity_visibility = visibility;
            break;
        default:
            throw new Error( "invalid target: " + target );
        }
    }


    /**
     * @summary 可視性を取得
     * @desc
     * <p>target に属するオブジェクトを表示するかどうかを取得する。</p>
     *
     * @param  {mapray.Viewer.Category} target  表示対象
     * @return {boolean}  表示するとき true, 表示しないとき false
     *
     * @see {@link mapray.Viewer#setVisibility}
     */
    getVisibility( target, visibility )
    {
        switch ( target ) {
        case Category.GROUND:
            return this._ground_visibility;
        case Category.ENTITY:
            return this._entity_visibility;
        default:
            throw new Error( "invalid target: " + target );
        }
    }


    /**
     * @summary 指定位置の標高を取得
     * @desc
     * <p>緯度 lat, 経度 lon が示す場所の標高を返す。</p>
     * <p>現在メモリに存在する DEM データの中で最も正確度が高いデータから標高を計算する。</p>
     * <p>さらに正確度が高い DEM データがサーバーに存在すれば、それを非同期に読み込む。そのため時間を置いてこのメソッドを呼び出すと、さらに正確な値が取得できることがある。</p>
     * @param  {number} lat  緯度 (Degrees)
     * @param  {number} lon  経度 (Degrees)
     * @return {number}      標高 (Meters)
     */
    getElevation( lat, lon )
    {
        // 正規化緯経度 (Degrees)
        var _lon = lon + 180 * Math.floor( (90 - lat) / 360 + Math.floor( (90 + lat) / 360 ) );
        var nlat = 90 - Math.abs( 90 - lat + 360 * Math.floor( (90 + lat) / 360 ) );  // 正規化緯度 [-90,90]
        var nlon = _lon - 360 - 360 * Math.floor( (_lon - 180) / 360 );               // 正規化緯度 [-180,180)

        // 単位球メルカトル座標
        var xm = nlon * GeoMath.DEGREE;
        var ym = GeoMath.invGudermannian( nlat * GeoMath.DEGREE );

        // 基底タイル座標 (左上(0, 0)、右下(1, 1))
        var dPI = 2 * Math.PI;
        var  xt = xm / dPI + 0.5;
        var  yt = 0.5 - ym / dPI;

        if ( yt < 0 || yt > 1 ) {
            // 緯度が Web メルカトルの範囲外 (極に近い)
            return 0;
        }

        // 正確度が最も高い DEM タイルの取得
        var globe = this._globe;
        var dem   = globe.findHighestAccuracy( xt, yt );
        if ( dem === null ) {
            // まだ標高を取得することができない
            return 0;
        }

        // 標高をサンプル
        var   ρ = globe.dem_provider.getResolutionPower();
        var size = 1 << ρ;               // 2^ρ
        var  pow = Math.pow( 2, dem.z );  // 2^ze
        var   uf = size * (pow * xt - dem.x);
        var   vf = size * (pow * yt - dem.y);
        var   ui = GeoMath.clamp( Math.floor( uf ), 0, size - 1 );
        var   vi = GeoMath.clamp( Math.floor( vf ), 0, size - 1 );

        var heights = dem.getHeights( ui, vi );
        var h00 = heights[0];
        var h10 = heights[1];
        var h01 = heights[2];
        var h11 = heights[3];

        // 標高を補間
        var    s = uf - ui;
        var    t = vf - vi;
        return (h00 * (1 - s) + h10 * s) * (1 - t) + (h01 * (1 - s) + h11 * s) * t;
    }


    /**
     * @summary 現行の標高を取得
     *
     * @desc
     * <p>現在メモリーにある最高精度の標高値を取得する。</p>
     * <p>まだ DEM データが存在しない、または経度, 緯度が範囲外の場所は標高を 0 とする。</p>
     *
     * <p>このメソッドは DEM のリクエストは発生しない。また DEM のキャッシュには影響を与えない。</p>
     *
     * <p>一般的に画面に表示されていない場所は標高の精度が低い。</p>
     *
     * @param  {mapray.GeoPoint} position  位置 (高度は無視される)
     * @return {number}                    標高
     *
     * @see mapray.Viewer#getExistingElevations
     */
    getExistingElevation( position )
    {
        const array = [position.longitude, position.latitude, 0];

        this._globe.getExistingElevations( 1, array, 0, 3, array, 2, 3 );

        return array[2];
    }


    /**
     * @summary 現行の標高 (複数) を取得
     *
     * @desc
     * <p>現在メモリーにある最高精度の標高値を一括で取得する。</p>
     * <p>まだ DEM データが存在しない、または経度, 緯度が範囲外の場所は標高を 0 とする。</p>
     *
     * <p>このメソッドは DEM のリクエストは発生しない。また DEM のキャッシュには影響を与えない。</p>
     *
     * <p>一般的に画面に表示されていない場所は標高の精度が低い。</p>
     *
     * @param  {number}   num_points  入出力データ数
     * @param  {number[]} src_array   入力配列 (経度, 緯度, ...)
     * @param  {number}   src_offset  入力データの先頭インデックス
     * @param  {number}   src_stride  入力データのストライド
     * @param  {number[]} dst_array   出力配列 (標高, ...)
     * @param  {number}   dst_offset  出力データの先頭インデックス
     * @param  {number}   dst_stride  出力データのストライド
     * @return {number[]}             dst_array
     *
     * @see mapray.Viewer#getExistingElevation
     */
    getExistingElevations( num_points, src_array, src_offset, src_stride, dst_array, dst_offset, dst_stride )
    {
        return this._globe.getExistingElevations( num_points, src_array, src_offset, src_stride, dst_array, dst_offset, dst_stride );
    }


    /**
     * @summary レイと地表の交点を取得
     * @desc
     * <p>ray と地表の最も近い交点を取得する。ただし交点が存在しない場合は null を返す。</p>
     * @param  {mapray.Ray}      ray  レイ (GOCS)
     * @return {?mapray.Vector3}      交点または null
     */
    getRayIntersection( ray )
    {
        var globe = this._globe;

        if ( globe.status !== Globe.Status.READY ) {
            // Globe の準備ができていない
            return null;
        }

        var distance = globe.root_flake.findRayDistance( ray, Number.MAX_VALUE );
        if ( distance === Number.MAX_VALUE ) {
            // 交点が見つからなかった
            return null;
        }

        // P = Q + distance V
        var p = GeoMath.createVector3();
        var q = ray.position;
        var v = ray.direction;

        p[0] = q[0] + distance * v[0];
        p[1] = q[1] + distance * v[1];
        p[2] = q[2] + distance * v[2];

        return p;
    }


    /**
     * 次のフレーム更新を要求する。
     * @private
     */
    _requestNextFrame()
    {
        this._frame_req_id = window.maprayRequestAnimationFrame( () => this._updateFrame() );
    }


    /**
     * フレーム更新のときに呼び出される。
     * @private
     * @see mapray.RenderStage
     */
    _updateFrame()
    {
        var delta_time = this._updateTime();
        this._requestNextFrame();

        this._updateCanvasSize();

        this._render_callback.onUpdateFrameInner( delta_time );

        if ( this._debug_stats !== null ) {
            this._debug_stats.clearStats();
        }

        var stage = new RenderStage( this );
        stage.render();

        this._finishDebugStats();
    }


    /**
     * 現在のビューにおいて指定されたスクリーン位置の情報を取得します
     * @param {Vector2} screen_position スクリーン位置(キャンバス左上を原点としたピクセル座標)
     * @return {mapray.Viewer.PickResult} ピック結果
     */
    pick(screen_position) {
        const stage = new PickStage( this, screen_position );
        stage.render();
        return stage.pick_result;
    }


    /**
     * @summary 時間の更新
     * @return {number}  前フレームからの経過時間 (秒)
     * @private
     */
    _updateTime()
    {
        var   now_time = window.maprayNow();
        var delta_time = (this._previous_time !== undefined) ? (now_time - this._previous_time) / 1000 : 0;
        this._previous_time = now_time;

        return delta_time;
    }


    /**
     * @summary Canvas サイズを更新
     * @private
     */
    _updateCanvasSize()
    {
        var canvas = this._canvas_element;

        // 要素のサイズとキャンバスのサイズを一致させる
        if ( canvas.width != canvas.clientWidth ) {
            canvas.width = canvas.clientWidth;
        }
        if ( canvas.height != canvas.clientHeight ) {
            canvas.height = canvas.clientHeight;
        }
    }


    /**
     * @summary デバッグ統計の最終処理
     * @private
     */
    _finishDebugStats()
    {
        var stats = this._debug_stats;
        if ( stats === null ) {
            // 統計オブジェクトは指定されていない
            return;
        }

        // 統計値の取得
        stats.num_wait_reqs_dem = this._globe.getNumDemWaitingRequests();
        stats.num_wait_reqs_img = this._tile_texture_cache.getNumWaitingRequests();

        // 統計の更新を通知
        stats.onUpdate();
    }


    /**
     * EasyBindingBlock.DescendantUnbinder 処理
     *
     * @private
     */
    _unbindDescendantAnimations()
    {
        this._scene.animation.unbindAllRecursively();
    }

}


/**
 * @summary ピック結果
 * @typedef {object} PickResult
 * @desc
 * <p>関数型 {@link mapray.Viewer.pick} の戻り値のオブジェクト構造である。</p>
 * @property {mapray.Vector3} [point] ピックした3次元位置
 * @property {mapray.Entity} [entity|undefined] ピックしたエンティティ(ピック位置にエンティティがない場合はundefined)
 * @memberof mapray.Viewer
 */


/**
 * @summary 表示対象の列挙型
 * @desc
 * <p>{@link mapray.Viewer#setVisibility} と {@link mapray.Viewer#getVisibility} メソッドの target 引数に指定する値の型である。</p>
 * @enum {object}
 * @memberof mapray.Viewer
 * @constant
 */
var Category = {

    /**
     * 地表 (レイヤーも含む)
     */
    GROUND: { id: "GROUND" },


    /**
     * エンティティ
     */
    ENTITY: { id: "ENTITY" }

};


/**
 * @summary レンダリングモードの列挙型
 * @desc
 * {@link mapray.Viewer} の構築子の options.render_mode パラメータ、または {@link mapray.Viewer#render_mode} プロパティに指定する値の型である。
 * @enum {object}
 * @memberof mapray.Viewer
 * @constant
 */
var RenderMode = {

    /**
     * ポリゴン面 (既定値)
     */
    SURFACE: { id: "SURFACE" },


    /**
     * ワイヤーフレーム
     */
    WIREFRAME: { id: "WIREFRAME" }

};

// クラス定数の定義
{
    Viewer.Category   = Category;
    Viewer.RenderMode = RenderMode;

    // マウス・Attribution開発
    Viewer.ContainerPosition = ContainerController.ContainerPosition;

    // ロゴ・著作権表示用コンテナ名称
    Viewer._positions = ["control-top-left", "control-top-right", "control-bottom-left", "control-bottom-right"];
}


export default Viewer;