town tweak
This commit is contained in:
parent
84ad22f7af
commit
1ea8ba1701
12 changed files with 5005 additions and 379 deletions
505
adminRegions.js
505
adminRegions.js
|
|
@ -558,20 +558,22 @@ function canShareNaturalCompartment(a, b, classA, classB, barrier, river, flowAc
|
|||
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 majorRiverEdge = Math.max(river[a], river[b]) > 0.56 || Math.max(flowAccum[a], flowAccum[b]) > 0.68;
|
||||
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;
|
||||
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;
|
||||
return barrier < threshold && (!majorRiverEdge || urbanEdge);
|
||||
}
|
||||
|
||||
function refreshCompartmentStats(unit, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse) {
|
||||
let sx = 0, sy = 0, pop = 0, urbanWeight = 0, ridgeExposure = 0, riverExposure = unit.riverExposure || 0;
|
||||
let coastalExposure = 0, basinIdentity = 0, valleyIdentity = 0, lowlandFitness = 0, mountainFitness = 0;
|
||||
let minX = MAP_W, minY = MAP_H, maxX = 0, maxY = 0;
|
||||
for (const i of unit.cells) {
|
||||
const [x, y] = xyOf(i);
|
||||
sx += x; sy += y; pop += populationDensity[i];
|
||||
minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y);
|
||||
urbanWeight += urbanBoundaryPenalty(i, populationDensity, landuse);
|
||||
ridgeExposure += ridgeField[i];
|
||||
coastalExposure += coastalLowland[i];
|
||||
|
|
@ -584,6 +586,13 @@ function refreshCompartmentStats(unit, elevation, slope, ridgeField, valleyField
|
|||
unit.area = area;
|
||||
unit.x = sx / Math.max(1, area);
|
||||
unit.y = sy / Math.max(1, area);
|
||||
unit.minX = area ? minX : 0;
|
||||
unit.minY = area ? minY : 0;
|
||||
unit.maxX = area ? maxX : 0;
|
||||
unit.maxY = area ? maxY : 0;
|
||||
unit.width = area ? maxX - minX + 1 : 0;
|
||||
unit.height = area ? maxY - minY + 1 : 0;
|
||||
unit.elongation = Math.max(unit.width, unit.height) / Math.max(1, Math.min(unit.width, unit.height));
|
||||
unit.population = pop;
|
||||
unit.urbanWeight = urbanWeight / Math.max(1, area);
|
||||
unit.ridgeExposure = ridgeExposure / Math.max(1, area);
|
||||
|
|
@ -596,13 +605,25 @@ function refreshCompartmentStats(unit, elevation, slope, ridgeField, valleyField
|
|||
}
|
||||
|
||||
function splitOneNaturalCompartment(unit, newId, compartmentId, fields, seed) {
|
||||
if (!unit || unit.area < 28 || unit.lowlandFitness < 0.24 || unit.mountainFitness > 0.72) return null;
|
||||
const { elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore } = fields;
|
||||
let first = -1, second = -1, bestA = -INF, bestB = -INF;
|
||||
if (!unit || unit.area < 24) return null;
|
||||
const { elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore, river, flowAccum } = fields;
|
||||
const minPart = Math.max(8, Math.min(28, Math.floor(unit.area * 0.20)));
|
||||
|
||||
let first = -1;
|
||||
let second = -1;
|
||||
let bestA = -INF;
|
||||
let bestB = -INF;
|
||||
const width = unit.width || (unit.maxX - unit.minX + 1) || 1;
|
||||
const height = unit.height || (unit.maxY - unit.minY + 1) || 1;
|
||||
const horizontal = width >= height;
|
||||
const elongated = Math.max(width, height) / Math.max(1, Math.min(width, height)) > 1.65;
|
||||
|
||||
for (const i of unit.cells) {
|
||||
const [x, y] = xyOf(i);
|
||||
const low = lowlandCompartmentFitness(i, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse);
|
||||
const score = low + populationDensity[i] * 0.22 + hashSeededTie(x, y, seed) * 0.04;
|
||||
const settled = populationDensity[i] * 0.28 + ([2, 3, 4, 7, 8].includes(landuse[i]) ? 0.34 : 0);
|
||||
const axis = elongated ? (horizontal ? (unit.maxX - x) / Math.max(1, width) : (unit.maxY - y) / Math.max(1, height)) : 0.0;
|
||||
const score = axis * 1.7 + low * 0.42 + settled + hashSeededTie(x, y, seed) * 0.05 - ridgeField[i] * 0.10;
|
||||
if (score > bestA) { bestA = score; first = i; }
|
||||
}
|
||||
if (first < 0) return null;
|
||||
|
|
@ -610,36 +631,54 @@ function splitOneNaturalCompartment(unit, newId, compartmentId, fields, seed) {
|
|||
for (const i of unit.cells) {
|
||||
const [x, y] = xyOf(i);
|
||||
const low = lowlandCompartmentFitness(i, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse);
|
||||
const axis = elongated ? (horizontal ? (x - unit.minX) / Math.max(1, width) : (y - unit.minY) / Math.max(1, height)) : 0.0;
|
||||
const d = Math.hypot(x - fx, y - fy);
|
||||
const score = d * (0.55 + low * 0.45) + hashSeededTie(x, y, seed + 17) * 0.20;
|
||||
const score = axis * 1.9 + d * (0.18 + low * 0.22) + hashSeededTie(x, y, seed + 17) * 0.08 - ridgeField[i] * 0.08;
|
||||
if (score > bestB) { bestB = score; second = i; }
|
||||
}
|
||||
if (second < 0 || second === first) return null;
|
||||
|
||||
const cellSet = new Set(unit.cells);
|
||||
const localOwner = new Map([[first, 0], [second, 1]]);
|
||||
const queue = [first, second];
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
const owner = localOwner.get(cur);
|
||||
const [x, y] = xyOf(cur);
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const owner = new Int8Array(SIZE);
|
||||
owner.fill(-1);
|
||||
const dist = new Float32Array(SIZE);
|
||||
dist.fill(INF);
|
||||
const heap = new MinHeap();
|
||||
for (const [source, sourceOwner] of [[first, 0], [second, 1]]) {
|
||||
owner[source] = sourceOwner;
|
||||
dist[source] = 0;
|
||||
heap.push({ i: source, f: 0, owner: sourceOwner });
|
||||
}
|
||||
|
||||
while (heap.length > 0) {
|
||||
const cur = heap.pop();
|
||||
if (!cur || cur.f > dist[cur.i] + 1e-5) continue;
|
||||
const [x, y] = xyOf(cur.i);
|
||||
for (const [nx, ny, step] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!cellSet.has(ni) || localOwner.has(ni)) continue;
|
||||
localOwner.set(ni, owner);
|
||||
queue.push(ni);
|
||||
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 nd = cur.f + stepCost;
|
||||
if (nd < dist[ni]) {
|
||||
dist[ni] = nd;
|
||||
owner[ni] = cur.owner;
|
||||
heap.push({ i: ni, f: nd, owner: cur.owner });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const ci of unit.cells) if (!localOwner.has(ci)) {
|
||||
const [x, y] = xyOf(ci);
|
||||
const d0 = Math.hypot(x - fx, y - fy);
|
||||
const [sx, sy] = xyOf(second);
|
||||
const d1 = Math.hypot(x - sx, y - sy);
|
||||
localOwner.set(ci, d0 <= d1 ? 0 : 1);
|
||||
|
||||
const aCells = [];
|
||||
const bCells = [];
|
||||
for (const ci of unit.cells) {
|
||||
if (owner[ci] === 1) bCells.push(ci);
|
||||
else aCells.push(ci);
|
||||
}
|
||||
const aCells = [], bCells = [];
|
||||
for (const ci of unit.cells) (localOwner.get(ci) === 0 ? aCells : bCells).push(ci);
|
||||
if (aCells.length < 10 || bCells.length < 10) return null;
|
||||
if (aCells.length < minPart || bCells.length < minPart) return null;
|
||||
|
||||
unit.cells = aCells;
|
||||
const newUnit = { ...unit, id: newId, cells: bCells, centerIds: [], adjacent: new Map() };
|
||||
for (const ci of bCells) compartmentId[ci] = newId;
|
||||
|
|
@ -663,7 +702,369 @@ function naturalGroupKey(unit) {
|
|||
return `plain:${Math.round(unit.x / 14)}:${Math.round(unit.y / 14)}`;
|
||||
}
|
||||
|
||||
|
||||
function 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 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;
|
||||
seen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
components.push(cells);
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
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);
|
||||
const mountain = mountainCompartmentFitness(i, elevation, slope, ridgeField, populationDensity, landuse);
|
||||
const stableInterior = clamp(1 - (naturalBarrierScore[i] || 0));
|
||||
const settlement = clamp(populationDensity[i] * 0.65 + (klassUrban ? 0.24 : 0));
|
||||
const streamCorridor = clamp(valleyField[i] * 0.28 + river[i] * 0.08);
|
||||
const mountainInterior = clamp(mountain * 0.45 + stableInterior * 0.28 - ridgeField[i] * 0.22);
|
||||
return stableInterior * 0.56 + lowland * 0.42 + mountainInterior * 0.32 + settlement * 0.26 + streamCorridor + hashSeededTie(...xyOf(i), seed) * 0.13 - slope[i] * 0.10;
|
||||
}
|
||||
|
||||
function chooseNaturalCompartmentSeeds(landComponents, targetCount, fields, seed) {
|
||||
const { elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore } = fields;
|
||||
const totalArea = landComponents.reduce((sum, cells) => sum + cells.length, 0);
|
||||
const seeds = [];
|
||||
const seedComponentId = [];
|
||||
const minCellsPerUnit = 9;
|
||||
let remainingTarget = Math.max(1, Math.min(targetCount || Math.round(totalArea / 42), Math.floor(totalArea / minCellsPerUnit)));
|
||||
|
||||
const sortedComponents = landComponents
|
||||
.map((cells, componentIndex) => ({ cells, componentIndex, area: cells.length }))
|
||||
.sort((a, b) => b.area - a.area);
|
||||
|
||||
for (let componentOrder = 0; componentOrder < sortedComponents.length; componentOrder++) {
|
||||
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);
|
||||
localTarget = Math.min(localTarget, Math.max(1, Math.floor(area / minCellsPerUnit)));
|
||||
if (componentOrder === sortedComponents.length - 1) localTarget = Math.max(1, Math.min(localTarget, remainingTarget));
|
||||
remainingTarget -= localTarget;
|
||||
|
||||
const candidates = cells
|
||||
.map((i) => ({ i, score: naturalSeedScore(i, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore, seed + componentIndex * 1009) }))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
const localSeeds = [];
|
||||
const idealSpacing = Math.sqrt(area / Math.max(1, localTarget));
|
||||
const spacingPasses = [0.95, 0.78, 0.62, 0.48, 0.34];
|
||||
for (const factor of spacingPasses) {
|
||||
const minDist = Math.max(2.2, idealSpacing * factor);
|
||||
for (const candidate of candidates) {
|
||||
if (localSeeds.length >= localTarget) break;
|
||||
const [x, y] = xyOf(candidate.i);
|
||||
let ok = true;
|
||||
for (const existing of localSeeds) {
|
||||
const [ex, ey] = xyOf(existing);
|
||||
if (Math.hypot(x - ex, y - ey) < minDist) { ok = false; break; }
|
||||
}
|
||||
if (ok) localSeeds.push(candidate.i);
|
||||
}
|
||||
if (localSeeds.length >= localTarget) break;
|
||||
}
|
||||
for (const i of localSeeds) {
|
||||
seeds.push(i);
|
||||
seedComponentId.push(componentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (seeds.length === 0 && landComponents[0]?.length) {
|
||||
seeds.push(landComponents[0][0]);
|
||||
seedComponentId.push(0);
|
||||
}
|
||||
return { seeds, seedComponentId };
|
||||
}
|
||||
|
||||
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;
|
||||
const ridge = Math.max(ridgeField[a], ridgeField[b]);
|
||||
const riverEdge = Math.max(river[a], river[b]);
|
||||
const flowEdge = Math.max(flowAccum?.[a] || 0, flowAccum?.[b] || 0);
|
||||
const majorRiverCrossing = riverEdge > 0.44 || flowEdge > 0.55;
|
||||
const elevationBreak = Math.abs(elevation[a] - elevation[b]);
|
||||
const slopeBreak = Math.max(slope[a], slope[b]);
|
||||
const classBreak = cellClass[a] !== cellClass[b] ? 0.34 : -0.08;
|
||||
const bothUrban = ((landuse[a] >= 2 && landuse[a] <= 4) || landuse[a] === 7 || populationDensity[a] > 0.30) &&
|
||||
((landuse[b] >= 2 && landuse[b] <= 4) || landuse[b] === 7 || populationDensity[b] > 0.30);
|
||||
const lowlandContinuity = Math.min(
|
||||
(plain?.[a] || 0) + (agriculture?.[a] || 0) * 0.35 + basinField[a] * 0.25 + coastalLowland[a] * 0.20,
|
||||
(plain?.[b] || 0) + (agriculture?.[b] || 0) * 0.35 + basinField[b] * 0.25 + coastalLowland[b] * 0.20
|
||||
);
|
||||
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;
|
||||
return Math.max(0.16,
|
||||
0.72 +
|
||||
barrier * 5.1 +
|
||||
ridge * 0.82 +
|
||||
elevationBreak * 3.0 +
|
||||
slopeBreak * 0.56 +
|
||||
riverPenalty +
|
||||
classBreak -
|
||||
corridorBonus
|
||||
);
|
||||
}
|
||||
|
||||
function buildUnitsFromAssignment(compartmentId, cellClass, prefectureMask, sea, fields) {
|
||||
const { elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse } = fields;
|
||||
let maxId = -1;
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && compartmentId[i] > maxId) maxId = compartmentId[i];
|
||||
const units = Array.from({ length: maxId + 1 }, (_, id) => ({ id, cells: [], centerIds: [], adjacent: new Map(), area: 0 }));
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
const id = compartmentId[i];
|
||||
if (id >= 0 && units[id]) units[id].cells.push(i);
|
||||
}
|
||||
for (const unit of units) {
|
||||
if (!unit.cells.length) { unit.area = 0; continue; }
|
||||
const counts = new Map();
|
||||
for (const ci of unit.cells) counts.set(cellClass[ci], (counts.get(cellClass[ci]) || 0) + 1);
|
||||
let klass = -1, best = -1;
|
||||
for (const [k, count] of counts) if (count > best) { best = count; klass = k; }
|
||||
unit.classId = klass;
|
||||
unit.dominantLandscapeClass = klass;
|
||||
refreshCompartmentStats(unit, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse);
|
||||
let riverExposure = 0;
|
||||
for (const ci of unit.cells) riverExposure += river[ci] + (fields.flowAccum?.[ci] || 0) * 0.45;
|
||||
unit.riverExposure = riverExposure / Math.max(1, unit.area);
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
|
||||
function splitDisconnectedCompartments(compartmentId, compartments, prefectureMask, sea) {
|
||||
const queue = [];
|
||||
for (const unit of [...compartments]) {
|
||||
if (!unit || unit.area === 0 || !unit.cells?.length) continue;
|
||||
const unitCellSet = new Set(unit.cells);
|
||||
const seen = new Set();
|
||||
const components = [];
|
||||
for (const start of unit.cells) {
|
||||
if (seen.has(start) || compartmentId[start] !== unit.id) continue;
|
||||
const cells = [];
|
||||
queue.length = 0;
|
||||
queue.push(start);
|
||||
seen.add(start);
|
||||
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 (!prefectureMask[ni] || sea[ni] || seen.has(ni) || compartmentId[ni] !== unit.id || !unitCellSet.has(ni)) continue;
|
||||
seen.add(ni);
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
components.push(cells);
|
||||
}
|
||||
if (components.length <= 1) continue;
|
||||
components.sort((a, b) => b.length - a.length);
|
||||
unit.cells = components[0];
|
||||
for (const extra of components.slice(1)) {
|
||||
const newId = compartments.length;
|
||||
for (const ci of extra) compartmentId[ci] = newId;
|
||||
compartments.push({ id: newId, cells: extra, centerIds: [], adjacent: new Map(), classId: unit.classId, dominantLandscapeClass: unit.dominantLandscapeClass });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renumberCompartments(compartmentId, compartments, prefectureMask, sea) {
|
||||
const active = compartments.filter((unit) => unit && unit.area > 0 && unit.cells?.length);
|
||||
const idMap = new Map();
|
||||
active.forEach((unit, newId) => idMap.set(unit.id, newId));
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
const id = compartmentId[i];
|
||||
if (!prefectureMask[i] || sea[i]) compartmentId[i] = -1;
|
||||
else if (idMap.has(id)) compartmentId[i] = idMap.get(id);
|
||||
}
|
||||
active.forEach((unit, newId) => { unit.id = newId; unit.centerIds = []; });
|
||||
return active;
|
||||
}
|
||||
|
||||
function refreshAllCompartmentStats(compartments, fields) {
|
||||
const { elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, river, flowAccum } = fields;
|
||||
for (const unit of compartments) {
|
||||
if (!unit || unit.area === 0 || !unit.cells?.length) continue;
|
||||
refreshCompartmentStats(unit, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse);
|
||||
let riverExposure = 0;
|
||||
const counts = new Map();
|
||||
for (const ci of unit.cells) {
|
||||
riverExposure += river[ci] + (flowAccum?.[ci] || 0) * 0.45;
|
||||
if (unit._cellClass) counts.set(unit._cellClass[ci], (counts.get(unit._cellClass[ci]) || 0) + 1);
|
||||
}
|
||||
unit.riverExposure = riverExposure / Math.max(1, unit.area);
|
||||
}
|
||||
}
|
||||
|
||||
function splitNaturalCompartmentCompact(unit, newId, compartmentId, fields, seed) {
|
||||
if (!unit || unit.area < 20) return null;
|
||||
const { elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore, river, flowAccum, cellClass } = fields;
|
||||
const minPart = Math.max(7, Math.min(30, Math.floor(unit.area * 0.18)));
|
||||
let cx = unit.x || 0, cy = unit.y || 0;
|
||||
let first = -1, second = -1, bestA = -INF, bestB = -INF;
|
||||
const elongated = (unit.elongation || 1) > 2.3;
|
||||
const horizontal = (unit.width || 0) >= (unit.height || 0);
|
||||
for (const i of unit.cells) {
|
||||
const [x, y] = xyOf(i);
|
||||
const centerDist = Math.hypot(x - cx, y - cy);
|
||||
const axis = elongated ? Math.abs((horizontal ? x - cx : y - cy)) / Math.max(1, horizontal ? unit.width : unit.height) : 0;
|
||||
const interior = 1 - (naturalBarrierScore[i] || 0);
|
||||
const score = centerDist * 0.13 + axis * 1.1 + interior * 0.35 + hashSeededTie(x, y, seed) * 0.08 - ridgeField[i] * 0.10;
|
||||
if (score > bestA) { bestA = score; first = i; }
|
||||
}
|
||||
if (first < 0) return null;
|
||||
const [fx, fy] = xyOf(first);
|
||||
for (const i of unit.cells) {
|
||||
const [x, y] = xyOf(i);
|
||||
const d = Math.hypot(x - fx, y - fy);
|
||||
const interior = 1 - (naturalBarrierScore[i] || 0);
|
||||
const score = d * 0.20 + interior * 0.38 + hashSeededTie(x, y, seed + 31) * 0.08 - ridgeField[i] * 0.08;
|
||||
if (score > bestB) { bestB = score; second = i; }
|
||||
}
|
||||
if (second < 0 || second === first) return null;
|
||||
|
||||
const cellSet = new Set(unit.cells);
|
||||
const owner = new Int8Array(SIZE);
|
||||
owner.fill(-1);
|
||||
const dist = new Float32Array(SIZE);
|
||||
dist.fill(INF);
|
||||
const heap = new MinHeap();
|
||||
for (const [source, sourceOwner] of [[first, 0], [second, 1]]) {
|
||||
owner[source] = sourceOwner;
|
||||
dist[source] = 0;
|
||||
heap.push({ i: source, f: 0, owner: sourceOwner });
|
||||
}
|
||||
while (heap.length > 0) {
|
||||
const cur = heap.pop();
|
||||
if (!cur || cur.f > dist[cur.i] + 1e-5) continue;
|
||||
const [x, y] = xyOf(cur.i);
|
||||
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;
|
||||
if (nd < dist[ni]) {
|
||||
dist[ni] = nd;
|
||||
owner[ni] = cur.owner;
|
||||
heap.push({ i: ni, f: nd, owner: cur.owner });
|
||||
}
|
||||
}
|
||||
}
|
||||
const aCells = [], bCells = [];
|
||||
for (const ci of unit.cells) (owner[ci] === 1 ? bCells : aCells).push(ci);
|
||||
if (aCells.length < minPart || bCells.length < minPart) return null;
|
||||
unit.cells = aCells;
|
||||
for (const ci of bCells) compartmentId[ci] = newId;
|
||||
const newUnit = { id: newId, cells: bCells, 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 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 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)));
|
||||
const { seeds } = chooseNaturalCompartmentSeeds(landComponents, targetCount, fields, options.seed || 0);
|
||||
const compartmentId = new Int32Array(SIZE);
|
||||
compartmentId.fill(-1);
|
||||
const dist = new Float32Array(SIZE);
|
||||
dist.fill(INF);
|
||||
const heap = new MinHeap();
|
||||
seeds.forEach((i, id) => {
|
||||
compartmentId[i] = id;
|
||||
dist[i] = 0;
|
||||
heap.push({ i, f: 0, id });
|
||||
});
|
||||
while (heap.length > 0) {
|
||||
const cur = heap.pop();
|
||||
if (!cur || cur.f > dist[cur.i] + 1e-5) continue;
|
||||
const [x, y] = xyOf(cur.i);
|
||||
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;
|
||||
if (nd < dist[ni]) {
|
||||
dist[ni] = nd;
|
||||
compartmentId[ni] = cur.id;
|
||||
heap.push({ i: ni, f: nd, id: cur.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && compartmentId[i] < 0) compartmentId[i] = 0;
|
||||
|
||||
let compartments = buildUnitsFromAssignment(compartmentId, cellClass, prefectureMask, sea, fields);
|
||||
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
mergeTinyLandscapeUnits(compartmentId, compartments, 9);
|
||||
splitDisconnectedCompartments(compartmentId, compartments, prefectureMask, sea);
|
||||
compartments = renumberCompartments(compartmentId, compartments, prefectureMask, sea);
|
||||
refreshAllCompartmentStats(compartments, fields);
|
||||
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
|
||||
const maxNaturalCompartmentArea = options.maxNaturalCompartmentArea || Math.max(28, Math.round(landArea / Math.max(1, targetCount) * 1.55));
|
||||
let guard = Math.max(80, targetCount * 3);
|
||||
while (guard-- > 0) {
|
||||
let active = compartments.filter((unit) => unit && unit.area > 0);
|
||||
const needMore = active.length < targetCount;
|
||||
const worst = active
|
||||
.filter((unit) => unit.area >= 20 && (needMore || unit.area > maxNaturalCompartmentArea * 1.18 || (unit.elongation || 1) > 4.2))
|
||||
.sort((a, b) => {
|
||||
const sa = (a.area / maxNaturalCompartmentArea) * 1.8 + Math.max(0, (a.elongation || 1) - 3.0) * 1.2;
|
||||
const sb = (b.area / maxNaturalCompartmentArea) * 1.8 + Math.max(0, (b.elongation || 1) - 3.0) * 1.2;
|
||||
return sb - sa;
|
||||
})[0];
|
||||
if (!worst) break;
|
||||
const newUnit = splitNaturalCompartmentCompact(worst, compartments.length, compartmentId, fields, (options.seed || 0) + guard * 97);
|
||||
if (!newUnit) {
|
||||
worst._splitRejected = (worst._splitRejected || 0) + 1;
|
||||
if (worst._splitRejected > 2) worst.elongation = Math.min(worst.elongation || 1, 3.1);
|
||||
if (!needMore) break;
|
||||
continue;
|
||||
}
|
||||
compartments.push(newUnit);
|
||||
if (compartments.filter((unit) => unit && unit.area > 0).length >= targetCount && newUnit.area <= maxNaturalCompartmentArea) {
|
||||
const stillBad = compartments.some((unit) => unit && unit.area >= 20 && (
|
||||
unit.area > maxNaturalCompartmentArea * 1.35 ||
|
||||
((unit.elongation || 1) > 4.2 && unit.area > 28)
|
||||
));
|
||||
if (!stillBad) break;
|
||||
}
|
||||
}
|
||||
|
||||
splitDisconnectedCompartments(compartmentId, compartments, prefectureMask, sea);
|
||||
compartments = renumberCompartments(compartmentId, compartments, prefectureMask, sea);
|
||||
refreshAllCompartmentStats(compartments, fields);
|
||||
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
return { compartmentId, compartments, naturalBarrierScore };
|
||||
}
|
||||
|
||||
export function buildNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore = null, plain = null, agriculture = null, populationDensity = null, landuse = null, options = {}) {
|
||||
return buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse, options);
|
||||
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);
|
||||
|
|
@ -679,6 +1080,7 @@ export function buildNaturalCompartments(prefectureMask, sea, elevation, slope,
|
|||
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;
|
||||
let minX = MAP_W, minY = MAP_H, maxX = 0, maxY = 0;
|
||||
queue.length = 0;
|
||||
queue.push(i);
|
||||
compartmentId[i] = id;
|
||||
|
|
@ -687,6 +1089,7 @@ export function buildNaturalCompartments(prefectureMask, sea, elevation, slope,
|
|||
const [x, y] = xyOf(cur);
|
||||
cells.push(cur);
|
||||
sx += x; sy += y; pop += populationDensity[cur];
|
||||
minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y);
|
||||
urbanWeight += urbanBoundaryPenalty(cur, populationDensity, landuse);
|
||||
ridgeExposure += ridgeField[cur];
|
||||
riverExposure += river[cur] + flowAccum[cur] * 0.45;
|
||||
|
|
@ -711,6 +1114,13 @@ export function buildNaturalCompartments(prefectureMask, sea, elevation, slope,
|
|||
y: sy / area,
|
||||
classId: startClass,
|
||||
dominantLandscapeClass: startClass,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
width: maxX - minX + 1,
|
||||
height: maxY - minY + 1,
|
||||
elongation: Math.max(maxX - minX + 1, maxY - minY + 1) / Math.max(1, Math.min(maxX - minX + 1, maxY - minY + 1)),
|
||||
population: pop,
|
||||
urbanWeight: urbanWeight / area,
|
||||
ridgeExposure: ridgeExposure / area,
|
||||
|
|
@ -730,22 +1140,47 @@ export function buildNaturalCompartments(prefectureMask, sea, elevation, slope,
|
|||
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
const targetCount = options.targetCompartmentCount || 0;
|
||||
if (targetCount > 0) {
|
||||
const fields = { elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore };
|
||||
let guard = targetCount * 3;
|
||||
const fields = { elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore, river, flowAccum };
|
||||
const landArea = compartments.reduce((sum, unit) => sum + (unit.area || 0), 0);
|
||||
const maxNaturalCompartmentArea = options.maxNaturalCompartmentArea || Math.max(34, Math.round(landArea / Math.max(1, targetCount) * 1.65));
|
||||
const splitScore = (unit) => {
|
||||
const elongated = Math.max(0, (unit.elongation || 1) - 2.1);
|
||||
const areaPressure = unit.area / Math.max(1, maxNaturalCompartmentArea);
|
||||
const settled = (unit.lowlandFitness || 0) * 0.65 + (unit.urbanWeight || 0) * 0.35;
|
||||
return areaPressure * 2.2 + elongated * 1.4 + settled - (unit.mountainFitness || 0) * 0.20;
|
||||
};
|
||||
let guard = Math.max(targetCount * 4, 80);
|
||||
while (compartments.filter((unit) => unit.area > 0).length < targetCount && guard-- > 0) {
|
||||
const candidates = compartments
|
||||
.filter((unit) => unit.area > 0 && unit.lowlandFitness > 0.24 && unit.mountainFitness < 0.74 && unit.area >= 28)
|
||||
.sort((a, b) => (b.area * (0.45 + b.lowlandFitness) - b.mountainFitness * 80) - (a.area * (0.45 + a.lowlandFitness) - a.mountainFitness * 80));
|
||||
.filter((unit) => unit.area > 0 && unit.area >= 24 && ((unit.lowlandFitness || 0) > 0.18 || unit.area > maxNaturalCompartmentArea * 1.20 || (unit.elongation || 1) > 2.8))
|
||||
.sort((a, b) => splitScore(b) - splitScore(a));
|
||||
const target = candidates[0];
|
||||
if (!target) break;
|
||||
const newUnit = splitOneNaturalCompartment(target, compartments.length, compartmentId, fields, (options.seed || 0) + guard);
|
||||
if (!newUnit) {
|
||||
target.lowlandFitness = 0;
|
||||
target._splitRejected = (target._splitRejected || 0) + 1;
|
||||
target.elongation = Math.max(1, (target.elongation || 1) * 0.72);
|
||||
if (target._splitRejected > 2) target.area = target.cells.length;
|
||||
continue;
|
||||
}
|
||||
compartments.push(newUnit);
|
||||
if (compartments.filter((unit) => unit.area > 0).length % 12 === 0) rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
}
|
||||
|
||||
guard = Math.max(targetCount * 2, 60);
|
||||
while (guard-- > 0) {
|
||||
const target = compartments
|
||||
.filter((unit) => unit.area > 0 && unit.area >= 24 && (unit.area > maxNaturalCompartmentArea * 1.55 || ((unit.elongation || 1) > 3.2 && unit.area > maxNaturalCompartmentArea * 0.85)))
|
||||
.sort((a, b) => splitScore(b) - splitScore(a))[0];
|
||||
if (!target) break;
|
||||
const newUnit = splitOneNaturalCompartment(target, compartments.length, compartmentId, fields, (options.seed || 0) + guard + 991);
|
||||
if (!newUnit) {
|
||||
target.elongation = Math.max(1, (target.elongation || 1) * 0.70);
|
||||
break;
|
||||
}
|
||||
compartments.push(newUnit);
|
||||
if (compartments.filter((unit) => unit.area > 0).length % 12 === 0) rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
}
|
||||
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
|
||||
}
|
||||
return { compartmentId, compartments, naturalBarrierScore };
|
||||
|
|
@ -1081,6 +1516,12 @@ export function assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, e
|
|||
...relationMetrics,
|
||||
compartmentBorders: extractCompartmentBorders(compartmentId, prefectureMask, sea),
|
||||
averageCompartmentArea: activeCompartments.length ? activeCompartments.reduce((sum, unit) => sum + unit.area, 0) / activeCompartments.length : 0,
|
||||
maxCompartmentArea: activeCompartments.length ? Math.max(...activeCompartments.map((unit) => unit.area || 0)) : 0,
|
||||
maxCompartmentElongation: activeCompartments.length ? Math.max(...activeCompartments.map((unit) => unit.elongation || 1)) : 1,
|
||||
worstNaturalCompartments: activeCompartments
|
||||
.map((unit) => ({ id: unit.id, area: unit.area || 0, width: unit.width || 0, height: unit.height || 0, elongation: unit.elongation || 1, classId: unit.classId, x: Math.round(unit.x || 0), y: Math.round(unit.y || 0) }))
|
||||
.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),
|
||||
voronoiLikeRateBefore: 0,
|
||||
voronoiLikeRateAfter: weakVoronoiLikeRate(adminId, adminCenters, prefectureMask, sea, naturalBarrierScore),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue