japan-map-selector
Version:
Interactive Japan map component for selecting prefectures and municipalities
1,386 lines (1,378 loc) • 67.8 kB
JavaScript
'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