This commit is contained in:
33333-33333 2026-05-28 15:48:42 +09:00
commit 860471f805
13 changed files with 1643 additions and 356 deletions

View file

@ -177,8 +177,9 @@ function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCiti
}
target.isPrefecturalCapital = true;
target.isRegionalCapital = true;
target.rank = target.rank || "Regional Capital";
target.kind = target.kind === "Market Town" || target.kind === "Port Town" || target.kind === "Valley Market Town" ? "Regional Capital" : (target.kind || "Regional Capital");
target.rank = "Prefectural Capital";
target.kind = "Prefectural Capital";
target.labelPriorityBase = Math.max(target.labelPriorityBase || 0, 1150);
target.urbanRadius = clamp(6.2 + Math.sqrt(target.population) / 80, 9, 30);
target.coreRadius = clamp(2.0 + Math.sqrt(target.population) / 390, 2.4, 6.4);
target.sprawlRadius = clamp(target.urbanRadius * 1.34, target.urbanRadius + 2, 38);
@ -210,8 +211,9 @@ function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames
const prefId = prefectureRegionId[indexOf(city.x, city.y)];
if (prefId < 0) continue;
const current = capitalNameByPref.get(prefId);
const score = (city.isPrefecturalCapital ? 2_000_000 : 0) + (city.isRegionalCapital ? 500_000 : 0) + (city.population || 0);
if (!current || score > current.score) capitalNameByPref.set(prefId, { name: stripMunicipalSuffix(city.name), score, x: city.x, y: city.y, population: city.population || 0, source: "city" });
const tier = city.isPrefecturalCapital || city.rank === "Prefectural Capital" || city.kind === "Prefectural Capital" ? 3 : city.isRegionalCapital || city.rank === "Regional Capital" || city.kind === "Regional Capital" ? 2 : 1;
const score = tier * 50_000_000 + (city.population || 0);
if (!current || score > current.score) capitalNameByPref.set(prefId, { name: stripMunicipalSuffix(city.name), score, x: city.x, y: city.y, population: city.population || 0, source: tier === 3 ? "prefecture-capital" : "city" });
}
for (const center of adminCenters || []) {
if (!center || !inside(center.x, center.y) || !center.name) continue;
@ -265,7 +267,7 @@ function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames
x = labelI % MAP_W;
y = Math.floor(labelI / MAP_W);
}
regions.push({ id: row.id, x, y, area: row.area, kind: row.id === 0 ? "Current Prefecture" : "Prefecture", labelPriorityBase: 900 + Math.sqrt(row.area), capitalX: capitalInfo?.x, capitalY: capitalInfo?.y });
regions.push({ id: row.id, x, y, area: row.area, kind: row.id === 0 ? "Current Prefecture" : "Prefecture", labelPriorityBase: 950 + Math.sqrt(row.area), forceLabel: true, capitalX: capitalInfo?.x, capitalY: capitalInfo?.y });
}
const named = attachIdsAndNames(regions, "prefecture", seed + 41000, null, fields, usedNames, nameDebug)
.map((region, index) => ({ ...region, featureId: region.id, id: regions[index].id, labelName: region.name }));
@ -720,6 +722,24 @@ export function finishMapOutput({
return false;
}
function pathNearRequiredExpresswayCity(path) {
if (!path || path.length < 2) return false;
for (const city of modernCities || []) {
if (!city || (city.population || 0) < 100000 || !inside(city.x, city.y)) continue;
const inner = Math.max(7, (city.coreRadius || 4) + 4.5);
const outer = Math.max(inner + 7, (city.urbanRadius || 12) * 2.15);
let inBand = false;
let exits = false;
for (const [x, y] of path) {
const d = Math.hypot(city.x - x, city.y - y);
if (d >= inner && d <= outer) inBand = true;
if (d >= Math.max(22, (city.urbanRadius || 12) * 1.45)) exits = true;
if (inBand && exits) return true;
}
}
return false;
}
function components() {
const occ = new Uint8Array(MAP_W * MAP_H);
for (const [, paths] of groups) for (const path of paths || []) rasterize(path, (x, y) => {
@ -751,8 +771,165 @@ export function finishMapOutput({
}
return out.sort((a, b) => b.size - a.size);
}
function buildLandComponentIds() {
const ids = new Int32Array(MAP_W * MAP_H);
ids.fill(-1);
let id = 0;
const q = [];
for (let i = 0; i < ids.length; i++) {
if (ids[i] >= 0 || sea[i]) continue;
ids[i] = id;
q.length = 0;
q.push(i);
for (let h = 0; h < q.length; h++) {
const cur = q[h];
const [x, y] = xyOf(cur);
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
const nx = x + dx, ny = y + dy;
if (!inside(nx, ny)) continue;
const ni = indexOf(nx, ny);
if (sea[ni] || ids[ni] >= 0) continue;
ids[ni] = id;
q.push(ni);
}
}
id++;
}
return ids;
}
function majorityLandId(cells, landIds) {
const counts = new Map();
for (const i of cells || []) {
const id = landIds[i];
if (id < 0) continue;
counts.set(id, (counts.get(id) || 0) + 1);
}
let best = -1, bestN = 0;
for (const [id, n] of counts) if (n > bestN) { best = id; bestN = n; }
return best;
}
function componentNearAdminCenter(comp, radius = 3.2) {
if (!comp?.cells?.length) return false;
const mask = new Uint8Array(MAP_W * MAP_H);
for (const ci of comp.cells) mask[ci] = 1;
const r = Math.ceil(radius);
for (const center of adminCenters || []) {
if (!center || !inside(center.x, center.y)) continue;
const cx = Math.round(center.x), cy = Math.round(center.y);
for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
if (dx * dx + dy * dy > radius * radius) continue;
const x = cx + dx, y = cy + dy;
if (inside(x, y) && mask[indexOf(x, y)]) return true;
}
}
return false;
}
function routeIsolatedComponentToMain(comp, mainMask, mainCentroid, landIds, landIdValue) {
if (!comp?.cells?.length || landIdValue < 0) return [];
const dist = new Float64Array(MAP_W * MAP_H);
dist.fill(INF);
const prev = new Int32Array(MAP_W * MAP_H);
prev.fill(-1);
const heap = new MinHeap();
let seeded = 0;
const stride = Math.max(1, Math.floor(comp.cells.length / 96));
for (let k = 0; k < comp.cells.length; k += stride) {
const i = comp.cells[k];
if (sea[i] || landIds[i] !== landIdValue) continue;
dist[i] = 0;
prev[i] = i;
const [x, y] = xyOf(i);
heap.push({ i, f: Math.hypot(x - mainCentroid.x, y - mainCentroid.y) * 0.22 });
seeded++;
}
if (!seeded) return [];
let goal = -1;
let expanded = 0;
const maxExpanded = 22000;
while (heap.length && expanded < maxExpanded) {
const current = heap.pop();
if (!current) break;
const cur = current.i;
expanded++;
if (mainMask[cur] && dist[cur] > 2) { goal = cur; break; }
const [x, y] = xyOf(cur);
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
const nx = x + dx, ny = y + dy;
if (!inside(nx, ny)) continue;
const ni = indexOf(nx, ny);
if (sea[ni] || landIds[ni] !== landIdValue) continue;
const step = Math.hypot(dx, dy);
const terrainCost = 1.0 + (slope?.[ni] || 0) * 1.28 + (ridgeField?.[ni] || 0) * 0.66 + Math.max(0, (elevation?.[ni] || 0) - 0.66) * 1.75 - (valleyField?.[ni] || 0) * 0.42 - (plain?.[ni] || 0) * 0.18 - (coastalLowland?.[ni] || 0) * 0.08;
const nd = dist[cur] + step * Math.max(0.38, terrainCost);
if (nd >= dist[ni]) continue;
dist[ni] = nd;
prev[ni] = cur;
const h = Math.hypot(nx - mainCentroid.x, ny - mainCentroid.y) * 0.22;
heap.push({ i: ni, f: nd + h });
}
}
if (goal < 0) return [];
const path = [];
let cur = goal;
for (let guard = 0; guard < 240 && cur >= 0; guard++) {
const [x, y] = xyOf(cur);
path.push([x, y]);
if (prev[cur] === cur) break;
cur = prev[cur];
}
path.reverse();
if (path.length < 4 || path.length > 150) return [];
return routeQualityAcceptable(path, { sea, elevation, slope, potential: populationDensity, highElevationThreshold: 0.80, steepThreshold: 0.55 }, {
minLength: 4,
maxLength: 150,
maxCompactness: 5.4,
maxHighElevationShare: 0.42,
maxSteepShare: 0.66,
}) ? path : [];
}
function attemptConnectSameLandmassAdminRoadComponents(comps) {
const result = { attempted: 0, added: 0, skippedIsland: 0, failed: 0 };
if (!comps || comps.length <= 1) return result;
const landIds = buildLandComponentIds();
const mainLand = majorityLandId(comps[0].cells, landIds);
const mainMask = new Uint8Array(MAP_W * MAP_H);
let sx = 0, sy = 0, sn = 0;
for (const ci of comps[0].cells) {
const [cx, cy] = xyOf(ci);
sx += cx; sy += cy; sn++;
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) {
if (dx * dx + dy * dy > 5) continue;
const nx = cx + dx, ny = cy + dy;
if (inside(nx, ny)) mainMask[indexOf(nx, ny)] = 1;
}
}
const centroid = { x: sx / Math.max(1, sn), y: sy / Math.max(1, sn) };
for (const comp of comps.slice(1, 24)) {
if (!componentNearAdminCenter(comp)) continue;
const land = majorityLandId(comp.cells, landIds);
if (land !== mainLand) { result.skippedIsland++; continue; }
result.attempted++;
const path = routeIsolatedComponentToMain(comp, mainMask, centroid, landIds, land);
if (path.length >= 4) {
minorRoads.push(path);
result.added++;
} else {
result.failed++;
}
}
return result;
}
let comps = components();
const before = comps.length;
const mountainConnect = attemptConnectSameLandmassAdminRoadComponents(comps);
if (mountainConnect.added > 0) comps = components();
const pruned = { minor: 0, national: 0, external: 0, expressway: 0, externalExpressway: 0 };
for (let pass = 0; pass < 4 && comps.length > 1; pass++) {
const mainMask = new Uint8Array(MAP_W * MAP_H);
@ -776,7 +953,7 @@ export function finishMapOutput({
for (const [key, paths] of groups) {
const kept = [];
for (const path of paths || []) {
if (touchesMain(path) || pathNearAdminCenter(path)) kept.push(path);
if (touchesMain(path) || pathNearAdminCenter(path) || ((key === "expressway" || key === "externalExpressway") && pathNearRequiredExpresswayCity(path))) kept.push(path);
else pruned[key]++;
}
paths.length = 0;
@ -784,7 +961,7 @@ export function finishMapOutput({
}
comps = components();
}
debugLayers.finalOutputRoadConnectivity = { beforeComponents: before, afterComponents: comps.length, pruned };
debugLayers.finalOutputRoadConnectivity = { beforeComponents: before, afterComponents: comps.length, pruned, mountainAdminConnections: mountainConnect };
}
pruneIsolatedFinalRoadComponents();
@ -815,6 +992,178 @@ export function finishMapOutput({
}
ensureAdminCenterCellsAfterOutputPrune();
function connectNearbyRoadEndpoints() {
const debugLayers = transportDebug ? (transportDebug.layers || (transportDebug.layers = {})) : {};
const ordinaryGroups = [
{ key: "minor", paths: minorRoads || [] },
{ key: "national", paths: nationalRoads || [] },
{ key: "external", paths: externalRoads || [] },
{ key: "ring", paths: ringRoads || [] },
];
const occ = new Uint8Array(MAP_W * MAP_H);
function rasterize(path, fn) {
for (let k = 1; k < (path?.length || 0); k++) {
const [x0, y0] = path[k - 1];
const [x1, y1] = path[k];
const steps = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0)));
for (let s = 0; s <= steps; s++) {
const t = s / steps;
const x = Math.round(x0 + (x1 - x0) * t);
const y = Math.round(y0 + (y1 - y0) * t);
if (inside(x, y) && !sea[indexOf(x, y)]) fn(x, y);
}
}
}
for (const group of ordinaryGroups) for (const path of group.paths || []) rasterize(path, (x, y) => { occ[indexOf(x, y)] = 1; });
const comp = new Int32Array(MAP_W * MAP_H);
comp.fill(-1);
let compId = 0;
const queue = [];
for (let i = 0; i < occ.length; i++) {
if (!occ[i] || comp[i] >= 0) continue;
comp[i] = compId;
queue.length = 0;
queue.push(i);
for (let q = 0; q < queue.length; q++) {
const cur = queue[q];
const [x, y] = xyOf(cur);
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
const nx = x + dx, ny = y + dy;
if (!inside(nx, ny)) continue;
const ni = indexOf(nx, ny);
if (!occ[ni] || comp[ni] >= 0) continue;
comp[ni] = compId;
queue.push(ni);
}
}
compId++;
}
function endpointComponent(x, y) {
if (!inside(x, y) || sea[indexOf(x, y)]) return -1;
const here = comp[indexOf(x, y)];
if (here >= 0) return here;
for (let r = 1; r <= 2; r++) {
for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
const nx = x + dx, ny = y + dy;
if (!inside(nx, ny) || sea[indexOf(nx, ny)]) continue;
const id = comp[indexOf(nx, ny)];
if (id >= 0) return id;
}
}
return -1;
}
function directConnector(a, b) {
const steps = Math.max(1, Math.ceil(Math.hypot(a.x - b.x, a.y - b.y)));
const path = [];
for (let s = 0; s <= steps; s++) {
const t = s / steps;
const x = Math.round(a.x + (b.x - a.x) * t);
const y = Math.round(a.y + (b.y - a.y) * t);
if (!inside(x, y) || sea[indexOf(x, y)]) return [];
if (!path.length || path[path.length - 1][0] !== x || path[path.length - 1][1] !== y) path.push([x, y]);
}
return path.length >= 2 ? path : [];
}
const endpoints = [];
for (const group of ordinaryGroups) {
for (let pathIdx = 0; pathIdx < (group.paths?.length || 0); pathIdx++) {
const path = group.paths[pathIdx];
if (!path || path.length < 2) continue;
for (const end of [0, 1]) {
const raw = end === 0 ? path[0] : path[path.length - 1];
const x = Math.round(raw[0]), y = Math.round(raw[1]);
if (!inside(x, y) || sea[indexOf(x, y)]) continue;
const ci = indexOf(x, y);
const ruralBias = Math.max(0, 0.42 - (populationDensity?.[ci] || 0));
endpoints.push({ group: group.key, pathIdx, end, x, y, comp: endpointComponent(x, y), ruralBias });
}
}
}
const pairs = [];
for (let i = 0; i < endpoints.length; i++) {
const a = endpoints[i];
if (a.comp < 0) continue;
for (let j = i + 1; j < endpoints.length; j++) {
const b = endpoints[j];
if (b.comp < 0 || a.comp === b.comp) continue;
if (a.group === b.group && a.pathIdx === b.pathIdx) continue;
const d = Math.hypot(a.x - b.x, a.y - b.y);
const limit = (a.ruralBias + b.ruralBias) > 0.38 ? 6.5 : 4.4;
if (d < 1.1 || d > limit) continue;
const path = directConnector(a, b);
if (path.length < 2 || path.length > 9) continue;
pairs.push({ a, b, d, path, score: d - (a.ruralBias + b.ruralBias) * 1.25 + (a.group === "minor" && b.group === "minor" ? 0.25 : 0) });
}
}
pairs.sort((a, b) => a.score - b.score || a.d - b.d);
const used = new Set();
let added = 0;
for (const pair of pairs) {
if (added >= 180) break;
const ak = `${pair.a.group}:${pair.a.pathIdx}:${pair.a.end}`;
const bk = `${pair.b.group}:${pair.b.pathIdx}:${pair.b.end}`;
if (used.has(ak) || used.has(bk)) continue;
minorRoads.push(pair.path);
used.add(ak); used.add(bk);
added++;
}
debugLayers.nearbyRoadEndpointConnectorsAdded = added;
return added;
}
connectNearbyRoadEndpoints();
function renameInterchangesFromMunicipalities() {
if (!interchanges?.length || !adminCenters?.length || !adminId) return 0;
const centerByAdmin = new Map();
for (const center of adminCenters || []) {
if (!center || !inside(center.x, center.y)) continue;
const id = adminId[indexOf(center.x, center.y)];
if (id >= 0 && !centerByAdmin.has(id)) centerByAdmin.set(id, center);
}
const allCenters = [...centerByAdmin.values()].filter((c) => c?.name);
const used = new Set();
const directionNames = ["北", "東", "南", "西", "中央", "上", "下", "新"];
let renamed = 0;
function cleanBase(name) {
return String(name || "").replace(/[ICインターチェンジ\s]+$/u, "").replace(/[市町村区]$/u, "");
}
for (const [idx, ic] of interchanges.entries()) {
if (!ic || !inside(ic.x, ic.y)) continue;
const cell = indexOf(ic.x, ic.y);
const admin = adminId[cell];
const primary = centerByAdmin.get(admin) || allCenters.slice().sort((a, b) => Math.hypot(a.x - ic.x, a.y - ic.y) - Math.hypot(b.x - ic.x, b.y - ic.y))[0];
const nearbyCenters = allCenters
.slice()
.sort((a, b) => Math.hypot(a.x - ic.x, a.y - ic.y) - Math.hypot(b.x - ic.x, b.y - ic.y));
const candidates = [];
if (primary?.name) candidates.push(`${cleanBase(primary.municipalityName || primary.name)}IC`);
for (const center of nearbyCenters.slice(0, 12)) {
const base = cleanBase(center.municipalityName || center.name);
if (base) candidates.push(`${base}IC`);
}
if (primary?.name) {
const base = cleanBase(primary.municipalityName || primary.name);
for (const dir of directionNames) candidates.push(`${base}${dir}IC`);
}
candidates.push(`自治${idx + 1}IC`);
let name = candidates.find((candidate) => candidate && !used.has(candidate));
if (!name) name = `自治${idx + 1}IC`;
ic.name = name;
ic.labelName = name;
ic.municipalityNameBased = true;
used.add(name);
renamed++;
}
if (transportDebug) {
transportDebug.layers ||= {};
transportDebug.layers.municipalityBasedInterchangeNames = renamed;
}
return renamed;
}
renameInterchangesFromMunicipalities();
nameDebug.maxDerivedPerBase = 0;
const prefectureRegions = buildPrefectureRegions(prefectureRegionId, sea, nameFields, seed, usedNames, nameDebug, { modernCities, adminCenters });
const regionalPrefectureBorders = adminRegionalPrefectureBorders || [];