503 lines
29 KiB
JavaScript
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) };
|
|
}
|