import { applyLandscapeUnitAdminPartition, assignAdminRegionsFromNaturalCompartments, lockSmallUrbanComponentsToMunicipality, mergeTinyMunicipalities, removeMunicipalExclaves, smoothAdminRegionsTerrainAware, splitOversizedLowlandMunicipalities, snapAdminBoundariesToTerrain, } from "./adminRegions.js"; import { INF, MAP_H, MAP_W, SIZE, MinHeap, clamp, hash2, indexOf, inside, pickEntities, rand, xyOf } from "./mapUtils.js"; import { extractAdminBorderSegments } from "./mapGeneratorHelpers.js"; function changedCellsSince(before, after, prefectureMask, sea) { let changed = 0; for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && before[i] !== after[i]) changed++; return changed; } function municipalityAreaById(adminId, prefectureMask, sea) { const area = new Map(); for (let i = 0; i < SIZE; i++) { if (!prefectureMask[i] || sea[i] || adminId[i] < 0) continue; area.set(adminId[i], (area.get(adminId[i]) || 0) + 1); } return area; } function computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages }) { let landCells = 0; let habitableCells = 0; let lowlandCells = 0; let coastlineComplexity = 0; let mountainCells = 0; for (let y = 1; y < MAP_H - 1; y++) { for (let x = 1; x < MAP_W - 1; x++) { const i = indexOf(x, y); if (!prefectureMask[i] || sea[i]) continue; landCells++; if (slope[i] < 0.42 && ridgeField[i] < 0.55 && (!elevation || elevation[i] < 0.72)) habitableCells++; if ((coastalLowland[i] > 0.20 || basinField[i] > 0.24) && slope[i] < 0.36 && ridgeField[i] < 0.52) lowlandCells++; if (ridgeField[i] > 0.52 || slope[i] > 0.48) mountainCells++; for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) { const ni = indexOf(nx, ny); if (sea[ni]) { coastlineComplexity += 1 + coastalLowland[i] * 0.8; break; } } } } const independentSatellites = (satelliteCities || []).filter((p) => p.municipalityClass === "independentSatelliteMunicipality").length; const settlementWeight = modernCities.length * 1.6 + markets.length * 1.0 + ports.length * 0.8 + independentSatellites * 0.7 + villages.length * 0.25; const basinBonus = Math.min(8, [...basinField].filter((v, i) => prefectureMask[i] && !sea[i] && v > 0.34).length / 520); const mountainRatio = landCells ? mountainCells / landCells : 0; const lowlandBonus = Math.min(7, lowlandCells / 430); const target = Math.round(habitableCells / 230 + settlementWeight + coastlineComplexity * 0.03 + basinBonus * 0.55 + lowlandBonus - mountainRatio * 2.2); return clamp(target, 20, 50); } function estimateUrbanComponentArea(city, prefectureMask, sea, landuse, populationDensity) { if (!city || !inside(city.x, city.y)) return 0; const start = indexOf(city.x, city.y); if (!prefectureMask[start] || sea[start]) return 0; const radius = Math.ceil(Math.max(7, (city.urbanRadius || 6) * 1.7)); const seen = new Uint8Array(SIZE); const queue = [start]; seen[start] = 1; let area = 0; for (let q = 0; q < queue.length; q++) { const cur = queue[q]; const [x, y] = xyOf(cur); const d = Math.hypot(x - city.x, y - city.y); if (d > radius) continue; const urban = (landuse[cur] >= 2 && landuse[cur] <= 4) || landuse[cur] === 7 || landuse[cur] === 8 || populationDensity[cur] > 0.18; if (!urban) continue; area++; for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = x + dx, ny = y + dy; if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (seen[ni] || !prefectureMask[ni] || sea[ni]) continue; seen[ni] = 1; queue.push(ni); } } return area; } function terrainSeparationBetween(a, b, ridgeField, river, flowAccum, populationDensity, landuse) { if (!a || !b) return { separatedByBarrier: false, ruralGap: false, averageDensity: 0, maxBarrier: 0 }; const steps = Math.max(1, Math.ceil(Math.hypot(a.x - b.x, a.y - b.y))); let maxBarrier = 0; let lowUrbanRun = 0; let bestLowUrbanRun = 0; let densitySum = 0; for (let s = 0; s <= steps; s++) { const t = s / steps; const x = Math.round(a.x + (b.x - a.x) * t); const y = Math.round(a.y + (b.y - a.y) * t); if (!inside(x, y)) continue; const i = indexOf(x, y); const barrier = Math.max(ridgeField[i] * 0.95, river[i] * 0.85, flowAccum[i] * 0.42); maxBarrier = Math.max(maxBarrier, barrier); densitySum += populationDensity[i]; const urban = (landuse[i] >= 2 && landuse[i] <= 4) || landuse[i] === 7 || populationDensity[i] > 0.20; if (urban) lowUrbanRun = 0; else { lowUrbanRun++; bestLowUrbanRun = Math.max(bestLowUrbanRun, lowUrbanRun); } } return { separatedByBarrier: maxBarrier > 0.56, ruralGap: bestLowUrbanRun >= 4, averageDensity: densitySum / (steps + 1), maxBarrier, }; } function classifySatelliteMunicipalities(satelliteCities, modernCities, prefectureMask, sea, landuse, populationDensity, roadInfluence, railInfluence2, ridgeField, river, flowAccum) { let independent = 0; let attached = 0; for (const sat of satelliteCities || []) { if (!sat || !inside(sat.x, sat.y) || !prefectureMask[indexOf(sat.x, sat.y)] || sea[indexOf(sat.x, sat.y)]) continue; const parent = modernCities[sat.parentCityIndex] || modernCities.slice().sort((a, b) => Math.hypot(a.x - sat.x, a.y - sat.y) - Math.hypot(b.x - sat.x, b.y - sat.y))[0]; const parentDistance = parent ? Math.hypot(parent.x - sat.x, parent.y - sat.y) : 99; const separation = terrainSeparationBetween(sat, parent, ridgeField, river, flowAccum, populationDensity, landuse); const urbanArea = estimateUrbanComponentArea(sat, prefectureMask, sea, landuse, populationDensity); const i = indexOf(sat.x, sat.y); const continuousUrban = parent && parentDistance < Math.max(10, (parent.urbanRadius || 12) + (sat.urbanRadius || 5) + 5) && separation.averageDensity > 0.14 && !separation.ruralGap && !separation.separatedByBarrier; const newTownLike = landuse[i] === 7 || (railInfluence2[i] > 0.22 && roadInfluence[i] > 0.12 && (sat.population || 0) < 70000); let municipalityClass = "independentSatelliteMunicipality"; if (continuousUrban && (sat.population || 0) < 90000) municipalityClass = "suburbanDistrictMergedWithParent"; else if (newTownLike && (sat.population || 0) < 85000 && !separation.separatedByBarrier) municipalityClass = "newTownDistrict"; else if ((sat.population || 0) < 42000 && urbanArea < 55 && !separation.separatedByBarrier) municipalityClass = "smallTownAttachedToRuralMunicipality"; else if ((sat.population || 0) >= 60000 && urbanArea >= 42 && (separation.separatedByBarrier || separation.ruralGap || parentDistance > 15)) municipalityClass = "independentSatelliteMunicipality"; sat.municipalityClass = municipalityClass; sat.parentX = parent?.x; sat.parentY = parent?.y; sat.parentAdminHint = -1; sat.distinctUrbanComponentArea = urbanArea; sat.separatedByBarrier = separation.separatedByBarrier || separation.ruralGap; sat.satelliteMinArea = clamp(90 + Math.sqrt(sat.population || 24000) * 0.62 + (sat.urbanRadius || 5) * 12, 80, 360); if (municipalityClass === "independentSatelliteMunicipality") independent++; else attached++; } return { independent, attached }; } function expandSatelliteMunicipalityCatchment(adminId, satellite, targetAdmin, context) { const { prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence, modernCities } = context; if (!satellite || targetAdmin < 0 || !inside(satellite.x, satellite.y)) return 0; const start = indexOf(satellite.x, satellite.y); if (!prefectureMask[start] || sea[start]) return 0; const targetAreaBase = clamp(90 + Math.sqrt(satellite.population || 24000) * 0.8 + (satellite.urbanRadius || 5) * 18, 120, 520); const targetArea = satellite.municipalityClass === "smallTownAttachedToRuralMunicipality" ? Math.min(130, targetAreaBase * 0.55) : satellite.municipalityClass === "suburbanDistrictMergedWithParent" || satellite.municipalityClass === "newTownDistrict" ? Math.min(190, targetAreaBase * 0.62) : targetAreaBase; const maxCost = satellite.municipalityClass === "independentSatelliteMunicipality" ? 46 : 32; const heap = new MinHeap(); const best = new Float32Array(SIZE); best.fill(INF); heap.push({ i: start, f: 0 }); best[start] = 0; const claimed = []; while (heap.length > 0 && claimed.length < targetArea) { const cur = heap.pop(); if (!cur || cur.f > best[cur.i] + 1e-5 || cur.f > maxCost) continue; const [x, y] = xyOf(cur.i); const d = Math.hypot(x - satellite.x, y - satellite.y); if (!prefectureMask[cur.i] || sea[cur.i]) continue; let invadesOtherCore = false; for (const city of modernCities || []) { if (!city || (city.population || 0) < 140000) continue; if (Math.hypot(city.x - satellite.x, city.y - satellite.y) < 4) continue; if (Math.hypot(city.x - x, city.y - y) <= Math.max(3.5, (city.coreRadius || 4) * 1.25)) { invadesOtherCore = true; break; } } if (invadesOtherCore) continue; const compatible = d <= (satellite.urbanRadius || 5) * 1.25 || [2, 3, 4, 7, 8].includes(landuse[cur.i]) || populationDensity[cur.i] > 0.12 || roadInfluence[cur.i] > 0.12 || railInfluence2[cur.i] > 0.10 || stationInfluence?.[cur.i] > 0.10 || basinField[cur.i] > 0.22 || valleyField[cur.i] > 0.24 || coastalLowland[cur.i] > 0.20; if (!compatible && claimed.length > targetArea * 0.55) continue; claimed.push(cur.i); for (const [dx, dy, step] of [[1, 0, 1], [-1, 0, 1], [0, 1, 1], [0, -1, 1], [1, 1, 1.41], [-1, 1, 1.41], [1, -1, 1.41], [-1, -1, 1.41]]) { const nx = x + dx, ny = y + dy; if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (!prefectureMask[ni] || sea[ni]) continue; const barrier = ridgeField[ni] * 5.2 + Math.max(0, elevation[ni] - 0.58) * 4.0 + slope[ni] * 2.2 + (river[ni] > 0.55 || flowAccum[ni] > 0.70 ? 7.5 : river[ni] > 0.30 ? 2.8 : 0); const living = ([2, 3, 4, 7, 8].includes(landuse[ni]) ? 2.2 : 0) + populationDensity[ni] * 2.0 + roadInfluence[ni] * 0.85 + railInfluence2[ni] * 0.95 + (stationInfluence?.[ni] || 0) * 1.2 + basinField[ni] * 0.42 + valleyField[ni] * 0.48 + coastalLowland[ni] * 0.32; const distanceCost = Math.hypot(nx - satellite.x, ny - satellite.y) / Math.max(7, (satellite.urbanRadius || 5) * 1.9); const nd = cur.f + Math.max(0.28, 1.05 + barrier - living + distanceCost) * step; if (nd < best[ni]) { best[ni] = nd; heap.push({ i: ni, f: nd }); } } } let changed = 0; for (const i of claimed) { if (adminId[i] !== targetAdmin) changed++; adminId[i] = targetAdmin; } return changed; } export function generateAdminLayout({ 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, }) { const boundaryRidgeField = naturalBarrierScore ? Float32Array.from(ridgeField, (value, i) => clamp(value * 0.72 + naturalBarrierScore[i] * 0.46)) : ridgeField; const municipalityCandidates = []; for (let y = 2; y < MAP_H - 2; y++) { for (let x = 2; x < MAP_W - 2; x++) { const i = indexOf(x, y); if (!prefectureMask[i] || sea[i]) continue; const urbanBias = landuse[i] === 3 ? 0.62 : landuse[i] === 2 ? 0.56 : landuse[i] === 4 ? 0.5 : landuse[i] === 1 ? 0.4 : 0.28; const score = urbanBias + settlementScore[i] * 0.22 + roadInfluence[i] * 0.08 + railInfluence2[i] * 0.05 + villageInfluence[i] * 0.04 - slope[i] * 0.18 - ridgeField[i] * 0.06 + hash2(x, y, seed + 1300) * 0.025; if (score > 0.40) municipalityCandidates.push({ x, y, score }); } } const majorMunicipalSeeds = modernCities .filter((city) => (city.population || 0) >= 220000 && prefectureMask[indexOf(city.x, city.y)]) .map((city) => ({ x: city.x, y: city.y, score: 1.55 + (city.population || 0) / 700000, protectedCity: city })); const filteredMunicipalityCandidates = municipalityCandidates.filter((p) => { const nearMajor = majorMunicipalSeeds.some((city) => Math.hypot(city.x - p.x, city.y - p.y) < clamp(12 + Math.sqrt(city.protectedCity.population || 300000) / 130, 14, 28)); const nearSmallUrban = modernCities.some((city) => (city.population || 0) < 260000 && Math.hypot(city.x - p.x, city.y - p.y) < 8 && p.x !== city.x && p.y !== city.y); return !nearMajor && !nearSmallUrban; }); 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, modernCities, markets, ports, satelliteCities, villages }); const satelliteMunicipalSeeds = (satelliteCities || []) .filter((city) => prefectureMask[indexOf(city.x, city.y)] && city.municipalityClass === "independentSatelliteMunicipality") .map((city) => ({ x: city.x, y: city.y, score: 1.05 + (city.population || 40000) / 260000, protectedSatellite: city })); let adminCentersRaw = [ ...majorMunicipalSeeds, ...satelliteMunicipalSeeds, ...pickEntities(filteredMunicipalityCandidates.filter((p) => satelliteMunicipalSeeds.every((s) => Math.hypot(s.x - p.x, s.y - p.y) >= 6)), { max: Math.max(0, targetMunicipalityCount - majorMunicipalSeeds.length - satelliteMunicipalSeeds.length), minDistance: 6 + Math.floor(rand(seed, 1302) * 3), threshold: 0.34, seed: seed + 1300, jitter: 0.025, }), ]; if (adminCentersRaw.length < Math.min(targetMunicipalityCount, 18)) { const fallback = [...modernCities, ...(satelliteCities || []).filter((p) => p.municipalityClass === "independentSatelliteMunicipality"), ...markets, ...ports, ...newTowns, ...stations, ...villages] .filter((p) => prefectureMask[indexOf(p.x, p.y)]) .map((p) => ({ x: p.x, y: p.y, score: (p.score || 0.5) + (p.population || 0) / 900000 })); const extraFallback = pickEntities(fallback, { max: targetMunicipalityCount, minDistance: 5, threshold: 0, seed: seed + 1303 }); for (const p of extraFallback) if (adminCentersRaw.length < targetMunicipalityCount && adminCentersRaw.every((q) => Math.hypot(p.x - q.x, p.y - q.y) >= 4)) adminCentersRaw.push(p); } if (adminCentersRaw.length < targetMunicipalityCount) { const extra = pickEntities(municipalityCandidates, { max: targetMunicipalityCount - adminCentersRaw.length, minDistance: 5, threshold: 0.26, seed: seed + 1304 }); adminCentersRaw.push(...extra.filter((p) => adminCentersRaw.every((q) => Math.hypot(p.x - q.x, p.y - q.y) >= 4))); } if (adminCentersRaw.length > targetMunicipalityCount) adminCentersRaw = adminCentersRaw.slice(0, targetMunicipalityCount); const compartmentAssignment = assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw); const adminId = compartmentAssignment.adminId; 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: "habitable cells, settlement weight, coastline complexity, basin/lowland bonus, and mountain-ratio adjustment", changedAfterCompartmentAssignment: compartmentAssignment.debug?.naturalCompartmentCount || 0, oversizedRuralSplits: 0, oversizedLowlandSplits: 0, ruralSplitsAccepted: 0, ruralSplitsRejected: 0, satelliteMunicipalitiesCreated: satelliteMunicipalSeeds.length, satelliteMunicipalitiesMerged: 0, satelliteMunicipalitiesExpanded: 0, satelliteMunicipalitiesTooSmall: 0, averageSatelliteMunicipalityArea: 0, minSatelliteMunicipalityArea: 0, satelliteMunicipalityAreaByNameOrIndex: {}, independentSatelliteMunicipalities: satelliteClassificationDebug.independent, attachedSatelliteDistricts: satelliteClassificationDebug.attached, satelliteMunicipalityStats: satelliteClassificationDebug, ...compartmentAssignment.debug, }; function markChanged(field) { adminDebug[field] = changedCellsSince(previousSnapshot, adminId, prefectureMask, sea); previousSnapshot = new Int16Array(adminId); } 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) => { 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) => { 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"); mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities, 120, { satelliteCities, satelliteStats: adminDebug, satelliteMinArea: 100, protectedPoints: adminCentersRaw }); markChanged("changedAfterInitialMerge"); removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, modernCities, 180); markChanged("changedAfterInitialExclaveRemoval"); applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw); 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, }); } markChanged("changedAfterLandscapePartition"); const oversizedSplitDebug = splitOversizedLowlandMunicipalities(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw, [...(satelliteCities || []), ...newTowns, ...markets, ...villages]); 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; previousSnapshot = new Int16Array(adminId); 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, 360); markChanged("changedAfterFinalExclaveRemoval"); mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities, 80, { satelliteCities, satelliteStats: adminDebug, satelliteMinArea: 90, protectedPoints: adminCentersRaw }); 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, }); } removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, modernCities, 260); 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 < 80 || ((sat.population || 0) >= 60000 && area < 120))) { 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.naturalCompartmentCount = adminDebug.naturalCompartmentCount || adminDebug.compartmentCount || 0; adminDebug.actualMunicipalityCount = new Set([...adminId].filter((id, i) => id >= 0 && prefectureMask[i] && !sea[i])).size; adminDebug.borderNaturalBarrierAverage = adminDebug.finalBorderNaturalBarrierAverage ?? 0; adminDebug.voronoiLikeRate = adminDebug.voronoiLikeRateAfter ?? 0; const adminBorders = extractAdminBorderSegments(adminId, prefectureMask); return { adminCentersRaw, adminId, adminBorders, adminDebug }; }