Source: ImageIconEntity.js

import Entity from "./Entity";
import Primitive from "./Primitive";
import Mesh from "./Mesh";
import Texture from "./Texture";
import ImageIconMaterial from "./ImageIconMaterial";
import GeoMath from "./GeoMath";
import GeoPoint from "./GeoPoint";
import { RenderTarget } from "./RenderStage";
import AltitudeMode from "./AltitudeMode";
import EntityRegion from "./EntityRegion";
import { ImageIconLoader } from "./IconLoader";
import Dom from "./util/Dom";
import EasyBindingBlock from "./animation/EasyBindingBlock";
import Type from "./animation/Type";
import AnimUtil from "./animation/AnimUtil";
import Resource, { URLResource } from "./Resource";
import AbstractPointEntity from "./AbstractPointEntity";


/**
 * @summary 画像アイコンエンティティ
 * @memberof mapray
 * @extends mapray.Entity
 */
class ImageIconEntity extends AbstractPointEntity {

    /**
     * @param {mapray.Scene} scene        所属可能シーン
     * @param {object}       [opts]       オプション集合
     * @param {object}       [opts.json]  生成情報
     * @param {object}       [opts.refs]  参照辞書
     * @param {mapray.Loader.TransformCallback} [opts.transform] 
     */
    constructor( scene, opts )
    {
        super( scene, opts );

        // 親プロパティ
        this._parent_props = {
            size: null,
            origin: null,
        };

        // Entity.PrimitiveProducer インスタンス
        this._primitive_producer = new PrimitiveProducer( this );

        this._animation.addDescendantUnbinder( () => { this._unbindDescendantAnimations(); } );
        this._setupAnimationBindingBlock();

        // 生成情報から設定
        if ( opts && opts.json ) {
            this._setupByJson( opts.json );
        }
    }


    /**
     * @override
     */
    getPrimitiveProducer()
    {
        return this._primitive_producer;
    }


    /**
     * EasyBindingBlock.DescendantUnbinder 処理
     *
     * @private
     */
    _unbindDescendantAnimations()
    {
        // すべてのエントリーを解除
        for ( let entry of this._entries ) {
            entry.animation.unbindAllRecursively();
        }
    }


    /**
     * アニメーションの BindingBlock を初期化
     *
     * @private
     */
    _setupAnimationBindingBlock()
    {
        const block = this.animation;  // 実体は EasyBindingBlock

        const number = Type.find( "number" );
        const vector2 = Type.find( "vector2" );

        // パラメータ名: size
        // パラメータ型: vector2 | number
        //   型が vector2 のとき アイコンのピクセルサイズX, Y 順であると解釈
        //   型が number のとき アイコンのピクセルサイズX, Y の値
        const size_temp = GeoMath.createVector2();
        let   size_type;

        let size_tsolver = curve => {
            size_type = AnimUtil.findFirstTypeSupported( curve, [vector2, number] );
            return size_type;
        };

        block.addEntry( "size", [vector2, number], size_tsolver, value => {
            if ( size_type === vector2 ) {
                this.setSize( value );
            }
            else { // size_type === number
                size_temp[0] = value;
                size_temp[1] = value;
                this.setSize( size_temp );
            }
        } );
    }


    /**
     * @summary アイコンのサイズを指定
     * @param {mapray.Vector2} size  アイコンのピクセルサイズ
     */
    setSize( size ) {
        this._setVector2Property( "size", size );
    }


    /**
     * @summary アイコンの原点位置を指定
     * @param {mapray.Vector2} origin  アイコンの原点位置
     */
    setOrigin( origin ) {
        this._setVector2Property( "origin", origin );
    }


    /**
     * @summary Add Image Icon
     * @param {URL|HTMLImageElement|HTMLCanvasElement} image_src    画像
     * @param {mapray.GeoPoint} position            位置
     * @param {object}          [props]             プロパティ
     * @param {mapray.Vector2}  [props.size]        アイコンサイズ
     * @param {string}          [props.id]          Entryを識別するID
     * @param {mapray.Loader.Transform} [props.transform] URL変換関数
     * @return {mapray.ImageIconEntity.ImageEntry}  追加したEntry
     */
    addImageIcon( image_src, position, props ) 
    {
        var entry = new ImageEntry( this, image_src, position, props );
        this._entries.push( entry );
        this._primitive_producer.onAddEntry();
        return entry;
    }


    /**
     * @summary 専用マテリアルを取得
     * @private
     */
    _getMaterial( render_target )
    {
        var scene = this.scene;
        if ( render_target === RenderTarget.SCENE ) {
            if ( !scene._ImageEntity_image_material ) {
                // scene にマテリアルをキャッシュ
                scene._ImageEntity_image_material = new ImageIconMaterial( scene.glenv );
            }
            return scene._ImageEntity_image_material;
        }
        else if (render_target === RenderTarget.RID) {
            if ( !scene._ImageEntity_image_material_pick ) {
                // scene にマテリアルをキャッシュ
                scene._ImageEntity_image_material_pick = new ImageIconMaterial( scene.glenv, { ridMaterial: true } );
            }
            return scene._ImageEntity_image_material_pick;
        }
    }


    /**
     * @private
     */
    _setValueProperty( name, value )
    {
        var props = this._parent_props;
        if ( props[name] != value ) {
            props[name] = value;
            this._primitive_producer.onChangeParentProperty();
        }
    }

    /**
     * @private
     */
    _setVector2Property( name, value )
    {
        var dst = this._parent_props[name];
        if ( !dst ) {
            this._parent_props[name] = GeoMath.createVector2f( value );
            this._primitive_producer.onChangeParentProperty();
        }
        else if ( dst[0] !== value[0] || dst[1] !== value[1] ) {
            GeoMath.copyVector2( value, dst );
            this._primitive_producer.onChangeParentProperty();
        }
    }

    /**
     * @private
     */
    _setupByJson( json )
    {
        var position = new GeoPoint();

        for ( let entry of json.entries ) {
            position.setFromArray( entry.position );
            this.addImageIcon( position, entry );
        }

        if ( json.size )     this.setSize( json.size );
        if ( json.origin )   this.setOrigin( json.origin );
    }

    
    /**
     * @summary IDでEntryを取得
     * @param {string}  id  ID
     * @return {mapray.ImageIconEntity.ImageEntry}  IDが一致するEntry(無ければundefined)
     */
    getEntry( id )
    {
        return this._entries.find((entry) => entry.id === id);
    }
}


// クラス定数の定義
{
    ImageIconEntity.DEFAULT_COLOR       = GeoMath.createVector3f( [1, 1, 1] );
    ImageIconEntity.SAFETY_PIXEL_MARGIN = 1;
    ImageIconEntity.MAX_IMAGE_WIDTH     = 4096;
    ImageIconEntity.CIRCLE_SEP_LENGTH   = 32;
    ImageIconEntity.DEFAULT_ICON_SIZE   = GeoMath.createVector2f( [30, 30] );
    ImageIconEntity.DEFAULT_ORIGIN      = GeoMath.createVector2f( [ 0.5, 0.5 ] );

    ImageIconEntity.SAFETY_PIXEL_MARGIN = 1;
    ImageIconEntity.MAX_IMAGE_WIDTH     = 4096;
}



/**
 * @summary PrimitiveProducer
 *
 * TODO: relative で標高の変化のたびにテクスチャを生成する必要はないので
 *       Layout でのテクスチャの生成とメッシュの生成を分離する
 *
 * @private
 */
class PrimitiveProducer extends Entity.PrimitiveProducer {

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

        this._glenv = entity.scene.glenv;
        this._dirty = true;

        // プリミティブの要素
        this._transform  = GeoMath.setIdentity( GeoMath.createMatrix() );
        this._properties = {
            image: null,       // アイコン画像
        };

        // プリミティブ
        var primitive = new Primitive( this._glenv, null, entity._getMaterial( RenderTarget.SCENE ), this._transform );
        primitive.properties = this._properties;
        this._primitive = primitive;

        var pickPrimitive = new Primitive( this._glenv, null, entity._getMaterial( RenderTarget.RID ), this._transform );
        pickPrimitive.properties = this._properties;
        this._pickPrimitive = pickPrimitive;

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

        this._pickPrimitives = [];
    }


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

        for ( let {position} of this.entity._entries ) {
            region.addPoint( position );
        }

        return [region];
    }


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


    /**
     * @override
     */
    getPrimitives( stage )
    {
        this._updatePrimitive();
        return stage.getRenderTarget() === RenderTarget.SCENE ? this._primitives : this._pickPrimitives;
    }


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


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


    /**
     * @summary 高度モードが変更されたことを通知
     */
    onChangeAltitudeMode()
    {
        this._dirty = true;
    }


    /**
     * @summary エントリが追加されたことを通知
     */
    onAddEntry()
    {
        // 変化した可能性がある
        this.needToCreateRegions();
        this._dirty = true;
    }


    /**
     * @summary プリミティブの更新
     *
     * @desc
     * 入力:
     *   this.entity._entries
     *   this._dirty
     * 出力:
     *   this._transform
     *   this._properties.image
     *   this._primitive.mesh
     *   this._primitives
     *   this._dirty
     *
     * @return {array.<mapray.Prmitive>}  this._primitives
     *
     * @private
     */
    _updatePrimitive()
    {
        if ( !this._dirty ) {
            // 更新する必要はない
            return;
        }

        if ( this.entity._entries.length == 0 ) {
            this._primitives = [];
            this._pickPrimitives = [];
            this._dirty = false;
            return;
        }

        // 各エントリーの GOCS 位置を生成 (平坦化配列)
        var gocs_array = this._createFlatGocsArray();

        // プリミティブの更新
        //   primitive.transform
        this._updateTransform( gocs_array );

        var layout = new Layout( this, gocs_array );
        if ( !layout.isValid() ) {
            // 更新に失敗
            this._primitives = [];
            this._pickPrimitives = [];
            this._dirty = false;
            return;
        }

        // テクスチャ設定
        var properties = this._properties;
        if ( properties.image ) {
            properties.image.dispose();
        }
        properties.image = layout.texture;

        // メッシュ生成
        var mesh_data = {
            vtype: [
                { name: "a_position", size: 3 },
                { name: "a_offset",   size: 2 },
                { name: "a_texcoord", size: 2 },
            ],
            vertices: layout.vertices,
            indices:  layout.indices
        };
        var mesh = new Mesh( this._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._primitives = [primitive];
        this._pickPrimitives = [pickPrimitive];
        this._dirty = false;
    }


    /**
     * @summary プリミティブの更新
     *
     * @desc
     * 条件:
     *   this.entity._entries.length > 0
     * 入力:
     *   this.entity._entries.length
     * 出力:
     *   this._transform
     *
     * @param {number[]} gocs_array  GOCS 平坦化配列
     *
     * @private
     */
    _updateTransform( gocs_array )
    {
        var num_entries = this.entity._entries.length;
        var        xsum = 0;
        var        ysum = 0;
        var        zsum = 0;

        for ( let i = 0; i < num_entries; ++i ) {
            let ibase = 3*i;
            xsum += gocs_array[ibase];
            ysum += gocs_array[ibase + 1];
            zsum += gocs_array[ibase + 2];
        }

        // 変換行列の更新
        var transform = this._transform;
        transform[12] = xsum / num_entries;
        transform[13] = ysum / num_entries;
        transform[14] = zsum / num_entries;
    }


    /**
     * @summary GOCS 平坦化配列を取得
     *
     * 入力: this.entity._entries
     *
     * @return {number[]}  GOCS 平坦化配列
     * @private
     */
    _createFlatGocsArray()
    {
        const num_points = this.entity._entries.length;
        return GeoPoint.toGocsArray( this._getFlatGeoPoints_with_Absolute(), num_points,
                                     new Float64Array( 3 * num_points ) );
    }


    /**
     * @summary GeoPoint 平坦化配列を取得 (絶対高度)
     *
     * 入力: this.entity._entries
     *
     * @return {number[]}  GeoPoint 平坦化配列
     * @private
     */
    _getFlatGeoPoints_with_Absolute()
    {
        const owner      = this.entity;
        const entries    = owner._entries;
        const num_points = entries.length;
        const flat_array = new Float64Array( 3 * num_points );

        // flat_array[] に経度要素と緯度要素を設定
        for ( let i = 0; i < num_points; ++i ) {
            let pos = entries[i].position;
            flat_array[3*i]     = pos.longitude;
            flat_array[3*i + 1] = pos.latitude;
        }

        switch ( owner.altitude_mode ) {
        case AltitudeMode.RELATIVE:
        case AltitudeMode.CLAMP:
            // flat_array[] の高度要素に現在の標高を設定
            owner.scene.viewer.getExistingElevations( num_points, flat_array, 0, 3, flat_array, 2, 3 );

            if ( owner.altitude_mode === AltitudeMode.RELATIVE ) {
                // flat_array[] の高度要素に絶対高度を設定
                for ( let i = 0; i < num_points; ++i ) {
                    flat_array[3*i + 2] += entries[i].position.altitude;
                }
            }
            break;

        default: // AltitudeMode.ABSOLUTE
            // flat_array[] の高度要素に絶対高度を設定
            for ( let i = 0; i < num_points; ++i ) {
                flat_array[3*i + 2] = entries[i].position.altitude;
            }
            break;
        }

        return flat_array;
    }

}



/**
 * @summary 要素
 * @hideconstructor
 * @memberof mapray.ImageIconEntity
 * @public
 */
class ImageEntry {

    /**
     * @param {mapray.ImageIconEntity} owner        所有者
     * @param {string}                 image_src    アイコン画像
     * @param {mapray.GeoPoint}        position     位置
     * @param {object}                 [props]      プロパティ
     * @param {mapray.Vector2}         [props.size] アイコンサイズ
     * @param {string}                 [props.id]   Entryを識別するID
     * @param {mapray.Loader.Transform} [props.transform] URL変換関数
     */
    constructor( owner, image_src, position, props )
    {
        this._owner = owner;
        this._position = position.clone();

        // animation.BindingBlock
        this._animation = new EasyBindingBlock();
        
        this._setupAnimationBindingBlock();

        this._props = Object.assign( {}, props );  // props の複製
        this._copyPropertyVector2f( "size" );      // deep copy
        this._copyPropertyVector2f( "origin" );    // deep copy

        this.setImage( image_src );
    }

    /**
     * @summary 位置
     * @type {mapray.GeoPoint}
     * @readonly
     * @package
     */
    get position()
    {
        return this._position;
    }

    /**
     * @summary ID
     * @type {string}
     * @readonly
     */
    get id()
    {
        return this._props.hasOwnProperty( "id" ) ? this._props.id : "";
    }

    /**
     * @summary アイコンサイズ (Pixels)
     * @type {mapray.Vector2}
     * @readonly
     * @package
     */
    get size()
    {
        const props = this._props;
        const parent = this._owner._parent_props;
        return (
            props.size || parent.size ||
            GeoMath.createVector2f( [ this._icon.width, this._icon.height ] )
        );
    }

    /**
     * @summary アイコンオリジン位置 (左上を(0, 0)、右下を(1, 1)としする数字を指定する。)
     * @type {mapray.Vector2}
     * @readonly
     * @package
     */
    get origin()
    {
        const props = this._props;
        const parent = this._owner._parent_props;
        return props.origin || parent.origin || ImageIconEntity.DEFAULT_ORIGIN;
    }

    /**
     * @summary アニメーションパラメータ設定
     *
     * @type {mapray.animation.BindingBlock}
     * @readonly
     */
    get animation() { return this._animation; }
        
    /**
     * アニメーションの BindingBlock を初期化
     *
     * @private
     */
    _setupAnimationBindingBlock()
    {
        const block = this.animation;  // 実体は EasyBindingBlock

        const number  = Type.find( "number"  );
        const string  = Type.find( "string"  );
        const vector2 = Type.find( "vector2" );
        const vector3 = Type.find( "vector3" );
        
        // パラメータ名: image_src
        // パラメータ型: string
        //   画像のパス
        block.addEntry( "image_src", [string], null, value => {
            this.setImage( value );
        } );

        // パラメータ名: position
        // パラメータ型: vector3
        //   ベクトルの要素が longitude, latitude, altitude 順であると解釈
        const position_temp = new GeoPoint();

        block.addEntry( "position", [vector3], null, value => {
            position_temp.setFromArray( value );  // Vector3 -> GeoPoint
            this.setPosition( position_temp );
        } );

        // パラメータ名: size
        // パラメータ型: vector2 | number
        //   型が vector2 のとき アイコンのピクセルサイズX, Y 順であると解釈
        //   型が number のとき アイコンのピクセルサイズX, Y は同値
        const size_temp = GeoMath.createVector2();
        let   size_type;

        let size_tsolver = curve => {
            size_type = AnimUtil.findFirstTypeSupported( curve, [vector2, number] );
            return size_type;
        };

        block.addEntry( "size", [vector2, number], size_tsolver, value => {
            if ( size_type === vector2 ) {
                this.setSize( value );
            }
            else { // size_type === number
                size_temp[0] = value;
                size_temp[1] = value;
                this.setSize( size_temp );
            }
        } );
    }

    
    /**
     * @summary 画像のパスを設定
     * @param {string} image_src  画像のパス
     */
    setImage( image_src )
    {
        if ( this._image_src !== image_src ) {
            // 画像のパスが変更された
            this._image_src = image_src;
            const resource = (
                image_src instanceof Resource ? image_src:
                new URLResource( image_src, { transform: this._props.transform })
            );
            this._icon = ImageEntry.iconLoader.load( resource );
            this._icon.onEnd(item => {
                    this._owner.getPrimitiveProducer()._dirty = true;
            });
        }
    }

    /**
     * @summary テキスト原点位置を設定
     *
     * @param {mapray.GeoPoint} position  テキスト原点の位置
     */
    setPosition( position )
    {
        if ( this._position.longitude !== position.longitude ||
             this._position.latitude  !== position.latitude  ||
             this._position.altitude  !== position.altitude ) {
            // 位置が変更された
            this._position.assign( position );
            this._owner.getPrimitiveProducer().onChangeChildProperty();
        }
    }

    /**
     * @summary アイコンのサイズを指定
     * @param {mapray.Vector2} size  アイコンのピクセルサイズ
     */
    setSize( size ) {
        this._setVector2Property( "size", size );
    }

    /**
     * @private
     */
    _copyPropertyVector3f( name )
    {
        var props = this._props;
        if ( props.hasOwnProperty( name ) ) {
            props[name] = GeoMath.createVector3f( props[name] );
        }
    }

    /**
     * @private
     */
    _copyPropertyVector2f( name )
    {
        var props = this._props;
        if ( props.hasOwnProperty( name ) ) {
            if ( typeof( props[name] ) === 'number' ) {
                props[name] = GeoMath.createVector2f( [ props[name], props[name] ] );
            }
            else {
                props[name] = GeoMath.createVector2f( props[name] );
            }
        }
    }

    /**
     * @private
     */
    _setVector2Property( name, value )
    {
        var dst = this._props[name];
        if ( !dst ) {
            this._props[name] = GeoMath.createVector2f( value );
            this._owner.getPrimitiveProducer().onChangeChildProperty();
        }
        else if ( dst[0] !== value[0] || dst[1] !== value[1] ) {
            GeoMath.copyVector2( value, dst );
            this._owner.getPrimitiveProducer().onChangeChildProperty();
        }
    }

    isLoaded() {
        return this._icon.isLoaded();
    }

    get icon() {
        return this._icon;
    }

    draw( context, x, y, width, height ) {
        this._icon.draw( context, x, y, width, height );
    }
}

ImageIconEntity.ImageEntry = ImageEntry;


{
    ImageEntry.iconLoader = new ImageIconLoader();
}



/**
 * @summary Pin画像を Canvas 上にレイアウト
 * @memberof mapray.ImageIconEntity
 * @private
 */
class Layout {

    /**
     * @desc
     * 入力:
     *   owner._glenv
     *   owner.entity._entries
     *   owner._transform
     *
     * @param {PrimitiveProducer} owner       所有者
     * @param {number[]}          gocs_array  GOCS 平坦化配列
     */
    constructor( owner, gocs_array )
    {
        this._owner = owner;
        this._items = this._createItemList();
        this._is_valid = true;

        var row_layouts = this._createRowLayouts();
        if ( row_layouts.length == 0 ) {
            // 有効なテキストが1つも無い
            this._is_valid = false;
            return;
        }

        // アイテムの配置の設定とキャンバスサイズの決定
        var size = this._setupLocation( row_layouts );

        this._texture  = this._createTexture( size.width, size.height );
        this._vertices = this._createVertices( size.width, size.height, gocs_array );
        this._indices  = this._createIndices();
    }


    /**
     * @summary 有効なオブジェクトか?
     * @desc
     * <p>無効のとき、他のメソッドは呼び出せない。</p>
     * @return {boolean}  有効のとき true, 無効のとき false
     */
    isValid()
    {
        return this._is_valid;
    }


    /**
     * @summary テクスチャ
     * @type {mapray.Texture}
     * @readonly
     */
    get texture()
    {
        return this._texture;
    }

    /**
     * @summary 頂点配列
     * @desc
     * 条件:
     *   this._entries.length > 0
     * 入力:
     *   this._entries
     *   this._transform
     * @type {Float32Array}
     * @readonly
     */
    get vertices()
    {
        return this._vertices;
    }


    /**
     * @summary インデックス配列
     * @type {Uint32Array}
     * @readonly
     */
    get indices()
    {
        return this._indices;
    }


    /**
     * @summary レイアウトアイテムのリストを生成
     * @return {array.<mapray.ImageIconEntity.LItem>}
     * @private
     */
    _createItemList()
    {
        const map = new Map();

        const items = [];
        let counter = 0;
        for ( let entry of this._owner.entity._entries ) {
            if ( entry.isLoaded() ) {
                let item = map.get( entry.icon );
                if ( !item ) {
                    map.set( entry.icon, item = new LItem( this ) );
                    items.push( item );
                }
                item.add( counter++, entry );
            }
        }

        return items;
    }

    /**
     * @summary RowLayout のリストを生成
     * @return {array.<mapray.ImageIconEntity.RowLayout>}
     * @private
     */
    _createRowLayouts()
    {
        // アイテムリストの複製
        var items = [].concat( this._items );

        // RowLayout 内であまり高さに差が出ないように、アイテムリストを高さで整列
        items.sort( function( a, b ) { return a.height_pixel - b.height_pixel; } );

        // リストを生成
        var row_layouts = [];
        while ( items.length > 0 ) {
            var row_layout = new RowLayout( items );
            if ( row_layout.isValid() ) {
                row_layouts.push( row_layout );
            }
        }

        return row_layouts;
    }


    /**
     * @summary テクスチャを生成
     * @param  {number} width    横幅
     * @param  {number} height   高さ
     * @return {mapray.Texture}  テキストテクスチャ
     * @private
     */
    _createTexture( width, height )
    {
        var context = Dom.createCanvasContext( width, height );

        var items = this._items;
        for ( var i = 0; i < items.length; ++i ) {
            var item = items[i];
            if ( item.is_canceled ) continue;
            item.draw( context );
        }

        var glenv = this._owner._glenv;
        var opts = {
            usage: Texture.Usage.ICON
        };
        return new Texture( glenv, context.canvas, opts );
    }


    /**
     * @summary 頂点配列を生成
     *
     * @param  {number}   width       横幅
     * @param  {number}   height      高さ
     * @param  {number[]} gocs_array  GOCS 平坦化配列
     * @return {array.<number>}  頂点配列 [左下0, 右下0, 左上0, 右上0, ...]
     *
     * @private
     */
    _createVertices( width, height, gocs_array )
    {
        var vertices = [];

        // テキスト集合の原点 (GOCS)
        var transform = this._owner._transform;
        var xo = transform[12];
        var yo = transform[13];
        var zo = transform[14];

        /*

        |<----size[0]px---->|

        0-------------------3 ------------------
        |                   |  ^              ^ 
        |                   |  | origin[1]    | 
        |                   |  |              | 
        |                   |  v              | size[1]px
        |           o       | ---             | 
        |                   |  ^              | 
        |                   |  | 1-origin[1]  | 
        |                   |  v              v 
        1-------------------2 ------------------
        
        |           |<----->|    1 - origin[0]
        |<--------->|            origin[0]
        */

        var xn = 1 / width;
        var yn = 1 / height;

        var items = this._items;
        for ( var i = 0; i < items.length; ++i ) {
            var item = items[i];
            if ( item.is_canceled ) continue;

            for ( var ie = 0; ie < item.entries.length; ie++ ) {
                var eitem = item.entries[ie];
                var entry = eitem.entry;
                var size = entry.size;
                var origin = entry.origin;

                // Relativize based on (xo, yo, zo)
                var ibase = eitem.index * 3;
                var xm = gocs_array[ibase]     - xo;
                var ym = gocs_array[ibase + 1] - yo;
                var zm = gocs_array[ibase + 2] - zo;

                // Image dimensions (Image Coordinate)
                var xc = item.pos_x;
                var yc = item.pos_y;
                var xsize = item.width;
                var ysize = item.height;

                // p0
                vertices.push( xm, ym, zm );                                     // a_position
                vertices.push( -origin[0]*size[0], (origin[1])*size[1] );        // a_offset
                vertices.push( xc * xn, 1.0 - yc * yn );                         // a_texcoord

                // p1
                vertices.push( xm, ym, zm );                                    // a_position
                vertices.push( -origin[0]*size[0], -(1-origin[1])*size[1] );    // a_offset
                vertices.push( xc * xn, 1 - (yc + ysize) * yn );                // a_texcoord

                // p2
                vertices.push( xm, ym, zm );                                    // a_position
                vertices.push( (1-origin[0])*size[0], -(1-origin[1])*size[1] ); // a_offset
                vertices.push( (xc + xsize) * xn, 1 - (yc + ysize) * yn );      // a_texcoord

                // p3
                vertices.push( xm, ym, zm );                                    // a_position
                vertices.push( (1-origin[0])*size[0], origin[1]*size[1] );      // a_offset
                vertices.push( (xc + xsize) * xn, 1 - yc * yn );                // a_texcoord
            }
        }

        return vertices;
    }


    /**
     * @summary インデックス配列を生成
     * @return {array.<number>}  インデックス配列 []
     * @private
     */
    _createIndices()
    {
        var indices = [];

        var items = this._items;
        for ( var i = 0; i < items.length; ++i ) {
            var item = items[i];
            if ( item.is_canceled ) continue;

            for ( var ie = 0; ie < item.entries.length; ie++ ) {
                var eitem = item.entries[ie];
                var base = 4 * eitem.index;

                var p = base;
                indices.push( p, p+1, p+2 );
                indices.push( p, p+2, p+3 );
            }
        }

        return indices;
    }


    /**
     * @summary アイテムの配置を設定
     * @param  {array.<mapray.ImageIconEntity.RowLayout>} row_layouts
     * @return {object}                              キャンバスサイズ
     * @private
     */
    _setupLocation( row_layouts )
    {
        var width  = 0;
        var height = 0;

        height += ImageIconEntity.SAFETY_PIXEL_MARGIN;

        for ( var i = 0; i < row_layouts.length; ++i ) {
            var row_layout = row_layouts[i];
            row_layout.locate( height );
            width   = Math.max( row_layout.width_assumed, width );
            height += row_layout.height_pixel + ImageIconEntity.SAFETY_PIXEL_MARGIN;
        }

        return {
            width:  width,
            height: height
        };
    }
}



/**
 * @summary レイアウト対象
 * @memberof mapray.ImageIconEntity
 * @private
 */
class LItem {

    /**
     * @param {mapray.ImageIconEntity.Layout} layout   所有者
     * @param {mapray.ImageIconEntity.Entry}  entry    ImageIconEntityのエントリ
     */
    constructor( layout )
    {
        this.entries = [];

        // テキストの基点
        this._pos_x = 0;  // 左端
        this._pos_y = 0;  // ベースライン位置

        this._height = this._width = null;

        this._is_canceled = false;
    }

    add( index, entry ) {
        var size = entry.size;
        if ( this._width === null || this._width < size[0] ) this._width = size[0];
        if ( this._height === null || this._height < size[1] ) this._height = size[1];
        this.entries.push( { index, entry } );
    }

    /**
     * @type {number}
     * @readonly
     */
    get pos_x()
    {
        return this._pos_x;
    }


    /**
     * @type {number}
     * @readonly
     */
    get pos_y()
    {
        return this._pos_y;
    }


    /**
     * @type {number}
     * @readonly
     */
    get width()
    {
        return this._width;
    }

    get height()
    {
        return this._height;
    }


    /**
     * キャンバス上でのテキストの横画素数
     * @type {number}
     * @readonly
     */
    get width_pixel()
    {
        return Math.ceil( this._width );
    }


    /**
     * キャンバス上でのテキストの縦画素数
     * @type {number}
     * @readonly
     */
    get height_pixel()
    {
        return Math.ceil( this._height );
    }


    /**
     * 取り消し状態か?
     * @type {boolean}
     * @readonly
     */
    get is_canceled()
    {
        return this._is_canceled;
    }


    /**
     * @summary 取り消し状態に移行
     */
    cancel()
    {
        this._is_canceled = true;
    }


    /**
     * @summary 配置を決定
     * @param {number} x  テキスト矩形左辺の X 座標 (キャンバス座標系)
     * @param {number} y  テキスト矩形上辺の Y 座標 (キャンバス座標系)
     */
    locate( x, y )
    {
        this._pos_x = x;
        this._pos_y = y;
    }

    draw( context ) {

        this.entries[0].entry.draw( context, this._pos_x, this.pos_y, this.width, this.height ); // @Todo: fix this

        var RENDER_BOUNDS = false;
        if ( RENDER_BOUNDS ) {
            context.beginPath();
            context.moveTo( this._pos_x             , this._pos_y );
            context.lineTo( this._pos_x + this.width, this._pos_y );
            context.lineTo( this._pos_x + this.width, this._pos_y + this.height );
            context.lineTo( this._pos_x             , this._pos_y + this.height );
            context.closePath();
            context.stroke();
        }
    }
}



/**
 * @summary 水平レイアウト
 * @memberof mapray.ImageIconEntity
 * @private
 */
class RowLayout {

    /**
     * @desc
     * <p>レイアウトされた、またはレイアウトに失敗したアイテムは src_items から削除される。</p>
     * <p>レイアウトに失敗したアイテムは取り消し (is_canceled) になる。</p>
     * @param {array.<mapray.ImageIconEntity.LItem>} src_items  アイテムリスト
     */
    constructor( src_items )
    {
        var width_assumed_total = 0;
        var height_pixel_max    = 0;
        var row_items           = [];

        width_assumed_total += ImageIconEntity.SAFETY_PIXEL_MARGIN;  // 左マージン

        while ( src_items.length > 0 ) {
            var item          = src_items.shift();
            var width_assumed = item.width_pixel + ImageIconEntity.SAFETY_PIXEL_MARGIN;  // テキスト幅 + 右マージン

            if ( width_assumed_total + width_assumed <= ImageIconEntity.MAX_IMAGE_WIDTH ) {
                // 行にアイテムを追加
                row_items.push( item );
                width_assumed_total += width_assumed;
                height_pixel_max = Math.max( item.height_pixel, height_pixel_max );
            }
            else {
                if ( row_items.length == 0 ) {
                    // テキストが長すぎて表示できない
                    item.cancel();
                }
                else {
                    // 次の行になるため差し戻して終了
                    src_items.unshift( item );
                    break;
                }
            }
        }

        this._items         = row_items;
        this._width_assumed = width_assumed_total;
        this._height_pixel  = height_pixel_max;
    }


    /**
     * @summary 有効なオブジェクトか?
     * @desc
     * <p>無効のとき、他のメソッドは呼び出せない。</p>
     * @return {boolean}  有効のとき true, 無効のとき false
     */
    isValid()
    {
        return this._items.length > 0;
    }


    /**
     * 
     * @type {array.<mapray.ImageIconEntity.LItem>}
     * @readonly
     */
    get items()
    {
        return this._items;
    }


    /**
     * キャンバス上での行の横占有画素数
     * @type {number}
     * @readonly
     */
    get width_assumed()
    {
        return this._width_assumed;
    }


    /**
     * キャンバス上での行の縦画素数
     * @type {number}
     * @readonly
     */
    get height_pixel()
    {
        return this._height_pixel;
    }


    /**
     * @summary レイアウトの配置を決定
     * @param {number} y  テキスト矩形上辺の Y 座標 (キャンバス座標系)
     */
    locate( y )
    {
        var items = this._items;
        var x = 0;

        x += ImageIconEntity.SAFETY_PIXEL_MARGIN;  // 左マージン

        for ( var i = 0; i < items.length; ++i ) {
            var item = items[i];
            item.locate( x, y );
            x += item.width_pixel + ImageIconEntity.SAFETY_PIXEL_MARGIN;  // テキスト幅 + 右マージン
        }
    }

}




export default ImageIconEntity;