Source: AbstractLineEntity.js

import Entity from "./Entity";
import Primitive from "./Primitive";
import Mesh from "./Mesh";
import LineMaterial from "./LineMaterial";
import GeoMath from "./GeoMath";
import GeoPoint from "./GeoPoint";
import GeoRegion from "./GeoRegion";
import AltitudeMode from "./AltitudeMode";
import EntityRegion from "./EntityRegion";
import AreaUtil from "./AreaUtil";
import QAreaManager from "./QAreaManager";
import Type from "./animation/Type";
import { RenderTarget } from "./RenderStage";


/**
 * @summary 線エンティティ
 *
 * @classdesc
 * <p>{@link mapray.MarkerLineEntity} と {@link mapray.PathEntity} の共通機能を
 *    提供するクラスである。</p>
 *
 * @memberof mapray
 * @extends mapray.Entity
 * @abstract
 * @protected
 */
class AbstractLineEntity extends Entity {

    /**
     * @param {mapray.Scene} scene        所属可能シーン
     * @param {mapray.AbstractLineEntity.LineType} line_type  クラス種別
     * @param {object}       [opts]       オプション集合
     * @param {object}       [opts.json]  生成情報
     * @param {object}       [opts.refs]  参照辞書
     */
    constructor( scene, line_type, opts )
    {
        super( scene, opts );
        
        this._line_type = line_type;

        if ( this.altitude_mode === AltitudeMode.CLAMP ) {
            this._producer = new FlakePrimitiveProducer( this );
            this._is_flake_mode = true;
        }
        else {
            this._producer = new PrimitiveProducer( this );
            this._is_flake_mode = false;
        }
    }


    /**
     * @override
     */
    getPrimitiveProducer()
    {
        return (!this._is_flake_mode) ? this._producer : null;
    }


    /**
     * @override
     */
    getFlakePrimitiveProducer()
    {
        return (this._is_flake_mode) ? this._producer : null;
    }


    /**
     * @override
     */
    onChangeAltitudeMode( prev_mode )
    {
        if ( this.altitude_mode === AltitudeMode.CLAMP ) {
            this._producer = new FlakePrimitiveProducer( this );
            this._is_flake_mode = true;
        }
        else {
            this._producer = new PrimitiveProducer( this );
            this._is_flake_mode = false;
        }
    }


    /**
     * @summary 線の太さを設定
     *
     * @param {number} width  線の太さ (画素単位)
     */
    setLineWidth( width )
    {
        if ( this._width !== width ) {
            this._width = width;
            this._producer.onChangeProperty();
        }
    }


    /**
     * @summary 基本色を設定
     *
     * @param {mapray.Vector3} color  基本色
     */
    setColor( color )
    {
        if ( this._color[0] !== color[0] ||
             this._color[1] !== color[1] ||
             this._color[2] !== color[2] ) {
            // 位置が変更された
            GeoMath.copyVector3( color, this._color );
            this._producer.onChangeProperty();
        }
    }


    /**
     * @summary 不透明度を設定
     *
     * @param {number} opacity  不透明度
     */
    setOpacity( opacity )
    {
        if ( this._opacity !== opacity ) {
            this._opacity = opacity;
            this._producer.onChangeProperty();
        }
    }


    /**
     * @summary すべての頂点のバウンディングを算出
     *
     * @override
     * @return {mapray.GeoRegion}  バウンディング情報を持ったGeoRegion
     */
    getBounds()
    {
        const region = new GeoRegion();
        region.addPointsAsArray( this._point_array );
        return region;
    }


    /**
     * @summary 専用マテリアルを取得
     * @private
     */
    _getLineMaterial( render_target )
    {
        const scene    = this.scene;
        const cache_id = (
            "_AbstractLineEntity_material" +
            (this._line_type === LineType.PATH ? "_path" : "_markerline") +
            (render_target === RenderTarget.RID ? "_pick" : "")
        );

        if ( !scene[cache_id] ) {
            // scene にマテリアルをキャッシュ
            const opt = { ridMaterial: render_target === RenderTarget.RID };
            scene[cache_id] = new LineMaterial( scene.glenv, this._line_type, opt );
        }

        return scene[cache_id];
    }
}


/**
 * @summary MarkerLineEntity の PrimitiveProducer
 *
 * @private
 */
class PrimitiveProducer extends Entity.PrimitiveProducer {

    /**
     * @param {mapray.MarkerLineEntity} entity
     */
    constructor( entity )
    {
        super( entity );

        // プリミティブの要素
        this._transform = GeoMath.setIdentity( GeoMath.createMatrix() );
        this._pivot     = GeoMath.createVector3();
        this._bbox      = [GeoMath.createVector3(),
                            GeoMath.createVector3()];

        this._properties = {
            width:   1.0,
            color:   GeoMath.createVector3f(),
            opacity: 1.0
        };
        
        if ( entity._line_type == LineType.PATH ) {
            this._properties["lower_length"] = 0.0;
            this._properties["upper_length"] = 0.0;
        }

        // プリミティブ
        const      material = entity._getLineMaterial( RenderTarget.SCENE );
        const primitive = new Primitive( entity.scene.glenv, null, material, this._transform );
        primitive.pivot      = this._pivot;
        primitive.bbox       = this._bbox;
        primitive.properties = this._properties;
        this._primitive = primitive;

        const pick_material = entity._getLineMaterial( RenderTarget.RID );
        const pickPrimitive = new Primitive( entity.scene.glenv, null, pick_material, this._transform );
        pickPrimitive.pivot      = this._pivot;
        pickPrimitive.bbox       = this._bbox;
        pickPrimitive.properties = this._properties;
        this._pickPrimitive = pickPrimitive;

        // プリミティブ配列
        this._primitives = [primitive];
        this._pickPrimitives = [pickPrimitive];

        this._geom_dirty = true;
    }


    /**
     * @override
     */
    createRegions()
    {
        let region = new EntityRegion();

        region.addPoints( this.entity._point_array, 0, 3, this._numPoints() );

        return [region];
    }


    /**
     * @override
     */
    onChangeElevation( regions )
    {
        this._geom_dirty = true;
    }


    /**
     * @override
     */
    getPrimitives( stage )
    {
        if ( this._num_floats < 6 ) {
            // 2頂点未満は表示しない
            return [];
        }
        else {
            this._updatePrimitive();
            return stage.getRenderTarget() === RenderTarget.SCENE ? this._primitives : this._pickPrimitives;
        }
    }


    /**
     * @summary 頂点が変更されたことを通知
     */
    onChangePoints()
    {
        this.needToCreateRegions();
        this._geom_dirty = true;
    }


    /**
     * @summary プロパティが変更されたことを通知
     */
    onChangeProperty()
    {
    }


    /**
     * @summary プリミティブの更新
     *
     * @desc
     * <pre>
     * 条件: this._num_floats >= 6
     * 入力:
     *   this._geom_dirty
     *   this.entity._point_array
     *   this.entity._num_floats
     *   this.entity._width
     *   this.entity._color
     *   this.entity._opacity
     *   this.entity._length_array
     * 出力:
     *   this._transform
     *   this._pivot
     *   this._bbox
     *   this._properties
     *   this._primitive.mesh
     *   this._geom_dirty
     * </pre>
     *
     * @private
     */
    _updatePrimitive()
    {
        this._updateProperties();

        if ( !this._geom_dirty ) {
            // メッシュは更新する必要がない
            return;
        }

        let entity = this.entity;

        // GeoPoint 平坦化配列を GOCS 平坦化配列に変換
        var  num_points = this._numPoints();
        var gocs_buffer = GeoPoint.toGocsArray( this._getFlatGeoPoints_with_Absolute(), num_points,
                                                new Float64Array( entity._num_floats ) );

        // プリミティブの更新
        //   primitive.transform
        //   primitive.pivot
        //   primitive.bbox
        this._updateTransformPivotBBox( gocs_buffer, num_points );

        let add_length = (entity._line_type === LineType.PATH);
        let length_array = add_length ? entity._length_array : undefined;

        // メッシュ生成
        var mesh_data = {
            vtype: [
                { name: "a_position",  size: 3 },
                { name: "a_direction", size: 3 },
                { name: "a_where",     size: 2 }
            ],
            vertices: this._createVertices( gocs_buffer, num_points, length_array ),
            indices:  this._createIndices()
        };

        if ( add_length ) {
            mesh_data.vtype.push( { name: "a_length", size: 1 } );
        }

        var mesh = new Mesh( entity.scene.glenv, mesh_data );

        // メッシュ設定
        //   primitive.mesh
        var primitive = this._primitive;
        if ( primitive.mesh ) {
            primitive.mesh.dispose();
        }
        primitive.mesh = mesh;

        var pickPrimitive = this._pickPrimitive;
        if ( pickPrimitive.mesh ) {
            pickPrimitive.mesh.dispose();
        }
        pickPrimitive.mesh = mesh;

        // 更新終了
        this._geom_dirty = false;
    }


    /**
     * @summary プロパティを更新
     *
     * @desc
     * <pre>
     * 入力:
     *   this.entity._width
     *   this.entity._color
     *   this.entity._opacity
     *   this.entity._lower_length
     *   this.entity._upper_length
     * 出力:
     *   this._properties
     * </pre>
     *
     * @private
     */
    _updateProperties()
    {
        let entity = this.entity;
        let props  = this._properties;

        props.width = entity._width;
        GeoMath.copyVector3( entity._color, props.color );
        props.opacity = entity._opacity;
        props.lower_length = entity._lower_length;
        props.upper_length = entity._upper_length;
    }


    /**
     * @summary GeoPoint 平坦化配列を取得 (絶対高度)
     *
     * @return {number[]}  GeoPoint 平坦化配列
     * @private
     */
    _getFlatGeoPoints_with_Absolute()
    {
        let entity      = this.entity;
        let point_array = entity._point_array;
        let num_floats  = entity._num_floats;

        var abs_buffer = null;

        switch ( entity.altitude_mode ) {
        case AltitudeMode.RELATIVE:
            var num_points = this._numPoints();
            abs_buffer = new Float64Array( num_floats );
            // abs_buffer[] の高度要素に現在の標高を設定
            entity.scene.viewer.getExistingElevations( num_points, point_array, 0, 3, abs_buffer, 2, 3 );
            // abs_buffer[] に経度要素と緯度要素を設定し、高度要素に絶対高度を設定
            for ( var i = 0; i < num_floats; i += 3 ) {
                abs_buffer[i    ]  = point_array[i    ];  // 経度
                abs_buffer[i + 1]  = point_array[i + 1];  // 緯度
                abs_buffer[i + 2] += point_array[i + 2];  // 絶対高度
            }
            break;

        default: // AltitudeMode.ABSOLUTE
            abs_buffer = point_array;
            break;
        }

        return abs_buffer;
    }


    /**
     * @summary プリミティブの更新
     *
     * @desc
     * <pre>
     * 出力:
     *   this._transform
     *   this._pivot
     *   this._bbox
     * </pre>
     *
     * @param {Float64Array} gocs_buffer  入力頂点配列 (GOCS)
     * @param {number}       num_points   入力頂点数
     * @private
     */
    _updateTransformPivotBBox( gocs_buffer, num_points )
    {
        // モデル座標系の原点 (GOCS)
        var ox = gocs_buffer[0];
        var oy = gocs_buffer[1];
        var oz = gocs_buffer[2];

        // 変換行列の更新
        var transform = this._transform;
        transform[12] = ox;
        transform[13] = oy;
        transform[14] = oz;

        // 統計
        var xsum = 0;
        var ysum = 0;
        var zsum = 0;

        var xmin = Number.MAX_VALUE;
        var ymin = Number.MAX_VALUE;
        var zmin = Number.MAX_VALUE;

        var xmax = -Number.MAX_VALUE;
        var ymax = -Number.MAX_VALUE;
        var zmax = -Number.MAX_VALUE;

        for ( var i = 0; i < num_points; ++i ) {
            var b = 3 * i;
            var x = gocs_buffer[b]     - ox;
            var y = gocs_buffer[b + 1] - oy;
            var z = gocs_buffer[b + 2] - oz;

            xsum += x;
            ysum += y;
            zsum += z;

            if ( x < xmin ) { xmin = x; }
            if ( y < ymin ) { ymin = y; }
            if ( z < zmin ) { zmin = z; }

            if ( x > xmax ) { xmax = x; }
            if ( y > ymax ) { ymax = y; }
            if ( z > zmax ) { zmax = z; }
        }

        // 中心点
        var pivot = this._pivot;
        pivot[0] = xsum / num_points;
        pivot[1] = ysum / num_points;
        pivot[2] = zsum / num_points;

        // 境界箱
        var bbox  = this._bbox;
        var bmin = bbox[0];
        var bmax = bbox[1];
        bmin[0] = xmin;
        bmin[1] = ymin;
        bmin[2] = zmin;
        bmax[0] = xmax;
        bmax[1] = ymax;
        bmax[2] = zmax;
    }


    /**
     * @summary 頂点配列の生成
     *
     * @param  {Float64Array} gocs_buffer  入力頂点配列 (GOCS)
     * @param  {number}       num_points   入力頂点数
     * @return {Float32Array}              Mesh 用の頂点配列
     *
     * @private
     */
    _createVertices( gocs_buffer, num_points, length_array = undefined )
    {
        // 頂点の距離を追加するか
        var add_length = (length_array !== undefined);

        // モデル座標系の原点 (GOCS)
        var ox = gocs_buffer[0];
        var oy = gocs_buffer[1];
        var oz = gocs_buffer[2];

        var num_segments = num_points - 1;
        var num_vertices = 4 * num_segments;
        var vertices     = new Float32Array( (add_length ? 9 : 8) * num_vertices );

        for ( var i = 0; i < num_segments; ++i ) {
            var  b = 3 * i;
            var sx = gocs_buffer[b]     - ox;
            var sy = gocs_buffer[b + 1] - oy;
            var sz = gocs_buffer[b + 2] - oz;
            var ex = gocs_buffer[b + 3] - ox;
            var ey = gocs_buffer[b + 4] - oy;
            var ez = gocs_buffer[b + 5] - oz;
            var dx = gocs_buffer[b + 3] - gocs_buffer[b];
            var dy = gocs_buffer[b + 4] - gocs_buffer[b + 1];
            var dz = gocs_buffer[b + 5] - gocs_buffer[b + 2];
            var  v = (add_length ? 36 : 32) * i;

            // 始左、始右、終左、終右のループ
            for ( var j = 0; j < 4; ++j ) {
                var start = j < 2;
                var id = v + j * ( add_length ? 9 : 8 );

                vertices[id]      = start ? sx : ex;  // a_position.x
                vertices[id +  1] = start ? sy : ey;  // a_position.y
                vertices[id +  2] = start ? sz : ez;  // a_position.z
                vertices[id +  3] = dx;  // a_direction.x
                vertices[id +  4] = dy;  // a_direction.y
                vertices[id +  5] = dz;  // a_direction.z

                switch ( j ) {
                    case 0:
                        vertices[id +  6] = -1;  // a_where.x
                        vertices[id +  7] =  1;  // a_where.y
                        break;
                    case 1:
                        vertices[id +  6] = -1;  // a_where.x
                        vertices[id +  7] = -1;  // a_where.y
                        break;
                    case 2:
                        vertices[id +  6] =  1;  // a_where.x
                        vertices[id +  7] =  1;  // a_where.y
                        break;
                    case 3:
                        vertices[id +  6] =  1;  // a_where.x
                        vertices[id +  7] = -1;  // a_where.y
                        break;
                }

                if ( add_length ) {
                    vertices[id + 8] = length_array[start ? i : i + 1];
                }
            }
        }

        return vertices;
    }


    /**
     * @summary 頂点インデックスの生成
     *
     * @desc
     * <pre>
     * 条件: this.entity._num_floats >= 6
     * 入力: this.entity._num_floats
     * </pre>
     *
     * @return {Uint32Array}  インデックス配列
     *
     * @private
     */
    _createIndices()
    {
        var num_points   = this._numPoints();
        var num_segments = num_points - 1;
        var num_indices = 6 * num_segments;
        var indices     = new Uint32Array( num_indices );

        for ( var i = 0; i < num_segments; ++i ) {
            var base_d = 6 * i;
            var base_s = 4 * i;
            indices[base_d]     = base_s;
            indices[base_d + 1] = base_s + 1;
            indices[base_d + 2] = base_s + 2;
            indices[base_d + 3] = base_s + 2;
            indices[base_d + 4] = base_s + 1;
            indices[base_d + 5] = base_s + 3;
        }

        return indices;
    }


    /**
     * @summary 頂点数を取得
     *
     * @return {number} 頂点数
     *
     * @private
     */
    _numPoints()
    {
        return Math.floor( this.entity._num_floats / 3 );
    }

}


/**
 * @summary MarkerLineEntity の FlakePrimitiveProducer
 *
 * @private
 */
class FlakePrimitiveProducer extends Entity.FlakePrimitiveProducer {

    /**
     * @param {mapray.MarkerLineEntity} entity
     */
    constructor( entity )
    {
        super( entity );

        this._material_map = Object.keys(RenderTarget).reduce((map, key) => {
                const render_target = RenderTarget[key];
                map.set( render_target, entity._getLineMaterial( render_target ) );
                return map;
        }, new Map());
        this._properties   = null;
        this._area_manager = new LineAreaManager( entity );
    }


    /**
     * @override
     */
    getAreaStatus( area )
    {
        return this._area_manager.getAreaStatus( area );
    }


    /**
     * @override
     */
    createMesh( area, dpows, dem )
    {
        let segments = this._divideXY( area, dpows );
        if ( segments.length == 0 ) {
            return null;
        }

        let add_length = (this.entity._line_type === LineType.PATH);

        // メッシュ生成
        let mesh_data = {
            vtype: [
                { name: "a_position",  size: 3 },
                { name: "a_direction", size: 3 },
                { name: "a_where",     size: 2 }
            ],
            vertices: this._createVertices( area, dem, segments, add_length ),
            indices:  this._createIndices( segments.length )
        };

        if ( add_length ) {
            mesh_data.vtype.push( { name: "a_length", size: 1 } );
        }

        return new Mesh( this.entity.scene.glenv, mesh_data );
    }


    /**
     * @override
     */
    getMaterialAndProperties( stage )
    {
        if ( this._properties === null ) {
            let entity = this.entity;
            this._properties = {
                width:   entity._width,
                color:   GeoMath.createVector3f( entity._color ),
                opacity: entity._opacity
            };

            if ( entity._line_type == LineType.PATH ) {
                this._properties["lower_length"] = entity._lower_length;
                this._properties["upper_length"] = entity._upper_length;
            }
        }

        return {
            material:     this._material_map.get( stage.getRenderTarget() ),
            properties:   this._properties
        };
    }


    /**
     * @summary 頂点が変更されたことを通知
     */
    onChangePoints()
    {
        this._area_manager.notifyForUpdateContent();
        this.notifyForUpdate();
    }


    /**
     * @summary プロパティが変更されたことを通知
     */
    onChangeProperty()
    {
        this._properties = null;
    }


    /**
     * @summary すべての線分を垂直グリッドで分割
     *
     * @param {mapray.Area} area   地表断片の領域
     * @param {number}      msize  area 寸法 ÷ π (厳密値)
     * @param {number}      dpow   area の x 分割指数
     *
     * @private
     */
    _divideXOnly( area, msize, dpow )
    {
        let x_min = Math.PI * (area.x * msize - 1);
        let x_max = Math.PI * ((area.x + 1) * msize - 1);

        let  div_x = 1 << dpow;                // 横分割数: 2^dpow
        let step_x = (x_max - x_min) / div_x;  // 横分割間隔

        let segments = [];

        // 垂直グリッド線で分割
        for ( let [px, py, pl, qx, qy, ql] of this._area_manager.getAreaContent( area ) ) {

            let [x0, y0, l0, x1, y1, l1] = (px <= qx) ? [px, py, pl, qx, qy, ql] : [qx, qy, ql, px, py, pl];
            // assert: x0 <= x1

            if ( x1 < x_min || x0 >= x_max ) {
                // 線分の x 座標が area の範囲外
                continue;
            }

            if ( x0 == x1 ) {
                // 垂直線分なので、垂直グリッド線で分割しない
                segments.push( [x0, y0, l0, x1, y1, l1] );
                continue;
            }

            // 左端でトリミング
            let tx0 = x0;
            let ty0 = y0;
            let tl0 = l0;
            if ( x0 < x_min ) {
                let mu1 = (x_min - x0) / (x1 - x0);
                let mu0 = 1 - mu1;
                tx0 = x_min;
                ty0 = mu0*y0 + mu1*y1;  // 左端線と線分の交点の y 座標
                tl0 = mu0*l0 + mu1*l1;
            }

            // 右端でトリミング
            let tx1 = x1;
            let ty1 = y1;
            let tl1 = l1;
            if ( x1 > x_max ) {
                let mu1 = (x_max - x0) / (x1 - x0);
                let mu0 = 1 - mu1;
                tx1 = x_max;
                ty1 = mu0*y0 + mu1*y1;  // 右端線と線分の交点の y 座標
                tl1 = mu0*l0 + mu1*l1;
            }

            // グリッド線の範囲
            let i_min = Math.max( Math.ceil(  (x0 - x_min) / step_x ), 1         );
            let i_max = Math.min( Math.floor( (x1 - x_min) / step_x ), div_x - 1 );

            let prev_x = tx0;
            let prev_y = ty0;
            let prev_l = tl0;

            for ( let i = i_min; i <= i_max; ++i ) {
                let next_x = x_min + step_x * i;  // 垂直グリッド線の x 座標

                let mu1 = (next_x - x0) / (x1 - x0);
                let mu0 = 1 - mu1;

                let next_y = mu0*y0 + mu1*y1;  // 垂直グリッド線と線分の交点の y 座標
                let next_l = mu0*l0 + mu1*l1;

                if ( prev_x != next_x || prev_y != next_y ) {
                    segments.push( [prev_x, prev_y, prev_l, next_x, next_y, next_l] );
                }

                prev_x = next_x;
                prev_y = next_y;
                prev_l = next_l;
            }

            if ( prev_x != tx1 || prev_y != ty1 ) {
                segments.push( [prev_x, prev_y, prev_l, tx1, ty1, tl1] );
            }
        }

        return segments;
    }


    /**
     * @summary すべての線分をグリッドで分割
     *
     * @param {mapray.Area} area   地表断片の領域
     * @param {number[]}    dpows  area の xy 分割指数
     *
     * @private
     */
    _divideXY( area, dpows )
    {
        // area 寸法 ÷ π (厳密値)
        // 線分の場合、領域の端によるクリッピングがシビアなので厳密値 (2^整数) を使う
        let msize = 2 / Math.round( Math.pow( 2, area.z ) );

        // area の y 座標の範囲
        let y_min = Math.PI * (1 - (area.y + 1) * msize);
        let y_max = Math.PI * (1 - area.y * msize);

        let  div_y = 1 << dpows[1];            // 縦分割数: 2^dpow
        let step_y = (y_max - y_min) / div_y;  // 縦分割間隔

        let segments = [];

        // 水平グリッド線で分割
        for ( let [px, py, pl, qx, qy, ql] of this._divideXOnly( area, msize, dpows[0] ) ) {

            let [x0, y0, l0, x1, y1, l1] = (py <= qy) ? [px, py, pl, qx, qy, ql] : [qx, qy, ql, px, py, pl];
            // assert: y0 <= y1

            if ( y1 < y_min || y0 >= y_max ) {
                // 線分の y 座標が area の範囲外
                continue;
            }

            if ( y0 == y1 ) {
                // 水平線分なので、水平グリッド線で分割しない
                segments.push( [x0, y0, l0, x1, y1, l1] );
                continue;
            }

            // 下端でトリミング
            let tx0 = x0;
            let ty0 = y0;
            let tl0 = l0;
            if ( y0 < y_min ) {
                let mu1 = (y_min - y0) / (y1 - y0);
                let mu0 = 1 - mu1;
                tx0 = mu0*x0 + mu1*x1;  // 下端線と線分の交点の x 座標
                ty0 = y_min;
                tl0 = mu0*l0 + mu1*l1;
            }

            // 上端でトリミング
            let tx1 = x1;
            let ty1 = y1;
            let tl1 = l1;
            if ( y1 > y_max ) {
                let mu1 = (y_max - y0) / (y1 - y0);
                let mu0 = 1 - mu1;
                tx1 = mu0*x0 + mu1*x1;  // 上端線と線分の交点の x 座標
                ty1 = y_max;
                tl1 = mu0*l0 + mu1*l1;
            }

            // グリッド線の範囲
            let i_min = Math.max( Math.ceil(  (y0 - y_min) / step_y ), 1         );
            let i_max = Math.min( Math.floor( (y1 - y_min) / step_y ), div_y - 1 );

            let prev_x = tx0;
            let prev_y = ty0;
            let prev_l = tl0;

            for ( let i = i_min; i <= i_max; ++i ) {
                let next_y = y_min + step_y * i;  // 水平グリッド線の y 座標

                let mu1 = (next_y - y0) / (y1 - y0);
                let mu0 = 1 - mu1;

                let next_x = mu0*x0 + mu1*x1;  // 水平グリッド線と線分の交点の x 座標
                let next_l = mu0*l0 + mu1*l1;

                if ( prev_x != next_x || prev_y != next_y ) {
                    segments.push( [prev_x, prev_y, prev_l, next_x, next_y, next_l] );
                }

                prev_x = next_x;
                prev_y = next_y;
                prev_l = next_l;
            }

            if ( prev_x != tx1 || prev_y != ty1 ) {
                segments.push( [prev_x, prev_y, prev_l, tx1, ty1, tl1] );
            }
        }

        return segments;
    }


    /**
     * @summary 頂点配列の生成
     *
     * @param {mapray.Area}      area  地表断片の領域
     * @param {mapray.DemBinary} dem   DEM バイナリ
     *
     * @return {Float32Array}  Mesh 用の頂点配列
     *
     * @private
     */
    _createVertices( area, dem, segments, add_length = false )
    {
        let sampler = dem.newLinearSampler();
        let [ox, oy, oz] = AreaUtil.getCenter( area, GeoMath.createVector3() );

        let num_segments = segments.length;
        let num_vertices = 4 * num_segments;
        let vertices     = new Float32Array( (add_length ? 9 : 8) * num_vertices );

        for ( let i = 0; i < num_segments; ++i ) {
            let [smx, smy, prev_length, emx, emy, next_length] = segments[i];

            let [sgx, sgy, sgz] = toGocs( smx, smy, sampler );
            let [egx, egy, egz] = toGocs( emx, emy, sampler );

            let sx = sgx - ox;
            let sy = sgy - oy;
            let sz = sgz - oz;

            let ex = egx - ox;
            let ey = egy - oy;
            let ez = egz - oz;

            let dx = egx - sgx;
            let dy = egy - sgy;
            let dz = egz - sgz;

            let v = (add_length ? 36 : 32) * i;

            // 始左、始右、終左、終右のループ
            for ( var j = 0; j < 4; ++j ) {
                var start = j < 2;
                var id = v + j * ( add_length ? 9 : 8 );

                vertices[id]      = start ? sx : ex;  // a_position.x
                vertices[id +  1] = start ? sy : ey;  // a_position.y
                vertices[id +  2] = start ? sz : ez;  // a_position.z
                vertices[id +  3] = dx;  // a_direction.x
                vertices[id +  4] = dy;  // a_direction.y
                vertices[id +  5] = dz;  // a_direction.z
                
                switch ( j ) {
                    case 0:
                        vertices[id +  6] = -1;  // a_where.x
                        vertices[id +  7] =  1;  // a_where.y
                        break;
                    case 1:
                        vertices[id +  6] = -1;  // a_where.x
                        vertices[id +  7] = -1;  // a_where.y
                        break;
                    case 2:
                        vertices[id +  6] =  1;  // a_where.x
                        vertices[id +  7] =  1;  // a_where.y
                        break;
                    case 3:
                        vertices[id +  6] =  1;  // a_where.x
                        vertices[id +  7] = -1;  // a_where.y
                        break;
                }

                if ( add_length ) {
                    vertices[id +  8] = start ? prev_length : next_length;
                }
            }
        }

        return vertices;
    }


    /**
     * @summary @summary 頂点インデックスの生成
     *
     * @param {number} num_segments  線分の数
     *
     * @return {Uint32Array}  Mesh 用の頂点インデックス
     *
     * @private
     */
    _createIndices( num_segments )
    {
        let num_indices = 6 * num_segments;
        let indices     = new Uint32Array( num_indices );

        for ( let i = 0; i < num_segments; ++i ) {
            let base_d = 6 * i;
            let base_s = 4 * i;
            indices[base_d    ] = base_s;
            indices[base_d + 1] = base_s + 1;
            indices[base_d + 2] = base_s + 2;
            indices[base_d + 3] = base_s + 2;
            indices[base_d + 4] = base_s + 1;
            indices[base_d + 5] = base_s + 3;
        }

        return indices;
    }

}


/**
 * @private
 */
function
toGocs( x, y, sampler )
{
    let λ = x;
    let φ = GeoMath.gudermannian( y );
    let r = GeoMath.EARTH_RADIUS + sampler.sample( x, y );

    let cosφ = Math.cos( φ );

    return [r * cosφ * Math.cos( λ ),
            r * cosφ * Math.sin( λ ),
            r * Math.sin( φ )];
}


/**
 * @summary 線分の領域管理
 *
 * @private
 */
class LineAreaManager extends QAreaManager {

    /**
     * @param {mapray.MarkerLineEntity} entity  管理対象のエンティティ
     */
    constructor( entity )
    {
        super();

        this._entity = entity;
    }


    /**
     * @override
     */
    getInitialContent()
    {
        const Degree = GeoMath.DEGREE;
        const RAngle = Math.PI / 2;  // 直角
        const TwoPI  = 2 * Math.PI;  // 2π

        let segments = [];

        // 頂点データ
        let points    = this._entity._point_array;
        let end_point = this._entity._num_floats;

        if ( end_point < 6 ) {
            // 線分なし
            return segments;
        }

        let is_path = (this._entity._line_type === LineType.PATH);
        let length_array = is_path ? this._entity._length_array : null;

        // 線分の始点 (ラジアン)
        let lon0 = points[0] * Degree;
        let lat0 = points[1] * Degree;
        let length0 = (is_path ? length_array[0] : 0);
        let lon1;
        let lat1;
        let length1;

        for ( let i = 3; i < end_point; i += 3, lon0 = lon1, lat0 = lat1, length0 = length1 ) {
            // 線分の終点 (ラジアン)
            lon1 = points[i    ] * Degree;
            lat1 = points[i + 1] * Degree;
            length1 = (is_path ? length_array[i / 3] : 0);

            if ( lat0 <= -RAngle || lat0 >= RAngle ||
                 lat1 <= -RAngle || lat1 >= RAngle ) {
                // 端点の緯度の絶対値が RAngle 以上の線分は除外
                // ※ まだ検討していないので、とりあえずの処置
                continue;
            }

            // 単位球メルカトル座標系に変換
            let x0 = lon0;
            let y0 = GeoMath.invGudermannian( lat0 );
            let l0 = length0;
            let x1 = lon1;
            let y1 = GeoMath.invGudermannian( lat1 );
            let l1 = length1;

            // 左端点と右端点
            let [xL, yL, lL, xR, yR, lR] = (x0 < x1) ? [x0, y0, l0, x1, y1, l1] : [x1, y1, l1, x0, y0, l0];

            // -π <= xL < π になるように xL を正規化
            if ( xL < -Math.PI || xL >= Math.PI ) {
                let dx = xR - xL;
                xL -= TwoPI * (Math.floor( (xL - Math.PI) / TwoPI ) + 1);
                if ( xL < -Math.PI || xL >= Math.PI ) {
                    // 誤差対策
                    xL = -Math.PI;
                }
                xR = xL + dx;
            }

            if ( xL == xR && yL == yR ) {
                // 長さ 0 の線分は除外
                continue;
            }

            // 線分を追加
            segments.push( [xL, yL, lL, xR, yR, lR] );

            if ( xR > Math.PI ) {
                // 線分が 180 度子午線をまたぐとき
                // こちらは多少厳密さを無視する
                segments.push( [xL - TwoPI, yL, lL, xR - TwoPI, yR, lR] );
            }
        }

        return segments;
    }


    /**
     * @override
     */
    createAreaContent( min_x, min_y, msize, parent_content )
    {
        // 単位球メルカトルでの領域に変換
        const x_area_min = Math.PI * min_x;
        const x_area_max = Math.PI * (min_x + msize);
        const y_area_min = Math.PI * min_y;
        const y_area_max = Math.PI * (min_y + msize);

        let segments = [];

        for ( let segment of parent_content ) {
            let [xP, yP, /*lP*/, xQ, yQ, /*lQ*/] = segment;
            if ( this._intersect( x_area_min, x_area_max, y_area_min, y_area_max, xP, yP, xQ, yQ ) ) {
                segments.push( segment );
            }
        }

        return (segments.length > 0) ? segments : Entity.AreaStatus.EMPTY;
    }


    /**
     * @summary 矩形と線分の交差判定
     *
     * @desc
     * <p>矩形領域と線分が交差するかどうかを返す。</p>
     * <p>矩形領域には x 座標が x_area_max の点と、y 座標が y_area_max の点は含まれないものとする。</p>
     *
     * <pre>
     * 事前条件:
     *   x_area_min < x_area_max
     *   y_area_min < y_area_max
     * </pre>
     *
     * @param {number} x_area_min  矩形領域の最小 x 座標
     * @param {number} x_area_max  矩形領域の最大 x 座標
     * @param {number} y_area_min  矩形領域の最小 y 座標
     * @param {number} y_area_max  矩形領域の最大 y 座標
     * @param {number} xP          線分端点 P の x 座標
     * @param {number} yP          線分端点 P の y 座標
     * @param {number} xQ          線分端点 Q の x 座標
     * @param {number} yQ          線分端点 Q の y 座標
     *
     * @return {boolean}  交差するとき true, それ以外のとき false
     *
     * @private
     */
    _intersect( x_area_min, x_area_max, y_area_min, y_area_max, xP, yP, xQ, yQ )
    {
        if ( Math.abs( xP - xQ ) < Math.abs( yP - yQ ) ) {
            // 線分が垂直に近いとき
            return this._nhorz_intersect( x_area_min, x_area_max, y_area_min, y_area_max, xP, yP, xQ, yQ );
        }
        else {
            // 線分が水平に近いとき
            return this._nhorz_intersect( y_area_min, y_area_max, x_area_min, x_area_max, yP, xP, yQ, xQ );
        }
    }


    /**
     * @summary 矩形と非水平線分の交差判定
     *
     * @desc
     * <p>矩形領域と線分が交差するかどうかを返す。</p>
     * <p>矩形領域には x 座標が x_area_max の点と、y 座標が y_area_max の点は含まれないものとする。</p>
     *
     * <pre>
     * 事前条件:
     *   x_area_min < x_area_max
     *   y_area_min < y_area_max
     *   yP != yQ
     * </pre>
     *
     * <p>注意: |yP - yQ| が小さいと精度が悪くなる。</p>
     *
     * @param {number} x_area_min  矩形領域の最小 x 座標
     * @param {number} x_area_max  矩形領域の最大 x 座標
     * @param {number} y_area_min  矩形領域の最小 y 座標
     * @param {number} y_area_max  矩形領域の最大 y 座標
     * @param {number} xP          線分端点 P の x 座標
     * @param {number} yP          線分端点 P の y 座標
     * @param {number} xQ          線分端点 Q の x 座標
     * @param {number} yQ          線分端点 Q の y 座標
     *
     * @return {boolean}  交差するとき true, それ以外のとき false
     *
     * @private
     */
    _nhorz_intersect( x_area_min, x_area_max, y_area_min, y_area_max, xP, yP, xQ, yQ )
    {
        // 線分の y 座標の範囲
        let [y_line_min, y_line_max] = (yP < yQ) ? [yP, yQ] : [yQ, yP];

        if ( y_line_min >= y_area_max || y_line_max < y_area_min ) {
            // 線分の y 範囲が矩形領域の y 範囲の外側なので交差しない
            return false;
        }

        // 矩形領域と線分の y 座標が重なる範囲 (順不同)
        let y_range_0 = (y_area_min >= y_line_max) ? y_area_min : y_line_max;
        let y_range_1 = (y_area_max <= y_line_min) ? y_area_max : y_line_min;

        // y が {y_range_0, y_range_1} 範囲での線分の x 範囲 (順不同)
        let x_range_0 = xP + (xQ - xP) * (y_range_0 - yP) / (yQ - yP);
        let x_range_1 = xP + (xQ - xP) * (y_range_1 - yP) / (yQ - yP);

        // y が {y_range_0, y_range_1} 範囲での線分の x 範囲
        let [x_range_min, x_range_max] = (x_range_0 < x_range_1) ? [x_range_0, x_range_1] : [x_range_1, x_range_0];

        // [x_range_min, x_range_max] 範囲は矩形領域の x の範囲と重なるか?
        return (x_range_min < x_area_max) && (x_range_max >= x_area_min);
    }

}


 /**
 * @summary エンティティの種類の列挙型
 * @enum {object}
 * @memberof mapray.AbstractLineEntity
 * @constant
 * @see mapray.AbstractLineEntity#line_type
 */
var LineType = {

    /**
     * MarkerLineEntity
     */
    MARKERLINE: { id: "MARKERLINE" },


    /**
     * PathEntity
     */
    PATH: { id: "PATH" }
};

AbstractLineEntity.LineType = LineType;


export default AbstractLineEntity;