chinko
This commit is contained in:
parent
0359bb2445
commit
b17be0e0d2
21 changed files with 8034 additions and 2737 deletions
237
mapOutput.js
237
mapOutput.js
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue