chinko
This commit is contained in:
parent
0359bb2445
commit
b17be0e0d2
21 changed files with 8034 additions and 2737 deletions
|
|
@ -306,11 +306,11 @@ export function terrainBoundaryTargetScore(i, elevation, slope, river, ridgeFiel
|
|||
const urbanPenalty = urbanBoundaryPenalty(i, populationDensity, landuse);
|
||||
const majorRiver = clamp(Math.max(river[i] - 0.32, 0) * 1.9 + Math.max(flowAccum[i] - 0.38, 0) * 0.75);
|
||||
const minorStream = clamp(river[i] * 0.34 + flowAccum[i] * 0.18);
|
||||
const ridgeDivide = clamp(ridgeField[i] * 1.55 + Math.max(0, elevation[i] - 0.54) * ridgeField[i] * 0.95);
|
||||
const slopeBreak = clamp(slope[i] * 0.58 + Math.max(0, slope[i] - 0.32) * 0.68);
|
||||
const highGround = Math.max(0, elevation[i] - 0.56) * 0.22;
|
||||
const valleyFloorPenalty = valleyField[i] * (majorRiver > 0.34 ? -0.10 : -0.62);
|
||||
return clamp(ridgeDivide + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72);
|
||||
const ridgeDivide = clamp(ridgeField[i] * 2.12 + Math.max(0, elevation[i] - 0.52) * ridgeField[i] * 1.32);
|
||||
const slopeBreak = clamp(slope[i] * 0.70 + Math.max(0, slope[i] - 0.30) * 0.88);
|
||||
const highGround = Math.max(0, elevation[i] - 0.54) * 0.34;
|
||||
const valleyFloorPenalty = valleyField[i] * (majorRiver > 0.34 ? -0.08 : -0.68);
|
||||
return clamp(ridgeDivide + majorRiver * 0.92 + minorStream * 0.20 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.92);
|
||||
}
|
||||
|
||||
function urbanBoundaryPenalty(i, populationDensity, landuse) {
|
||||
|
|
@ -518,20 +518,20 @@ export function buildNaturalBarrierScore(prefectureMask, sea, elevation, slope,
|
|||
for (const [nx, ny] of neighbors8(x, y)) if (sea[indexOf(nx, ny)]) coastEdge = 1;
|
||||
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 ridgeDivide = clamp(ridgeField[i] * 2.05 + Math.max(0, elevation[i] - 0.50) * ridgeField[i] * 1.28);
|
||||
const crest = crestCrossingScore ? crestCrossingScore[i] : clamp(Math.max(0, elevation[i] - 0.53) * ridgeField[i] * 1.72 + slope[i] * ridgeField[i] * 1.02);
|
||||
const basinRim = basinField ? clamp(Math.max(0, basinField[i] - 0.30) * Math.max(0, slope[i] - 0.16) * 1.32 + Math.max(0, ridgeField[i] - 0.32) * basinField[i] * 0.78) : 0;
|
||||
const foothillBreak = clamp(Math.max(0, slope[i] - 0.28) * Math.max(ridgeField[i], Math.max(0, elevation[i] - 0.46)) * 1.02);
|
||||
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 +
|
||||
ridgeDivide * 1.42 +
|
||||
crest * 1.10 +
|
||||
majorRiver * 0.86 +
|
||||
basinRim * 0.54 +
|
||||
foothillBreak * 0.48 +
|
||||
basinRim * 0.66 +
|
||||
foothillBreak * 0.58 +
|
||||
coastEdge * 0.34 +
|
||||
terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity || score, landuse || score) * 0.38 -
|
||||
livingCorridor * 0.50 -
|
||||
terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity || score, landuse || score) * 0.44 -
|
||||
livingCorridor * 0.28 -
|
||||
urbanContinuity * 0.72
|
||||
);
|
||||
}
|
||||
|
|
@ -568,7 +568,7 @@ function canShareNaturalCompartment(a, b, classA, classB, barrier, river, flowAc
|
|||
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.48 && !majorRiverEdge;
|
||||
const threshold = urbanEdge ? 0.78 : valleyContinuity ? 0.62 : classA === 8 || classB === 8 ? 0.36 : 0.50;
|
||||
const threshold = urbanEdge ? 0.74 : valleyContinuity ? 0.56 : classA === 8 || classB === 8 ? 0.28 : 0.43;
|
||||
return barrier < threshold && (!majorRiverEdge || urbanEdge);
|
||||
}
|
||||
|
||||
|
|
@ -665,9 +665,9 @@ function splitOneNaturalCompartment(unit, newId, compartmentId, fields, seed) {
|
|||
if (!cellSet.has(ni)) continue;
|
||||
const barrier = ((naturalBarrierScore?.[cur.i] || 0) + (naturalBarrierScore?.[ni] || 0)) * 0.5;
|
||||
const riverBarrier = Math.max(river?.[cur.i] || 0, river?.[ni] || 0) + Math.max(flowAccum?.[cur.i] || 0, flowAccum?.[ni] || 0) * 0.32;
|
||||
const ridgeStep = Math.max(ridgeField[cur.i], ridgeField[ni]) * 0.80 + Math.abs(elevation[cur.i] - elevation[ni]) * 1.25;
|
||||
const corridorBonus = Math.min(0.48, ((valleyField[cur.i] + valleyField[ni]) * 0.5 + (plain?.[ni] || 0) * 0.18 + (coastalLowland?.[ni] || 0) * 0.12));
|
||||
const stepCost = Math.max(0.18, 0.78 + barrier * 3.0 + riverBarrier * 1.10 + ridgeStep + slope[ni] * 0.38 - corridorBonus) * step;
|
||||
const ridgeStep = Math.max(ridgeField[cur.i], ridgeField[ni]) * 1.18 + Math.abs(elevation[cur.i] - elevation[ni]) * 1.48;
|
||||
const corridorBonus = Math.min(0.42, ((valleyField[cur.i] + valleyField[ni]) * 0.5 + (plain?.[ni] || 0) * 0.15 + (coastalLowland?.[ni] || 0) * 0.10));
|
||||
const stepCost = Math.max(0.18, 0.78 + barrier * 3.75 + riverBarrier * 1.10 + ridgeStep + slope[ni] * 0.48 - corridorBonus) * step;
|
||||
const nd = cur.f + stepCost;
|
||||
if (nd < dist[ni]) {
|
||||
dist[ni] = nd;
|
||||
|
|
@ -735,6 +735,44 @@ function collectLandComponents(prefectureMask, sea) {
|
|||
return components;
|
||||
}
|
||||
|
||||
|
||||
function collectNaturalGrowthComponents(prefectureMask, sea, watershedId = null) {
|
||||
if (!watershedId) return collectLandComponents(prefectureMask, sea);
|
||||
const seen = new Uint8Array(SIZE);
|
||||
const components = [];
|
||||
const queue = [];
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (seen[i] || !prefectureMask[i] || sea[i]) continue;
|
||||
const wid = watershedId[i];
|
||||
const cells = [];
|
||||
queue.length = 0;
|
||||
queue.push(i);
|
||||
seen[i] = 1;
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
cells.push(cur);
|
||||
const [x, y] = xyOf(cur);
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (seen[ni] || !prefectureMask[ni] || sea[ni]) continue;
|
||||
if (watershedId[ni] !== wid) continue;
|
||||
seen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
components.push(cells);
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
function isWatershedBoundary(a, b, fields) {
|
||||
const watershedId = fields?.watershedId;
|
||||
if (!watershedId) return false;
|
||||
const aw = watershedId[a];
|
||||
const bw = watershedId[b];
|
||||
return aw >= 0 && bw >= 0 && aw !== bw;
|
||||
}
|
||||
|
||||
function naturalSeedScore(i, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore, seed) {
|
||||
const klassUrban = (landuse[i] >= 2 && landuse[i] <= 4) || landuse[i] === 7 || landuse[i] === 8;
|
||||
const lowland = lowlandCompartmentFitness(i, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse);
|
||||
|
|
@ -762,8 +800,23 @@ function chooseNaturalCompartmentSeeds(landComponents, targetCount, fields, seed
|
|||
const { cells, componentIndex, area } = sortedComponents[componentOrder];
|
||||
if (area <= 0) continue;
|
||||
const proportional = Math.round((targetCount || Math.round(totalArea / 42)) * area / Math.max(1, totalArea));
|
||||
let localTarget = Math.max(1, proportional);
|
||||
let highland = 0;
|
||||
let rugged = 0;
|
||||
for (const ci of cells) {
|
||||
highland += clamp((elevation[ci] - 0.52) * 2.1 + ridgeField[ci] * 0.55 + slope[ci] * 0.45);
|
||||
rugged += clamp(ridgeField[ci] * 0.75 + slope[ci] * 0.55 + Math.max(0, elevation[ci] - 0.58) * 0.85);
|
||||
}
|
||||
highland /= Math.max(1, area);
|
||||
rugged /= Math.max(1, area);
|
||||
// Watersheds can be very large in mountain ranges. The watershed switch is
|
||||
// a hard stop, but a single watershed still needs several internal natural
|
||||
// units; otherwise an entire mountain massif becomes one compartment. Keep
|
||||
// the global target budget roughly intact by capping the terrain boost.
|
||||
const terrainBoost = highland > 0.48 ? 2.0 : rugged > 0.38 ? 1.55 : 1.0;
|
||||
let localTarget = Math.max(1, Math.round(Math.max(1, proportional) * terrainBoost));
|
||||
localTarget = Math.min(localTarget, Math.max(1, Math.floor(area / minCellsPerUnit)));
|
||||
const reservedForRest = Math.max(0, sortedComponents.length - componentOrder - 1);
|
||||
localTarget = Math.min(localTarget, Math.max(1, remainingTarget - reservedForRest));
|
||||
if (componentOrder === sortedComponents.length - 1) localTarget = Math.max(1, Math.min(localTarget, remainingTarget));
|
||||
remainingTarget -= localTarget;
|
||||
|
||||
|
|
@ -800,6 +853,13 @@ function chooseNaturalCompartmentSeeds(landComponents, targetCount, fields, seed
|
|||
return { seeds, seedComponentId };
|
||||
}
|
||||
|
||||
function isHardNaturalRidgeCrossing(a, b, cellClass, fields) {
|
||||
// Natural compartments now use drainage basins as the only hard barrier.
|
||||
// Ridges still increase naturalStepCost, but they must not freeze an entire
|
||||
// mountain block into one unsplittable component inside the same basin.
|
||||
return isWatershedBoundary(a, b, fields);
|
||||
}
|
||||
|
||||
function naturalStepCost(a, b, cellClass, fields) {
|
||||
const { elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore, flowAccum } = fields;
|
||||
const barrier = ((naturalBarrierScore?.[a] || 0) + (naturalBarrierScore?.[b] || 0)) * 0.5;
|
||||
|
|
@ -819,12 +879,13 @@ function naturalStepCost(a, b, cellClass, fields) {
|
|||
const valleyContinuity = Math.min(valleyField[a], valleyField[b]) * (majorRiverCrossing ? 0.10 : 0.45);
|
||||
const corridorBonus = Math.min(0.42, lowlandContinuity * 0.22 + valleyContinuity + (bothUrban ? 0.18 : 0));
|
||||
const riverPenalty = majorRiverCrossing && !bothUrban ? 1.85 + flowEdge * 1.45 : riverEdge > 0.22 ? 0.38 : 0;
|
||||
if (isHardNaturalRidgeCrossing(a, b, cellClass, fields)) return INF;
|
||||
return Math.max(0.16,
|
||||
0.72 +
|
||||
barrier * 5.1 +
|
||||
ridge * 0.82 +
|
||||
elevationBreak * 3.0 +
|
||||
slopeBreak * 0.56 +
|
||||
barrier * 8.8 +
|
||||
ridge * 2.35 +
|
||||
elevationBreak * 5.10 +
|
||||
slopeBreak * 1.08 +
|
||||
riverPenalty +
|
||||
classBreak -
|
||||
corridorBonus
|
||||
|
|
@ -894,6 +955,42 @@ function splitDisconnectedCompartments(compartmentId, compartments, prefectureMa
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function splitCompartmentsByWatershed(compartmentId, compartments, fields) {
|
||||
const watershedId = fields?.watershedId;
|
||||
if (!watershedId) return 0;
|
||||
let split = 0;
|
||||
for (const unit of [...compartments]) {
|
||||
if (!unit || unit.area === 0 || !unit.cells?.length) continue;
|
||||
const groups = new Map();
|
||||
for (const ci of unit.cells) {
|
||||
const wid = watershedId[ci];
|
||||
const key = wid >= 0 ? wid : -1;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(ci);
|
||||
}
|
||||
if (groups.size <= 1) continue;
|
||||
const sorted = [...groups.values()].sort((a, b) => b.length - a.length);
|
||||
unit.cells = sorted[0];
|
||||
unit.area = sorted[0].length;
|
||||
for (const extra of sorted.slice(1)) {
|
||||
const newId = compartments.length;
|
||||
for (const ci of extra) compartmentId[ci] = newId;
|
||||
compartments.push({
|
||||
id: newId,
|
||||
cells: extra,
|
||||
centerIds: [],
|
||||
adjacent: new Map(),
|
||||
area: extra.length,
|
||||
classId: unit.classId,
|
||||
dominantLandscapeClass: unit.dominantLandscapeClass,
|
||||
});
|
||||
split++;
|
||||
}
|
||||
}
|
||||
return split;
|
||||
}
|
||||
|
||||
function renumberCompartments(compartmentId, compartments, prefectureMask, sea) {
|
||||
const active = compartments.filter((unit) => unit && unit.area > 0 && unit.cells?.length);
|
||||
const idMap = new Map();
|
||||
|
|
@ -967,7 +1064,9 @@ function splitNaturalCompartmentCompact(unit, newId, compartmentId, fields, seed
|
|||
for (const [nx, ny, step] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!cellSet.has(ni)) continue;
|
||||
const nd = cur.f + naturalStepCost(cur.i, ni, cellClass, fields) * step;
|
||||
const stepCost = naturalStepCost(cur.i, ni, cellClass, fields);
|
||||
if (stepCost >= INF) continue;
|
||||
const nd = cur.f + stepCost * step;
|
||||
if (nd < dist[ni]) {
|
||||
dist[ni] = nd;
|
||||
owner[ni] = cur.owner;
|
||||
|
|
@ -986,14 +1085,43 @@ function splitNaturalCompartmentCompact(unit, newId, compartmentId, fields, seed
|
|||
return newUnit;
|
||||
}
|
||||
|
||||
function splitNaturalCompartmentByAxis(unit, newId, compartmentId, fields, seed) {
|
||||
if (!unit || unit.area < 20 || !unit.cells?.length) return null;
|
||||
const { elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse } = fields;
|
||||
const minPart = Math.max(7, Math.min(34, Math.floor(unit.area * 0.20)));
|
||||
const horizontal = (unit.width || 0) >= (unit.height || 0);
|
||||
const cx = unit.x || 0;
|
||||
const cy = unit.y || 0;
|
||||
const sorted = [...unit.cells].sort((a, b) => {
|
||||
const [ax, ay] = xyOf(a);
|
||||
const [bx, by] = xyOf(b);
|
||||
const av = (horizontal ? ax : ay) + hashSeededTie(ax, ay, seed) * 0.35 + Math.abs((horizontal ? ay - cy : ax - cx)) * 0.015;
|
||||
const bv = (horizontal ? bx : by) + hashSeededTie(bx, by, seed) * 0.35 + Math.abs((horizontal ? by - cy : bx - cx)) * 0.015;
|
||||
return av - bv;
|
||||
});
|
||||
const cut = Math.max(minPart, Math.min(sorted.length - minPart, Math.floor(sorted.length * 0.50)));
|
||||
if (cut <= 0 || sorted.length - cut < minPart) return null;
|
||||
const aCells = sorted.slice(0, cut);
|
||||
const bCells = sorted.slice(cut);
|
||||
unit.cells = aCells;
|
||||
unit.area = aCells.length;
|
||||
for (const ci of aCells) compartmentId[ci] = unit.id;
|
||||
for (const ci of bCells) compartmentId[ci] = newId;
|
||||
const newUnit = { id: newId, cells: bCells, area: bCells.length, centerIds: [], adjacent: new Map(), classId: unit.classId, dominantLandscapeClass: unit.dominantLandscapeClass };
|
||||
refreshCompartmentStats(unit, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse);
|
||||
refreshCompartmentStats(newUnit, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse);
|
||||
return newUnit;
|
||||
}
|
||||
|
||||
function buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore = null, plain = null, agriculture = null, populationDensity = null, landuse = null, options = {}) {
|
||||
const progress = typeof options.progress === "function" ? options.progress : null;
|
||||
const naturalBarrierScore = buildNaturalBarrierScore(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse);
|
||||
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 fields = { elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, naturalBarrierScore, cellClass };
|
||||
const landComponents = collectLandComponents(prefectureMask, sea);
|
||||
const watershedId = options.watershedId || null;
|
||||
const fields = { elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, naturalBarrierScore, cellClass, watershedId };
|
||||
const landComponents = collectNaturalGrowthComponents(prefectureMask, sea, watershedId);
|
||||
const landArea = landComponents.reduce((sum, cells) => sum + cells.length, 0);
|
||||
const requestedTarget = options.targetCompartmentCount || clamp(Math.round(landArea / 34), 40, 360);
|
||||
const targetCount = clamp(Math.round(requestedTarget), Math.min(1, landArea), Math.max(1, Math.floor(landArea / 8)));
|
||||
|
|
@ -1016,7 +1144,9 @@ function buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, r
|
|||
for (const [nx, ny, step] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const nd = cur.f + naturalStepCost(cur.i, ni, cellClass, fields) * step;
|
||||
const stepCost = naturalStepCost(cur.i, ni, cellClass, fields);
|
||||
if (stepCost >= INF) continue;
|
||||
const nd = cur.f + stepCost * step;
|
||||
if (nd < dist[ni]) {
|
||||
dist[ni] = nd;
|
||||
compartmentId[ni] = cur.id;
|
||||
|
|
@ -1033,6 +1163,7 @@ function buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, r
|
|||
mergeWeakArtificialLandscapeUnits(compartmentId, compartments, fields, prefectureMask, sea, Math.max(42, Math.round(landArea / Math.max(1, targetCount) * 2.15)), 5);
|
||||
progress?.(`natural post-split units: ${compartments.filter((unit) => unit && unit.area > 0).length}`);
|
||||
splitDisconnectedCompartments(compartmentId, compartments, prefectureMask, sea);
|
||||
splitCompartmentsByWatershed(compartmentId, compartments, fields);
|
||||
compartments = renumberCompartments(compartmentId, compartments, prefectureMask, sea);
|
||||
refreshAllCompartmentStats(compartments, fields);
|
||||
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
|
|
@ -1051,7 +1182,8 @@ function buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, r
|
|||
return sb - sa;
|
||||
})[0];
|
||||
if (!worst) break;
|
||||
const newUnit = splitNaturalCompartmentCompact(worst, compartments.length, compartmentId, fields, (options.seed || 0) + guard * 97);
|
||||
const newUnit = splitNaturalCompartmentCompact(worst, compartments.length, compartmentId, fields, (options.seed || 0) + guard * 97)
|
||||
|| splitNaturalCompartmentByAxis(worst, compartments.length, compartmentId, fields, (options.seed || 0) + guard * 131);
|
||||
if (!newUnit) {
|
||||
worst._splitRejected = (worst._splitRejected || 0) + 1;
|
||||
if (worst._splitRejected > 2) worst.elongation = Math.min(worst.elongation || 1, 3.1);
|
||||
|
|
@ -1070,6 +1202,7 @@ function buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, r
|
|||
|
||||
mergeWeakArtificialLandscapeUnits(compartmentId, compartments, fields, prefectureMask, sea, Math.max(48, Math.round(landArea / Math.max(1, targetCount) * 2.05)), 4);
|
||||
splitDisconnectedCompartments(compartmentId, compartments, prefectureMask, sea);
|
||||
splitCompartmentsByWatershed(compartmentId, compartments, fields);
|
||||
compartments = renumberCompartments(compartmentId, compartments, prefectureMask, sea);
|
||||
refreshAllCompartmentStats(compartments, fields);
|
||||
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
|
|
@ -1214,8 +1347,14 @@ function mergeWeakArtificialLandscapeUnits(unitId, units, fields, prefectureMask
|
|||
const lowlandContinuity = Math.min(unit.lowlandFitness || 0, other.lowlandFitness || 0);
|
||||
const mountainContinuity = Math.min(unit.mountainFitness || 0, other.mountainFitness || 0);
|
||||
const urbanGuard = Math.max(unit.urbanWeight || 0, other.urbanWeight || 0);
|
||||
const weakDivider = avgBarrier < (sameClass ? 0.46 : 0.36);
|
||||
if (!weakDivider) continue;
|
||||
const ridgeDivider = avgBarrier > 0.58 || Math.max(unit.ridgeExposure || 0, other.ridgeExposure || 0) > 0.62;
|
||||
const weakDivider = avgBarrier < (sameClass ? 0.40 : 0.30);
|
||||
const bothMountain = (unit.mountainFitness || 0) > 0.56 && (other.mountainFitness || 0) > 0.56;
|
||||
// Large highland units are visually important. Do not erase their
|
||||
// internal subdivision just because two neighbouring cells share a class
|
||||
// inside the same watershed.
|
||||
if (bothMountain && combinedArea > Math.max(32, maxMergedArea * 0.72)) continue;
|
||||
if (!weakDivider || ridgeDivider) continue;
|
||||
const score =
|
||||
(sameClass ? 1.7 : 0) +
|
||||
(sameGroup ? 1.2 : 0) +
|
||||
|
|
@ -1241,8 +1380,10 @@ function naturalOwnershipAffinity(unit, neighbor, edge) {
|
|||
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;
|
||||
const ridgeExposure = Math.max(unit.ridgeExposure || 0, neighbor.ridgeExposure || 0);
|
||||
const hardRidgePenalty = boundaryTarget > 0.62 || ridgeExposure > 0.62 ? 2.4 : 0;
|
||||
const strongDividerPenalty = boundaryTarget * (edge.count > 2 ? 5.2 : 3.8) + ridgeExposure * 1.35 + hardRidgePenalty;
|
||||
return edge.count * 0.50 + sameClass + sameGroup + urbanContinuity + (bothCorridor ? 0.58 : 0) - strongDividerPenalty;
|
||||
}
|
||||
|
||||
function compartmentCrossingCost(unit, neighbor, edge) {
|
||||
|
|
@ -1255,17 +1396,41 @@ function compartmentCrossingCost(unit, neighbor, edge) {
|
|||
const ridgePenalty = Math.max(unit.ridgeExposure || 0, neighbor.ridgeExposure || 0);
|
||||
return Math.max(0.18,
|
||||
1.0 +
|
||||
boundaryScore * 5.2 +
|
||||
mountainPenalty * 1.8 +
|
||||
ridgePenalty * 0.9 -
|
||||
boundaryScore * 9.2 +
|
||||
mountainPenalty * 2.65 +
|
||||
ridgePenalty * 2.75 +
|
||||
(boundaryScore > 0.64 || ridgePenalty > 0.64 ? 5.5 : 0) -
|
||||
sameClass * 0.45 -
|
||||
sameGroup * 0.35 -
|
||||
lowlandContinuity * 1.15 -
|
||||
urbanContinuity * 0.70 -
|
||||
lowlandContinuity * 0.98 -
|
||||
urbanContinuity * 0.64 -
|
||||
Math.min(1.0, edge.count / 12) * 0.25
|
||||
);
|
||||
}
|
||||
|
||||
function enrichCompartmentsWithUnifiedGeography(compartments, options = {}) {
|
||||
const geo = options.geography || options || {};
|
||||
const fields = [
|
||||
["habitability", geo.habitability],
|
||||
["accessibility", geo.accessibility],
|
||||
["centrality", geo.centrality],
|
||||
["boundaryAvoidance", geo.boundaryAvoidance],
|
||||
["adminBoundaryPreference", geo.adminBoundaryPreference],
|
||||
["geographicBarrier", geo.geographicBarrier],
|
||||
];
|
||||
if (!fields.some(([, field]) => field)) return false;
|
||||
for (const unit of compartments || []) {
|
||||
if (!unit || !unit.cells?.length) continue;
|
||||
for (const [name, field] of fields) {
|
||||
if (!field) continue;
|
||||
let sum = 0;
|
||||
for (const i of unit.cells) sum += field[i] || 0;
|
||||
unit[name] = sum / Math.max(1, unit.cells.length);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function graphVoronoiCompartmentOwners(compartments, compartmentId, adminCenters, options = {}) {
|
||||
const owner = new Int16Array(compartments.length);
|
||||
const dist = new Float32Array(compartments.length);
|
||||
|
|
@ -1297,7 +1462,8 @@ function graphVoronoiCompartmentOwners(compartments, compartmentId, adminCenters
|
|||
const crossing = compartmentCrossingCost(unit, neighbor, edge);
|
||||
const euclideanTie = center ? Math.hypot(neighbor.x - center.x, neighbor.y - center.y) * 0.006 : 0;
|
||||
const hinterlandDrag = (neighbor.mountainFitness || 0) > 0.64 && (neighbor.population || 0) < 6 ? 0.35 : 0;
|
||||
const next = cur.f + crossing + euclideanTie + hinterlandDrag;
|
||||
const ridgeExpansionDrag = Math.max(0, (neighbor.ridgeExposure || 0) - (unit.ridgeExposure || 0)) * 1.75 + (crossing > 9.0 ? 2.8 : 0);
|
||||
const next = cur.f + crossing + euclideanTie + hinterlandDrag + ridgeExpansionDrag;
|
||||
if (next + 1e-5 < dist[neighborId]) {
|
||||
dist[neighborId] = next;
|
||||
owner[neighborId] = cur.owner;
|
||||
|
|
@ -1364,6 +1530,25 @@ function averageFinalBorderBarrier(adminId, prefectureMask, sea, naturalBarrierS
|
|||
return count ? sum / count : 0;
|
||||
}
|
||||
|
||||
function averageFinalBorderField(adminId, prefectureMask, sea, field) {
|
||||
if (!field) return 0;
|
||||
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 += ((field[i] || 0) + (field[ni] || 0)) * 0.5;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count ? sum / count : 0;
|
||||
}
|
||||
|
||||
function assignCompartmentsToAdminOwners(compartments, compartmentId, adminCenters, naturalBarrierScore, prefectureMask, sea) {
|
||||
const owner = new Int16Array(compartments.length);
|
||||
owner.fill(-1);
|
||||
|
|
@ -1453,6 +1638,7 @@ export function assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, e
|
|||
progress?.("natural compartments built");
|
||||
const adminId = new Int16Array(SIZE);
|
||||
adminId.fill(-1);
|
||||
const unifiedGeographyApplied = enrichCompartmentsWithUnifiedGeography(compartments, options);
|
||||
const owner = graphVoronoiCompartmentOwners(compartments, compartmentId, adminCenters, options);
|
||||
progress?.("natural compartments assigned");
|
||||
for (const unit of compartments) {
|
||||
|
|
@ -1485,6 +1671,10 @@ export function assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, e
|
|||
.sort((a, b) => (b.elongation * Math.sqrt(Math.max(1, b.area))) - (a.elongation * Math.sqrt(Math.max(1, a.area))))
|
||||
.slice(0, 8),
|
||||
finalBorderNaturalBarrierAverage: averageFinalBorderBarrier(adminId, prefectureMask, sea, naturalBarrierScore),
|
||||
unifiedGeographyAppliedToAdminCompartments: unifiedGeographyApplied,
|
||||
finalBorderUnifiedBoundaryPreferenceAverage: averageFinalBorderField(adminId, prefectureMask, sea, options.adminBoundaryPreference || options.geography?.adminBoundaryPreference),
|
||||
finalBorderUnifiedBoundaryAvoidanceAverage: averageFinalBorderField(adminId, prefectureMask, sea, options.boundaryAvoidance || options.geography?.boundaryAvoidance),
|
||||
finalBorderUnifiedCentralityAverage: averageFinalBorderField(adminId, prefectureMask, sea, options.centrality || options.geography?.centrality),
|
||||
voronoiLikeRateBefore: 0,
|
||||
voronoiLikeRateAfter: weakVoronoiLikeRate(adminId, adminCenters, prefectureMask, sea, naturalBarrierScore),
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue