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

Source: core/selection/VoxelSelector.js

日本語 | English

English

See also: Class: VoxelSelector

/**
 * @fileoverview VoxelSelector - Voxel selection strategies implementation
 * ADR-0009 Phase2: VoxelRenderer責任分離 - ボクセル選択戦略専門クラス
 * 
 * 機能:
 * - 密度選択戦略(density)
 * - カバレッジ選択戦略(coverage - 層化抽出)
 * - ハイブリッド選択戦略(hybrid - 密度+カバレッジ)
 * - TopN強調対応
 * - 統計情報収集
 */

import { Logger } from '../../utils/logger.js';

/**
 * Voxel selection strategy executor.
 * VoxelSelector - ボクセル選択戦略の実装。
 *
 * Single Responsibility: ボクセル選択ロジックのみを担当
 * - 戦略パターンを使用して選択アルゴリズムを切り替え可能
 * - 純粋関数として設計(Cesium依存なし)
 * - エラー時は密度ソート選抜にフォールバック
 */
export class VoxelSelector {
  
  /**
   * Create a VoxelSelector instance.
   * VoxelSelectorインスタンスを作成。
   * 
   * @param {Object} options - Selection options / 選択オプション
   * @param {string} [options.renderLimitStrategy='density'] - Selection strategy / 選択戦略
   * @param {number} [options.highlightTopN=0] - TopN highlight count / TopN強調数
   * @param {number|string} [options.coverageBinsXY='auto'] - Coverage bins / カバレッジビン数
   * @param {number} [options.minCoverageRatio=0.2] - Min coverage ratio for hybrid / ハイブリッド用最小カバレッジ比率
   */
  constructor(options = {}) {
    this.options = {
      renderLimitStrategy: 'density',
      highlightTopN: 0,
      coverageBinsXY: 'auto',
      minCoverageRatio: 0.2,
      ...options
    };
    
    this._lastSelectionStats = null;
    
    Logger.debug(`VoxelSelector initialized with strategy: ${this.options.renderLimitStrategy}`);
  }
  
  /**
   * Select voxels for rendering based on configured strategy.
   * 設定された戦略に基づいてレンダリング用ボクセルを選択。
   * 
   * @param {Array} allVoxels - All voxels to select from / 選択元の全ボクセル
   * @param {number} maxCount - Maximum number of voxels to select / 選択する最大ボクセル数
   * @param {Object} context - Selection context / 選択コンテキスト
   * Properties: `grid` (Object) – Grid information / グリッド情報,
   * `bounds` (Object, optional) – Data bounds / データ境界
   * @returns {Object} Selection result / 選択結果
   */
  selectVoxels(allVoxels, maxCount, context = {}) {
    try {
      // 入力バリデーション
      if (!Array.isArray(allVoxels) || allVoxels.length === 0) {
        Logger.warn('VoxelSelector: Empty or invalid voxel array provided');
        return this._createEmptyResult();
      }
      
      if (maxCount <= 0) {
        Logger.warn(`VoxelSelector: Invalid maxCount: ${maxCount}`);
        return this._createEmptyResult();
      }
      
      // 全て選択可能な場合は早期リターン
      if (allVoxels.length <= maxCount) {
        return this._createResult(allVoxels, this.options.renderLimitStrategy, allVoxels.length, 0);
      }
      
      const { grid } = context;
      const strategy = this.options.renderLimitStrategy || 'density';
      
      // TopN強調ボクセルの特定
      const topNVoxels = this._identifyTopNVoxels(allVoxels);
      
      let result;
      
      // 戦略別選択実行
      switch (strategy) {
        case 'coverage':
          result = this._selectByCoverageStrategy(allVoxels, maxCount, grid, topNVoxels);
          break;
          
        case 'hybrid':
          result = this._selectByHybridStrategy(allVoxels, maxCount, grid, topNVoxels);
          break;
          
        case 'density':
        default:
          result = this._selectByDensityStrategy(allVoxels, maxCount, topNVoxels);
          break;
      }
      
      // 統計情報の保存
      this._lastSelectionStats = {
        strategy: result.strategy,
        clippedNonEmpty: result.clippedNonEmpty,
        coverageRatio: result.coverageRatio || null,
        selectedCount: result.selectedVoxels.length,
        totalCount: allVoxels.length
      };
      
      Logger.debug(`VoxelSelector: Applied ${result.strategy} strategy - selected ${result.selectedVoxels.length}/${allVoxels.length} voxels`);
      
      return result;
      
    } catch (error) {
      Logger.error(`VoxelSelector: Selection failed: ${error.message}. Falling back to density strategy.`);
      return this._fallbackToDensitySelection(allVoxels, maxCount);
    }
  }
  
  /**
   * Get the last selection statistics.
   * 最後の選択統計を取得。
   * 
   * @returns {Object|null} Selection statistics / 選択統計
   */
  getLastSelectionStats() {
    return this._lastSelectionStats;
  }
  
  /**
   * Identify TopN voxels for forced inclusion.
   * 強制包含用のTopNボクセルを特定。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @returns {Set} Set of TopN voxel keys / TopNボクセルキーのSet
   * @private
   */
  _identifyTopNVoxels(allVoxels) {
    const topNVoxels = new Set();
    
    if (this.options.highlightTopN && this.options.highlightTopN > 0) {
      const sortedForTopN = [...allVoxels].sort((a, b) => b.info.count - a.info.count);
      const topN = sortedForTopN.slice(0, this.options.highlightTopN);
      topN.forEach(voxel => topNVoxels.add(voxel.key));
      
      Logger.debug(`VoxelSelector: Identified ${topN.length} TopN voxels for forced inclusion`);
    }
    
    return topNVoxels;
  }
  
  /**
   * Select voxels using density strategy.
   * 密度戦略でボクセルを選択。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @param {number} maxCount - Maximum count / 最大数
   * @param {Set} forceInclude - Voxels to force include / 強制包含ボクセル
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _selectByDensityStrategy(allVoxels, maxCount, forceInclude = new Set()) {
    // 密度でソートして上位を選択
    const sorted = [...allVoxels].sort((a, b) => b.info.count - a.info.count);
    
    // 強制包含ボクセルを最初に追加
    const selected = [];
    const included = new Set();
    
    // TopNなど強制包含ボクセルを先に追加
    sorted.forEach(voxel => {
      if (forceInclude.has(voxel.key) && selected.length < maxCount) {
        selected.push(voxel);
        included.add(voxel.key);
      }
    });
    
    // 残りを密度順で追加
    sorted.forEach(voxel => {
      if (!included.has(voxel.key) && selected.length < maxCount) {
        selected.push(voxel);
        included.add(voxel.key);
      }
    });
    
    const clippedCount = allVoxels.length - selected.length;
    return this._createResult(selected, 'density', selected.length, clippedCount);
  }
  
  /**
   * Select voxels using coverage strategy (stratified sampling).
   * カバレッジ戦略でボクセルを選択(層化抽出)。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @param {number} maxCount - Maximum count / 最大数
   * @param {Object} grid - Grid information / グリッド情報
   * @param {Set} forceInclude - Voxels to force include / 強制包含ボクセル
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _selectByCoverageStrategy(allVoxels, maxCount, grid, forceInclude = new Set()) {
    const selected = [];
    const included = new Set();
    
    // 強制包含ボクセルを先に追加
    allVoxels.forEach(voxel => {
      if (forceInclude.has(voxel.key) && selected.length < maxCount) {
        selected.push(voxel);
        included.add(voxel.key);
      }
    });
    
    // 格子分割数の決定
    const binsXY = this.options.coverageBinsXY === 'auto' 
      ? Math.ceil(Math.sqrt(maxCount / 4)) // 自動計算: 平均4ボクセル/ビン
      : this.options.coverageBinsXY;
    
    // 空間をグリッド分割
    const bins = new Map();
    const remainingVoxels = allVoxels.filter(voxel => !included.has(voxel.key));
    
    remainingVoxels.forEach(voxel => {
      const binX = Math.max(0, Math.min(binsXY - 1, Math.floor((voxel.info.x / Math.max(1, grid.numVoxelsX)) * binsXY)));
      const binY = Math.max(0, Math.min(binsXY - 1, Math.floor((voxel.info.y / Math.max(1, grid.numVoxelsY)) * binsXY)));
      const binKey = `${binX},${binY}`;
      
      if (!bins.has(binKey)) {
        bins.set(binKey, []);
      }
      bins.get(binKey).push(voxel);
    });
    
    // 各ビンから代表ボクセルを選択
    const binKeys = Array.from(bins.keys());
    let binIndex = 0;
    
    while (selected.length < maxCount && binIndex < binKeys.length * 10) { // 最大10周
      const binKey = binKeys[binIndex % binKeys.length];
      const binVoxels = bins.get(binKey);
      
      if (binVoxels && binVoxels.length > 0) {
        // ビン内で最高密度のボクセルを選択
        binVoxels.sort((a, b) => b.info.count - a.info.count);
        const voxel = binVoxels.shift();
        
        if (!included.has(voxel.key)) {
          selected.push(voxel);
          included.add(voxel.key);
        }
        
        // 空になったビンを削除
        if (binVoxels.length === 0) {
          bins.delete(binKey);
          binKeys.splice(binKeys.indexOf(binKey), 1);
        }
      }
      
      binIndex++;
    }
    
    const clippedCount = allVoxels.length - selected.length;
    return this._createResult(selected, 'coverage', selected.length, clippedCount);
  }
  
  /**
   * Select voxels using hybrid strategy (density + coverage).
   * ハイブリッド戦略でボクセルを選択(密度 + カバレッジ)。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @param {number} maxCount - Maximum count / 最大数
   * @param {Object} grid - Grid information / グリッド情報
   * @param {Set} forceInclude - Voxels to force include / 強制包含ボクセル
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _selectByHybridStrategy(allVoxels, maxCount, grid, forceInclude = new Set()) {
    const minCoverageRatio = this.options.minCoverageRatio || 0.2;
    
    const selected = [];
    const included = new Set();
    
    // 強制包含ボクセルを先に追加
    allVoxels.forEach(voxel => {
      if (forceInclude.has(voxel.key) && selected.length < maxCount) {
        selected.push(voxel);
        included.add(voxel.key);
      }
    });
    
    const remainingCount = maxCount - selected.length;
    const adjustedCoverageCount = Math.floor(remainingCount * minCoverageRatio);
    const adjustedDensityCount = remainingCount - adjustedCoverageCount;
    
    // カバレッジ選択(層化抽出)
    if (adjustedCoverageCount > 0) {
      const coverageResult = this._selectByCoverageStrategy(
        allVoxels.filter(voxel => !included.has(voxel.key)), 
        adjustedCoverageCount, 
        grid, 
        new Set()
      );
      
      coverageResult.selectedVoxels.forEach(voxel => {
        if (selected.length < maxCount && !included.has(voxel.key)) {
          selected.push(voxel);
          included.add(voxel.key);
        }
      });
    }
    
    // 密度選択(残り)
    if (adjustedDensityCount > 0) {
      const densityResult = this._selectByDensityStrategy(
        allVoxels.filter(voxel => !included.has(voxel.key)), 
        adjustedDensityCount, 
        new Set()
      );
      
      densityResult.selectedVoxels.forEach(voxel => {
        if (selected.length < maxCount && !included.has(voxel.key)) {
          selected.push(voxel);
          included.add(voxel.key);
        }
      });
    }
    
    // 実際のカバレッジ比率を計算
    const actualCoverageRatio = adjustedCoverageCount > 0 ? 
      (selected.length - forceInclude.size - adjustedDensityCount) / (selected.length - forceInclude.size) : 0;
    
    const clippedCount = allVoxels.length - selected.length;
    return this._createResult(selected, 'hybrid', selected.length, clippedCount, actualCoverageRatio);
  }
  
  /**
   * Fallback to density selection when other strategies fail.
   * 他の戦略が失敗した場合の密度選択フォールバック。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @param {number} maxCount - Maximum count / 最大数
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _fallbackToDensitySelection(allVoxels, maxCount) {
    try {
      const topNVoxels = new Set(); // エラー時はTopN無効化
      const result = this._selectByDensityStrategy(allVoxels, maxCount, topNVoxels);
      result.strategy = 'density-fallback';
      
      this._lastSelectionStats = {
        strategy: result.strategy,
        clippedNonEmpty: result.clippedNonEmpty,
        coverageRatio: null,
        selectedCount: result.selectedVoxels.length,
        totalCount: allVoxels.length,
        error: true
      };
      
      return result;
    } catch (error) {
      Logger.error(`VoxelSelector: Even fallback failed: ${error.message}`);
      return this._createEmptyResult();
    }
  }
  
  /**
   * Create a selection result object.
   * 選択結果オブジェクトを作成。
   * 
   * @param {Array} selectedVoxels - Selected voxels / 選択されたボクセル
   * @param {string} strategy - Strategy used / 使用された戦略
   * @param {number} selectedCount - Number selected / 選択数
   * @param {number} clippedCount - Number clipped / クリップ数
   * @param {number} [coverageRatio] - Coverage ratio for hybrid / ハイブリッド用カバレッジ比率
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _createResult(selectedVoxels, strategy, selectedCount, clippedCount, coverageRatio = null) {
    return {
      selectedVoxels,
      strategy,
      clippedNonEmpty: clippedCount,
      coverageRatio
    };
  }
  
  /**
   * Create an empty selection result.
   * 空の選択結果を作成。
   * 
   * @returns {Object} Empty selection result / 空の選択結果
   * @private
   */
  _createEmptyResult() {
    return {
      selectedVoxels: [],
      strategy: 'none',
      clippedNonEmpty: 0,
      coverageRatio: null
    };
  }
}

日本語

関連: VoxelSelectorクラス

/**
 * @fileoverview VoxelSelector - Voxel selection strategies implementation
 * ADR-0009 Phase2: VoxelRenderer責任分離 - ボクセル選択戦略専門クラス
 * 
 * 機能:
 * - 密度選択戦略(density)
 * - カバレッジ選択戦略(coverage - 層化抽出)
 * - ハイブリッド選択戦略(hybrid - 密度+カバレッジ)
 * - TopN強調対応
 * - 統計情報収集
 */

import { Logger } from '../../utils/logger.js';

/**
 * Voxel selection strategy executor.
 * VoxelSelector - ボクセル選択戦略の実装。
 *
 * Single Responsibility: ボクセル選択ロジックのみを担当
 * - 戦略パターンを使用して選択アルゴリズムを切り替え可能
 * - 純粋関数として設計(Cesium依存なし)
 * - エラー時は密度ソート選抜にフォールバック
 */
export class VoxelSelector {
  
  /**
   * Create a VoxelSelector instance.
   * VoxelSelectorインスタンスを作成。
   * 
   * @param {Object} options - Selection options / 選択オプション
   * @param {string} [options.renderLimitStrategy='density'] - Selection strategy / 選択戦略
   * @param {number} [options.highlightTopN=0] - TopN highlight count / TopN強調数
   * @param {number|string} [options.coverageBinsXY='auto'] - Coverage bins / カバレッジビン数
   * @param {number} [options.minCoverageRatio=0.2] - Min coverage ratio for hybrid / ハイブリッド用最小カバレッジ比率
   */
  constructor(options = {}) {
    this.options = {
      renderLimitStrategy: 'density',
      highlightTopN: 0,
      coverageBinsXY: 'auto',
      minCoverageRatio: 0.2,
      ...options
    };
    
    this._lastSelectionStats = null;
    
    Logger.debug(`VoxelSelector initialized with strategy: ${this.options.renderLimitStrategy}`);
  }
  
  /**
   * Select voxels for rendering based on configured strategy.
   * 設定された戦略に基づいてレンダリング用ボクセルを選択。
   * 
   * @param {Array} allVoxels - All voxels to select from / 選択元の全ボクセル
   * @param {number} maxCount - Maximum number of voxels to select / 選択する最大ボクセル数
   * @param {Object} context - Selection context / 選択コンテキスト
   * Properties: `grid` (Object) – Grid information / グリッド情報,
   * `bounds` (Object, optional) – Data bounds / データ境界
   * @returns {Object} Selection result / 選択結果
   */
  selectVoxels(allVoxels, maxCount, context = {}) {
    try {
      // 入力バリデーション
      if (!Array.isArray(allVoxels) || allVoxels.length === 0) {
        Logger.warn('VoxelSelector: Empty or invalid voxel array provided');
        return this._createEmptyResult();
      }
      
      if (maxCount <= 0) {
        Logger.warn(`VoxelSelector: Invalid maxCount: ${maxCount}`);
        return this._createEmptyResult();
      }
      
      // 全て選択可能な場合は早期リターン
      if (allVoxels.length <= maxCount) {
        return this._createResult(allVoxels, this.options.renderLimitStrategy, allVoxels.length, 0);
      }
      
      const { grid } = context;
      const strategy = this.options.renderLimitStrategy || 'density';
      
      // TopN強調ボクセルの特定
      const topNVoxels = this._identifyTopNVoxels(allVoxels);
      
      let result;
      
      // 戦略別選択実行
      switch (strategy) {
        case 'coverage':
          result = this._selectByCoverageStrategy(allVoxels, maxCount, grid, topNVoxels);
          break;
          
        case 'hybrid':
          result = this._selectByHybridStrategy(allVoxels, maxCount, grid, topNVoxels);
          break;
          
        case 'density':
        default:
          result = this._selectByDensityStrategy(allVoxels, maxCount, topNVoxels);
          break;
      }
      
      // 統計情報の保存
      this._lastSelectionStats = {
        strategy: result.strategy,
        clippedNonEmpty: result.clippedNonEmpty,
        coverageRatio: result.coverageRatio || null,
        selectedCount: result.selectedVoxels.length,
        totalCount: allVoxels.length
      };
      
      Logger.debug(`VoxelSelector: Applied ${result.strategy} strategy - selected ${result.selectedVoxels.length}/${allVoxels.length} voxels`);
      
      return result;
      
    } catch (error) {
      Logger.error(`VoxelSelector: Selection failed: ${error.message}. Falling back to density strategy.`);
      return this._fallbackToDensitySelection(allVoxels, maxCount);
    }
  }
  
  /**
   * Get the last selection statistics.
   * 最後の選択統計を取得。
   * 
   * @returns {Object|null} Selection statistics / 選択統計
   */
  getLastSelectionStats() {
    return this._lastSelectionStats;
  }
  
  /**
   * Identify TopN voxels for forced inclusion.
   * 強制包含用のTopNボクセルを特定。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @returns {Set} Set of TopN voxel keys / TopNボクセルキーのSet
   * @private
   */
  _identifyTopNVoxels(allVoxels) {
    const topNVoxels = new Set();
    
    if (this.options.highlightTopN && this.options.highlightTopN > 0) {
      const sortedForTopN = [...allVoxels].sort((a, b) => b.info.count - a.info.count);
      const topN = sortedForTopN.slice(0, this.options.highlightTopN);
      topN.forEach(voxel => topNVoxels.add(voxel.key));
      
      Logger.debug(`VoxelSelector: Identified ${topN.length} TopN voxels for forced inclusion`);
    }
    
    return topNVoxels;
  }
  
  /**
   * Select voxels using density strategy.
   * 密度戦略でボクセルを選択。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @param {number} maxCount - Maximum count / 最大数
   * @param {Set} forceInclude - Voxels to force include / 強制包含ボクセル
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _selectByDensityStrategy(allVoxels, maxCount, forceInclude = new Set()) {
    // 密度でソートして上位を選択
    const sorted = [...allVoxels].sort((a, b) => b.info.count - a.info.count);
    
    // 強制包含ボクセルを最初に追加
    const selected = [];
    const included = new Set();
    
    // TopNなど強制包含ボクセルを先に追加
    sorted.forEach(voxel => {
      if (forceInclude.has(voxel.key) && selected.length < maxCount) {
        selected.push(voxel);
        included.add(voxel.key);
      }
    });
    
    // 残りを密度順で追加
    sorted.forEach(voxel => {
      if (!included.has(voxel.key) && selected.length < maxCount) {
        selected.push(voxel);
        included.add(voxel.key);
      }
    });
    
    const clippedCount = allVoxels.length - selected.length;
    return this._createResult(selected, 'density', selected.length, clippedCount);
  }
  
  /**
   * Select voxels using coverage strategy (stratified sampling).
   * カバレッジ戦略でボクセルを選択(層化抽出)。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @param {number} maxCount - Maximum count / 最大数
   * @param {Object} grid - Grid information / グリッド情報
   * @param {Set} forceInclude - Voxels to force include / 強制包含ボクセル
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _selectByCoverageStrategy(allVoxels, maxCount, grid, forceInclude = new Set()) {
    const selected = [];
    const included = new Set();
    
    // 強制包含ボクセルを先に追加
    allVoxels.forEach(voxel => {
      if (forceInclude.has(voxel.key) && selected.length < maxCount) {
        selected.push(voxel);
        included.add(voxel.key);
      }
    });
    
    // 格子分割数の決定
    const binsXY = this.options.coverageBinsXY === 'auto' 
      ? Math.ceil(Math.sqrt(maxCount / 4)) // 自動計算: 平均4ボクセル/ビン
      : this.options.coverageBinsXY;
    
    // 空間をグリッド分割
    const bins = new Map();
    const remainingVoxels = allVoxels.filter(voxel => !included.has(voxel.key));
    
    remainingVoxels.forEach(voxel => {
      const binX = Math.max(0, Math.min(binsXY - 1, Math.floor((voxel.info.x / Math.max(1, grid.numVoxelsX)) * binsXY)));
      const binY = Math.max(0, Math.min(binsXY - 1, Math.floor((voxel.info.y / Math.max(1, grid.numVoxelsY)) * binsXY)));
      const binKey = `${binX},${binY}`;
      
      if (!bins.has(binKey)) {
        bins.set(binKey, []);
      }
      bins.get(binKey).push(voxel);
    });
    
    // 各ビンから代表ボクセルを選択
    const binKeys = Array.from(bins.keys());
    let binIndex = 0;
    
    while (selected.length < maxCount && binIndex < binKeys.length * 10) { // 最大10周
      const binKey = binKeys[binIndex % binKeys.length];
      const binVoxels = bins.get(binKey);
      
      if (binVoxels && binVoxels.length > 0) {
        // ビン内で最高密度のボクセルを選択
        binVoxels.sort((a, b) => b.info.count - a.info.count);
        const voxel = binVoxels.shift();
        
        if (!included.has(voxel.key)) {
          selected.push(voxel);
          included.add(voxel.key);
        }
        
        // 空になったビンを削除
        if (binVoxels.length === 0) {
          bins.delete(binKey);
          binKeys.splice(binKeys.indexOf(binKey), 1);
        }
      }
      
      binIndex++;
    }
    
    const clippedCount = allVoxels.length - selected.length;
    return this._createResult(selected, 'coverage', selected.length, clippedCount);
  }
  
  /**
   * Select voxels using hybrid strategy (density + coverage).
   * ハイブリッド戦略でボクセルを選択(密度 + カバレッジ)。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @param {number} maxCount - Maximum count / 最大数
   * @param {Object} grid - Grid information / グリッド情報
   * @param {Set} forceInclude - Voxels to force include / 強制包含ボクセル
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _selectByHybridStrategy(allVoxels, maxCount, grid, forceInclude = new Set()) {
    const minCoverageRatio = this.options.minCoverageRatio || 0.2;
    
    const selected = [];
    const included = new Set();
    
    // 強制包含ボクセルを先に追加
    allVoxels.forEach(voxel => {
      if (forceInclude.has(voxel.key) && selected.length < maxCount) {
        selected.push(voxel);
        included.add(voxel.key);
      }
    });
    
    const remainingCount = maxCount - selected.length;
    const adjustedCoverageCount = Math.floor(remainingCount * minCoverageRatio);
    const adjustedDensityCount = remainingCount - adjustedCoverageCount;
    
    // カバレッジ選択(層化抽出)
    if (adjustedCoverageCount > 0) {
      const coverageResult = this._selectByCoverageStrategy(
        allVoxels.filter(voxel => !included.has(voxel.key)), 
        adjustedCoverageCount, 
        grid, 
        new Set()
      );
      
      coverageResult.selectedVoxels.forEach(voxel => {
        if (selected.length < maxCount && !included.has(voxel.key)) {
          selected.push(voxel);
          included.add(voxel.key);
        }
      });
    }
    
    // 密度選択(残り)
    if (adjustedDensityCount > 0) {
      const densityResult = this._selectByDensityStrategy(
        allVoxels.filter(voxel => !included.has(voxel.key)), 
        adjustedDensityCount, 
        new Set()
      );
      
      densityResult.selectedVoxels.forEach(voxel => {
        if (selected.length < maxCount && !included.has(voxel.key)) {
          selected.push(voxel);
          included.add(voxel.key);
        }
      });
    }
    
    // 実際のカバレッジ比率を計算
    const actualCoverageRatio = adjustedCoverageCount > 0 ? 
      (selected.length - forceInclude.size - adjustedDensityCount) / (selected.length - forceInclude.size) : 0;
    
    const clippedCount = allVoxels.length - selected.length;
    return this._createResult(selected, 'hybrid', selected.length, clippedCount, actualCoverageRatio);
  }
  
  /**
   * Fallback to density selection when other strategies fail.
   * 他の戦略が失敗した場合の密度選択フォールバック。
   * 
   * @param {Array} allVoxels - All voxels / 全ボクセル
   * @param {number} maxCount - Maximum count / 最大数
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _fallbackToDensitySelection(allVoxels, maxCount) {
    try {
      const topNVoxels = new Set(); // エラー時はTopN無効化
      const result = this._selectByDensityStrategy(allVoxels, maxCount, topNVoxels);
      result.strategy = 'density-fallback';
      
      this._lastSelectionStats = {
        strategy: result.strategy,
        clippedNonEmpty: result.clippedNonEmpty,
        coverageRatio: null,
        selectedCount: result.selectedVoxels.length,
        totalCount: allVoxels.length,
        error: true
      };
      
      return result;
    } catch (error) {
      Logger.error(`VoxelSelector: Even fallback failed: ${error.message}`);
      return this._createEmptyResult();
    }
  }
  
  /**
   * Create a selection result object.
   * 選択結果オブジェクトを作成。
   * 
   * @param {Array} selectedVoxels - Selected voxels / 選択されたボクセル
   * @param {string} strategy - Strategy used / 使用された戦略
   * @param {number} selectedCount - Number selected / 選択数
   * @param {number} clippedCount - Number clipped / クリップ数
   * @param {number} [coverageRatio] - Coverage ratio for hybrid / ハイブリッド用カバレッジ比率
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _createResult(selectedVoxels, strategy, selectedCount, clippedCount, coverageRatio = null) {
    return {
      selectedVoxels,
      strategy,
      clippedNonEmpty: clippedCount,
      coverageRatio
    };
  }
  
  /**
   * Create an empty selection result.
   * 空の選択結果を作成。
   * 
   * @returns {Object} Empty selection result / 空の選択結果
   * @private
   */
  _createEmptyResult() {
    return {
      selectedVoxels: [],
      strategy: 'none',
      clippedNonEmpty: 0,
      coverageRatio: null
    };
  }
}