import Entity from "./Entity"; import Primitive from "./Primitive"; import Mesh from "./Mesh"; import Texture from "./Texture"; import TextMaterial from "./TextMaterial"; import SimpleTextMaterial from "./SimpleTextMaterial"; import GeoMath from "./GeoMath"; import GeoPoint from "./GeoPoint"; import { RenderTarget } from "./RenderStage"; import AltitudeMode from "./AltitudeMode"; import EntityRegion from "./EntityRegion"; import Dom from "./util/Dom"; import Color from "./util/Color"; import EasyBindingBlock from "./animation/EasyBindingBlock"; import Type from "./animation/Type"; import AbstractPointEntity from "./AbstractPointEntity"; /** * @summary テキストエンティティ * * @memberof mapray * @extends mapray.Entity */ class TextEntity extends AbstractPointEntity { /** * @param {mapray.Scene} scene 所属可能シーン * @param {object} [opts] オプション集合 * @param {object} [opts.json] 生成情報 * @param {object} [opts.refs] 参照辞書 */ constructor( scene, opts ) { super( scene, opts ); // テキストの親プロパティ this._text_parent_props = { font_style: "normal", font_weight: "normal", font_size: TextEntity.DEFAULT_FONT_SIZE, font_family: TextEntity.DEFAULT_FONT_FAMILY, color: Color.generateOpacityColor( TextEntity.DEFAULT_COLOR ), stroke_color: Color.generateOpacityColor( TextEntity.DEFAULT_STROKE_COLOR ), stroke_width: TextEntity.DEFAULT_STROKE_WIDTH, bg_color: Color.generateOpacityColor( TextEntity.DEFAULT_BG_COLOR ), enable_stroke: false, enable_bg: false }; 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 ) { if ( this._primitive_producer ) { 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 string = Type.find( "string" ); const vector3 = Type.find( "vector3" ); // パラメータ名: font_style // パラメータ型: string // フォントスタイル block.addEntry( "font_style", [string], null, value => { this.setFontStyle( value ); } ); // パラメータ名: font_weight // パラメータ型: string // フォントの太さ block.addEntry( "font_weight", [string], null, value => { this.setFontWeight( value ); } ); // パラメータ名: font_size // パラメータ型: number // フォントの大きさ block.addEntry( "font_size", [number], null, value => { this.setFontSize( value ); } ); // パラメータ名: color // パラメータ型: vector3 // テキストの色 block.addEntry( "color", [vector3], null, value => { this.setColor( value ); } ); // パラメータ名: stroke_color // パラメータ型: vector3 // 縁の色 block.addEntry( "stroke_color", [vector3], null, value => { this.setStrokeColor( value ); } ); // パラメータ名: stroke_width // パラメータ型: number // 縁の線幅 block.addEntry( "stroke_width", [number], null, value => { this.setStrokeLineWidth( value ); } ); } /** * @summary フォントスタイルを設定 * @param {string} style フォントスタイル ("normal" | "italic" | "oblique") */ setFontStyle( style ) { this._setValueProperty( "font_style", style ); } /** * @summary フォントの太さを設定 * @param {string} weight フォントの太さ ("normal" | "bold") */ setFontWeight( weight ) { this._setValueProperty( "font_weight", weight ); } /** * @summary フォントの大きさを設定 * @param {number} size フォントの大きさ (Pixels) */ setFontSize( size ) { this._setValueProperty( "font_size", size ); } /** * @summary フォントファミリーを設定 * @param {string} family フォントファミリー * @see https://developer.mozilla.org/ja/docs/Web/CSS/font-family */ setFontFamily( family ) { this._setValueProperty( "font_family", family ); } /** * @summary テキストの色を設定 * @param {mapray.Vector3} color テキストの色 */ setColor( color ) { this._setColorProperty( "color", color ); } /** * @summary テキスト縁の色を設定 * @param {mapray.Vector3} color 縁の色 */ setStrokeColor( color ) { this._setColorProperty( "stroke_color", color ); } /** * @summary テキスト縁の太さを設定 * @param {mapray.number} width 縁の線幅 */ setStrokeLineWidth( width ) { this._setValueProperty( "stroke_width", width ); } /** * @summary テキスト縁を有効にするかどうか * @param {boolean} enable trueなら有効 */ setEnableStroke( enable ) { this._setValueProperty( "enable_stroke", enable ); this._primitive_producer = new PrimitiveProducer( this ); } /** * @summary テキスト背景の色を設定 * @param {mapray.Vector3} color テキストの色 */ setBackgroundColor( color ) { this._setColorProperty( "bg_color", color ); } /** * @summary テキスト背景を有効にするかどうか * @param {boolean} enable trueなら有効 */ setEnableBackground( enable ) { this._setValueProperty( "enable_bg", enable ); this._primitive_producer = new PrimitiveProducer( this ); } /** * @summary テキストを追加 * @param {string} text テキスト * @param {mapray.GeoPoint} position 位置 * @param {object} [props] プロパティ * @param {string} [props.font_style] フォントスタイル ("normal" | "italic" | "oblique") * @param {string} [props.font_weight] フォントの太さ ("normal" | "bold") * @param {number} [props.font_size] フォントの大きさ (Pixels) * @param {string} [props.font_family] フォントファミリー * @param {mapray.Color} [props.color] テキストの色 * @param {mapray.Color} [props.stroke_color] テキスト縁の色 * @param {number} [props.stroke_width] テキスト縁の幅 * @param {mapray.Color} [props.bg_color] テキスト背景色 * @param {boolean} [props.enable_stroke] テキストの縁取りを有効にするか * @param {string} [props.id] Entryを識別するID * @return {mapray.TextEntity.Entry} 追加したEntry */ addText( text, position, props ) { var entry = new Entry( this, text, position, props ); this._entries.push( entry ); this._primitive_producer = new PrimitiveProducer( this ); this._primitive_producer.onAddTextEntry(); return entry; } /** * @summary 専用マテリアルを取得 * @private */ _getTextMaterial( render_target ) { var scene = this.scene; if ( render_target === RenderTarget.SCENE ) { if ( !scene._TextEntity_text_material ) { // scene にマテリアルをキャッシュ scene._TextEntity_text_material = new TextMaterial( scene.glenv ); } return scene._TextEntity_text_material; } else if (render_target === RenderTarget.RID) { if ( !scene._TextEntity_text_material_pick ) { // scene にマテリアルをキャッシュ scene._TextEntity_text_material_pick = new TextMaterial( scene.glenv, { ridMaterial: true } ); } return scene._TextEntity_text_material_pick; } } /** * @summary テキストだけを描画する専用マテリアルを取得 * @private */ _getSimpleTextMaterial( render_target ) { var scene = this.scene; if ( render_target === RenderTarget.SCENE ) { if ( !scene._SimpleTextEntity_text_material ) { // scene にマテリアルをキャッシュ scene._SimpleTextEntity_text_material = new SimpleTextMaterial( scene.glenv ); } return scene._SimpleTextEntity_text_material; } else if (render_target === RenderTarget.RID) { if ( !scene._SimpleTextEntity_text_material_pick ) { // scene にマテリアルをキャッシュ scene._SimpleTextEntity_text_material_pick = new SimpleTextMaterial( scene.glenv, { ridMaterial: true } ); } return scene._SimpleTextEntity_text_material_pick; } } /** * @private */ _setValueProperty( name, value ) { var props = this._text_parent_props; if ( props[name] != value ) { props[name] = value; if ( this._primitive_producer ) { this._primitive_producer.onChangeParentProperty(); } } } /** * @private */ _setColorProperty( name, value ) { var dst = this._text_parent_props[name]; if ( dst.r != value[0] || dst.g != value[1] || dst.b != value[2] ) { Color.setOpacityColor( value, dst ); if ( this._primitive_producer ) { this._primitive_producer.onChangeParentProperty(); } } } /** * @private */ _setupByJson( json ) { var position = new GeoPoint(); for ( let entry of json.entries ) { position.setFromArray( entry.position ); this.addText( entry.text, position, entry ); } if ( json.font_style !== undefined ) this.setFontStyle( json.font_style ); if ( json.font_weight !== undefined ) this.setFontWeight( json.font_weight ); if ( json.font_size !== undefined ) this.setFontSize( json.font_size ); if ( json.font_family !== undefined ) this.setFontFamily( json.font_family ); if ( json.color !== undefined ) this.setColor( json.color ); if ( json.stroke_color !== undefined ) this.setStrokeColor ( json.stroke_color ); if ( json.stroke_width !== undefined ) this.setStrokeLineWidth ( json.stroke_width ); if ( json.enable_stroke !== undefined ) this.setEnableStroke ( json.enable_stroke ); if ( json.bg_color !== undefined ) this.setBackgroundColor( json.bg_color ); if ( json.enable_bg !== undefined ) this.setEnableBackground ( json.enable_bg ); } /** * @private */ _enableStroke( ) { return this._text_parent_props["enable_stroke"]; } /** * @summary IDでEntryを取得 * @param {string} id ID * @return {mapray.TextEntity.Entry} IDが一致するEntry(無ければundefined) */ getEntry( id ) { return this._entries.find((entry) => entry.id === id); } } // クラス定数の定義 { TextEntity.DEFAULT_FONT_SIZE = 16; TextEntity.DEFAULT_FONT_FAMILY = "sans-serif"; TextEntity.DEFAULT_COLOR = [1, 1, 1]; TextEntity.DEFAULT_STROKE_COLOR = [0.0, 0.0, 0.0]; TextEntity.DEFAULT_STROKE_WIDTH = 0.48; TextEntity.DEFAULT_BG_COLOR = [0.3, 0.3, 0.3]; TextEntity.DEFAULT_TEXT_UPPER = 1.1; TextEntity.DEFAULT_TEXT_LOWER = 0.38; TextEntity.SAFETY_PIXEL_MARGIN = 1; TextEntity.MAX_IMAGE_WIDTH = 4096; } /** * @summary TextEntity の PrimitiveProducer * * TODO: relative で標高の変化のたびにテクスチャを生成する必要はないので * Layout でのテクスチャの生成とメッシュの生成を分離する * * @private */ class PrimitiveProducer extends Entity.PrimitiveProducer { /** * @param {mapray.TextEntity} entity */ constructor( entity ) { super( entity ); this._glenv = entity.scene.glenv; this._dirty = true; // プリミティブの要素 this._transform = GeoMath.setIdentity( GeoMath.createMatrix() ); this._properties = { enable_bg: false, image: null // テキスト画像 }; // プリミティブ var material = null, pickMaterial = null; if ( this._isSimpleText() ) { material = entity._getSimpleTextMaterial( RenderTarget.SCENE ); pickMaterial = entity._getSimpleTextMaterial( RenderTarget.RID ); } else { material = entity._getTextMaterial( RenderTarget.SCENE ); pickMaterial = entity._getTextMaterial( RenderTarget.RID ); } var primitive = new Primitive( this._glenv, null, material, this._transform ); primitive.properties = this._properties; this._primitive = primitive; var pickPrimitive = new Primitive( this._glenv, null, pickMaterial, 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 テキストが追加されたことを通知 */ onAddTextEntry() { // 変化した可能性がある 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; } this._updateProperties(); 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._dirty = false; return this._primitives; } // テクスチャ設定 var properties = this._properties; if ( properties.image ) { properties.image.dispose(); } properties.image = layout.texture; // メッシュ生成 var vtype = []; if ( this._isSimpleText() ) { vtype = [ { name: "a_position", size: 3 }, { name: "a_offset", size: 2 }, { name: "a_texcoord", size: 2 }, { name: "a_color", size: 4 } ]; } else { vtype = [ { name: "a_position", size: 3 }, { name: "a_offset", size: 2 }, { name: "a_texcoord", size: 2 }, ]; } var mesh_data = { vtype, 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 * <pre> * 入力: * this.entity * 出力: * this._properties * </pre> * * @private */ _updateProperties() { let entity = this.entity; let props = this._properties; props.enable_bg = entity._text_parent_props.enable_bg; } /** * @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 シンプルテキストモードかどうかを確認 * * * @return {boolean} シンプルテキストモードならtrue. * @private */ _isSimpleText() { let entity = this.entity; let enable = true; // check enable bg color or stroke; if ( entity._text_parent_props.enable_bg || entity._text_parent_props.enable_stroke ) { enable = false; } // check enable stroke let i = 0; while ( enable && entity._entries.length > i ) { let entry = entity._entries[i]; enable = !entry.enable_stroke; i++; } return enable; } } /** * @summary テキスト要素 * @hideconstructor * @memberof mapray.TextEntity * @public */ class Entry { /** * @param {mapray.TextEntity} owner 所有者 * @param {string} text テキスト * @param {mapray.GeoPoint} position 位置 * @param {object} [props] プロパティ * @param {string} [props.font_style] フォントスタイル ("normal" | "italic" | "oblique") * @param {string} [props.font_weight] フォントの太さ ("normal" | "bold") * @param {number} [props.font_size] フォントの大きさ (Pixels) * @param {string} [props.font_family] フォントファミリー * @param {mapray.Color} [props.color] テキストの色 * @param {mapray.Color} [props.stroke_color] テキスト縁の色 * @param {number} [props.stroke_width] テキスト縁の幅 * @param {number} [props.enable_stroke] テキストの縁取りを有効にするか * @param {string} [props.id] Entryを識別するID */ constructor( owner, text, position, props ) { this._owner = owner; this._text = text; this._position = position.clone(); // animation.BindingBlock this._animation = new EasyBindingBlock(); this._setupAnimationBindingBlock(); this._props = Object.assign( {}, props ); // props の複製 this._copyColorProperty( "color" ); // deep copy this._copyColorProperty( "stroke_color" ); // deep copy this._copyColorProperty( "bg_color" ); // deep copy } /** * @summary テキスト * @type {string} * @readonly * @package */ get text() { return this._text; } /** * @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 {number} * @readonly * @package */ get size() { var props = this._props; var parent = this._owner._text_parent_props; return props.font_size || parent.font_size; } /** * @summary テキストの色 * @type {mapray.Vector3} * @readonly * @package */ get color() { var props = this._props; var parent = this._owner._text_parent_props; return props.color || parent.color; } /** * @summary フォント * @type {string} * @readonly * @package * @see https://developer.mozilla.org/ja/docs/Web/CSS/font */ get font() { var props = this._props; var parent = this._owner._text_parent_props; var style = props.font_style || parent.font_style; var variant = "normal"; var weight = props.font_weight || parent.font_weight; var family = props.font_family || parent.font_family; return style + " " + variant + " " + weight + " " + this.size + "px " + family; } /** * @summary テキスト縁の色 * @type {mapray.Color} * @readonly * @package */ get stroke_color() { var props = this._props; var parent = this._owner._text_parent_props; return props.stroke_color || parent.stroke_color; } /** * @summary 縁の幅 (Pixels) * @type {number} * @readonly * @package */ get stroke_width() { var props = this._props; var parent = this._owner._text_parent_props; return props.stroke_width || parent.stroke_width; } /** * @summary 縁を描画するか * @type {boolean} * @readonly * @package */ get enable_stroke() { var props = this._props; var parent = this._owner._text_parent_props; return props.enable_stroke || parent.enable_stroke; } /** * @summary 背景色 * @type {mapray.Color} * @readonly * @package */ get bg_color() { var props = this._props; var parent = this._owner._text_parent_props; return props.bg_color || parent.bg_color; } /** * @summary 背景描画するか * @type {boolean} * @readonly * @package */ get enable_background() { // Enable or Disable background can be set by parent. var parent = this._owner._text_parent_props; return parent.enable_bg; } /** * @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 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 ); } ); // パラメータ名: font_style // パラメータ型: string // フォントスタイル block.addEntry( "font_style", [string], null, value => { this.setFontStyle( value ); } ); // パラメータ名: font_weight // パラメータ型: string // フォントの太さ block.addEntry( "font_weight", [string], null, value => { this.setFontWeight( value ); } ); // パラメータ名: font_size // パラメータ型: number // フォントの大きさ block.addEntry( "font_size", [number], null, value => { this.setFontSize( value ); } ); // パラメータ名: color // パラメータ型: vector3 // テキストの色 block.addEntry( "color", [vector3], null, value => { this.setColor( value ); } ); // パラメータ名: stroke_color // パラメータ型: vector3 // 縁の色 block.addEntry( "stroke_color", [vector3], null, value => { this.setStrokeColor( value ); } ); // パラメータ名: stroke_width // パラメータ型: number // 縁の線幅 block.addEntry( "stroke_width", [number], null, value => { this.setStrokeLineWidth( value ); } ); // パラメータ名: text // パラメータ型: string // テキスト block.addEntry( "text", [string], null, value => { this.setText( value ); } ); } /** * @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 {string} style フォントスタイル ("normal" | "italic" | "oblique") */ setFontStyle( style ) { this._setValueProperty( "font_style", style ); } /** * @summary フォントの太さを設定 * @param {string} weight フォントの太さ ("normal" | "bold") */ setFontWeight( weight ) { this._setValueProperty( "font_weight", weight ); } /** * @summary フォントの大きさを設定 * @param {number} size フォントの大きさ (Pixels) */ setFontSize( size ) { this._setValueProperty( "font_size", size ); } /** * @summary テキストの色を設定 * @param {mapray.Vector3} color テキストの色 */ setColor( color ) { this._setColorProperty( "color", color ); } /** * @summary テキスト縁の色を設定 * @param {mapray.Vector3} color 縁の色 */ setStrokeColor( color ) { this._setColorProperty( "stroke_color", color ); } /** * @summary テキスト縁の太さを設定 * @param {mapray.number} width 縁の線幅 */ setStrokeLineWidth( width ) { this._setValueProperty( "stroke_width", width ); } /** * @summary テキスト縁を有効にするかどうか * @param {boolean} enable trueなら有効 */ setEnableStroke( enable ) { this._setValueProperty( "enable_stroke", enable ); this._owner._primitive_producer = new PrimitiveProducer( this._owner ); } /** * @summary テキストを設定 * @param {string} text テキスト */ setText( text ) { if ( this._text !== text ) { this._text = text; this._owner.getPrimitiveProducer().onChangeChildProperty(); } } /** * @private */ _copyColorProperty( name ) { var props = this._props; if ( props.hasOwnProperty( name ) ) { props[name] = Color.generateOpacityColor( props[name] ); } } /** * @private */ _setValueProperty( name, value ) { var props = this._props; if ( props[name] != value ) { props[name] = value; this._owner.getPrimitiveProducer().onChangeChildProperty(); } } /** * @private */ _setColorProperty( name, value ) { var dst = this._props[name]; if ( dst ) { if ( dst.r != value[0] || dst.g != value[1] || dst.b != value[2] ) { Color.setOpacityColor( value, dst ); this._owner.getPrimitiveProducer().onChangeChildProperty(); } } else { this._props[name] = Color.generateOpacityColor( value ); this._owner.getPrimitiveProducer().onChangeChildProperty(); } } } TextEntity.Entry = Entry; /** * @summary テキスト画像を Canvas 上にレイアウト * * @memberof mapray.TextEntity * @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 ); if ( this._isSimpleTextWithAllItems( this._items ) ) { this._texture = this._createTextureForSimple( size.width, size.height ); this._vertices = this._createVerticesForSimple( size.width, size.height, gocs_array ); } else { 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.TextEntity.LItem>} * @private */ _createItemList() { var context = Dom.createCanvasContext( 1, 1 ); var items = []; for ( let entry of this._owner.entity._entries ) { items.push( new LItem( this, entry, context ) ); } return items; } /** * @summary RowLayout のリストを生成 * @return {array.<mapray.TextEntity.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 ); context.textAlign = "left"; context.textBaseline = "alphabetic"; context.fillStyle = "rgba( 255, 255, 255, 1.0 )"; var items = this._items; for ( var i = 0; i < items.length; ++i ) { var item = items[i]; if ( item.is_canceled ) continue; if ( item._entry.enable_background ) { item.drawRect( context ); } if ( item._entry.enable_stroke ) { item.drawStrokeText( context ); } item.drawText( context ); } var glenv = this._owner._glenv; var opts = { usage: Texture.Usage.TEXT }; 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]; var items = this._items; for ( var i = 0; i < items.length; ++i ) { var item = items[i]; if ( item.is_canceled ) continue; // テキストの位置 (モデル座標系) var ibase = 3 * i; var xm = gocs_array[ibase] - xo; var ym = gocs_array[ibase + 1] - yo; var zm = gocs_array[ibase + 2] - zo; // ベースライン左端 (キャンバス座標系) var xc = item.pos_x; var yc = item.pos_y; var upper = item.upper; var lower = item.lower; var xsize = item.width; var xn = 1 / width; var yn = 1 / height; // 左下 vertices.push( xm, ym, zm ); // a_position vertices.push( -xsize / 2, -lower ); // a_offset vertices.push( xc * xn, 1 - (yc + lower) * yn ); // a_texcoord // 右下 vertices.push( xm, ym, zm ); // a_position vertices.push( xsize / 2, -lower ); // a_offset vertices.push( (xc + xsize) * xn, 1 - (yc + lower) * yn ); // a_texcoord // 左上 vertices.push( xm, ym, zm ); // a_position vertices.push( -xsize / 2, upper ); // a_offset vertices.push( xc * xn, 1 - (yc - upper) * yn ); // a_texcoord // 右上 vertices.push( xm, ym, zm ); // a_position vertices.push( xsize / 2, upper ); // a_offset vertices.push( (xc + xsize) * xn, 1 - (yc - upper) * yn ); // a_texcoord } return vertices; } /** * @summary 単純テキスト用テクスチャを生成 * @param {number} width 横幅 * @param {number} height 高さ * @return {mapray.Texture} テキストテクスチャ * @private */ _createTextureForSimple( width, height ) { var context = Dom.createCanvasContext( width, height ); context.textAlign = "left"; context.textBaseline = "alphabetic"; context.fillStyle = "rgba( 255, 255, 255, 1.0 )"; var items = this._items; for ( var i = 0; i < items.length; ++i ) { var item = items[i]; if ( item.is_canceled ) continue; item.drawText( context ); } var glenv = this._owner._glenv; var opts = { usage: Texture.Usage.SIMPLETEXT }; 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 */ _createVerticesForSimple( width, height, gocs_array ) { var vertices = []; // テキスト集合の原点 (GOCS) var transform = this._owner._transform; var xo = transform[12]; var yo = transform[13]; var zo = transform[14]; var items = this._items; for ( var i = 0; i < items.length; ++i ) { var item = items[i]; if ( item.is_canceled ) continue; var entry = item.entry; // テキストの色 var color = entry.color.toVector4(); // テキストの位置 (モデル座標系) var ibase = 3 * i; var xm = gocs_array[ibase] - xo; var ym = gocs_array[ibase + 1] - yo; var zm = gocs_array[ibase + 2] - zo; // ベースライン左端 (キャンバス座標系) var xc = item.pos_x; var yc = item.pos_y; var upper = item.upper; var lower = item.lower; var xsize = item.width; var xn = 1 / width; var yn = 1 / height; // 左下 vertices.push( xm, ym, zm ); // a_position vertices.push( -xsize / 2, -lower ); // a_offset vertices.push( xc * xn, 1 - (yc + lower) * yn ); // a_texcoord vertices.push( color[0], color[1], color[2], 1 ); // a_color // 右下 vertices.push( xm, ym, zm ); // a_position vertices.push( xsize / 2, -lower ); // a_offset vertices.push( (xc + xsize) * xn, 1 - (yc + lower) * yn ); // a_texcoord vertices.push( color[0], color[1], color[2], 1 ); // a_color // 左上 vertices.push( xm, ym, zm ); // a_position vertices.push( -xsize / 2, upper ); // a_offset vertices.push( xc * xn, 1 - (yc - upper) * yn ); // a_texcoord vertices.push( color[0], color[1], color[2], 1 ); // a_color // 右上 vertices.push( xm, ym, zm ); // a_position vertices.push( xsize / 2, upper ); // a_offset vertices.push( (xc + xsize) * xn, 1 - (yc - upper) * yn ); // a_texcoord vertices.push( color[0], color[1], color[2], 1 ); // a_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; var b = 4 * i; indices.push( b, b + 1, b + 2, b + 2, b + 1 , b + 3 ); } return indices; } /** * @summary アイテムの配置を設定 * @param {array.<mapray.TextEntity.RowLayout>} row_layouts * @return {object} キャンバスサイズ * @private */ _setupLocation( row_layouts ) { var width = 0; var height = 0; height += TextEntity.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 + TextEntity.SAFETY_PIXEL_MARGIN; } return { width: width, height: height }; } /** * @summary シンプルテキストモードかどうか * @param {mapray.TextEntity.LItem} item * @return {boolean} シンプルテキストモードならtrue * @private */ _isSimpleText( item ) { if ( item._entry.enable_background || item._entry.enable_stroke ) { return false; } return true; } /** * @summary シンプルテキストモードかどうか * @param {array.<mapray.TextEntity.LItem>} items * @return {boolean} シンプルテキストモードならtrue * @private */ _isSimpleTextWithAllItems( items ) { let enable = true; let i = 0; while( enable && items.length > i) { let item = items[i]; enable = this._isSimpleText( item ); i++; } return enable; } } /** * @summary レイアウト対象 * @memberof mapray.TextEntity * @private */ class LItem { /** * @param {mapray.TextEntity.Layout} layout 所有者 * @param {mapray.TextEntity.Entry} entry TextEntity エントリ * @param {CanvasRenderingContext2D} context 測定用コンテキスト */ constructor( layout, entry, context ) { this._entry = entry; // テキストの基点 this._pos_x = 0; // 左端 this._pos_y = 0; // ベースライン位置 // テキストの横幅 context.font = entry.font; this._width = context.measureText( entry.text ).width; // テキストの上下範囲 this._upper = entry.size * TextEntity.DEFAULT_TEXT_UPPER; this._lower = entry.size * TextEntity.DEFAULT_TEXT_LOWER; this._is_canceled = false; } /** * @type {mapray.TextEntity.Entry} * @readonly */ get entry() { return this._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; } /** * @type {number} * @readonly */ get upper() { return this._upper; } /** * @type {number} * @readonly */ get lower() { return this._lower; } /** * キャンバス上でのテキストの横画素数 * @type {number} * @readonly */ get width_pixel() { return Math.ceil( this._width ); } /** * キャンバス上でのテキストの縦画素数 * @type {number} * @readonly */ get height_pixel() { return Math.ceil( this._upper ) + Math.ceil( this._lower ); } /** * 取り消し状態か? * @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 + Math.ceil( this._upper ); } /** * @summary テキストだけを描画 (stokeやfillRectとは組み合わせ不可) * @desc * <p>context は以下のように設定していること。</p> * <pre> * context.textAlign = "left"; * context.textBaseline = "alphabetic"; * context.fillStyle = "rgba( 255, 255, 255, 1.0 )"; * </pre> * @param {CanvasRenderingContext2D} context 描画先コンテキスト */ drawTextOnly( context ) { var entry = this._entry; context.font = entry.font; context.fillText( entry.text, this._pos_x, this._pos_y ); } /** * @summary テキストを描画 * @desc * <p>context は以下のように設定していること。</p> * <pre> * context.textAlign = "left"; * context.textBaseline = "alphabetic"; * context.fillStyle = "rgba( 255, 255, 255, 1.0 )"; * </pre> * @param {CanvasRenderingContext2D} context 描画先コンテキスト */ drawText( context ) { var entry = this._entry; context.font = entry.font; context.fillStyle = entry.color.toRGBString(); context.fillText( entry.text, this._pos_x, this._pos_y ); } /** * @summary テキストの淵を描画 * @desc * <p>drawTextOnlyとは組み合わせ不可</p> * @param {CanvasRenderingContext2D} context 描画先コンテキスト */ drawStrokeText( context ) { /* context.fillText() .------------. |',',',',',',| |',',',',',',| |',',',',',',| |',',',',',',| |',',',',',',| context.strokeText() .--------------------. |',',',',',',',',',',| |','.------------.,',| |','|',',',',',',|,',| |','|','.----.,',|,',| |','|','| |,',|,',| |','|','| |,',|,',| |<--|-->| a b b will be overwrite by fillText(); */ var entry = this._entry; context.font = entry.font; context.strokeStyle = entry.stroke_color.toRGBString(); context.lineWidth = entry.stroke_width * 2; context.lineJoin = "round"; context.strokeText( entry.text, this._pos_x, this._pos_y ); } /** * @summary テキストの背景を描画 * @desc * <p>drawTextOnlyとは組み合わせ不可</p> * @param {CanvasRenderingContext2D} context 描画先コンテキスト */ drawRect( context ) { var entry = this._entry; context.fillStyle = entry.bg_color.toRGBString(); context.fillRect( this._pos_x - TextEntity.SAFETY_PIXEL_MARGIN, this._pos_y - this._upper - TextEntity.SAFETY_PIXEL_MARGIN, this.width_pixel + TextEntity.SAFETY_PIXEL_MARGIN, this.height_pixel + TextEntity.SAFETY_PIXEL_MARGIN ); } } /** * @summary 水平レイアウト * @memberof mapray.TextEntity * @private */ class RowLayout { /** * @desc * <p>レイアウトされた、またはレイアウトに失敗したアイテムは src_items から削除される。</p> * <p>レイアウトに失敗したアイテムは取り消し (is_canceled) になる。</p> * @param {array.<mapray.TextEntity.LItem>} src_items アイテムリスト */ constructor( src_items ) { var width_assumed_total = 0; var height_pixel_max = 0; var row_items = []; width_assumed_total += TextEntity.SAFETY_PIXEL_MARGIN; // 左マージン while ( src_items.length > 0 ) { var item = src_items.shift(); var width_assumed = item.width_pixel + TextEntity.SAFETY_PIXEL_MARGIN; // テキスト幅 + 右マージン if ( width_assumed_total + width_assumed <= TextEntity.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.TextEntity.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 += TextEntity.SAFETY_PIXEL_MARGIN; // 左マージン for ( var i = 0; i < items.length; ++i ) { var item = items[i]; item.locate( x, y ); x += item.width_pixel + TextEntity.SAFETY_PIXEL_MARGIN; // テキスト幅 + 右マージン } } } export default TextEntity;