map/mapAdminStage.js
2026-05-28 15:48:42 +09:00

503 lines
29 KiB
JavaScript

import {
applyLandscapeUnitAdminPartition,
assignAdminRegionsFromNaturalCompartments,
lockSmallUrbanComponentsToMunicipality,
mergeTinyMunicipalities,
removeMunicipalExclaves,
enforceMunicipalityConnectivityStrict,
smoothAdminRegionsTerrainAware,
snapAdminBoundariesToTerrain,
} from "./adminRegions.js";
import { INF, clamp, indexOf, inside, rand } from "./mapUtils.js";
import { extractAdminBorderSegments } from "./mapGeneratorHelpers.js";
import {
changedCellsSince,
municipalityAreaById,
maskLandArea,
isProtectedAdminSeed,
buildSeedLifecycle,
activeSeedIds,
} from "./mapAdminShared.js";
import {
compactWholeCompartmentMunicipalities,
enforceCompartmentMunicipalityOwnership,
enforceSimpleAdministrativeHierarchy,
lockCompactUrbanAreasToDominantAdmin,
repairAdminSingleOwnerEnclaves,
splitOversizedCompartmentMunicipalities,
} from "./mapAdminCompartmentRepair.js";
import { generatePrefecturesFromMunicipalities } from "./mapPrefectureStage.js";
import {
absorbSeedCompartments,
splitOversizedLowlandsWithPendingSeeds,
promotePendingSeedsForMunicipalityCount,
restoreSurvivedSeedsByCompartment,
} from "./mapAdminSeedLifecycle.js";
import {
computeTargetMunicipalityCount,
buildLowlandAdminSeeds,
} from "./mapAdminTargets.js";
import {
classifySatelliteMunicipalities,
expandSatelliteMunicipalityCatchment,
enforceCityMunicipalityCatchments,
} from "./mapAdminUrbanCatchments.js";
function generateAdminLayoutForMask({
seed,
prefectureMask,
sea,
elevation,
slope,
river,
ridgeField,
naturalBarrierScore,
valleyField,
basinField,
coastalLowland,
flowAccum,
plain,
agriculture,
settlementScore,
populationDensity,
stationInfluence,
roadInfluence,
railInfluence2,
villageInfluence,
landuse,
modernCities,
satelliteCities,
newTowns,
markets,
villages,
ports,
stations,
industrialZones,
logisticsParks,
naturalCompartmentId,
naturalCompartments,
geography = null,
habitability = null,
accessibility = null,
centrality = null,
geographicBarrier = null,
geographicBarrierCost = null,
adminBoundaryPreference = null,
boundaryAvoidance = null,
adminRegionMeta = {},
adminProgress = null,
}) {
const unifiedBoundaryPreference = adminBoundaryPreference || geography?.adminBoundaryPreference || null;
const unifiedBoundaryAvoidance = boundaryAvoidance || geography?.boundaryAvoidance || null;
const unifiedGeographicBarrier = geographicBarrier || geography?.geographicBarrier || null;
const boundaryRidgeField = naturalBarrierScore
? Float32Array.from(ridgeField, (value, i) => clamp(value * 0.72 + naturalBarrierScore[i] * 0.46))
: ridgeField;
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "classify satellites" });
const satelliteClassificationDebug = classifySatelliteMunicipalities(satelliteCities, modernCities, prefectureMask, sea, landuse, populationDensity, roadInfluence, railInfluence2, boundaryRidgeField, river, flowAccum);
const targetMunicipalityCount = computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField: boundaryRidgeField, coastalLowland, basinField, habitability: habitability || geography?.habitability, centrality: centrality || geography?.centrality, accessibility: accessibility || geography?.accessibility, geographicBarrier: unifiedGeographicBarrier, modernCities, markets, ports, satelliteCities, villages, adminRegionMeta });
const compartmentMultiplier = clamp(4.6 + rand(seed, 1320) * 2.4, 4.6, 7.0);
const regionLandArea = adminRegionMeta.landArea || maskLandArea(prefectureMask, sea);
const minCompartmentTarget = clamp(Math.round(Math.max(targetMunicipalityCount * 3.4, regionLandArea / 78)), 22, 150);
const maxCompartmentTarget = clamp(Math.round(Math.max(targetMunicipalityCount * 6.0, regionLandArea / 36)), minCompartmentTarget, 320);
let targetCompartmentCount = clamp(Math.round(targetMunicipalityCount * compartmentMultiplier), minCompartmentTarget, maxCompartmentTarget);
let adminCentersRaw = buildLowlandAdminSeeds({
seed,
targetMunicipalityCount,
prefectureMask,
sea,
elevation,
slope,
ridgeField: boundaryRidgeField,
plain,
basinField,
coastalLowland,
settlementScore,
populationDensity,
roadInfluence,
railInfluence2,
stationInfluence,
landuse,
habitability: habitability || geography?.habitability,
accessibility: accessibility || geography?.accessibility,
centrality: centrality || geography?.centrality,
boundaryAvoidance: unifiedBoundaryAvoidance,
adminBoundaryPreference: unifiedBoundaryPreference,
geographicBarrier: unifiedGeographicBarrier,
modernCities,
satelliteCities,
markets,
ports,
newTowns,
stations,
});
if (adminCentersRaw.length < targetMunicipalityCount) targetCompartmentCount = Math.max(targetCompartmentCount, minCompartmentTarget);
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "natural compartments", targetMunicipalityCount, targetCompartmentCount, seedCount: adminCentersRaw.length });
const compartmentAssignment = assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw, {
seed,
targetMunicipalityCount,
targetCompartmentCount,
maxNaturalCompartmentArea: Math.max(32, Math.round(maskLandArea(prefectureMask, sea) / Math.max(1, targetCompartmentCount) * 1.65)),
naturalCompartmentId,
naturalCompartments,
naturalBarrierScore,
geography,
habitability: habitability || geography?.habitability,
accessibility: accessibility || geography?.accessibility,
centrality: centrality || geography?.centrality,
boundaryAvoidance: unifiedBoundaryAvoidance,
adminBoundaryPreference: unifiedBoundaryPreference,
geographicBarrier: unifiedGeographicBarrier,
progress: (step) => adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step }),
});
const adminId = compartmentAssignment.adminId;
if (naturalCompartmentId && naturalCompartments) {
const oversizedCompartmentSplit = splitOversizedCompartmentMunicipalities(adminId, adminCentersRaw, compartmentAssignment.compartments, prefectureMask, sea, {
elevation, slope, ridgeField: boundaryRidgeField, plain, agriculture, basinField, coastalLowland, populationDensity,
}, seed + 21900);
const changedAfterInitialCompartmentOwnership = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea);
const hierarchyRepair = enforceSimpleAdministrativeHierarchy(adminId, compartmentAssignment.compartments, prefectureMask, sea, {
minCompartmentsPerMunicipality: 2,
maxUrbanClusterCells: 1600,
modernCities,
});
const changedAfterFinalCompartmentOwnership = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea);
const compacted = compactWholeCompartmentMunicipalities(adminId, adminCentersRaw, prefectureMask, sea, { populationDensity, plain, slope });
const strictConnectivityChangedCells = enforceMunicipalityConnectivityStrict(compacted.adminId, prefectureMask, sea, compacted.adminCentersRaw, [...modernCities, ...(compacted.adminCentersRaw || [])], 8);
const strictEnclaveRepairChangedCells = repairAdminSingleOwnerEnclaves(compacted.adminId, prefectureMask, sea, 4);
const adminBorders = extractAdminBorderSegments(compacted.adminId, prefectureMask);
const actualMunicipalityCount = new Set([...compacted.adminId].filter((id, i) => id >= 0 && prefectureMask[i] && !sea[i])).size;
const naturalCompartmentCount = compartmentAssignment.debug?.naturalCompartmentCount || naturalCompartments.filter((unit) => unit && unit.area > 0).length;
const adminDebug = {
...compartmentAssignment.debug,
simpleHierarchyPrototype: true,
administrativeHierarchySpec: "unified-geography->natural-compartments->living-sphere-municipalities->prefectures",
unifiedGeographyAdministrativeBasis: true,
naturalCompartmentsImmutable: true,
municipalitiesAreCompartmentGroups: true,
prefecturesAreMunicipalityGroups: true,
cellLevelAdminSmoothingDisabled: true,
sharedNaturalCompartmentLayer: true,
skippedLegacyCellCleanupForHierarchy: true,
targetMunicipalityCount,
actualMunicipalityCount,
finalMunicipalityCount: actualMunicipalityCount,
changedAfterStrictMunicipalityConnectivity: strictConnectivityChangedCells,
changedAfterStrictMunicipalityEnclaveRepair: strictEnclaveRepairChangedCells,
candidateSeedCount: adminCentersRaw.length,
municipalOfficePointCount: compacted.adminCentersRaw.length,
seedCellRevivalCount: 0,
survivedSeedCount: compacted.activeMunicipalityCount,
pendingSeedCount: 0,
absorbedSeedCount: Math.max(0, adminCentersRaw.length - compacted.activeMunicipalityCount),
targetNaturalCompartmentCount: targetCompartmentCount,
naturalCompartmentCount,
compartmentCount: naturalCompartmentCount,
changedAfterCompartmentAssignment: naturalCompartmentCount,
changedAfterInitialCompartmentOwnership,
changedAfterFinalCompartmentOwnership,
changedAfterUrbanUnification: hierarchyRepair.changedAfterUrbanUnification,
urbanComponentsUnified: hierarchyRepair.urbanComponentsUnified,
changedAfterCityMetroMunicipalityUnification: hierarchyRepair.changedAfterCityMetroMunicipalityUnification,
cityMetroMunicipalitiesUnified: hierarchyRepair.cityMetroMunicipalitiesUnified,
changedAfterCompartmentConnectivity: hierarchyRepair.changedAfterCompartmentConnectivity,
disconnectedCompartmentComponentsMerged: hierarchyRepair.disconnectedCompartmentComponentsMerged,
changedAfterAdminEnclaveRepair: hierarchyRepair.changedAfterCompartmentEnclaveRepair,
compartmentEnclaveComponentsMerged: hierarchyRepair.compartmentEnclaveComponentsMerged,
changedAfterSingleCompartmentMunicipalityMerge: hierarchyRepair.changedAfterSingleCompartmentMunicipalityMerge,
singleCompartmentMunicipalitiesMerged: hierarchyRepair.singleCompartmentMunicipalitiesMerged,
remainingSingleCompartmentMunicipalities: hierarchyRepair.remainingSingleCompartmentMunicipalities,
changedAfterPostMergeCompartmentOwnership: changedAfterFinalCompartmentOwnership,
changedAfterOversizedCompartmentSplit: oversizedCompartmentSplit.changedCells,
oversizedCompartmentMunicipalitiesSplit: oversizedCompartmentSplit.splitMunicipalities,
oversizedCompartmentSplitAddedCenters: oversizedCompartmentSplit.addedCenters,
oversizedCompartmentSplitMaxArea: oversizedCompartmentSplit.maxArea || 0,
finalTinyMunicipalityCount: [...municipalityAreaById(compacted.adminId, prefectureMask, sea).values()].filter((area) => area > 0 && area < 8).length,
compartmentBorders: compartmentAssignment.debug?.compartmentBorders || [],
borderNaturalBarrierAverage: compartmentAssignment.debug?.finalBorderNaturalBarrierAverage || 0,
voronoiLikeRate: compartmentAssignment.debug?.voronoiLikeRateAfter || 0,
};
return {
adminCentersRaw: compacted.adminCentersRaw,
adminId: compacted.adminId,
adminBorders,
adminDebug,
naturalCompartmentId: compartmentAssignment.compartmentId,
naturalCompartments: compartmentAssignment.compartments,
};
}
let previousSnapshot = new Int16Array(adminId);
const adminDebug = {
changedAfterSmooth: 0,
changedAfterUrbanLock: 0,
changedAfterSmallUrbanLock: 0,
changedAfterInitialMerge: 0,
changedAfterInitialExclaveRemoval: 0,
changedAfterLandscapePartition: 0,
changedAfterSnap: 0,
changedAfterOversizedRuralSplit: 0,
changedAfterFinalExclaveRemoval: 0,
changedAfterFinalMerge: 0,
targetMunicipalityCount,
actualMunicipalityCount: 0,
municipalityCountReason: "unified habitability/accessibility, settlement hierarchy, coastline complexity, basin/lowland bonus, and natural-barrier adjustment",
unifiedGeographyAdministrativeBasis: true,
administrativeHierarchySpec: "unified-geography->natural-compartments->living-sphere-municipalities->prefectures",
changedAfterCompartmentAssignment: compartmentAssignment.debug?.naturalCompartmentCount || 0,
oversizedRuralSplits: 0,
oversizedLowlandSplits: 0,
ruralSplitsAccepted: 0,
ruralSplitsRejected: 0,
targetNaturalCompartmentCount: targetCompartmentCount,
compartmentMultiplier,
lowlandAdminSeedCount: adminCentersRaw.filter((p) => p.invisibleLowlandAdminSeed).length,
lowlandAdminSeeds: adminCentersRaw.filter((p) => p.invisibleLowlandAdminSeed).map((p) => ({ x: p.x, y: p.y })),
realAdminSeedCount: adminCentersRaw.filter((p) => !p.invisibleLowlandAdminSeed).length,
highMountainAdminSeedCount: adminCentersRaw.filter((p) => {
const i = indexOf(p.x, p.y);
return elevation[i] > 0.70 || slope[i] > 0.52 || boundaryRidgeField[i] > 0.62;
}).length,
candidateSeedCount: adminCentersRaw.length,
protectedSeedCount: adminCentersRaw.filter(isProtectedAdminSeed).length,
survivedSeedCount: 0,
pendingSeedCount: 0,
absorbedSeedCount: 0,
pendingSeedsUsedForLowlandSplit: 0,
finalMunicipalityCount: 0,
finalTinyMunicipalityCount: 0,
seedCellRevivalCount: 0,
satelliteMunicipalitiesCreated: adminCentersRaw.filter((p) => p.protectedSatellite).length,
satelliteMunicipalitiesMerged: 0,
satelliteMunicipalitiesExpanded: 0,
satelliteMunicipalitiesTooSmall: 0,
averageSatelliteMunicipalityArea: 0,
minSatelliteMunicipalityArea: 0,
satelliteMunicipalityAreaByNameOrIndex: {},
independentSatelliteMunicipalities: satelliteClassificationDebug.independent,
attachedSatelliteDistricts: satelliteClassificationDebug.attached,
satelliteMunicipalityStats: satelliteClassificationDebug,
...compartmentAssignment.debug,
};
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "seed lifecycle" });
const seedLifecycle = buildSeedLifecycle(adminCentersRaw, adminId, prefectureMask, sea, 35);
const pendingSplitDebug = splitOversizedLowlandsWithPendingSeeds(adminId, prefectureMask, sea, {
plain, agriculture, basinField, coastalLowland, valleyField, ridgeField: boundaryRidgeField, slope, elevation,
}, compartmentAssignment.compartments, adminCentersRaw, seedLifecycle, [...(satelliteCities || []), ...newTowns, ...markets, ...villages, ...ports]);
adminDebug.changedAfterPendingSeedLowlandSplit = pendingSplitDebug.changedCells;
adminDebug.pendingSeedsUsedForLowlandSplit = pendingSplitDebug.pendingSeedsUsed;
adminDebug.oversizedLowlandSplits += pendingSplitDebug.splitMunicipalities;
const pendingPromotionDebug = promotePendingSeedsForMunicipalityCount(adminId, prefectureMask, sea, {
plain, agriculture, basinField, coastalLowland, valleyField, ridgeField: boundaryRidgeField, slope, elevation,
}, compartmentAssignment.compartments, adminCentersRaw, seedLifecycle, Math.min(24, targetMunicipalityCount));
adminDebug.changedAfterPendingSeedCountRepair = pendingPromotionDebug.changedCells;
adminDebug.pendingSeedsPromotedForCount = pendingPromotionDebug.promotedSeeds;
let areaAfterPendingSplit = municipalityAreaById(adminId, prefectureMask, sea);
for (const seedState of seedLifecycle) {
if (seedState.state !== "pending") continue;
seedState.area = areaAfterPendingSplit.get(seedState.id) || 0;
if (seedState.area >= 35) seedState.state = "survived";
else seedState.state = "absorbed";
}
adminDebug.changedAfterAbsorbingSeeds = absorbSeedCompartments(adminId, compartmentAssignment.compartments, seedLifecycle);
let activeAdminIds = activeSeedIds(seedLifecycle);
function markChanged(field) {
adminDebug[field] = changedCellsSince(previousSnapshot, adminId, prefectureMask, sea);
previousSnapshot = new Int16Array(adminId);
}
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "smooth boundaries" });
smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, populationDensity, landuse, 2);
markChanged("changedAfterSmooth");
function lockUrbanClusterToMunicipality(city, radius, allowSuburban = true) {
if (!city || !prefectureMask[indexOf(city.x, city.y)]) return;
let bestAdmin = -1;
let bestD = INF;
adminCentersRaw.forEach((center, id) => {
if (!activeAdminIds.has(id)) return;
const d = Math.hypot(center.x - city.x, center.y - city.y);
if (d < bestD) { bestD = d; bestAdmin = id; }
});
if (bestAdmin < 0) return;
const r = Math.ceil(radius);
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
const x = city.x + dx;
const y = city.y + dy;
if (!inside(x, y)) continue;
const i = indexOf(x, y);
if (!prefectureMask[i] || sea[i]) continue;
const d = Math.hypot(dx, dy);
if (d > radius) continue;
const urban = landuse[i] === 2 || landuse[i] === 3 || (allowSuburban && (landuse[i] === 4 || landuse[i] === 7 || landuse[i] === 8));
if (urban || populationDensity[i] > 0.22) adminId[i] = bestAdmin;
}
}
}
for (const city of modernCities) {
const radius = (city.population || 0) >= 500000
? clamp(17 + Math.sqrt(city.population) / 120, 20, 38)
: clamp(5 + Math.sqrt(city.population || 70000) / 210, 6, 11);
lockUrbanClusterToMunicipality(city, radius, true);
}
for (const sat of satelliteCities || []) {
if (!prefectureMask[indexOf(sat.x, sat.y)]) continue;
let bestAdmin = -1;
if (sat.municipalityClass === "independentSatelliteMunicipality") {
let bestD = INF;
adminCentersRaw.forEach((center, id) => {
if (!activeAdminIds.has(id)) return;
const d = Math.hypot(center.x - sat.x, center.y - sat.y);
if (d < bestD) { bestD = d; bestAdmin = id; }
});
} else if (inside(sat.parentX ?? -1, sat.parentY ?? -1)) {
bestAdmin = adminId[indexOf(sat.parentX, sat.parentY)];
}
if (bestAdmin < 0) continue;
sat.parentAdminHint = bestAdmin;
const changed = expandSatelliteMunicipalityCatchment(adminId, sat, bestAdmin, {
prefectureMask, sea, elevation, slope, river, ridgeField: boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum,
landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence, modernCities,
});
if (changed > 0 && sat.municipalityClass === "independentSatelliteMunicipality") adminDebug.satelliteMunicipalitiesExpanded++;
}
markChanged("changedAfterUrbanLock");
lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, 520);
lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, 620);
markChanged("changedAfterSmallUrbanLock");
const activeAdminCenters = () => adminCentersRaw.filter((_, id) => activeAdminIds.has(id));
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities, 120, { satelliteCities, satelliteStats: adminDebug, satelliteMinArea: 100, protectedPoints: activeAdminCenters() });
markChanged("changedAfterInitialMerge");
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "topology cleanup" });
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, modernCities, 180);
markChanged("changedAfterInitialExclaveRemoval");
// The initial compartment graph assignment is now the primary natural partition.
// Re-running the older raw landscape-unit pass here collapses lowland seeds into a few broad owners.
markChanged("changedAfterLandscapePartition");
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "split oversized municipalities" });
// The older oversized-lowland pass rebuilds natural compartments a second time.
// The current pipeline already performs pending-seed lowland splitting on the active
// compartment graph above, so keep the full admin layout while avoiding the duplicate
// high-cost recomputation.
const oversizedSplitDebug = {
changedCells: 0,
splitMunicipalities: 0,
rejectedMunicipalities: 0,
skippedDuplicateCompartmentRebuild: true,
skippedForVisibleFragment: false,
};
adminDebug.changedAfterOversizedRuralSplit = oversizedSplitDebug.changedCells;
adminDebug.oversizedRuralMunicipalitiesSplit = oversizedSplitDebug.splitMunicipalities;
adminDebug.oversizedRuralSplits = oversizedSplitDebug.splitMunicipalities;
adminDebug.oversizedLowlandSplits = oversizedSplitDebug.splitMunicipalities;
adminDebug.ruralSplitsAccepted = oversizedSplitDebug.splitMunicipalities;
adminDebug.ruralSplitsRejected = oversizedSplitDebug.rejectedMunicipalities || 0;
adminDebug.oversizedSplitSkippedForVisibleFragment = Boolean(oversizedSplitDebug.skippedForVisibleFragment);
adminDebug.oversizedSplitSkippedDuplicateCompartmentRebuild = Boolean(oversizedSplitDebug.skippedDuplicateCompartmentRebuild);
previousSnapshot = new Int16Array(adminId);
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "terrain snap" });
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, flowAccum, populationDensity, landuse, adminCentersRaw, [...modernCities, ...satelliteCities, ...ports, ...industrialZones, ...logisticsParks], 2);
markChanged("changedAfterSnap");
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 360);
markChanged("changedAfterFinalExclaveRemoval");
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities, 80, { satelliteCities, satelliteStats: adminDebug, satelliteMinArea: 90, protectedPoints: activeAdminCenters() });
markChanged("changedAfterFinalMerge");
for (const sat of satelliteCities || []) {
if (sat.municipalityClass !== "independentSatelliteMunicipality" || !inside(sat.x, sat.y)) continue;
const targetAdmin = adminId[indexOf(sat.x, sat.y)];
if (targetAdmin < 0) continue;
expandSatelliteMunicipalityCatchment(adminId, sat, targetAdmin, {
prefectureMask, sea, elevation, slope, river, ridgeField: boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum,
landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence, modernCities,
});
}
const cityCatchmentDebug = enforceCityMunicipalityCatchments(adminId, modernCities, {
prefectureMask, sea, elevation, slope, river, ridgeField: boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum,
landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence,
});
adminDebug.changedAfterCityMunicipalityCatchment = cityCatchmentDebug.changed;
adminDebug.protectedCityMunicipalityCount = cityCatchmentDebug.protectedCities;
adminDebug.tooSmallCityMunicipalityCountBeforeRepair = cityCatchmentDebug.tooSmall;
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, flowAccum, populationDensity, landuse, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 2);
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 260);
const finalPromotionDebug = promotePendingSeedsForMunicipalityCount(adminId, prefectureMask, sea, {
plain, agriculture, basinField, coastalLowland, valleyField, ridgeField: boundaryRidgeField, slope, elevation,
}, compartmentAssignment.compartments, adminCentersRaw, seedLifecycle, Math.min(22, targetMunicipalityCount));
adminDebug.changedAfterFinalPendingSeedCountRepair = finalPromotionDebug.changedCells;
adminDebug.pendingSeedsPromotedForCount += finalPromotionDebug.promotedSeeds;
const restoredSeedDebug = restoreSurvivedSeedsByCompartment(adminId, prefectureMask, sea, compartmentAssignment.compartments, adminCentersRaw, seedLifecycle, Math.min(22, targetMunicipalityCount));
adminDebug.changedAfterSurvivedSeedCompartmentRestore = restoredSeedDebug.changedCells;
adminDebug.survivedSeedsRestoredByCompartment = restoredSeedDebug.restoredSeeds;
let finalAreaBySeed = municipalityAreaById(adminId, prefectureMask, sea);
for (const seedState of seedLifecycle) {
seedState.area = finalAreaBySeed.get(seedState.id) || 0;
if (!seedState.protected && seedState.state === "pending" && seedState.area < 25) seedState.state = "absorbed";
}
absorbSeedCompartments(adminId, compartmentAssignment.compartments, seedLifecycle);
activeAdminIds = activeSeedIds(seedLifecycle);
adminDebug.changedAfterFinalCompartmentOwnership = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea);
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 220);
adminDebug.changedAfterPostCompartmentExclaveRemoval = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea);
adminDebug.changedAfterUrbanUnification = lockCompactUrbanAreasToDominantAdmin(adminId, prefectureMask, sea, landuse, populationDensity, 980);
adminDebug.changedAfterAdminEnclaveRepair = repairAdminSingleOwnerEnclaves(adminId, prefectureMask, sea, 6);
adminDebug.changedAfterFinalCompartmentOwnership += enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea);
adminDebug.changedAfterStrictMunicipalityConnectivity = enforceMunicipalityConnectivityStrict(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 8);
adminDebug.changedAfterStrictMunicipalityEnclaveRepair = repairAdminSingleOwnerEnclaves(adminId, prefectureMask, sea, 4);
const areaById = municipalityAreaById(adminId, prefectureMask, sea);
const satelliteAreas = [];
(satelliteCities || []).forEach((sat, index) => {
if (!inside(sat.x, sat.y) || !prefectureMask[indexOf(sat.x, sat.y)]) return;
const id = adminId[indexOf(sat.x, sat.y)];
const area = areaById.get(id) || 0;
const key = sat.name || `satellite-${index}`;
adminDebug.satelliteMunicipalityAreaByNameOrIndex[key] = area;
if (sat.municipalityClass === "independentSatelliteMunicipality" && (area < Math.max(120, sat.satelliteMinArea || 0) || ((sat.population || 0) >= 60000 && area < 150))) {
sat.municipalityClass = "smallTownAttachedToRuralMunicipality";
adminDebug.satelliteMunicipalitiesTooSmall++;
return;
}
if (sat.municipalityClass === "independentSatelliteMunicipality") {
satelliteAreas.push(area);
if (area < 80) adminDebug.satelliteMunicipalitiesTooSmall++;
}
});
adminDebug.averageSatelliteMunicipalityArea = satelliteAreas.length ? satelliteAreas.reduce((sum, value) => sum + value, 0) / satelliteAreas.length : 0;
adminDebug.minSatelliteMunicipalityArea = satelliteAreas.length ? Math.min(...satelliteAreas) : 0;
adminDebug.satelliteMunicipalitiesIndependent = satelliteAreas.length;
const landscapeDebug = applyLandscapeUnitAdminPartition.lastDebug || {};
Object.assign(adminDebug, landscapeDebug);
adminDebug.targetNaturalCompartmentCount = compartmentAssignment.debug?.targetNaturalCompartmentCount || targetCompartmentCount;
adminDebug.naturalCompartmentCount = compartmentAssignment.debug?.naturalCompartmentCount || adminDebug.naturalCompartmentCount || adminDebug.compartmentCount || 0;
adminDebug.compartmentCount = adminDebug.naturalCompartmentCount;
adminDebug.averageCompartmentsPerMunicipality = compartmentAssignment.debug?.averageCompartmentsPerMunicipality || adminDebug.averageCompartmentsPerMunicipality || 0;
adminDebug.singleCompartmentMunicipalityRatio = compartmentAssignment.debug?.singleCompartmentMunicipalityRatio ?? adminDebug.singleCompartmentMunicipalityRatio ?? 0;
adminDebug.actualMunicipalityCount = new Set([...adminId].filter((id, i) => id >= 0 && prefectureMask[i] && !sea[i])).size;
adminDebug.averageCompartmentsPerMunicipality = adminDebug.actualMunicipalityCount ? adminDebug.naturalCompartmentCount / adminDebug.actualMunicipalityCount : 0;
adminDebug.survivedSeedCount = seedLifecycle.filter((seed) => seed.state === "survived").length;
adminDebug.pendingSeedCount = seedLifecycle.filter((seed) => seed.state === "pending").length;
adminDebug.absorbedSeedCount = seedLifecycle.filter((seed) => seed.state === "absorbed").length;
adminDebug.finalMunicipalityCount = adminDebug.actualMunicipalityCount;
adminDebug.finalTinyMunicipalityCount = [...areaById.values()].filter((area) => area > 0 && area < 8).length;
adminDebug.seedLifecycle = seedLifecycle.map((seed) => ({ id: seed.id, state: seed.state, protected: seed.protected, area: seed.area }));
adminDebug.borderNaturalBarrierAverage = adminDebug.finalBorderNaturalBarrierAverage ?? 0;
adminDebug.voronoiLikeRate = adminDebug.voronoiLikeRateAfter ?? 0;
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "extract borders" });
const adminBorders = extractAdminBorderSegments(adminId, prefectureMask);
return {
adminCentersRaw,
adminId,
adminBorders,
adminDebug,
naturalCompartmentId: compartmentAssignment.compartmentId,
naturalCompartments: compartmentAssignment.compartments,
};
}
export function generateAdminLayout(context) {
const layout = generateAdminLayoutForMask(context);
return { ...layout, ...generatePrefecturesFromMunicipalities(context, layout) };
}