This commit is contained in:
33333-33333 2026-05-28 00:30:09 +09:00
commit b17be0e0d2
21 changed files with 8034 additions and 2737 deletions

View file

@ -25,10 +25,7 @@ function municipalityNameFromRoot(root, center, fields, seed, ordinal = 0) {
if (!value) value = `自治${ordinal + 1}`;
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("");
if (!value) value = fallback || "里";
return `${value}${municipalitySuffixForCenter(center, fields, seed, ordinal)}`;
}
@ -37,15 +34,19 @@ function assignMunicipalityPopulations(adminCenters, adminId, fields, settlement
if (!adminCenters?.length || !adminId) return;
const totals = new Float64Array(adminCenters.length);
const settlementTotals = new Float64Array(adminCenters.length);
const landCells = new Uint32Array(adminCenters.length);
const inhabitedCells = new Uint32Array(adminCenters.length);
for (let i = 0; i < adminId.length; i++) {
const id = adminId[i];
if (id < 0 || id >= totals.length || fields.sea?.[i]) continue;
landCells[id]++;
const density = fields.populationDensity?.[i] || 0;
const lu = fields.landuse?.[i] ?? 0;
const plain = fields.plain?.[i] || 0;
const agri = fields.agriculture?.[i] || 0;
const builtWeight = lu === 3 ? 520 : lu === 2 ? 360 : lu === 4 || lu === 7 || lu === 8 ? 260 : lu === 5 || lu === 6 ? 160 : lu === 1 ? 82 : 22;
const ruralFloor = lu === 1 ? 10 + agri * 18 + plain * 10 : 0;
if (density > 0.006 || ruralFloor > 0 || lu > 0) inhabitedCells[id]++;
totals[id] += density * builtWeight + ruralFloor;
}
// Population-bearing generated settlements are canonical entities, so add
@ -78,8 +79,17 @@ function assignMunicipalityPopulations(adminCenters, adminId, fields, settlement
}
for (let id = 0; id < adminCenters.length; id++) {
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);
const minimumResidentPopulation = landCells[id] > 0
? Math.round(Math.max(100, Math.min(3800, 80 + landCells[id] * 9 + inhabitedCells[id] * 16)) / 100) * 100
: 0;
const adjustedRaw = Math.max(raw, minimumResidentPopulation);
const rounded = adjustedRaw >= 10000 ? Math.round(adjustedRaw / 1000) * 1000 : Math.max(minimumResidentPopulation, Math.round(adjustedRaw / 100) * 100);
const safePopulation = Math.max(landCells[id] > 0 ? 100 : 0, rounded);
adminCenters[id].municipalityPopulation = safePopulation;
// Some consumers still read the generic `population` field from municipal
// centers. Mirror the municipality total there so no municipality is shown
// as 0人 merely because it is not a canonical city/market entity.
adminCenters[id].population = Math.max(adminCenters[id].population || 0, safePopulation);
adminCenters[id].municipalitySettlementPopulation = Math.max(0, Math.round((settlementTotals[id] || 0) / 100) * 100);
adminCenters[id].municipalitySkippedDuplicateSettlementPopulation = Math.max(0, Math.round(skippedDuplicateSettlementPopulation / 100) * 100);
}
@ -93,6 +103,33 @@ function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCiti
const id = prefectureRegionId[i];
if (!sea[i] && id >= 0) prefIds.add(id);
}
const prefProfiles = new Map();
for (let i = 0; i < prefectureRegionId.length; i++) {
const prefId = prefectureRegionId[i];
if (sea[i] || prefId < 0) continue;
const profile = prefProfiles.get(prefId) || { landCells: 0, habitableCells: 0, lowlandCells: 0, densitySum: 0 };
profile.landCells++;
const slopeV = fields.slope?.[i] || 0;
const ridgeV = fields.ridgeField?.[i] || 0;
const elevV = fields.elevation?.[i] || 0;
const lowland = ((fields.plain?.[i] || 0) > 0.24 || (fields.basinField?.[i] || 0) > 0.26 || (fields.coastalLowland?.[i] || 0) > 0.22) && slopeV < 0.38 && ridgeV < 0.55;
if (slopeV < 0.42 && ridgeV < 0.58 && elevV < 0.74) profile.habitableCells++;
if (lowland) profile.lowlandCells++;
profile.densitySum += fields.populationDensity?.[i] || 0;
prefProfiles.set(prefId, profile);
}
function capitalFloorForPref(prefId) {
const p = prefProfiles.get(prefId) || { landCells: 0, habitableCells: 0, lowlandCells: 0, densitySum: 0 };
const lowlandRatio = p.landCells ? p.lowlandCells / p.landCells : 0;
const densityBoost = clamp((p.densitySum / Math.max(1, p.landCells) - 0.16) / 0.42);
let base = 45000;
if (p.lowlandCells > 900 || (p.lowlandCells > 650 && lowlandRatio > 0.28)) base = minPopulation * 0.90;
else if (p.lowlandCells > 520) base = 160000;
else if (p.lowlandCells > 260) base = 110000;
else if (p.lowlandCells > 120 || p.habitableCells > 420) base = 70000;
const adjusted = base + densityBoost * 50000;
return Math.round(clamp(adjusted, 42000, minPopulation + 45000) / 1000) * 1000;
}
let promoted = 0;
function prefAt(p) {
if (!p || !inside(p.x, p.y)) return -1;
@ -130,7 +167,10 @@ function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCiti
}
}
if (!target) continue;
const promotedPopulation = Math.round((minPopulation + rand(seed + 52000, prefId * 37 + 11) * 120000) / 1000) * 1000;
const regionalFloor = capitalFloorForPref(prefId);
const candidateCapacity = Number.isFinite(target.capacity) ? Math.max(42000, target.capacity * 1.18) : Infinity;
const randomizedFloor = Math.round((regionalFloor + rand(seed + 52000, prefId * 37 + 11) * Math.max(12000, regionalFloor * 0.28)) / 1000) * 1000;
const promotedPopulation = Math.max(42000, Math.round(Math.min(randomizedFloor, candidateCapacity) / 1000) * 1000);
if ((target.population || 0) < promotedPopulation) {
target.population = promotedPopulation;
promoted++;
@ -251,6 +291,7 @@ export function finishMapOutput({
terrain,
features,
admin,
geography = null,
}) {
const {
terrainTemplate,
@ -272,6 +313,7 @@ export function finishMapOutput({
basinField,
coastalLowland,
flowAccum,
watershedId,
erosionField,
depositionField,
arcSpineField,
@ -296,6 +338,7 @@ export function finishMapOutput({
railInfluence2,
settlementCluster,
villages: inputVillages,
geographicUrbanAnchors: inputGeographicUrbanAnchors = [],
ports: inputPorts,
crossings: inputCrossings,
passes: inputPasses,
@ -342,6 +385,7 @@ export function finishMapOutput({
} = admin;
let villages = inputVillages;
let geographicUrbanAnchors = inputGeographicUrbanAnchors;
let ports = inputPorts;
let crossings = inputCrossings;
let passes = inputPasses;
@ -386,6 +430,7 @@ export function finishMapOutput({
outputProgress("feature naming");
villages = attachIdsAndNames(tagInsidePrefecture(villages, prefectureMask), "village", seed, null, nameFields, usedNames, nameDebug);
geographicUrbanAnchors = attachIdsAndNames(tagInsidePrefecture(geographicUrbanAnchors, prefectureMask), "geoAnchor", seed, null, nameFields, usedNames, nameDebug);
ports = attachIdsAndNames(tagInsidePrefecture(ports, prefectureMask), "port", seed, null, nameFields, usedNames, nameDebug);
crossings = attachIdsAndNames(tagInsidePrefecture(crossings, prefectureMask), "crossing", seed, null, nameFields, usedNames, nameDebug);
passes = attachIdsAndNames(tagInsidePrefecture(passes, prefectureMask), "pass", seed, null, nameFields, usedNames, nameDebug);
@ -458,7 +503,7 @@ export function finishMapOutput({
].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}`;
candidate = `${root}${suffix}`;
}
}
center.name = candidate;
@ -615,6 +660,161 @@ export function finishMapOutput({
}
}
addMunicipalCenterLocalAccess();
function pruneIsolatedFinalRoadComponents() {
const debugLayers = transportDebug ? (transportDebug.layers || (transportDebug.layers = {})) : {};
const groups = [
["minor", minorRoads],
["national", nationalRoads],
["external", externalRoads],
["expressway", expressways],
["externalExpressway", externalExpressways],
];
function rasterize(path, fn) {
for (let k = 0; k < (path?.length || 0); k++) {
const [x0, y0] = path[k];
const [x1, y1] = path[Math.min(k + 1, path.length - 1)];
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);
fn(x, y);
}
}
}
function splitPathOnSea(path) {
const chunks = [];
let chunk = [];
function pushPoint(x, y) {
if (!inside(x, y) || sea[indexOf(x, y)]) {
if (chunk.length >= 2) chunks.push(chunk);
chunk = [];
return;
}
if (!chunk.length || chunk[chunk.length - 1][0] !== x || chunk[chunk.length - 1][1] !== y) chunk.push([x, y]);
}
for (let k = 0; k < (path?.length || 0); k++) {
const [x0, y0] = path[k];
const [x1, y1] = path[Math.min(k + 1, path.length - 1)];
const steps = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0)));
for (let s = 0; s <= steps; s++) {
const t = s / steps;
pushPoint(Math.round(x0 + (x1 - x0) * t), Math.round(y0 + (y1 - y0) * t));
}
}
if (chunk.length >= 2) chunks.push(chunk);
return chunks;
}
// Keep sea-crossing cells in the stored path so the renderer can draw
// explicit bridge overlays. Connectivity analysis below ignores sea cells
// when rasterizing components, so preserving them here does not make islands
// falsely connected by ordinary land roads.
function pathNearAdminCenter(path, radius = 0.75) {
for (const center of adminCenters || []) {
if (!center || !inside(center.x, center.y)) continue;
for (const [x, y] of path || []) {
if (Math.hypot(center.x - x, center.y - y) <= radius) 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) => {
if (inside(x, y) && !sea[indexOf(x, y)]) occ[indexOf(x, y)] = 1;
});
const seen = new Uint8Array(MAP_W * MAP_H);
const out = [];
for (let i = 0; i < occ.length; i++) {
if (!occ[i] || seen[i]) continue;
const queue = [i];
const cells = [];
seen[i] = 1;
for (let q = 0; q < queue.length; q++) {
const cur = queue[q];
cells.push(cur);
const [x, y] = xyOf(cur);
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) {
if (!dx && !dy) continue;
if (dx * dx + dy * dy > 5) continue;
const nx = x + dx, ny = y + dy;
if (!inside(nx, ny)) continue;
const ni = indexOf(nx, ny);
if (!occ[ni] || seen[ni]) continue;
seen[ni] = 1;
queue.push(ni);
}
}
out.push({ cells, size: cells.length });
}
return out.sort((a, b) => b.size - a.size);
}
let comps = components();
const before = comps.length;
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);
for (const ci of comps[0].cells) {
const [cx, cy] = xyOf(ci);
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;
}
}
function touchesMain(path) {
let hit = 0, n = 0;
rasterize(path, (x, y) => {
if (!inside(x, y) || sea[indexOf(x, y)]) return;
n++;
if (mainMask[indexOf(x, y)]) hit++;
});
return n > 0 && hit / n >= (pass === 0 ? 0.10 : 0.01);
}
for (const [key, paths] of groups) {
const kept = [];
for (const path of paths || []) {
if (touchesMain(path) || pathNearAdminCenter(path)) kept.push(path);
else pruned[key]++;
}
paths.length = 0;
paths.push(...kept);
}
comps = components();
}
debugLayers.finalOutputRoadConnectivity = { beforeComponents: before, afterComponents: comps.length, pruned };
}
pruneIsolatedFinalRoadComponents();
function ensureAdminCenterCellsAfterOutputPrune() {
let added = 0;
function roadTouches(center) {
for (const path of [...minorRoads, ...nationalRoads, ...externalRoads]) {
for (const [x, y] of path || []) if (Math.hypot(center.x - x, center.y - y) <= 0.65) return true;
}
return false;
}
for (const center of adminCenters || []) {
if (!center || !inside(center.x, center.y) || sea[indexOf(center.x, center.y)]) continue;
if (roadTouches(center)) continue;
const x = Math.round(center.x);
const y = Math.round(center.y);
const horizontal = [[Math.max(0, x - 1), y], [x, y], [Math.min(MAP_W - 1, x + 1), y]]
.filter((pt, idx, arr) => idx === 0 || pt[0] !== arr[idx - 1][0] || pt[1] !== arr[idx - 1][1]);
const vertical = [[x, Math.max(0, y - 1)], [x, y], [x, Math.min(MAP_H - 1, y + 1)]]
.filter((pt, idx, arr) => idx === 0 || pt[0] !== arr[idx - 1][0] || pt[1] !== arr[idx - 1][1]);
minorRoads.push(horizontal.length >= 2 ? horizontal : vertical);
added++;
}
if (transportDebug) {
transportDebug.layers ||= {};
transportDebug.layers.adminCenterFinalStubs = added;
}
}
ensureAdminCenterCellsAfterOutputPrune();
nameDebug.maxDerivedPerBase = 0;
const prefectureRegions = buildPrefectureRegions(prefectureRegionId, sea, nameFields, seed, usedNames, nameDebug, { modernCities, adminCenters });
const regionalPrefectureBorders = adminRegionalPrefectureBorders || [];
@ -657,6 +857,25 @@ export function finishMapOutput({
regionalDebug,
terrainDebug,
regionalPrefectureBorders,
geography,
geographyDebug: geography?.geographyDebug || null,
habitability: geography?.habitability || null,
accessibility: geography?.accessibility || null,
centrality: geography?.centrality || null,
naturalCentrality: geography?.naturalCentrality || null,
humanCentrality: geography?.humanCentrality || null,
transportAccessibility: geography?.transportAccessibility || null,
geographicBarrier: geography?.geographicBarrier || null,
geographicBarrierCost: geography?.geographicBarrierCost || geography?.barrierCost || null,
barrierCost: geography?.barrierCost || geography?.geographicBarrierCost || null,
corridorSuitability: geography?.corridorSuitability || null,
adminBoundaryPreference: geography?.adminBoundaryPreference || null,
boundaryAvoidance: geography?.boundaryAvoidance || null,
lowlandCapacity: geography?.lowlandCapacity || null,
valleyAccess: geography?.valleyAccess || null,
coastalAccess: geography?.coastalAccess || null,
geographicCompartmentProfiles: geography?.compartmentProfiles || [],
watershedProfiles: geography?.watershedProfiles || [],
elevation,
moisture,
slope,
@ -675,6 +894,7 @@ export function finishMapOutput({
basinField,
coastalLowland,
flowAccum,
watershedId,
erosionField,
depositionField,
arcSpineField,
@ -686,6 +906,7 @@ export function finishMapOutput({
naturalCompartmentId,
naturalCompartments,
villages,
geographicUrbanAnchors,
ports,
crossings,
passes,