UNPKG

japan-map-selector

Version:

Interactive Japan map component for selecting prefectures and municipalities

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