455 lines
24 KiB
JavaScript
455 lines
24 KiB
JavaScript
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, slope, ridgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages }) {
|
|
let landCells = 0;
|
|
let habitableCells = 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) habitableCells++;
|
|
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 settlementNodes = modernCities.length * 1.25 + markets.length * 0.9 + ports.length * 0.7 + independentSatellites * 0.8 + villages.length * 0.35;
|
|
const basinBonus = Math.min(8, [...basinField].filter((v, i) => prefectureMask[i] && !sea[i] && v > 0.34).length / 520);
|
|
const mountainRatio = landCells ? mountainCells / landCells : 0;
|
|
return clamp(Math.round(habitableCells / 260 + settlementNodes * 0.45 + coastlineComplexity * 0.04 + basinBonus + mountainRatio * 4), 18, 48);
|
|
}
|
|
|
|
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, 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,
|
|
changedAfterCompartmentAssignment: compartmentAssignment.debug?.naturalCompartmentCount || 0,
|
|
oversizedRuralSplits: 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;
|
|
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;
|
|
const adminBorders = extractAdminBorderSegments(adminId, prefectureMask);
|
|
|
|
|
|
return { adminCentersRaw, adminId, adminBorders, adminDebug };
|
|
}
|