road update

This commit is contained in:
33333-33333 2026-05-26 21:14:37 +09:00
commit a6e66f2838
9 changed files with 2174 additions and 258 deletions

View file

@ -1,9 +1,13 @@
import { createNameDebug } from "./names.js";
import { CELL_SIZE, INF, MAP_H, MAP_W, clamp, indexOf, inside, rand } from "./mapUtils.js";
import { applyOutputOptions, attachIdsAndNames, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
import { applyOutputOptions, attachIdsAndNames, influenceFromPaths, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
const MUNICIPAL_SUFFIX_RE = /[市町村区]$/u;
function stripMunicipalSuffix(name) {
return String(name || "").replace(/[市町村区]$/u, "").trim();
}
function municipalitySuffixForCenter(center, fields, seed, ordinal = 0) {
const i = inside(center?.x || 0, center?.y || 0) ? indexOf(center.x, center.y) : 0;
const density = fields.populationDensity?.[i] || 0;
@ -18,16 +22,20 @@ function municipalitySuffixForCenter(center, fields, seed, ordinal = 0) {
function municipalityNameFromRoot(root, center, fields, seed, ordinal = 0) {
let value = String(root || center?.name || "").trim();
if (!value) value = `自治${ordinal + 1}`;
value = value.replace(/[駅港城跡宿]$/u, "");
if (Array.from(value).length < 2) value = `${value}${String(center?.generatedMunicipalityName || "里")}`.slice(0, 3);
if (MUNICIPAL_SUFFIX_RE.test(value)) return value;
value = value.replace(/[市町村区駅港城跡宿]$/gu, "");
const fallback = String(center?.generatedMunicipalityName || "里").replace(/[市町村区駅港城跡宿]$/gu, "");
if (Array.from(value).length < 2) value = `${value}${fallback || "里"}`;
// Municipality roots should be at most two toponymic elements. The admin
// suffix is separate; avoid direction+root+suffix three-element names.
value = Array.from(value).slice(0, 2).join("");
return `${value}${municipalitySuffixForCenter(center, fields, seed, ordinal)}`;
}
function assignMunicipalityPopulations(adminCenters, adminId, fields) {
function assignMunicipalityPopulations(adminCenters, adminId, fields, settlementFeatures = []) {
if (!adminCenters?.length || !adminId) return;
const totals = new Float64Array(adminCenters.length);
const settlementTotals = new Float64Array(adminCenters.length);
for (let i = 0; i < adminId.length; i++) {
const id = adminId[i];
if (id < 0 || id >= totals.length || fields.sea?.[i]) continue;
@ -39,15 +47,108 @@ function assignMunicipalityPopulations(adminCenters, adminId, fields) {
const ruralFloor = lu === 1 ? 10 + agri * 18 + plain * 10 : 0;
totals[id] += density * builtWeight + ruralFloor;
}
// Population-bearing generated settlements are canonical entities, so add
// their explicit populations exactly once to the municipality containing the
// point. Some towns are promoted to cities later, so the same coordinate can
// appear in both `markets` and `modernCities`; keep only the strongest record
// per coordinate to avoid double counting.
const uniqueSettlementByCell = new Map();
for (const feature of settlementFeatures || []) {
if (!feature || !Number.isFinite(feature.population) || feature.population <= 0) continue;
if (!inside(feature.x, feature.y)) continue;
const i = indexOf(feature.x, feature.y);
if (fields.sea?.[i]) continue;
const key = `${Math.round(feature.x)}:${Math.round(feature.y)}`;
const current = uniqueSettlementByCell.get(key);
const priority = (feature.isPrefecturalCapital ? 4_000_000 : 0) + (feature.isRegionalCapital ? 1_000_000 : 0) + (feature.population || 0);
if (!current || priority > current.priority) uniqueSettlementByCell.set(key, { feature, priority, i });
}
let skippedDuplicateSettlementPopulation = 0;
for (const { feature, i } of uniqueSettlementByCell.values()) {
const id = adminId[i];
if (id < 0 || id >= totals.length) continue;
settlementTotals[id] += feature.population;
}
for (const feature of settlementFeatures || []) {
if (!feature || !Number.isFinite(feature.population) || feature.population <= 0 || !inside(feature.x, feature.y)) continue;
const key = `${Math.round(feature.x)}:${Math.round(feature.y)}`;
const kept = uniqueSettlementByCell.get(key)?.feature;
if (kept && kept !== feature) skippedDuplicateSettlementPopulation += feature.population || 0;
}
for (let id = 0; id < adminCenters.length; id++) {
const raw = totals[id] || 0;
const raw = (totals[id] || 0) + (settlementTotals[id] || 0);
const rounded = raw >= 10000 ? Math.round(raw / 1000) * 1000 : Math.round(raw / 100) * 100;
adminCenters[id].municipalityPopulation = Math.max(0, rounded);
adminCenters[id].municipalitySettlementPopulation = Math.max(0, Math.round((settlementTotals[id] || 0) / 100) * 100);
adminCenters[id].municipalitySkippedDuplicateSettlementPopulation = Math.max(0, Math.round(skippedDuplicateSettlementPopulation / 100) * 100);
}
}
function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames, nameDebug) {
function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, fields, seed, minPopulation = 200000) {
if (!prefectureRegionId) return 0;
const prefIds = new Set();
for (let i = 0; i < prefectureRegionId.length; i++) {
const id = prefectureRegionId[i];
if (!sea[i] && id >= 0) prefIds.add(id);
}
let promoted = 0;
function prefAt(p) {
if (!p || !inside(p.x, p.y)) return -1;
return prefectureRegionId[indexOf(p.x, p.y)] ?? -1;
}
for (const prefId of [...prefIds].sort((a, b) => a - b)) {
const cities = (modernCities || []).filter((p) => prefAt(p) === prefId);
let target = cities.slice().sort((a, b) =>
((b.isRegionalCapital ? 800000 : 0) + (b.population || 0)) - ((a.isRegionalCapital ? 800000 : 0) + (a.population || 0))
)[0];
if (!target) {
const market = (markets || []).filter((p) => prefAt(p) === prefId).sort((a, b) => (b.population || 0) - (a.population || 0))[0];
if (market) {
target = {
...market,
kind: "Regional Capital",
rank: "Regional Capital",
promotedFromMarketTown: true,
labelPriorityBase: 900,
};
modernCities.push(target);
}
}
if (!target) {
const center = (adminCenters || []).filter((p) => prefAt(p) === prefId).sort((a, b) => (b.municipalityPopulation || 0) - (a.municipalityPopulation || 0))[0];
if (center) {
target = {
...center,
kind: "Regional Capital",
rank: "Regional Capital",
promotedFromMunicipalCenter: true,
labelPriorityBase: 880,
};
modernCities.push(target);
}
}
if (!target) continue;
const promotedPopulation = Math.round((minPopulation + rand(seed + 52000, prefId * 37 + 11) * 120000) / 1000) * 1000;
if ((target.population || 0) < promotedPopulation) {
target.population = promotedPopulation;
promoted++;
}
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.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);
target.urbanWeight = clamp(1.05 + Math.log10(Math.max(10000, target.population)) * 0.30, 1.25, 2.55);
}
return promoted;
}
function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames, nameDebug, options = {}) {
if (!prefectureRegionId) return [];
const { modernCities = [], adminCenters = [] } = options;
const byId = new Map();
for (let i = 0; i < prefectureRegionId.length; i++) {
const id = prefectureRegionId[i];
@ -62,6 +163,21 @@ function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames
row.cells.push(i);
}
const regions = [];
const capitalNameByPref = new Map();
for (const city of modernCities || []) {
if (!city || !inside(city.x, city.y) || !city.name) continue;
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" });
}
for (const center of adminCenters || []) {
if (!center || !inside(center.x, center.y) || !center.name) continue;
const prefId = prefectureRegionId[indexOf(center.x, center.y)];
if (prefId < 0 || capitalNameByPref.has(prefId)) continue;
capitalNameByPref.set(prefId, { name: stripMunicipalSuffix(center.name), score: center.municipalityPopulation || 0, x: center.x, y: center.y, population: center.municipalityPopulation || 0, source: "admin" });
}
for (const row of [...byId.values()].sort((a, b) => a.id - b.id)) {
const cx = row.sx / Math.max(1, row.area);
const cy = row.sy / Math.max(1, row.area);
@ -80,14 +196,54 @@ function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames
Math.hypot(x - cx, y - cy) * 0.08;
if (score > bestScore) { bestScore = score; bestI = i; }
}
const x = bestI % MAP_W;
const y = Math.floor(bestI / 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) });
let x = bestI % MAP_W;
let y = Math.floor(bestI / MAP_W);
const capitalInfo = capitalNameByPref.get(row.id);
if (capitalInfo && inside(capitalInfo.x, capitalInfo.y)) {
const targetRing = clamp(5.5 + Math.sqrt(row.area) / 52, 6, 15);
let labelI = bestI;
let labelScore = -INF;
for (const i of row.cells) {
const lx = i % MAP_W;
const ly = Math.floor(i / MAP_W);
const d = Math.hypot(lx - capitalInfo.x, ly - capitalInfo.y);
if (d < 2 || d > Math.max(19, targetRing * 2.2)) continue;
const lu = fields.landuse?.[i] ?? 0;
const builtPenalty = lu === 3 ? 1.2 : lu === 2 || lu === 4 || lu === 7 || lu === 8 ? 0.70 : 0;
const score =
-Math.abs(d - targetRing) * 0.38 -
(fields.populationDensity?.[i] || 0) * 1.1 -
builtPenalty +
(fields.plain?.[i] || 0) * 0.22 +
(fields.basinField?.[i] || 0) * 0.14 +
(fields.coastalLowland?.[i] || 0) * 0.10 -
(fields.slope?.[i] || 0) * 0.30 -
(fields.ridgeField?.[i] || 0) * 0.24;
if (score > labelScore) { labelScore = score; labelI = i; }
}
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 });
}
return attachIdsAndNames(regions, "prefecture", seed + 41000, null, fields, usedNames, nameDebug)
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 }));
const usedPrefNames = new Set();
for (const region of named) {
const capital = capitalNameByPref.get(region.id)?.name;
let candidate = capital && Array.from(capital).length >= 2 ? capital : stripMunicipalSuffix(region.name);
if (!candidate) candidate = stripMunicipalSuffix(region.name) || `県域${region.id + 1}`;
if (usedPrefNames.has(candidate)) candidate = `${candidate}${region.id + 1}`;
candidate = `${String(candidate).replace(/[都道府県]$/u, "")}`;
region.name = candidate;
region.labelName = candidate;
region.prefectureCapitalDerivedName = Boolean(capital);
usedPrefNames.add(candidate);
}
return named;
}
export function finishMapOutput({
seed,
options,
@ -284,18 +440,24 @@ export function finishMapOutput({
for (const [index, center] of adminCenters.entries()) {
center.adminNumericId = index;
center.municipalityId = index;
let candidate = center.canonicalSettlementName || municipalityNameFromRoot(center.municipalityRootName || center.generatedMunicipalityName || center.name, center, nameFields, seed, index);
let candidate = municipalityNameFromRoot(center.municipalityRootName || center.canonicalSettlementName || center.generatedMunicipalityName || center.name, center, nameFields, seed, index);
const generated = municipalityNameFromRoot(center.generatedMunicipalityName || center.name, center, nameFields, seed + 177, index);
if (usedAdminNames.has(candidate) && Array.from(generated).length >= 2 && !usedAdminNames.has(generated)) {
candidate = generated;
}
if (usedAdminNames.has(candidate)) {
const suffix = municipalitySuffixForCenter(center, nameFields, seed + 313, index);
const root = String(center.municipalityRootName || center.canonicalSettlementName || center.generatedMunicipalityName || center.name || `自治${index + 1}`).replace(/[市町村区]$/u, "");
const directions = ["東", "西", "南", "北", "上", "下", "中"];
for (let attempt = 0; attempt < directions.length + 3 && usedAdminNames.has(candidate); attempt++) {
const prefix = directions[attempt % directions.length];
candidate = attempt < directions.length ? `${prefix}${root}${suffix}` : `${root}${index + 1}${suffix}`;
const rootSource = String(center.generatedMunicipalityName || center.name || center.municipalityRootName || `自治${index + 1}`).replace(/[市町村区駅港城跡宿]$/gu, "");
const chars = Array.from(rootSource || "里郷");
const alternates = [
chars.slice(0, 2).join(""),
chars.slice(-2).join(""),
`${chars[0] || "里"}${["里", "郷", "野", "田", "川", "原", "浜", "浦"][(seed + index) % 8]}`,
`${["東", "西", "南", "北", "上", "下", "中"][(seed + index) % 7]}${chars[0] || "里"}`,
].filter((v) => Array.from(v).length >= 2);
for (let attempt = 0; attempt < alternates.length + 12 && usedAdminNames.has(candidate); attempt++) {
const root = attempt < alternates.length ? alternates[attempt] : `${(index + attempt) % 10}`;
candidate = `${Array.from(root).slice(0, 2).join("")}${suffix}`;
}
}
center.name = candidate;
@ -303,13 +465,93 @@ export function finishMapOutput({
center.municipalityName = candidate;
usedAdminNames.add(center.name);
}
assignMunicipalityPopulations(adminCenters, adminId, nameFields);
const promotedPrefectureCapitals = promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, nameFields, seed, 220000);
assignMunicipalityPopulations(adminCenters, adminId, nameFields, [
...modernCities,
...markets,
...villages,
...satelliteCities,
...newTowns,
]);
function addMunicipalCenterLocalAccess() {
if (!transportDebug) return;
const debugLayers = transportDebug.layers || (transportDebug.layers = { repairedSegments: [], unservedSettlements: [] });
debugLayers.repairedSegments ||= [];
debugLayers.unservedSettlements ||= [];
const accessInfluence = influenceFromPaths([...nationalRoads, ...railways, ...externalRoads, ...minorRoads], 7);
const localPenalty = influenceFromPaths(minorRoads, 4);
const candidates = adminCenters
.filter((p) => inside(p.x, p.y) && !sea[indexOf(p.x, p.y)] && accessInfluence[indexOf(p.x, p.y)] < 0.16)
.sort((a, b) => (b.municipalityPopulation || 0) - (a.municipalityPopulation || 0))
.slice(0, 45);
function routeAccess(start) {
const maxSteps = 72;
const out = [];
const visited = new Set([`${start.x},${start.y}`]);
let x = start.x;
let y = start.y;
let lastKey = "";
for (let step = 0; step < maxSteps; step++) {
const i = indexOf(x, y);
if (step > 3 && (accessInfluence[i] > 0.20 || localPenalty[i] > 0.10)) return out;
let best = null;
let bestScore = INF;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
const nx = x + dx;
const ny = y + dy;
if (!inside(nx, ny)) continue;
const ni = indexOf(nx, ny);
if (sea[ni]) continue;
if (visited.has(`${nx},${ny}`)) continue;
const cost =
1.1 +
slope[ni] * 1.4 +
ridgeField[ni] * 0.55 +
Math.max(0, elevation[ni] - 0.70) * 1.2 -
Math.max(roadInfluence[ni], railInfluence2[ni]) * 2.8 -
plain[ni] * 0.28 -
valleyField[ni] * 0.18 -
coastalLowland[ni] * 0.16 +
localPenalty[ni] * 0.55;
if (cost < bestScore) {
bestScore = cost;
best = [nx, ny];
}
}
}
if (!best) break;
x = best[0];
y = best[1];
const key = `${x},${y}`;
visited.add(key);
if (key !== lastKey) {
out.push([x, y]);
lastKey = key;
}
}
return [];
}
for (const center of candidates) {
const path = routeAccess(center);
debugLayers.unservedSettlements.push({ x: center.x, y: center.y, kind: "Municipal Center", mode: "municipal-access", repaired: path.length >= 4 });
if (path.length < 4) continue;
minorRoads.push(path);
debugLayers.repairedSegments.push({ mode: "municipal-local", path, from: "municipal-center", to: "network" });
}
}
addMunicipalCenterLocalAccess();
nameDebug.maxDerivedPerBase = 0;
const prefectureRegions = buildPrefectureRegions(prefectureRegionId, sea, nameFields, seed, usedNames, nameDebug);
const prefectureRegions = buildPrefectureRegions(prefectureRegionId, sea, nameFields, seed, usedNames, nameDebug, { modernCities, adminCenters });
const regionalPrefectureBorders = adminRegionalPrefectureBorders || [];
if (regionalDebug) {
regionalDebug.finalRegionalPrefectureBorderCount = regionalPrefectureBorders.length;
regionalDebug.regionalPrefectureBordersRebuiltFromFinalId = true;
regionalDebug.promotedPrefectureCapitals = promotedPrefectureCapitals;
}
outputProgress("final package");