581 lines
23 KiB
JavaScript
581 lines
23 KiB
JavaScript
import { CELL_SIZE, MAP_H, MAP_W, clamp, indexOf } from "./mapUtils.js";
|
||
|
||
function distToNearest(points, x, y, fallback = 999) {
|
||
let best = fallback;
|
||
for (const p of points) best = Math.min(best, Math.hypot(p.x - x, p.y - y));
|
||
return best;
|
||
}
|
||
|
||
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),
|
||
];
|
||
}
|
||
|
||
function fieldSample(field, fx, fy) {
|
||
const x0 = Math.max(0, Math.min(MAP_W - 1, Math.floor(fx)));
|
||
const y0 = Math.max(0, Math.min(MAP_H - 1, Math.floor(fy)));
|
||
const x1 = Math.max(0, Math.min(MAP_W - 1, x0 + 1));
|
||
const y1 = Math.max(0, Math.min(MAP_H - 1, y0 + 1));
|
||
const tx = fx - x0;
|
||
const ty = fy - y0;
|
||
|
||
const a = field[indexOf(x0, y0)];
|
||
const b = field[indexOf(x1, y0)];
|
||
const c = field[indexOf(x0, y1)];
|
||
const d = field[indexOf(x1, y1)];
|
||
|
||
return ((a * (1 - tx) + b * tx) * (1 - ty)) + ((c * (1 - tx) + d * tx) * ty);
|
||
}
|
||
|
||
function terrainColorContinuous(map, fx, fy, mode) {
|
||
const i = indexOf(Math.max(0, Math.min(MAP_W - 1, Math.floor(fx))), Math.max(0, Math.min(MAP_H - 1, Math.floor(fy))));
|
||
const isInside = Boolean(map.prefectureMask[i]);
|
||
|
||
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)];
|
||
} 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),
|
||
];
|
||
} 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;
|
||
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),
|
||
];
|
||
} 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];
|
||
}
|
||
|
||
return blendOutside(color, isInside);
|
||
}
|
||
|
||
function discreteColor(map, x, y, mode) {
|
||
const i = indexOf(x, y);
|
||
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
|
||
};
|
||
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],
|
||
];
|
||
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),
|
||
];
|
||
} else {
|
||
color = terrainColorContinuous(map, x, y, "terrain");
|
||
}
|
||
|
||
return blendOutside(color, Boolean(map.prefectureMask[i]));
|
||
}
|
||
|
||
function drawBase(ctx, map, mode, continuousTerrain) {
|
||
const width = MAP_W * CELL_SIZE;
|
||
const height = MAP_H * CELL_SIZE;
|
||
const img = ctx.createImageData(width, height);
|
||
const continuousModes = ["terrain", "suitability", "development", "all"];
|
||
|
||
if (continuousTerrain && continuousModes.includes(mode)) {
|
||
for (let py = 0; py < height; py++) {
|
||
const fy = py / CELL_SIZE;
|
||
for (let px = 0; px < width; px++) {
|
||
const fx = px / CELL_SIZE;
|
||
const [r, g, b] = terrainColorContinuous(map, fx, fy, mode === "all" ? "terrain" : mode);
|
||
|
||
const eL = fieldSample(map.elevation, fx - 0.6, fy);
|
||
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 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 + 3] = 255;
|
||
}
|
||
}
|
||
} else {
|
||
for (let y = 0; y < MAP_H; y++) {
|
||
for (let x = 0; x < MAP_W; x++) {
|
||
const [r, g, b] = discreteColor(map, x, y, mode);
|
||
for (let dy = 0; dy < CELL_SIZE; dy++) {
|
||
for (let dx = 0; dx < CELL_SIZE; dx++) {
|
||
const ii = ((y * CELL_SIZE + dy) * width + (x * CELL_SIZE + dx)) * 4;
|
||
img.data[ii] = r;
|
||
img.data[ii + 1] = g;
|
||
img.data[ii + 2] = b;
|
||
img.data[ii + 3] = 255;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
ctx.putImageData(img, 0, 0);
|
||
}
|
||
|
||
function drawPath(ctx, path, color, width, dashed = false) {
|
||
if (!path || path.length < 2) return;
|
||
ctx.save();
|
||
ctx.lineCap = "round";
|
||
ctx.lineJoin = "round";
|
||
ctx.strokeStyle = color;
|
||
ctx.lineWidth = width;
|
||
if (dashed) ctx.setLineDash([6, 5]);
|
||
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);
|
||
}
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
}
|
||
|
||
function segmentPointKey(p) {
|
||
return `${p[0]},${p[1]}`;
|
||
}
|
||
|
||
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);
|
||
}
|
||
return chains;
|
||
}
|
||
|
||
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]);
|
||
}
|
||
next.push(out[out.length - 1]);
|
||
out = next;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function drawSegments(ctx, segments, color, width, dashed = false, smooth = 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;
|
||
}
|
||
|
||
for (const seg of segments) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(seg[0][0] * CELL_SIZE, seg[0][1] * CELL_SIZE);
|
||
ctx.lineTo(seg[1][0] * CELL_SIZE, seg[1][1] * CELL_SIZE);
|
||
ctx.stroke();
|
||
}
|
||
ctx.restore();
|
||
}
|
||
|
||
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)", // 沿道 - クリーム
|
||
};
|
||
|
||
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]) continue;
|
||
const lu = map.landuse[i];
|
||
if (!colors[lu]) continue;
|
||
|
||
const px = x * CELL_SIZE;
|
||
const py = y * CELL_SIZE;
|
||
ctx.fillStyle = colors[lu];
|
||
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
|
||
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
ctx.restore();
|
||
}
|
||
|
||
function dot(ctx, p, radius, fill, stroke = "white") {
|
||
ctx.beginPath();
|
||
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.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) {
|
||
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";
|
||
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],
|
||
];
|
||
|
||
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 };
|
||
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.strokeText(p.name, x, y);
|
||
ctx.fillText(p.name, x, y);
|
||
occupied.push(box);
|
||
ctx.restore();
|
||
return true;
|
||
}
|
||
ctx.restore();
|
||
return false;
|
||
}
|
||
|
||
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) }))
|
||
.sort((a, b) => b.labelPriority - a.labelPriority);
|
||
for (const p of prioritized.slice(0, limit)) labelWithCollision(ctx, p, occupied);
|
||
}
|
||
|
||
export function drawMap(canvas, map, options) {
|
||
const ctx = canvas.getContext("2d");
|
||
if (!ctx) return;
|
||
|
||
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);
|
||
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);
|
||
|
||
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);
|
||
|
||
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)");
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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);
|
||
}
|
||
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)");
|
||
}
|
||
|
||
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.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)");
|
||
}
|
||
}
|
||
|
||
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);
|
||
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);
|
||
}
|
||
}
|