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

Source: utils/sampleData.js

日本語 | English

English

See also: Class: sampleData

/**
 * Utility helpers for generating sample datasets.
 * サンプルデータ生成ユーティリティ群。
 */
import * as Cesium from 'cesium';

/**
 * Generate pseudo-random test entities within the specified bounds.
 * 指定範囲内にランダムなテストエンティティを生成します。
 * @param {Object} viewer - CesiumJS Viewer
 * @param {Object} bounds - 生成範囲 {minLon, maxLon, minLat, maxLat, minAlt, maxAlt}
 * @param {number} count - 生成数(デフォルト: 500)
 * @returns {Array} 生成されたエンティティ配列
 */
export function generateTestEntities(viewer, bounds, count = 500) {
  if (!viewer) {
    throw new Error('viewer is required');
  }
  
  if (!bounds || !('minLon' in bounds) || !('maxLon' in bounds) || !('minLat' in bounds) || !('maxLat' in bounds)) {
    throw new Error('bounds must include minLon, maxLon, minLat, maxLat');
  }
  
  const entities = [];
  
  // デフォルト高度範囲
  const minAlt = bounds.minAlt || 0;
  const maxAlt = bounds.maxAlt || 100;
  
  for (let i = 0; i < count; i++) {
    const lon = bounds.minLon + (bounds.maxLon - bounds.minLon) * Math.random();
    const lat = bounds.minLat + (bounds.maxLat - bounds.minLat) * Math.random();
    const alt = minAlt + (maxAlt - minAlt) * Math.random();
    
    const entity = viewer.entities.add({
      id: `test-entity-${i}`,
      position: Cesium.Cartesian3.fromDegrees(lon, lat, alt),
      point: {
        pixelSize: 5,
        color: Cesium.Color.YELLOW,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 1
      },
      label: {
        text: `Test ${i}`,
        font: '10pt sans-serif',
        pixelOffset: new Cesium.Cartesian2(0, -30),
        fillColor: Cesium.Color.WHITE,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 1,
        style: Cesium.LabelStyle.FILL_AND_OUTLINE
      }
    });
    
    entities.push(entity);
  }
  
  return entities;
}

/**
 * Retrieve every entity registered on the given viewer.
 * 指定された Viewer の全エンティティを取得します。
 * @param {Object} viewer - CesiumJS Viewer
 * @returns {Array} エンティティ配列
 */
export function getAllEntities(viewer) {
  if (!viewer || !viewer.entities) {
    throw new Error('Invalid viewer');
  }
  
  return viewer.entities.values;
}

/**
 * Generate clustered sample entities without adding to a viewer.
 * ビューアに追加せず、クラスター状のサンプルエンティティ配列を生成します。
 * @param {number} total - 総エンティティ数
 * @param {Object} config - { clusters: Array<{ center:[lon,lat,alt], radius:number, density:number, count:number }> }
 * @returns {Array<Cesium.Entity>} エンティティ配列
 */
export function generateSampleData(total, config = {}) {
  const clusters = Array.isArray(config.clusters) && config.clusters.length > 0
    ? config.clusters
    : [{ center: [139.75, 35.7, 50], radius: 0.02, density: 0.5, count: total }];

  const entities = [];
  let remaining = total;

  clusters.forEach((c, idx) => {
    const count = Math.min(remaining, Math.max(0, c.count || Math.floor(total / clusters.length)));
    remaining -= count;

    const [lon0, lat0, alt0] = c.center || [139.75, 35.7, 50];
    const radius = c.radius || 0.02; // degrees
    const altRange = 100;

    for (let i = 0; i < count; i++) {
      // Uniform random within circle (approx in degrees)
      const r = Math.sqrt(Math.random()) * radius;
      const theta = Math.random() * Math.PI * 2;
      const lon = lon0 + r * Math.cos(theta);
      const lat = lat0 + r * Math.sin(theta);
      const alt = alt0 + (Math.random() - 0.5) * altRange;

      const entity = new Cesium.Entity({
        id: `sample-${idx}-${i}-${Math.random().toString(36).slice(2, 8)}`,
        position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
      });
      entities.push(entity);
    }
  });

  // If any remainder, add around the first cluster center
  while (remaining-- > 0) {
    const [lon0, lat0, alt0] = clusters[0].center;
    const lon = lon0 + (Math.random() - 0.5) * 0.02;
    const lat = lat0 + (Math.random() - 0.5) * 0.02;
    const alt = alt0 + (Math.random() - 0.5) * 100;
    entities.push(new Cesium.Entity({
      id: `sample-extra-${Math.random().toString(36).slice(2, 8)}`,
      position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
    }));
  }

  return entities;
}

/**
 * Return a convenience bounding box covering the Tokyo Station area.
 * 東京駅周辺の境界情報を返します。
 * @returns {Object} 東京駅周辺の境界情報
 */
export function getTokyoStationBounds() {
  return {
    minLon: 139.7640,
    maxLon: 139.7680,
    minLat: 35.6790,
    maxLat: 35.6820,
    minAlt: 0,
    maxAlt: 100,
    centerLon: 139.7660,
    centerLat: 35.6805,
    centerAlt: 50
  };
}

/**
 * Create an axis-aligned bounding box from a centre point and edge length.
 * 指定された中心点とサイズから境界を生成します。
 * @param {number} centerLon - 中心経度
 * @param {number} centerLat - 中心緯度
 * @param {number} centerAlt - 中心高度
 * @param {number} sizeMeters - 一辺のサイズ(メートル)
 * @returns {Object} 境界情報
 */
export function createBoundsFromCenter(centerLon, centerLat, centerAlt, sizeMeters) {
  // 度からメートルへの概算変換
  const latDelta = sizeMeters / 111000 / 2;
  const lonDelta = sizeMeters / (111000 * Math.cos(centerLat * Math.PI / 180)) / 2;
  const altDelta = sizeMeters / 2;
  
  return {
    minLon: centerLon - lonDelta,
    maxLon: centerLon + lonDelta,
    minLat: centerLat - latDelta,
    maxLat: centerLat + latDelta,
    minAlt: centerAlt - altDelta,
    maxAlt: centerAlt + altDelta,
    centerLon,
    centerLat,
    centerAlt
  };
}

/**
 * v0.1.15 Phase 1: Density pattern generators for parameter optimization (ADR-0011)
 * パラメータ最適化用の密度パターン生成関数
 */

/**
 * Generate a clustered density pattern.
 * 高密度クラスターの分布を生成します。
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @param {number} clusterCount - Number of clusters (default: 3)
 * @returns {Array<Cesium.Entity>} Generated entities
 */
function generateClusteredPattern(bounds, count, clusterCount = 3) {
  const entities = [];
  if (!Number.isFinite(count) || count <= 0) {
    return entities;
  }

  const clusters = Math.max(1, Math.min(Math.floor(clusterCount) || 1, count));
  const basePerCluster = Math.floor(count / clusters);
  const remainder = count - basePerCluster * clusters;

  const centerLon = (bounds.minLon + bounds.maxLon) / 2;
  const centerLat = (bounds.minLat + bounds.maxLat) / 2;
  const centerAlt = (bounds.minAlt + bounds.maxAlt) / 2;
  
  let globalIndex = 0;
  for (let c = 0; c < clusters; c++) {
    const clusterSize = basePerCluster + (c < remainder ? 1 : 0);
    if (clusterSize <= 0) {
      continue;
    }
    // クラスター中心をランダムに配置
    const clusterLon = centerLon + (Math.random() - 0.5) * (bounds.maxLon - bounds.minLon) * 0.6;
    const clusterLat = centerLat + (Math.random() - 0.5) * (bounds.maxLat - bounds.minLat) * 0.6;
    const clusterAlt = centerAlt + (Math.random() - 0.5) * (bounds.maxAlt - bounds.minAlt) * 0.6;
    
    // 各クラスター内にエンティティを密集配置
    const clusterRadius = Math.min(
      (bounds.maxLon - bounds.minLon),
      (bounds.maxLat - bounds.minLat)
    ) * 0.1;
    
    for (let i = 0; i < clusterSize; i++) {
      const r = Math.sqrt(Math.random()) * clusterRadius;
      const theta = Math.random() * Math.PI * 2;
      
      const lon = clusterLon + r * Math.cos(theta);
      const lat = clusterLat + r * Math.sin(theta);
      const alt = clusterAlt + (Math.random() - 0.5) * (bounds.maxAlt - bounds.minAlt) * 0.2;
      
      entities.push(new Cesium.Entity({
        id: `clustered-${c}-${i}-${globalIndex}-${Math.random().toString(36).slice(2, 8)}`,
        position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
      }));
      globalIndex++;
    }
  }
  
  return entities;
}

/**
 * Generate a scattered density pattern.
 * 疎分布のデータセットを生成します。
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @returns {Array<Cesium.Entity>} Generated entities
 */
function generateScatteredPattern(bounds, count) {
  const entities = [];
  
  for (let i = 0; i < count; i++) {
    const lon = bounds.minLon + Math.random() * (bounds.maxLon - bounds.minLon);
    const lat = bounds.minLat + Math.random() * (bounds.maxLat - bounds.minLat);
    const alt = bounds.minAlt + Math.random() * (bounds.maxAlt - bounds.minAlt);
    
    entities.push(new Cesium.Entity({
      id: `scattered-${i}-${Math.random().toString(36).slice(2, 8)}`,
      position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
    }));
  }
  
  return entities;
}

/**
 * Generate a gradient-based density pattern.
 * グラデーション分布のデータセットを生成します。
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @returns {Array<Cesium.Entity>} Generated entities
 */
function generateGradientPattern(bounds, count) {
  const entities = [];
  
  for (let i = 0; i < count; i++) {
    // X軸方向に密度が増加するグラデーション
    const x = Math.random();
    const densityWeight = x * x; // 二乗で密度を増加
    
    const lon = bounds.minLon + x * (bounds.maxLon - bounds.minLon);
    const lat = bounds.minLat + Math.random() * (bounds.maxLat - bounds.minLat);
    const alt = bounds.minAlt + Math.random() * (bounds.maxAlt - bounds.minAlt);
    
    // 密度の高いエリアでは複数のエンティティを近接配置
    if (densityWeight > 0.7 && Math.random() > 0.5) {
      const offset = 0.0001; // 約10m
      entities.push(new Cesium.Entity({
        id: `gradient-${i}-extra-${Math.random().toString(36).slice(2, 8)}`,
        position: Cesium.Cartesian3.fromDegrees(
          lon + (Math.random() - 0.5) * offset,
          lat + (Math.random() - 0.5) * offset,
          alt
        )
      }));
    }
    
    entities.push(new Cesium.Entity({
      id: `gradient-${i}-${Math.random().toString(36).slice(2, 8)}`,
      position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
    }));
  }
  
  return entities;
}

/**
 * Generate a mixed density pattern.
 * 混在分布のデータセットを生成します。
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @returns {Array<Cesium.Entity>} Generated entities
 */
function generateMixedPattern(bounds, count) {
  const entities = [];
  
  // 30%を高密度クラスター、70%を疎分布として配置
  const clusteredCount = Math.floor(count * 0.3);
  const scatteredCount = count - clusteredCount;
  
  // 高密度クラスター部分
  const clusteredEntities = generateClusteredPattern(bounds, clusteredCount, 2);
  entities.push(...clusteredEntities);
  
  // 疎分布部分
  const scatteredEntities = generateScatteredPattern(bounds, scatteredCount);
  entities.push(...scatteredEntities);
  
  return entities;
}

/**
 * Density pattern generators map (ADR-0011 Phase 1)
 * パラメータ最適化用密度パターンマップ
 */
export const DENSITY_PATTERNS = {
  clustered: generateClusteredPattern,
  scattered: generateScatteredPattern,
  gradient: generateGradientPattern,
  mixed: generateMixedPattern
};

/**
 * Generate test dataset with specified pattern
 * 指定したパターンでテストデータセットを生成
 * @param {string} pattern - Pattern name ('clustered', 'scattered', 'gradient', 'mixed')
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @returns {Array<Cesium.Entity>} Generated entities
 */
export function generatePatternData(pattern, bounds, count) {
  const generator = DENSITY_PATTERNS[pattern];
  if (!generator) {
    throw new Error(`Unknown pattern: ${pattern}. Available: ${Object.keys(DENSITY_PATTERNS).join(', ')}`);
  }
  return generator(bounds, count);
}

日本語

関連: sampleDataクラス

/**
 * Utility helpers for generating sample datasets.
 * サンプルデータ生成ユーティリティ群。
 */
import * as Cesium from 'cesium';

/**
 * Generate pseudo-random test entities within the specified bounds.
 * 指定範囲内にランダムなテストエンティティを生成します。
 * @param {Object} viewer - CesiumJS Viewer
 * @param {Object} bounds - 生成範囲 {minLon, maxLon, minLat, maxLat, minAlt, maxAlt}
 * @param {number} count - 生成数(デフォルト: 500)
 * @returns {Array} 生成されたエンティティ配列
 */
export function generateTestEntities(viewer, bounds, count = 500) {
  if (!viewer) {
    throw new Error('viewer is required');
  }
  
  if (!bounds || !('minLon' in bounds) || !('maxLon' in bounds) || !('minLat' in bounds) || !('maxLat' in bounds)) {
    throw new Error('bounds must include minLon, maxLon, minLat, maxLat');
  }
  
  const entities = [];
  
  // デフォルト高度範囲
  const minAlt = bounds.minAlt || 0;
  const maxAlt = bounds.maxAlt || 100;
  
  for (let i = 0; i < count; i++) {
    const lon = bounds.minLon + (bounds.maxLon - bounds.minLon) * Math.random();
    const lat = bounds.minLat + (bounds.maxLat - bounds.minLat) * Math.random();
    const alt = minAlt + (maxAlt - minAlt) * Math.random();
    
    const entity = viewer.entities.add({
      id: `test-entity-${i}`,
      position: Cesium.Cartesian3.fromDegrees(lon, lat, alt),
      point: {
        pixelSize: 5,
        color: Cesium.Color.YELLOW,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 1
      },
      label: {
        text: `Test ${i}`,
        font: '10pt sans-serif',
        pixelOffset: new Cesium.Cartesian2(0, -30),
        fillColor: Cesium.Color.WHITE,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 1,
        style: Cesium.LabelStyle.FILL_AND_OUTLINE
      }
    });
    
    entities.push(entity);
  }
  
  return entities;
}

/**
 * Retrieve every entity registered on the given viewer.
 * 指定された Viewer の全エンティティを取得します。
 * @param {Object} viewer - CesiumJS Viewer
 * @returns {Array} エンティティ配列
 */
export function getAllEntities(viewer) {
  if (!viewer || !viewer.entities) {
    throw new Error('Invalid viewer');
  }
  
  return viewer.entities.values;
}

/**
 * Generate clustered sample entities without adding to a viewer.
 * ビューアに追加せず、クラスター状のサンプルエンティティ配列を生成します。
 * @param {number} total - 総エンティティ数
 * @param {Object} config - { clusters: Array<{ center:[lon,lat,alt], radius:number, density:number, count:number }> }
 * @returns {Array<Cesium.Entity>} エンティティ配列
 */
export function generateSampleData(total, config = {}) {
  const clusters = Array.isArray(config.clusters) && config.clusters.length > 0
    ? config.clusters
    : [{ center: [139.75, 35.7, 50], radius: 0.02, density: 0.5, count: total }];

  const entities = [];
  let remaining = total;

  clusters.forEach((c, idx) => {
    const count = Math.min(remaining, Math.max(0, c.count || Math.floor(total / clusters.length)));
    remaining -= count;

    const [lon0, lat0, alt0] = c.center || [139.75, 35.7, 50];
    const radius = c.radius || 0.02; // degrees
    const altRange = 100;

    for (let i = 0; i < count; i++) {
      // Uniform random within circle (approx in degrees)
      const r = Math.sqrt(Math.random()) * radius;
      const theta = Math.random() * Math.PI * 2;
      const lon = lon0 + r * Math.cos(theta);
      const lat = lat0 + r * Math.sin(theta);
      const alt = alt0 + (Math.random() - 0.5) * altRange;

      const entity = new Cesium.Entity({
        id: `sample-${idx}-${i}-${Math.random().toString(36).slice(2, 8)}`,
        position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
      });
      entities.push(entity);
    }
  });

  // If any remainder, add around the first cluster center
  while (remaining-- > 0) {
    const [lon0, lat0, alt0] = clusters[0].center;
    const lon = lon0 + (Math.random() - 0.5) * 0.02;
    const lat = lat0 + (Math.random() - 0.5) * 0.02;
    const alt = alt0 + (Math.random() - 0.5) * 100;
    entities.push(new Cesium.Entity({
      id: `sample-extra-${Math.random().toString(36).slice(2, 8)}`,
      position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
    }));
  }

  return entities;
}

/**
 * Return a convenience bounding box covering the Tokyo Station area.
 * 東京駅周辺の境界情報を返します。
 * @returns {Object} 東京駅周辺の境界情報
 */
export function getTokyoStationBounds() {
  return {
    minLon: 139.7640,
    maxLon: 139.7680,
    minLat: 35.6790,
    maxLat: 35.6820,
    minAlt: 0,
    maxAlt: 100,
    centerLon: 139.7660,
    centerLat: 35.6805,
    centerAlt: 50
  };
}

/**
 * Create an axis-aligned bounding box from a centre point and edge length.
 * 指定された中心点とサイズから境界を生成します。
 * @param {number} centerLon - 中心経度
 * @param {number} centerLat - 中心緯度
 * @param {number} centerAlt - 中心高度
 * @param {number} sizeMeters - 一辺のサイズ(メートル)
 * @returns {Object} 境界情報
 */
export function createBoundsFromCenter(centerLon, centerLat, centerAlt, sizeMeters) {
  // 度からメートルへの概算変換
  const latDelta = sizeMeters / 111000 / 2;
  const lonDelta = sizeMeters / (111000 * Math.cos(centerLat * Math.PI / 180)) / 2;
  const altDelta = sizeMeters / 2;
  
  return {
    minLon: centerLon - lonDelta,
    maxLon: centerLon + lonDelta,
    minLat: centerLat - latDelta,
    maxLat: centerLat + latDelta,
    minAlt: centerAlt - altDelta,
    maxAlt: centerAlt + altDelta,
    centerLon,
    centerLat,
    centerAlt
  };
}

/**
 * v0.1.15 Phase 1: Density pattern generators for parameter optimization (ADR-0011)
 * パラメータ最適化用の密度パターン生成関数
 */

/**
 * Generate a clustered density pattern.
 * 高密度クラスターの分布を生成します。
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @param {number} clusterCount - Number of clusters (default: 3)
 * @returns {Array<Cesium.Entity>} Generated entities
 */
function generateClusteredPattern(bounds, count, clusterCount = 3) {
  const entities = [];
  if (!Number.isFinite(count) || count <= 0) {
    return entities;
  }

  const clusters = Math.max(1, Math.min(Math.floor(clusterCount) || 1, count));
  const basePerCluster = Math.floor(count / clusters);
  const remainder = count - basePerCluster * clusters;

  const centerLon = (bounds.minLon + bounds.maxLon) / 2;
  const centerLat = (bounds.minLat + bounds.maxLat) / 2;
  const centerAlt = (bounds.minAlt + bounds.maxAlt) / 2;
  
  let globalIndex = 0;
  for (let c = 0; c < clusters; c++) {
    const clusterSize = basePerCluster + (c < remainder ? 1 : 0);
    if (clusterSize <= 0) {
      continue;
    }
    // クラスター中心をランダムに配置
    const clusterLon = centerLon + (Math.random() - 0.5) * (bounds.maxLon - bounds.minLon) * 0.6;
    const clusterLat = centerLat + (Math.random() - 0.5) * (bounds.maxLat - bounds.minLat) * 0.6;
    const clusterAlt = centerAlt + (Math.random() - 0.5) * (bounds.maxAlt - bounds.minAlt) * 0.6;
    
    // 各クラスター内にエンティティを密集配置
    const clusterRadius = Math.min(
      (bounds.maxLon - bounds.minLon),
      (bounds.maxLat - bounds.minLat)
    ) * 0.1;
    
    for (let i = 0; i < clusterSize; i++) {
      const r = Math.sqrt(Math.random()) * clusterRadius;
      const theta = Math.random() * Math.PI * 2;
      
      const lon = clusterLon + r * Math.cos(theta);
      const lat = clusterLat + r * Math.sin(theta);
      const alt = clusterAlt + (Math.random() - 0.5) * (bounds.maxAlt - bounds.minAlt) * 0.2;
      
      entities.push(new Cesium.Entity({
        id: `clustered-${c}-${i}-${globalIndex}-${Math.random().toString(36).slice(2, 8)}`,
        position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
      }));
      globalIndex++;
    }
  }
  
  return entities;
}

/**
 * Generate a scattered density pattern.
 * 疎分布のデータセットを生成します。
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @returns {Array<Cesium.Entity>} Generated entities
 */
function generateScatteredPattern(bounds, count) {
  const entities = [];
  
  for (let i = 0; i < count; i++) {
    const lon = bounds.minLon + Math.random() * (bounds.maxLon - bounds.minLon);
    const lat = bounds.minLat + Math.random() * (bounds.maxLat - bounds.minLat);
    const alt = bounds.minAlt + Math.random() * (bounds.maxAlt - bounds.minAlt);
    
    entities.push(new Cesium.Entity({
      id: `scattered-${i}-${Math.random().toString(36).slice(2, 8)}`,
      position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
    }));
  }
  
  return entities;
}

/**
 * Generate a gradient-based density pattern.
 * グラデーション分布のデータセットを生成します。
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @returns {Array<Cesium.Entity>} Generated entities
 */
function generateGradientPattern(bounds, count) {
  const entities = [];
  
  for (let i = 0; i < count; i++) {
    // X軸方向に密度が増加するグラデーション
    const x = Math.random();
    const densityWeight = x * x; // 二乗で密度を増加
    
    const lon = bounds.minLon + x * (bounds.maxLon - bounds.minLon);
    const lat = bounds.minLat + Math.random() * (bounds.maxLat - bounds.minLat);
    const alt = bounds.minAlt + Math.random() * (bounds.maxAlt - bounds.minAlt);
    
    // 密度の高いエリアでは複数のエンティティを近接配置
    if (densityWeight > 0.7 && Math.random() > 0.5) {
      const offset = 0.0001; // 約10m
      entities.push(new Cesium.Entity({
        id: `gradient-${i}-extra-${Math.random().toString(36).slice(2, 8)}`,
        position: Cesium.Cartesian3.fromDegrees(
          lon + (Math.random() - 0.5) * offset,
          lat + (Math.random() - 0.5) * offset,
          alt
        )
      }));
    }
    
    entities.push(new Cesium.Entity({
      id: `gradient-${i}-${Math.random().toString(36).slice(2, 8)}`,
      position: Cesium.Cartesian3.fromDegrees(lon, lat, alt)
    }));
  }
  
  return entities;
}

/**
 * Generate a mixed density pattern.
 * 混在分布のデータセットを生成します。
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @returns {Array<Cesium.Entity>} Generated entities
 */
function generateMixedPattern(bounds, count) {
  const entities = [];
  
  // 30%を高密度クラスター、70%を疎分布として配置
  const clusteredCount = Math.floor(count * 0.3);
  const scatteredCount = count - clusteredCount;
  
  // 高密度クラスター部分
  const clusteredEntities = generateClusteredPattern(bounds, clusteredCount, 2);
  entities.push(...clusteredEntities);
  
  // 疎分布部分
  const scatteredEntities = generateScatteredPattern(bounds, scatteredCount);
  entities.push(...scatteredEntities);
  
  return entities;
}

/**
 * Density pattern generators map (ADR-0011 Phase 1)
 * パラメータ最適化用密度パターンマップ
 */
export const DENSITY_PATTERNS = {
  clustered: generateClusteredPattern,
  scattered: generateScatteredPattern,
  gradient: generateGradientPattern,
  mixed: generateMixedPattern
};

/**
 * Generate test dataset with specified pattern
 * 指定したパターンでテストデータセットを生成
 * @param {string} pattern - Pattern name ('clustered', 'scattered', 'gradient', 'mixed')
 * @param {Object} bounds - Bounding box
 * @param {number} count - Total entity count
 * @returns {Array<Cesium.Entity>} Generated entities
 */
export function generatePatternData(pattern, bounds, count) {
  const generator = DENSITY_PATTERNS[pattern];
  if (!generator) {
    throw new Error(`Unknown pattern: ${pattern}. Available: ${Object.keys(DENSITY_PATTERNS).join(', ')}`);
  }
  return generator(bounds, count);
}