before tweaking
This commit is contained in:
parent
da9d8ef904
commit
e48fae5509
8 changed files with 980 additions and 246 deletions
254
adminRegions.js
254
adminRegions.js
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue