Heatbox.js - hiro-nyon/cesium-heatbox GitHub Wiki

Source: Heatbox.js

日本語 | English

English

/**
 * CesiumJS Heatbox - Main orchestration class.
 * CesiumJS Heatbox - メインオーケストレーションクラス
 */
import * as Cesium from 'cesium';
import { DEFAULT_OPTIONS, ERROR_MESSAGES, PERFORMANCE_LIMITS } from './utils/constants.js';
import {
  isValidViewer,
  isValidEntities,
  validateAndNormalizeOptions,
  validateVoxelCount,
  estimateInitialVoxelSize,
  calculateDataRange
} from './utils/validation.js';
import { applyAutoRenderBudget } from './utils/deviceTierDetector.js';
import { Logger } from './utils/logger.js';
import { CoordinateTransformer } from './core/CoordinateTransformer.js';
import { VoxelGrid } from './core/VoxelGrid.js';
import { DataProcessor } from './core/DataProcessor.js';
import { VoxelRenderer } from './core/VoxelRenderer.js';
import { getProfileNames, getProfile, applyProfile } from './utils/profiles.js';
import { PerformanceOverlay } from './utils/performanceOverlay.js';
import { Legend } from './ui/Legend.js';
import { TimeController } from './core/temporal/TimeController.js';

/**
 * @typedef {('mobile-fast'|'desktop-balanced'|'dense-data'|'sparse-data')} ProfileName
 * @since 0.1.12
 */

/**
 * @typedef {('standard'|'inset'|'emulation-only')} OutlineRenderMode
 * @since 0.1.12
 */

/**
 * @typedef {('off'|'topn'|'non-topn'|'all')} EmulationScope
 * @since 0.1.12
 */

/**
 * @typedef {('thin'|'medium'|'thick'|'adaptive')} OutlineWidthPreset
 * @since 0.1.12
 */

/**
 * @typedef {Object} PerformanceOverlayConfig
 * @property {boolean} [enabled=false] - Enable performance overlay / パフォーマンスオーバーレイを有効化
 * @property {('top-left'|'top-right'|'bottom-left'|'bottom-right')} [position='top-right'] - Overlay position / 配置位置
 * @property {boolean} [autoShow=false] - Show overlay automatically / 自動表示
 * @property {boolean} [autoUpdate=true] - Auto refresh overlay after render / 描画後に自動更新するか
 * @property {number} [updateIntervalMs=500] - Update interval in milliseconds / 更新間隔(ミリ秒)
 * @property {number} [fpsAveragingWindowMs=1000] - FPS 平滑化窓(ミリ秒)/ Averaging window for FPS
 * @since 0.1.12
 */

/**
 * @typedef {Object} HeatboxHighlightStyle
 * @property {number} [outlineWidth=4] - Outline width applied to highlighted voxels / ハイライト対象ボクセルに適用する枠線太さ
 * @property {number} [boostOpacity=0.2] - Extra opacity applied to highlighted voxels / ハイライト時に加算する不透明度
 * @property {number} [boostOutlineWidth] - Optional outline width override / 枠線太さの上書き指定
 */

/**
 * @typedef {Object} HeatboxAdaptiveParams
 * @property {number} [neighborhoodRadius=30] - Neighbor radius in meters used for density sampling / 密度サンプリングに用いる近傍半径(メートル)
 * @property {number} [densityThreshold=3] - Density threshold (entities per voxel) / 密度しきい値(エンティティ/ボクセル)
 * @property {number} [cameraDistanceFactor=0.8] - Camera distance compensation factor / カメラ距離補正係数
 * @property {number} [overlapRiskFactor=0.4] - Overlap risk factor used for diagnostics / 重なりリスク係数
 * @property {(Array.<number>|null)} [outlineWidthRange=null] - `[min,max]` outline width clamp / 枠線太さの許容範囲 `[最小, 最大]`
 * @property {(Array.<number>|null)} [boxOpacityRange=null] - `[min,max]` box opacity clamp / ボックス不透明度の許容範囲
 * @property {(Array.<number>|null)} [outlineOpacityRange=null] - `[min,max]` outline opacity clamp / 枠線不透明度の許容範囲
 * @property {boolean} [adaptiveOpacityEnabled=false] - Reserved flag for adaptive opacity / 適応透明度(プレースホルダー)
 * @property {boolean} [zScaleCompensation=true] - Enable Z scale compensation / Z軸スケール補正の有効化
 * @property {boolean} [overlapDetection=false] - Enable overlap diagnostics / 重なり検出を有効化
 */

/**
 * @typedef {Object} HeatboxFitViewOptions
 * @property {number} [paddingPercent=0.1] - Padding ratio around bounds / 境界に対するパディング割合
 * @property {number} [pitchDegrees=-30] - Camera pitch angle in degrees / カメラ俯角(度)
 * @property {number} [headingDegrees=0] - Camera heading in degrees / カメラ方位(度)
 * @property {('auto'|'manual')} [altitudeStrategy='auto'] - Altitude strategy for camera / カメラ高度の計算方法
 */

/**
 * @typedef {Object} TemporalDataEntry
 * @property {Cesium.JulianDate|string|Date|number} start - Interval start time / インターバル開始時刻
 * @property {Cesium.JulianDate|string|Date|number} stop - Interval end time / インターバル終了時刻
 * @property {Array<Cesium.Entity|Object>} data - Entities rendered during the interval / その期間に描画するエンティティ配列
 */

/**
 * @typedef {Object} TemporalOptions
 * @property {boolean} [enabled=false] - Enable temporal mode / 時間依存モードを有効化
 * @property {TemporalDataEntry[]} [data=[]] - Ordered temporal slices / ソート済みの時系列スライス
 * @property {('global'|'per-time')} [classificationScope='global'] - Classification scope / 分類スコープ
 * @property {('frame'|number)} [updateInterval=100] - Update interval (`frame` or milliseconds) / 更新間隔
 * @property {('clear'|'hold')} [outOfRangeBehavior='hold'] - Behaviour when clock is outside data range / データ範囲外時の挙動
 * @property {('skip'|'prefer-earlier'|'prefer-later')} [overlapResolution='prefer-earlier'] - Overlap resolution strategy / 重複時の解決方法
 * @property {boolean} [interpolate=false] - Reserved flag for interpolation (future) / 将来の補間フラグ(現状は未使用)
 */

/**
 * @typedef {Object} HeatboxBounds
 * @property {number} minLon - Minimum longitude / 最小経度
 * @property {number} maxLon - Maximum longitude / 最大経度
 * @property {number} minLat - Minimum latitude / 最小緯度
 * @property {number} maxLat - Maximum latitude / 最大緯度
 * @property {number} minAlt - Minimum altitude / 最小高度
 * @property {number} maxAlt - Maximum altitude / 最大高度
 * @property {number} [centerLon] - Center longitude / 中心経度
 * @property {number} [centerLat] - Center latitude / 中心緯度
 * @property {number} [centerAlt] - Center altitude / 中心高度
 */

/**
 * @typedef {Object} HeatboxGridInfo
 * @property {number} numVoxelsX - Number of voxels along X axis / X軸方向のボクセル数
 * @property {number} numVoxelsY - Number of voxels along Y axis / Y軸方向のボクセル数
 * @property {number} numVoxelsZ - Number of voxels along Z axis / Z軸方向のボクセル数
 * @property {number} voxelSizeMeters - Voxel size in meters / ボクセルサイズ(メートル)
 * @property {number} totalVoxels - Total voxel count / 総ボクセル数
 */

/**
 * @typedef {Object} HeatboxLayerStat
 * @property {string} key - Layer key / レイヤキー
 * @property {number} total - Total entity count for this layer / このレイヤの総エンティティ数
 */

/**
 * @typedef {Object} SpatialIdEdgeCaseMetrics
 * @property {number} datelineNeighborsChecked - Number of neighbor checks near dateline / 日付変更線近傍で検証した近傍セル数
 * @property {number} datelineNeighborsMismatched - Number of neighbor mismatches near dateline / 日付変更線近傍で不一致となった近傍セル数
 * @property {number} polarTilesChecked - Number of polar tiles evaluated / 極域タイルの検証数
 * @property {number} polarMaxRelativeErrorXY - Maximum relative XY error near poles / 極域近傍でのXY相対誤差の最大値
 * @property {number} hemisphereBoundsChecked - Number of hemisphere-crossing bounds evaluated / 半球跨ぎboundsの検証数
 * @property {number} hemisphereBoundsMismatched - Number of hemisphere-crossing mismatches / 半球跨ぎで不一致となったケース数
 */

/**
 * @typedef {Object} HeatboxStatistics
 * @property {number} totalVoxels - Total voxels generated / 生成された総ボクセル数
 * @property {number} renderedVoxels - Voxels actually rendered / 実際に描画されたボクセル数
 * @property {number} nonEmptyVoxels - Non-empty voxels / データを含むボクセル数
 * @property {number} emptyVoxels - Empty voxels / 空ボクセル数
 * @property {number} totalEntities - Entities processed / 処理したエンティティ数
 * @property {number} minCount - Minimum entity count per voxel / 1ボクセルあたり最小エンティティ数
 * @property {number} maxCount - Maximum entity count per voxel / 1ボクセルあたり最大エンティティ数
 * @property {number} averageCount - Average entity count per voxel / 平均エンティティ数
 * @property {boolean} [autoAdjusted] - Whether auto adjustments occurred / 自動調整が行われたか
 * @property {number|null} [originalVoxelSize] - Original voxel size before adjustment / 調整前のボクセルサイズ
 * @property {number|null} [finalVoxelSize] - Final voxel size after adjustment / 調整後のボクセルサイズ
 * @property {string|null} [adjustmentReason] - Reason for auto adjustment / 自動調整の理由
 * @property {number} [renderTimeMs] - Render time in milliseconds / 描画時間(ミリ秒)
 * @property {string} [selectionStrategy] - Selection strategy used / 適用された選択戦略
 * @property {number} [clippedNonEmpty] - Non-empty voxels clipped by limits / 制限により除外された非空ボクセル数
 * @property {number} [coverageRatio] - Coverage ratio when hybrid strategy used / ハイブリッド戦略時のカバレッジ比率
 * @property {string} [renderBudgetTier] - Auto render budget tier label / 自動レンダーバジェットの区分
 * @property {number} [autoMaxRenderVoxels] - Auto-assigned maxRenderVoxels / 自動設定された maxRenderVoxels
 * @property {number|null} [occupancyRatio] - Ratio of rendered voxels to limit / 描画ボクセルと上限の比率
 * @property {HeatboxLayerStat[]} [layers] - Top-N layer aggregation (v0.1.18 ADR-0014) / 上位N個のレイヤ集約
 * @property {Object} [spatialId] - Spatial ID statistics (v0.1.19 ADR-0015) / 空間ID関連の統計情報
 * @property {boolean} [spatialId.enabled] - Whether Spatial ID mode is enabled / 空間IDモードが有効か
 * @property {string|null} [spatialId.provider] - Spatial ID provider identifier ('ouranos-gex' or null) / 空間IDプロバイダー識別子
 * @property {number|null} [spatialId.zoom] - Resolved zoom level / 解決済みズームレベル
 * @property {('auto'|'manual'|null)} [spatialId.zoomControl] - Zoom control mode / ズーム制御モード
 * @property {SpatialIdEdgeCaseMetrics|null} [spatialId.edgeCaseMetrics] - QA metrics for global edge cases / グローバル端ケースQA用メトリクス
 */

/**
 * @typedef {Object} HeatboxAutoVoxelSizeInfo
 * @property {boolean} enabled - Whether auto voxel sizing ran / 自動ボクセル調整が実行されたか
 * @property {boolean} adjusted - Whether voxel size was adjusted / サイズが調整されたか
 * @property {string|null} reason - Adjustment reason / 調整理由
 * @property {number|null} originalSize - Initial estimate / 初期推定値
 * @property {number|null} finalSize - Final size / 最終サイズ
 * @property {Object|null} [dataRange] - Estimated data range / 推定データ範囲
 * @property {number|null} [estimatedDensity] - Estimated density / 推定密度
 */

/**
 * @typedef {Object} HeatboxDebugInfo
 * @property {HeatboxOptions} options - Effective options / 有効なオプション
 * @property {HeatboxBounds|null} bounds - Current bounds / 現在の境界
 * @property {HeatboxGridInfo|null} grid - Grid information / グリッド情報
 * @property {HeatboxStatistics|null} statistics - Statistics snapshot / 統計情報
 * @property {HeatboxAutoVoxelSizeInfo|null} [autoVoxelSizeInfo] - Auto voxel sizing details / 自動ボクセル調整の詳細
 */

/**
 * @typedef {Object} HeatboxResolverVoxelInfo
 * @property {number} x - Voxel grid index (X) / ボクセルのXインデックス
 * @property {number} y - Voxel grid index (Y) / ボクセルのYインデックス
 * @property {number} z - Voxel grid index (Z) / ボクセルのZインデックス
 * @property {number} count - Number of entities within the voxel / ボクセル内のエンティティ数
 */

/**
 * @typedef {Object} HeatboxOutlineWidthResolverParams
 * @property {HeatboxResolverVoxelInfo} voxel - Voxel information / ボクセル情報
 * @property {boolean} isTopN - Whether voxel is part of highlighted TopN / TopN対象か
 * @property {number} normalizedDensity - Density normalised to 0-1 / 正規化密度(0〜1)
 * @property {HeatboxStatistics} statistics - Latest statistics snapshot / 最新統計情報
 * @property {HeatboxAdaptiveParams|null} [adaptiveParams] - Adaptive parameters / 適応パラメータ
 */

/**
 * @typedef {Object} HeatboxOpacityResolverContext
 * @property {HeatboxResolverVoxelInfo} voxel - Voxel information / ボクセル情報
 * @property {boolean} isTopN - Whether voxel is part of highlighted TopN / TopN対象か
 * @property {number} normalizedDensity - Density normalised to 0-1 / 正規化密度(0〜1)
 * @property {HeatboxStatistics} statistics - Latest statistics snapshot / 最新統計情報
 * @property {HeatboxAdaptiveParams|null} [adaptiveParams] - Adaptive parameters / 適応パラメータ
 */

/**
 * @typedef {Object} HeatboxOptions
 * @property {ProfileName} [profile] - Named preset to start from / 推奨プリセット名
 * @property {number} [voxelSize=20] - Voxel size in meters / ボクセルサイズ(メートル)
 * @property {boolean} [autoVoxelSize=false] - Enable auto voxel size estimation / 自動ボクセルサイズ推定
 * @property {('basic'|'occupancy')} [autoVoxelSizeMode='basic'] - Auto voxel mode / 自動ボクセルモード
 * @property {number} [autoVoxelTargetFill=0.6] - Target occupancy ratio for auto mode / 自動モード時の目標充填率
 * @property {number} [maxRenderVoxels=50000] - Max voxels to render / 描画ボクセル上限
 * @property {('density'|'coverage'|'hybrid')} [renderLimitStrategy='density'] - Voxel selection strategy / ボクセル選択戦略
 * @property {number} [minCoverageRatio=0.2] - Minimum coverage ratio for hybrid strategy / ハイブリッド戦略時の最小カバレッジ比率
 * @property {('auto'|number)} [coverageBinsXY='auto'] - Grid bins for coverage strategy / カバレッジ戦略用グリッド分割
 * @property {boolean} [showOutline=true] - Draw voxel outlines / 枠線を描画
 * @property {boolean} [showEmptyVoxels=false] - Show empty voxels / 空ボクセルを描画
 * @property {boolean} [wireframeOnly=false] - Render outlines only / 枠線のみ描画
 * @property {boolean} [heightBased=false] - Scale height by density / 密度に応じて高さを調整
 * @property {number} [outlineWidth=2] - Base outline width / 基本枠線太さ
 * @property {number} [voxelGap=0] - Gap between voxels in meters / ボクセル間ギャップ(メートル)
 * @property {number} [opacity=0.8] - Box opacity / ボックス不透明度
 * @property {number} [emptyOpacity=0.03] - Empty voxel opacity / 空ボクセル不透明度
 * @property {number[]} [minColor=[0,32,255]] - RGB colour for minimum density / 最低密度時のRGB
 * @property {number[]} [maxColor=[255,64,0]] - RGB colour for maximum density / 最高密度時のRGB
 * @property {('custom'|'viridis'|'inferno')} [colorMap='custom'] - Colour map preset / カラーマップ
 * @property {boolean} [diverging=false] - Use diverging colour mode / 発散配色モード
 * @property {number} [divergingPivot=0] - Diverging pivot value / 発散配色のピボット
 * @property {number|null} [highlightTopN=null] - Highlight top N voxels / 上位Nボクセルの強調
 * @property {HeatboxHighlightStyle} [highlightStyle] - Highlight styling / ハイライトスタイル
 * @property {OutlineRenderMode} [outlineRenderMode='standard'] - Outline rendering mode / 枠線描画モード
 * @property {EmulationScope} [emulationScope='off'] - Emulation scope / エミュレーション範囲
 * @property {boolean} [adaptiveOutlines=false] - Enable adaptive outline mode / 適応枠線制御を有効化
 * @property {OutlineWidthPreset} [outlineWidthPreset='medium'] - Outline width preset / 枠線プリセット
 * @property {?function(HeatboxOutlineWidthResolverParams):number} [outlineWidthResolver=null] - Custom outline width resolver / 枠線太さの独自制御
 * @property {?function(HeatboxOpacityResolverContext):number} [outlineOpacityResolver=null] - Custom outline opacity resolver / 枠線透明度の独自制御
 * @property {?function(HeatboxOpacityResolverContext):number} [boxOpacityResolver=null] - Custom box opacity resolver / ボックス透明度の独自制御
 * @property {number} [outlineInset=0] - Inset outline offset in meters / インセット枠線のオフセット
 * @property {('all'|'topn'|'none')} [outlineInsetMode='all'] - Inset outline target / インセット枠線の適用対象
 * @property {boolean} [enableThickFrames=false] - Enable thick frame fill / 厚枠フレーム補完
 * @property {HeatboxAdaptiveParams} [adaptiveParams] - Adaptive control parameters / 適応制御パラメータ
 * @property {boolean} [autoView=false] - Automatically fit camera after render / 描画後に視点を自動調整
 * @property {HeatboxFitViewOptions} [fitViewOptions] - Camera fit options / ビューフィットの設定
 * @property {PerformanceOverlayConfig|null} [performanceOverlay=null] - Performance overlay config / パフォーマンスオーバーレイ設定
 * @property {('manual'|'auto')} [renderBudgetMode='manual'] - Render budget mode / レンダーバジェット制御モード
 * @property {(boolean|Object)} [debug=false] - Debug options (`true` enables verbose logging, object can contain `showBounds`) / デバッグ設定(trueで詳細ログ、オブジェクトの場合は`showBounds`などを指定)
 * @property {Object} [spatialId] - Spatial ID configuration (v0.1.17+) / 空間ID設定(v0.1.17+)
 * @property {boolean} [spatialId.enabled=false] - Enable spatial ID mode / 空間IDモードを有効化
 * @property {('tile-grid'|'voxel-grid')} [spatialId.mode='tile-grid'] - Spatial ID mode / 空間IDモード
 * @property {(number|'auto')} [spatialId.zoom='auto'] - Zoom level or auto / ズームレベルまたは自動
 * @property {('auto'|'manual')} [spatialId.zoomControl='auto'] - Zoom control mode / ズーム制御モード
 * @property {number} [spatialId.zoomTolerancePct=10] - Zoom tolerance percentage / ズーム許容誤差(%)
 * @property {Object} [aggregation] - Layer aggregation configuration (v0.1.18+) / レイヤ別集約設定(v0.1.18+)
 * @property {boolean} [aggregation.enabled=false] - Enable layer aggregation / レイヤ別集約を有効化
 * @property {string|null} [aggregation.byProperty=null] - Entity property key to use as layer key / レイヤキーとして使用するエンティティプロパティ
 * @property {?function(entity):string} [aggregation.keyResolver=null] - Custom layer key resolver function (takes precedence over byProperty) / カスタムレイヤキー解決関数(byPropertyより優先)
 * @property {boolean} [aggregation.showInDescription=true] - Show layer breakdown in voxel description / ボクセル説明文にレイヤ内訳を表示
 * @property {number} [aggregation.topN=10] - Number of top layers to include in statistics / 統計情報に含める上位レイヤ数
 * @property {TemporalOptions|null} [temporal=null] - Temporal playback configuration / 時系列再生の設定
 */

/**
 * Main class of CesiumJS Heatbox.
 * Provides 3D voxel-based heatmap visualization in CesiumJS environments.
 * Refer to {@link HeatboxOptions} for the full option catalogue with defaults.
 *
 * CesiumJS Heatbox メインクラス。
 * CesiumJS 環境で 3D ボクセルベースのヒートマップ可視化を提供します。
 * 利用可能なオプションと既定値は {@link HeatboxOptions} を参照してください。
 */
export class Heatbox {
  /**
   * Constructor.
   * Prepares the renderer, normalises options, and wires core event listeners.
   *
   * 初期化処理ではオプションの正規化とレンダラー生成、必要なイベント購読を行います。
   *
   * @param {Cesium.Viewer} viewer - CesiumJS Viewer instance / CesiumJS Viewer インスタンス
   * @param {HeatboxOptions} [options={}] - Configuration options / 設定オプション
   */
  constructor(viewer, options = {}) {
    if (!isValidViewer(viewer)) {
      throw new Error(ERROR_MESSAGES.INVALID_VIEWER);
    }

    this.viewer = viewer;

    // v0.1.9: Auto Render Budgetの適用
    // Phase 4: Ensure profile and legacy migration are applied before merging defaults
    let userOptions = { ...(options || {}) };
    // Apply profile before merging defaults (defaults <- profile <- user)
    if (userOptions.profile && getProfileNames().includes(userOptions.profile)) {
      userOptions = applyProfile(userOptions.profile, userOptions);
      delete userOptions.profile;
    }
    const mergedOptions = { ...DEFAULT_OPTIONS, ...userOptions };
    this.options = validateAndNormalizeOptions(applyAutoRenderBudget(mergedOptions));

    // ログレベルをオプションに基づいて設定
    Logger.setLogLevel(this.options);
    this.renderer = new VoxelRenderer(this.viewer, this.options);

    this._bounds = null;
    this._grid = null;
    this._voxelData = null;
    this._statistics = null;
    this._spatialIdEdgeCaseMetrics = null;
    this._eventHandler = null;
    this._performanceOverlay = null;
    this._lastRenderTime = null;
    this._overlayLastUpdate = 0;
    this._postRenderListener = null;
    this._prevFrameTimestamp = null;
    this._postRenderListener = null;
    this._prevFrameTimestamp = null;
    this._legend = null;
    this._timeController = null;
    this._timeControllerSignature = null;

    this._initializeEventListeners();

    // v0.1.12: Initialize performance overlay if enabled
    if (this.options.performanceOverlay && this.options.performanceOverlay.enabled) {
      this._initializePerformanceOverlay();
    }

    // v1.2.0: Initialize TimeController if temporal mode is enabled
    if (this.options.temporal && this.options.temporal.enabled) {
      this._initializeTimeController();
    }
  }

  /**
   * Get effective normalized options snapshot.
   * 正規化済みオプションのスナップショットを取得します。
   * @returns {HeatboxOptions} options snapshot / オプションのスナップショット
   */
  getEffectiveOptions() {
    try {
      return JSON.parse(JSON.stringify(this.options));
    } catch (_) {
      // Fallback shallow copy
      return { ...this.options };
    }
  }

  /**
   * Get list of available configuration profiles
   * 利用可能な設定プロファイルの一覧を取得
   * 
   * @returns {ProfileName[]} Array of profile names / プロファイル名の配列
   * @static
   * @since 0.1.12
   */
  static listProfiles() {
    return getProfileNames();
  }

  /**
   * Get configuration profile details
   * 設定プロファイルの詳細を取得
   * 
   * @param {string} profileName - Profile name / プロファイル名
   * Returned object shares the same keys as {@link HeatboxOptions} plus an optional `description`.
   * 戻り値は {@link HeatboxOptions} と同じキーに加えて `description` フィールドを含みます。
   * @returns {Object|null} Profile configuration with description / 説明付きプロファイル設定
   * @static
   * @since 0.1.12
   */
  static getProfileDetails(profileName) {
    return getProfile(profileName);
  }

  /**
   * Initialize performance overlay
   * パフォーマンスオーバーレイを初期化
   * @private
   * @since 0.1.12
   */
  _initializePerformanceOverlay() {
    if (typeof window === 'undefined') {
      Logger.warn('Performance overlay requires browser environment');
      return;
    }

    const overlayOptions = {
      position: 'top-right',
      fpsAveragingWindowMs: 1000,
      autoUpdate: true,
      updateIntervalMs: 500,
      ...this.options.performanceOverlay
    };

    this._performanceOverlay = new PerformanceOverlay(overlayOptions);

    // Show immediately if configured
    if (overlayOptions.autoShow) {
      this._performanceOverlay.show();
    }

    Logger.debug('Performance overlay initialized');

    // Hook postRender to provide real-time updates with low overhead
    this._hookPerformanceOverlayUpdates();
  }

  /**
   * Initialize TimeController
   * TimeController を初期化します
   * @private
   * @since 1.2.0
   */
  _initializeTimeController() {
    try {
      this._timeController = new TimeController(this.viewer, this, this.options.temporal);
      this._timeController.activate();
      this._timeControllerSignature = this._serializeTemporalOptions(this.options.temporal);
      Logger.info('TimeController initialized and activated');
    } catch (error) {
      this._timeController = null;
      this._timeControllerSignature = null;
      Logger.error('Failed to initialize TimeController:', error);
    }
  }

  /**
   * Deactivate and dispose TimeController instance safely.
   * TimeController を安全に停止・破棄します。
   * @private
   */
  _teardownTimeController() {
    if (!this._timeController) {
      return;
    }

    try {
      this._timeController.deactivate();
    } catch (error) {
      Logger.debug('timeController deactivate failed (non-fatal)', error);
    }

    this._timeController = null;
    this._timeControllerSignature = null;
  }

  /**
   * Synchronize TimeController when temporal options change.
   * temporalオプション変更時にTimeControllerを同期します。
   * @param {Object|null|undefined} previousTemporal
   * @param {Object|null|undefined} nextTemporal
   * @param {boolean} wasTemporalUpdated
   * @private
   */
  _syncTimeController(previousTemporal, nextTemporal, wasTemporalUpdated) {
    const prevEnabled = !!(previousTemporal && previousTemporal.enabled);
    const nextEnabled = !!(nextTemporal && nextTemporal.enabled);

    if (!prevEnabled && nextEnabled) {
      this._initializeTimeController();
      return;
    }

    if (prevEnabled && !nextEnabled) {
      this._teardownTimeController();
      return;
    }

    if (nextEnabled && wasTemporalUpdated) {
      const nextSignature = this._serializeTemporalOptions(nextTemporal);
      if (this._timeControllerSignature !== nextSignature) {
        this._teardownTimeController();
        this._initializeTimeController();
      }
    }
  }

  /**
   * Create a stable signature for temporal options to detect config changes.
   * temporalオプションの変更検知用シグネチャを生成します。
   * @param {Object|null|undefined} temporalOptions
   * @returns {string|null}
   * @private
   */
  _serializeTemporalOptions(temporalOptions) {
    if (!temporalOptions) {
      return null;
    }

    try {
      return JSON.stringify(temporalOptions, (key, value) => {
        if (typeof value === 'function') {
          return `[Function:${value.name || 'anonymous'}]`;
        }
        return value;
      });
    } catch (error) {
      Logger.debug('Failed to serialize temporal options for comparison', error);
      return null;
    }
  }

  /**
   * Toggle performance overlay visibility
   * パフォーマンスオーバーレイの表示/非表示切り替え
   * 
   * @returns {boolean} New visibility state / 新しい表示状態
   * @since 0.1.12
   */
  togglePerformanceOverlay() {
    if (!this._performanceOverlay) {
      Logger.warn('Performance overlay not initialized. Set performanceOverlay.enabled: true in options.');
      return false;
    }

    this._performanceOverlay.toggle();
    return this._performanceOverlay.isVisible;
  }

  /**
   * Show performance overlay
   * パフォーマンスオーバーレイを表示
   * @since 0.1.12
   */
  showPerformanceOverlay() {
    if (this._performanceOverlay) {
      this._performanceOverlay.show();
    }
  }

  /**
   * Hide performance overlay
   * パフォーマンスオーバーレイを非表示
   * @since 0.1.12
   */
  hidePerformanceOverlay() {
    if (this._performanceOverlay) {
      this._performanceOverlay.hide();
    }
  }

  /**
   * Enable or disable performance overlay at runtime.
   * 実行時にパフォーマンスオーバーレイを有効/無効化します。
   * @param {boolean} enabled - true to enable, false to disable
   * @param {PerformanceOverlayConfig} [options] - Optional overlay options to apply / 追加設定
   * @returns {boolean} Current enabled state / 現在の有効状態
   * @since 0.1.12
   */
  setPerformanceOverlayEnabled(enabled, options = {}) {
    if (enabled) {
      if (!this._performanceOverlay) {
        // Initialize with given options overriding existing config
        this.options.performanceOverlay = { enabled: true, ...(this.options.performanceOverlay || {}), ...options };
        this._initializePerformanceOverlay();
      } else {
        // Apply options if provided
        if (options && Object.keys(options).length > 0) {
          this._performanceOverlay.options = { ...this._performanceOverlay.options, ...options };
        }
        this._performanceOverlay.show();
      }
      return true;
    }

    // Disable and cleanup listener
    if (this._performanceOverlay) {
      this._performanceOverlay.hide();
    }
    if (this._postRenderListener) {
      try { this.viewer.scene.postRender.removeEventListener(this._postRenderListener); } catch (_) { Logger.debug('postRender listener removal failed (non-fatal)'); }
      this._postRenderListener = null;
    }
    return false;
  }

  /**
   * Estimate memory usage for performance monitoring
   * パフォーマンス監視用のメモリ使用量推定
   * @private
   * @since 0.1.12
   */
  _estimateMemoryUsage() {
    try {
      // Rough estimation based on rendered entities and data
      const entityCount = (this.renderer?.geometryRenderer?.entities?.length)
        || (this.renderer?.voxelEntities?.length) || 0;
      let voxelDataSize = 0;
      if (this._voxelData) {
        if (this._voxelData instanceof Map) {
          voxelDataSize = this._voxelData.size;
        } else if (typeof this._voxelData.size === 'number') {
          voxelDataSize = this._voxelData.size;
        } else if (Array.isArray(this._voxelData)) {
          voxelDataSize = this._voxelData.length;
        } else if (typeof this._voxelData === 'object') {
          voxelDataSize = Object.keys(this._voxelData).length;
        }
      }

      // Estimate: ~1KB per entity + ~100B per voxel data entry
      const estimated = (entityCount * 1024 + voxelDataSize * 100) / (1024 * 1024);
      return Math.max(0.1, estimated);
    } catch (_error) {
      return 0;
    }
  }

  /**
   * Set heatmap data and render.
   * Calculates bounds, prepares the voxel grid, runs classification, and finally renders.
   *
   * ヒートマップデータを設定し、境界計算→ボクセル分類→描画の順で処理します。
   *
   * @param {Cesium.Entity[]} entities - Target entities array / 対象エンティティ配列
   * @returns {Promise<void>} Resolves when rendering is completed / 描画完了時に解決
   */
  async setData(entities) {
    if (!isValidEntities(entities)) {
      this.clear();
      return;
    }

    try {
      Logger.debug('Heatbox.setData - 処理開始:', entities.length, '個のエンティティ');

      // 1. 境界計算
      Logger.debug('Step 1: 境界計算');
      this._bounds = CoordinateTransformer.calculateBounds(entities);
      if (!this._bounds) {
        Logger.error('境界計算に失敗');
        this.clear();
        return;
      }
      Logger.debug('境界計算完了:', this._bounds);

      // v0.1.4+v0.1.9: 自動ボクセルサイズ調整(占有率ベース対応)
      let finalVoxelSize = this.options.voxelSize || DEFAULT_OPTIONS.voxelSize;
      let autoAdjustmentInfo = null;

      if (this.options.autoVoxelSize && !this.options.voxelSize) {
        try {
          Logger.debug('自動ボクセルサイズ調整開始');

          // v0.1.9: 占有率ベースの計算オプション
          const sizeOptions = {
            autoVoxelSizeMode: this.options.autoVoxelSizeMode,
            autoVoxelTargetFill: this.options.autoVoxelTargetFill,
            maxRenderVoxels: this.options.maxRenderVoxels
          };

          const estimatedSize = estimateInitialVoxelSize(this._bounds, entities.length, sizeOptions);
          const tempGrid = VoxelGrid.createGrid(this._bounds, estimatedSize);
          const validation = validateVoxelCount(tempGrid.totalVoxels, estimatedSize);

          if (!validation.valid && validation.recommendedSize) {
            finalVoxelSize = validation.recommendedSize;
            autoAdjustmentInfo = {
              enabled: true,
              mode: this.options.autoVoxelSizeMode,
              originalSize: estimatedSize,
              finalSize: finalVoxelSize,
              adjusted: true,
              reason: `Performance limit exceeded: ${tempGrid.totalVoxels} > ${PERFORMANCE_LIMITS.maxVoxels}`
            };
            Logger.info(`Auto-adjusted voxelSize: ${estimatedSize}m → ${finalVoxelSize}m (${tempGrid.totalVoxels} voxels)`);
          } else {
            finalVoxelSize = estimatedSize;
            autoAdjustmentInfo = {
              enabled: true,
              mode: this.options.autoVoxelSizeMode,
              originalSize: estimatedSize,
              finalSize: finalVoxelSize,
              adjusted: false,
              reason: null
            };
            Logger.info(`Auto-determined voxelSize: ${finalVoxelSize}m`);
          }
        } catch (error) {
          Logger.warn('Auto voxel size adjustment failed, using default:', error);
          finalVoxelSize = DEFAULT_OPTIONS.voxelSize;
          autoAdjustmentInfo = {
            enabled: true,
            adjusted: false,
            reason: 'Estimation failed, using default size',
            originalSize: null,
            finalSize: finalVoxelSize
          };
        }
      }

      // 2. グリッド生成(最終的なボクセルサイズを使用)
      Logger.debug('Step 2: グリッド生成 (サイズ:', finalVoxelSize, 'm)');
      this._grid = VoxelGrid.createGrid(this._bounds, finalVoxelSize);
      Logger.debug('グリッド生成完了:', this._grid);

      // 3. エンティティ分類(v0.1.17: 空間IDサポート)
      Logger.debug('Step 3: エンティティ分類');
      // Pass options with voxelSize for spatial ID auto zoom calculation
      const classificationOptions = { ...this.options, voxelSize: finalVoxelSize };
      this._voxelData = await DataProcessor.classifyEntitiesIntoVoxels(entities, this._bounds, this._grid, classificationOptions);
      Logger.debug('エンティティ分類完了:', this._voxelData.size, '個のボクセル');

      // 4. 統計計算
      Logger.debug('Step 4: 統計計算');
      this._statistics = DataProcessor.calculateStatistics(this._voxelData, this._grid, this.options);
      Logger.debug('統計情報:', this._statistics);

      // 統計情報に自動調整情報を追加
      if (autoAdjustmentInfo) {
        this._statistics.autoAdjusted = autoAdjustmentInfo.adjusted;
        this._statistics.originalVoxelSize = autoAdjustmentInfo.originalSize;
        this._statistics.finalVoxelSize = autoAdjustmentInfo.finalSize;
        this._statistics.adjustmentReason = autoAdjustmentInfo.reason;
      }

      // v0.1.17: 空間ID情報を統計に追加
      if (classificationOptions.spatialId?.enabled) {
        this._statistics.spatialIdEnabled = true;
        this._statistics.spatialIdMode = classificationOptions.spatialId.mode;
        this._statistics.spatialIdProvider = classificationOptions._spatialIdProvider || null;
        this._statistics.spatialIdZoom = classificationOptions._resolvedZoom || null;
        this._statistics.zoomControl = classificationOptions.spatialId.zoomControl;
      } else {
        this._statistics.spatialIdEnabled = false;
      }

      // 5. 描画(レンダリング時間の計測)
      Logger.debug('Step 5: 描画');
      const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
      const renderedVoxelCount = this.renderer.render(this._voxelData, this._bounds, this._grid, this._statistics);
      const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
      this._lastRenderTime = Math.max(0, t1 - t0);

      // 統計情報に実際の描画数を反映
      this._statistics.renderedVoxels = renderedVoxelCount;
      this._statistics.renderTimeMs = this._lastRenderTime;
      Logger.info('描画完了 - 実際の描画数:', renderedVoxelCount);

      // v0.1.9: 自動視点調整
      if (this.options.autoView) {
        try {
          Logger.debug('Auto view adjustment triggered');
          await this.fitView();
          Logger.debug('Auto view adjustment completed');
        } catch (error) {
          Logger.warn('Auto view adjustment failed:', error);
          // 自動視点調整の失敗は致命的エラーとしない
        }
      }

      Logger.debug('Heatbox.setData - 処理完了');

      // Update overlay immediately after render if available
      if (this._performanceOverlay && this._performanceOverlay.isVisible) {
        const stats = this.getStatistics() || {};
        stats.renderTimeMs = this._lastRenderTime;
        stats.memoryUsageMB = this._estimateMemoryUsage();
        this._performanceOverlay.update(stats, undefined);
      }

    } catch (error) {
      Logger.error('ヒートマップ作成エラー:', error);
      this.clear();
      throw error;
    }
  }

  /**
   * Create heatmap from entities (async).
   * エンティティからヒートマップを作成(非同期 API)。
   * Resolves with the statistics snapshot calculated by {@link getStatistics}.
   * 描画完了後に {@link getStatistics} と同じ統計スナップショットを返します。
   * @param {Cesium.Entity[]} entities - Target entities array / 対象エンティティ配列
   * @returns {Promise<HeatboxStatistics>} Statistics info / 統計情報
   */
  async createFromEntities(entities) {
    if (!isValidEntities(entities)) {
      throw new Error(ERROR_MESSAGES.NO_ENTITIES);
    }
    await this.setData(entities);
    return this.getStatistics();
  }

  /**
   * Toggle visibility.
   * 表示/非表示を切り替えます。
   * @param {boolean} show - true to show / 表示する場合は true
   */
  setVisible(show) {
    this.renderer.setVisible(show);
  }

  /**
   * Clear the heatmap and internal state.
   * ヒートマップと内部状態をクリアします。
   */
  clear() {
    this.renderer.clear();
    this._bounds = null;
    this._grid = null;
    this._voxelData = null;
    this._statistics = null;
  }

  /**
   * Destroy the instance and release event listeners.
   * インスタンスを破棄し、イベントリスナーを解放します。
   */
  destroy() {
    this.clear();
    if (this._eventHandler && !this._eventHandler.isDestroyed()) {
      this._eventHandler.destroy();
    }
    // Remove overlay listener and destroy overlay
    if (this._postRenderListener) {
      try { this.viewer.scene.postRender.removeEventListener(this._postRenderListener); } catch (_) { Logger.debug('postRender listener removal failed (non-fatal)'); }
      this._postRenderListener = null;
    }
    if (this._performanceOverlay) {
      try { this._performanceOverlay.destroy(); } catch (_) { Logger.debug('overlay destroy failed (non-fatal)'); }
      this._performanceOverlay = null;
    }
    if (this._legend) {
      try { this._legend.destroy(); } catch (_) { Logger.debug('legend destroy failed (non-fatal)'); }
      this._legend = null;
    }
    this._teardownTimeController();
    this._eventHandler = null;
  }

  /**
   * Alias for destroy() to match examples and tests.
   * 互換性のための別名。destroy() を呼び出します。
   */
  dispose() {
    this.destroy();
  }

  /**
   * Get current options.
   * 現在のオプションを取得します。
   * @returns {HeatboxOptions} Options / オプション
   */
  getOptions() {
    return { ...this.options };
  }

  /**
   * Update options and re-render if applicable.
   * オプションを更新し、必要に応じて再描画します。
   * @param {HeatboxOptions} newOptions - New options (partial allowed) / 新しいオプション(部分指定可)
   */
  updateOptions(newOptions) {
    const previousTemporal = this.options ? this.options.temporal : undefined;
    const temporalUpdated = newOptions ? Object.prototype.hasOwnProperty.call(newOptions, 'temporal') : false;

    this.options = validateAndNormalizeOptions({ ...this.options, ...newOptions });
    this.renderer.options = this.options;

    if (this.renderer.adaptiveController && typeof this.renderer.adaptiveController.updateOptions === 'function') {
      this.renderer.adaptiveController.updateOptions(this.options);
    }
    if (this.renderer.geometryRenderer && typeof this.renderer.geometryRenderer.updateOptions === 'function') {
      this.renderer.geometryRenderer.updateOptions(this.options);
    }

    // 既存のヒートマップがある場合は再描画
    if (this._voxelData) {
      const renderedVoxelCount = this.renderer.render(this._voxelData, this._bounds, this._grid, this._statistics);
      // 統計情報を更新
      this._statistics.renderedVoxels = renderedVoxelCount;
    }

    this._syncTimeController(previousTemporal, this.options.temporal, temporalUpdated);
  }

  /**
   * Initialize internal event listeners.
   * 内部のイベントリスナーを初期化します。
   * @private
   */
  _initializeEventListeners() {
    this._eventHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);

    // クリックイベントでInfoBoxを更新
    this._eventHandler.setInputAction(movement => {
      const pickedObject = this.viewer.scene.pick(movement.position);
      if (Cesium.defined(pickedObject) && pickedObject.id &&
        pickedObject.id.properties &&
        pickedObject.id.properties.type === 'voxel') {
        // プロパティからキー値を取得
        const voxelKey = pickedObject.id.properties.key;
        const voxelInfo = {
          x: pickedObject.id.properties.x,
          y: pickedObject.id.properties.y,
          z: pickedObject.id.properties.z,
          count: pickedObject.id.properties.count
        };

        // InfoBoxに表示するためのダミーエンティティを作成
        const dummyEntity = new Cesium.Entity({
          id: `voxel-${voxelKey}`,
          description: this.renderer.createVoxelDescription(voxelInfo, voxelKey)
        });
        this.viewer.selectedEntity = dummyEntity;
      }
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  }

  /**
   * Get statistics information.
   * 統計情報を取得します(未作成の場合は null)。
   * @returns {HeatboxStatistics|null} Statistics or null / 統計情報 または null
   */
  getStatistics() {
    if (!this._statistics) {
      return null;
    }

    // 基本統計情報
    const stats = { ...this._statistics };

    // v0.1.9: 選択戦略統計を追加
    const selectionStats = this.renderer.getSelectionStats();
    if (selectionStats) {
      stats.selectionStrategy = selectionStats.strategy;
      stats.clippedNonEmpty = selectionStats.clippedNonEmpty;
      stats.coverageRatio = selectionStats.coverageRatio ?? 0;
    }

    // v0.1.9: Auto Render Budget統計を追加
    if (this.options._autoRenderBudget) {
      stats.renderBudgetTier = this.options._autoRenderBudget.tier;
      stats.autoMaxRenderVoxels = this.options._autoRenderBudget.autoMaxRenderVoxels;
    }

    // v0.1.9: occupancy ratio (rendered / budget) for diagnostics
    if (typeof this.options.maxRenderVoxels === 'number' && this.options.maxRenderVoxels > 0) {
      stats.occupancyRatio = Math.min(1, Math.max(0, (stats.renderedVoxels || 0) / this.options.maxRenderVoxels));
    } else {
      stats.occupancyRatio = null;
    }

    // v0.1.19: Spatial ID statistics sub-object (ADR-0015)
    const spatialIdOptions = this.options && this.options.spatialId ? this.options.spatialId : {};
    const spatialIdEnabled = Boolean(stats.spatialIdEnabled);
    const edgeCaseMetrics = this._spatialIdEdgeCaseMetrics ?? null;
    stats.spatialId = {
      enabled: spatialIdEnabled,
      provider: spatialIdEnabled ? (stats.spatialIdProvider ?? null) : null,
      zoom: spatialIdEnabled ? (stats.spatialIdZoom ?? null) : null,
      zoomControl: stats.zoomControl ?? spatialIdOptions.zoomControl ?? null,
      edgeCaseMetrics
    };

    // v0.1.18: Layer aggregation statistics (ADR-0014)
    if (this.options.aggregation?.enabled && this._voxelData) {
      const globalLayerCounts = new Map();

      // Aggregate across all voxels / 全ボクセルを集約
      for (const voxelInfo of this._voxelData.values()) {
        if (voxelInfo.layerStats) {
          for (const [layerKey, count] of voxelInfo.layerStats) {
            globalLayerCounts.set(
              layerKey,
              (globalLayerCounts.get(layerKey) || 0) + count
            );
          }
        }
      }

      // Top N layers (configurable via options.aggregation.topN) / 上位N個のレイヤ(options.aggregation.topNで設定可能)
      const topN = this.options.aggregation?.topN ?? 10;
      const sorted = Array.from(globalLayerCounts.entries())
        .sort((a, b) => b[1] - a[1])  // Sort by count descending / カウント降順でソート
        .slice(0, topN);

      stats.layers = sorted.map(([key, total]) => ({ key, total }));

      Logger.debug(`[aggregation] Aggregated ${globalLayerCounts.size} unique layers, returning top ${stats.layers.length}`);
    }

    return stats;
  }

  /**
   * Get bounds info if available.
   * 境界情報を取得します(未作成の場合は null)。
   * @returns {HeatboxBounds|null} Bounds or null / 境界情報 または null
   */
  getBounds() {
    return this._bounds;
  }

  /**
   * Get debug information.
   * デバッグ情報を取得します。
   * @returns {HeatboxDebugInfo} Debug info / デバッグ情報
   */
  getDebugInfo() {
    const baseInfo = {
      options: { ...this.options },
      bounds: this._bounds,
      grid: this._grid,
      statistics: this._statistics
    };

    // v0.1.4: 自動調整情報を追加
    if (this.options.autoVoxelSize) {
      baseInfo.autoVoxelSizeInfo = {
        enabled: this.options.autoVoxelSize,
        originalSize: this._statistics?.originalVoxelSize,
        finalSize: this._statistics?.finalVoxelSize,
        adjusted: this._statistics?.autoAdjusted || false,
        reason: this._statistics?.adjustmentReason,
        dataRange: this._bounds ? calculateDataRange(this._bounds) : null,
        estimatedDensity: this._bounds && this._statistics ?
          this._statistics.totalEntities / (calculateDataRange(this._bounds).x * calculateDataRange(this._bounds).y * calculateDataRange(this._bounds).z) : null
      };
    }

    return baseInfo;
  }

  /**
   * Hook viewer postRender to feed overlay with periodic updates.
   * viewer の postRender にフックし、オーバーレイへ定期更新を供給します。
   * @private
   */
  _hookPerformanceOverlayUpdates() {
    if (!this._performanceOverlay || this._postRenderListener) return;

    const interval = this._performanceOverlay.options?.updateIntervalMs ?? 500;
    this._postRenderListener = () => {
      // Skip if overlay not visible
      if (!this._performanceOverlay || !this._performanceOverlay.isVisible) return;

      const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();

      // Calculate frame time from previous timestamp
      let frameTime;
      if (this._prevFrameTimestamp != null) {
        frameTime = Math.max(0, now - this._prevFrameTimestamp);
      }
      this._prevFrameTimestamp = now;

      // Throttle updates
      if (now - this._overlayLastUpdate < interval) return;
      this._overlayLastUpdate = now;

      const stats = this.getStatistics() || {};
      // Attach last render time and estimated memory usage
      if (this._lastRenderTime != null) stats.renderTimeMs = this._lastRenderTime;
      stats.memoryUsageMB = this._estimateMemoryUsage();

      try {
        this._performanceOverlay.update(stats, frameTime);
      } catch (_e) {
        // Ignore overlay update errors to avoid impacting render loop
      }
    };

    try {
      this.viewer.scene.postRender.addEventListener(this._postRenderListener);
    } catch (_e) {
      // No-op if event registration fails
    }
  }

  /**
   * Fit view to data bounds with smart camera positioning.
   * データ境界にスマートなカメラ位置でビューをフィットします。
   *
   * 実装メモ(v0.1.12):
   * - 描画とカメラ移動の競合を避けるため、`viewer.scene.postRender` で1回だけ実行します。
   * - 矩形境界(経緯度)から `Cesium.Rectangle` → `Cesium.BoundingSphere` を生成し、
   *   `camera.flyToBoundingSphere` + `HeadingPitchRange` で安定的にズームします。
   * - 俯角は安全範囲にクランプ(既定: -35°, 範囲: [-85°, -10°])。
   * - 失敗時は `viewer.zoomTo(viewer.entities)` へフォールバックします。
   *
   * @param {HeatboxBounds|null} [bounds=null] - Target bounds(省略時は現在のデータ境界)
   * @param {HeatboxFitViewOptions} [options={}] - Fit view options / フィットビュー設定
   * @returns {Promise<void>} カメラ移動完了時に解決する Promise
   * @example
   * // データを適用後、安定的にビューフィット
   * await heatbox.setData(viewer.entities.values);
   * await heatbox.fitView(null, { headingDegrees: 0, pitchDegrees: -35, paddingPercent: 0.1 });
   */
  async fitView(bounds = null, options = {}) {
    try {
      const targetBounds = bounds || this._bounds;
      if (!targetBounds) {
        Logger.warn('No bounds available for fitView');
        return;
      }

      // 境界の妥当性チェック
      if (!this._isValidBounds(targetBounds)) {
        Logger.warn('Invalid bounds provided to fitView:', targetBounds);
        return;
      }

      const fitOptions = {
        ...this.options.fitViewOptions,
        ...options
      };

      Logger.debug('fitView called with bounds:', targetBounds, 'options:', fitOptions);

      // postRenderで一回だけ実行して描画との競合を回避
      const safeOptions = { ...fitOptions };
      if (!Number.isFinite(safeOptions.pitchDegrees)) safeOptions.pitchDegrees = -35;
      if (!Number.isFinite(safeOptions.headingDegrees)) safeOptions.headingDegrees = 0;

      return await new Promise((resolve) => {
        let fired = false;
        const handler = async () => {
          if (fired) return;
          fired = true;
          try {
            await this._fitByBoundingSphere(targetBounds, safeOptions);
          } catch (e) {
            Logger.warn('fitView (postRender) failed, trying fallback:', e);
            try {
              await this.viewer.zoomTo(this.viewer.entities);
            } catch (zoomErr) {
              Logger.warn('zoomTo fallback failed:', zoomErr);
            }
          } finally {
            try {
              this.viewer.scene.postRender.removeEventListener(handler);
            } catch (remErr) {
              Logger.debug('postRender removeEventListener failed (non-fatal):', remErr);
            }
            resolve();
          }
        };
        try {
          this.viewer.scene.postRender.addEventListener(handler);
        } catch (e) {
          Logger.warn('postRender addEventListener failed:', e);
          resolve();
        }
      });

    } catch (error) {
      Logger.error('fitView failed:', error);
      throw error;
    }
  }

  /**
   * Fit by bounding sphere derived from rectangle bounds
   * @private
   */
  async _fitByBoundingSphere(bounds, fitOptions) {
    const rect = Cesium.Rectangle.fromDegrees(bounds.minLon, bounds.minLat, bounds.maxLon, bounds.maxLat);
    const bs = Cesium.BoundingSphere.fromRectangle3D(rect, Cesium.Ellipsoid.WGS84, Math.max(0, bounds.minAlt || 0));
    const heading = Cesium.Math.toRadians(fitOptions.headingDegrees ?? 0);
    const pitchDeg = Math.max(-85, Math.min(-10, fitOptions.pitchDegrees ?? -35));
    const pitch = Cesium.Math.toRadians(pitchDeg);
    const range = Math.max(bs.radius * 2.2, 1000.0);
    await this.viewer.camera.flyToBoundingSphere(bs, {
      duration: 1.2,
      offset: new Cesium.HeadingPitchRange(heading, pitch, range)
    });
  }

  /**
   * Validate bounds object.
   * 境界オブジェクトの妥当性をチェックします。
   * @param {Object} bounds - Bounds to validate / 検証する境界
   * @returns {boolean} True if valid / 有効な場合true
   * @private
   */
  _isValidBounds(bounds) {
    return bounds &&
      typeof bounds.minLon === 'number' && !isNaN(bounds.minLon) &&
      typeof bounds.maxLon === 'number' && !isNaN(bounds.maxLon) &&
      typeof bounds.minLat === 'number' && !isNaN(bounds.minLat) &&
      typeof bounds.maxLat === 'number' && !isNaN(bounds.maxLat) &&
      typeof bounds.minAlt === 'number' && !isNaN(bounds.minAlt) &&
      typeof bounds.maxAlt === 'number' && !isNaN(bounds.maxAlt) &&
      bounds.minLon <= bounds.maxLon &&
      bounds.minLat <= bounds.maxLat &&
      bounds.minAlt <= bounds.maxAlt;
  }

  /**
   * Handle minimal data range case.
   * 極小データ範囲の場合の処理
   * @param {number} centerLon - Center longitude / 中心経度
   * @param {number} centerLat - Center latitude / 中心緯度
   * @param {number} centerAlt - Center altitude / 中心高度
   * @param {Object} fitOptions - Fit options / フィットオプション
   * @returns {Promise} Camera movement promise / カメラ移動Promise
   * @private
   */
  async _handleMinimalDataRange(centerLon, centerLat, centerAlt, fitOptions) {
    Logger.debug('Handling minimal data range');

    const destination = Cesium.Cartesian3.fromDegrees(centerLon, centerLat, centerAlt + 2000);
    const heading = Cesium.Math.toRadians(fitOptions.headingDegrees || fitOptions.heading);
    const pitch = Cesium.Math.toRadians(fitOptions.pitchDegrees || fitOptions.pitch);

    return this.viewer.camera.flyTo({
      destination,
      orientation: { heading, pitch, roll: 0 },
      duration: 1.5
    });
  }

  /**
   * Handle large data range case.
   * 極大データ範囲の場合の処理
   * @param {Object} bounds - Target bounds / 対象境界
   * @param {Object} fitOptions - Fit options / フィットオプション
   * @returns {Promise} Camera movement promise / カメラ移動Promise
   * @private
   */
  async _handleLargeDataRange(bounds, fitOptions) {
    Logger.debug('Handling large data range with bounding sphere');

    const centerLon = (bounds.minLon + bounds.maxLon) / 2;
    const centerLat = (bounds.minLat + bounds.maxLat) / 2;
    const centerAlt = (bounds.minAlt + bounds.maxAlt) / 2;

    const dataRange = calculateDataRange(bounds);
    const maxRange = Math.max(dataRange.x, dataRange.y, dataRange.z);

    const boundingSphere = new Cesium.BoundingSphere(
      Cesium.Cartesian3.fromDegrees(centerLon, centerLat, centerAlt),
      maxRange / 2
    );

    const heading = Cesium.Math.toRadians(fitOptions.headingDegrees || fitOptions.heading);
    const pitch = Cesium.Math.toRadians(fitOptions.pitchDegrees || fitOptions.pitch);

    return this.viewer.camera.flyToBoundingSphere(boundingSphere, {
      duration: 2.5,
      offset: new Cesium.HeadingPitchRange(heading, pitch, 0)
    });
  }

  /**
   * Calculate optimal camera height.
   * 最適なカメラ高度を計算します。
   * @param {number} maxRange - Maximum data range / 最大データ範囲
   * @param {number} paddingMeters - Padding in meters / パディング(メートル)
   * @param {Object} fitOptions - Fit options / フィットオプション
   * @returns {number} Optimal camera height / 最適なカメラ高度
   * @private
   */
  _calculateOptimalCameraHeight(maxRange, paddingMeters, fitOptions) {
    if (fitOptions.altitudeStrategy !== 'auto') {
      return fitOptions.altitude || 5000;
    }

    try {
      const pitch = Cesium.Math.toRadians(fitOptions.pitchDegrees || fitOptions.pitch);
      const fov = this.viewer.camera.frustum.fovy || Cesium.Math.toRadians(60);

      // 幾何学的計算: データがフレームに収まる高度を計算
      const adjustedRange = maxRange + paddingMeters;
      const baseCameraHeight = adjustedRange / (2 * Math.tan(fov / 2));

      // ピッチ補正(斜め視点での見え方調整)
      const absPitch = Math.abs(pitch);
      const pitchFactor = Math.max(0.5, Math.sin(Math.PI / 2 - absPitch) + 0.3);
      let cameraHeight = baseCameraHeight * pitchFactor;

      // アスペクト比補正(極端に細長いデータの場合)
      const aspectRatio = maxRange / Math.min(maxRange, 100);
      if (aspectRatio > 5) {
        cameraHeight *= Math.log10(aspectRatio) + 1;
      }

      // 制限値適用(データ範囲に基づく適応的制限)
      const minHeight = Math.max(500, maxRange * 0.1);
      const maxHeight = Math.min(100000, maxRange * 10);
      cameraHeight = Math.max(minHeight, Math.min(maxHeight, cameraHeight));

      Logger.debug(`Camera height calculated: ${cameraHeight.toFixed(0)}m (range: ${maxRange.toFixed(0)}m, pitch: ${fitOptions.pitchDegrees || fitOptions.pitch}°)`);
      return cameraHeight;

    } catch (error) {
      Logger.warn('Camera height calculation failed, using fallback:', error);
      return Math.max(2000, maxRange * 2);
    }
  }

  /**
   * Execute camera movement.
   * カメラ移動を実行します。
   * @param {number} centerLon - Center longitude / 中心経度
   * @param {number} centerLat - Center latitude / 中心緯度
   * @param {number} centerAlt - Center altitude / 中心高度
   * @param {number} cameraHeight - Camera height / カメラ高度
   * @param {Object} fitOptions - Fit options / フィットオプション
   * @param {number} maxRange - Maximum range / 最大範囲
   * @param {number} paddingMeters - Padding meters / パディング(メートル)
   * @returns {Promise} Camera movement promise / カメラ移動Promise
   * @private
   */
  async _executeCameraMovement(centerLon, centerLat, centerAlt, cameraHeight, fitOptions, maxRange, paddingMeters) {
    try {
      // 目標カメラ位置
      const destination = Cesium.Cartesian3.fromDegrees(
        centerLon,
        centerLat,
        centerAlt + cameraHeight
      );

      // カメラの向き設定
      const heading = Cesium.Math.toRadians(fitOptions.headingDegrees || fitOptions.heading);
      const pitch = Cesium.Math.toRadians(fitOptions.pitchDegrees || fitOptions.pitch);
      const roll = 0;

      const orientation = {
        heading,
        pitch,
        roll
      };

      Logger.debug(`Camera target: position=${centerLon.toFixed(6)},${centerLat.toFixed(6)},${(centerAlt + cameraHeight).toFixed(0)}, heading=${fitOptions.headingDegrees || fitOptions.heading}°, pitch=${fitOptions.pitchDegrees || fitOptions.pitch}°`);

      // 距離に応じた移動時間の調整
      const duration = Math.max(1.0, Math.min(3.0, Math.log10(maxRange) * 0.8));

      // プライマリ: flyTo を使用
      const flyPromise = this.viewer.camera.flyTo({
        destination,
        orientation,
        duration,
        complete: () => {
          Logger.debug('fitView camera movement completed');
        },
        cancel: () => {
          Logger.debug('fitView camera movement cancelled');
        }
      });

      // flyToが利用できない場合のフォールバック
      if (!flyPromise) {
        Logger.debug('Using fallback: flyToBoundingSphere');
        const boundingSphere = new Cesium.BoundingSphere(
          Cesium.Cartesian3.fromDegrees(centerLon, centerLat, centerAlt),
          maxRange / 2 + paddingMeters
        );

        await this.viewer.camera.flyToBoundingSphere(boundingSphere, {
          duration,
          offset: new Cesium.HeadingPitchRange(heading, pitch, 0)
        });
      } else {
        await flyPromise;
      }

      Logger.info('fitView completed successfully');

    } catch (error) {
      Logger.error('Camera movement execution failed:', error);
      throw error;
    }
  }

  /**
   * Create a classification legend (Phase 5).
   * 分類用の凡例UIを生成します。
   * @param {HTMLElement} [container] - 追加先コンテナ(省略時はbodyに追加)
   * @returns {HTMLElement|null} legend element / 凡例DOM要素
   */
  createLegend(container = null) {
    if (!this._legend) {
      this._legend = new Legend({ container });
    }
    this._legend.render(this.renderer?._classifier || null, this.options.classification || {});
    return this._legend.container || null;
  }

  /**
   * Update legend contents using latest classifier.
   * 最新の分類状態で凡例を更新します。
   */
  updateLegend() {
    if (this._legend) {
      this._legend.update(this.renderer?._classifier || null, this.options.classification || {});
    }
  }

  /**
   * Destroy legend UI.
   * 凡例UIを破棄します。
   */
  destroyLegend() {
    if (this._legend) {
      this._legend.destroy();
      this._legend = null;
    }
  }

  /**
   * Filter entity array (utility static method).
   * エンティティ配列をフィルタします(ユーティリティ・静的メソッド)。
   * @param {Cesium.Entity[]} entities - Entity array / エンティティ配列
   * @param {Function} predicate - Predicate function / フィルタ関数
   * @returns {Cesium.Entity[]} Filtered array / フィルタ済み配列
   */
  static filterEntities(entities, predicate) {
    if (!Array.isArray(entities) || typeof predicate !== 'function') return [];
    return entities.filter(predicate);
  }
}

日本語

/**
 * CesiumJS Heatbox - Main orchestration class.
 * CesiumJS Heatbox - メインオーケストレーションクラス
 */
import * as Cesium from 'cesium';
import { DEFAULT_OPTIONS, ERROR_MESSAGES, PERFORMANCE_LIMITS } from './utils/constants.js';
import {
  isValidViewer,
  isValidEntities,
  validateAndNormalizeOptions,
  validateVoxelCount,
  estimateInitialVoxelSize,
  calculateDataRange
} from './utils/validation.js';
import { applyAutoRenderBudget } from './utils/deviceTierDetector.js';
import { Logger } from './utils/logger.js';
import { CoordinateTransformer } from './core/CoordinateTransformer.js';
import { VoxelGrid } from './core/VoxelGrid.js';
import { DataProcessor } from './core/DataProcessor.js';
import { VoxelRenderer } from './core/VoxelRenderer.js';
import { getProfileNames, getProfile, applyProfile } from './utils/profiles.js';
import { PerformanceOverlay } from './utils/performanceOverlay.js';
import { Legend } from './ui/Legend.js';
import { TimeController } from './core/temporal/TimeController.js';

/**
 * @typedef {('mobile-fast'|'desktop-balanced'|'dense-data'|'sparse-data')} ProfileName
 * @since 0.1.12
 */

/**
 * @typedef {('standard'|'inset'|'emulation-only')} OutlineRenderMode
 * @since 0.1.12
 */

/**
 * @typedef {('off'|'topn'|'non-topn'|'all')} EmulationScope
 * @since 0.1.12
 */

/**
 * @typedef {('thin'|'medium'|'thick'|'adaptive')} OutlineWidthPreset
 * @since 0.1.12
 */

/**
 * @typedef {Object} PerformanceOverlayConfig
 * @property {boolean} [enabled=false] - Enable performance overlay / パフォーマンスオーバーレイを有効化
 * @property {('top-left'|'top-right'|'bottom-left'|'bottom-right')} [position='top-right'] - Overlay position / 配置位置
 * @property {boolean} [autoShow=false] - Show overlay automatically / 自動表示
 * @property {boolean} [autoUpdate=true] - Auto refresh overlay after render / 描画後に自動更新するか
 * @property {number} [updateIntervalMs=500] - Update interval in milliseconds / 更新間隔(ミリ秒)
 * @property {number} [fpsAveragingWindowMs=1000] - FPS 平滑化窓(ミリ秒)/ Averaging window for FPS
 * @since 0.1.12
 */

/**
 * @typedef {Object} HeatboxHighlightStyle
 * @property {number} [outlineWidth=4] - Outline width applied to highlighted voxels / ハイライト対象ボクセルに適用する枠線太さ
 * @property {number} [boostOpacity=0.2] - Extra opacity applied to highlighted voxels / ハイライト時に加算する不透明度
 * @property {number} [boostOutlineWidth] - Optional outline width override / 枠線太さの上書き指定
 */

/**
 * @typedef {Object} HeatboxAdaptiveParams
 * @property {number} [neighborhoodRadius=30] - Neighbor radius in meters used for density sampling / 密度サンプリングに用いる近傍半径(メートル)
 * @property {number} [densityThreshold=3] - Density threshold (entities per voxel) / 密度しきい値(エンティティ/ボクセル)
 * @property {number} [cameraDistanceFactor=0.8] - Camera distance compensation factor / カメラ距離補正係数
 * @property {number} [overlapRiskFactor=0.4] - Overlap risk factor used for diagnostics / 重なりリスク係数
 * @property {(Array.<number>|null)} [outlineWidthRange=null] - `[min,max]` outline width clamp / 枠線太さの許容範囲 `[最小, 最大]`
 * @property {(Array.<number>|null)} [boxOpacityRange=null] - `[min,max]` box opacity clamp / ボックス不透明度の許容範囲
 * @property {(Array.<number>|null)} [outlineOpacityRange=null] - `[min,max]` outline opacity clamp / 枠線不透明度の許容範囲
 * @property {boolean} [adaptiveOpacityEnabled=false] - Reserved flag for adaptive opacity / 適応透明度(プレースホルダー)
 * @property {boolean} [zScaleCompensation=true] - Enable Z scale compensation / Z軸スケール補正の有効化
 * @property {boolean} [overlapDetection=false] - Enable overlap diagnostics / 重なり検出を有効化
 */

/**
 * @typedef {Object} HeatboxFitViewOptions
 * @property {number} [paddingPercent=0.1] - Padding ratio around bounds / 境界に対するパディング割合
 * @property {number} [pitchDegrees=-30] - Camera pitch angle in degrees / カメラ俯角(度)
 * @property {number} [headingDegrees=0] - Camera heading in degrees / カメラ方位(度)
 * @property {('auto'|'manual')} [altitudeStrategy='auto'] - Altitude strategy for camera / カメラ高度の計算方法
 */

/**
 * @typedef {Object} TemporalDataEntry
 * @property {Cesium.JulianDate|string|Date|number} start - Interval start time / インターバル開始時刻
 * @property {Cesium.JulianDate|string|Date|number} stop - Interval end time / インターバル終了時刻
 * @property {Array<Cesium.Entity|Object>} data - Entities rendered during the interval / その期間に描画するエンティティ配列
 */

/**
 * @typedef {Object} TemporalOptions
 * @property {boolean} [enabled=false] - Enable temporal mode / 時間依存モードを有効化
 * @property {TemporalDataEntry[]} [data=[]] - Ordered temporal slices / ソート済みの時系列スライス
 * @property {('global'|'per-time')} [classificationScope='global'] - Classification scope / 分類スコープ
 * @property {('frame'|number)} [updateInterval=100] - Update interval (`frame` or milliseconds) / 更新間隔
 * @property {('clear'|'hold')} [outOfRangeBehavior='hold'] - Behaviour when clock is outside data range / データ範囲外時の挙動
 * @property {('skip'|'prefer-earlier'|'prefer-later')} [overlapResolution='prefer-earlier'] - Overlap resolution strategy / 重複時の解決方法
 * @property {boolean} [interpolate=false] - Reserved flag for interpolation (future) / 将来の補間フラグ(現状は未使用)
 */

/**
 * @typedef {Object} HeatboxBounds
 * @property {number} minLon - Minimum longitude / 最小経度
 * @property {number} maxLon - Maximum longitude / 最大経度
 * @property {number} minLat - Minimum latitude / 最小緯度
 * @property {number} maxLat - Maximum latitude / 最大緯度
 * @property {number} minAlt - Minimum altitude / 最小高度
 * @property {number} maxAlt - Maximum altitude / 最大高度
 * @property {number} [centerLon] - Center longitude / 中心経度
 * @property {number} [centerLat] - Center latitude / 中心緯度
 * @property {number} [centerAlt] - Center altitude / 中心高度
 */

/**
 * @typedef {Object} HeatboxGridInfo
 * @property {number} numVoxelsX - Number of voxels along X axis / X軸方向のボクセル数
 * @property {number} numVoxelsY - Number of voxels along Y axis / Y軸方向のボクセル数
 * @property {number} numVoxelsZ - Number of voxels along Z axis / Z軸方向のボクセル数
 * @property {number} voxelSizeMeters - Voxel size in meters / ボクセルサイズ(メートル)
 * @property {number} totalVoxels - Total voxel count / 総ボクセル数
 */

/**
 * @typedef {Object} HeatboxLayerStat
 * @property {string} key - Layer key / レイヤキー
 * @property {number} total - Total entity count for this layer / このレイヤの総エンティティ数
 */

/**
 * @typedef {Object} SpatialIdEdgeCaseMetrics
 * @property {number} datelineNeighborsChecked - Number of neighbor checks near dateline / 日付変更線近傍で検証した近傍セル数
 * @property {number} datelineNeighborsMismatched - Number of neighbor mismatches near dateline / 日付変更線近傍で不一致となった近傍セル数
 * @property {number} polarTilesChecked - Number of polar tiles evaluated / 極域タイルの検証数
 * @property {number} polarMaxRelativeErrorXY - Maximum relative XY error near poles / 極域近傍でのXY相対誤差の最大値
 * @property {number} hemisphereBoundsChecked - Number of hemisphere-crossing bounds evaluated / 半球跨ぎboundsの検証数
 * @property {number} hemisphereBoundsMismatched - Number of hemisphere-crossing mismatches / 半球跨ぎで不一致となったケース数
 */

/**
 * @typedef {Object} HeatboxStatistics
 * @property {number} totalVoxels - Total voxels generated / 生成された総ボクセル数
 * @property {number} renderedVoxels - Voxels actually rendered / 実際に描画されたボクセル数
 * @property {number} nonEmptyVoxels - Non-empty voxels / データを含むボクセル数
 * @property {number} emptyVoxels - Empty voxels / 空ボクセル数
 * @property {number} totalEntities - Entities processed / 処理したエンティティ数
 * @property {number} minCount - Minimum entity count per voxel / 1ボクセルあたり最小エンティティ数
 * @property {number} maxCount - Maximum entity count per voxel / 1ボクセルあたり最大エンティティ数
 * @property {number} averageCount - Average entity count per voxel / 平均エンティティ数
 * @property {boolean} [autoAdjusted] - Whether auto adjustments occurred / 自動調整が行われたか
 * @property {number|null} [originalVoxelSize] - Original voxel size before adjustment / 調整前のボクセルサイズ
 * @property {number|null} [finalVoxelSize] - Final voxel size after adjustment / 調整後のボクセルサイズ
 * @property {string|null} [adjustmentReason] - Reason for auto adjustment / 自動調整の理由
 * @property {number} [renderTimeMs] - Render time in milliseconds / 描画時間(ミリ秒)
 * @property {string} [selectionStrategy] - Selection strategy used / 適用された選択戦略
 * @property {number} [clippedNonEmpty] - Non-empty voxels clipped by limits / 制限により除外された非空ボクセル数
 * @property {number} [coverageRatio] - Coverage ratio when hybrid strategy used / ハイブリッド戦略時のカバレッジ比率
 * @property {string} [renderBudgetTier] - Auto render budget tier label / 自動レンダーバジェットの区分
 * @property {number} [autoMaxRenderVoxels] - Auto-assigned maxRenderVoxels / 自動設定された maxRenderVoxels
 * @property {number|null} [occupancyRatio] - Ratio of rendered voxels to limit / 描画ボクセルと上限の比率
 * @property {HeatboxLayerStat[]} [layers] - Top-N layer aggregation (v0.1.18 ADR-0014) / 上位N個のレイヤ集約
 * @property {Object} [spatialId] - Spatial ID statistics (v0.1.19 ADR-0015) / 空間ID関連の統計情報
 * @property {boolean} [spatialId.enabled] - Whether Spatial ID mode is enabled / 空間IDモードが有効か
 * @property {string|null} [spatialId.provider] - Spatial ID provider identifier ('ouranos-gex' or null) / 空間IDプロバイダー識別子
 * @property {number|null} [spatialId.zoom] - Resolved zoom level / 解決済みズームレベル
 * @property {('auto'|'manual'|null)} [spatialId.zoomControl] - Zoom control mode / ズーム制御モード
 * @property {SpatialIdEdgeCaseMetrics|null} [spatialId.edgeCaseMetrics] - QA metrics for global edge cases / グローバル端ケースQA用メトリクス
 */

/**
 * @typedef {Object} HeatboxAutoVoxelSizeInfo
 * @property {boolean} enabled - Whether auto voxel sizing ran / 自動ボクセル調整が実行されたか
 * @property {boolean} adjusted - Whether voxel size was adjusted / サイズが調整されたか
 * @property {string|null} reason - Adjustment reason / 調整理由
 * @property {number|null} originalSize - Initial estimate / 初期推定値
 * @property {number|null} finalSize - Final size / 最終サイズ
 * @property {Object|null} [dataRange] - Estimated data range / 推定データ範囲
 * @property {number|null} [estimatedDensity] - Estimated density / 推定密度
 */

/**
 * @typedef {Object} HeatboxDebugInfo
 * @property {HeatboxOptions} options - Effective options / 有効なオプション
 * @property {HeatboxBounds|null} bounds - Current bounds / 現在の境界
 * @property {HeatboxGridInfo|null} grid - Grid information / グリッド情報
 * @property {HeatboxStatistics|null} statistics - Statistics snapshot / 統計情報
 * @property {HeatboxAutoVoxelSizeInfo|null} [autoVoxelSizeInfo] - Auto voxel sizing details / 自動ボクセル調整の詳細
 */

/**
 * @typedef {Object} HeatboxResolverVoxelInfo
 * @property {number} x - Voxel grid index (X) / ボクセルのXインデックス
 * @property {number} y - Voxel grid index (Y) / ボクセルのYインデックス
 * @property {number} z - Voxel grid index (Z) / ボクセルのZインデックス
 * @property {number} count - Number of entities within the voxel / ボクセル内のエンティティ数
 */

/**
 * @typedef {Object} HeatboxOutlineWidthResolverParams
 * @property {HeatboxResolverVoxelInfo} voxel - Voxel information / ボクセル情報
 * @property {boolean} isTopN - Whether voxel is part of highlighted TopN / TopN対象か
 * @property {number} normalizedDensity - Density normalised to 0-1 / 正規化密度(0〜1)
 * @property {HeatboxStatistics} statistics - Latest statistics snapshot / 最新統計情報
 * @property {HeatboxAdaptiveParams|null} [adaptiveParams] - Adaptive parameters / 適応パラメータ
 */

/**
 * @typedef {Object} HeatboxOpacityResolverContext
 * @property {HeatboxResolverVoxelInfo} voxel - Voxel information / ボクセル情報
 * @property {boolean} isTopN - Whether voxel is part of highlighted TopN / TopN対象か
 * @property {number} normalizedDensity - Density normalised to 0-1 / 正規化密度(0〜1)
 * @property {HeatboxStatistics} statistics - Latest statistics snapshot / 最新統計情報
 * @property {HeatboxAdaptiveParams|null} [adaptiveParams] - Adaptive parameters / 適応パラメータ
 */

/**
 * @typedef {Object} HeatboxOptions
 * @property {ProfileName} [profile] - Named preset to start from / 推奨プリセット名
 * @property {number} [voxelSize=20] - Voxel size in meters / ボクセルサイズ(メートル)
 * @property {boolean} [autoVoxelSize=false] - Enable auto voxel size estimation / 自動ボクセルサイズ推定
 * @property {('basic'|'occupancy')} [autoVoxelSizeMode='basic'] - Auto voxel mode / 自動ボクセルモード
 * @property {number} [autoVoxelTargetFill=0.6] - Target occupancy ratio for auto mode / 自動モード時の目標充填率
 * @property {number} [maxRenderVoxels=50000] - Max voxels to render / 描画ボクセル上限
 * @property {('density'|'coverage'|'hybrid')} [renderLimitStrategy='density'] - Voxel selection strategy / ボクセル選択戦略
 * @property {number} [minCoverageRatio=0.2] - Minimum coverage ratio for hybrid strategy / ハイブリッド戦略時の最小カバレッジ比率
 * @property {('auto'|number)} [coverageBinsXY='auto'] - Grid bins for coverage strategy / カバレッジ戦略用グリッド分割
 * @property {boolean} [showOutline=true] - Draw voxel outlines / 枠線を描画
 * @property {boolean} [showEmptyVoxels=false] - Show empty voxels / 空ボクセルを描画
 * @property {boolean} [wireframeOnly=false] - Render outlines only / 枠線のみ描画
 * @property {boolean} [heightBased=false] - Scale height by density / 密度に応じて高さを調整
 * @property {number} [outlineWidth=2] - Base outline width / 基本枠線太さ
 * @property {number} [voxelGap=0] - Gap between voxels in meters / ボクセル間ギャップ(メートル)
 * @property {number} [opacity=0.8] - Box opacity / ボックス不透明度
 * @property {number} [emptyOpacity=0.03] - Empty voxel opacity / 空ボクセル不透明度
 * @property {number[]} [minColor=[0,32,255]] - RGB colour for minimum density / 最低密度時のRGB
 * @property {number[]} [maxColor=[255,64,0]] - RGB colour for maximum density / 最高密度時のRGB
 * @property {('custom'|'viridis'|'inferno')} [colorMap='custom'] - Colour map preset / カラーマップ
 * @property {boolean} [diverging=false] - Use diverging colour mode / 発散配色モード
 * @property {number} [divergingPivot=0] - Diverging pivot value / 発散配色のピボット
 * @property {number|null} [highlightTopN=null] - Highlight top N voxels / 上位Nボクセルの強調
 * @property {HeatboxHighlightStyle} [highlightStyle] - Highlight styling / ハイライトスタイル
 * @property {OutlineRenderMode} [outlineRenderMode='standard'] - Outline rendering mode / 枠線描画モード
 * @property {EmulationScope} [emulationScope='off'] - Emulation scope / エミュレーション範囲
 * @property {boolean} [adaptiveOutlines=false] - Enable adaptive outline mode / 適応枠線制御を有効化
 * @property {OutlineWidthPreset} [outlineWidthPreset='medium'] - Outline width preset / 枠線プリセット
 * @property {?function(HeatboxOutlineWidthResolverParams):number} [outlineWidthResolver=null] - Custom outline width resolver / 枠線太さの独自制御
 * @property {?function(HeatboxOpacityResolverContext):number} [outlineOpacityResolver=null] - Custom outline opacity resolver / 枠線透明度の独自制御
 * @property {?function(HeatboxOpacityResolverContext):number} [boxOpacityResolver=null] - Custom box opacity resolver / ボックス透明度の独自制御
 * @property {number} [outlineInset=0] - Inset outline offset in meters / インセット枠線のオフセット
 * @property {('all'|'topn'|'none')} [outlineInsetMode='all'] - Inset outline target / インセット枠線の適用対象
 * @property {boolean} [enableThickFrames=false] - Enable thick frame fill / 厚枠フレーム補完
 * @property {HeatboxAdaptiveParams} [adaptiveParams] - Adaptive control parameters / 適応制御パラメータ
 * @property {boolean} [autoView=false] - Automatically fit camera after render / 描画後に視点を自動調整
 * @property {HeatboxFitViewOptions} [fitViewOptions] - Camera fit options / ビューフィットの設定
 * @property {PerformanceOverlayConfig|null} [performanceOverlay=null] - Performance overlay config / パフォーマンスオーバーレイ設定
 * @property {('manual'|'auto')} [renderBudgetMode='manual'] - Render budget mode / レンダーバジェット制御モード
 * @property {(boolean|Object)} [debug=false] - Debug options (`true` enables verbose logging, object can contain `showBounds`) / デバッグ設定(trueで詳細ログ、オブジェクトの場合は`showBounds`などを指定)
 * @property {Object} [spatialId] - Spatial ID configuration (v0.1.17+) / 空間ID設定(v0.1.17+)
 * @property {boolean} [spatialId.enabled=false] - Enable spatial ID mode / 空間IDモードを有効化
 * @property {('tile-grid'|'voxel-grid')} [spatialId.mode='tile-grid'] - Spatial ID mode / 空間IDモード
 * @property {(number|'auto')} [spatialId.zoom='auto'] - Zoom level or auto / ズームレベルまたは自動
 * @property {('auto'|'manual')} [spatialId.zoomControl='auto'] - Zoom control mode / ズーム制御モード
 * @property {number} [spatialId.zoomTolerancePct=10] - Zoom tolerance percentage / ズーム許容誤差(%)
 * @property {Object} [aggregation] - Layer aggregation configuration (v0.1.18+) / レイヤ別集約設定(v0.1.18+)
 * @property {boolean} [aggregation.enabled=false] - Enable layer aggregation / レイヤ別集約を有効化
 * @property {string|null} [aggregation.byProperty=null] - Entity property key to use as layer key / レイヤキーとして使用するエンティティプロパティ
 * @property {?function(entity):string} [aggregation.keyResolver=null] - Custom layer key resolver function (takes precedence over byProperty) / カスタムレイヤキー解決関数(byPropertyより優先)
 * @property {boolean} [aggregation.showInDescription=true] - Show layer breakdown in voxel description / ボクセル説明文にレイヤ内訳を表示
 * @property {number} [aggregation.topN=10] - Number of top layers to include in statistics / 統計情報に含める上位レイヤ数
 * @property {TemporalOptions|null} [temporal=null] - Temporal playback configuration / 時系列再生の設定
 */

/**
 * Main class of CesiumJS Heatbox.
 * Provides 3D voxel-based heatmap visualization in CesiumJS environments.
 * Refer to {@link HeatboxOptions} for the full option catalogue with defaults.
 *
 * CesiumJS Heatbox メインクラス。
 * CesiumJS 環境で 3D ボクセルベースのヒートマップ可視化を提供します。
 * 利用可能なオプションと既定値は {@link HeatboxOptions} を参照してください。
 */
export class Heatbox {
  /**
   * Constructor.
   * Prepares the renderer, normalises options, and wires core event listeners.
   *
   * 初期化処理ではオプションの正規化とレンダラー生成、必要なイベント購読を行います。
   *
   * @param {Cesium.Viewer} viewer - CesiumJS Viewer instance / CesiumJS Viewer インスタンス
   * @param {HeatboxOptions} [options={}] - Configuration options / 設定オプション
   */
  constructor(viewer, options = {}) {
    if (!isValidViewer(viewer)) {
      throw new Error(ERROR_MESSAGES.INVALID_VIEWER);
    }

    this.viewer = viewer;

    // v0.1.9: Auto Render Budgetの適用
    // Phase 4: Ensure profile and legacy migration are applied before merging defaults
    let userOptions = { ...(options || {}) };
    // Apply profile before merging defaults (defaults <- profile <- user)
    if (userOptions.profile && getProfileNames().includes(userOptions.profile)) {
      userOptions = applyProfile(userOptions.profile, userOptions);
      delete userOptions.profile;
    }
    const mergedOptions = { ...DEFAULT_OPTIONS, ...userOptions };
    this.options = validateAndNormalizeOptions(applyAutoRenderBudget(mergedOptions));

    // ログレベルをオプションに基づいて設定
    Logger.setLogLevel(this.options);
    this.renderer = new VoxelRenderer(this.viewer, this.options);

    this._bounds = null;
    this._grid = null;
    this._voxelData = null;
    this._statistics = null;
    this._spatialIdEdgeCaseMetrics = null;
    this._eventHandler = null;
    this._performanceOverlay = null;
    this._lastRenderTime = null;
    this._overlayLastUpdate = 0;
    this._postRenderListener = null;
    this._prevFrameTimestamp = null;
    this._postRenderListener = null;
    this._prevFrameTimestamp = null;
    this._legend = null;
    this._timeController = null;
    this._timeControllerSignature = null;

    this._initializeEventListeners();

    // v0.1.12: Initialize performance overlay if enabled
    if (this.options.performanceOverlay && this.options.performanceOverlay.enabled) {
      this._initializePerformanceOverlay();
    }

    // v1.2.0: Initialize TimeController if temporal mode is enabled
    if (this.options.temporal && this.options.temporal.enabled) {
      this._initializeTimeController();
    }
  }

  /**
   * Get effective normalized options snapshot.
   * 正規化済みオプションのスナップショットを取得します。
   * @returns {HeatboxOptions} options snapshot / オプションのスナップショット
   */
  getEffectiveOptions() {
    try {
      return JSON.parse(JSON.stringify(this.options));
    } catch (_) {
      // Fallback shallow copy
      return { ...this.options };
    }
  }

  /**
   * Get list of available configuration profiles
   * 利用可能な設定プロファイルの一覧を取得
   * 
   * @returns {ProfileName[]} Array of profile names / プロファイル名の配列
   * @static
   * @since 0.1.12
   */
  static listProfiles() {
    return getProfileNames();
  }

  /**
   * Get configuration profile details
   * 設定プロファイルの詳細を取得
   * 
   * @param {string} profileName - Profile name / プロファイル名
   * Returned object shares the same keys as {@link HeatboxOptions} plus an optional `description`.
   * 戻り値は {@link HeatboxOptions} と同じキーに加えて `description` フィールドを含みます。
   * @returns {Object|null} Profile configuration with description / 説明付きプロファイル設定
   * @static
   * @since 0.1.12
   */
  static getProfileDetails(profileName) {
    return getProfile(profileName);
  }

  /**
   * Initialize performance overlay
   * パフォーマンスオーバーレイを初期化
   * @private
   * @since 0.1.12
   */
  _initializePerformanceOverlay() {
    if (typeof window === 'undefined') {
      Logger.warn('Performance overlay requires browser environment');
      return;
    }

    const overlayOptions = {
      position: 'top-right',
      fpsAveragingWindowMs: 1000,
      autoUpdate: true,
      updateIntervalMs: 500,
      ...this.options.performanceOverlay
    };

    this._performanceOverlay = new PerformanceOverlay(overlayOptions);

    // Show immediately if configured
    if (overlayOptions.autoShow) {
      this._performanceOverlay.show();
    }

    Logger.debug('Performance overlay initialized');

    // Hook postRender to provide real-time updates with low overhead
    this._hookPerformanceOverlayUpdates();
  }

  /**
   * Initialize TimeController
   * TimeController を初期化します
   * @private
   * @since 1.2.0
   */
  _initializeTimeController() {
    try {
      this._timeController = new TimeController(this.viewer, this, this.options.temporal);
      this._timeController.activate();
      this._timeControllerSignature = this._serializeTemporalOptions(this.options.temporal);
      Logger.info('TimeController initialized and activated');
    } catch (error) {
      this._timeController = null;
      this._timeControllerSignature = null;
      Logger.error('Failed to initialize TimeController:', error);
    }
  }

  /**
   * Deactivate and dispose TimeController instance safely.
   * TimeController を安全に停止・破棄します。
   * @private
   */
  _teardownTimeController() {
    if (!this._timeController) {
      return;
    }

    try {
      this._timeController.deactivate();
    } catch (error) {
      Logger.debug('timeController deactivate failed (non-fatal)', error);
    }

    this._timeController = null;
    this._timeControllerSignature = null;
  }

  /**
   * Synchronize TimeController when temporal options change.
   * temporalオプション変更時にTimeControllerを同期します。
   * @param {Object|null|undefined} previousTemporal
   * @param {Object|null|undefined} nextTemporal
   * @param {boolean} wasTemporalUpdated
   * @private
   */
  _syncTimeController(previousTemporal, nextTemporal, wasTemporalUpdated) {
    const prevEnabled = !!(previousTemporal && previousTemporal.enabled);
    const nextEnabled = !!(nextTemporal && nextTemporal.enabled);

    if (!prevEnabled && nextEnabled) {
      this._initializeTimeController();
      return;
    }

    if (prevEnabled && !nextEnabled) {
      this._teardownTimeController();
      return;
    }

    if (nextEnabled && wasTemporalUpdated) {
      const nextSignature = this._serializeTemporalOptions(nextTemporal);
      if (this._timeControllerSignature !== nextSignature) {
        this._teardownTimeController();
        this._initializeTimeController();
      }
    }
  }

  /**
   * Create a stable signature for temporal options to detect config changes.
   * temporalオプションの変更検知用シグネチャを生成します。
   * @param {Object|null|undefined} temporalOptions
   * @returns {string|null}
   * @private
   */
  _serializeTemporalOptions(temporalOptions) {
    if (!temporalOptions) {
      return null;
    }

    try {
      return JSON.stringify(temporalOptions, (key, value) => {
        if (typeof value === 'function') {
          return `[Function:${value.name || 'anonymous'}]`;
        }
        return value;
      });
    } catch (error) {
      Logger.debug('Failed to serialize temporal options for comparison', error);
      return null;
    }
  }

  /**
   * Toggle performance overlay visibility
   * パフォーマンスオーバーレイの表示/非表示切り替え
   * 
   * @returns {boolean} New visibility state / 新しい表示状態
   * @since 0.1.12
   */
  togglePerformanceOverlay() {
    if (!this._performanceOverlay) {
      Logger.warn('Performance overlay not initialized. Set performanceOverlay.enabled: true in options.');
      return false;
    }

    this._performanceOverlay.toggle();
    return this._performanceOverlay.isVisible;
  }

  /**
   * Show performance overlay
   * パフォーマンスオーバーレイを表示
   * @since 0.1.12
   */
  showPerformanceOverlay() {
    if (this._performanceOverlay) {
      this._performanceOverlay.show();
    }
  }

  /**
   * Hide performance overlay
   * パフォーマンスオーバーレイを非表示
   * @since 0.1.12
   */
  hidePerformanceOverlay() {
    if (this._performanceOverlay) {
      this._performanceOverlay.hide();
    }
  }

  /**
   * Enable or disable performance overlay at runtime.
   * 実行時にパフォーマンスオーバーレイを有効/無効化します。
   * @param {boolean} enabled - true to enable, false to disable
   * @param {PerformanceOverlayConfig} [options] - Optional overlay options to apply / 追加設定
   * @returns {boolean} Current enabled state / 現在の有効状態
   * @since 0.1.12
   */
  setPerformanceOverlayEnabled(enabled, options = {}) {
    if (enabled) {
      if (!this._performanceOverlay) {
        // Initialize with given options overriding existing config
        this.options.performanceOverlay = { enabled: true, ...(this.options.performanceOverlay || {}), ...options };
        this._initializePerformanceOverlay();
      } else {
        // Apply options if provided
        if (options && Object.keys(options).length > 0) {
          this._performanceOverlay.options = { ...this._performanceOverlay.options, ...options };
        }
        this._performanceOverlay.show();
      }
      return true;
    }

    // Disable and cleanup listener
    if (this._performanceOverlay) {
      this._performanceOverlay.hide();
    }
    if (this._postRenderListener) {
      try { this.viewer.scene.postRender.removeEventListener(this._postRenderListener); } catch (_) { Logger.debug('postRender listener removal failed (non-fatal)'); }
      this._postRenderListener = null;
    }
    return false;
  }

  /**
   * Estimate memory usage for performance monitoring
   * パフォーマンス監視用のメモリ使用量推定
   * @private
   * @since 0.1.12
   */
  _estimateMemoryUsage() {
    try {
      // Rough estimation based on rendered entities and data
      const entityCount = (this.renderer?.geometryRenderer?.entities?.length)
        || (this.renderer?.voxelEntities?.length) || 0;
      let voxelDataSize = 0;
      if (this._voxelData) {
        if (this._voxelData instanceof Map) {
          voxelDataSize = this._voxelData.size;
        } else if (typeof this._voxelData.size === 'number') {
          voxelDataSize = this._voxelData.size;
        } else if (Array.isArray(this._voxelData)) {
          voxelDataSize = this._voxelData.length;
        } else if (typeof this._voxelData === 'object') {
          voxelDataSize = Object.keys(this._voxelData).length;
        }
      }

      // Estimate: ~1KB per entity + ~100B per voxel data entry
      const estimated = (entityCount * 1024 + voxelDataSize * 100) / (1024 * 1024);
      return Math.max(0.1, estimated);
    } catch (_error) {
      return 0;
    }
  }

  /**
   * Set heatmap data and render.
   * Calculates bounds, prepares the voxel grid, runs classification, and finally renders.
   *
   * ヒートマップデータを設定し、境界計算→ボクセル分類→描画の順で処理します。
   *
   * @param {Cesium.Entity[]} entities - Target entities array / 対象エンティティ配列
   * @returns {Promise<void>} Resolves when rendering is completed / 描画完了時に解決
   */
  async setData(entities) {
    if (!isValidEntities(entities)) {
      this.clear();
      return;
    }

    try {
      Logger.debug('Heatbox.setData - 処理開始:', entities.length, '個のエンティティ');

      // 1. 境界計算
      Logger.debug('Step 1: 境界計算');
      this._bounds = CoordinateTransformer.calculateBounds(entities);
      if (!this._bounds) {
        Logger.error('境界計算に失敗');
        this.clear();
        return;
      }
      Logger.debug('境界計算完了:', this._bounds);

      // v0.1.4+v0.1.9: 自動ボクセルサイズ調整(占有率ベース対応)
      let finalVoxelSize = this.options.voxelSize || DEFAULT_OPTIONS.voxelSize;
      let autoAdjustmentInfo = null;

      if (this.options.autoVoxelSize && !this.options.voxelSize) {
        try {
          Logger.debug('自動ボクセルサイズ調整開始');

          // v0.1.9: 占有率ベースの計算オプション
          const sizeOptions = {
            autoVoxelSizeMode: this.options.autoVoxelSizeMode,
            autoVoxelTargetFill: this.options.autoVoxelTargetFill,
            maxRenderVoxels: this.options.maxRenderVoxels
          };

          const estimatedSize = estimateInitialVoxelSize(this._bounds, entities.length, sizeOptions);
          const tempGrid = VoxelGrid.createGrid(this._bounds, estimatedSize);
          const validation = validateVoxelCount(tempGrid.totalVoxels, estimatedSize);

          if (!validation.valid && validation.recommendedSize) {
            finalVoxelSize = validation.recommendedSize;
            autoAdjustmentInfo = {
              enabled: true,
              mode: this.options.autoVoxelSizeMode,
              originalSize: estimatedSize,
              finalSize: finalVoxelSize,
              adjusted: true,
              reason: `Performance limit exceeded: ${tempGrid.totalVoxels} > ${PERFORMANCE_LIMITS.maxVoxels}`
            };
            Logger.info(`Auto-adjusted voxelSize: ${estimatedSize}m → ${finalVoxelSize}m (${tempGrid.totalVoxels} voxels)`);
          } else {
            finalVoxelSize = estimatedSize;
            autoAdjustmentInfo = {
              enabled: true,
              mode: this.options.autoVoxelSizeMode,
              originalSize: estimatedSize,
              finalSize: finalVoxelSize,
              adjusted: false,
              reason: null
            };
            Logger.info(`Auto-determined voxelSize: ${finalVoxelSize}m`);
          }
        } catch (error) {
          Logger.warn('Auto voxel size adjustment failed, using default:', error);
          finalVoxelSize = DEFAULT_OPTIONS.voxelSize;
          autoAdjustmentInfo = {
            enabled: true,
            adjusted: false,
            reason: 'Estimation failed, using default size',
            originalSize: null,
            finalSize: finalVoxelSize
          };
        }
      }

      // 2. グリッド生成(最終的なボクセルサイズを使用)
      Logger.debug('Step 2: グリッド生成 (サイズ:', finalVoxelSize, 'm)');
      this._grid = VoxelGrid.createGrid(this._bounds, finalVoxelSize);
      Logger.debug('グリッド生成完了:', this._grid);

      // 3. エンティティ分類(v0.1.17: 空間IDサポート)
      Logger.debug('Step 3: エンティティ分類');
      // Pass options with voxelSize for spatial ID auto zoom calculation
      const classificationOptions = { ...this.options, voxelSize: finalVoxelSize };
      this._voxelData = await DataProcessor.classifyEntitiesIntoVoxels(entities, this._bounds, this._grid, classificationOptions);
      Logger.debug('エンティティ分類完了:', this._voxelData.size, '個のボクセル');

      // 4. 統計計算
      Logger.debug('Step 4: 統計計算');
      this._statistics = DataProcessor.calculateStatistics(this._voxelData, this._grid, this.options);
      Logger.debug('統計情報:', this._statistics);

      // 統計情報に自動調整情報を追加
      if (autoAdjustmentInfo) {
        this._statistics.autoAdjusted = autoAdjustmentInfo.adjusted;
        this._statistics.originalVoxelSize = autoAdjustmentInfo.originalSize;
        this._statistics.finalVoxelSize = autoAdjustmentInfo.finalSize;
        this._statistics.adjustmentReason = autoAdjustmentInfo.reason;
      }

      // v0.1.17: 空間ID情報を統計に追加
      if (classificationOptions.spatialId?.enabled) {
        this._statistics.spatialIdEnabled = true;
        this._statistics.spatialIdMode = classificationOptions.spatialId.mode;
        this._statistics.spatialIdProvider = classificationOptions._spatialIdProvider || null;
        this._statistics.spatialIdZoom = classificationOptions._resolvedZoom || null;
        this._statistics.zoomControl = classificationOptions.spatialId.zoomControl;
      } else {
        this._statistics.spatialIdEnabled = false;
      }

      // 5. 描画(レンダリング時間の計測)
      Logger.debug('Step 5: 描画');
      const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
      const renderedVoxelCount = this.renderer.render(this._voxelData, this._bounds, this._grid, this._statistics);
      const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
      this._lastRenderTime = Math.max(0, t1 - t0);

      // 統計情報に実際の描画数を反映
      this._statistics.renderedVoxels = renderedVoxelCount;
      this._statistics.renderTimeMs = this._lastRenderTime;
      Logger.info('描画完了 - 実際の描画数:', renderedVoxelCount);

      // v0.1.9: 自動視点調整
      if (this.options.autoView) {
        try {
          Logger.debug('Auto view adjustment triggered');
          await this.fitView();
          Logger.debug('Auto view adjustment completed');
        } catch (error) {
          Logger.warn('Auto view adjustment failed:', error);
          // 自動視点調整の失敗は致命的エラーとしない
        }
      }

      Logger.debug('Heatbox.setData - 処理完了');

      // Update overlay immediately after render if available
      if (this._performanceOverlay && this._performanceOverlay.isVisible) {
        const stats = this.getStatistics() || {};
        stats.renderTimeMs = this._lastRenderTime;
        stats.memoryUsageMB = this._estimateMemoryUsage();
        this._performanceOverlay.update(stats, undefined);
      }

    } catch (error) {
      Logger.error('ヒートマップ作成エラー:', error);
      this.clear();
      throw error;
    }
  }

  /**
   * Create heatmap from entities (async).
   * エンティティからヒートマップを作成(非同期 API)。
   * Resolves with the statistics snapshot calculated by {@link getStatistics}.
   * 描画完了後に {@link getStatistics} と同じ統計スナップショットを返します。
   * @param {Cesium.Entity[]} entities - Target entities array / 対象エンティティ配列
   * @returns {Promise<HeatboxStatistics>} Statistics info / 統計情報
   */
  async createFromEntities(entities) {
    if (!isValidEntities(entities)) {
      throw new Error(ERROR_MESSAGES.NO_ENTITIES);
    }
    await this.setData(entities);
    return this.getStatistics();
  }

  /**
   * Toggle visibility.
   * 表示/非表示を切り替えます。
   * @param {boolean} show - true to show / 表示する場合は true
   */
  setVisible(show) {
    this.renderer.setVisible(show);
  }

  /**
   * Clear the heatmap and internal state.
   * ヒートマップと内部状態をクリアします。
   */
  clear() {
    this.renderer.clear();
    this._bounds = null;
    this._grid = null;
    this._voxelData = null;
    this._statistics = null;
  }

  /**
   * Destroy the instance and release event listeners.
   * インスタンスを破棄し、イベントリスナーを解放します。
   */
  destroy() {
    this.clear();
    if (this._eventHandler && !this._eventHandler.isDestroyed()) {
      this._eventHandler.destroy();
    }
    // Remove overlay listener and destroy overlay
    if (this._postRenderListener) {
      try { this.viewer.scene.postRender.removeEventListener(this._postRenderListener); } catch (_) { Logger.debug('postRender listener removal failed (non-fatal)'); }
      this._postRenderListener = null;
    }
    if (this._performanceOverlay) {
      try { this._performanceOverlay.destroy(); } catch (_) { Logger.debug('overlay destroy failed (non-fatal)'); }
      this._performanceOverlay = null;
    }
    if (this._legend) {
      try { this._legend.destroy(); } catch (_) { Logger.debug('legend destroy failed (non-fatal)'); }
      this._legend = null;
    }
    this._teardownTimeController();
    this._eventHandler = null;
  }

  /**
   * Alias for destroy() to match examples and tests.
   * 互換性のための別名。destroy() を呼び出します。
   */
  dispose() {
    this.destroy();
  }

  /**
   * Get current options.
   * 現在のオプションを取得します。
   * @returns {HeatboxOptions} Options / オプション
   */
  getOptions() {
    return { ...this.options };
  }

  /**
   * Update options and re-render if applicable.
   * オプションを更新し、必要に応じて再描画します。
   * @param {HeatboxOptions} newOptions - New options (partial allowed) / 新しいオプション(部分指定可)
   */
  updateOptions(newOptions) {
    const previousTemporal = this.options ? this.options.temporal : undefined;
    const temporalUpdated = newOptions ? Object.prototype.hasOwnProperty.call(newOptions, 'temporal') : false;

    this.options = validateAndNormalizeOptions({ ...this.options, ...newOptions });
    this.renderer.options = this.options;

    if (this.renderer.adaptiveController && typeof this.renderer.adaptiveController.updateOptions === 'function') {
      this.renderer.adaptiveController.updateOptions(this.options);
    }
    if (this.renderer.geometryRenderer && typeof this.renderer.geometryRenderer.updateOptions === 'function') {
      this.renderer.geometryRenderer.updateOptions(this.options);
    }

    // 既存のヒートマップがある場合は再描画
    if (this._voxelData) {
      const renderedVoxelCount = this.renderer.render(this._voxelData, this._bounds, this._grid, this._statistics);
      // 統計情報を更新
      this._statistics.renderedVoxels = renderedVoxelCount;
    }

    this._syncTimeController(previousTemporal, this.options.temporal, temporalUpdated);
  }

  /**
   * Initialize internal event listeners.
   * 内部のイベントリスナーを初期化します。
   * @private
   */
  _initializeEventListeners() {
    this._eventHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);

    // クリックイベントでInfoBoxを更新
    this._eventHandler.setInputAction(movement => {
      const pickedObject = this.viewer.scene.pick(movement.position);
      if (Cesium.defined(pickedObject) && pickedObject.id &&
        pickedObject.id.properties &&
        pickedObject.id.properties.type === 'voxel') {
        // プロパティからキー値を取得
        const voxelKey = pickedObject.id.properties.key;
        const voxelInfo = {
          x: pickedObject.id.properties.x,
          y: pickedObject.id.properties.y,
          z: pickedObject.id.properties.z,
          count: pickedObject.id.properties.count
        };

        // InfoBoxに表示するためのダミーエンティティを作成
        const dummyEntity = new Cesium.Entity({
          id: `voxel-${voxelKey}`,
          description: this.renderer.createVoxelDescription(voxelInfo, voxelKey)
        });
        this.viewer.selectedEntity = dummyEntity;
      }
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  }

  /**
   * Get statistics information.
   * 統計情報を取得します(未作成の場合は null)。
   * @returns {HeatboxStatistics|null} Statistics or null / 統計情報 または null
   */
  getStatistics() {
    if (!this._statistics) {
      return null;
    }

    // 基本統計情報
    const stats = { ...this._statistics };

    // v0.1.9: 選択戦略統計を追加
    const selectionStats = this.renderer.getSelectionStats();
    if (selectionStats) {
      stats.selectionStrategy = selectionStats.strategy;
      stats.clippedNonEmpty = selectionStats.clippedNonEmpty;
      stats.coverageRatio = selectionStats.coverageRatio ?? 0;
    }

    // v0.1.9: Auto Render Budget統計を追加
    if (this.options._autoRenderBudget) {
      stats.renderBudgetTier = this.options._autoRenderBudget.tier;
      stats.autoMaxRenderVoxels = this.options._autoRenderBudget.autoMaxRenderVoxels;
    }

    // v0.1.9: occupancy ratio (rendered / budget) for diagnostics
    if (typeof this.options.maxRenderVoxels === 'number' && this.options.maxRenderVoxels > 0) {
      stats.occupancyRatio = Math.min(1, Math.max(0, (stats.renderedVoxels || 0) / this.options.maxRenderVoxels));
    } else {
      stats.occupancyRatio = null;
    }

    // v0.1.19: Spatial ID statistics sub-object (ADR-0015)
    const spatialIdOptions = this.options && this.options.spatialId ? this.options.spatialId : {};
    const spatialIdEnabled = Boolean(stats.spatialIdEnabled);
    const edgeCaseMetrics = this._spatialIdEdgeCaseMetrics ?? null;
    stats.spatialId = {
      enabled: spatialIdEnabled,
      provider: spatialIdEnabled ? (stats.spatialIdProvider ?? null) : null,
      zoom: spatialIdEnabled ? (stats.spatialIdZoom ?? null) : null,
      zoomControl: stats.zoomControl ?? spatialIdOptions.zoomControl ?? null,
      edgeCaseMetrics
    };

    // v0.1.18: Layer aggregation statistics (ADR-0014)
    if (this.options.aggregation?.enabled && this._voxelData) {
      const globalLayerCounts = new Map();

      // Aggregate across all voxels / 全ボクセルを集約
      for (const voxelInfo of this._voxelData.values()) {
        if (voxelInfo.layerStats) {
          for (const [layerKey, count] of voxelInfo.layerStats) {
            globalLayerCounts.set(
              layerKey,
              (globalLayerCounts.get(layerKey) || 0) + count
            );
          }
        }
      }

      // Top N layers (configurable via options.aggregation.topN) / 上位N個のレイヤ(options.aggregation.topNで設定可能)
      const topN = this.options.aggregation?.topN ?? 10;
      const sorted = Array.from(globalLayerCounts.entries())
        .sort((a, b) => b[1] - a[1])  // Sort by count descending / カウント降順でソート
        .slice(0, topN);

      stats.layers = sorted.map(([key, total]) => ({ key, total }));

      Logger.debug(`[aggregation] Aggregated ${globalLayerCounts.size} unique layers, returning top ${stats.layers.length}`);
    }

    return stats;
  }

  /**
   * Get bounds info if available.
   * 境界情報を取得します(未作成の場合は null)。
   * @returns {HeatboxBounds|null} Bounds or null / 境界情報 または null
   */
  getBounds() {
    return this._bounds;
  }

  /**
   * Get debug information.
   * デバッグ情報を取得します。
   * @returns {HeatboxDebugInfo} Debug info / デバッグ情報
   */
  getDebugInfo() {
    const baseInfo = {
      options: { ...this.options },
      bounds: this._bounds,
      grid: this._grid,
      statistics: this._statistics
    };

    // v0.1.4: 自動調整情報を追加
    if (this.options.autoVoxelSize) {
      baseInfo.autoVoxelSizeInfo = {
        enabled: this.options.autoVoxelSize,
        originalSize: this._statistics?.originalVoxelSize,
        finalSize: this._statistics?.finalVoxelSize,
        adjusted: this._statistics?.autoAdjusted || false,
        reason: this._statistics?.adjustmentReason,
        dataRange: this._bounds ? calculateDataRange(this._bounds) : null,
        estimatedDensity: this._bounds && this._statistics ?
          this._statistics.totalEntities / (calculateDataRange(this._bounds).x * calculateDataRange(this._bounds).y * calculateDataRange(this._bounds).z) : null
      };
    }

    return baseInfo;
  }

  /**
   * Hook viewer postRender to feed overlay with periodic updates.
   * viewer の postRender にフックし、オーバーレイへ定期更新を供給します。
   * @private
   */
  _hookPerformanceOverlayUpdates() {
    if (!this._performanceOverlay || this._postRenderListener) return;

    const interval = this._performanceOverlay.options?.updateIntervalMs ?? 500;
    this._postRenderListener = () => {
      // Skip if overlay not visible
      if (!this._performanceOverlay || !this._performanceOverlay.isVisible) return;

      const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();

      // Calculate frame time from previous timestamp
      let frameTime;
      if (this._prevFrameTimestamp != null) {
        frameTime = Math.max(0, now - this._prevFrameTimestamp);
      }
      this._prevFrameTimestamp = now;

      // Throttle updates
      if (now - this._overlayLastUpdate < interval) return;
      this._overlayLastUpdate = now;

      const stats = this.getStatistics() || {};
      // Attach last render time and estimated memory usage
      if (this._lastRenderTime != null) stats.renderTimeMs = this._lastRenderTime;
      stats.memoryUsageMB = this._estimateMemoryUsage();

      try {
        this._performanceOverlay.update(stats, frameTime);
      } catch (_e) {
        // Ignore overlay update errors to avoid impacting render loop
      }
    };

    try {
      this.viewer.scene.postRender.addEventListener(this._postRenderListener);
    } catch (_e) {
      // No-op if event registration fails
    }
  }

  /**
   * Fit view to data bounds with smart camera positioning.
   * データ境界にスマートなカメラ位置でビューをフィットします。
   *
   * 実装メモ(v0.1.12):
   * - 描画とカメラ移動の競合を避けるため、`viewer.scene.postRender` で1回だけ実行します。
   * - 矩形境界(経緯度)から `Cesium.Rectangle` → `Cesium.BoundingSphere` を生成し、
   *   `camera.flyToBoundingSphere` + `HeadingPitchRange` で安定的にズームします。
   * - 俯角は安全範囲にクランプ(既定: -35°, 範囲: [-85°, -10°])。
   * - 失敗時は `viewer.zoomTo(viewer.entities)` へフォールバックします。
   *
   * @param {HeatboxBounds|null} [bounds=null] - Target bounds(省略時は現在のデータ境界)
   * @param {HeatboxFitViewOptions} [options={}] - Fit view options / フィットビュー設定
   * @returns {Promise<void>} カメラ移動完了時に解決する Promise
   * @example
   * // データを適用後、安定的にビューフィット
   * await heatbox.setData(viewer.entities.values);
   * await heatbox.fitView(null, { headingDegrees: 0, pitchDegrees: -35, paddingPercent: 0.1 });
   */
  async fitView(bounds = null, options = {}) {
    try {
      const targetBounds = bounds || this._bounds;
      if (!targetBounds) {
        Logger.warn('No bounds available for fitView');
        return;
      }

      // 境界の妥当性チェック
      if (!this._isValidBounds(targetBounds)) {
        Logger.warn('Invalid bounds provided to fitView:', targetBounds);
        return;
      }

      const fitOptions = {
        ...this.options.fitViewOptions,
        ...options
      };

      Logger.debug('fitView called with bounds:', targetBounds, 'options:', fitOptions);

      // postRenderで一回だけ実行して描画との競合を回避
      const safeOptions = { ...fitOptions };
      if (!Number.isFinite(safeOptions.pitchDegrees)) safeOptions.pitchDegrees = -35;
      if (!Number.isFinite(safeOptions.headingDegrees)) safeOptions.headingDegrees = 0;

      return await new Promise((resolve) => {
        let fired = false;
        const handler = async () => {
          if (fired) return;
          fired = true;
          try {
            await this._fitByBoundingSphere(targetBounds, safeOptions);
          } catch (e) {
            Logger.warn('fitView (postRender) failed, trying fallback:', e);
            try {
              await this.viewer.zoomTo(this.viewer.entities);
            } catch (zoomErr) {
              Logger.warn('zoomTo fallback failed:', zoomErr);
            }
          } finally {
            try {
              this.viewer.scene.postRender.removeEventListener(handler);
            } catch (remErr) {
              Logger.debug('postRender removeEventListener failed (non-fatal):', remErr);
            }
            resolve();
          }
        };
        try {
          this.viewer.scene.postRender.addEventListener(handler);
        } catch (e) {
          Logger.warn('postRender addEventListener failed:', e);
          resolve();
        }
      });

    } catch (error) {
      Logger.error('fitView failed:', error);
      throw error;
    }
  }

  /**
   * Fit by bounding sphere derived from rectangle bounds
   * @private
   */
  async _fitByBoundingSphere(bounds, fitOptions) {
    const rect = Cesium.Rectangle.fromDegrees(bounds.minLon, bounds.minLat, bounds.maxLon, bounds.maxLat);
    const bs = Cesium.BoundingSphere.fromRectangle3D(rect, Cesium.Ellipsoid.WGS84, Math.max(0, bounds.minAlt || 0));
    const heading = Cesium.Math.toRadians(fitOptions.headingDegrees ?? 0);
    const pitchDeg = Math.max(-85, Math.min(-10, fitOptions.pitchDegrees ?? -35));
    const pitch = Cesium.Math.toRadians(pitchDeg);
    const range = Math.max(bs.radius * 2.2, 1000.0);
    await this.viewer.camera.flyToBoundingSphere(bs, {
      duration: 1.2,
      offset: new Cesium.HeadingPitchRange(heading, pitch, range)
    });
  }

  /**
   * Validate bounds object.
   * 境界オブジェクトの妥当性をチェックします。
   * @param {Object} bounds - Bounds to validate / 検証する境界
   * @returns {boolean} True if valid / 有効な場合true
   * @private
   */
  _isValidBounds(bounds) {
    return bounds &&
      typeof bounds.minLon === 'number' && !isNaN(bounds.minLon) &&
      typeof bounds.maxLon === 'number' && !isNaN(bounds.maxLon) &&
      typeof bounds.minLat === 'number' && !isNaN(bounds.minLat) &&
      typeof bounds.maxLat === 'number' && !isNaN(bounds.maxLat) &&
      typeof bounds.minAlt === 'number' && !isNaN(bounds.minAlt) &&
      typeof bounds.maxAlt === 'number' && !isNaN(bounds.maxAlt) &&
      bounds.minLon <= bounds.maxLon &&
      bounds.minLat <= bounds.maxLat &&
      bounds.minAlt <= bounds.maxAlt;
  }

  /**
   * Handle minimal data range case.
   * 極小データ範囲の場合の処理
   * @param {number} centerLon - Center longitude / 中心経度
   * @param {number} centerLat - Center latitude / 中心緯度
   * @param {number} centerAlt - Center altitude / 中心高度
   * @param {Object} fitOptions - Fit options / フィットオプション
   * @returns {Promise} Camera movement promise / カメラ移動Promise
   * @private
   */
  async _handleMinimalDataRange(centerLon, centerLat, centerAlt, fitOptions) {
    Logger.debug('Handling minimal data range');

    const destination = Cesium.Cartesian3.fromDegrees(centerLon, centerLat, centerAlt + 2000);
    const heading = Cesium.Math.toRadians(fitOptions.headingDegrees || fitOptions.heading);
    const pitch = Cesium.Math.toRadians(fitOptions.pitchDegrees || fitOptions.pitch);

    return this.viewer.camera.flyTo({
      destination,
      orientation: { heading, pitch, roll: 0 },
      duration: 1.5
    });
  }

  /**
   * Handle large data range case.
   * 極大データ範囲の場合の処理
   * @param {Object} bounds - Target bounds / 対象境界
   * @param {Object} fitOptions - Fit options / フィットオプション
   * @returns {Promise} Camera movement promise / カメラ移動Promise
   * @private
   */
  async _handleLargeDataRange(bounds, fitOptions) {
    Logger.debug('Handling large data range with bounding sphere');

    const centerLon = (bounds.minLon + bounds.maxLon) / 2;
    const centerLat = (bounds.minLat + bounds.maxLat) / 2;
    const centerAlt = (bounds.minAlt + bounds.maxAlt) / 2;

    const dataRange = calculateDataRange(bounds);
    const maxRange = Math.max(dataRange.x, dataRange.y, dataRange.z);

    const boundingSphere = new Cesium.BoundingSphere(
      Cesium.Cartesian3.fromDegrees(centerLon, centerLat, centerAlt),
      maxRange / 2
    );

    const heading = Cesium.Math.toRadians(fitOptions.headingDegrees || fitOptions.heading);
    const pitch = Cesium.Math.toRadians(fitOptions.pitchDegrees || fitOptions.pitch);

    return this.viewer.camera.flyToBoundingSphere(boundingSphere, {
      duration: 2.5,
      offset: new Cesium.HeadingPitchRange(heading, pitch, 0)
    });
  }

  /**
   * Calculate optimal camera height.
   * 最適なカメラ高度を計算します。
   * @param {number} maxRange - Maximum data range / 最大データ範囲
   * @param {number} paddingMeters - Padding in meters / パディング(メートル)
   * @param {Object} fitOptions - Fit options / フィットオプション
   * @returns {number} Optimal camera height / 最適なカメラ高度
   * @private
   */
  _calculateOptimalCameraHeight(maxRange, paddingMeters, fitOptions) {
    if (fitOptions.altitudeStrategy !== 'auto') {
      return fitOptions.altitude || 5000;
    }

    try {
      const pitch = Cesium.Math.toRadians(fitOptions.pitchDegrees || fitOptions.pitch);
      const fov = this.viewer.camera.frustum.fovy || Cesium.Math.toRadians(60);

      // 幾何学的計算: データがフレームに収まる高度を計算
      const adjustedRange = maxRange + paddingMeters;
      const baseCameraHeight = adjustedRange / (2 * Math.tan(fov / 2));

      // ピッチ補正(斜め視点での見え方調整)
      const absPitch = Math.abs(pitch);
      const pitchFactor = Math.max(0.5, Math.sin(Math.PI / 2 - absPitch) + 0.3);
      let cameraHeight = baseCameraHeight * pitchFactor;

      // アスペクト比補正(極端に細長いデータの場合)
      const aspectRatio = maxRange / Math.min(maxRange, 100);
      if (aspectRatio > 5) {
        cameraHeight *= Math.log10(aspectRatio) + 1;
      }

      // 制限値適用(データ範囲に基づく適応的制限)
      const minHeight = Math.max(500, maxRange * 0.1);
      const maxHeight = Math.min(100000, maxRange * 10);
      cameraHeight = Math.max(minHeight, Math.min(maxHeight, cameraHeight));

      Logger.debug(`Camera height calculated: ${cameraHeight.toFixed(0)}m (range: ${maxRange.toFixed(0)}m, pitch: ${fitOptions.pitchDegrees || fitOptions.pitch}°)`);
      return cameraHeight;

    } catch (error) {
      Logger.warn('Camera height calculation failed, using fallback:', error);
      return Math.max(2000, maxRange * 2);
    }
  }

  /**
   * Execute camera movement.
   * カメラ移動を実行します。
   * @param {number} centerLon - Center longitude / 中心経度
   * @param {number} centerLat - Center latitude / 中心緯度
   * @param {number} centerAlt - Center altitude / 中心高度
   * @param {number} cameraHeight - Camera height / カメラ高度
   * @param {Object} fitOptions - Fit options / フィットオプション
   * @param {number} maxRange - Maximum range / 最大範囲
   * @param {number} paddingMeters - Padding meters / パディング(メートル)
   * @returns {Promise} Camera movement promise / カメラ移動Promise
   * @private
   */
  async _executeCameraMovement(centerLon, centerLat, centerAlt, cameraHeight, fitOptions, maxRange, paddingMeters) {
    try {
      // 目標カメラ位置
      const destination = Cesium.Cartesian3.fromDegrees(
        centerLon,
        centerLat,
        centerAlt + cameraHeight
      );

      // カメラの向き設定
      const heading = Cesium.Math.toRadians(fitOptions.headingDegrees || fitOptions.heading);
      const pitch = Cesium.Math.toRadians(fitOptions.pitchDegrees || fitOptions.pitch);
      const roll = 0;

      const orientation = {
        heading,
        pitch,
        roll
      };

      Logger.debug(`Camera target: position=${centerLon.toFixed(6)},${centerLat.toFixed(6)},${(centerAlt + cameraHeight).toFixed(0)}, heading=${fitOptions.headingDegrees || fitOptions.heading}°, pitch=${fitOptions.pitchDegrees || fitOptions.pitch}°`);

      // 距離に応じた移動時間の調整
      const duration = Math.max(1.0, Math.min(3.0, Math.log10(maxRange) * 0.8));

      // プライマリ: flyTo を使用
      const flyPromise = this.viewer.camera.flyTo({
        destination,
        orientation,
        duration,
        complete: () => {
          Logger.debug('fitView camera movement completed');
        },
        cancel: () => {
          Logger.debug('fitView camera movement cancelled');
        }
      });

      // flyToが利用できない場合のフォールバック
      if (!flyPromise) {
        Logger.debug('Using fallback: flyToBoundingSphere');
        const boundingSphere = new Cesium.BoundingSphere(
          Cesium.Cartesian3.fromDegrees(centerLon, centerLat, centerAlt),
          maxRange / 2 + paddingMeters
        );

        await this.viewer.camera.flyToBoundingSphere(boundingSphere, {
          duration,
          offset: new Cesium.HeadingPitchRange(heading, pitch, 0)
        });
      } else {
        await flyPromise;
      }

      Logger.info('fitView completed successfully');

    } catch (error) {
      Logger.error('Camera movement execution failed:', error);
      throw error;
    }
  }

  /**
   * Create a classification legend (Phase 5).
   * 分類用の凡例UIを生成します。
   * @param {HTMLElement} [container] - 追加先コンテナ(省略時はbodyに追加)
   * @returns {HTMLElement|null} legend element / 凡例DOM要素
   */
  createLegend(container = null) {
    if (!this._legend) {
      this._legend = new Legend({ container });
    }
    this._legend.render(this.renderer?._classifier || null, this.options.classification || {});
    return this._legend.container || null;
  }

  /**
   * Update legend contents using latest classifier.
   * 最新の分類状態で凡例を更新します。
   */
  updateLegend() {
    if (this._legend) {
      this._legend.update(this.renderer?._classifier || null, this.options.classification || {});
    }
  }

  /**
   * Destroy legend UI.
   * 凡例UIを破棄します。
   */
  destroyLegend() {
    if (this._legend) {
      this._legend.destroy();
      this._legend = null;
    }
  }

  /**
   * Filter entity array (utility static method).
   * エンティティ配列をフィルタします(ユーティリティ・静的メソッド)。
   * @param {Cesium.Entity[]} entities - Entity array / エンティティ配列
   * @param {Function} predicate - Predicate function / フィルタ関数
   * @returns {Cesium.Entity[]} Filtered array / フィルタ済み配列
   */
  static filterEntities(entities, predicate) {
    if (!Array.isArray(entities) || typeof predicate !== 'function') return [];
    return entities.filter(predicate);
  }
}
⚠️ **GitHub.com Fallback** ⚠️