road update
This commit is contained in:
parent
71b58cf033
commit
a6e66f2838
9 changed files with 2174 additions and 258 deletions
280
mapOutput.js
280
mapOutput.js
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue