import Entity from "./Entity"; import Primitive from "./Primitive"; import Mesh from "./Mesh"; import Texture from "./Texture"; import PinMaterial from "./PinMaterial"; import GeoMath from "./GeoMath"; import GeoPoint from "./GeoPoint"; import { RenderTarget } from "./RenderStage"; import AltitudeMode from "./AltitudeMode"; import EntityRegion from "./EntityRegion"; import IconLoader, { URLTemplateIconLoader, TextIconLoader } from "./IconLoader"; import Dom from "./util/Dom"; import EasyBindingBlock from "./animation/EasyBindingBlock"; import Type from "./animation/Type"; import AnimUtil from "./animation/AnimUtil"; import AbstractPointEntity from "./AbstractPointEntity"; /** * @summary ピンエンティティ * @memberof mapray * @extends mapray.Entity * * @example * var pin = new mapray.PinEntity(viewer.scene); * pin.addTextPin( "32", new mapray.GeoPoint(139.768, 35.635) ); * pin.addTextPin( "A", new mapray.GeoPoint(139.768, 35.636), { fg_color: [0.0, 0.0, 1.0], bg_color: [1.0, 0.0, 0.0] } ); * pin.addTextPin( "始", new mapray.GeoPoint(139.768, 35.637), { size: 50 } ); * pin.addTextPin( "終", new mapray.GeoPoint(139.768, 35.639), { size: 50, font_family: "Georgia" } ); * pin.addPin( new mapray.GeoPoint(139.766, 35.6361) ); * pin.addMakiIconPin( "ferry-15", new mapray.GeoPoint(139.764, 35.6361), { size: 150, fg_color: [0.2, 0.2, 0.2], bg_color: [1.0, 1.0, 1.0] } ); * pin.addMakiIconPin( "car-15", new mapray.GeoPoint(139.762, 35.6361), { size: 60, fg_color: [1.0, 1.0, 1.0], bg_color: [0.2, 0.2, 0.2] } ); * pin.addMakiIconPin( "bus-15", new mapray.GeoPoint(139.760, 35.6361), { size: 40, fg_color: [1.0, 0.3, 0.1], bg_color: [0.1, 0.3, 1.0] } ); * pin.addMakiIconPin( "bus-15", new mapray.GeoPoint(139.759, 35.6361) ); * pin.addMakiIconPin( "car-15", new mapray.GeoPoint(139.758, 35.6361) ); * viewer.scene.addEntity(pin); * */ class PinEntity extends AbstractPointEntity { /** * @param {mapray.Scene} scene 所属可能シーン * @param {object} [opts] オプション集合 * @param {object} [opts.json] 生成情報 * @param {object} [opts.refs] 参照辞書 */ constructor( scene, opts ) { super( scene, opts ); // 親プロパティ this._parent_props = { fg_color: null, bg_color: null, size: null, font_family: 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; } /** * @override */ onChangeAltitudeMode( prev_mode ) { this._primitive_producer.onChangeAltitudeMode(); } /** * 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" ); const vector3 = Type.find( "vector3" ); // パラメータ名: fg_color // パラメータ型: vector3 // アイコンの色 block.addEntry( "fg_color", [vector3], null, value => { this.setFGColor( value ); } ); // パラメータ名: bg_color // パラメータ型: vector3 // アイコン背景の色 block.addEntry( "bg_color", [vector3], null, value => { this.setBGColor( value ); } ); // パラメータ名: 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.Vector3} size アイコンのピクセルサイズ */ setSize( size ) { this._setVector2Property( "size", size ); } /** * @summary アイコンの色を設定 * @param {mapray.Vector3} color アイコンの色 */ setFGColor( color ) { this._setVector3Property( "fg_color", color ); } /** * @summary アイコン背景の色を設定 * @param {mapray.Vector3} color アイコン背景の色 */ setBGColor( color ) { this._setVector3Property( "bg_color", color ); } /** * @summary テキストアイコンのフォントを設定 * @param {string} font_family フォントファミリー */ setFontFamily( font_family ) { this._setValueProperty( "font_family", font_family ); } /** * Add Pin * @param {mapray.GeoPoint} position 位置 * @param {object} [props] プロパティ * @param {float} [props.size] アイコンサイズ * @param {mapray.Vector3} [props.fg_color] アイコン色 * @param {mapray.Vector3} [props.bg_color] 背景色 * @param {string} [props.id] Entryを識別するID * @return {mapray.PinEntity.TextPinEntry} 追加したEntry */ addPin( position, props ) { return this.addTextPin( "", position, props ); } /** * Add Maki Icon Pin * @param {string} id ID of Maki Icon * @param {mapray.GeoPoint} position 位置 * @param {object} [props] プロパティ * @param {float} [props.size] アイコンサイズ * @param {mapray.Vector3} [props.fg_color] アイコン色 * @param {mapray.Vector3} [props.bg_color] 背景色 * @param {string} [props.id] Entryを識別するID * @return {mapray.PinEntity.MakiIconPinEntry} 追加したEntry */ addMakiIconPin( id, position, props ) { var entry = new MakiIconPinEntry( this, id, position, props ); this._entries.push( entry ); this._primitive_producer.onAddEntry(); return entry; } /** * Add Text Pin * @param {string} text ピンに表示されるテキスト * @param {mapray.GeoPoint} position 位置 * @param {object} [props] プロパティ * @param {float} [props.size] アイコンサイズ * @param {mapray.Vector3} [props.fg_color] アイコン色 * @param {mapray.Vector3} [props.bg_color] 背景色 * @param {string} [props.font_family] フォントファミリー * @param {string} [props.id] Entryを識別するID * @return {mapray.PinEntity.TextPinEntry} 追加したEntry */ addTextPin( text, position, props ) { var entry = new TextPinEntry( this, text, 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._PinEntity_pin_material ) { // scene にマテリアルをキャッシュ scene._PinEntity_pin_material = new PinMaterial( scene.glenv ); } return scene._PinEntity_pin_material; } else if (render_target === RenderTarget.RID) { if ( !scene._PinEntity_pin_material_pick ) { // scene にマテリアルをキャッシュ scene._PinEntity_pin_material_pick = new PinMaterial( scene.glenv, { ridMaterial: true } ); } return scene._PinEntity_pin_material_pick; } } /** * @private */ _setValueProperty( name, value ) { var props = this._parent_props; if ( props[name] != value ) { props[name] = value; this._primitive_producer.onChangeParentProperty(); } } /** * @private */ _setVector3Property( name, value ) { var dst = this._parent_props[name]; if ( !dst ) { dst = this._parent_props[name] = GeoMath.createVector3f( value ); this._primitive_producer.onChangeParentProperty(); } else if ( dst[0] !== value[0] || dst[1] !== value[1] || dst[2] !== value[2] ) { GeoMath.copyVector3( value, dst ); 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.addPin( position, entry ); } if ( json.size ) this.setSize( json.size ); if ( json.fg_color ) this.setFGColor( json.fg_color ); if ( json.bg_color ) this.setBGColor( json.bg_color ); if ( json.font_family ) this.setBGColor( json.font_family ); } /** * @summary IDでEntryを取得 * @param {string} id ID * @return {mapray.PinEntity.MakiIconPinEntry|mapray.PinEntity.TextPinEntry} IDが一致するEntry(無ければundefined) */ getEntry( id ) { return this._entries.find((entry) => entry.id === id); } } // クラス定数の定義 { PinEntity.SAFETY_PIXEL_MARGIN = 1; PinEntity.MAX_IMAGE_WIDTH = 4096; PinEntity.CIRCLE_SEP_LENGTH = 32; PinEntity.DEFAULT_SIZE = GeoMath.createVector2f( [30, 30] ); PinEntity.DEFAULT_FONT_FAMILY = "sans-serif"; PinEntity.DEFAULT_FG_COLOR = GeoMath.createVector3f( [1.0, 1.0, 1.0] ); PinEntity.DEFAULT_BG_COLOR = GeoMath.createVector3f( [0.35, 0.61, 0.81] ); PinEntity.SAFETY_PIXEL_MARGIN = 1; PinEntity.MAX_IMAGE_WIDTH = 4096; } /** * @summary PrimitiveProducer * * TODO: relative で標高の変化のたびにテクスチャを生成する必要はないので * Layout でのテクスチャの生成とメッシュの生成を分離する * * @private */ class PrimitiveProducer extends Entity.PrimitiveProducer { /** * @param {mapray.PinEntity} entity */ constructor( entity ) { super( entity ); this._glenv = entity.scene.glenv; this._dirty = true; // プリミティブの要素 this._transform = GeoMath.setIdentity( GeoMath.createMatrix() ); this._properties = { image: null, // 画像 image_mask: 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( stage ); 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; if ( properties.image_mask ) { properties.image_mask.dispose(); } properties.image_mask = layout.texture_mask; // メッシュ生成 var mesh_data = { vtype: [ { name: "a_position", size: 3 }, { name: "a_offset", size: 2 }, { name: "a_texcoord", size: 2 }, { name: "a_texmaskcoord", size: 2 }, { name: "a_fg_color", size: 3 }, { name: "a_bg_color", size: 3 }, ], 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(); } var pickPrimitive = this._pickPrimitive; if ( pickPrimitive.mesh ) { pickPrimitive.mesh.dispose(); } primitive.mesh = mesh; 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.PinEntity * @public * @abstract */ class AbstractPinEntry { constructor( owner, position, props ) { this._owner = owner; this._position = position.clone(); // animation.BindingBlock this._animation = new EasyBindingBlock(); this._setupAnimationBindingBlock(); this._props = Object.assign( {}, props ); // props の複製 this._copyPropertyVector3f( "fg_color" ); // deep copy this._copyPropertyVector3f( "bg_color" ); // deep copy this._copyPropertyVector2f( "size" ); // deep copy } _loadIcon() { throw new Error("loadIcon() is not implemented: " + this.constructor.name); } /** * @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 || ( this.icon ? GeoMath.createVector2f( [ this.icon.width, this.icon.height ] ): PinEntity.DEFAULT_SIZE ) ); } /** * @summary アイコン色 * @type {mapray.Vector3} * @readonly * @package */ get fg_color() { const props = this._props; const parent = this._owner._parent_props; return props.fg_color || parent.fg_color || PinEntity.DEFAULT_FG_COLOR; } /** * @summary アイコン背景色 * @type {mapray.Vector3} * @readonly * @package */ get bg_color() { const props = this._props; const parent = this._owner._parent_props; return props.bg_color || parent.bg_color || PinEntity.DEFAULT_BG_COLOR; } /** * @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 vector2 = Type.find( "vector2" ); const vector3 = Type.find( "vector3" ); // パラメータ名: 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 ); } ); // パラメータ名: fg_color // パラメータ型: vector3 // アイコンの色 block.addEntry( "fg_color", [vector3], null, value => { this.setFGColor( value ); } ); // パラメータ名: bg_color // パラメータ型: vector3 // アイコン背景の色 block.addEntry( "bg_color", [vector3], null, value => { this.setBGColor( value ); } ); // パラメータ名: 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.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.Vector3} size アイコンのピクセルサイズ */ setSize( size ) { this._setVector2Property( "size", size ); } /** * @summary アイコンの色を設定 * @param {mapray.Vector3} color アイコンの色 */ setFGColor( color ) { this._setVector3Property( "fg_color", color ); } /** * @summary アイコン背景の色を設定 * @param {mapray.Vector3} color アイコン背景の色 */ setBGColor( color ) { this._setVector3Property( "bg_color", color ); } /** * @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 */ _setVector3Property( name, value ) { var dst = this._props[name]; if ( !dst ) { dst = this._props[name] = GeoMath.createVector3f( value ); this._owner.getPrimitiveProducer().onChangeChildProperty(); } else if ( dst[0] !== value[0] || dst[1] !== value[1] || dst[2] !== value[2] ) { GeoMath.copyVector3( value, dst ); this._owner.getPrimitiveProducer().onChangeChildProperty(); } } /** * @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 ); } } PinEntity.AbstractPinEntry = AbstractPinEntry; /** * @summary MakiIcon要素 * @hideconstructor * @memberof mapray.PinEntity * @extends mapray.PinEntity.AbstractPinEntry * @public */ class MakiIconPinEntry extends AbstractPinEntry { /** * @param {mapray.PinEntity} owner 所有者 * @param {string} id MakiアイコンのID * @param {mapray.GeoPoint} position 位置 * @param {object} [props] プロパティ * @param {float} [props.size] アイコンサイズ * @param {mapray.Vector3} [props.fg_color] アイコン色 * @param {mapray.Vector3} [props.bg_color] 背景色 * @param {string} [props.id] Entryを識別するID */ constructor( owner, id, position, props ) { super( owner, position, props ); this.setId( id ); this._setupMakiIconPinAnimationBindingBlock(); } /** * アニメーションの BindingBlock を初期化 * * @private */ _setupMakiIconPinAnimationBindingBlock() { const block = this.animation; // 実体は EasyBindingBlock const string = Type.find( "string" ); // パラメータ名: id // パラメータ型: string // アイコンのID block.addEntry( "id", [string], null, value => { this.setId( value ); } ); } /** * @summary アイコンのIDを設定 * @param {string} maki_id アイコンのID */ setId( maki_id ) { if ( this._maki_id !== maki_id ) { // アイコンのIDが変更された this._maki_id = maki_id; this._icon = MakiIconPinEntry.makiIconLoader.load( maki_id ); this._icon.onEnd(item => { this._owner.getPrimitiveProducer()._dirty = true; }); } } } PinEntity.MakiIconPinEntry = MakiIconPinEntry; { MakiIconPinEntry.makiIconLoader = new URLTemplateIconLoader( "https://resource.mapray.com/styles/v1/icons/maki/", ".svg" ); } /** * @summary MakiIcon要素 * @hideconstructor * @memberof mapray.PinEntity * @extends mapray.PinEntity.AbstractPinEntry * @public */ class TextPinEntry extends AbstractPinEntry { /** * @param {mapray.PinEntity} owner 所有者 * @param {string} text テキスト * @param {mapray.GeoPoint} position 位置 * @param {object} [props] プロパティ * @param {float} [props.size] アイコンピクセルサイズ * @param {mapray.Vector3} [props.fg_color] アイコン色 * @param {mapray.Vector3} [props.bg_color] 背景色 * @param {string} [props.font_family] フォントファミリー * @param {string} [props.id] Entryを識別するID */ constructor( owner, text, position, props ) { super( owner, position, props ); this.setText( text ); this._setupTextPinAnimationBindingBlock(); } /** * @summary フォントファミリー * @type {string} * @readonly * @package */ get font_family() { const props = this._props; const parent = this._owner._parent_props; return props.font_family || parent.font_family || PinEntity.DEFAULT_FONT_FAMILY; } /** * アニメーションの BindingBlock を初期化 * * @private */ _setupTextPinAnimationBindingBlock() { const block = this.animation; // 実体は EasyBindingBlock const string = Type.find( "string" ); // パラメータ名: text // パラメータ型: string // テキスト block.addEntry( "text", [string], null, value => { this.setText( value ); } ); } /** * @summary テキストを設定 * @param {string} text テキスト */ setText( text ) { if ( this._text !== text ) { // テキストが変更された this._text = text; this._icon = TextPinEntry.textIconLoader.load( { text: this._text, props: { size: this.size, font_family: this.font_family, } } ); this._icon.onEnd(item => { this._owner.getPrimitiveProducer()._dirty = true; }); } } } PinEntity.TextPinEntry = TextPinEntry; { TextPinEntry.textIconLoader = new TextIconLoader(); } /** * @summary 要素を Canvas 上にレイアウト * * @memberof mapray.PinEntity * @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._texture_mask = this._createTextureMask(); 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 テクスチャマスク * @type {mapray.Texture} * @readonly */ get texture_mask() { return this._texture_mask; } /** * @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.PinEntity.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.PinEntity.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 ); } _createTextureMask() { var context = Dom.createCanvasContext( 3, 3 ); context.fillRect( 1, 1, 1, 1 ); var glenv = this._owner._glenv; var opts = { usage: Texture.Usage.ICON, mag_filter: glenv.context.NEAREST }; 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.x->| | | | |<--rx--->| ___-------___ ---- / \ ^ / \ ry | | | ---- | | v ^ | c | ---- size.y | | ^ V | | | ---- \ / | '----_0___3_----' | | | | | | h | | | | | | | | | | | v 1---2 ------------ >| w |< */ 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 rx = size[0] * 1.5 / 2; var ry = size[1] * 1.5 / 2; var h = ry * 2; var w = Math.max(2, rx / 10); // 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; var fg_color = entry.fg_color; var bg_color = entry.bg_color; // Image dimensions (Image Coordinate) var xc = item.pos_x; var yc = item.pos_y; var xsize = item.width; var ysize = item.height; var vertices_push_texture = ( px, py ) => { vertices.push( (xc + xsize * px) * xn, 1 - (yc + ysize * py) * yn ); }; // p0 vertices.push( xm, ym, zm ); // a_position vertices.push( -w / 2, h - ry ); // a_offset vertices_push_texture( 0.5 - (w/2/rx), 1.5/2 + 0.5 ); // a_texcoord vertices.push( -0.25 + 0.5, -0.25 + 0.5 ); // a_texmaskcoord vertices.push( ...fg_color ); vertices.push( ...bg_color ); // p1 vertices.push( xm, ym, zm ); // a_position vertices.push( -w / 2, 0 ); // a_offset vertices_push_texture( 0.5 - (w/2/rx), 1.5/2 + 0.5 ); // a_texcoord vertices.push( -0.25 + 0.5, -0.25 + 0.5 ); // a_texmaskcoord vertices.push( ...fg_color ); vertices.push( ...bg_color ); // p2 vertices.push( xm, ym, zm ); // a_position vertices.push( w / 2, 0 ); // a_offset vertices_push_texture( 0.5 + (w/2/rx), 1.5/2 + 0.5 ); // a_texcoord vertices.push( -0.25 + 0.5, -0.25 + 0.5 ); // a_texmaskcoord vertices.push( ...fg_color ); vertices.push( ...bg_color ); // p3 vertices.push( xm, ym, zm ); // a_position vertices.push( w / 2, h - ry ); // a_offset vertices_push_texture( 0.5 + (w/2/rx), 1.5/2 + 0.5 ); // a_texcoord vertices.push( -0.25 + 0.5, -0.25 + 0.5 ); // a_texmaskcoord vertices.push( ...fg_color ); vertices.push( ...bg_color ); // c vertices.push( xm, ym, zm ); // a_position vertices.push( 0, h ); // a_offset vertices_push_texture( 0.5, 0.5 ); // a_texcoord vertices.push( 0.5, 0.5 ); // a_texmaskcoord vertices.push( ...fg_color ); vertices.push( ...bg_color ); for ( var k = 1; k < PinEntity.CIRCLE_SEP_LENGTH; k++ ) { var th = (k / PinEntity.CIRCLE_SEP_LENGTH * 2 - 0.5) * Math.PI; var cos_th = Math.cos(th); var sin_th = Math.sin(th); vertices.push( xm, ym, zm ); // a_position vertices.push( rx * cos_th, ry * sin_th + h ); // a_offset vertices_push_texture( 1.5 * cos_th / 2 + 0.5, -1.5 * sin_th / 2 + 0.5 ); // a_texcoord vertices.push( cos_th * 0.25 + 0.5 , sin_th * 0.25 + 0.5 ); // a_texmaskcoord vertices.push( ...fg_color ); vertices.push( ...bg_color ); } } } 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 + 1 + PinEntity.CIRCLE_SEP_LENGTH - 1 ) * eitem.index; var p = base; var p0 = p; var p3 = p + 3; indices.push( p, p+1, p+2 ); indices.push( p, p+2, p+3 ); p += 4; var centerPos = p++; indices.push( centerPos, p0, p3 ); indices.push( centerPos, p3, p ); for ( var j = 1; j < PinEntity.CIRCLE_SEP_LENGTH - 1; j++ ) { indices.push( centerPos, p++, p ); } indices.push( centerPos, p++, p0 ); } } return indices; } /** * @summary アイテムの配置を設定 * @param {array.<mapray.PinEntity.RowLayout>} row_layouts * @return {object} キャンバスサイズ * @private */ _setupLocation( row_layouts ) { var width = 0; var height = 0; height += PinEntity.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 + PinEntity.SAFETY_PIXEL_MARGIN; } return { width: width, height: height }; } } /** * @summary レイアウト対象 * @memberof mapray.PinEntity * @private */ class LItem { /** * @param {mapray.PinEntity.Layout} layout 所有者 * @param {mapray.PinEntity.Entry} entry PinEntityのエントリ */ 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 ); 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.PinEntity * @private */ class RowLayout { /** * @desc * <p>レイアウトされた、またはレイアウトに失敗したアイテムは src_items から削除される。</p> * <p>レイアウトに失敗したアイテムは取り消し (is_canceled) になる。</p> * @param {array.<mapray.PinEntity.LItem>} src_items アイテムリスト */ constructor( src_items ) { var width_assumed_total = 0; var height_pixel_max = 0; var row_items = []; width_assumed_total += PinEntity.SAFETY_PIXEL_MARGIN; // 左マージン while ( src_items.length > 0 ) { var item = src_items.shift(); var width_assumed = item.width_pixel + PinEntity.SAFETY_PIXEL_MARGIN; // テキスト幅 + 右マージン if ( width_assumed_total + width_assumed <= PinEntity.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.PinEntity.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 += PinEntity.SAFETY_PIXEL_MARGIN; // 左マージン for ( var i = 0; i < items.length; ++i ) { var item = items[i]; item.locate( x, y ); x += item.width_pixel + PinEntity.SAFETY_PIXEL_MARGIN; // テキスト幅 + 右マージン } } } export default PinEntity;