666 lines
30 KiB
JavaScript
666 lines
30 KiB
JavaScript
import { MinHeap } from "./graph.js";
|
|
import { INF, MAP_H, MAP_W, SIZE, clamp, indexOf, inside, xyOf } from "./grid.js";
|
|
import { weightedScore } from "./scoring.js";
|
|
|
|
function neighbors8(x, y) {
|
|
const out = [];
|
|
for (let dy = -1; dy <= 1; dy++) {
|
|
for (let dx = -1; dx <= 1; dx++) {
|
|
if (dx === 0 && dy === 0) continue;
|
|
const nx = x + dx;
|
|
const ny = y + dy;
|
|
if (inside(nx, ny)) out.push([nx, ny, Math.hypot(dx, dy)]);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function neighbors4(x, y) {
|
|
const out = [];
|
|
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
|
const nx = x + dx;
|
|
const ny = y + dy;
|
|
if (inside(nx, ny)) out.push([nx, ny, 1]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, crestCrossingScore, populationDensity, landuse) {
|
|
const adminId = new Int16Array(SIZE);
|
|
adminId.fill(-1);
|
|
const dist = new Float32Array(SIZE);
|
|
dist.fill(INF);
|
|
const heap = new MinHeap();
|
|
|
|
centers.forEach((center, regionId) => {
|
|
const i = indexOf(center.x, center.y);
|
|
dist[i] = 0;
|
|
adminId[i] = regionId;
|
|
heap.push({ i, f: 0, regionId });
|
|
});
|
|
|
|
let guard = 0;
|
|
while (heap.length > 0 && guard++ < SIZE * 12) {
|
|
const current = heap.pop();
|
|
if (!current) continue;
|
|
const curIndex = current.i;
|
|
const curRegion = adminId[curIndex];
|
|
if (curRegion < 0 || current.f > dist[curIndex] + 1e-5) continue;
|
|
|
|
const [cx, cy] = xyOf(curIndex);
|
|
for (const [nx, ny, step] of neighbors8(cx, cy)) {
|
|
const ni = indexOf(nx, ny);
|
|
if (!prefectureMask[ni] || sea[ni]) continue;
|
|
|
|
const ridgeBarrier = Math.max(ridgeField[ni], ridgeField[curIndex]);
|
|
const riverBarrier = Math.max(river[ni], river[curIndex]);
|
|
const highDivide = Math.max(elevation[ni], elevation[curIndex]);
|
|
const crest = Math.max(crestCrossingScore?.[ni] || 0, crestCrossingScore?.[curIndex] || 0);
|
|
const watershedBarrier = ridgeBarrier * (27.0 + Math.max(0, highDivide - 0.46) * 46.0);
|
|
const crestPenalty = crest * (18.0 + Math.max(0, highDivide - 0.48) * 38.0) * (1 + Math.max(0, highDivide - 0.56) * 1.9);
|
|
const ridgePenalty = Math.max(0, highDivide - 0.36) * 16.0 + Math.abs(elevation[ni] - elevation[curIndex]) * 12.4 + watershedBarrier;
|
|
const slopePenalty = slope[ni] * 10.6;
|
|
const valleyBarrier = valleyField[ni] > 0.50 ? valleyField[ni] * (riverBarrier > 0.16 ? 7.2 : 2.6) : 0;
|
|
const riverPenalty = riverBarrier > 0.7 ? 22.0 : riverBarrier > 0.42 ? 14.8 : riverBarrier > 0.22 ? 7.4 : riverBarrier > 0.12 ? 2.2 : 0;
|
|
const urbanContinuityBonus = (landuse[ni] >= 2 && landuse[ni] <= 4 && populationDensity[ni] > 0.20) ? 1.65 : 0;
|
|
const valleyLocalityBonus = valleyField[ni] * 0.16;
|
|
const stepCost = Math.max(0.25, 0.72 + ridgePenalty + crestPenalty + slopePenalty + riverPenalty + valleyBarrier - valleyLocalityBonus - urbanContinuityBonus) * step;
|
|
const nextDist = dist[curIndex] + stepCost;
|
|
|
|
if (nextDist < dist[ni]) {
|
|
dist[ni] = nextDist;
|
|
adminId[ni] = curRegion;
|
|
heap.push({ i: ni, f: nextDist, regionId: curRegion });
|
|
}
|
|
}
|
|
}
|
|
|
|
return adminId;
|
|
}
|
|
|
|
function terrainBoundaryStrength(i, elevation, slope, river, ridgeField, valleyField) {
|
|
return clamp(weightedScore([
|
|
[ridgeField[i], 3.15],
|
|
[river[i], 2.45],
|
|
[valleyField[i], 0.62],
|
|
[slope[i], 1.06],
|
|
[Math.max(0, elevation[i] - 0.5), 1.18],
|
|
]));
|
|
}
|
|
|
|
export function smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse, passes = 5) {
|
|
let current = new Int16Array(adminId);
|
|
for (let pass = 0; pass < passes; pass++) {
|
|
const next = new Int16Array(current);
|
|
for (let y = 1; y < MAP_H - 1; y++) {
|
|
for (let x = 1; x < MAP_W - 1; x++) {
|
|
const i = indexOf(x, y);
|
|
const own = current[i];
|
|
if (!prefectureMask[i] || sea[i] || own < 0) continue;
|
|
const urbanCell = (landuse[i] >= 2 && landuse[i] <= 4) || landuse[i] === 7 || populationDensity[i] > 0.24;
|
|
const barrier = terrainBoundaryStrength(i, elevation, slope, river, ridgeField, valleyField);
|
|
if (barrier > 0.62 || urbanCell) continue;
|
|
|
|
const counts = new Map();
|
|
let ownCount = 0;
|
|
for (const [nx, ny] of neighbors4(x, y)) {
|
|
const ni = indexOf(nx, ny);
|
|
if (!prefectureMask[ni] || sea[ni]) continue;
|
|
const id = current[ni];
|
|
if (id < 0) continue;
|
|
const weight = terrainBoundaryStrength(ni, elevation, slope, river, ridgeField, valleyField) > 0.72 ? 0.45 : 1;
|
|
counts.set(id, (counts.get(id) || 0) + weight);
|
|
if (id === own) ownCount += weight;
|
|
}
|
|
let bestId = own;
|
|
let best = ownCount;
|
|
for (const [id, score] of counts) if (score > best) { best = score; bestId = id; }
|
|
if (bestId !== own && (best >= 4.2 || ownCount <= 2.1)) next[i] = bestId;
|
|
}
|
|
}
|
|
current = next;
|
|
}
|
|
adminId.set(current);
|
|
}
|
|
|
|
export function lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, maxCells = 360) {
|
|
const seen = new Uint8Array(SIZE);
|
|
const queue = [];
|
|
for (let i = 0; i < SIZE; i++) {
|
|
if (seen[i] || !prefectureMask[i] || sea[i]) continue;
|
|
const isUrbanStart = (landuse[i] >= 2 && landuse[i] <= 4) || landuse[i] === 7 || landuse[i] === 8 || populationDensity[i] > 0.20;
|
|
if (!isUrbanStart) continue;
|
|
const component = [];
|
|
queue.length = 0;
|
|
queue.push(i);
|
|
seen[i] = 1;
|
|
for (let q = 0; q < queue.length; q++) {
|
|
const cur = queue[q];
|
|
component.push(cur);
|
|
const [x, y] = xyOf(cur);
|
|
for (const [nx, ny] of neighbors8(x, y)) {
|
|
const ni = indexOf(nx, ny);
|
|
if (seen[ni] || !prefectureMask[ni] || sea[ni]) continue;
|
|
const isUrban = (landuse[ni] >= 2 && landuse[ni] <= 4) || landuse[ni] === 7 || landuse[ni] === 8 || populationDensity[ni] > 0.20;
|
|
if (!isUrban) continue;
|
|
seen[ni] = 1;
|
|
queue.push(ni);
|
|
}
|
|
}
|
|
if (component.length === 0 || component.length > maxCells) continue;
|
|
const counts = new Map();
|
|
for (const ci of component) {
|
|
const id = adminId[ci];
|
|
if (id >= 0) counts.set(id, (counts.get(id) || 0) + 1 + populationDensity[ci]);
|
|
}
|
|
let bestId = -1;
|
|
let best = -1;
|
|
for (const [id, score] of counts) if (score > best) { best = score; bestId = id; }
|
|
if (bestId >= 0) for (const ci of component) adminId[ci] = bestId;
|
|
}
|
|
}
|
|
|
|
export function mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities = [], minArea = 320) {
|
|
const area = new Map();
|
|
const pop = new Map();
|
|
const adjacency = new Map();
|
|
const cityMunicipalities = new Set();
|
|
for (const city of modernCities || []) if (inside(city.x, city.y)) cityMunicipalities.add(adminId[indexOf(city.x, city.y)]);
|
|
|
|
for (let y = 0; y < MAP_H; y++) {
|
|
for (let x = 0; x < MAP_W; x++) {
|
|
const i = indexOf(x, y);
|
|
if (!prefectureMask[i] || sea[i]) continue;
|
|
const id = adminId[i];
|
|
if (id < 0) continue;
|
|
area.set(id, (area.get(id) || 0) + 1);
|
|
pop.set(id, (pop.get(id) || 0) + populationDensity[i]);
|
|
for (const [nx, ny] of [[x + 1, y], [x, y + 1]]) {
|
|
if (!inside(nx, ny)) continue;
|
|
const ni = indexOf(nx, ny);
|
|
if (!prefectureMask[ni] || sea[ni]) continue;
|
|
const other = adminId[ni];
|
|
if (other < 0 || other === id) continue;
|
|
const key = id < other ? `${id}:${other}` : `${other}:${id}`;
|
|
adjacency.set(key, (adjacency.get(key) || 0) + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
const mergeTarget = new Map();
|
|
for (const [id, cells] of area) {
|
|
const score = cells + (pop.get(id) || 0) * 16;
|
|
if (cells >= minArea || cityMunicipalities.has(id)) continue;
|
|
let bestNeighbor = -1;
|
|
let bestScore = -1;
|
|
for (const [key, border] of adjacency) {
|
|
const [a, b] = key.split(":").map(Number);
|
|
if (a !== id && b !== id) continue;
|
|
const other = a === id ? b : a;
|
|
const candidate = border * 3 + (area.get(other) || 0) * 0.012 + (pop.get(other) || 0) * 0.24;
|
|
if (candidate > bestScore) { bestScore = candidate; bestNeighbor = other; }
|
|
}
|
|
if (bestNeighbor >= 0 && (area.get(bestNeighbor) || 0) >= score * 0.35) mergeTarget.set(id, bestNeighbor);
|
|
}
|
|
if (mergeTarget.size === 0) return;
|
|
for (let i = 0; i < SIZE; i++) if (mergeTarget.has(adminId[i])) adminId[i] = mergeTarget.get(adminId[i]);
|
|
}
|
|
|
|
export function removeMunicipalExclaves(adminId, prefectureMask, sea, adminCenters = [], protectedPoints = [], maxIslandCells = 220) {
|
|
const protectedByAdmin = new Map();
|
|
for (const p of [...adminCenters, ...protectedPoints]) {
|
|
if (!p || !inside(p.x, p.y)) continue;
|
|
const id = adminId[indexOf(p.x, p.y)];
|
|
if (id < 0) continue;
|
|
if (!protectedByAdmin.has(id)) protectedByAdmin.set(id, new Set());
|
|
protectedByAdmin.get(id).add(indexOf(p.x, p.y));
|
|
}
|
|
|
|
const ids = new Set();
|
|
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && adminId[i] >= 0) ids.add(adminId[i]);
|
|
|
|
const globalSeen = new Uint8Array(SIZE);
|
|
const queue = [];
|
|
for (const id of ids) {
|
|
const components = [];
|
|
for (let i = 0; i < SIZE; i++) {
|
|
if (globalSeen[i] || adminId[i] !== id || !prefectureMask[i] || sea[i]) continue;
|
|
const comp = [];
|
|
let hasProtected = protectedByAdmin.get(id)?.has(i) || false;
|
|
queue.length = 0;
|
|
queue.push(i);
|
|
globalSeen[i] = 1;
|
|
for (let q = 0; q < queue.length; q++) {
|
|
const cur = queue[q];
|
|
comp.push(cur);
|
|
if (protectedByAdmin.get(id)?.has(cur)) hasProtected = true;
|
|
const [x, y] = xyOf(cur);
|
|
for (const [nx, ny] of neighbors4(x, y)) {
|
|
const ni = indexOf(nx, ny);
|
|
if (globalSeen[ni] || adminId[ni] !== id || !prefectureMask[ni] || sea[ni]) continue;
|
|
globalSeen[ni] = 1;
|
|
queue.push(ni);
|
|
}
|
|
}
|
|
components.push({ cells: comp, hasProtected });
|
|
}
|
|
if (components.length <= 1) continue;
|
|
components.sort((a, b) => (b.hasProtected ? 1000000 : 0) + b.cells.length - ((a.hasProtected ? 1000000 : 0) + a.cells.length));
|
|
for (const component of components.slice(1)) {
|
|
const mainSize = components[0].cells.length;
|
|
if (component.hasProtected && component.cells.length > maxIslandCells && component.cells.length > mainSize * 0.42) continue;
|
|
if (component.cells.length > maxIslandCells && component.cells.length > mainSize * 0.36) continue;
|
|
const counts = new Map();
|
|
for (const ci of component.cells) {
|
|
const [x, y] = xyOf(ci);
|
|
for (const [nx, ny] of neighbors4(x, y)) {
|
|
const ni = indexOf(nx, ny);
|
|
if (!prefectureMask[ni] || sea[ni]) continue;
|
|
const other = adminId[ni];
|
|
if (other >= 0 && other !== id) counts.set(other, (counts.get(other) || 0) + 1);
|
|
}
|
|
}
|
|
let target = -1;
|
|
let best = -1;
|
|
for (const [other, count] of counts) if (count > best) { best = count; target = other; }
|
|
if (target >= 0) for (const ci of component.cells) adminId[ci] = target;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse) {
|
|
const urbanPenalty = urbanBoundaryPenalty(i, populationDensity, landuse);
|
|
const crest = crestCrossingScore?.[i] || 0;
|
|
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 + crest * 1.38 + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72);
|
|
}
|
|
|
|
function urbanBoundaryPenalty(i, populationDensity, landuse) {
|
|
const lu = landuse[i];
|
|
const core = lu === 3 ? 1.45 : lu === 2 ? 1.12 : lu === 4 ? 0.95 : lu === 7 ? 0.90 : lu === 8 ? 0.64 : lu === 5 || lu === 6 ? 0.48 : 0;
|
|
return clamp(core + populationDensity[i] * 1.35);
|
|
}
|
|
|
|
function isAdminBoundaryCell(labels, prefectureMask, sea, x, y, useEight = true) {
|
|
const i = indexOf(x, y);
|
|
const own = labels[i];
|
|
if (!prefectureMask[i] || sea[i] || own < 0) return false;
|
|
const neighbors = useEight ? neighbors8(x, y) : neighbors4(x, y);
|
|
for (const [nx, ny] of neighbors) {
|
|
const ni = indexOf(nx, ny);
|
|
if (prefectureMask[ni] && !sea[ni] && labels[ni] >= 0 && labels[ni] !== own) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function buildBoundaryBand(labels, prefectureMask, sea, radius = 5) {
|
|
const band = new Uint8Array(SIZE);
|
|
for (let y = 1; y < MAP_H - 1; y++) {
|
|
for (let x = 1; x < MAP_W - 1; x++) {
|
|
if (!isAdminBoundaryCell(labels, prefectureMask, sea, x, y, true)) continue;
|
|
for (let dy = -radius; dy <= radius; dy++) {
|
|
for (let dx = -radius; dx <= radius; dx++) {
|
|
if (Math.hypot(dx, dy) > radius) continue;
|
|
const nx = x + dx;
|
|
const ny = y + dy;
|
|
if (!inside(nx, ny)) continue;
|
|
const ni = indexOf(nx, ny);
|
|
if (prefectureMask[ni] && !sea[ni]) band[ni] = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return band;
|
|
}
|
|
|
|
function buildAdminProtectedMask(adminId, prefectureMask, sea, adminCenters = [], protectedPoints = [], populationDensity, landuse) {
|
|
const protectedMask = new Uint8Array(SIZE);
|
|
function protectDisk(p, radius) {
|
|
if (!p || !inside(p.x, p.y)) return;
|
|
const owner = adminId[indexOf(p.x, p.y)];
|
|
if (owner < 0) return;
|
|
const r = Math.ceil(radius);
|
|
for (let dy = -r; dy <= r; dy++) {
|
|
for (let dx = -r; dx <= r; dx++) {
|
|
if (Math.hypot(dx, dy) > radius) continue;
|
|
const x = p.x + dx;
|
|
const y = p.y + dy;
|
|
if (!inside(x, y)) continue;
|
|
const i = indexOf(x, y);
|
|
if (prefectureMask[i] && !sea[i] && adminId[i] === owner) protectedMask[i] = 1;
|
|
}
|
|
}
|
|
}
|
|
for (const center of adminCenters) protectDisk(center, 2.2);
|
|
for (const p of protectedPoints || []) protectDisk(p, p.population ? clamp(1.6 + Math.sqrt(p.population) / 520, 2.1, 6.0) : p.portClass ? 2.0 : 1.7);
|
|
for (let i = 0; i < SIZE; i++) {
|
|
if (prefectureMask[i] && !sea[i] && (landuse[i] === 3 || populationDensity[i] > 0.72)) protectedMask[i] = 1;
|
|
}
|
|
return protectedMask;
|
|
}
|
|
|
|
function localBoundaryEnergy(labels, i, candidateId, targetScore, centerDist, populationDensity, landuse, river, valleyField) {
|
|
const [x, y] = xyOf(i);
|
|
const oldId = labels[i];
|
|
let energy = (centerDist[candidateId]?.[i] ?? 0) * 0.035;
|
|
let same4 = 0;
|
|
let diff4 = 0;
|
|
let diagDiff = 0;
|
|
|
|
for (const [nx, ny] of neighbors8(x, y)) {
|
|
const ni = indexOf(nx, ny);
|
|
const neighborId = labels[ni];
|
|
if (neighborId < 0) continue;
|
|
const isCardinal = nx === x || ny === y;
|
|
const differs = neighborId !== candidateId;
|
|
if (isCardinal) {
|
|
if (differs) {
|
|
diff4++;
|
|
const boundaryTarget = (targetScore[i] + targetScore[ni]) * 0.5;
|
|
const urbanCut = (urbanBoundaryPenalty(i, populationDensity, landuse) + urbanBoundaryPenalty(ni, populationDensity, landuse)) * 0.5;
|
|
const minorValley = (valleyField[i] + valleyField[ni]) * 0.5 > 0.34 && Math.max(river[i], river[ni]) < 0.30 ? 0.72 : 0;
|
|
const dHere = centerDist[candidateId]?.[i] ?? 99;
|
|
const dThere = centerDist[neighborId]?.[i] ?? 99;
|
|
const weakBisectorPenalty = Math.abs(dHere - dThere) < 4.0 && boundaryTarget < 0.42 ? 0.62 : 0;
|
|
energy += 2.15 - boundaryTarget * 1.55 + urbanCut * 3.0 + minorValley + weakBisectorPenalty;
|
|
} else same4++;
|
|
} else if (differs) diagDiff++;
|
|
}
|
|
|
|
if (same4 === 0) energy += 5.2;
|
|
if (same4 === 1) energy += 1.7;
|
|
if (diff4 >= 3 && targetScore[i] < 0.42) energy += 1.25;
|
|
if (diagDiff >= 3 && diff4 >= 2 && targetScore[i] < 0.50) energy += 0.42;
|
|
if (candidateId !== oldId && centerDist[candidateId] && centerDist[oldId]) {
|
|
const drift = centerDist[candidateId][i] - centerDist[oldId][i];
|
|
if (drift > 0) energy += Math.min(0.55, drift * 0.006);
|
|
}
|
|
return energy;
|
|
}
|
|
|
|
function buildCenterDistanceFields(adminIds, adminCenters, prefectureMask, sea) {
|
|
const fields = [];
|
|
for (const id of adminIds) {
|
|
const center = adminCenters[id];
|
|
const field = new Float32Array(SIZE);
|
|
if (!center || !inside(center.x, center.y) || sea[indexOf(center.x, center.y)] || !prefectureMask[indexOf(center.x, center.y)]) field.fill(24);
|
|
else {
|
|
for (let y = 0; y < MAP_H; y++) for (let x = 0; x < MAP_W; x++) field[indexOf(x, y)] = Math.hypot(x - center.x, y - center.y);
|
|
}
|
|
fields[id] = field;
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
function repairAdminTopology(adminId, prefectureMask, sea, adminCenters = [], targetScore = null, populationDensity = null, landuse = null) {
|
|
const ids = new Set();
|
|
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && adminId[i] >= 0) ids.add(adminId[i]);
|
|
const queue = [];
|
|
|
|
for (const id of ids) {
|
|
const seen = new Uint8Array(SIZE);
|
|
const components = [];
|
|
for (let i = 0; i < SIZE; i++) {
|
|
if (seen[i] || adminId[i] !== id || !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] || adminId[ni] !== id || !prefectureMask[ni] || sea[ni]) continue;
|
|
seen[ni] = 1;
|
|
queue.push(ni);
|
|
}
|
|
}
|
|
components.push(cells);
|
|
}
|
|
if (components.length <= 1) continue;
|
|
|
|
const centerIndex = adminCenters[id] && inside(adminCenters[id].x, adminCenters[id].y) ? indexOf(adminCenters[id].x, adminCenters[id].y) : -1;
|
|
let keepIndex = centerIndex >= 0 ? components.findIndex((cells) => cells.includes(centerIndex)) : -1;
|
|
if (keepIndex < 0) {
|
|
let bestSize = -1;
|
|
for (let c = 0; c < components.length; c++) if (components[c].length > bestSize) { bestSize = components[c].length; keepIndex = c; }
|
|
}
|
|
|
|
for (let c = 0; c < components.length; c++) {
|
|
if (c === keepIndex) continue;
|
|
const counts = new Map();
|
|
for (const ci of components[c]) {
|
|
const [x, y] = xyOf(ci);
|
|
for (const [nx, ny] of neighbors4(x, y)) {
|
|
const ni = indexOf(nx, ny);
|
|
if (!prefectureMask[ni] || sea[ni]) continue;
|
|
const other = adminId[ni];
|
|
if (other < 0 || other === id) continue;
|
|
const terrainFit = targetScore ? targetScore[ci] * 0.18 : 0;
|
|
const urbanFit = populationDensity && landuse ? (1 - urbanBoundaryPenalty(ci, populationDensity, landuse)) * 0.08 : 0;
|
|
counts.set(other, (counts.get(other) || 0) + 1 + terrainFit + urbanFit);
|
|
}
|
|
}
|
|
let target = -1;
|
|
let best = -1;
|
|
for (const [other, score] of counts) if (score > best) { best = score; target = other; }
|
|
if (target >= 0) for (const ci of components[c]) adminId[ci] = target;
|
|
}
|
|
}
|
|
}
|
|
|
|
function classifyLandscapeCell(i, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse) {
|
|
if (landuse[i] === 3 || populationDensity[i] > 0.70) return 1;
|
|
if ((landuse[i] >= 2 && landuse[i] <= 4) || landuse[i] === 7 || landuse[i] === 8 || populationDensity[i] > 0.24) return 2;
|
|
if (landuse[i] === 5 || landuse[i] === 6) return 3;
|
|
if ((river[i] > 0.50 && flowAccum[i] > 0.34) || flowAccum[i] > 0.68) return 4;
|
|
if (coastalLowland[i] > 0.42 && elevation[i] < 0.44) return 5;
|
|
if (basinField[i] > 0.38 && plain[i] > 0.26) return 6;
|
|
if (valleyField[i] > 0.42 && ridgeField[i] < 0.55) return 7;
|
|
if (ridgeField[i] > 0.54 || (ridgeField[i] > 0.40 && elevation[i] > 0.54)) return 8;
|
|
if (elevation[i] > 0.62 || slope[i] > 0.42) return 9;
|
|
if (landuse[i] === 0 || agriculture[i] > 0.45 || plain[i] > 0.48) return 10;
|
|
return 11;
|
|
}
|
|
|
|
function buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse) {
|
|
const unitId = new Int32Array(SIZE);
|
|
unitId.fill(-1);
|
|
const cellClass = new Int16Array(SIZE);
|
|
cellClass.fill(-1);
|
|
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) cellClass[i] = classifyLandscapeCell(i, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse);
|
|
|
|
const units = [];
|
|
const queue = [];
|
|
for (let i = 0; i < SIZE; i++) {
|
|
if (cellClass[i] < 0 || unitId[i] >= 0) continue;
|
|
const id = units.length;
|
|
const klass = cellClass[i];
|
|
const cells = [];
|
|
let sx = 0, sy = 0, pop = 0, urbanWeight = 0, ridgeExposure = 0, riverExposure = 0;
|
|
queue.length = 0;
|
|
queue.push(i);
|
|
unitId[i] = id;
|
|
for (let q = 0; q < queue.length; q++) {
|
|
const cur = queue[q];
|
|
const [x, y] = xyOf(cur);
|
|
cells.push(cur);
|
|
sx += x; sy += y; pop += populationDensity[cur];
|
|
urbanWeight += urbanBoundaryPenalty(cur, populationDensity, landuse);
|
|
ridgeExposure += ridgeField[cur];
|
|
riverExposure += river[cur] + flowAccum[cur] * 0.45;
|
|
for (const [nx, ny] of neighbors4(x, y)) {
|
|
const ni = indexOf(nx, ny);
|
|
if (unitId[ni] >= 0 || cellClass[ni] !== klass) continue;
|
|
unitId[ni] = id;
|
|
queue.push(ni);
|
|
}
|
|
}
|
|
units.push({ id, classId: klass, cells, area: cells.length, x: sx / cells.length, y: sy / cells.length, population: pop, urbanWeight: urbanWeight / cells.length, ridgeExposure: ridgeExposure / cells.length, riverExposure: riverExposure / cells.length, adjacent: new Map(), centerIds: [], owner: -1 });
|
|
}
|
|
|
|
const targetScore = new Float32Array(SIZE);
|
|
for (let i = 0; i < SIZE; i++) if (cellClass[i] >= 0) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse);
|
|
rebuildLandscapeUnitAdjacency(unitId, units, targetScore, prefectureMask, sea);
|
|
mergeTinyLandscapeUnits(unitId, units, 10);
|
|
rebuildLandscapeUnitAdjacency(unitId, units, targetScore, prefectureMask, sea);
|
|
return { unitId, units, targetScore };
|
|
}
|
|
|
|
function rebuildLandscapeUnitAdjacency(unitId, units, targetScore, prefectureMask, sea) {
|
|
for (const unit of units) unit.adjacent = new Map();
|
|
for (let y = 0; y < MAP_H; y++) {
|
|
for (let x = 0; x < MAP_W; x++) {
|
|
const i = indexOf(x, y);
|
|
if (!prefectureMask[i] || sea[i]) continue;
|
|
const a = unitId[i];
|
|
if (a < 0 || !units[a] || units[a].area === 0) continue;
|
|
for (const [nx, ny] of [[x + 1, y], [x, y + 1]]) {
|
|
if (!inside(nx, ny)) continue;
|
|
const ni = indexOf(nx, ny);
|
|
if (!prefectureMask[ni] || sea[ni]) continue;
|
|
const b = unitId[ni];
|
|
if (b < 0 || b === a || !units[b] || units[b].area === 0) continue;
|
|
const v = (targetScore[i] + targetScore[ni]) * 0.5;
|
|
const keyA = units[a].adjacent.get(b) || { count: 0, target: 0 };
|
|
keyA.count++; keyA.target += v; units[a].adjacent.set(b, keyA);
|
|
const keyB = units[b].adjacent.get(a) || { count: 0, target: 0 };
|
|
keyB.count++; keyB.target += v; units[b].adjacent.set(a, keyB);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function mergeTinyLandscapeUnits(unitId, units, minArea = 10) {
|
|
for (const unit of units) {
|
|
if (unit.area === 0 || unit.area >= minArea) continue;
|
|
let bestId = -1, bestScore = -INF;
|
|
for (const [otherId, edge] of unit.adjacent) {
|
|
const other = units[otherId];
|
|
if (!other || other.area === 0) continue;
|
|
const score = edge.count * 3 + (other.classId === unit.classId ? 8 : 0) + other.area * 0.01 - edge.target / Math.max(1, edge.count);
|
|
if (score > bestScore) { bestScore = score; bestId = otherId; }
|
|
}
|
|
if (bestId < 0) continue;
|
|
const target = units[bestId];
|
|
for (const i of unit.cells) { unitId[i] = bestId; target.cells.push(i); }
|
|
const totalArea = target.area + unit.area;
|
|
target.x = (target.x * target.area + unit.x * unit.area) / totalArea;
|
|
target.y = (target.y * target.area + unit.y * unit.area) / totalArea;
|
|
target.population += unit.population;
|
|
target.urbanWeight = (target.urbanWeight * target.area + unit.urbanWeight * unit.area) / totalArea;
|
|
target.ridgeExposure = (target.ridgeExposure * target.area + unit.ridgeExposure * unit.area) / totalArea;
|
|
target.riverExposure = (target.riverExposure * target.area + unit.riverExposure * unit.area) / totalArea;
|
|
target.area = totalArea;
|
|
unit.area = 0;
|
|
unit.cells = [];
|
|
}
|
|
}
|
|
|
|
function landscapeTransitionCost(a, b, edge) {
|
|
const boundaryTarget = edge.target / Math.max(1, edge.count);
|
|
const bothUrban = a.classId <= 3 && b.classId <= 3;
|
|
const bothCorridor = (a.classId === 5 || a.classId === 7 || a.classId === 10) && (b.classId === 5 || b.classId === 7 || b.classId === 10);
|
|
const urbanContinuity = bothUrban ? 2.1 : (a.urbanWeight + b.urbanWeight) > 0.75 && Math.abs(a.urbanWeight - b.urbanWeight) < 0.35 ? 0.9 : 0;
|
|
return Math.max(0.18, 0.70 + boundaryTarget * 4.2 + (a.classId === b.classId ? 0 : 0.75) + ((a.classId === 8 || b.classId === 8) ? 1.2 : 0) - urbanContinuity - (bothCorridor ? 0.55 : 0));
|
|
}
|
|
|
|
export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse, adminCenters = []) {
|
|
const { unitId, units, targetScore } = buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse);
|
|
if (units.length === 0) return;
|
|
for (let id = 0; id < adminCenters.length; id++) {
|
|
const center = adminCenters[id];
|
|
if (!center || !inside(center.x, center.y)) continue;
|
|
const unit = units[unitId[indexOf(center.x, center.y)]];
|
|
if (unit) unit.centerIds.push(id);
|
|
}
|
|
|
|
const owner = new Int16Array(units.length);
|
|
const dist = new Float32Array(units.length);
|
|
owner.fill(-1); dist.fill(INF);
|
|
const heap = new MinHeap();
|
|
for (const unit of units) {
|
|
if (unit.area === 0 || unit.centerIds.length === 0) continue;
|
|
const id = unit.centerIds[0];
|
|
owner[unit.id] = id; dist[unit.id] = 0; heap.push({ i: unit.id, f: 0 });
|
|
}
|
|
while (heap.length) {
|
|
const cur = heap.pop();
|
|
if (!cur || cur.f > dist[cur.i] + 1e-5) continue;
|
|
const unit = units[cur.i];
|
|
const currentOwner = owner[cur.i];
|
|
if (!unit || currentOwner < 0) continue;
|
|
for (const [nextId, edge] of unit.adjacent) {
|
|
const next = units[nextId];
|
|
if (!next || next.area === 0) continue;
|
|
const nextDist = dist[cur.i] + landscapeTransitionCost(unit, next, edge) + Math.sqrt(next.area) * 0.012 + (next.urbanWeight > 0.75 && next.centerIds.length === 0 ? -0.20 : 0);
|
|
if (nextDist < dist[nextId]) {
|
|
dist[nextId] = nextDist; owner[nextId] = currentOwner; heap.push({ i: nextId, f: nextDist });
|
|
}
|
|
}
|
|
}
|
|
for (const unit of units) {
|
|
const assigned = owner[unit.id];
|
|
if (assigned >= 0) for (const i of unit.cells) adminId[i] = assigned;
|
|
}
|
|
for (let id = 0; id < adminCenters.length; id++) {
|
|
const center = adminCenters[id];
|
|
if (!center || !inside(center.x, center.y)) continue;
|
|
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) {
|
|
if (Math.hypot(dx, dy) > 2) continue;
|
|
const x = center.x + dx, y = center.y + dy;
|
|
if (!inside(x, y)) continue;
|
|
const i = indexOf(x, y);
|
|
if (prefectureMask[i] && !sea[i]) adminId[i] = id;
|
|
}
|
|
}
|
|
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, targetScore, populationDensity, landuse);
|
|
}
|
|
|
|
export function snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse, adminCenters = [], protectedPoints = [], passes = 6) {
|
|
const targetScore = new Float32Array(SIZE);
|
|
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse);
|
|
const protectedMask = buildAdminProtectedMask(adminId, prefectureMask, sea, adminCenters, protectedPoints, populationDensity, landuse);
|
|
const band = buildBoundaryBand(adminId, prefectureMask, sea, 8);
|
|
const adminIds = [...new Set([...adminId].filter((id) => id >= 0))];
|
|
const centerDist = buildCenterDistanceFields(adminIds, adminCenters, prefectureMask, sea);
|
|
let current = new Int16Array(adminId);
|
|
|
|
for (let pass = 0; pass < passes; pass++) {
|
|
const next = new Int16Array(current);
|
|
let changed = 0;
|
|
for (let y = 1; y < MAP_H - 1; y++) {
|
|
for (let x = 1; x < MAP_W - 1; x++) {
|
|
const i = indexOf(x, y);
|
|
const own = current[i];
|
|
if (!band[i] || protectedMask[i] || !prefectureMask[i] || sea[i] || own < 0) continue;
|
|
if (!isAdminBoundaryCell(current, prefectureMask, sea, x, y, true)) continue;
|
|
const candidates = new Set();
|
|
for (const [nx, ny] of neighbors8(x, y)) {
|
|
const ni = indexOf(nx, ny);
|
|
if (prefectureMask[ni] && !sea[ni] && current[ni] >= 0 && current[ni] !== own) candidates.add(current[ni]);
|
|
}
|
|
if (candidates.size === 0) continue;
|
|
const currentEnergy = localBoundaryEnergy(current, i, own, targetScore, centerDist, populationDensity, landuse, river, valleyField);
|
|
let bestId = own, bestEnergy = currentEnergy;
|
|
for (const candidate of candidates) {
|
|
const candidateEnergy = localBoundaryEnergy(current, i, candidate, targetScore, centerDist, populationDensity, landuse, river, valleyField);
|
|
const threshold = 0.18 + (targetScore[i] < 0.36 ? 0.16 : 0) + urbanBoundaryPenalty(i, populationDensity, landuse) * 0.25;
|
|
if (bestEnergy - candidateEnergy > threshold) { bestEnergy = candidateEnergy; bestId = candidate; }
|
|
}
|
|
if (bestId !== own) { next[i] = bestId; changed++; }
|
|
}
|
|
}
|
|
current = next;
|
|
if (changed === 0) break;
|
|
}
|
|
adminId.set(current);
|
|
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, targetScore, populationDensity, landuse);
|
|
}
|