tweak
This commit is contained in:
parent
f420a3f8f3
commit
b707dadec9
12 changed files with 4324 additions and 3065 deletions
398
adminRegions.js
398
adminRegions.js
|
|
@ -156,12 +156,31 @@ export function lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask,
|
|||
}
|
||||
}
|
||||
|
||||
export function mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities = [], minArea = 320) {
|
||||
export function mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities = [], minArea = 320, options = {}) {
|
||||
const area = new Map();
|
||||
const pop = new Map();
|
||||
const adjacency = new Map();
|
||||
const cityMunicipalities = new Set();
|
||||
for (const city of modernCities || []) if (inside(city.x, city.y)) cityMunicipalities.add(adminId[indexOf(city.x, city.y)]);
|
||||
for (const city of modernCities || []) {
|
||||
if (!inside(city.x, city.y)) continue;
|
||||
const id = adminId[indexOf(city.x, city.y)];
|
||||
if (id < 0) continue;
|
||||
if (options.protectAllModernCities !== false || city.isPrefecturalCapital || (city.population || 0) >= (options.majorCityPopulationThreshold || 120000)) cityMunicipalities.add(id);
|
||||
}
|
||||
for (const point of options.protectedPoints || []) {
|
||||
if (!point || !inside(point.x, point.y)) continue;
|
||||
const id = adminId[indexOf(point.x, point.y)];
|
||||
if (id >= 0) cityMunicipalities.add(id);
|
||||
}
|
||||
const satelliteByAdmin = new Map();
|
||||
for (const sat of options.satelliteCities || []) {
|
||||
if (!sat || !inside(sat.x, sat.y)) continue;
|
||||
const id = adminId[indexOf(sat.x, sat.y)];
|
||||
if (id < 0) continue;
|
||||
if (!satelliteByAdmin.has(id)) satelliteByAdmin.set(id, []);
|
||||
satelliteByAdmin.get(id).push(sat);
|
||||
}
|
||||
const satelliteStats = options.satelliteStats || null;
|
||||
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
|
|
@ -187,16 +206,35 @@ export function mergeTinyMunicipalities(adminId, prefectureMask, sea, population
|
|||
for (const [id, cells] of area) {
|
||||
const score = cells + (pop.get(id) || 0) * 16;
|
||||
if (cells >= minArea || cityMunicipalities.has(id)) continue;
|
||||
const satellites = satelliteByAdmin.get(id) || [];
|
||||
const protectedSatellite = satellites.some((sat) => {
|
||||
const minSatelliteArea = sat.satelliteMinArea || options.satelliteMinArea || 110;
|
||||
return sat.municipalityClass === "independentSatelliteMunicipality" && (
|
||||
cells >= minSatelliteArea ||
|
||||
(sat.population || 0) >= (options.satelliteIndependentPopulationThreshold || 60000) ||
|
||||
(sat.distinctUrbanComponentArea || 0) >= 80 ||
|
||||
sat.separatedByBarrier
|
||||
);
|
||||
});
|
||||
if (protectedSatellite) continue;
|
||||
let bestNeighbor = -1;
|
||||
let bestScore = -1;
|
||||
for (const [key, border] of adjacency) {
|
||||
const [a, b] = key.split(":").map(Number);
|
||||
if (a !== id && b !== id) continue;
|
||||
const other = a === id ? b : a;
|
||||
const candidate = border * 3 + (area.get(other) || 0) * 0.012 + (pop.get(other) || 0) * 0.24;
|
||||
const parentBias = satellites.some((sat) => inside(sat.parentX ?? -1, sat.parentY ?? -1) && adminId[indexOf(sat.parentX, sat.parentY)] === other) ? 26 : 0;
|
||||
const ruralBias = satellites.some((sat) => sat.municipalityClass === "smallTownAttachedToRuralMunicipality") ? Math.min(12, (area.get(other) || 0) * 0.01) : 0;
|
||||
const candidate = border * 3 + (area.get(other) || 0) * 0.012 + (pop.get(other) || 0) * 0.24 + parentBias + ruralBias;
|
||||
if (candidate > bestScore) { bestScore = candidate; bestNeighbor = other; }
|
||||
}
|
||||
if (bestNeighbor >= 0 && (area.get(bestNeighbor) || 0) >= score * 0.35) mergeTarget.set(id, bestNeighbor);
|
||||
if (bestNeighbor >= 0 && (area.get(bestNeighbor) || 0) >= score * 0.35) {
|
||||
mergeTarget.set(id, bestNeighbor);
|
||||
if (satelliteStats && satellites.length) {
|
||||
satelliteStats.satelliteMunicipalitiesMerged += satellites.length;
|
||||
for (const sat of satellites) sat.mergedMunicipalityTarget = bestNeighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mergeTarget.size === 0) return;
|
||||
for (let i = 0; i < SIZE; i++) if (mergeTarget.has(adminId[i])) adminId[i] = mergeTarget.get(adminId[i]);
|
||||
|
|
@ -465,6 +503,119 @@ function classifyLandscapeCell(i, elevation, slope, river, ridgeField, valleyFie
|
|||
return 11;
|
||||
}
|
||||
|
||||
export function buildNaturalBarrierScore(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore = null, plain = null, agriculture = null, populationDensity = null, landuse = null) {
|
||||
const score = new Float32Array(SIZE);
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!prefectureMask[i] || sea[i]) continue;
|
||||
const urbanContinuity = populationDensity && landuse ? urbanBoundaryPenalty(i, populationDensity, landuse) : 0;
|
||||
const majorRiver = clamp(Math.max(river[i] - 0.34, 0) * 1.95 + Math.max(flowAccum[i] - 0.42, 0) * 0.82);
|
||||
const ridgeDivide = clamp(ridgeField[i] * 1.65 + Math.max(0, elevation[i] - 0.52) * ridgeField[i] * 1.05);
|
||||
const crest = crestCrossingScore ? crestCrossingScore[i] : clamp(Math.max(0, elevation[i] - 0.55) * ridgeField[i] * 1.4 + slope[i] * ridgeField[i] * 0.8);
|
||||
const basinRim = basinField ? clamp(Math.max(0, basinField[i] - 0.32) * Math.max(0, slope[i] - 0.18) * 1.15 + Math.max(0, ridgeField[i] - 0.34) * basinField[i] * 0.62) : 0;
|
||||
const foothillBreak = clamp(Math.max(0, slope[i] - 0.30) * Math.max(ridgeField[i], Math.max(0, elevation[i] - 0.48)) * 0.82);
|
||||
const livingCorridor = clamp((plain?.[i] || 0) * 0.34 + (agriculture?.[i] || 0) * 0.26 + valleyField[i] * (majorRiver > 0.34 ? 0.10 : 0.46) + coastalLowland[i] * 0.18);
|
||||
score[i] = clamp(
|
||||
ridgeDivide * 0.92 +
|
||||
crest * 0.72 +
|
||||
majorRiver * 0.86 +
|
||||
basinRim * 0.54 +
|
||||
foothillBreak * 0.48 +
|
||||
terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity || score, landuse || score) * 0.38 -
|
||||
livingCorridor * 0.50 -
|
||||
urbanContinuity * 0.72
|
||||
);
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
function canShareNaturalCompartment(a, b, classA, classB, barrier, river, flowAccum, valleyField, populationDensity, landuse) {
|
||||
if (classA !== classB) {
|
||||
const bothUrban = classA <= 3 && classB <= 3;
|
||||
const bothLivingCorridor = [5, 6, 7, 10].includes(classA) && [5, 6, 7, 10].includes(classB);
|
||||
if (!bothUrban && !bothLivingCorridor) return false;
|
||||
}
|
||||
const majorRiverEdge = Math.max(river[a], river[b]) > 0.58 || Math.max(flowAccum[a], flowAccum[b]) > 0.72;
|
||||
const urbanEdge = ((landuse[a] >= 2 && landuse[a] <= 4) || landuse[a] === 7 || populationDensity[a] > 0.34) &&
|
||||
((landuse[b] >= 2 && landuse[b] <= 4) || landuse[b] === 7 || populationDensity[b] > 0.34);
|
||||
const valleyContinuity = (valleyField[a] + valleyField[b]) * 0.5 > 0.42 && !majorRiverEdge;
|
||||
const threshold = urbanEdge ? 0.84 : valleyContinuity ? 0.76 : classA === 8 || classB === 8 ? 0.42 : 0.62;
|
||||
return barrier < threshold && (!majorRiverEdge || urbanEdge);
|
||||
}
|
||||
|
||||
function naturalGroupKey(unit) {
|
||||
if (unit.classId <= 3) return `urban:${Math.round(unit.x / 10)}:${Math.round(unit.y / 10)}`;
|
||||
if (unit.classId === 5) return `coast:${Math.round(unit.y / 8)}`;
|
||||
if (unit.classId === 6) return `basin:${Math.round(unit.x / 12)}:${Math.round(unit.y / 12)}`;
|
||||
if (unit.classId === 7) return `valley:${Math.round((unit.x + unit.y) / 12)}`;
|
||||
if (unit.classId === 8 || unit.classId === 9) return `mountain:${Math.round(unit.x / 14)}:${Math.round(unit.y / 14)}`;
|
||||
return `plain:${Math.round(unit.x / 14)}:${Math.round(unit.y / 14)}`;
|
||||
}
|
||||
|
||||
export function buildNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore = null, plain = null, agriculture = null, populationDensity = null, landuse = null) {
|
||||
const naturalBarrierScore = buildNaturalBarrierScore(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse);
|
||||
const compartmentId = new Int32Array(SIZE);
|
||||
compartmentId.fill(-1);
|
||||
const cellClass = new Int16Array(SIZE);
|
||||
cellClass.fill(-1);
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) cellClass[i] = classifyLandscapeCell(i, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse);
|
||||
|
||||
const compartments = [];
|
||||
const queue = [];
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (cellClass[i] < 0 || compartmentId[i] >= 0) continue;
|
||||
const id = compartments.length;
|
||||
const startClass = cellClass[i];
|
||||
const cells = [];
|
||||
let sx = 0, sy = 0, pop = 0, urbanWeight = 0, ridgeExposure = 0, riverExposure = 0, coastalExposure = 0, basinIdentity = 0, valleyIdentity = 0;
|
||||
queue.length = 0;
|
||||
queue.push(i);
|
||||
compartmentId[i] = id;
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
const [x, y] = xyOf(cur);
|
||||
cells.push(cur);
|
||||
sx += x; sy += y; pop += populationDensity[cur];
|
||||
urbanWeight += urbanBoundaryPenalty(cur, populationDensity, landuse);
|
||||
ridgeExposure += ridgeField[cur];
|
||||
riverExposure += river[cur] + flowAccum[cur] * 0.45;
|
||||
coastalExposure += coastalLowland[cur];
|
||||
basinIdentity += basinField[cur];
|
||||
valleyIdentity += valleyField[cur];
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (compartmentId[ni] >= 0 || cellClass[ni] < 0) continue;
|
||||
const edgeBarrier = (naturalBarrierScore[cur] + naturalBarrierScore[ni]) * 0.5;
|
||||
if (!canShareNaturalCompartment(cur, ni, startClass, cellClass[ni], edgeBarrier, river, flowAccum, valleyField, populationDensity, landuse)) continue;
|
||||
compartmentId[ni] = id;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
const area = cells.length;
|
||||
compartments.push({
|
||||
id,
|
||||
cells,
|
||||
area,
|
||||
x: sx / area,
|
||||
y: sy / area,
|
||||
classId: startClass,
|
||||
dominantLandscapeClass: startClass,
|
||||
population: pop,
|
||||
urbanWeight: urbanWeight / area,
|
||||
ridgeExposure: ridgeExposure / area,
|
||||
riverExposure: riverExposure / area,
|
||||
coastalExposure: coastalExposure / area,
|
||||
basinIdentity: basinIdentity / area,
|
||||
valleyIdentity: valleyIdentity / area,
|
||||
centerIds: [],
|
||||
adjacent: new Map(),
|
||||
});
|
||||
}
|
||||
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
mergeTinyLandscapeUnits(compartmentId, compartments, 12);
|
||||
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
return { compartmentId, compartments, naturalBarrierScore };
|
||||
}
|
||||
|
||||
function buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse) {
|
||||
const unitId = new Int32Array(SIZE);
|
||||
unitId.fill(-1);
|
||||
|
|
@ -559,52 +710,124 @@ function mergeTinyLandscapeUnits(unitId, units, minArea = 10) {
|
|||
}
|
||||
}
|
||||
|
||||
function landscapeTransitionCost(a, b, edge) {
|
||||
function naturalOwnershipAffinity(unit, neighbor, edge) {
|
||||
const boundaryTarget = edge.target / Math.max(1, edge.count);
|
||||
const bothUrban = a.classId <= 3 && b.classId <= 3;
|
||||
const bothCorridor = (a.classId === 5 || a.classId === 7 || a.classId === 10) && (b.classId === 5 || b.classId === 7 || b.classId === 10);
|
||||
const urbanContinuity = bothUrban ? 2.1 : (a.urbanWeight + b.urbanWeight) > 0.75 && Math.abs(a.urbanWeight - b.urbanWeight) < 0.35 ? 0.9 : 0;
|
||||
return Math.max(0.18, 0.70 + boundaryTarget * 4.2 + (a.classId === b.classId ? 0 : 0.75) + ((a.classId === 8 || b.classId === 8) ? 1.2 : 0) - urbanContinuity - (bothCorridor ? 0.55 : 0));
|
||||
const sameClass = unit.classId === neighbor.classId ? 1.0 : 0;
|
||||
const sameGroup = naturalGroupKey(unit) === naturalGroupKey(neighbor) ? 1.1 : 0;
|
||||
const bothUrban = unit.classId <= 3 && neighbor.classId <= 3;
|
||||
const bothCorridor = [5, 6, 7, 10].includes(unit.classId) && [5, 6, 7, 10].includes(neighbor.classId);
|
||||
const urbanContinuity = bothUrban ? 1.35 : (unit.urbanWeight + neighbor.urbanWeight) > 0.75 && Math.abs(unit.urbanWeight - neighbor.urbanWeight) < 0.35 ? 0.58 : 0;
|
||||
const strongDividerPenalty = boundaryTarget * (edge.count > 2 ? 2.8 : 1.8);
|
||||
return edge.count * 0.55 + sameClass + sameGroup + urbanContinuity + (bothCorridor ? 0.72 : 0) - strongDividerPenalty;
|
||||
}
|
||||
|
||||
export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCenters = []) {
|
||||
const { unitId, units, targetScore } = buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse);
|
||||
if (units.length === 0) return;
|
||||
for (let id = 0; id < adminCenters.length; id++) {
|
||||
const center = adminCenters[id];
|
||||
if (!center || !inside(center.x, center.y)) continue;
|
||||
const unit = units[unitId[indexOf(center.x, center.y)]];
|
||||
if (unit) unit.centerIds.push(id);
|
||||
}
|
||||
|
||||
const owner = new Int16Array(units.length);
|
||||
const dist = new Float32Array(units.length);
|
||||
owner.fill(-1); dist.fill(INF);
|
||||
const heap = new MinHeap();
|
||||
for (const unit of units) {
|
||||
if (unit.area === 0 || unit.centerIds.length === 0) continue;
|
||||
const id = unit.centerIds[0];
|
||||
owner[unit.id] = id; dist[unit.id] = 0; heap.push({ i: unit.id, f: 0 });
|
||||
}
|
||||
while (heap.length) {
|
||||
const cur = heap.pop();
|
||||
if (!cur || cur.f > dist[cur.i] + 1e-5) continue;
|
||||
const unit = units[cur.i];
|
||||
const currentOwner = owner[cur.i];
|
||||
if (!unit || currentOwner < 0) continue;
|
||||
for (const [nextId, edge] of unit.adjacent) {
|
||||
const next = units[nextId];
|
||||
if (!next || next.area === 0) continue;
|
||||
const nextDist = dist[cur.i] + landscapeTransitionCost(unit, next, edge) + Math.sqrt(next.area) * 0.012 + (next.urbanWeight > 0.75 && next.centerIds.length === 0 ? -0.20 : 0);
|
||||
if (nextDist < dist[nextId]) {
|
||||
dist[nextId] = nextDist; owner[nextId] = currentOwner; heap.push({ i: nextId, f: nextDist });
|
||||
function averageFinalBorderBarrier(adminId, prefectureMask, sea, naturalBarrierScore) {
|
||||
let sum = 0;
|
||||
let count = 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] || adminId[i] < 0) continue;
|
||||
for (const [nx, ny] of [[x + 1, y], [x, y + 1]]) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni] || adminId[ni] < 0 || adminId[ni] === adminId[i]) continue;
|
||||
sum += (naturalBarrierScore[i] + naturalBarrierScore[ni]) * 0.5;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const unit of units) {
|
||||
return count ? sum / count : 0;
|
||||
}
|
||||
|
||||
function weakVoronoiLikeRate(adminId, adminCenters, prefectureMask, sea, naturalBarrierScore) {
|
||||
let weak = 0;
|
||||
let total = 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] || adminId[i] < 0) continue;
|
||||
for (const [nx, ny] of [[x + 1, y], [x, y + 1]]) {
|
||||
const ni = indexOf(nx, ny);
|
||||
const a = adminId[i], b = adminId[ni];
|
||||
if (!prefectureMask[ni] || sea[ni] || a < 0 || b < 0 || a === b) continue;
|
||||
total++;
|
||||
const ca = adminCenters[a], cb = adminCenters[b];
|
||||
if (!ca || !cb) continue;
|
||||
const mx = (x + nx) * 0.5, my = (y + ny) * 0.5;
|
||||
const nearBisector = Math.abs(Math.hypot(mx - ca.x, my - ca.y) - Math.hypot(mx - cb.x, my - cb.y)) < 4.0;
|
||||
if (nearBisector && (naturalBarrierScore[i] + naturalBarrierScore[ni]) * 0.5 < 0.38) weak++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return total ? weak / total : 0;
|
||||
}
|
||||
|
||||
export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCenters = []) {
|
||||
const before = new Int16Array(adminId);
|
||||
const initialNaturalBarrierScore = buildNaturalBarrierScore(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, null, plain, agriculture, populationDensity, landuse);
|
||||
const beforeVoronoiLikeRate = weakVoronoiLikeRate(adminId, adminCenters, prefectureMask, sea, initialNaturalBarrierScore);
|
||||
const { compartmentId, compartments, naturalBarrierScore } = buildNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, null, plain, agriculture, populationDensity, landuse);
|
||||
if (compartments.length === 0) return;
|
||||
for (let id = 0; id < adminCenters.length; id++) {
|
||||
const center = adminCenters[id];
|
||||
if (!center || !inside(center.x, center.y)) continue;
|
||||
const unit = compartments[compartmentId[indexOf(center.x, center.y)]];
|
||||
if (unit) unit.centerIds.push(id);
|
||||
}
|
||||
|
||||
const owner = new Int16Array(compartments.length);
|
||||
owner.fill(-1);
|
||||
for (const unit of compartments) {
|
||||
if (unit.area === 0 || unit.centerIds.length === 0) continue;
|
||||
owner[unit.id] = unit.centerIds[0];
|
||||
}
|
||||
|
||||
for (let pass = 0; pass < compartments.length + 4; pass++) {
|
||||
let changed = 0;
|
||||
for (const unit of compartments) {
|
||||
if (!unit || unit.area === 0 || owner[unit.id] >= 0) continue;
|
||||
let bestOwner = -1;
|
||||
let bestScore = -INF;
|
||||
for (const [neighborId, edge] of unit.adjacent) {
|
||||
const neighborOwner = owner[neighborId];
|
||||
if (neighborOwner < 0) continue;
|
||||
const neighbor = compartments[neighborId];
|
||||
if (!neighbor || neighbor.area === 0) continue;
|
||||
const score = naturalOwnershipAffinity(unit, neighbor, edge) + Math.min(0.8, Math.sqrt(Math.max(1, neighbor.area)) * 0.018);
|
||||
if (score > bestScore) { bestScore = score; bestOwner = neighborOwner; }
|
||||
}
|
||||
const accept = unit.classId <= 3 ? bestScore > -0.15 : unit.classId === 8 || unit.classId === 9 ? bestScore > -0.80 : bestScore > -0.45;
|
||||
if (bestOwner >= 0 && accept) { owner[unit.id] = bestOwner; changed++; }
|
||||
}
|
||||
if (changed === 0) break;
|
||||
}
|
||||
|
||||
for (const unit of compartments) {
|
||||
if (!unit || unit.area === 0 || owner[unit.id] >= 0) continue;
|
||||
let bestId = -1, bestScore = -INF;
|
||||
for (let id = 0; id < adminCenters.length; id++) {
|
||||
const center = adminCenters[id];
|
||||
if (!center || !inside(center.x, center.y)) continue;
|
||||
const centerComp = compartments[compartmentId[indexOf(center.x, center.y)]];
|
||||
const sameGroup = centerComp && naturalGroupKey(centerComp) === naturalGroupKey(unit) ? 2.4 : 0;
|
||||
const sameClass = centerComp && centerComp.classId === unit.classId ? 0.9 : 0;
|
||||
const urbanFit = unit.urbanWeight > 0.55 && centerComp?.urbanWeight > 0.55 ? 1.2 : 0;
|
||||
const d = Math.hypot(unit.x - center.x, unit.y - center.y);
|
||||
const score = sameGroup + sameClass + urbanFit - d * 0.018 - unit.ridgeExposure * 0.18;
|
||||
if (score > bestScore) { bestScore = score; bestId = id; }
|
||||
}
|
||||
owner[unit.id] = bestId >= 0 ? bestId : 0;
|
||||
}
|
||||
|
||||
for (const unit of compartments) {
|
||||
const assigned = owner[unit.id];
|
||||
if (assigned >= 0) for (const i of unit.cells) adminId[i] = assigned;
|
||||
}
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!prefectureMask[i] || sea[i] || adminId[i] >= 0) continue;
|
||||
const comp = compartments[compartmentId[i]];
|
||||
adminId[i] = comp && owner[comp.id] >= 0 ? owner[comp.id] : 0;
|
||||
}
|
||||
for (let id = 0; id < adminCenters.length; id++) {
|
||||
const center = adminCenters[id];
|
||||
if (!center || !inside(center.x, center.y)) continue;
|
||||
|
|
@ -616,7 +839,18 @@ export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, e
|
|||
if (prefectureMask[i] && !sea[i]) adminId[i] = id;
|
||||
}
|
||||
}
|
||||
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, targetScore, populationDensity, landuse);
|
||||
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, naturalBarrierScore, populationDensity, landuse);
|
||||
let changedCells = 0;
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && before[i] !== adminId[i]) changedCells++;
|
||||
const activeCompartments = compartments.filter((unit) => unit.area > 0);
|
||||
applyLandscapeUnitAdminPartition.lastDebug = {
|
||||
compartmentCount: activeCompartments.length,
|
||||
averageCompartmentArea: activeCompartments.length ? activeCompartments.reduce((sum, unit) => sum + unit.area, 0) / activeCompartments.length : 0,
|
||||
changedAfterNaturalCompartmentPartition: changedCells,
|
||||
finalBorderNaturalBarrierAverage: averageFinalBorderBarrier(adminId, prefectureMask, sea, naturalBarrierScore),
|
||||
voronoiLikeRateBefore: beforeVoronoiLikeRate,
|
||||
voronoiLikeRateAfter: weakVoronoiLikeRate(adminId, adminCenters, prefectureMask, sea, naturalBarrierScore),
|
||||
};
|
||||
}
|
||||
|
||||
export function snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse, adminCenters = [], protectedPoints = [], passes = 6) {
|
||||
|
|
@ -659,3 +893,83 @@ export function snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, eleva
|
|||
adminId.set(current);
|
||||
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, targetScore, populationDensity, landuse);
|
||||
}
|
||||
|
||||
export function splitOversizedRuralMunicipalities(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCenters = [], settlements = []) {
|
||||
const before = new Int16Array(adminId);
|
||||
const area = new Map();
|
||||
const lowland = new Map();
|
||||
const mountain = new Map();
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!prefectureMask[i] || sea[i] || adminId[i] < 0) continue;
|
||||
const id = adminId[i];
|
||||
area.set(id, (area.get(id) || 0) + 1);
|
||||
const living = (plain[i] || 0) * 0.42 + (agriculture[i] || 0) * 0.28 + basinField[i] * 0.20 + coastalLowland[i] * 0.20 + valleyField[i] * 0.12;
|
||||
const rough = ridgeField[i] * 0.54 + slope[i] * 0.36 + Math.max(0, elevation[i] - 0.58) * 0.38;
|
||||
lowland.set(id, (lowland.get(id) || 0) + living);
|
||||
mountain.set(id, (mountain.get(id) || 0) + rough);
|
||||
}
|
||||
const areas = [...area.values()].sort((a, b) => a - b);
|
||||
const median = areas.length ? areas[Math.floor(areas.length / 2)] : 0;
|
||||
if (!median) return { changedCells: 0, splitMunicipalities: 0 };
|
||||
|
||||
const { compartmentId, compartments, naturalBarrierScore } = buildNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, null, plain, agriculture, populationDensity, landuse);
|
||||
const unitOwner = new Int16Array(compartments.length);
|
||||
unitOwner.fill(-1);
|
||||
for (const unit of compartments) {
|
||||
if (!unit || unit.area === 0) continue;
|
||||
const counts = new Map();
|
||||
for (const i of unit.cells) {
|
||||
const id = adminId[i];
|
||||
if (id >= 0) counts.set(id, (counts.get(id) || 0) + 1);
|
||||
}
|
||||
let bestId = -1, best = -1;
|
||||
for (const [id, count] of counts) if (count > best) { best = count; bestId = id; }
|
||||
unitOwner[unit.id] = bestId;
|
||||
}
|
||||
|
||||
const adminCenterIndex = new Map();
|
||||
for (let id = 0; id < adminCenters.length; id++) {
|
||||
const c = adminCenters[id];
|
||||
if (c && inside(c.x, c.y)) adminCenterIndex.set(id, indexOf(c.x, c.y));
|
||||
}
|
||||
|
||||
let splitMunicipalities = 0;
|
||||
for (const [id, cells] of area) {
|
||||
const averageLowland = (lowland.get(id) || 0) / cells;
|
||||
const averageMountain = (mountain.get(id) || 0) / cells;
|
||||
if (cells < median * 2.25 || averageLowland < 0.28 || averageMountain > 0.44) continue;
|
||||
const localSettlements = settlements.filter((p) => p && inside(p.x, p.y) && adminId[indexOf(p.x, p.y)] === id);
|
||||
const meaningfulNodes = localSettlements.filter((p) => p.kind === "Satellite City" || p.kind === "New Town" || p.kind === "Market Town" || (p.population || 0) >= 30000);
|
||||
if (meaningfulNodes.length < 2) continue;
|
||||
let changedHere = 0;
|
||||
for (const unit of compartments) {
|
||||
if (!unit || unit.area === 0 || unitOwner[unit.id] !== id) continue;
|
||||
const centerIndex = adminCenterIndex.get(id);
|
||||
if (centerIndex >= 0 && unit.cells.includes(centerIndex)) continue;
|
||||
if (unit.classId === 8 || unit.classId === 9) continue;
|
||||
let bestNeighbor = -1;
|
||||
let bestScore = -INF;
|
||||
for (const [neighborId, edge] of unit.adjacent) {
|
||||
const neighborOwner = unitOwner[neighborId];
|
||||
if (neighborOwner < 0 || neighborOwner === id) continue;
|
||||
const boundaryTarget = edge.target / Math.max(1, edge.count);
|
||||
const neighbor = compartments[neighborId];
|
||||
const nodePull = meaningfulNodes.reduce((best, p) => Math.max(best, 1 / (1 + Math.hypot(p.x - unit.x, p.y - unit.y) / 6)), 0);
|
||||
const score = edge.count * 0.7 + boundaryTarget * 1.4 + nodePull * 1.2 - Math.max(0, (neighbor?.ridgeExposure || 0) - unit.ridgeExposure) * 0.35;
|
||||
if (score > bestScore) { bestScore = score; bestNeighbor = neighborOwner; }
|
||||
}
|
||||
if (bestNeighbor < 0 || bestScore < 2.2) continue;
|
||||
for (const ci of unit.cells) {
|
||||
if (adminId[ci] === id) {
|
||||
adminId[ci] = bestNeighbor;
|
||||
changedHere++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changedHere > Math.max(28, cells * 0.035)) splitMunicipalities++;
|
||||
}
|
||||
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, naturalBarrierScore, populationDensity, landuse);
|
||||
let changedCells = 0;
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && before[i] !== adminId[i]) changedCells++;
|
||||
return { changedCells, splitMunicipalities };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue