stable
This commit is contained in:
parent
d2e7a80e72
commit
860471f805
13 changed files with 1643 additions and 356 deletions
|
|
@ -302,6 +302,101 @@ export function removeMunicipalExclaves(adminId, prefectureMask, sea, adminCente
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function enforceMunicipalityConnectivityStrict(adminId, prefectureMask, sea, adminCenters = [], protectedPoints = [], maxPasses = 6) {
|
||||
// Final cell-level invariant: each municipality should be one contiguous land
|
||||
// component. Earlier stages are allowed to leave sizeable satellite pieces
|
||||
// while boundaries are still being snapped; this pass removes the remaining
|
||||
// visual exclaves by attaching every non-primary component to the neighboring
|
||||
// municipality with the largest shared boundary. A component that contains a
|
||||
// protected point may become the primary component, but it no longer protects
|
||||
// additional detached pieces.
|
||||
const protectedByAdmin = new Map();
|
||||
for (const p of [...(adminCenters || []), ...(protectedPoints || [])]) {
|
||||
if (!p || !inside(p.x, p.y)) continue;
|
||||
const i = indexOf(Math.round(p.x), Math.round(p.y));
|
||||
const id = adminId[i];
|
||||
if (id < 0) continue;
|
||||
if (!protectedByAdmin.has(id)) protectedByAdmin.set(id, new Set());
|
||||
protectedByAdmin.get(id).add(i);
|
||||
}
|
||||
|
||||
let changed = 0;
|
||||
const queue = [];
|
||||
for (let pass = 0; pass < maxPasses; pass++) {
|
||||
let passChanged = 0;
|
||||
const ids = new Set();
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && adminId[i] >= 0) ids.add(adminId[i]);
|
||||
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 comp = [];
|
||||
let protectedHits = 0;
|
||||
queue.length = 0;
|
||||
queue.push(i);
|
||||
seen[i] = 1;
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
comp.push(cur);
|
||||
if (protectedByAdmin.get(id)?.has(cur)) protectedHits++;
|
||||
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: comp, protectedHits });
|
||||
}
|
||||
if (components.length <= 1) continue;
|
||||
components.sort((a, b) =>
|
||||
(b.protectedHits ? 1_000_000 : 0) + b.cells.length -
|
||||
((a.protectedHits ? 1_000_000 : 0) + a.cells.length)
|
||||
);
|
||||
const primary = components[0];
|
||||
for (const component of components.slice(1)) {
|
||||
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) {
|
||||
const bonus = protectedByAdmin.get(other)?.size ? 0.25 : 0;
|
||||
const score = count + bonus;
|
||||
if (score > best || (score === best && other < target)) { best = score; target = other; }
|
||||
}
|
||||
if (target < 0) {
|
||||
// Very rare: a detached island component has no labeled neighbor.
|
||||
// Keep the largest/protected primary and merge the component into it
|
||||
// only if it is directly adjacent after previous changes; otherwise
|
||||
// leave it for the next pass rather than inventing over-sea ownership.
|
||||
target = id;
|
||||
}
|
||||
if (target >= 0 && target !== id) {
|
||||
for (const ci of component.cells) adminId[ci] = target;
|
||||
passChanged += component.cells.length;
|
||||
} else if (component !== primary) {
|
||||
// If no external target exists, still mark it as handled by keeping it;
|
||||
// another pass may expose a target after surrounding cells change.
|
||||
}
|
||||
}
|
||||
}
|
||||
changed += passChanged;
|
||||
if (!passChanged) break;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
export function terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse) {
|
||||
const urbanPenalty = urbanBoundaryPenalty(i, populationDensity, landuse);
|
||||
const majorRiver = clamp(Math.max(river[i] - 0.32, 0) * 1.9 + Math.max(flowAccum[i] - 0.38, 0) * 0.75);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue