This commit is contained in:
33333-33333 2026-05-22 02:11:18 +09:00
commit da9d8ef904
11 changed files with 612 additions and 344 deletions

View file

@ -9,9 +9,9 @@ function distToNearest(points, x, y, fallback = 999) {
function blendOutside(color, isInside) {
if (isInside) return color;
return [
Math.round(color[0] * 0.55 + 112),
Math.round(color[1] * 0.55 + 112),
Math.round(color[2] * 0.55 + 112),
Math.round(color[0] * 0.8 + 50),
Math.round(color[1] * 0.8 + 50),
Math.round(color[2] * 0.8 + 50),
];
}
@ -38,47 +38,36 @@ function terrainColorContinuous(map, fx, fy, mode) {
let color;
if (map.sea[i]) {
// Google Map風の海の色明るい青
const depth = clamp((0.35 - fieldSample(map.elevation, fx, fy)) * 2.4);
color = [Math.round(170 + depth * 10), Math.round(211 + depth * 15), Math.round(223 + depth * 20)];
color = [Math.round(170 + depth * 5), Math.round(218 + depth * 10), Math.round(255 - depth * 5)];
} else if (mode === "suitability") {
const a = fieldSample(map.agriculture, fx, fy);
const p = fieldSample(map.plain, fx, fy);
const f = fieldSample(map.floodplain, fx, fy);
color = [
Math.round(230 - p * 25 + f * 15),
Math.round(235 + a * 15),
Math.round(210 - a * 25 + p * 20),
Math.round(240 - p * 15 + f * 10),
Math.round(242 + a * 10),
Math.round(235 - a * 15 + p * 10),
];
} else if (mode === "development") {
const x = Math.floor(fx);
const y = Math.floor(fy);
const baseIndex = indexOf(Math.max(0, Math.min(MAP_W - 1, x)), Math.max(0, Math.min(MAP_H - 1, y)));
const dCity = distToNearest(map.modernCities, fx, fy);
const dInd = distToNearest(map.industrialZones, fx, fy);
const dLog = distToNearest(map.logisticsParks, fx, fy);
const dNew = distToNearest(map.newTowns, fx, fy);
const urban = clamp(1 - dCity / 25);
const density = map.populationDensity ? fieldSample(map.populationDensity, fx, fy) : urban;
const industrial = clamp(1 - dInd / 10);
const logistics = clamp(1 - dLog / 9);
const newTown = clamp(1 - dNew / 9);
const base = 214 + map.plain[baseIndex] * 22;
const base = 235;
color = [
Math.round(base + density * 30 + urban * 8 + industrial * 14),
Math.round(base + density * 10 + logistics * 15 + newTown * 12),
Math.round(208 + map.agriculture[baseIndex] * 22 + density * 18 + newTown * 22),
Math.round(base + density * 20),
Math.round(base + density * 5),
Math.round(230 + density * 10),
];
} else {
// 起伏の大きさが読めるよう、標高段彩をやや強める
// 地形色を少し濃く(暗く)調整
const e = fieldSample(map.elevation, fx, fy);
const m = fieldSample(map.moisture, fx, fy);
if (e > 0.82) color = [178, 170, 160];
else if (e > 0.68) color = [198, 188, 164];
else if (e > 0.52) color = [205, 222 + m * 5, 184];
else if (e > 0.34) color = [224, 238 + m * 6, 206];
else if (e > 0.24) color = [236, 245 + m * 5, 220];
else color = [218, 232 + m * 6, 206];
if (e > 0.82) color = [210, 205, 195];
else if (e > 0.68) color = [218, 215, 205];
else if (e > 0.52) color = [220, 225, 210];
else if (e > 0.34) color = [225, 230, 215];
else if (e > 0.24) color = [230, 235, 220];
else color = [238, 242, 228];
}
return blendOutside(color, isInside);
@ -89,62 +78,31 @@ function discreteColor(map, x, y, mode) {
let color;
if (map.sea[i]) {
// Google Map like styled
color = [170, 218, 255];
} else if (mode === "landuse") {
const colors = {
0: [230, 242, 220], // farmland
1: [235, 245, 225], // plain
2: [235, 230, 220], // old city
3: [224, 202, 190], // CBD
4: [245, 240, 230], // suburb
5: [220, 220, 225], // industrial area
6: [225, 235, 225], // logistics area
7: [238, 242, 248], // new town
8: [248, 242, 230], // coastal development
9: [225, 238, 220], // others
0: [242, 248, 238],
1: [248, 250, 245],
2: [240, 238, 232],
3: [245, 230, 220],
4: [250, 248, 245],
5: [235, 235, 240],
6: [240, 245, 240],
7: [245, 248, 252],
8: [250, 248, 240],
9: [240, 245, 238],
};
color = colors[map.landuse[i]] || colors[0];
} else if (mode === "admin") {
const palette = [
[245, 235, 230],
[235, 245, 235],
[240, 240, 250],
[250, 245, 230],
[245, 240, 248],
[230, 245, 245],
[250, 240, 240],
[240, 250, 235],
[250, 245, 242], [245, 250, 245], [245, 245, 252],
[252, 250, 242], [250, 245, 250], [242, 250, 250]
];
const a = map.adminId[i];
color = a >= 0 ? palette[a % palette.length] : [220, 225, 220];
} else if (mode === "terrain-debug") {
const ridge = clamp(map.ridgeField[i] * 0.68 + (map.arcSpineField?.[i] || 0) * 0.42 + (map.branchRidgeField?.[i] || 0) * 0.34);
const deposit = clamp((map.depositionField?.[i] || 0) * 5.0 + (map.depositionalLowland?.[i] || 0) * 0.48 + (map.alluvialFanField?.[i] || 0) * 0.34 + (map.deltaField?.[i] || 0) * 0.46);
const valley = clamp(map.valleyField[i] * 0.72 + map.river[i] * 0.22);
const barrier = clamp(map.naturalBarrierScore?.[i] || ridge);
color = [
Math.round(220 - deposit * 70 + ridge * 48),
Math.round(226 + deposit * 38 + valley * 28 - barrier * 52),
Math.round(214 + valley * 58 + barrier * 34 - ridge * 42),
];
} else if (mode === "admin-debug" || mode === "borders-debug") {
const barrier = clamp(
map.ridgeField[i] * 0.88 +
Math.max(0, map.river[i] - 0.28) * 1.25 +
Math.max(0, map.flowAccum[i] - 0.36) * 0.72 +
map.slope[i] * 0.48 +
Math.max(0, map.elevation[i] - 0.54) * 0.34
);
color = [
Math.round(238 - barrier * 28),
Math.round(242 - barrier * 88),
Math.round(226 + barrier * 20),
];
color = a >= 0 ? palette[a % palette.length] : [240, 242, 240];
} else {
color = terrainColorContinuous(map, x, y, "terrain");
}
return blendOutside(color, Boolean(map.prefectureMask[i]));
}
@ -165,17 +123,12 @@ function drawBase(ctx, map, mode, continuousTerrain) {
const eR = fieldSample(map.elevation, fx + 0.6, fy);
const eU = fieldSample(map.elevation, fx, fy - 0.6);
const eD = fieldSample(map.elevation, fx, fy + 0.6);
const shade = clamp(0.9 + (eR - eL) * 1.0 + (eD - eU) * 0.65, 0.72, 1.18);
const elevation = fieldSample(map.elevation, fx, fy);
const contour = Math.abs((elevation * 16) - Math.round(elevation * 16));
const majorContour = Math.abs((elevation * 8) - Math.round(elevation * 8));
const isLand = !map.sea[indexOf(Math.max(0, Math.min(MAP_W - 1, Math.floor(fx))), Math.max(0, Math.min(MAP_H - 1, Math.floor(fy))))];
const contourFactor = isLand && majorContour < 0.022 ? 0.86 : isLand && contour < 0.028 ? 0.94 : 1;
const shade = clamp(0.95 + (eR - eL) * 0.6 + (eD - eU) * 0.4, 0.85, 1.08);
const ii = (py * width + px) * 4;
img.data[ii] = Math.round(r * shade * contourFactor);
img.data[ii + 1] = Math.round(g * shade * contourFactor);
img.data[ii + 2] = Math.round(b * shade * contourFactor);
img.data[ii] = Math.round(r * shade);
img.data[ii + 1] = Math.round(g * shade);
img.data[ii + 2] = Math.round(b * shade);
img.data[ii + 3] = 255;
}
}
@ -195,7 +148,6 @@ function drawBase(ctx, map, mode, continuousTerrain) {
}
}
}
ctx.putImageData(img, 0, 0);
}
@ -206,7 +158,7 @@ function drawPath(ctx, path, color, width, dashed = false) {
ctx.lineJoin = "round";
ctx.strokeStyle = color;
ctx.lineWidth = width;
if (dashed) ctx.setLineDash([6, 5]);
if (dashed) ctx.setLineDash([8, 6]);
ctx.beginPath();
ctx.moveTo(path[0][0] * CELL_SIZE + CELL_SIZE / 2, path[0][1] * CELL_SIZE + CELL_SIZE / 2);
for (let k = 1; k < path.length; k++) {
@ -216,72 +168,64 @@ function drawPath(ctx, path, color, width, dashed = false) {
ctx.restore();
}
function segmentPointKey(p) {
return `${p[0]},${p[1]}`;
}
// 魚の骨(私鉄記号)スタイルを描画するための専用関数
function drawRailway(ctx, path, color, lineWidth, tickLen, spacing) {
if (!path || path.length < 2) return;
ctx.save();
ctx.lineCap = "butt";
ctx.lineJoin = "round";
ctx.strokeStyle = color;
function chainSegments(segments) {
const unused = segments.map((seg) => [seg[0], seg[1]]);
const chains = [];
while (unused.length) {
const chain = unused.pop();
let grew = true;
while (grew) {
grew = false;
const head = segmentPointKey(chain[0]);
const tail = segmentPointKey(chain[chain.length - 1]);
for (let i = unused.length - 1; i >= 0; i--) {
const [a, b] = unused[i];
const ak = segmentPointKey(a);
const bk = segmentPointKey(b);
if (ak === tail) { chain.push(b); unused.splice(i, 1); grew = true; break; }
if (bk === tail) { chain.push(a); unused.splice(i, 1); grew = true; break; }
if (bk === head) { chain.unshift(a); unused.splice(i, 1); grew = true; break; }
if (ak === head) { chain.unshift(b); unused.splice(i, 1); grew = true; break; }
}
}
chains.push(chain);
// 中心の実線を描画
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.moveTo(path[0][0] * CELL_SIZE + CELL_SIZE / 2, path[0][1] * CELL_SIZE + CELL_SIZE / 2);
for (let k = 1; k < path.length; k++) {
ctx.lineTo(path[k][0] * CELL_SIZE + CELL_SIZE / 2, path[k][1] * CELL_SIZE + CELL_SIZE / 2);
}
return chains;
}
ctx.stroke();
function chaikin(points, passes = 1) {
let out = points;
for (let pass = 0; pass < passes; pass++) {
if (out.length < 3) return out;
const next = [out[0]];
for (let i = 0; i < out.length - 1; i++) {
const a = out[i];
const b = out[i + 1];
next.push([a[0] * 0.75 + b[0] * 0.25, a[1] * 0.75 + b[1] * 0.25]);
next.push([a[0] * 0.25 + b[0] * 0.75, a[1] * 0.25 + b[1] * 0.75]);
// 棘(クロスハッチ)を描画
ctx.lineWidth = 1.0;
ctx.beginPath();
let leftover = 0;
for (let k = 0; k < path.length - 1; k++) {
const x1 = path[k][0] * CELL_SIZE + CELL_SIZE / 2;
const y1 = path[k][1] * CELL_SIZE + CELL_SIZE / 2;
const x2 = path[k+1][0] * CELL_SIZE + CELL_SIZE / 2;
const y2 = path[k+1][1] * CELL_SIZE + CELL_SIZE / 2;
const dx = x2 - x1;
const dy = y2 - y1;
const dist = Math.hypot(dx, dy);
if (dist === 0) continue;
// 法線(直角)ベクトル
const nx = dx / dist;
const ny = dy / dist;
const px = -ny * (tickLen / 2);
const py = nx * (tickLen / 2);
let d = (spacing / 2) + leftover;
while (d < dist) {
const cx = x1 + nx * d;
const cy = y1 + ny * d;
ctx.moveTo(cx + px, cy + py);
ctx.lineTo(cx - px, cy - py);
d += spacing;
}
next.push(out[out.length - 1]);
out = next;
leftover = d - dist;
}
return out;
ctx.stroke();
ctx.restore();
}
function drawSegments(ctx, segments, color, width, dashed = false, smooth = false) {
function drawSegments(ctx, segments, color, width, dashed = false) {
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = "round";
ctx.lineJoin = "round";
if (dashed) ctx.setLineDash([4, 4]);
if (smooth) {
for (const chain of chainSegments(segments)) {
const points = chaikin(chain, 1);
if (points.length < 2) continue;
ctx.beginPath();
ctx.moveTo(points[0][0] * CELL_SIZE, points[0][1] * CELL_SIZE);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0] * CELL_SIZE, points[i][1] * CELL_SIZE);
ctx.stroke();
}
ctx.restore();
return;
}
if (dashed) ctx.setLineDash([6, 5]);
for (const seg of segments) {
ctx.beginPath();
@ -296,15 +240,14 @@ function drawUrbanAreas(ctx, map, mode) {
const visibleModes = ["all", "modern", "development", "landuse", "roads", "admin"];
if (!visibleModes.includes(mode)) return;
// Google Map風の都市部の色
const colors = {
2: "rgba(235, 230, 220, 0.75)", // 旧市街 - 薄いベージュ
3: "rgba(224, 202, 190, 0.9)", // 中心市街地 / CBD - cell fill
4: "rgba(245, 242, 235, 0.7)", // 郊外 - 薄いクリーム
5: "rgba(220, 220, 228, 0.8)", // 工業地域 - 薄いグレー
6: "rgba(225, 235, 228, 0.75)", // 物流 - 薄い緑グレー
7: "rgba(238, 242, 250, 0.75)", // ニュータウン - 薄い青白
8: "rgba(248, 245, 238, 0.7)", // 沿道 - クリーム
2: "rgba(225, 222, 215, 0.6)",
3: "rgba(240, 220, 205, 0.85)",
4: "rgba(242, 240, 235, 0.5)",
5: "rgba(220, 220, 225, 0.6)",
6: "rgba(225, 230, 225, 0.5)",
7: "rgba(235, 240, 245, 0.6)",
8: "rgba(245, 242, 235, 0.5)",
};
ctx.save();
@ -319,38 +262,22 @@ function drawUrbanAreas(ctx, map, mode) {
const py = y * CELL_SIZE;
ctx.fillStyle = colors[lu];
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
}
}
ctx.restore();
}
const h = ((x * 92821 + y * 68917 + lu * 131) >>> 0);
if (lu === 3 || lu === 2 || lu === 5 || lu === 6) {
// 建物の表現を控えめに
ctx.fillStyle = lu === 3 ? "rgba(200,200,200,0.25)" : "rgba(210,210,210,0.2)";
if (h % 3 !== 0) ctx.fillRect(px + 1, py + 1, 2, 2);
if (h % 5 !== 0) ctx.fillRect(px + 3, py + 2, 2, 2);
if (h % 7 !== 0) ctx.fillRect(px + 2, py + 4, 2, 1.5);
} else if (lu === 4 || lu === 7) {
ctx.strokeStyle = lu === 7 ? "rgba(220,220,230,0.15)" : "rgba(200,190,180,0.15)";
ctx.lineWidth = 0.8;
ctx.beginPath();
if (h % 2 === 0) {
ctx.moveTo(px + 1, py + 1);
ctx.lineTo(px + CELL_SIZE - 1, py + 1);
ctx.moveTo(px + 1, py + 4);
ctx.lineTo(px + CELL_SIZE - 1, py + 4);
} else {
ctx.moveTo(px + 1, py + 1);
ctx.lineTo(px + 1, py + CELL_SIZE - 1);
ctx.moveTo(px + 4, py + 1);
ctx.lineTo(px + 4, py + CELL_SIZE - 1);
}
ctx.stroke();
} else if (lu === 8) {
ctx.strokeStyle = "rgba(180,170,160,0.2)";
ctx.lineWidth = 0.9;
ctx.beginPath();
ctx.moveTo(px + 1, py + 3);
ctx.lineTo(px + CELL_SIZE - 1, py + 3);
ctx.stroke();
}
function drawDebugCells(ctx, map, field, color) {
if (!field) return;
ctx.save();
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
const i = indexOf(x, y);
if (!map.prefectureMask[i] || map.sea[i]) continue;
const v = clamp(field[i] || 0, 0, 1);
if (v <= 0.12) continue;
ctx.fillStyle = color(v);
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
}
}
ctx.restore();
@ -361,91 +288,41 @@ function dot(ctx, p, radius, fill, stroke = "white") {
ctx.arc(p.x * CELL_SIZE + CELL_SIZE / 2, p.y * CELL_SIZE + CELL_SIZE / 2, radius, 0, Math.PI * 2);
ctx.fillStyle = fill;
ctx.fill();
ctx.lineWidth = 1.5;
ctx.lineWidth = 1.2;
ctx.strokeStyle = stroke;
ctx.stroke();
}
function squareIcon(ctx, p, size, fill, stroke = "white") {
const x = p.x * CELL_SIZE + CELL_SIZE / 2;
const y = p.y * CELL_SIZE + CELL_SIZE / 2;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.rect(x - size / 2, y - size / 2, size, size);
ctx.fill();
ctx.stroke();
ctx.restore();
}
function triangleIcon(ctx, p, size, fill, stroke = "white") {
const x = p.x * CELL_SIZE + CELL_SIZE / 2;
const y = p.y * CELL_SIZE + CELL_SIZE / 2;
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(x, y - size / 2);
ctx.lineTo(x + size / 2, y + size / 2);
ctx.lineTo(x - size / 2, y + size / 2);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}
function railStationIcon(ctx, p) {
squareIcon(ctx, p, 5.2, "rgba(255,255,255,0.96)", "rgba(55,55,55,0.92)");
}
function drawHarborWorks(ctx, map) {
if (!map.harborWorks) return;
ctx.save();
ctx.strokeStyle = "rgba(95, 120, 150, 0.9)";
ctx.lineWidth = 2.2;
ctx.lineCap = "round";
for (const harbor of map.harborWorks) {
for (const seg of harbor.segments || []) {
ctx.beginPath();
ctx.moveTo(seg[0][0] * CELL_SIZE + CELL_SIZE / 2, seg[0][1] * CELL_SIZE + CELL_SIZE / 2);
ctx.lineTo(seg[1][0] * CELL_SIZE + CELL_SIZE / 2, seg[1][1] * CELL_SIZE + CELL_SIZE / 2);
ctx.stroke();
}
}
ctx.restore();
}
function boxesOverlap(a, b, pad = 2) {
function boxesOverlap(a, b, pad = 3) {
return !(a.x2 + pad < b.x1 || a.x1 - pad > b.x2 || a.y2 + pad < b.y1 || a.y1 - pad > b.y2);
}
function labelWithCollision(ctx, p, occupied) {
if (!p.name) return false;
ctx.save();
ctx.font = "11px ui-sans-serif, system-ui, sans-serif";
ctx.font = "600 11.5px ui-sans-serif, system-ui, -apple-system, sans-serif";
const baseX = p.x * CELL_SIZE + CELL_SIZE / 2;
const baseY = p.y * CELL_SIZE + CELL_SIZE / 2;
const textW = ctx.measureText(p.name).width;
const textH = 12;
const candidates = [
[7, -5], [7, 12], [-textW - 7, -5], [-textW - 7, 12],
[-textW / 2, -13], [-textW / 2, 20], [12, 2], [-textW - 12, 2],
[7, -4], [7, 13], [-textW - 7, -4], [-textW - 7, 13],
[-textW / 2, -12], [-textW / 2, 20], [12, 4], [-textW - 12, 4],
];
for (const [ox, oy] of candidates) {
const x = baseX + ox;
const y = baseY + oy;
const box = { x1: x - 2, y1: y - textH, x2: x + textW + 2, y2: y + 3 };
const box = { x1: x - 2, y1: y - textH, x2: x + textW + 2, y2: y + 4 };
if (box.x1 < 0 || box.y1 < 0 || box.x2 > MAP_W * CELL_SIZE || box.y2 > MAP_H * CELL_SIZE) continue;
if (occupied.some((b) => boxesOverlap(box, b))) continue;
ctx.lineWidth = 3;
ctx.strokeStyle = "rgba(255,255,255,0.95)";
ctx.fillStyle = "rgba(40,40,40,0.95)";
ctx.lineJoin = "round";
ctx.lineWidth = 3.5;
ctx.strokeStyle = "rgba(255, 255, 255, 0.95)";
ctx.strokeText(p.name, x, y);
ctx.fillStyle = p.isPrefecturalCapital ? "#111111" : "#333333";
ctx.fillText(p.name, x, y);
occupied.push(box);
ctx.restore();
@ -459,7 +336,7 @@ function drawLabels(ctx, points, limit = Infinity) {
const occupied = [];
const prioritized = points
.filter((p) => p?.name)
.map((p) => ({ ...p, labelPriority: (p.labelPriorityBase || 0) + (p.isPrefecturalCapital ? 1000 : 0) + (p.population || 0) / 900 + (p.portClass === "major" ? 170 : p.portClass === "regional" ? 95 : 0) + (p.kind === "Market Town" ? 48 : 0) + (p.kind?.includes("Castle") ? 70 : 0) + (p.kind === "External Gateway" ? 60 : 0) + (p.kind === "Municipal Center" ? 34 : 0) }))
.map((p) => ({ ...p, labelPriority: (p.labelPriorityBase || 0) + (p.isPrefecturalCapital ? 1000 : 0) + (p.population || 0) / 900 + (p.portClass === "major" ? 170 : p.portClass === "regional" ? 95 : 0) }))
.sort((a, b) => b.labelPriority - a.labelPriority);
for (const p of prioritized.slice(0, limit)) labelWithCollision(ctx, p, occupied);
}
@ -471,111 +348,113 @@ export function drawMap(canvas, map, options) {
const mode = options.mode || "all";
const showFeatures = options.showFeatures !== false;
const showLabels = options.showLabels !== false;
const continuousTerrain = true;
const width = MAP_W * CELL_SIZE;
const height = MAP_H * CELL_SIZE;
canvas.width = width;
canvas.height = height;
drawBase(ctx, map, mode, continuousTerrain);
// 1. Base Terrain & Urban
drawBase(ctx, map, mode, true);
drawUrbanAreas(ctx, map, mode);
// Rivers use the same hue family as the sea; hierarchy is expressed by width/opacity.
const waterBlue = "rgba(170, 218, 255, 0.95)";
// Small streams remain in the data model but are not drawn by default.
for (const path of map.tributaryRivers || map.riverPaths || []) drawPath(ctx, path, "rgba(170, 218, 255, 0.78)", 1.35);
for (const path of map.mainRivers) drawPath(ctx, path, waterBlue, 3.2);
drawHarborWorks(ctx, map);
// 2. Rivers
const waterBlue = "rgba(160, 205, 240, 1)";
for (const path of map.tributaryRivers || map.riverPaths || []) drawPath(ctx, path, "rgba(160, 205, 240, 0.8)", 1.5);
for (const path of map.mainRivers) drawPath(ctx, path, waterBlue, 3.5);
const debugBorders = mode === "admin-debug" || mode === "borders-debug";
if (map.regionalPrefectureBorders) drawSegments(ctx, map.regionalPrefectureBorders, debugBorders ? "rgba(40,40,40,0.82)" : mode === "all" ? "rgba(95,95,95,0.18)" : "rgba(95,95,95,0.30)", debugBorders ? 1.8 : 1.0, false, mode === "all");
drawSegments(ctx, map.prefectureBorder, "rgba(30,30,30,0.82)", 2.4, false, true);
drawSegments(ctx, map.prefectureBorder, "rgba(255,255,255,0.74)", 1.05, false, true);
const showHistory = ["history", "all", "terrain"].includes(mode);
const showModern = ["modern", "all", "development", "landuse", "roads", "admin-debug", "borders-debug"].includes(mode);
const showRoads = ["roads", "all", "development"].includes(mode);
const showAdmin = ["admin", "all", "admin-debug", "borders-debug"].includes(mode);
// 3. Borders
if (showAdmin && map.adminBorders) {
drawSegments(ctx, map.adminBorders, "rgba(255, 255, 255, 0.8)", 2.8, false);
drawSegments(ctx, map.adminBorders, "rgba(150, 140, 150, 0.9)", 1.2, true);
}
if (mode === "admin-debug" || mode === "borders-debug") {
drawDebugCells(ctx, map, map.naturalBarrierScore, (v) => `rgba(255, 120, 40, ${0.06 + v * 0.18})`);
if (map.adminDebug?.compartmentBorders) drawSegments(ctx, map.adminDebug.compartmentBorders, "rgba(60, 110, 170, 0.42)", 0.8, true);
if (map.regionalPrefectureBorders) drawSegments(ctx, map.regionalPrefectureBorders, "rgba(70, 55, 95, 0.95)", 2.4, false);
}
drawSegments(ctx, map.prefectureBorder, "rgba(255, 255, 255, 0.95)", 5.0, false);
drawSegments(ctx, map.prefectureBorder, "rgba(110, 90, 110, 1)", 2.2, true);
if (!showFeatures) return;
const showHistory = ["history", "all", "terrain", "suitability"].includes(mode);
const showModern = ["modern", "all", "development", "landuse"].includes(mode);
const showRoads = ["roads", "all", "development", "landuse"].includes(mode);
const showAdmin = ["admin", "all", "admin-debug", "borders-debug"].includes(mode);
if (debugBorders && map.adminDebug?.compartmentBorders) drawSegments(ctx, map.adminDebug.compartmentBorders, "rgba(70,70,70,0.32)", 0.75);
if (showAdmin) drawSegments(ctx, map.adminBorders, debugBorders ? "rgba(20,90,180,0.96)" : mode === "all" ? "rgba(120,120,120,0.28)" : "rgba(120,120,120,0.65)", debugBorders ? 2.1 : mode === "all" ? 0.9 : 1.3);
if (showAdmin && mode !== "all") {
for (const p of map.adminCenters || []) dot(ctx, p, 3.0, "rgba(255,255,255,0.96)", "rgba(70,90,120,0.85)");
}
// 4. Casings (Outlines)
if (showHistory) {
for (const path of map.premodernRoads) drawPath(ctx, path, mode === "all" ? "rgba(150, 120, 90, 0.34)" : "rgba(150, 120, 90, 0.55)", mode === "all" ? 1.15 : 1.45, true);
for (const path of map.minorRoads) drawPath(ctx, path, mode === "all" ? "rgba(180, 150, 120, 0.28)" : "rgba(180, 150, 120, 0.62)", mode === "all" ? 0.8 : 1.05);
// 古い道は白の実線が引き立つように淡いケーシングを敷く
for (const path of map.premodernRoads) drawPath(ctx, path, "rgba(210, 210, 210, 0.7)", 2.5);
}
if (showModern) {
// 鉄道 - 濃いグレー
for (const path of map.railways) drawPath(ctx, path, "rgba(80, 80, 80, 0.9)", 2.8);
for (const path of map.ringRailways || []) drawPath(ctx, path, "rgba(70, 70, 70, 0.82)", 2.1);
for (const path of map.branchRailways) drawPath(ctx, path, "rgba(100, 100, 100, 0.82)", 1.9);
for (const path of map.externalRailways) drawPath(ctx, path, "rgba(90, 90, 90, 0.9)", 2.4);
}
if (showRoads) {
// Google Map風の道路 - 白と黄色とオレンジ
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.2);
for (const path of map.icAccessRoads || []) drawPath(ctx, path, "rgba(255, 230, 150, 0.82)", 1.45);
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.0);
for (const path of map.expressways) drawPath(ctx, path, "rgba(245, 140, 60, 0.95)", 4.0);
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.5);
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(245, 140, 60, 0.95)", 4.2);
}
if (showHistory) {
for (const p of map.villages) dot(ctx, p, mode === "all" ? 1.35 : 1.9, mode === "all" ? "rgba(120, 100, 80, 0.42)" : "rgba(120, 100, 80, 0.72)");
for (const p of map.markets) dot(ctx, p, 4.8, "rgba(200, 130, 80, 0.95)");
for (const p of map.ports) {
const color = p.portClass === "major" ? "rgba(40, 105, 190, 0.98)" : p.portClass === "regional" ? "rgba(70, 130, 200, 0.95)" : p.portClass === "lake" ? "rgba(80, 155, 180, 0.92)" : "rgba(95, 150, 195, 0.82)";
triangleIcon(ctx, p, p.portClass === "major" ? 8.2 : 6.5, color);
if (showModern || showRoads) {
// 鉄道のケーシング(白背景を敷いて視認性を保つ)
for (const path of map.railways) drawPath(ctx, path, "rgba(255, 255, 255, 0.75)", 3.5);
for (const path of map.branchRailways) drawPath(ctx, path, "rgba(255, 255, 255, 0.6)", 2.5);
for (const path of map.externalRailways) drawPath(ctx, path, "rgba(255, 255, 255, 0.75)", 3.5);
// 幹線道路のケーシング(色を濃く)
if (showRoads) {
for (const path of map.expressways) drawPath(ctx, path, "rgba(80, 140, 100, 1)", 5.0);
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(80, 140, 100, 1)", 5.0);
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
}
for (const p of map.crossings) dot(ctx, p, 3.5, "rgba(255, 240, 180, 0.95)", "rgba(100,90,70,0.8)");
for (const p of map.passes) dot(ctx, p, 3.9, "rgba(150, 120, 180, 0.95)");
for (const p of map.castles) squareIcon(ctx, p, 7.0, "rgba(180, 70, 70, 0.96)");
for (const p of map.castleRuins) dot(ctx, p, 3.2, "rgba(110, 90, 90, 0.9)", "rgba(220,220,220,0.8)");
}
// 5. Fills (Inner colors) & Fishbones
if (showHistory) {
for (const path of map.premodernRoads) drawPath(ctx, path, "rgba(255, 255, 255, 1)", 1.2, false);
}
if (showModern || showRoads) {
// 鉄道の骨線描画(色, 線幅, 棘の長さ, 棘の間隔)
for (const path of map.railways) drawRailway(ctx, path, "rgba(110, 110, 110, 1)", 1.5, 5.0, 7.0);
for (const path of map.branchRailways) drawRailway(ctx, path, "rgba(140, 140, 140, 1)", 1.0, 4.0, 6.0);
for (const path of map.externalRailways) drawRailway(ctx, path, "rgba(110, 110, 110, 1)", 1.5, 5.0, 7.0);
// 幹線道路の塗り(色を濃く)
if (showRoads) {
for (const path of map.expressways) drawPath(ctx, path, "rgba(110, 185, 130, 1)", 3.0);
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(110, 185, 130, 1)", 3.0);
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
}
}
// 6. Icons & Labels
if (showModern) {
for (const p of map.industrialZones) squareIcon(ctx, p, 6.5, "rgba(140, 140, 150, 0.96)");
for (const p of map.stations) railStationIcon(ctx, p);
for (const p of map.satelliteCities || []) dot(ctx, p, 4.8, "rgba(215, 95, 145, 0.95)");
for (const p of map.newTowns) triangleIcon(ctx, p, 6.5, "rgba(180, 210, 240, 0.95)");
for (const p of map.stations) dot(ctx, p, 2.5, "#fff", "#444");
for (const p of map.modernCities) {
const popRadius = p.population ? Math.min(9.2, 3.4 + Math.sqrt(p.population) / 360) : 4.7;
const rankBoost = p.isPrefecturalCapital || p.rank === "Prefectural Capital" ? 1.6 : p.rank === "Regional Center" ? 0.45 : 0;
dot(ctx, p, popRadius + rankBoost, "rgba(230, 100, 100, 0.95)");
if (p.isPrefecturalCapital) dot(ctx, p, popRadius + 4.2, "rgba(255,255,255,0.0)", "rgba(180,60,60,0.95)");
const popRadius = p.population ? Math.min(8.5, 3.5 + Math.sqrt(p.population) / 400) : 4.5;
dot(ctx, p, popRadius, "rgba(240, 110, 110, 0.95)", "rgba(255,255,255,0.9)");
if (p.isPrefecturalCapital) dot(ctx, p, popRadius + 3.0, "transparent", "rgba(200,80,80,0.9)");
}
if (mode === "admin-debug" || mode === "borders-debug") {
for (const p of map.externalGateways || []) dot(ctx, p, 5.5, "rgba(255,255,255,0.95)", "rgba(40,80,170,0.95)");
for (const p of map.transportDebug?.missingRequiredNodes || []) dot(ctx, p, 6.5, "rgba(255,80,80,0.95)", "rgba(80,20,20,0.95)");
}
}
if (showRoads) {
for (const p of map.logisticsParks) squareIcon(ctx, p, 6.2, "rgba(110, 170, 140, 0.96)");
for (const p of map.interchanges) dot(ctx, p, 4.1, "rgba(255, 255, 255, 0.98)", "rgba(220, 90, 60, 0.95)");
for (const p of map.externalGateways) dot(ctx, p, 4.4, "rgba(255, 250, 200, 0.98)", "rgba(60,60,60,0.92)");
}
if (showLabels) {
const adminLabels = (map.adminCenters || [])
.map((p) => ({ ...p, labelPriorityBase: mode === "admin" || mode === "admin-debug" || mode === "borders-debug" ? 90 : 8 }))
.filter((p) => mode !== "all" || p.representativeFeatureName);
if (mode === "admin") {
drawLabels(ctx, map.adminCenters || [], Infinity);
return;
}
if (mode === "admin-debug" || mode === "borders-debug") {
drawLabels(ctx, [...(map.adminCenters || []), ...(map.externalGateways || [])], Infinity);
return;
}
const important = [
...map.modernCities,
...map.ports,
...map.markets.slice(0, mode === "all" ? 6 : 10),
...map.castles.slice(0, mode === "all" ? 5 : 8),
...(map.satelliteCities || []),
...map.newTowns,
...(mode === "admin" || mode === "admin-debug" || mode === "borders-debug" ? adminLabels : adminLabels.slice(0, 8)),
...map.externalGateways,
].filter((p) => p.insidePrefecture || p.kind === "External Gateway");
drawLabels(ctx, important, mode === "all" ? 34 : Infinity);
drawLabels(ctx, important, 60);
}
}