not good but not bad
This commit is contained in:
parent
5c82bfcab7
commit
4f0df3f6c5
11 changed files with 1284 additions and 622 deletions
859
mapAdminStage.js
859
mapAdminStage.js
|
|
@ -11,15 +11,6 @@ import {
|
|||
import { INF, MAP_H, MAP_W, SIZE, MinHeap, clamp, hash2, indexOf, inside, pickEntities, rand, xyOf } from "./mapUtils.js";
|
||||
import { extractAdminBorderSegments } from "./mapGeneratorHelpers.js";
|
||||
|
||||
const OUTER_ANCHOR_REGION_ID = -2;
|
||||
|
||||
function adminGenerationRegionIdAt(i, prefectureMask, prefectureRegionId) {
|
||||
if (prefectureMask[i]) return 0;
|
||||
const regionalId = prefectureRegionId?.[i] ?? -1;
|
||||
if (regionalId === 0) return OUTER_ANCHOR_REGION_ID;
|
||||
return regionalId;
|
||||
}
|
||||
|
||||
function changedCellsSince(before, after, prefectureMask, sea) {
|
||||
let changed = 0;
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && before[i] !== after[i]) changed++;
|
||||
|
|
@ -35,6 +26,326 @@ function municipalityAreaById(adminId, prefectureMask, sea) {
|
|||
return area;
|
||||
}
|
||||
|
||||
function maskLandArea(mask, sea) {
|
||||
let area = 0;
|
||||
for (let i = 0; i < SIZE; i++) if (mask[i] && !sea[i]) area++;
|
||||
return area;
|
||||
}
|
||||
|
||||
function compactWholeCompartmentMunicipalities(adminId, centers, prefectureMask, sea, fields = {}) {
|
||||
const activeIds = [...new Set([...adminId].filter((id, i) => id >= 0 && prefectureMask[i] && !sea[i]))].sort((a, b) => a - b);
|
||||
const idMap = new Map(activeIds.map((id, n) => [id, n]));
|
||||
const compactId = new Int16Array(SIZE);
|
||||
compactId.fill(-1);
|
||||
const stats = activeIds.map(() => ({ sx: 0, sy: 0, count: 0, bestI: -1, bestScore: -INF }));
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!prefectureMask[i] || sea[i] || adminId[i] < 0) continue;
|
||||
const nextId = idMap.get(adminId[i]);
|
||||
if (nextId === undefined) continue;
|
||||
compactId[i] = nextId;
|
||||
const [x, y] = xyOf(i);
|
||||
const row = stats[nextId];
|
||||
row.sx += x;
|
||||
row.sy += y;
|
||||
row.count++;
|
||||
const score = (fields.populationDensity?.[i] || 0) * 2.2 + (fields.plain?.[i] || 0) * 0.25 - (fields.slope?.[i] || 0) * 0.2;
|
||||
if (score > row.bestScore) { row.bestScore = score; row.bestI = i; }
|
||||
}
|
||||
const compactCenters = activeIds.map((oldId, newId) => {
|
||||
const current = centers[oldId];
|
||||
if (current && inside(current.x, current.y) && compactId[indexOf(current.x, current.y)] === newId) {
|
||||
return { ...current, originalAdminId: oldId };
|
||||
}
|
||||
const row = stats[newId];
|
||||
const fallback = row.bestI >= 0 ? xyOf(row.bestI) : [Math.round(row.sx / Math.max(1, row.count)), Math.round(row.sy / Math.max(1, row.count))];
|
||||
return {
|
||||
...(current || {}),
|
||||
x: fallback[0],
|
||||
y: fallback[1],
|
||||
originalAdminId: oldId,
|
||||
generatedOfficePoint: true,
|
||||
invisibleLowlandAdminSeed: current?.invisibleLowlandAdminSeed ?? true,
|
||||
seedKind: current?.seedKind || "compactedMunicipalityOffice",
|
||||
};
|
||||
});
|
||||
return { adminId: compactId, adminCentersRaw: compactCenters, activeMunicipalityCount: compactCenters.length };
|
||||
}
|
||||
|
||||
function enforceCompartmentMunicipalityOwnership(adminId, compartmentId, compartments, prefectureMask, sea) {
|
||||
if (!compartmentId || !compartments) return 0;
|
||||
let changed = 0;
|
||||
for (const comp of compartments) {
|
||||
if (!comp || !comp.cells?.length) continue;
|
||||
const counts = new Map();
|
||||
for (const i of comp.cells) {
|
||||
if (!prefectureMask[i] || sea[i]) continue;
|
||||
const id = adminId[i];
|
||||
if (id >= 0) counts.set(id, (counts.get(id) || 0) + 1);
|
||||
}
|
||||
let bestId = -1, bestCount = -1;
|
||||
for (const [id, count] of counts) {
|
||||
if (count > bestCount || (count === bestCount && id < bestId)) {
|
||||
bestId = id;
|
||||
bestCount = count;
|
||||
}
|
||||
}
|
||||
if (bestId < 0) continue;
|
||||
for (const i of comp.cells) {
|
||||
if (!prefectureMask[i] || sea[i] || adminId[i] === bestId) continue;
|
||||
adminId[i] = bestId;
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function buildMunicipalityGraph(adminId, prefectureMask, sea, naturalBarrierScore, populationDensity) {
|
||||
const nodes = new Map();
|
||||
const edges = new Map();
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!prefectureMask[i] || sea[i] || adminId[i] < 0) continue;
|
||||
const id = adminId[i];
|
||||
if (!nodes.has(id)) nodes.set(id, { id, area: 0, population: 0, sx: 0, sy: 0 });
|
||||
const node = nodes.get(id);
|
||||
const [x, y] = xyOf(i);
|
||||
node.area++;
|
||||
node.population += populationDensity?.[i] || 0;
|
||||
node.sx += x;
|
||||
node.sy += y;
|
||||
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] || adminId[ni] < 0 || adminId[ni] === id) continue;
|
||||
const a = Math.min(id, adminId[ni]);
|
||||
const b = Math.max(id, adminId[ni]);
|
||||
const key = `${a}:${b}`;
|
||||
const edge = edges.get(key) || { a, b, count: 0, barrier: 0 };
|
||||
edge.count++;
|
||||
edge.barrier += ((naturalBarrierScore?.[i] || 0) + (naturalBarrierScore?.[ni] || 0)) * 0.5;
|
||||
edges.set(key, edge);
|
||||
}
|
||||
}
|
||||
for (const node of nodes.values()) {
|
||||
node.x = node.sx / Math.max(1, node.area);
|
||||
node.y = node.sy / Math.max(1, node.area);
|
||||
node.adjacent = new Map();
|
||||
}
|
||||
for (const edge of edges.values()) {
|
||||
edge.barrier /= Math.max(1, edge.count);
|
||||
nodes.get(edge.a)?.adjacent.set(edge.b, edge);
|
||||
nodes.get(edge.b)?.adjacent.set(edge.a, edge);
|
||||
}
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
function choosePrefectureMunicipalitySeeds(nodes, seed) {
|
||||
const active = [...nodes.values()].filter((node) => node.area > 0).sort((a, b) => b.area - a.area || a.id - b.id);
|
||||
const totalArea = active.reduce((sum, node) => sum + node.area, 0);
|
||||
const targetCount = clamp(Math.round(totalArea / 2300), 5, 12);
|
||||
const seeds = [];
|
||||
const first = active.sort((a, b) => (b.population * 1.8 + b.area * 0.08) - (a.population * 1.8 + a.area * 0.08) || a.id - b.id)[0];
|
||||
if (first) seeds.push(first);
|
||||
while (seeds.length < targetCount) {
|
||||
let best = null, bestScore = -INF;
|
||||
for (const node of active) {
|
||||
if (seeds.includes(node)) continue;
|
||||
const nearest = Math.min(...seeds.map((s) => Math.hypot(node.x - s.x, node.y - s.y)));
|
||||
const score = nearest * (1.0 + hash2(seed, node.id) * 0.08) + Math.sqrt(node.area) * 0.12 + node.population * 0.28;
|
||||
if (score > bestScore) { bestScore = score; best = node; }
|
||||
}
|
||||
if (!best) break;
|
||||
seeds.push(best);
|
||||
}
|
||||
return seeds;
|
||||
}
|
||||
|
||||
function assignMunicipalitiesToPrefectures(nodes, seeds) {
|
||||
const owner = new Map();
|
||||
const area = new Map();
|
||||
const heap = new MinHeap();
|
||||
seeds.forEach((node, id) => {
|
||||
owner.set(node.id, id);
|
||||
area.set(id, node.area);
|
||||
heap.push({ i: node.id, id, f: 0 });
|
||||
});
|
||||
const totalArea = [...nodes.values()].reduce((sum, node) => sum + node.area, 0);
|
||||
const maxArea = Math.max(900, totalArea * 0.30);
|
||||
while (heap.length) {
|
||||
const cur = heap.pop();
|
||||
if (!cur || owner.get(cur.i) !== cur.id) continue;
|
||||
const node = nodes.get(cur.i);
|
||||
if (!node) continue;
|
||||
for (const [nextId, edge] of node.adjacent) {
|
||||
if (owner.has(nextId)) continue;
|
||||
const next = nodes.get(nextId);
|
||||
if (!next) continue;
|
||||
const areaPressure = Math.max(0, ((area.get(cur.id) || 0) + next.area - maxArea) / Math.max(1, maxArea));
|
||||
const cost = cur.f + 1.0 + edge.barrier * 5.5 + areaPressure * 14 + hash2(cur.id, nextId) * 0.05;
|
||||
owner.set(nextId, cur.id);
|
||||
area.set(cur.id, (area.get(cur.id) || 0) + next.area);
|
||||
heap.push({ i: nextId, id: cur.id, f: cost });
|
||||
}
|
||||
}
|
||||
let fallback = 0;
|
||||
for (const id of [...nodes.keys()].sort((a, b) => a - b)) {
|
||||
if (!owner.has(id)) owner.set(id, fallback++ % Math.max(1, seeds.length));
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
function repairPrefectureMunicipalityConnectivity(nodes, owner) {
|
||||
let changed = 0;
|
||||
for (let pass = 0; pass < 8; pass++) {
|
||||
let passChanged = 0;
|
||||
const prefIds = [...new Set(owner.values())].sort((a, b) => a - b);
|
||||
for (const prefId of prefIds) {
|
||||
const members = [...nodes.keys()].filter((id) => owner.get(id) === prefId);
|
||||
const memberSet = new Set(members);
|
||||
const seen = new Set();
|
||||
const components = [];
|
||||
for (const start of members) {
|
||||
if (seen.has(start)) continue;
|
||||
const queue = [start];
|
||||
const comp = [];
|
||||
seen.add(start);
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
comp.push(cur);
|
||||
for (const next of nodes.get(cur)?.adjacent.keys() || []) {
|
||||
if (!memberSet.has(next) || seen.has(next)) continue;
|
||||
seen.add(next);
|
||||
queue.push(next);
|
||||
}
|
||||
}
|
||||
components.push(comp);
|
||||
}
|
||||
if (components.length <= 1) continue;
|
||||
components.sort((a, b) => b.length - a.length);
|
||||
for (const comp of components.slice(1)) {
|
||||
const neighborCounts = new Map();
|
||||
for (const id of comp) {
|
||||
for (const next of nodes.get(id)?.adjacent.keys() || []) {
|
||||
const nOwner = owner.get(next);
|
||||
if (nOwner !== prefId) neighborCounts.set(nOwner, (neighborCounts.get(nOwner) || 0) + 1);
|
||||
}
|
||||
}
|
||||
let best = -1, bestCount = -1;
|
||||
for (const [id, count] of neighborCounts) if (count > bestCount || (count === bestCount && id < best)) { best = id; bestCount = count; }
|
||||
if (best < 0) continue;
|
||||
for (const id of comp) owner.set(id, best);
|
||||
passChanged += comp.length;
|
||||
}
|
||||
}
|
||||
changed += passChanged;
|
||||
if (!passChanged) break;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function mergeTinyMunicipalityPrefectures(nodes, owner) {
|
||||
let changed = 0;
|
||||
for (let pass = 0; pass < 6; pass++) {
|
||||
const areaByPref = new Map();
|
||||
for (const node of nodes.values()) {
|
||||
const pref = owner.get(node.id);
|
||||
areaByPref.set(pref, (areaByPref.get(pref) || 0) + node.area);
|
||||
}
|
||||
const totalArea = [...areaByPref.values()].reduce((sum, value) => sum + value, 0);
|
||||
const minArea = Math.max(520, totalArea / Math.max(1, areaByPref.size) * 0.34);
|
||||
const tiny = [...areaByPref.entries()]
|
||||
.filter(([, area]) => area > 0 && area < minArea && areaByPref.size > 4)
|
||||
.sort((a, b) => a[1] - b[1] || a[0] - b[0])[0];
|
||||
if (!tiny) break;
|
||||
const [tinyPref] = tiny;
|
||||
const neighborScores = new Map();
|
||||
for (const node of nodes.values()) {
|
||||
if (owner.get(node.id) !== tinyPref) continue;
|
||||
for (const [nextId, edge] of node.adjacent) {
|
||||
const nextPref = owner.get(nextId);
|
||||
if (nextPref === tinyPref || nextPref < 0) continue;
|
||||
const score = (neighborScores.get(nextPref) || 0) + edge.count * (0.6 + edge.barrier);
|
||||
neighborScores.set(nextPref, score);
|
||||
}
|
||||
}
|
||||
let best = -1, bestScore = -INF;
|
||||
for (const [pref, score] of neighborScores) {
|
||||
if (score > bestScore || (score === bestScore && pref < best)) { best = pref; bestScore = score; }
|
||||
}
|
||||
if (best < 0) break;
|
||||
for (const node of nodes.values()) if (owner.get(node.id) === tinyPref) { owner.set(node.id, best); changed++; }
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function extractPrefectureBordersFromMunicipalities(adminId, municipalityToPrefectureId, prefectureMask, sea) {
|
||||
const segments = [];
|
||||
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] || adminId[i] < 0) continue;
|
||||
const aPref = municipalityToPrefectureId[adminId[i]] ?? -1;
|
||||
if (x + 1 < MAP_W) {
|
||||
const ni = indexOf(x + 1, y);
|
||||
const bPref = !sea[ni] && adminId[ni] >= 0 ? municipalityToPrefectureId[adminId[ni]] ?? -1 : -1;
|
||||
if (prefectureMask[ni] && bPref >= 0 && aPref >= 0 && bPref !== aPref && adminId[ni] !== adminId[i]) segments.push([[x + 1, y], [x + 1, y + 1]]);
|
||||
}
|
||||
if (y + 1 < MAP_H) {
|
||||
const ni = indexOf(x, y + 1);
|
||||
const bPref = !sea[ni] && adminId[ni] >= 0 ? municipalityToPrefectureId[adminId[ni]] ?? -1 : -1;
|
||||
if (prefectureMask[ni] && bPref >= 0 && aPref >= 0 && bPref !== aPref && adminId[ni] !== adminId[i]) segments.push([[x, y + 1], [x + 1, y + 1]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
function generatePrefecturesFromMunicipalities(context, adminResult) {
|
||||
const { adminId } = adminResult;
|
||||
const { prefectureMask, sea, naturalBarrierScore, populationDensity, seed } = context;
|
||||
const graph = buildMunicipalityGraph(adminId, prefectureMask, sea, naturalBarrierScore, populationDensity);
|
||||
const seeds = choosePrefectureMunicipalitySeeds(graph.nodes, seed + 91001);
|
||||
const owner = assignMunicipalitiesToPrefectures(graph.nodes, seeds);
|
||||
const changedForTinyMerge = mergeTinyMunicipalityPrefectures(graph.nodes, owner);
|
||||
const changedForConnectivity = repairPrefectureMunicipalityConnectivity(graph.nodes, owner);
|
||||
const maxAdminId = Math.max(-1, ...[...graph.nodes.keys()]);
|
||||
const municipalityToPrefectureId = new Int16Array(maxAdminId + 1);
|
||||
municipalityToPrefectureId.fill(-1);
|
||||
for (const [admin, pref] of owner) municipalityToPrefectureId[admin] = pref;
|
||||
const prefectureRegionId = new Int16Array(SIZE);
|
||||
prefectureRegionId.fill(-1);
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!prefectureMask[i] || sea[i] || adminId[i] < 0) continue;
|
||||
prefectureRegionId[i] = municipalityToPrefectureId[adminId[i]] ?? -1;
|
||||
}
|
||||
const regionalPrefectureBorders = extractPrefectureBordersFromMunicipalities(adminId, municipalityToPrefectureId, prefectureMask, sea);
|
||||
const areaByPref = new Map();
|
||||
const popByPref = new Map();
|
||||
for (const node of graph.nodes.values()) {
|
||||
const pref = owner.get(node.id);
|
||||
areaByPref.set(pref, (areaByPref.get(pref) || 0) + node.area);
|
||||
popByPref.set(pref, (popByPref.get(pref) || 0) + node.population);
|
||||
}
|
||||
const totalArea = [...areaByPref.values()].reduce((sum, value) => sum + value, 0);
|
||||
return {
|
||||
prefectureRegionId,
|
||||
municipalityToPrefectureId,
|
||||
regionalPrefectureBorders,
|
||||
regionalDebug: {
|
||||
prefecturesGeneratedAfterMunicipalities: true,
|
||||
prefectureSource: "municipality-boundary-union",
|
||||
municipalityGraphNodeCount: graph.nodes.size,
|
||||
municipalityGraphEdgeCount: graph.edges.size,
|
||||
prefectureMunicipalitySeedCount: seeds.length,
|
||||
prefectureTinyMergeChangedMunicipalities: changedForTinyMerge,
|
||||
prefectureConnectivityRepairChangedMunicipalities: changedForConnectivity,
|
||||
finalRegionalMaxAreaShare: totalArea ? Math.max(0, ...areaByPref.values()) / totalArea : 0,
|
||||
finalRegionalTinyPrefectureCount: [...areaByPref.values()].filter((area) => area < 520).length,
|
||||
finalRegionalPrefectureBorderCount: regionalPrefectureBorders.length,
|
||||
regionalPrefectureBordersRebuiltFromFinalId: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isProtectedAdminSeed(seed) {
|
||||
if (!seed) return false;
|
||||
if (seed.seedKind === "capital" || seed.protectedCity?.isPrefecturalCapital) return true;
|
||||
|
|
@ -604,6 +915,82 @@ function expandSatelliteMunicipalityCatchment(adminId, satellite, targetAdmin, c
|
|||
return changed;
|
||||
}
|
||||
|
||||
function cityMinimumMunicipalityArea(city) {
|
||||
const populationArea = Math.sqrt(city.population || 0) * 0.72;
|
||||
const footprintArea = (city.urbanFootprintCells || 0) * 0.42;
|
||||
return clamp(95 + populationArea + footprintArea, 130, (city.population || 0) >= 450000 ? 780 : 520);
|
||||
}
|
||||
|
||||
function enforceCityMunicipalityCatchments(adminId, cities, context) {
|
||||
const { prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence } = context;
|
||||
const areaById = municipalityAreaById(adminId, prefectureMask, sea);
|
||||
let changed = 0;
|
||||
let protectedCities = 0;
|
||||
let tooSmall = 0;
|
||||
for (const city of cities || []) {
|
||||
if (!city || (city.population || 0) < 95000 || !inside(city.x, city.y)) continue;
|
||||
const start = indexOf(city.x, city.y);
|
||||
if (!prefectureMask[start] || sea[start]) continue;
|
||||
const targetAdmin = adminId[start];
|
||||
if (targetAdmin < 0) continue;
|
||||
protectedCities++;
|
||||
const minArea = cityMinimumMunicipalityArea(city);
|
||||
if ((areaById.get(targetAdmin) || 0) >= minArea) continue;
|
||||
tooSmall++;
|
||||
const heap = new MinHeap();
|
||||
const best = new Float32Array(SIZE);
|
||||
best.fill(INF);
|
||||
heap.push({ i: start, f: 0 });
|
||||
best[start] = 0;
|
||||
const claimed = [];
|
||||
const maxCost = (city.population || 0) >= 450000 ? 78 : 56;
|
||||
let projectedArea = areaById.get(targetAdmin) || 0;
|
||||
while (heap.length > 0 && projectedArea < minArea) {
|
||||
const cur = heap.pop();
|
||||
if (!cur || cur.f > best[cur.i] + 1e-5 || cur.f > maxCost) continue;
|
||||
const [x, y] = xyOf(cur.i);
|
||||
if (!prefectureMask[cur.i] || sea[cur.i]) continue;
|
||||
const d = Math.hypot(x - city.x, y - city.y);
|
||||
const compatible = d <= Math.max(5, (city.coreRadius || 3) * 2.0) ||
|
||||
[2, 3, 4, 7, 8].includes(landuse[cur.i]) ||
|
||||
populationDensity[cur.i] > 0.10 ||
|
||||
roadInfluence[cur.i] > 0.10 ||
|
||||
railInfluence2[cur.i] > 0.10 ||
|
||||
(stationInfluence?.[cur.i] || 0) > 0.10 ||
|
||||
valleyField[cur.i] > 0.22 ||
|
||||
basinField[cur.i] > 0.20 ||
|
||||
coastalLowland[cur.i] > 0.18;
|
||||
if (!compatible && claimed.length > minArea * 0.55) continue;
|
||||
claimed.push(cur.i);
|
||||
if (adminId[cur.i] !== targetAdmin) projectedArea++;
|
||||
for (const [dx, dy, step] of [[1, 0, 1], [-1, 0, 1], [0, 1, 1], [0, -1, 1], [1, 1, 1.41], [-1, 1, 1.41], [1, -1, 1.41], [-1, -1, 1.41]]) {
|
||||
const nx = x + dx, ny = y + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const majorBarrier = river[ni] > 0.62 || flowAccum[ni] > 0.74 || ridgeField[ni] > 0.70;
|
||||
const barrier = ridgeField[ni] * 5.4 + Math.max(0, elevation[ni] - 0.60) * 4.4 + slope[ni] * 2.6 + (majorBarrier ? 8.5 : river[ni] > 0.34 ? 2.6 : 0);
|
||||
const fit = ([2, 3, 4, 7, 8].includes(landuse[ni]) ? 2.4 : 0) + populationDensity[ni] * 2.2 + roadInfluence[ni] * 0.88 + railInfluence2[ni] * 0.92 + (stationInfluence?.[ni] || 0) * 1.15 + valleyField[ni] * 0.56 + basinField[ni] * 0.42 + coastalLowland[ni] * 0.34;
|
||||
const nd = cur.f + Math.max(0.30, 1.05 + barrier - fit + Math.hypot(nx - city.x, ny - city.y) / Math.max(8, (city.urbanRadius || 7) * 2.1)) * step;
|
||||
if (nd < best[ni]) {
|
||||
best[ni] = nd;
|
||||
heap.push({ i: ni, f: nd });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const i of claimed) {
|
||||
const old = adminId[i];
|
||||
if (old === targetAdmin) continue;
|
||||
if (old >= 0) areaById.set(old, Math.max(0, (areaById.get(old) || 0) - 1));
|
||||
adminId[i] = targetAdmin;
|
||||
areaById.set(targetAdmin, (areaById.get(targetAdmin) || 0) + 1);
|
||||
changed++;
|
||||
}
|
||||
city.municipalityMinArea = minArea;
|
||||
}
|
||||
return { changed, protectedCities, tooSmall };
|
||||
}
|
||||
|
||||
function generateAdminLayoutForMask({
|
||||
seed,
|
||||
prefectureMask,
|
||||
|
|
@ -635,6 +1022,8 @@ function generateAdminLayoutForMask({
|
|||
stations,
|
||||
industrialZones,
|
||||
logisticsParks,
|
||||
naturalCompartmentId,
|
||||
naturalCompartments,
|
||||
adminRegionMeta = {},
|
||||
adminProgress = null,
|
||||
}) {
|
||||
|
|
@ -680,9 +1069,45 @@ function generateAdminLayoutForMask({
|
|||
targetMunicipalityCount,
|
||||
targetCompartmentCount,
|
||||
maxNaturalCompartmentArea: Math.max(32, Math.round(maskLandArea(prefectureMask, sea) / Math.max(1, targetCompartmentCount) * 1.65)),
|
||||
naturalCompartmentId,
|
||||
naturalCompartments,
|
||||
naturalBarrierScore,
|
||||
progress: (step) => adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step }),
|
||||
});
|
||||
const adminId = compartmentAssignment.adminId;
|
||||
if (naturalCompartmentId && naturalCompartments) {
|
||||
const changedAfterFinalCompartmentOwnership = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea);
|
||||
const compacted = compactWholeCompartmentMunicipalities(adminId, adminCentersRaw, prefectureMask, sea, { populationDensity, plain, slope });
|
||||
const adminBorders = extractAdminBorderSegments(compacted.adminId, prefectureMask);
|
||||
const actualMunicipalityCount = compacted.activeMunicipalityCount;
|
||||
const adminDebug = {
|
||||
...compartmentAssignment.debug,
|
||||
sharedNaturalCompartmentLayer: true,
|
||||
skippedLegacyCellCleanupForHierarchy: true,
|
||||
targetMunicipalityCount,
|
||||
actualMunicipalityCount,
|
||||
finalMunicipalityCount: actualMunicipalityCount,
|
||||
candidateSeedCount: adminCentersRaw.length,
|
||||
municipalOfficePointCount: compacted.adminCentersRaw.length,
|
||||
targetNaturalCompartmentCount: targetCompartmentCount,
|
||||
naturalCompartmentCount: compartmentAssignment.debug?.naturalCompartmentCount || naturalCompartments.filter((unit) => unit && unit.area > 0).length,
|
||||
compartmentCount: compartmentAssignment.debug?.naturalCompartmentCount || naturalCompartments.filter((unit) => unit && unit.area > 0).length,
|
||||
changedAfterCompartmentAssignment: compartmentAssignment.debug?.naturalCompartmentCount || 0,
|
||||
changedAfterFinalCompartmentOwnership,
|
||||
finalTinyMunicipalityCount: [...municipalityAreaById(compacted.adminId, prefectureMask, sea).values()].filter((area) => area > 0 && area < 8).length,
|
||||
compartmentBorders: compartmentAssignment.debug?.compartmentBorders || [],
|
||||
borderNaturalBarrierAverage: compartmentAssignment.debug?.finalBorderNaturalBarrierAverage || 0,
|
||||
voronoiLikeRate: compartmentAssignment.debug?.voronoiLikeRateAfter || 0,
|
||||
};
|
||||
return {
|
||||
adminCentersRaw: compacted.adminCentersRaw,
|
||||
adminId: compacted.adminId,
|
||||
adminBorders,
|
||||
adminDebug,
|
||||
naturalCompartmentId: compartmentAssignment.compartmentId,
|
||||
naturalCompartments: compartmentAssignment.compartments,
|
||||
};
|
||||
}
|
||||
let previousSnapshot = new Int16Array(adminId);
|
||||
const adminDebug = {
|
||||
changedAfterSmooth: 0,
|
||||
|
|
@ -866,6 +1291,14 @@ function generateAdminLayoutForMask({
|
|||
landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence, modernCities,
|
||||
});
|
||||
}
|
||||
const cityCatchmentDebug = enforceCityMunicipalityCatchments(adminId, modernCities, {
|
||||
prefectureMask, sea, elevation, slope, river, ridgeField: boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum,
|
||||
landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence,
|
||||
});
|
||||
adminDebug.changedAfterCityMunicipalityCatchment = cityCatchmentDebug.changed;
|
||||
adminDebug.protectedCityMunicipalityCount = cityCatchmentDebug.protectedCities;
|
||||
adminDebug.tooSmallCityMunicipalityCountBeforeRepair = cityCatchmentDebug.tooSmall;
|
||||
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, flowAccum, populationDensity, landuse, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 2);
|
||||
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 260);
|
||||
const finalPromotionDebug = promotePendingSeedsForMunicipalityCount(adminId, prefectureMask, sea, {
|
||||
plain, agriculture, basinField, coastalLowland, valleyField, ridgeField: boundaryRidgeField, slope, elevation,
|
||||
|
|
@ -882,6 +1315,9 @@ function generateAdminLayoutForMask({
|
|||
}
|
||||
absorbSeedCompartments(adminId, compartmentAssignment.compartments, seedLifecycle);
|
||||
activeAdminIds = activeSeedIds(seedLifecycle);
|
||||
adminDebug.changedAfterFinalCompartmentOwnership = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea);
|
||||
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 220);
|
||||
adminDebug.changedAfterPostCompartmentExclaveRemoval = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea);
|
||||
|
||||
const areaById = municipalityAreaById(adminId, prefectureMask, sea);
|
||||
const satelliteAreas = [];
|
||||
|
|
@ -925,407 +1361,18 @@ function generateAdminLayoutForMask({
|
|||
const adminBorders = extractAdminBorderSegments(adminId, prefectureMask);
|
||||
|
||||
|
||||
return { adminCentersRaw, adminId, adminBorders, adminDebug };
|
||||
}
|
||||
|
||||
|
||||
function filterPointsForMask(points = [], mask, sea) {
|
||||
return (points || [])
|
||||
.filter((p) => p && inside(p.x, p.y) && mask[indexOf(p.x, p.y)] && !sea[indexOf(p.x, p.y)])
|
||||
.map((p) => ({ ...p }));
|
||||
}
|
||||
|
||||
function buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId) {
|
||||
const mask = new Uint8Array(SIZE);
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (sea[i]) continue;
|
||||
mask[i] = adminGenerationRegionIdAt(i, prefectureMask, prefectureRegionId) === regionId ? 1 : 0;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
function maskLandArea(mask, sea) {
|
||||
let area = 0;
|
||||
for (let i = 0; i < SIZE; i++) if (mask[i] && !sea[i]) area++;
|
||||
return area;
|
||||
}
|
||||
|
||||
function connectedMaskComponents(mask, sea, minArea = 1) {
|
||||
const seen = new Uint8Array(SIZE);
|
||||
const components = [];
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!mask[i] || sea[i] || seen[i]) continue;
|
||||
const queue = [i];
|
||||
const cells = [];
|
||||
seen[i] = 1;
|
||||
let head = 0;
|
||||
while (head < queue.length) {
|
||||
const cur = queue[head++];
|
||||
cells.push(cur);
|
||||
const [x, y] = xyOf(cur);
|
||||
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)) continue;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!mask[ni] || sea[ni] || seen[ni]) continue;
|
||||
seen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
if (cells.length >= minArea) {
|
||||
let minX = MAP_W, minY = MAP_H, maxX = 0, maxY = 0;
|
||||
for (const cell of cells) {
|
||||
const [x, y] = xyOf(cell);
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
components.push({ cells, area: cells.length, minX, minY, maxX, maxY });
|
||||
}
|
||||
}
|
||||
return components.sort((a, b) => b.area - a.area);
|
||||
}
|
||||
|
||||
function maskFromCells(cells) {
|
||||
const mask = new Uint8Array(SIZE);
|
||||
for (const i of cells || []) mask[i] = 1;
|
||||
return mask;
|
||||
}
|
||||
|
||||
|
||||
function splitDisconnectedAdminComponents(adminId, humanMask, sea, centers = [], fields = {}) {
|
||||
let nextId = Math.max(-1, ...adminId) + 1;
|
||||
let splitCount = 0;
|
||||
const ids = [...new Set([...adminId].filter((id, i) => id >= 0 && humanMask[i] && !sea[i]))];
|
||||
for (const id of ids) {
|
||||
const seen = new Uint8Array(SIZE);
|
||||
const components = [];
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (adminId[i] !== id || !humanMask[i] || sea[i] || seen[i]) continue;
|
||||
const cells = [];
|
||||
const queue = [i];
|
||||
seen[i] = 1;
|
||||
let head = 0;
|
||||
while (head < queue.length) {
|
||||
const cur = queue[head++];
|
||||
cells.push(cur);
|
||||
const [x, y] = xyOf(cur);
|
||||
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)) continue;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (adminId[ni] !== id || !humanMask[ni] || sea[ni] || seen[ni]) continue;
|
||||
seen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
components.push(cells);
|
||||
}
|
||||
if (components.length <= 1) continue;
|
||||
components.sort((a, b) => b.length - a.length);
|
||||
for (let c = 1; c < components.length; c++) {
|
||||
const newId = nextId++;
|
||||
let bestI = components[c][0];
|
||||
let bestScore = -INF;
|
||||
for (const i of components[c]) {
|
||||
const score =
|
||||
(fields.populationDensity?.[i] || 0) * 4.0 +
|
||||
(fields.stationInfluence?.[i] || 0) * 0.8 +
|
||||
(fields.roadInfluence?.[i] || 0) * 0.45 +
|
||||
(fields.settlementScore?.[i] || 0) * 0.6 +
|
||||
(fields.plain?.[i] || 0) * 0.2 -
|
||||
(fields.slope?.[i] || 0) * 0.2;
|
||||
if (score > bestScore) { bestScore = score; bestI = i; }
|
||||
}
|
||||
const [x, y] = xyOf(bestI);
|
||||
for (const i of components[c]) adminId[i] = newId;
|
||||
centers[newId] = { ...(centers[id] || {}), x, y, score: bestScore, seedKind: "splitDisconnectedMunicipality", generatedOfficePoint: true, municipalityOffice: true };
|
||||
splitCount++;
|
||||
}
|
||||
}
|
||||
return { adminId, centers, splitDisconnectedMunicipalityCount: splitCount };
|
||||
}
|
||||
|
||||
function compactAdminIdsAndCenters(adminId, humanMask, sea, centers = [], fields = {}) {
|
||||
const activeIds = [...new Set([...adminId].filter((id, i) => id >= 0 && humanMask[i] && !sea[i]))].sort((a, b) => a - b);
|
||||
const idMap = new Map(activeIds.map((oldId, newId) => [oldId, newId]));
|
||||
const newAdminId = new Int16Array(SIZE);
|
||||
newAdminId.fill(-1);
|
||||
const cellsByNewId = Array.from({ length: activeIds.length }, () => []);
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!humanMask[i] || sea[i]) continue;
|
||||
const newId = idMap.get(adminId[i]);
|
||||
if (newId === undefined) continue;
|
||||
newAdminId[i] = newId;
|
||||
cellsByNewId[newId].push(i);
|
||||
}
|
||||
|
||||
const chooseOffice = (newId, oldId) => {
|
||||
const cells = cellsByNewId[newId] || [];
|
||||
const current = centers[oldId];
|
||||
let currentValid = false;
|
||||
let currentScore = -INF;
|
||||
if (current && inside(current.x, current.y)) {
|
||||
const ci = indexOf(current.x, current.y);
|
||||
currentValid = newAdminId[ci] === newId && humanMask[ci] && !sea[ci];
|
||||
if (currentValid) currentScore = (fields.populationDensity?.[ci] || 0) + (fields.stationInfluence?.[ci] || 0) * 0.32 + (fields.roadInfluence?.[ci] || 0) * 0.20;
|
||||
}
|
||||
let sx = 0, sy = 0;
|
||||
for (const i of cells) {
|
||||
const [x, y] = xyOf(i);
|
||||
sx += x;
|
||||
sy += y;
|
||||
}
|
||||
const cx = cells.length ? sx / cells.length : current?.x || 0;
|
||||
const cy = cells.length ? sy / cells.length : current?.y || 0;
|
||||
let bestI = cells[0] ?? -1;
|
||||
let bestScore = -INF;
|
||||
for (const i of cells) {
|
||||
const [x, y] = xyOf(i);
|
||||
const land = fields.landuse?.[i] ?? 0;
|
||||
const urbanBonus = land === 3 ? 1.2 : land === 2 ? 1.0 : land === 4 || land === 7 || land === 8 ? 0.55 : land === 1 ? 0.24 : 0;
|
||||
const density = fields.populationDensity?.[i] || 0;
|
||||
const settlement = fields.settlementScore?.[i] || 0;
|
||||
const score =
|
||||
density * 4.20 +
|
||||
settlement * 0.72 +
|
||||
urbanBonus * 1.15 +
|
||||
(fields.plain?.[i] || 0) * 0.32 +
|
||||
(fields.basinField?.[i] || 0) * 0.24 +
|
||||
(fields.coastalLowland?.[i] || 0) * 0.18 +
|
||||
(fields.roadInfluence?.[i] || 0) * 0.48 +
|
||||
(fields.stationInfluence?.[i] || 0) * 0.86 -
|
||||
(fields.slope?.[i] || 0) * 0.52 -
|
||||
Math.hypot(x - cx, y - cy) * 0.018 +
|
||||
hash2(x, y, 91337 + newId) * 0.012;
|
||||
if (score > bestScore) { bestScore = score; bestI = i; }
|
||||
}
|
||||
if (currentValid && currentScore >= bestScore * 0.92 && (fields.populationDensity?.[indexOf(current.x, current.y)] || 0) >= 0.16) bestI = indexOf(current.x, current.y);
|
||||
const [bx, by] = bestI >= 0 ? xyOf(bestI) : [Math.round(cx), Math.round(cy)];
|
||||
return {
|
||||
...(current || {}),
|
||||
x: bx,
|
||||
y: by,
|
||||
score: bestScore > -INF ? bestScore : 0,
|
||||
seedKind: current?.seedKind || "generatedMunicipalOffice",
|
||||
invisibleLowlandAdminSeed: current?.invisibleLowlandAdminSeed ?? true,
|
||||
localAdminId: newId,
|
||||
oldAdminId: oldId,
|
||||
municipalityOffice: true,
|
||||
generatedOfficePoint: !current || !inside(current.x, current.y) || newAdminId[indexOf(current.x, current.y)] !== newId,
|
||||
};
|
||||
};
|
||||
|
||||
const adminCenters = activeIds.map((oldId, newId) => chooseOffice(newId, oldId));
|
||||
return {
|
||||
adminId: newAdminId,
|
||||
adminCenters,
|
||||
activeMunicipalityCount: activeIds.length,
|
||||
removedUnusedAdminCenterCount: Math.max(0, centers.length - activeIds.length),
|
||||
generatedOfficePointCount: adminCenters.filter((p) => p.generatedOfficePoint).length,
|
||||
adminCentersRaw,
|
||||
adminId,
|
||||
adminBorders,
|
||||
adminDebug,
|
||||
naturalCompartmentId: compartmentAssignment.compartmentId,
|
||||
naturalCompartments: compartmentAssignment.compartments,
|
||||
};
|
||||
}
|
||||
|
||||
function discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea) {
|
||||
const ids = new Set();
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (sea[i]) continue;
|
||||
const id = adminGenerationRegionIdAt(i, prefectureMask, prefectureRegionId);
|
||||
if (id >= 0 || id === OUTER_ANCHOR_REGION_ID) ids.add(id);
|
||||
}
|
||||
return [...ids].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
export function generateAdminLayout(context) {
|
||||
const { prefectureMask, prefectureRegionId, sea, populationDensity, plain, slope, adminProgress } = context;
|
||||
const minFullAdminRegionArea = 650;
|
||||
const minComponentArea = 18;
|
||||
const regionComponents = [];
|
||||
for (const regionId of discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea)) {
|
||||
const baseMask = buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId);
|
||||
const components = connectedMaskComponents(baseMask, sea, minComponentArea);
|
||||
components.forEach((component, componentIndex) => regionComponents.push({ regionId, componentIndex, component, area: component.area }));
|
||||
}
|
||||
const fullComponents = regionComponents.filter((row) => row.area >= minFullAdminRegionArea);
|
||||
|
||||
if (!prefectureRegionId || fullComponents.length <= 1) return generateAdminLayoutForMask(context);
|
||||
|
||||
let combinedAdminId = new Int16Array(SIZE);
|
||||
combinedAdminId.fill(-1);
|
||||
const combinedHumanMask = new Uint8Array(SIZE);
|
||||
let combinedCenters = [];
|
||||
const combinedCompartmentBorders = [];
|
||||
const perRegion = [];
|
||||
let idOffset = 0;
|
||||
|
||||
const processedCells = new Uint8Array(SIZE);
|
||||
for (const row of fullComponents) {
|
||||
const { regionId, componentIndex, component } = row;
|
||||
const regionMask = maskFromCells(component.cells);
|
||||
const regionArea = component.area;
|
||||
|
||||
const localContext = {
|
||||
...context,
|
||||
seed: (context.seed + regionId * 10007 + componentIndex * 9973) >>> 0,
|
||||
prefectureMask: regionMask,
|
||||
modernCities: filterPointsForMask(context.modernCities, regionMask, sea),
|
||||
satelliteCities: filterPointsForMask(context.satelliteCities, regionMask, sea),
|
||||
newTowns: filterPointsForMask(context.newTowns, regionMask, sea),
|
||||
markets: filterPointsForMask(context.markets, regionMask, sea),
|
||||
villages: filterPointsForMask(context.villages, regionMask, sea),
|
||||
ports: filterPointsForMask(context.ports, regionMask, sea),
|
||||
stations: filterPointsForMask(context.stations, regionMask, sea),
|
||||
industrialZones: filterPointsForMask(context.industrialZones, regionMask, sea),
|
||||
logisticsParks: filterPointsForMask(context.logisticsParks, regionMask, sea),
|
||||
adminRegionMeta: {
|
||||
regionId: `${regionId}:${componentIndex}`,
|
||||
sourceRegionId: regionId,
|
||||
componentIndex,
|
||||
landArea: regionArea,
|
||||
isFocusedRegion: regionId === 0,
|
||||
isOuterAnchorRegion: regionId === OUTER_ANCHOR_REGION_ID,
|
||||
},
|
||||
adminProgress,
|
||||
};
|
||||
|
||||
adminProgress?.({ status: "region-start", regionId, area: regionArea });
|
||||
const local = generateAdminLayoutForMask(localContext);
|
||||
adminProgress?.({ status: "region-done", regionId, area: regionArea, municipalities: local.adminDebug?.finalMunicipalityCount || local.adminDebug?.actualMunicipalityCount || 0 });
|
||||
if (local.adminDebug?.compartmentBorders?.length) combinedCompartmentBorders.push(...local.adminDebug.compartmentBorders);
|
||||
let localMaxAdminId = -1;
|
||||
for (let i = 0; i < SIZE; i++) if (regionMask[i] && !sea[i] && (local.adminId?.[i] ?? -1) > localMaxAdminId) localMaxAdminId = local.adminId[i];
|
||||
const localSlotCount = Math.max(local.adminCentersRaw?.length || 0, localMaxAdminId + 1);
|
||||
const localCenters = [];
|
||||
for (let localAdminId = 0; localAdminId < localSlotCount; localAdminId++) {
|
||||
let center = local.adminCentersRaw?.[localAdminId];
|
||||
if (!center) {
|
||||
let sx = 0, sy = 0, count = 0, bestI = -1, bestScore = -INF;
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!regionMask[i] || sea[i] || local.adminId?.[i] !== localAdminId) continue;
|
||||
const [x, y] = xyOf(i);
|
||||
sx += x;
|
||||
sy += y;
|
||||
count++;
|
||||
const score = (populationDensity?.[i] || 0) + (plain?.[i] || 0) * 0.2 - (slope?.[i] || 0) * 0.2;
|
||||
if (score > bestScore) { bestScore = score; bestI = i; }
|
||||
}
|
||||
if (count && bestI >= 0) {
|
||||
const [bx, by] = xyOf(bestI);
|
||||
center = { x: bx, y: by, score: bestScore, seedKind: "generatedAdminSlot", invisibleLowlandAdminSeed: true };
|
||||
}
|
||||
}
|
||||
if (!center) center = { x: 0, y: 0, score: 0, seedKind: "emptyAdminSlot", invisibleLowlandAdminSeed: true };
|
||||
localCenters.push({
|
||||
...center,
|
||||
regionId,
|
||||
localAdminId,
|
||||
adminIdOffset: idOffset,
|
||||
});
|
||||
}
|
||||
combinedCenters.push(...localCenters);
|
||||
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!regionMask[i] || sea[i]) continue;
|
||||
combinedHumanMask[i] = 1;
|
||||
processedCells[i] = 1;
|
||||
const localId = local.adminId?.[i] ?? -1;
|
||||
if (localId >= 0) combinedAdminId[i] = localId + idOffset;
|
||||
}
|
||||
|
||||
perRegion.push({
|
||||
regionId,
|
||||
componentIndex,
|
||||
area: regionArea,
|
||||
centerCount: localCenters.length,
|
||||
municipalityCount: local.adminDebug?.finalMunicipalityCount || local.adminDebug?.actualMunicipalityCount || new Set([...local.adminId].filter((id, i) => id >= 0 && regionMask[i] && !sea[i])).size,
|
||||
naturalCompartmentCount: local.adminDebug?.naturalCompartmentCount || 0,
|
||||
targetNaturalCompartmentCount: local.adminDebug?.targetNaturalCompartmentCount || 0,
|
||||
averageCompartmentArea: local.adminDebug?.averageCompartmentArea || 0,
|
||||
maxCompartmentArea: local.adminDebug?.maxCompartmentArea || 0,
|
||||
maxCompartmentElongation: local.adminDebug?.maxCompartmentElongation || 1,
|
||||
worstNaturalCompartments: local.adminDebug?.worstNaturalCompartments || [],
|
||||
singleCompartmentMunicipalityRatio: local.adminDebug?.singleCompartmentMunicipalityRatio || 0,
|
||||
});
|
||||
idOffset += localSlotCount;
|
||||
}
|
||||
|
||||
const leftoverRows = [];
|
||||
for (const row of regionComponents) {
|
||||
const cells = row.component.cells.filter((i) => !sea[i] && combinedAdminId[i] < 0);
|
||||
if (cells.length) leftoverRows.push({ ...row, cells });
|
||||
}
|
||||
for (const row of leftoverRows) {
|
||||
const { regionId, componentIndex, cells } = row;
|
||||
let bestI = cells[0], bestScore = -INF;
|
||||
for (const i of cells) {
|
||||
const score = (populationDensity?.[i] || 0) * 2.6 + (plain?.[i] || 0) * 0.22 - (slope?.[i] || 0) * 0.20;
|
||||
if (score > bestScore) { bestScore = score; bestI = i; }
|
||||
}
|
||||
const [cx, cy] = xyOf(bestI);
|
||||
const id = combinedCenters.length;
|
||||
combinedCenters.push({ x: cx, y: cy, score: bestScore, regionId, componentIndex, localAdminId: 0, seedKind: "tinyRegionAdminSeed", invisibleLowlandAdminSeed: true });
|
||||
for (const i of cells) {
|
||||
combinedHumanMask[i] = 1;
|
||||
combinedAdminId[i] = id;
|
||||
}
|
||||
perRegion.push({ regionId, componentIndex, area: cells.length, centerCount: 1, municipalityCount: 1, naturalCompartmentCount: 1, targetNaturalCompartmentCount: 1, averageCompartmentArea: cells.length, maxCompartmentArea: cells.length, maxCompartmentElongation: 1, singleCompartmentMunicipalityRatio: 1, tinyRegionFallback: true });
|
||||
}
|
||||
|
||||
const compactFields = {
|
||||
populationDensity,
|
||||
plain,
|
||||
slope,
|
||||
settlementScore: context.settlementScore,
|
||||
landuse: context.landuse,
|
||||
basinField: context.basinField,
|
||||
coastalLowland: context.coastalLowland,
|
||||
roadInfluence: context.roadInfluence,
|
||||
stationInfluence: context.stationInfluence,
|
||||
};
|
||||
let compactedAdmin = compactAdminIdsAndCenters(combinedAdminId, combinedHumanMask, sea, combinedCenters, compactFields);
|
||||
combinedAdminId = compactedAdmin.adminId;
|
||||
combinedCenters = compactedAdmin.adminCenters;
|
||||
const splitDisconnected = splitDisconnectedAdminComponents(combinedAdminId, combinedHumanMask, sea, combinedCenters, compactFields);
|
||||
combinedAdminId = splitDisconnected.adminId;
|
||||
combinedCenters = splitDisconnected.centers;
|
||||
compactedAdmin = compactAdminIdsAndCenters(combinedAdminId, combinedHumanMask, sea, combinedCenters, compactFields);
|
||||
combinedAdminId = compactedAdmin.adminId;
|
||||
combinedCenters = compactedAdmin.adminCenters;
|
||||
const adminBorders = extractAdminBorderSegments(combinedAdminId, combinedHumanMask);
|
||||
const totalMunicipalityCount = compactedAdmin.activeMunicipalityCount;
|
||||
const totalNaturalCompartmentCount = perRegion.reduce((sum, row) => sum + (row.naturalCompartmentCount || 0), 0);
|
||||
const totalTargetNaturalCompartmentCount = perRegion.reduce((sum, row) => sum + (row.targetNaturalCompartmentCount || 0), 0);
|
||||
const weightedCompartmentArea = perRegion.reduce((sum, row) => sum + (row.averageCompartmentArea || 0) * (row.naturalCompartmentCount || 0), 0);
|
||||
const weightedSingleRatio = perRegion.reduce((sum, row) => sum + (row.singleCompartmentMunicipalityRatio || 0) * (row.municipalityCount || 0), 0);
|
||||
const adminDebug = {
|
||||
multiRegionAdmin: true,
|
||||
adminRegionCount: perRegion.length,
|
||||
minFullAdminRegionArea,
|
||||
minComponentArea,
|
||||
connectedComponentAdmin: true,
|
||||
fullComponentCount: fullComponents.length,
|
||||
leftoverComponentCount: leftoverRows.length,
|
||||
splitDisconnectedMunicipalityCount: splitDisconnected.splitDisconnectedMunicipalityCount,
|
||||
perRegion,
|
||||
finalMunicipalityCount: totalMunicipalityCount,
|
||||
actualMunicipalityCount: totalMunicipalityCount,
|
||||
candidateSeedCount: combinedCenters.length,
|
||||
municipalOfficePointCount: combinedCenters.length,
|
||||
generatedOfficePointCount: compactedAdmin.generatedOfficePointCount,
|
||||
removedUnusedAdminCenterCount: compactedAdmin.removedUnusedAdminCenterCount,
|
||||
naturalCompartmentCount: totalNaturalCompartmentCount,
|
||||
compartmentCount: totalNaturalCompartmentCount,
|
||||
targetNaturalCompartmentCount: totalTargetNaturalCompartmentCount,
|
||||
averageCompartmentArea: totalNaturalCompartmentCount ? weightedCompartmentArea / totalNaturalCompartmentCount : 0,
|
||||
maxCompartmentArea: Math.max(0, ...perRegion.map((row) => row.maxCompartmentArea || 0)),
|
||||
maxCompartmentElongation: Math.max(1, ...perRegion.map((row) => row.maxCompartmentElongation || 1)),
|
||||
averageCompartmentsPerMunicipality: totalMunicipalityCount ? totalNaturalCompartmentCount / totalMunicipalityCount : 0,
|
||||
singleCompartmentMunicipalityRatio: totalMunicipalityCount ? weightedSingleRatio / totalMunicipalityCount : 0,
|
||||
compartmentBorders: combinedCompartmentBorders,
|
||||
};
|
||||
|
||||
return { adminCentersRaw: combinedCenters, adminId: combinedAdminId, adminBorders, adminDebug };
|
||||
const layout = generateAdminLayoutForMask(context);
|
||||
return { ...layout, ...generatePrefecturesFromMunicipalities(context, layout) };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue