UNPKG

japan-map-selector

Version:

Interactive Japan map component for selecting prefectures and municipalities

1 lines 121 kB
{"version":3,"file":"index.cjs","sources":["../src/core/prefecture-codes.ts","../src/core/dynamic-data-loader.ts","../src/core/data-loader.ts","../src/core/map-renderer.ts","../src/core/tokyo-islands.ts","../src/core/themes.ts","../src/core/grid-deformer.ts","../src/core/japan-map-selector.ts","../src/core/attribution.ts"],"sourcesContent":["// 都道府県名と都道府県コードのマッピング\nexport const PREFECTURE_CODES: { [key: string]: string } = {\n '北海道': '01',\n '青森県': '02',\n '岩手県': '03',\n '宮城県': '04',\n '秋田県': '05',\n '山形県': '06',\n '福島県': '07',\n '茨城県': '08',\n '栃木県': '09',\n '群馬県': '10',\n '埼玉県': '11',\n '千葉県': '12',\n '東京都': '13',\n '神奈川県': '14',\n '新潟県': '15',\n '富山県': '16',\n '石川県': '17',\n '福井県': '18',\n '山梨県': '19',\n '長野県': '20',\n '岐阜県': '21',\n '静岡県': '22',\n '愛知県': '23',\n '三重県': '24',\n '滋賀県': '25',\n '京都府': '26',\n '大阪府': '27',\n '兵庫県': '28',\n '奈良県': '29',\n '和歌山県': '30',\n '鳥取県': '31',\n '島根県': '32',\n '岡山県': '33',\n '広島県': '34',\n '山口県': '35',\n '徳島県': '36',\n '香川県': '37',\n '愛媛県': '38',\n '高知県': '39',\n '福岡県': '40',\n '佐賀県': '41',\n '長崎県': '42',\n '熊本県': '43',\n '大分県': '44',\n '宮崎県': '45',\n '鹿児島県': '46',\n '沖縄県': '47'\n};","// 都道府県別データの動的読み込み\nimport type { GeoJSONFeature } from '../types';\n\nexport interface PrefectureDataIndex {\n prefectures: {\n [code: string]: {\n name: string;\n municipalityCount: number;\n files: {\n [precision: string]: {\n path: string;\n size: number;\n features: number;\n }\n }\n }\n };\n generated: string;\n totalSize: {\n original: number;\n compressed: number;\n };\n}\n\n// キャッシュの型定義\ninterface DataCache {\n [key: string]: GeoJSONFeature[];\n}\n\nexport class DynamicDataLoader {\n private baseUrl: string;\n private cache: DataCache = {};\n private index?: PrefectureDataIndex;\n private currentPrecision: string = 'medium';\n \n constructor(baseUrl: string = './src/data') {\n this.baseUrl = baseUrl;\n }\n \n // インデックスファイルを読み込む\n async loadIndex(): Promise<void> {\n if (this.index) return;\n \n const indexUrl = `${this.baseUrl}/prefecture-index.json`;\n const response = await fetch(indexUrl);\n \n if (!response.ok) {\n throw new Error(`Failed to load index file: ${response.statusText}`);\n }\n \n this.index = await response.json();\n }\n \n // 精度レベルを設定\n setPrecision(precision: string): void {\n this.currentPrecision = precision;\n }\n \n // 都道府県のデータが利用可能かチェック\n async isPrefectureAvailable(prefectureCode: string): Promise<boolean> {\n await this.loadIndex();\n return this.index?.prefectures[prefectureCode] !== undefined;\n }\n \n // 都道府県の市区町村データを読み込む\n async loadMunicipalitiesForPrefecture(prefectureCode: string): Promise<GeoJSONFeature[]> {\n await this.loadIndex();\n \n // キャッシュキー\n const cacheKey = `${prefectureCode}-${this.currentPrecision}`;\n \n // キャッシュをチェック\n if (this.cache[cacheKey]) {\n return this.cache[cacheKey];\n }\n \n // インデックスから情報を取得\n const prefInfo = this.index?.prefectures[prefectureCode];\n if (!prefInfo) {\n throw new Error(`Prefecture ${prefectureCode} not found in index`);\n }\n \n const fileInfo = prefInfo.files[this.currentPrecision];\n if (!fileInfo) {\n throw new Error(`Precision level ${this.currentPrecision} not available for prefecture ${prefectureCode}`);\n }\n \n // データファイルを読み込む\n const dataUrl = `${this.baseUrl}/${fileInfo.path}`;\n const response = await fetch(dataUrl);\n \n if (!response.ok) {\n throw new Error(`Failed to load municipality data: ${response.statusText}`);\n }\n \n const geoJson = await response.json();\n const features = geoJson.features as GeoJSONFeature[];\n \n // キャッシュに保存\n this.cache[cacheKey] = features;\n \n return features;\n }\n \n // 複数の都道府県のデータを並列で読み込む\n async loadMunicipalitiesForPrefectures(prefectureCodes: string[]): Promise<GeoJSONFeature[]> {\n const promises = prefectureCodes.map(code => \n this.loadMunicipalitiesForPrefecture(code)\n );\n \n const results = await Promise.all(promises);\n return results.flat();\n }\n \n // キャッシュをクリア\n clearCache(prefectureCode?: string): void {\n if (prefectureCode) {\n // 特定の都道府県のキャッシュをクリア\n Object.keys(this.cache).forEach(key => {\n if (key.startsWith(prefectureCode)) {\n delete this.cache[key];\n }\n });\n } else {\n // 全キャッシュをクリア\n this.cache = {};\n }\n }\n \n // 現在のキャッシュサイズを取得(デバッグ用)\n getCacheInfo(): { count: number; prefectures: string[] } {\n const prefectures = new Set<string>();\n Object.keys(this.cache).forEach(key => {\n const [prefCode] = key.split('-');\n prefectures.add(prefCode);\n });\n \n return {\n count: Object.keys(this.cache).length,\n prefectures: Array.from(prefectures)\n };\n }\n \n // データサイズ情報を取得\n async getDataSizeInfo(prefectureCode: string): Promise<{\n name: string;\n sizes: { [precision: string]: number };\n } | null> {\n await this.loadIndex();\n \n const prefInfo = this.index?.prefectures[prefectureCode];\n if (!prefInfo) return null;\n \n const sizes: { [precision: string]: number } = {};\n Object.entries(prefInfo.files).forEach(([precision, info]) => {\n sizes[precision] = info.size;\n });\n \n return {\n name: prefInfo.name,\n sizes\n };\n }\n}","// データローダー - GeoJSONデータの読み込みと解析\n\nimport { \n GeoJSONFeatureCollection, \n Prefecture, \n Municipality \n} from '../types';\nimport { PREFECTURE_CODES } from './prefecture-codes';\nimport { DynamicDataLoader } from './dynamic-data-loader';\n\n// 都道府県データの読み込み\nexport async function loadPrefectureData(url: string): Promise<Prefecture[]> {\n const response = await fetch(url);\n const data: GeoJSONFeatureCollection = await response.json();\n \n return data.features.map((feature) => {\n const bounds = calculateBounds(feature.geometry.coordinates);\n const prefectureName = feature.properties.N03_001;\n // 都道府県名からコードを取得\n const prefectureCode = PREFECTURE_CODES[prefectureName] || '00';\n \n return {\n code: prefectureCode,\n name: prefectureName,\n bounds,\n feature: {\n ...feature,\n properties: {\n ...feature.properties,\n N03_007: prefectureCode\n }\n }\n };\n });\n}\n\n// 市区町村データの読み込み\nexport async function loadMunicipalityData(url: string): Promise<Municipality[]> {\n const response = await fetch(url);\n const data: GeoJSONFeatureCollection = await response.json();\n \n return data.features.map(feature => {\n // N03_007がない場合は都道府県名から推定\n let prefectureCode = '00';\n if (feature.properties.N03_007) {\n prefectureCode = feature.properties.N03_007.substring(0, 2);\n } else if (feature.properties.N03_001) {\n // 都道府県名からコードを取得\n prefectureCode = PREFECTURE_CODES[feature.properties.N03_001] || '00';\n }\n \n const municipalityName = feature.properties.N03_004 || \n feature.properties.N03_003 || \n feature.properties.N03_002 || '';\n \n // コードがない場合は生成\n const code = feature.properties.N03_007 || `${prefectureCode}999`;\n \n return {\n code,\n name: municipalityName,\n prefectureCode,\n feature\n };\n }).filter(m => m.prefectureCode !== '00'); // 都道府県コードが不明なものは除外\n}\n\n// ジオメトリの境界ボックスを計算\nfunction calculateBounds(\n coordinates: number[][][] | number[][][][]\n): [[number, number], [number, number]] {\n let minLng = Infinity;\n let minLat = Infinity;\n let maxLng = -Infinity;\n let maxLat = -Infinity;\n\n const processCoordinate = (coord: number[]) => {\n minLng = Math.min(minLng, coord[0]);\n maxLng = Math.max(maxLng, coord[0]);\n minLat = Math.min(minLat, coord[1]);\n maxLat = Math.max(maxLat, coord[1]);\n };\n\n const processPolygon = (polygon: number[][]) => {\n polygon.forEach(processCoordinate);\n };\n\n // MultiPolygonの場合\n if (Array.isArray(coordinates[0][0][0])) {\n (coordinates as number[][][][]).forEach(polygon => {\n polygon.forEach(processPolygon);\n });\n } else {\n // Polygonの場合\n (coordinates as number[][][]).forEach(processPolygon);\n }\n\n return [[minLng, minLat], [maxLng, maxLat]];\n}\n\n// 都道府県コードから市区町村をフィルタリング\nexport function filterMunicipalitiesByPrefecture(\n municipalities: Municipality[],\n prefectureCode: string\n): Municipality[] {\n return municipalities.filter(m => m.prefectureCode === prefectureCode);\n}\n\n// 動的データローダーのインスタンス\nlet dynamicLoader: DynamicDataLoader | null = null;\n\n// 動的データローダーを初期化\nexport function initializeDynamicLoader(baseUrl?: string): DynamicDataLoader {\n dynamicLoader = new DynamicDataLoader(baseUrl);\n return dynamicLoader;\n}\n\n// 都道府県別の市区町村データを動的に読み込む\nexport async function loadMunicipalitiesForPrefecture(prefectureCode: string): Promise<Municipality[]> {\n if (!dynamicLoader) {\n throw new Error('Dynamic loader not initialized. Call initializeDynamicLoader first.');\n }\n \n const features = await dynamicLoader.loadMunicipalitiesForPrefecture(prefectureCode);\n \n return features.map(feature => {\n const municipalityName = feature.properties.N03_004 || \n feature.properties.N03_003 || \n feature.properties.N03_002 || '';\n \n const code = feature.properties.N03_007 || `${prefectureCode}999`;\n \n return {\n code,\n name: municipalityName,\n prefectureCode,\n feature\n };\n });\n}\n\n// 動的ローダーの精度を設定\nexport function setDynamicLoaderPrecision(precision: string): void {\n if (dynamicLoader) {\n dynamicLoader.setPrecision(precision);\n }\n}\n\n// 動的ローダーのキャッシュをクリア\nexport function clearDynamicLoaderCache(prefectureCode?: string): void {\n if (dynamicLoader) {\n dynamicLoader.clearCache(prefectureCode);\n }\n}","// 地図レンダリング用のユーティリティ関数\n\nimport { GeoJSONFeature } from '../types';\n\n// SVGパス生成用の投影設定\nexport interface ProjectionConfig {\n scale: number;\n translateX: number;\n translateY: number;\n center: [number, number];\n}\n\n// メルカトル投影の簡易実装\nexport function mercatorProjection(\n longitude: number, \n latitude: number, \n config: ProjectionConfig\n): [number, number] {\n // 数値のバリデーション\n if (!isFinite(longitude) || !isFinite(latitude)) {\n console.warn('Invalid coordinates for projection:', { longitude, latitude });\n return [0, 0];\n }\n \n const x = (longitude - config.center[0]) * config.scale + config.translateX;\n const y = (-latitude + config.center[1]) * config.scale + config.translateY;\n \n // 結果のバリデーション\n if (!isFinite(x) || !isFinite(y)) {\n console.warn('Projection resulted in non-finite values:', { x, y, longitude, latitude, config });\n return [0, 0];\n }\n \n return [x, y];\n}\n\n// GeoJSONジオメトリからSVGパスデータを生成\nexport function geometryToPath(\n geometry: GeoJSONFeature['geometry'],\n projection: ProjectionConfig,\n transform?: { scale: number; translateX: number; translateY: number }\n): string {\n const pathData: string[] = [];\n\n const processRing = (ring: number[][]) => {\n // 空のリングをスキップ\n if (!ring || ring.length === 0) {\n return;\n }\n \n let validCoords = 0;\n ring.forEach((coord, index) => {\n // 座標が正しい形式か確認\n if (!Array.isArray(coord) || coord.length < 2) {\n console.warn('Invalid coordinate:', coord);\n return;\n }\n const [lng, lat] = coord;\n // 数値であることを確認\n if (typeof lng !== 'number' || typeof lat !== 'number') {\n console.warn('Non-numeric coordinate:', coord);\n return;\n }\n let [x, y] = mercatorProjection(lng, lat, projection);\n \n // 追加の変換を適用(沖縄県用など)\n if (transform) {\n x = x * transform.scale + transform.translateX;\n y = y * transform.scale + transform.translateY;\n }\n \n if (validCoords === 0) {\n pathData.push(`M${x},${y}`);\n } else {\n pathData.push(`L${x},${y}`);\n }\n validCoords++;\n });\n \n // 有効な座標がある場合のみパスを閉じる\n if (validCoords > 0) {\n pathData.push('Z');\n }\n };\n\n const processPolygon = (polygon: number[][][]) => {\n polygon.forEach(ring => processRing(ring));\n };\n\n if (geometry.type === 'Polygon') {\n processPolygon(geometry.coordinates as number[][][]);\n } else if (geometry.type === 'MultiPolygon') {\n (geometry.coordinates as number[][][][]).forEach(polygon => {\n processPolygon(polygon);\n });\n }\n\n return pathData.join(' ');\n}\n\n// 境界ボックスから適切なビューボックスを計算\nexport function calculateViewBox(\n bounds: [[number, number], [number, number]],\n padding: number = 20\n): { viewBox: string; projection: ProjectionConfig } {\n const [[west, south], [east, north]] = bounds;\n const centerLng = (west + east) / 2;\n const centerLat = (south + north) / 2;\n \n const width = east - west;\n const height = north - south;\n \n // アスペクト比を考慮したスケール計算\n const scale = Math.min(\n (800 - 2 * padding) / width,\n (600 - 2 * padding) / height\n );\n \n const projection: ProjectionConfig = {\n scale,\n translateX: 400,\n translateY: 300,\n center: [centerLng, centerLat]\n };\n \n return {\n viewBox: `0 0 800 600`,\n projection\n };\n}\n\n// 日本全体の標準的な境界ボックス\nexport const JAPAN_BOUNDS: [[number, number], [number, number]] = [\n [122.0, 24.0], // 南西(沖縄)\n [146.0, 46.0] // 北東(北海道)\n];\n\n// デフォルトの投影設定(日本全体用)\nexport function getDefaultProjection(): ProjectionConfig {\n return {\n scale: 30, // スケールを小さくして全体が入るように\n translateX: 400,\n translateY: 300,\n center: [138.0, 38.0] // 中心を北に移動\n };\n}","// 東京都の離島市区町村リスト\nexport const TOKYO_ISLANDS = [\n \"小笠原村\",\n \"大島町\",\n \"利島村\",\n \"新島村\",\n \"神津島村\",\n \"三宅村\",\n \"御蔵島村\",\n \"八丈町\",\n \"青ヶ島村\"\n];\n\n// 市区町村が東京都の離島かどうかを判定\nexport function isTokyoIsland(municipalityName: string): boolean {\n return TOKYO_ISLANDS.includes(municipalityName);\n}\n\n// 東京都の離島の市区町村コード\nexport const TOKYO_ISLAND_CODES = [\n \"13361\", // 大島町\n \"13362\", // 利島村\n \"13363\", // 新島村\n \"13364\", // 神津島村\n \"13381\", // 三宅村\n \"13382\", // 御蔵島村\n \"13401\", // 八丈町\n \"13402\", // 青ヶ島村\n \"13421\" // 小笠原村\n];\n\n// 市区町村コードから離島かどうかを判定\nexport function isTokyoIslandByCode(municipalityCode: string): boolean {\n return TOKYO_ISLAND_CODES.includes(municipalityCode);\n}","// カラーテーマの定義\n\nimport { ColorTheme } from '../types';\n\nexport const themes: Record<string, ColorTheme> = {\n default: {\n name: 'デフォルト',\n prefectureFill: '#e0e0e0',\n prefectureStroke: '#999',\n prefectureHoverFill: '#b0d0f0',\n prefectureSelectedFill: '#90c0ff',\n municipalityFill: '#f0f0f0',\n municipalityStroke: '#666',\n municipalityHoverFill: '#b0d0f0',\n municipalitySelectedFill: '#90c0ff',\n backgroundColor: '#f8f8f8',\n strokeWidth: 0.5\n },\n \n dark: {\n name: 'ダーク',\n prefectureFill: '#2a2a2a',\n prefectureStroke: '#555',\n prefectureHoverFill: '#404040',\n prefectureSelectedFill: '#505050',\n municipalityFill: '#333333',\n municipalityStroke: '#555',\n municipalityHoverFill: '#404040',\n municipalitySelectedFill: '#505050',\n backgroundColor: '#1a1a1a',\n strokeWidth: 0.5\n },\n \n warm: {\n name: 'ウォーム',\n prefectureFill: '#ffe0cc',\n prefectureStroke: '#cc9966',\n prefectureHoverFill: '#ffcc99',\n prefectureSelectedFill: '#ff9966',\n municipalityFill: '#fff0e6',\n municipalityStroke: '#cc9966',\n municipalityHoverFill: '#ffcc99',\n municipalitySelectedFill: '#ff9966',\n backgroundColor: '#fff5f0',\n strokeWidth: 0.5\n },\n \n cool: {\n name: 'クール',\n prefectureFill: '#d0e8f2',\n prefectureStroke: '#6699cc',\n prefectureHoverFill: '#a0d0f0',\n prefectureSelectedFill: '#6699ff',\n municipalityFill: '#e8f4f8',\n municipalityStroke: '#6699cc',\n municipalityHoverFill: '#a0d0f0',\n municipalitySelectedFill: '#6699ff',\n backgroundColor: '#f0f8ff',\n strokeWidth: 0.5\n },\n \n monochrome: {\n name: 'モノクローム',\n prefectureFill: '#f0f0f0',\n prefectureStroke: '#333',\n prefectureHoverFill: '#ddd',\n prefectureSelectedFill: '#ccc',\n municipalityFill: '#f8f8f8',\n municipalityStroke: '#333',\n municipalityHoverFill: '#ddd',\n municipalitySelectedFill: '#ccc',\n backgroundColor: '#fff',\n strokeWidth: 0.8\n },\n \n colorful: {\n name: 'カラフル',\n prefectureFill: '#ff6b6b', // この値は実際には使われない(getPrefectureFillで上書き)\n prefectureStroke: '#fff',\n prefectureHoverFill: '#4ecdc4',\n prefectureSelectedFill: '#45b7d1',\n municipalityFill: '#fff3e0',\n municipalityStroke: '#ff6b6b',\n municipalityHoverFill: '#ffe0b2',\n municipalitySelectedFill: '#ffcc80',\n backgroundColor: '#fafafa',\n strokeWidth: 1.0\n },\n \n random: {\n name: 'ランダム',\n prefectureFill: '#808080', // この値は実際には使われない(getPrefectureFillで上書き)\n prefectureStroke: '#333',\n prefectureHoverFill: '#666',\n prefectureSelectedFill: '#444',\n municipalityFill: '#f0f0f0',\n municipalityStroke: '#666',\n municipalityHoverFill: '#e0e0e0',\n municipalitySelectedFill: '#d0d0d0',\n backgroundColor: '#f8f8f8',\n strokeWidth: 0.8\n }\n};\n\n// テーマを取得する関数\nexport function getTheme(themeNameOrObject: ColorTheme | string | undefined): ColorTheme {\n if (!themeNameOrObject) {\n return themes.default;\n }\n \n if (typeof themeNameOrObject === 'string') {\n return themes[themeNameOrObject] || themes.default;\n }\n \n // カスタムテーマの場合はそのまま返す\n return themeNameOrObject;\n}\n\n// 個別の色設定を適用する関数\nexport function applyColorOverrides(theme: ColorTheme, props: any): ColorTheme {\n return {\n ...theme,\n prefectureFill: props.prefectureColor || theme.prefectureFill,\n prefectureHoverFill: props.prefectureHoverColor || theme.prefectureHoverFill,\n municipalityFill: props.municipalityColor || theme.municipalityFill,\n municipalityHoverFill: props.municipalityHoverColor || theme.municipalityHoverFill\n };\n}\n\n// カラフルな色のパレット\nconst colorfulPalette = [\n '#FF6B6B', '#4ECDC4', '#45B7D1', '#F7DC6F', '#BB8FCE',\n '#85C1E2', '#F8B500', '#6C5CE7', '#FD79A8', '#FDCB6E',\n '#6AB04C', '#EB4D4B', '#22A6B3', '#F0932B', '#EB4D4B',\n '#686DE0', '#30336B', '#F9CA24', '#F0932B', '#EB4D4B',\n '#6AB04C', '#BAD7E9', '#2BCBBA', '#FEA47F', '#25CCF7',\n '#FD7272', '#54A0FF', '#00D2D3', '#1ABC9C', '#2ECC71',\n '#3498DB', '#9B59B6', '#34495E', '#16A085', '#27AE60',\n '#2980B9', '#8E44AD', '#2C3E50', '#F39C12', '#E67E22',\n '#E74C3C', '#ECF0F1', '#95A5A6', '#D0B41A', '#A29BFE'\n];\n\n// 都道府県コードから色を生成する関数\nexport function getPrefectureFillColor(prefectureCode: string, themeName: string): string {\n if (themeName === 'colorful') {\n // 都道府県コードを数値に変換してインデックスとして使用\n const index = parseInt(prefectureCode, 10) - 1;\n return colorfulPalette[index % colorfulPalette.length];\n } else if (themeName === 'random') {\n // 都道府県コードを基にした疑似ランダム色を生成\n const seed = parseInt(prefectureCode, 10);\n const hue = (seed * 137.5) % 360; // 黄金角を使用して均等に分布\n const saturation = 65 + (seed % 20); // 65-85%\n const lightness = 45 + (seed % 20); // 45-65%\n return `hsl(${hue}, ${saturation}%, ${lightness}%)`;\n }\n \n // その他のテーマはデフォルトの色を返す\n return themes[themeName]?.prefectureFill || themes.default.prefectureFill;\n}\n\n// 16進数カラーをRGBに変換\nfunction hexToRgb(hex: string): { r: number, g: number, b: number } | null {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result ? {\n r: parseInt(result[1], 16),\n g: parseInt(result[2], 16),\n b: parseInt(result[3], 16)\n } : null;\n}\n\n// 色を明るくする関数\nfunction lightenColor(hex: string, percent: number): string {\n const rgb = hexToRgb(hex);\n if (!rgb) return hex;\n \n const r = Math.min(255, Math.floor(rgb.r + (255 - rgb.r) * percent));\n const g = Math.min(255, Math.floor(rgb.g + (255 - rgb.g) * percent));\n const b = Math.min(255, Math.floor(rgb.b + (255 - rgb.b) * percent));\n \n return `rgb(${r}, ${g}, ${b})`;\n}\n\n// 市区町村の色を生成する関数\nexport function getMunicipalityFillColor(municipalityCode: string, prefectureCode: string, themeName: string): string {\n if (themeName === 'colorful') {\n // 都道府県の色を基準に、明るい色を生成\n const prefectureColor = getPrefectureFillColor(prefectureCode, themeName);\n return lightenColor(prefectureColor, 0.7); // 70%明るくする\n } else if (themeName === 'random') {\n // 市区町村コードを基にした色を生成\n const seed = parseInt(municipalityCode.slice(-3), 10); // 下3桁を使用\n const prefSeed = parseInt(prefectureCode, 10);\n const baseHue = (prefSeed * 137.5) % 360;\n const hue = (baseHue + seed * 15) % 360; // 都道府県の色相から少しずらす\n const saturation = 50 + (seed % 20); // 50-70% (都道府県より薄め)\n const lightness = 65 + (seed % 15); // 65-80% (都道府県より明るめ)\n return `hsl(${hue}, ${saturation}%, ${lightness}%)`;\n }\n \n // その他のテーマはデフォルトの色を返す\n return themes[themeName]?.municipalityFill || themes.default.municipalityFill;\n}\n\n// 市区町村のホバー色を生成する関数\nexport function getMunicipalityHoverFillColor(municipalityCode: string, prefectureCode: string, themeName: string): string {\n if (themeName === 'colorful') {\n // 市区町村の通常色を少し暗くする\n const baseColor = getMunicipalityFillColor(municipalityCode, prefectureCode, themeName);\n // RGBカラーから少し暗くする\n const match = baseColor.match(/rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)/);\n if (match) {\n const r = Math.max(0, parseInt(match[1]) - 30);\n const g = Math.max(0, parseInt(match[2]) - 30);\n const b = Math.max(0, parseInt(match[3]) - 30);\n return `rgb(${r}, ${g}, ${b})`;\n }\n return themes.colorful.municipalityHoverFill;\n } else if (themeName === 'random') {\n // ランダムテーマの場合もHSLで少し暗くする\n const baseColor = getMunicipalityFillColor(municipalityCode, prefectureCode, themeName);\n const match = baseColor.match(/hsl\\((\\d+),\\s*(\\d+)%,\\s*(\\d+)%\\)/);\n if (match) {\n const h = parseInt(match[1]);\n const s = parseInt(match[2]);\n const l = Math.max(30, parseInt(match[3]) - 15); // 明度を15%下げる\n return `hsl(${h}, ${s}%, ${l}%)`;\n }\n return themes.random.municipalityHoverFill;\n }\n \n // その他のテーマはデフォルトのホバー色を返す\n return themes[themeName]?.municipalityHoverFill || themes.default.municipalityHoverFill;\n}","// グリッドベースのディフォルメ処理\n\nimport { Prefecture, Municipality } from '../types';\n\nexport interface GridDeformerOptions {\n gridSize: number; // グリッドサイズ(度単位)\n preserveTopology: boolean; // トポロジーを保持するか\n}\n\nexport class GridDeformer {\n protected gridSize: number;\n protected preserveTopology: boolean;\n\n constructor(options: GridDeformerOptions = { gridSize: 0.1, preserveTopology: true }) {\n this.gridSize = options.gridSize;\n this.preserveTopology = options.preserveTopology;\n }\n\n // 座標をグリッドにスナップ\n protected snapToGrid(coord: number[]): number[] {\n const [lng, lat] = coord;\n const snappedLng = Math.round(lng / this.gridSize) * this.gridSize;\n const snappedLat = Math.round(lat / this.gridSize) * this.gridSize;\n return [snappedLng, snappedLat];\n }\n\n // ポリゴンの面積を計算(簡易版)\n protected calculatePolygonArea(polygon: number[][]): number {\n let area = 0;\n for (let i = 0; i < polygon.length - 1; i++) {\n area += polygon[i][0] * polygon[i + 1][1];\n area -= polygon[i + 1][0] * polygon[i][1];\n }\n return Math.abs(area / 2);\n }\n\n // ポリゴンの中心点を計算\n protected calculatePolygonCenter(polygon: number[][]): number[] {\n let sumX = 0, sumY = 0;\n const count = polygon.length - 1; // 最後の点は最初の点と同じなので除外\n for (let i = 0; i < count; i++) {\n sumX += polygon[i][0];\n sumY += polygon[i][1];\n }\n return [sumX / count, sumY / count];\n }\n\n // ポリゴンをディフォルメ\n protected deformPolygon(polygon: number[][]): number[][] {\n // 元のポリゴンの面積を計算\n const originalArea = this.calculatePolygonArea(polygon);\n \n const deformed = polygon.map(coord => this.snapToGrid(coord));\n \n // 重複点を削除(トポロジー保持のため)\n if (this.preserveTopology) {\n const unique: number[][] = [];\n const seen = new Set<string>();\n \n for (let i = 0; i < deformed.length; i++) {\n const key = `${deformed[i][0]},${deformed[i][1]}`;\n if (!seen.has(key)) {\n seen.add(key);\n unique.push(deformed[i]);\n }\n }\n \n // 最低3点必要(三角形)\n if (unique.length < 3) {\n // ポリゴンが小さすぎる場合、中心点を基準に最小の三角形または四角形を作成\n const center = this.calculatePolygonCenter(polygon);\n const snappedCenter = this.snapToGrid(center);\n \n // グリッドサイズの半分のオフセット\n const offset = this.gridSize / 2;\n \n // 小さな四角形を作成\n if (originalArea > this.gridSize * this.gridSize * 0.1) {\n return [\n [snappedCenter[0] - offset, snappedCenter[1] - offset],\n [snappedCenter[0] + offset, snappedCenter[1] - offset],\n [snappedCenter[0] + offset, snappedCenter[1] + offset],\n [snappedCenter[0] - offset, snappedCenter[1] + offset],\n [snappedCenter[0] - offset, snappedCenter[1] - offset]\n ];\n } else {\n // 非常に小さい場合は元の形状を保持\n return polygon;\n }\n }\n \n // 閉じたポリゴンにする\n if (unique[0][0] !== unique[unique.length - 1][0] || \n unique[0][1] !== unique[unique.length - 1][1]) {\n unique.push([...unique[0]]);\n }\n \n return unique;\n }\n \n return deformed;\n }\n\n // ジオメトリをディフォルメ\n deformGeometry(geometry: any): any {\n if (geometry.type === 'Polygon') {\n const deformedRings = geometry.coordinates.map((ring: number[][]) => \n this.deformPolygon(ring)\n );\n \n return {\n type: 'Polygon',\n coordinates: deformedRings\n };\n } else if (geometry.type === 'MultiPolygon') {\n // 各ポリゴンをディフォルメし、有効なものだけを保持\n const deformedPolygons = geometry.coordinates\n .map((polygon: number[][][]) => {\n const deformedRings = polygon.map((ring: number[][]) => this.deformPolygon(ring));\n \n // 外側のリング(最初のリング)の面積をチェック\n if (deformedRings.length > 0) {\n const area = this.calculatePolygonArea(deformedRings[0]);\n // 非常に小さいポリゴンは除外\n if (area < this.gridSize * this.gridSize * 0.01) {\n return null;\n }\n }\n \n return deformedRings;\n })\n .filter((polygon: number[][][] | null): polygon is number[][][] => polygon !== null);\n \n // 有効なポリゴンがない場合、最大のポリゴンを中心に四角形を作成\n if (deformedPolygons.length === 0) {\n // 元のジオメトリから最大のポリゴンを見つける\n let maxArea = 0;\n let maxPolygon = null;\n \n geometry.coordinates.forEach((polygon: number[][][]) => {\n if (polygon.length > 0) {\n const area = this.calculatePolygonArea(polygon[0]);\n if (area > maxArea) {\n maxArea = area;\n maxPolygon = polygon[0];\n }\n }\n });\n \n if (maxPolygon) {\n const center = this.calculatePolygonCenter(maxPolygon);\n const snappedCenter = this.snapToGrid(center);\n const offset = this.gridSize;\n \n // グリッドサイズの四角形を作成\n const square = [\n [snappedCenter[0] - offset, snappedCenter[1] - offset],\n [snappedCenter[0] + offset, snappedCenter[1] - offset],\n [snappedCenter[0] + offset, snappedCenter[1] + offset],\n [snappedCenter[0] - offset, snappedCenter[1] + offset],\n [snappedCenter[0] - offset, snappedCenter[1] - offset]\n ];\n \n return {\n type: 'Polygon',\n coordinates: [square]\n };\n }\n }\n \n // 単一のポリゴンになった場合\n if (deformedPolygons.length === 1) {\n return {\n type: 'Polygon',\n coordinates: deformedPolygons[0]\n };\n }\n \n return {\n type: 'MultiPolygon',\n coordinates: deformedPolygons\n };\n }\n \n return geometry;\n }\n\n // 都道府県をディフォルメ\n deformPrefecture(prefecture: Prefecture): Prefecture {\n return {\n ...prefecture,\n feature: {\n ...prefecture.feature,\n geometry: this.deformGeometry(prefecture.feature.geometry)\n }\n };\n }\n\n // 市区町村をディフォルメ\n deformMunicipality(municipality: Municipality): Municipality {\n return {\n ...municipality,\n feature: {\n ...municipality.feature,\n geometry: this.deformGeometry(municipality.feature.geometry)\n }\n };\n }\n\n // グリッドサイズを変更\n setGridSize(size: number) {\n this.gridSize = size;\n }\n\n // 特定地域用の調整(北海道、沖縄など)\n adjustForRegion(prefectureCode: string): void {\n switch (prefectureCode) {\n case '01': // 北海道\n this.gridSize = 0.2; // より大きなグリッド\n break;\n case '47': // 沖縄\n this.gridSize = 0.05; // より細かいグリッド\n break;\n case '13': // 東京\n this.gridSize = 0.02; // 都市部は細かく\n break;\n default:\n this.gridSize = 0.1; // デフォルト\n }\n }\n}\n\n// より高度なディフォルメ(六角形グリッド)\nexport class HexagonalGridDeformer extends GridDeformer {\n // 六角形グリッドにスナップ\n private snapToHexGrid(coord: number[]): number[] {\n const [lng, lat] = coord;\n const size = this.gridSize;\n \n // 六角形グリッドの計算\n const a = size * 2;\n const b = size * Math.sqrt(3);\n const c = size;\n \n // 最も近い六角形の中心を見つける\n const i = Math.round(lng / a);\n const j = Math.round(lat / b);\n \n // 偶数行と奇数行でオフセット\n const xOffset = (j % 2) * c;\n const hexX = i * a + xOffset;\n const hexY = j * b;\n \n return [hexX, hexY];\n }\n \n // ポリゴンをディフォルメ(オーバーライド)\n protected deformPolygon(polygon: number[][]): number[][] {\n const deformed = polygon.map(coord => this.snapToHexGrid(coord));\n \n if (this.preserveTopology) {\n const unique: number[][] = [];\n for (let i = 0; i < deformed.length; i++) {\n if (i === 0 || \n deformed[i][0] !== deformed[i-1][0] || \n deformed[i][1] !== deformed[i-1][1]) {\n unique.push(deformed[i]);\n }\n }\n \n if (unique.length < 3) {\n return polygon;\n }\n \n if (unique[0][0] !== unique[unique.length - 1][0] || \n unique[0][1] !== unique[unique.length - 1][1]) {\n unique.push([...unique[0]]);\n }\n \n return unique;\n }\n \n return deformed;\n }\n}","// フレームワーク非依存のコアロジック\n\nimport {\n JapanMapSelectorProps,\n MapState,\n Prefecture,\n Municipality,\n ColorTheme\n} from '../types';\nimport {\n loadPrefectureData,\n loadMunicipalityData,\n filterMunicipalitiesByPrefecture,\n initializeDynamicLoader,\n loadMunicipalitiesForPrefecture,\n setDynamicLoaderPrecision,\n clearDynamicLoaderCache\n} from './data-loader';\nimport {\n geometryToPath,\n calculateViewBox,\n getDefaultProjection,\n ProjectionConfig\n} from './map-renderer';\nimport { isTokyoIsland } from './tokyo-islands';\nimport { getTheme, applyColorOverrides, getPrefectureFillColor, getMunicipalityFillColor, getMunicipalityHoverFillColor } from './themes';\nimport { GridDeformer, HexagonalGridDeformer } from './grid-deformer';\n\nexport class JapanMapSelector {\n private prefectures: Prefecture[] = [];\n private municipalities: Municipality[] = [];\n private municipalitiesCache: Map<string, Municipality[]> = new Map();\n private state: MapState;\n private listeners: Map<string, Function[]> = new Map();\n private props: JapanMapSelectorProps;\n private currentProjection: ProjectionConfig;\n private theme: ColorTheme;\n private currentThemeName: string = 'default';\n private deformer: GridDeformer | null = null;\n private deformMode: 'none' | 'grid' | 'hexagon' = 'none';\n private isDynamicLoadingEnabled: boolean = false;\n\n constructor(props: JapanMapSelectorProps = {}) {\n this.props = {\n width: 800,\n height: 600,\n ...props\n };\n \n // テーマの設定\n this.theme = applyColorOverrides(\n getTheme(props.theme),\n props\n );\n if (typeof props.theme === 'string') {\n this.currentThemeName = props.theme;\n }\n\n this.currentProjection = getDefaultProjection();\n this.state = {\n selectedPrefecture: null,\n hoveredPrefecture: null,\n hoveredMunicipality: null,\n viewBox: '0 0 800 600',\n scale: 1,\n translateX: 0,\n translateY: 0,\n showTokyoIslands: false\n };\n }\n\n // データの初期化\n async initialize(prefectureDataUrl: string, municipalityDataUrl: string) {\n // 動的読み込みが有効な場合\n if (this.props.enableDynamicLoading) {\n this.isDynamicLoadingEnabled = true;\n // 動的ローダーを初期化\n const loader = initializeDynamicLoader(this.props.dynamicDataBaseUrl);\n // 精度レベルを設定\n if (this.props.simplificationLevel) {\n setDynamicLoaderPrecision(this.props.simplificationLevel);\n }\n // 都道府県データのみ読み込む\n this.prefectures = await loadPrefectureData(prefectureDataUrl);\n // 市区町村データは空の配列に\n this.municipalities = [];\n } else {\n // 従来の一括読み込み\n this.prefectures = await loadPrefectureData(prefectureDataUrl);\n this.municipalities = await loadMunicipalityData(municipalityDataUrl);\n }\n \n // 全都道府県の境界から最適な投影を計算\n this.currentProjection = this.calculateOptimalProjection();\n \n this.emit('initialized');\n }\n \n // 全都道府県が収まる最適な投影を計算\n private calculateOptimalProjection(): ProjectionConfig {\n if (this.prefectures.length === 0) {\n return getDefaultProjection();\n }\n \n let minLng = Infinity, maxLng = -Infinity;\n let minLat = Infinity, maxLat = -Infinity;\n \n // 沖縄県と離島を除外して本州の境界を計算\n this.prefectures.forEach(pref => {\n if (pref.name !== '沖縄県') {\n const [[west, south], [east, north]] = pref.bounds;\n \n // 東京都の離島(小笠原諸島など)を除外\n if (pref.name === '東京都') {\n // 本州部分のみ(東経142度以西)\n minLng = Math.min(minLng, west);\n maxLng = Math.max(maxLng, Math.min(east, 142.0));\n minLat = Math.min(minLat, Math.max(south, 34.5)); // 伊豆諸島の一部も除外\n maxLat = Math.max(maxLat, north);\n } else if (pref.name === '北海道') {\n // 北海道の場合、北方領土を除外した実際の範囲を使用\n // 北海道本土は概ね東経139.5度〜145.5度の範囲\n minLng = Math.min(minLng, Math.max(west, 139.5)); // 西側の実際の境界\n maxLng = Math.max(maxLng, Math.min(east, 145.5)); // 東経145.5度以西(北方領土除外)\n minLat = Math.min(minLat, south);\n maxLat = Math.max(maxLat, north);\n } else {\n minLng = Math.min(minLng, west);\n maxLng = Math.max(maxLng, east);\n minLat = Math.min(minLat, south);\n maxLat = Math.max(maxLat, north);\n }\n }\n });\n \n const centerLng = (minLng + maxLng) / 2;\n const centerLat = (minLat + maxLat) / 2 + 2.0; // 中心を北に移動して地図全体を下に表示\n const width = maxLng - minLng;\n const height = maxLat - minLat;\n \n // パディングを考慮してスケールを計算\n const padding = 40; // パディングを調整\n const scaleX = (this.props.width! - 2 * padding) / width;\n const scaleY = (this.props.height! - 2 * padding) / height;\n const scale = Math.min(scaleX, scaleY) * 1.0; // 通常のスケール\n \n return {\n scale,\n translateX: this.props.width! / 2, // 中央に配置\n translateY: this.props.height! / 2 - 20, // 少し上にずらして沖縄枠のスペースを作る\n center: [centerLng, centerLat]\n };\n }\n\n // イベントリスナーの登録\n on(event: string, listener: Function) {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, []);\n }\n this.listeners.get(event)!.push(listener);\n }\n\n // イベントリスナーの削除\n off(event: string, listener: Function) {\n const listeners = this.listeners.get(event);\n if (listeners) {\n const index = listeners.indexOf(listener);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n }\n }\n\n // イベントの発火\n emit(event: string, data?: any) {\n const listeners = this.listeners.get(event);\n if (listeners) {\n listeners.forEach(listener => listener(data));\n }\n }\n\n // 都道府県が選択可能かチェック\n isPrefectureSelectable(prefectureCode: string): boolean {\n if (!this.props.selectablePrefectures || this.props.selectablePrefectures.length === 0) {\n return true; // 未指定の場合は全て選択可能\n }\n return this.props.selectablePrefectures.includes(prefectureCode);\n }\n \n // 都道府県の選択\n async selectPrefecture(prefectureCode: string) {\n // 選択可能かチェック\n if (!this.isPrefectureSelectable(prefectureCode)) {\n return;\n }\n \n const prefecture = this.prefectures.find(p => p.code === prefectureCode);\n if (prefecture) {\n this.state.selectedPrefecture = prefecture;\n this.state.showTokyoIslands = false; // 離島表示をリセット\n \n // 動的読み込みが有効な場合\n if (this.isDynamicLoadingEnabled) {\n // 都道府県を切り替えた時点で市区町村データをクリア\n this.municipalities = [];\n \n // キャッシュをチェック\n if (!this.municipalitiesCache.has(prefectureCode)) {\n // 読み込み開始を通知\n if (this.props.onMunicipalityLoadStart) {\n this.props.onMunicipalityLoadStart(prefecture);\n }\n \n try {\n // 市区町村データを動的に読み込む\n const municipalitiesForPrefecture = await loadMunicipalitiesForPrefecture(prefectureCode);\n this.municipalitiesCache.set(prefectureCode, municipalitiesForPrefecture);\n \n // キャッシュから市区町村データを設定(読み込み終了通知の前に設定)\n this.municipalities = municipalitiesForPrefecture;\n \n // 読み込み終了を通知(データ設定後)\n if (this.props.onMunicipalityLoadEnd) {\n this.props.onMunicipalityLoadEnd(prefecture);\n }\n } catch (error) {\n console.error(`Failed to load municipalities for prefecture ${prefectureCode}:`, error);\n // エラー時は空の配列を設定\n this.municipalities = [];\n // 読み込みエラー時も終了を通知\n if (this.props.onMunicipalityLoadEnd) {\n this.props.onMunicipalityLoadEnd(prefecture);\n }\n return;\n }\n } else {\n // キャッシュから市区町村データを設定\n this.municipalities = this.municipalitiesCache.get(prefectureCode) || [];\n }\n }\n \n // 東京都・北海道の場合は本土の境界を使用\n let bounds = prefecture.bounds;\n if (prefectureCode === '13') {\n // 東京都本土の境界(23区周辺)\n bounds = [[138.5, 35.3], [140.0, 36.0]];\n } else if (prefectureCode === '01') {\n // 北海道本土の境界(北方領土を除外)\n // 北海道の実際の市区町村データから正確な境界を計算\n const hokkaidoMunicipalities = this.municipalities.filter(m => m.prefectureCode === '01');\n let actualMinLng = Infinity, actualMaxLng = -Infinity;\n let actualMinLat = Infinity, actualMaxLat = -Infinity;\n \n hokkaidoMunicipalities.forEach(municipality => {\n const geometry = municipality.feature.geometry;\n const updateBounds = (coord: number[]) => {\n const [lng, lat] = coord;\n // 北方領土の座標を除外\n if (lng < 145.5) {\n actualMinLng = Math.min(actualMinLng, lng);\n actualMaxLng = Math.max(actualMaxLng, lng);\n actualMinLat = Math.min(actualMinLat, lat);\n actualMaxLat = Math.max(actualMaxLat, lat);\n }\n };\n \n if (geometry.type === 'Polygon') {\n (geometry.coordinates as number[][][]).forEach(ring => \n ring.forEach(updateBounds)\n );\n } else if (geometry.type === 'MultiPolygon') {\n (geometry.coordinates as number[][][][]).forEach(polygon =>\n polygon.forEach(ring => ring.forEach(updateBounds))\n );\n }\n });\n \n // 計算された実際の境界を使用\n if (actualMinLng !== Infinity) {\n bounds = [[actualMinLng, actualMinLat], [actualMaxLng, actualMaxLat]];\n }\n }\n \n // ビューボックスを都道府県に合わせて調整\n const { viewBox, projection } = calculateViewBox(bounds);\n this.state.viewBox = viewBox;\n this.currentProjection = projection;\n \n this.emit('prefectureSelected', prefecture);\n this.emit('stateChanged', this.state);\n \n if (this.props.onPrefectureSelect) {\n this.props.onPrefectureSelect(prefecture);\n }\n }\n }\n\n // 市区町村の選択\n selectMunicipality(municipalityCode: string) {\n const municipality = this.municipalities.find(m => m.code === municipalityCode);\n if (municipality) {\n this.emit('municipalitySelected', municipality);\n \n if (this.props.onMunicipalitySelect) {\n this.props.onMunicipalitySelect(municipality);\n }\n }\n }\n\n // 都道府県一覧の取得\n getPrefectures(): Prefecture[] {\n return this.prefectures;\n }\n\n // 選択された都道府県の市区町村を取得\n getSelectedMunicipalities(): Municipality[] {\n if (!this.state.selectedPrefecture) {\n return [];\n }\n \n // 動的読み込みが有効な場合は、現在の municipalities 配列を使用\n let allMunicipalities: Municipality[];\n if (this.isDynamicLoadingEnabled) {\n allMunicipalities = this.municipalities;\n } else {\n allMunicipalities = filterMunicipalitiesByPrefecture(\n this.municipalities,\n this.state.selectedPrefecture.code\n );\n }\n \n // 東京都の場合、離島表示の設定に応じてフィルタリング\n if (this.state.selectedPrefecture.code === '13') {\n return allMunicipalities.filter(m => {\n const isIsland = isTokyoIsland(m.name);\n return this.state.showTokyoIslands ? isIsland : !isIsland;\n });\n }\n \n // 北海道の場合、北方領土の市区町村を除外\n if (this.state.selectedPrefecture.code === '01') {\n return allMunicipalities.filter(m => {\n // 市区町村の座標から北方領土かどうか判定\n if (m.feature.geometry.type === 'MultiPolygon') {\n const coordinates = m.feature.geometry.coordinates as number[][][][];\n // 最初のポリゴンの中心点をチェック\n if (coordinates.length > 0 && coordinates[0].length > 0) {\n const ring = coordinates[0][0];\n let sumLng = 0, sumLat = 0;\n ring.forEach(coord => {\n sumLng += coord[0];\n sumLat += coord[1];\n });\n const centerLng = sumLng / ring.length;\n const centerLat = sumLat / ring.length;\n // 北方領土の市区町村を除外\n return !(centerLat > 43.5 && centerLng > 145.5);\n }\n } else if (m.feature.geometry.type === 'Polygon') {\n const coordinates = m.feature.geometry.coordinates as number[][][];\n if (coordinates.length > 0) {\n const ring = coordinates[0];\n let sumLng = 0, sumLat = 0;\n ring.forEach(coord => {\n sumLng += coord[0];\n sumLat += coord[1];\n });\n const centerLng = sumLng / ring.length;\n const centerLat = sumLat / ring.length;\n // 北方領土の市区町村を除外\n return !(centerLat > 43.5 && centerLng > 145.5);\n }\n }\n return true;\n });\n }\n \n return allMunicipalities;\n }\n\n // 都道府県のSVGパスを生成\n getPrefecturePath(prefecture: Prefecture): string {\n // ディフォルメを適用\n const deformed = this.getDeformedPrefecture(prefecture);\n \n // 全国表示の時\n if (!this.state.selectedPrefecture) {\n let geometry = deformed.feature.geometry;\n \n // 離島を含む都道府県の本土部分のみを表示\n switch (prefecture.name) {\n case '北海道':\n // 北方領土を除外\n geometry = this.filterHokkaidoMainland(geometry);\n break;\n case '東京都':\n // 東京都の離島を除外(特定の離島のみを除外する)\n geometry = this.filterTokyoMainlandOnly(geometry);\n break;\n case '鹿児島県':\n // 鹿児島県の離島を除外(主に南方の離島)\n geometry = this.filterGeometryByLatitude(geometry, 30.0, 'north');\n break;\n case '長崎県':\n // 長崎県の離島を簡易的にフィルタリング(五島列島などを除妖)\n geometry = this.filterGeometryByBounds(geometry, [[129.0, 32.5], [130.0, 34.0]]);\n break;\n // case '新潟県':\n // // 佐渡島を除外 - 一旦コメントアウト\n // geometry = this.filterGeometryByLongitude(geometry, 138.3, 'east');\n // break;\n case '島根県':\n // 隠岐諸島を除外\n geometry = this.filterGeometryByLongitude(geometry, 133.0, 'west');\n break;\n case '沖縄県':\n // 沖縄県は特別な投影設定で表示\n // 沖縄県の実際の境界を使用して中心を計算\n const [[west, south], [east, north]] = prefecture.bounds;\n const okinawaCenterLng = (west + east) / 2;\n const okinawaCenterLat = (south + north) / 2;\n \n const okinawaProjection: ProjectionConfig = {\n scale: this.currentProjection.scale * 0.7, // 適度に小さくして枠内に収める\n translateX: 210, // 枠の中央に配置(枠x:120 + 幅180/2)\n translateY: 120, // 枠の中央に配置(枠y:50 + 高さ140/2)\n center: [okinawaCenterLng, okinawaCenterLat] // 動的に計算した中心\n };\n return geometryToPath(geometry, okinawaProjection);\n }\n \n return geometryToPath(geometry, this.currentProjection);\n }\n \n return geometryToPath(deformed.feature.geometry, this.currentProjection);\n }\n \n // 東京都の本土のみを残すフィルタリング\n private filterTokyoMainlandOnly(\n geometry: Prefecture['feature']['geometry']\n ): Prefecture['feature']['geometry'] {\n if (geometry.type === 'MultiPolygon') {\n const filteredCoordinates = (geometry.coordinates as number[][][][]).filter(polygon => {\n // ポリゴンの中心点を計算\n const ring = polygon[0];\n let sumLng = 0, sumLat = 0;\n ring.forEach((coord: number[]) => {\n sumLng += coord[0];\n sumLat += coord[1];\n });\n const centerLng = sumLng / ring.length;\n const centerLat = sumLat / ring.length;\n \n // 離島を除外する条件\n // 伊豆諸島(南方): 緯度34度以南\n // 小笠原諸島(東方): 経度141度以東\n const isMainland = !(\n (centerLat < 34.0) || // 伊豆諸島など\n (centerLng > 141.0) // 小笠原諸島\n );\n \n return isMainland;\n });\n \n \n return {\n type: 'MultiPolygon',\n coordinates: filteredCoordinates\n };\n }\n return geometry;\n }\n \n // 経度でジオメトリをフィルタリング\n private filterGeometryByLongitude(\n geometry: Prefecture['feature']['geometry'], \n threshold: number, \n direction: 'east' | 'west'\n ): Prefecture['feature']['geometry'] {\n if (geometry.type === 'MultiPolygon') {\n const originalCount = (geometry.coordinates as number[][][][]).length;\n const filteredCoordinates = (geometry.coordinates as number[][][][]).filter(polygon => {\n // ポリゴンの全ての点をチェックして、すべてが閾値の条件を満たすか確認\n const ring = polygon[0];\n const allPointsMatch = ring.every((coord: number[]) => {\n return direction === 'west' ? coord[0] < threshold : coord[0] > threshold;\n });\n return allPointsMatch;\n });\n \n \n return {\n type: 'MultiPolygon',\n coordinates: filteredCoordinates\n };\n }\n return geometry;\n }\n \n // 緯度でジオメトリをフィルタリング\n private filterGeometryByLatitude(\n geometry: Prefecture['feature']['geometry'], \n threshold: number, \n direction: 'north' | 'south'\n ): Prefecture['feature']['geometry'] {\n if (geometry.type === 'MultiPolygon') {\n const filteredCoordinates = (geometry.coordinates as number[][][][]).filter(polygon => {\n const ring = polygon[0];\n const allPointsMatch = ring.every((coord: number[]) => {\n return direction === 'north' ? coord[1] > threshold : coord[1] < threshold;\n });\n return allPointsMatch;\n });\n \n return {\n type: 'MultiPolygon',\n coordinates: filteredCoordinates\n };\n }\n return geometry;\n }\n \n // 境界ボックスでジオメトリをフィルタリング\n private filterGeometryByBounds(\n geometry: Prefecture['feature']['geometry'],\n bounds: [[number, number], [number, number]]\n ): Prefecture['feature']['geometry'] {\n if (geometry.type === 'MultiPolygon') {\n const [[west, south], [east, north]] = bounds;\n const filteredCoordinates = (geometry.coordinates as number[][][][]).filter(polygon => {\n const ring = polygon[0];\n const allPointsMatch = ring.every((coord: number[]) => {\n return coord[0] >= west && coord[0] <= east &&\n coord[1] >= south && coord[1] <= north;\n });\n return allPointsMatch;\n });\n \n return {\n type: 'MultiPolygon',\n coordinates: filteredCoordinates\n };\n }\n return geometry;\n }\n \n // 北海道の本土のみを残すフィルタリング(北方領土を除外)\n private filterHokkaidoMainland(\n geometry: Prefecture['feature']['geometry']\n ): Prefecture['feature']['geometry'] {\n if (geometry.type === 'MultiPolygon') {\n const filteredCoordinates = (geometry.coordinates as number[][][][]).filter(polygon => {\n // ポリゴンの中心点を計算\n const ring = polygon[0];\n let sumLng = 0, sumLat = 0;\n ring.forEach((coord: number[]) => {\n sumLng += coord[0];\n sumLat += coord[1];\n });\n const centerLng = sumLng / ring.length;\n const centerLat = sumLat / ring.length;\n \n // 北方領土を除外する条件\n // 緯度43.5度以北かつ東経145.5度以東の領域を除外\n const isMainland = !(centerLat > 43.5 && centerLng > 145.5);\n \n return isMainland;\n });\n \n \n return {\n type: 'MultiPolygon',\n coordinates: filteredCoordinates\n };\n }\n return geometry;\n }\n \n // 沖縄県の枠を取得\n