before tweaking

This commit is contained in:
33333-33333 2026-05-22 13:57:52 +09:00
commit e48fae5509
8 changed files with 980 additions and 246 deletions

View file

@ -532,6 +532,26 @@ export function buildNaturalBarrierScore(prefectureMask, sea, elevation, slope,
return score;
}
function lowlandCompartmentFitness(i, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse) {
const lowRelief = clamp((0.68 - elevation[i]) * 1.25) + clamp((0.36 - slope[i]) * 1.45) + clamp((0.48 - ridgeField[i]) * 1.10);
const landuseFit = [1, 2, 3, 4, 7, 8].includes(landuse[i]) ? 0.42 : landuse[i] === 5 || landuse[i] === 6 ? 0.20 : 0;
return clamp(
lowRelief * 0.30 +
(plain?.[i] || 0) * 0.30 +
(agriculture?.[i] || 0) * 0.16 +
basinField[i] * 0.24 +
coastalLowland[i] * 0.24 +
valleyField[i] * 0.10 +
populationDensity[i] * 0.34 +
landuseFit
);
}
function mountainCompartmentFitness(i, elevation, slope, ridgeField, populationDensity, landuse) {
const settled = populationDensity[i] * 0.85 + ([2, 3, 4, 7, 8].includes(landuse[i]) ? 0.35 : 0);
return clamp(elevation[i] * 0.38 + slope[i] * 0.32 + ridgeField[i] * 0.42 - settled);
}
function canShareNaturalCompartment(a, b, classA, classB, barrier, river, flowAccum, valleyField, populationDensity, landuse) {
if (classA !== classB) {
const bothUrban = classA <= 3 && classB <= 3;
@ -546,6 +566,94 @@ function canShareNaturalCompartment(a, b, classA, classB, barrier, river, flowAc
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;
for (const i of unit.cells) {
const [x, y] = xyOf(i);
sx += x; sy += y; pop += populationDensity[i];
urbanWeight += urbanBoundaryPenalty(i, populationDensity, landuse);
ridgeExposure += ridgeField[i];
coastalExposure += coastalLowland[i];
basinIdentity += basinField[i];
valleyIdentity += valleyField[i];
lowlandFitness += lowlandCompartmentFitness(i, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse);
mountainFitness += mountainCompartmentFitness(i, elevation, slope, ridgeField, populationDensity, landuse);
}
const area = unit.cells.length;
unit.area = area;
unit.x = sx / Math.max(1, area);
unit.y = sy / Math.max(1, area);
unit.population = pop;
unit.urbanWeight = urbanWeight / Math.max(1, area);
unit.ridgeExposure = ridgeExposure / Math.max(1, area);
unit.riverExposure = riverExposure / Math.max(1, area);
unit.coastalExposure = coastalExposure / Math.max(1, area);
unit.basinIdentity = basinIdentity / Math.max(1, area);
unit.valleyIdentity = valleyIdentity / Math.max(1, area);
unit.lowlandFitness = lowlandFitness / Math.max(1, area);
unit.mountainFitness = mountainFitness / Math.max(1, area);
}
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;
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;
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 low = lowlandCompartmentFitness(i, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse);
const d = Math.hypot(x - fx, y - fy);
const score = d * (0.55 + low * 0.45) + hashSeededTie(x, y, seed + 17) * 0.20;
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 ni = indexOf(nx, ny);
if (!cellSet.has(ni) || localOwner.has(ni)) continue;
localOwner.set(ni, owner);
queue.push(ni);
}
}
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 = [], bCells = [];
for (const ci of unit.cells) (localOwner.get(ci) === 0 ? aCells : bCells).push(ci);
if (aCells.length < 10 || bCells.length < 10) return null;
unit.cells = aCells;
const newUnit = { ...unit, id: newId, cells: bCells, centerIds: [], adjacent: new Map() };
for (const ci of bCells) compartmentId[ci] = newId;
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 hashSeededTie(x, y, seed) {
let h = Math.imul((x | 0) ^ (seed | 0), 1597334677) ^ Math.imul((y | 0) ^ ((seed >>> 1) | 0), 3812015801);
h = (h ^ (h >>> 15)) >>> 0;
return h / 4294967295;
}
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)}`;
@ -555,7 +663,7 @@ function naturalGroupKey(unit) {
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) {
export function buildNaturalCompartments(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 compartmentId = new Int32Array(SIZE);
compartmentId.fill(-1);
@ -595,7 +703,7 @@ export function buildNaturalCompartments(prefectureMask, sea, elevation, slope,
}
}
const area = cells.length;
compartments.push({
const unit = {
id,
cells,
area,
@ -612,11 +720,34 @@ export function buildNaturalCompartments(prefectureMask, sea, elevation, slope,
valleyIdentity: valleyIdentity / area,
centerIds: [],
adjacent: new Map(),
});
};
unit.lowlandFitness = cells.reduce((sum, ci) => sum + lowlandCompartmentFitness(ci, elevation, slope, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse), 0) / area;
unit.mountainFitness = cells.reduce((sum, ci) => sum + mountainCompartmentFitness(ci, elevation, slope, ridgeField, populationDensity, landuse), 0) / area;
compartments.push(unit);
}
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
mergeTinyLandscapeUnits(compartmentId, compartments, 12);
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;
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));
const target = candidates[0];
if (!target) break;
const newUnit = splitOneNaturalCompartment(target, compartments.length, compartmentId, fields, (options.seed || 0) + guard);
if (!newUnit) {
target.lowlandFitness = 0;
continue;
}
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 };
}
@ -725,6 +856,107 @@ function naturalOwnershipAffinity(unit, neighbor, edge) {
return edge.count * 0.55 + sameClass + sameGroup + urbanContinuity + (bothCorridor ? 0.72 : 0) - strongDividerPenalty;
}
function compartmentCrossingCost(unit, neighbor, edge) {
const boundaryScore = edge.target / Math.max(1, edge.count);
const sameClass = unit.classId === neighbor.classId ? 1 : 0;
const sameGroup = naturalGroupKey(unit) === naturalGroupKey(neighbor) ? 1 : 0;
const lowlandContinuity = Math.min(unit.lowlandFitness || 0, neighbor.lowlandFitness || 0);
const urbanContinuity = Math.min(unit.urbanWeight || 0, neighbor.urbanWeight || 0);
const mountainPenalty = Math.max(unit.mountainFitness || 0, neighbor.mountainFitness || 0);
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 -
sameClass * 0.45 -
sameGroup * 0.35 -
lowlandContinuity * 1.15 -
urbanContinuity * 0.70 -
Math.min(1.0, edge.count / 12) * 0.25
);
}
function graphVoronoiCompartmentOwners(compartments, compartmentId, adminCenters, options = {}) {
const owner = new Int16Array(compartments.length);
const dist = new Float32Array(compartments.length);
owner.fill(-1);
dist.fill(INF);
const heap = new MinHeap();
for (let id = 0; id < adminCenters.length; id++) {
const center = adminCenters[id];
if (!center || !inside(center.x, center.y)) continue;
const compIndex = compartmentId[indexOf(center.x, center.y)];
const unit = compartments[compIndex];
if (compIndex < 0 || !unit || unit.area === 0) continue;
unit.centerIds.push(id);
if (dist[compIndex] > 0) {
dist[compIndex] = 0;
owner[compIndex] = id;
heap.push({ i: compIndex, f: 0, owner: id });
}
}
while (heap.length > 0) {
const cur = heap.pop();
if (!cur || cur.f > dist[cur.i] + 1e-5) continue;
const unit = compartments[cur.i];
if (!unit || unit.area === 0) continue;
const center = adminCenters[cur.owner];
for (const [neighborId, edge] of unit.adjacent) {
const neighbor = compartments[neighborId];
if (!neighbor || neighbor.area === 0) continue;
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;
if (next + 1e-5 < dist[neighborId]) {
dist[neighborId] = next;
owner[neighborId] = cur.owner;
heap.push({ i: neighborId, f: next, owner: cur.owner });
} else if (Math.abs(next - dist[neighborId]) < 0.08 && owner[neighborId] >= 0) {
const oldCenter = adminCenters[owner[neighborId]];
const oldD = oldCenter ? Math.hypot(neighbor.x - oldCenter.x, neighbor.y - oldCenter.y) : INF;
const newD = center ? Math.hypot(neighbor.x - center.x, neighbor.y - center.y) : INF;
if (newD < oldD - 1.5 || (newD < oldD + 1.5 && cur.owner < owner[neighborId])) owner[neighborId] = cur.owner;
}
}
}
for (const unit of compartments) {
if (!unit || unit.area === 0 || owner[unit.id] >= 0) continue;
let bestOwner = -1, bestScore = INF;
for (const [neighborId, edge] of unit.adjacent) {
if (owner[neighborId] < 0) continue;
const neighbor = compartments[neighborId];
const score = compartmentCrossingCost(unit, neighbor, edge) + (neighbor?.area || 0) * -0.001;
if (score < bestScore) { bestScore = score; bestOwner = owner[neighborId]; }
}
owner[unit.id] = bestOwner >= 0 ? bestOwner : 0;
}
return owner;
}
function compartmentMunicipalityMetrics(compartments, owner, targetMunicipalityCount = 0, targetCompartmentCount = 0) {
const counts = new Map();
let active = 0;
for (const unit of compartments) {
if (!unit || unit.area === 0) continue;
active++;
const id = owner[unit.id];
if (id >= 0) counts.set(id, (counts.get(id) || 0) + 1);
}
const actual = counts.size;
const singles = [...counts.values()].filter((value) => value === 1).length;
return {
targetMunicipalityCount,
actualMunicipalityCount: actual,
targetNaturalCompartmentCount: targetCompartmentCount,
naturalCompartmentCount: active,
compartmentCount: active,
averageCompartmentsPerMunicipality: actual ? active / actual : 0,
singleCompartmentMunicipalityRatio: actual ? singles / actual : 0,
};
}
function averageFinalBorderBarrier(adminId, prefectureMask, sea, naturalBarrierScore) {
let sum = 0;
let count = 0;
@ -822,11 +1054,11 @@ export function extractCompartmentBorders(compartmentId, prefectureMask, sea) {
return segments;
}
export function assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCenters = []) {
const { compartmentId, compartments, naturalBarrierScore } = buildNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, null, plain, agriculture, populationDensity, landuse);
export function assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCenters = [], options = {}) {
const { compartmentId, compartments, naturalBarrierScore } = buildNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, null, plain, agriculture, populationDensity, landuse, options);
const adminId = new Int16Array(SIZE);
adminId.fill(-1);
const owner = assignCompartmentsToAdminOwners(compartments, compartmentId, adminCenters, naturalBarrierScore, prefectureMask, sea);
const owner = graphVoronoiCompartmentOwners(compartments, compartmentId, adminCenters, options);
for (const unit of compartments) {
const assigned = owner[unit.id];
if (assigned < 0) continue;
@ -837,22 +1069,16 @@ export function assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, e
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;
const i = indexOf(center.x, center.y);
if (prefectureMask[i] && !sea[i]) adminId[i] = id;
}
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, naturalBarrierScore, populationDensity, landuse);
const activeCompartments = compartments.filter((unit) => unit.area > 0);
const relationMetrics = compartmentMunicipalityMetrics(compartments, owner, options.targetMunicipalityCount || adminCenters.length, options.targetCompartmentCount || 0);
return {
adminId,
compartmentId,
compartments,
naturalBarrierScore,
debug: {
naturalCompartmentCount: activeCompartments.length,
compartmentCount: activeCompartments.length,
...relationMetrics,
compartmentBorders: extractCompartmentBorders(compartmentId, prefectureMask, sea),
averageCompartmentArea: activeCompartments.length ? activeCompartments.reduce((sum, unit) => sum + unit.area, 0) / activeCompartments.length : 0,
finalBorderNaturalBarrierAverage: averageFinalBorderBarrier(adminId, prefectureMask, sea, naturalBarrierScore),