tweak
This commit is contained in:
parent
d762a88b22
commit
5c82bfcab7
13 changed files with 1028 additions and 8801 deletions
190
mapAdminStage.js
190
mapAdminStage.js
|
|
@ -950,6 +950,104 @@ function maskLandArea(mask, sea) {
|
|||
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);
|
||||
|
|
@ -968,11 +1066,12 @@ function compactAdminIdsAndCenters(adminId, humanMask, sea, centers = [], fields
|
|||
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);
|
||||
if (newAdminId[ci] === newId && humanMask[ci] && !sea[ci]) {
|
||||
return { ...current, localAdminId: newId, oldAdminId: oldId, municipalityOffice: true };
|
||||
}
|
||||
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) {
|
||||
|
|
@ -991,19 +1090,20 @@ function compactAdminIdsAndCenters(adminId, humanMask, sea, centers = [], fields
|
|||
const density = fields.populationDensity?.[i] || 0;
|
||||
const settlement = fields.settlementScore?.[i] || 0;
|
||||
const score =
|
||||
density * 2.25 +
|
||||
settlement * 0.75 +
|
||||
urbanBonus +
|
||||
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.34 +
|
||||
(fields.stationInfluence?.[i] || 0) * 0.45 -
|
||||
(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 || {}),
|
||||
|
|
@ -1041,11 +1141,17 @@ function discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea) {
|
|||
|
||||
export function generateAdminLayout(context) {
|
||||
const { prefectureMask, prefectureRegionId, sea, populationDensity, plain, slope, adminProgress } = context;
|
||||
const minFullAdminRegionArea = 1500;
|
||||
const regionIds = discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea)
|
||||
.filter((regionId) => regionId !== OUTER_ANCHOR_REGION_ID && maskLandArea(buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId), sea) >= minFullAdminRegionArea);
|
||||
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 || regionIds.length <= 1) return generateAdminLayoutForMask(context);
|
||||
if (!prefectureRegionId || fullComponents.length <= 1) return generateAdminLayoutForMask(context);
|
||||
|
||||
let combinedAdminId = new Int16Array(SIZE);
|
||||
combinedAdminId.fill(-1);
|
||||
|
|
@ -1055,14 +1161,15 @@ export function generateAdminLayout(context) {
|
|||
const perRegion = [];
|
||||
let idOffset = 0;
|
||||
|
||||
for (const regionId of regionIds) {
|
||||
const regionMask = buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId);
|
||||
const regionArea = maskLandArea(regionMask, sea);
|
||||
if (regionArea < minFullAdminRegionArea) continue;
|
||||
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) >>> 0,
|
||||
seed: (context.seed + regionId * 10007 + componentIndex * 9973) >>> 0,
|
||||
prefectureMask: regionMask,
|
||||
modernCities: filterPointsForMask(context.modernCities, regionMask, sea),
|
||||
satelliteCities: filterPointsForMask(context.satelliteCities, regionMask, sea),
|
||||
|
|
@ -1074,9 +1181,11 @@ export function generateAdminLayout(context) {
|
|||
industrialZones: filterPointsForMask(context.industrialZones, regionMask, sea),
|
||||
logisticsParks: filterPointsForMask(context.logisticsParks, regionMask, sea),
|
||||
adminRegionMeta: {
|
||||
regionId,
|
||||
regionId: `${regionId}:${componentIndex}`,
|
||||
sourceRegionId: regionId,
|
||||
componentIndex,
|
||||
landArea: regionArea,
|
||||
isFocusedRegion: true,
|
||||
isFocusedRegion: regionId === 0,
|
||||
isOuterAnchorRegion: regionId === OUTER_ANCHOR_REGION_ID,
|
||||
},
|
||||
adminProgress,
|
||||
|
|
@ -1121,12 +1230,14 @@ export function generateAdminLayout(context) {
|
|||
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,
|
||||
|
|
@ -1141,34 +1252,29 @@ export function generateAdminLayout(context) {
|
|||
idOffset += localSlotCount;
|
||||
}
|
||||
|
||||
const leftoverByRegion = new Map();
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (sea[i]) continue;
|
||||
const regionId = adminGenerationRegionIdAt(i, prefectureMask, prefectureRegionId);
|
||||
if ((regionId < 0 && regionId !== OUTER_ANCHOR_REGION_ID) || combinedAdminId[i] >= 0) continue;
|
||||
if (!leftoverByRegion.has(regionId)) leftoverByRegion.set(regionId, []);
|
||||
leftoverByRegion.get(regionId).push(i);
|
||||
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 [regionId, cells] of leftoverByRegion) {
|
||||
let sx = 0, sy = 0, bestI = cells[0], bestScore = -INF;
|
||||
for (const row of leftoverRows) {
|
||||
const { regionId, componentIndex, cells } = row;
|
||||
let bestI = cells[0], bestScore = -INF;
|
||||
for (const i of cells) {
|
||||
const [x, y] = xyOf(i);
|
||||
sx += x;
|
||||
sy += y;
|
||||
const score = (populationDensity?.[i] || 0) + (plain?.[i] || 0) * 0.2 - (slope?.[i] || 0) * 0.2;
|
||||
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, localAdminId: 0, seedKind: "tinyRegionAdminSeed", invisibleLowlandAdminSeed: true });
|
||||
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, area: cells.length, centerCount: 1, municipalityCount: 1, naturalCompartmentCount: 1, targetNaturalCompartmentCount: 1, averageCompartmentArea: cells.length, maxCompartmentArea: cells.length, maxCompartmentElongation: 1, singleCompartmentMunicipalityRatio: 1, tinyRegionFallback: true });
|
||||
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 compactedAdmin = compactAdminIdsAndCenters(combinedAdminId, combinedHumanMask, sea, combinedCenters, {
|
||||
const compactFields = {
|
||||
populationDensity,
|
||||
plain,
|
||||
slope,
|
||||
|
|
@ -1178,7 +1284,14 @@ export function generateAdminLayout(context) {
|
|||
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);
|
||||
|
|
@ -1191,6 +1304,11 @@ export function generateAdminLayout(context) {
|
|||
multiRegionAdmin: true,
|
||||
adminRegionCount: perRegion.length,
|
||||
minFullAdminRegionArea,
|
||||
minComponentArea,
|
||||
connectedComponentAdmin: true,
|
||||
fullComponentCount: fullComponents.length,
|
||||
leftoverComponentCount: leftoverRows.length,
|
||||
splitDisconnectedMunicipalityCount: splitDisconnected.splitDisconnectedMunicipalityCount,
|
||||
perRegion,
|
||||
finalMunicipalityCount: totalMunicipalityCount,
|
||||
actualMunicipalityCount: totalMunicipalityCount,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue