stable
This commit is contained in:
parent
d2e7a80e72
commit
860471f805
13 changed files with 1643 additions and 356 deletions
363
mapOutput.js
363
mapOutput.js
|
|
@ -177,8 +177,9 @@ function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCiti
|
|||
}
|
||||
target.isPrefecturalCapital = true;
|
||||
target.isRegionalCapital = true;
|
||||
target.rank = target.rank || "Regional Capital";
|
||||
target.kind = target.kind === "Market Town" || target.kind === "Port Town" || target.kind === "Valley Market Town" ? "Regional Capital" : (target.kind || "Regional Capital");
|
||||
target.rank = "Prefectural Capital";
|
||||
target.kind = "Prefectural Capital";
|
||||
target.labelPriorityBase = Math.max(target.labelPriorityBase || 0, 1150);
|
||||
target.urbanRadius = clamp(6.2 + Math.sqrt(target.population) / 80, 9, 30);
|
||||
target.coreRadius = clamp(2.0 + Math.sqrt(target.population) / 390, 2.4, 6.4);
|
||||
target.sprawlRadius = clamp(target.urbanRadius * 1.34, target.urbanRadius + 2, 38);
|
||||
|
|
@ -210,8 +211,9 @@ function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames
|
|||
const prefId = prefectureRegionId[indexOf(city.x, city.y)];
|
||||
if (prefId < 0) continue;
|
||||
const current = capitalNameByPref.get(prefId);
|
||||
const score = (city.isPrefecturalCapital ? 2_000_000 : 0) + (city.isRegionalCapital ? 500_000 : 0) + (city.population || 0);
|
||||
if (!current || score > current.score) capitalNameByPref.set(prefId, { name: stripMunicipalSuffix(city.name), score, x: city.x, y: city.y, population: city.population || 0, source: "city" });
|
||||
const tier = city.isPrefecturalCapital || city.rank === "Prefectural Capital" || city.kind === "Prefectural Capital" ? 3 : city.isRegionalCapital || city.rank === "Regional Capital" || city.kind === "Regional Capital" ? 2 : 1;
|
||||
const score = tier * 50_000_000 + (city.population || 0);
|
||||
if (!current || score > current.score) capitalNameByPref.set(prefId, { name: stripMunicipalSuffix(city.name), score, x: city.x, y: city.y, population: city.population || 0, source: tier === 3 ? "prefecture-capital" : "city" });
|
||||
}
|
||||
for (const center of adminCenters || []) {
|
||||
if (!center || !inside(center.x, center.y) || !center.name) continue;
|
||||
|
|
@ -265,7 +267,7 @@ function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames
|
|||
x = labelI % MAP_W;
|
||||
y = Math.floor(labelI / MAP_W);
|
||||
}
|
||||
regions.push({ id: row.id, x, y, area: row.area, kind: row.id === 0 ? "Current Prefecture" : "Prefecture", labelPriorityBase: 900 + Math.sqrt(row.area), capitalX: capitalInfo?.x, capitalY: capitalInfo?.y });
|
||||
regions.push({ id: row.id, x, y, area: row.area, kind: row.id === 0 ? "Current Prefecture" : "Prefecture", labelPriorityBase: 950 + Math.sqrt(row.area), forceLabel: true, capitalX: capitalInfo?.x, capitalY: capitalInfo?.y });
|
||||
}
|
||||
const named = attachIdsAndNames(regions, "prefecture", seed + 41000, null, fields, usedNames, nameDebug)
|
||||
.map((region, index) => ({ ...region, featureId: region.id, id: regions[index].id, labelName: region.name }));
|
||||
|
|
@ -720,6 +722,24 @@ export function finishMapOutput({
|
|||
return false;
|
||||
}
|
||||
|
||||
function pathNearRequiredExpresswayCity(path) {
|
||||
if (!path || path.length < 2) return false;
|
||||
for (const city of modernCities || []) {
|
||||
if (!city || (city.population || 0) < 100000 || !inside(city.x, city.y)) continue;
|
||||
const inner = Math.max(7, (city.coreRadius || 4) + 4.5);
|
||||
const outer = Math.max(inner + 7, (city.urbanRadius || 12) * 2.15);
|
||||
let inBand = false;
|
||||
let exits = false;
|
||||
for (const [x, y] of path) {
|
||||
const d = Math.hypot(city.x - x, city.y - y);
|
||||
if (d >= inner && d <= outer) inBand = true;
|
||||
if (d >= Math.max(22, (city.urbanRadius || 12) * 1.45)) exits = true;
|
||||
if (inBand && exits) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function components() {
|
||||
const occ = new Uint8Array(MAP_W * MAP_H);
|
||||
for (const [, paths] of groups) for (const path of paths || []) rasterize(path, (x, y) => {
|
||||
|
|
@ -751,8 +771,165 @@ export function finishMapOutput({
|
|||
}
|
||||
return out.sort((a, b) => b.size - a.size);
|
||||
}
|
||||
function buildLandComponentIds() {
|
||||
const ids = new Int32Array(MAP_W * MAP_H);
|
||||
ids.fill(-1);
|
||||
let id = 0;
|
||||
const q = [];
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
if (ids[i] >= 0 || sea[i]) continue;
|
||||
ids[i] = id;
|
||||
q.length = 0;
|
||||
q.push(i);
|
||||
for (let h = 0; h < q.length; h++) {
|
||||
const cur = q[h];
|
||||
const [x, y] = xyOf(cur);
|
||||
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
||||
if (!dx && !dy) continue;
|
||||
const nx = x + dx, ny = y + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (sea[ni] || ids[ni] >= 0) continue;
|
||||
ids[ni] = id;
|
||||
q.push(ni);
|
||||
}
|
||||
}
|
||||
id++;
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function majorityLandId(cells, landIds) {
|
||||
const counts = new Map();
|
||||
for (const i of cells || []) {
|
||||
const id = landIds[i];
|
||||
if (id < 0) continue;
|
||||
counts.set(id, (counts.get(id) || 0) + 1);
|
||||
}
|
||||
let best = -1, bestN = 0;
|
||||
for (const [id, n] of counts) if (n > bestN) { best = id; bestN = n; }
|
||||
return best;
|
||||
}
|
||||
|
||||
function componentNearAdminCenter(comp, radius = 3.2) {
|
||||
if (!comp?.cells?.length) return false;
|
||||
const mask = new Uint8Array(MAP_W * MAP_H);
|
||||
for (const ci of comp.cells) mask[ci] = 1;
|
||||
const r = Math.ceil(radius);
|
||||
for (const center of adminCenters || []) {
|
||||
if (!center || !inside(center.x, center.y)) continue;
|
||||
const cx = Math.round(center.x), cy = Math.round(center.y);
|
||||
for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
|
||||
if (dx * dx + dy * dy > radius * radius) continue;
|
||||
const x = cx + dx, y = cy + dy;
|
||||
if (inside(x, y) && mask[indexOf(x, y)]) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function routeIsolatedComponentToMain(comp, mainMask, mainCentroid, landIds, landIdValue) {
|
||||
if (!comp?.cells?.length || landIdValue < 0) return [];
|
||||
const dist = new Float64Array(MAP_W * MAP_H);
|
||||
dist.fill(INF);
|
||||
const prev = new Int32Array(MAP_W * MAP_H);
|
||||
prev.fill(-1);
|
||||
const heap = new MinHeap();
|
||||
let seeded = 0;
|
||||
const stride = Math.max(1, Math.floor(comp.cells.length / 96));
|
||||
for (let k = 0; k < comp.cells.length; k += stride) {
|
||||
const i = comp.cells[k];
|
||||
if (sea[i] || landIds[i] !== landIdValue) continue;
|
||||
dist[i] = 0;
|
||||
prev[i] = i;
|
||||
const [x, y] = xyOf(i);
|
||||
heap.push({ i, f: Math.hypot(x - mainCentroid.x, y - mainCentroid.y) * 0.22 });
|
||||
seeded++;
|
||||
}
|
||||
if (!seeded) return [];
|
||||
let goal = -1;
|
||||
let expanded = 0;
|
||||
const maxExpanded = 22000;
|
||||
while (heap.length && expanded < maxExpanded) {
|
||||
const current = heap.pop();
|
||||
if (!current) break;
|
||||
const cur = current.i;
|
||||
expanded++;
|
||||
if (mainMask[cur] && dist[cur] > 2) { goal = cur; break; }
|
||||
const [x, y] = xyOf(cur);
|
||||
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
||||
if (!dx && !dy) continue;
|
||||
const nx = x + dx, ny = y + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (sea[ni] || landIds[ni] !== landIdValue) continue;
|
||||
const step = Math.hypot(dx, dy);
|
||||
const terrainCost = 1.0 + (slope?.[ni] || 0) * 1.28 + (ridgeField?.[ni] || 0) * 0.66 + Math.max(0, (elevation?.[ni] || 0) - 0.66) * 1.75 - (valleyField?.[ni] || 0) * 0.42 - (plain?.[ni] || 0) * 0.18 - (coastalLowland?.[ni] || 0) * 0.08;
|
||||
const nd = dist[cur] + step * Math.max(0.38, terrainCost);
|
||||
if (nd >= dist[ni]) continue;
|
||||
dist[ni] = nd;
|
||||
prev[ni] = cur;
|
||||
const h = Math.hypot(nx - mainCentroid.x, ny - mainCentroid.y) * 0.22;
|
||||
heap.push({ i: ni, f: nd + h });
|
||||
}
|
||||
}
|
||||
if (goal < 0) return [];
|
||||
const path = [];
|
||||
let cur = goal;
|
||||
for (let guard = 0; guard < 240 && cur >= 0; guard++) {
|
||||
const [x, y] = xyOf(cur);
|
||||
path.push([x, y]);
|
||||
if (prev[cur] === cur) break;
|
||||
cur = prev[cur];
|
||||
}
|
||||
path.reverse();
|
||||
if (path.length < 4 || path.length > 150) return [];
|
||||
return routeQualityAcceptable(path, { sea, elevation, slope, potential: populationDensity, highElevationThreshold: 0.80, steepThreshold: 0.55 }, {
|
||||
minLength: 4,
|
||||
maxLength: 150,
|
||||
maxCompactness: 5.4,
|
||||
maxHighElevationShare: 0.42,
|
||||
maxSteepShare: 0.66,
|
||||
}) ? path : [];
|
||||
}
|
||||
|
||||
function attemptConnectSameLandmassAdminRoadComponents(comps) {
|
||||
const result = { attempted: 0, added: 0, skippedIsland: 0, failed: 0 };
|
||||
if (!comps || comps.length <= 1) return result;
|
||||
const landIds = buildLandComponentIds();
|
||||
const mainLand = majorityLandId(comps[0].cells, landIds);
|
||||
const mainMask = new Uint8Array(MAP_W * MAP_H);
|
||||
let sx = 0, sy = 0, sn = 0;
|
||||
for (const ci of comps[0].cells) {
|
||||
const [cx, cy] = xyOf(ci);
|
||||
sx += cx; sy += cy; sn++;
|
||||
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) {
|
||||
if (dx * dx + dy * dy > 5) continue;
|
||||
const nx = cx + dx, ny = cy + dy;
|
||||
if (inside(nx, ny)) mainMask[indexOf(nx, ny)] = 1;
|
||||
}
|
||||
}
|
||||
const centroid = { x: sx / Math.max(1, sn), y: sy / Math.max(1, sn) };
|
||||
for (const comp of comps.slice(1, 24)) {
|
||||
if (!componentNearAdminCenter(comp)) continue;
|
||||
const land = majorityLandId(comp.cells, landIds);
|
||||
if (land !== mainLand) { result.skippedIsland++; continue; }
|
||||
result.attempted++;
|
||||
const path = routeIsolatedComponentToMain(comp, mainMask, centroid, landIds, land);
|
||||
if (path.length >= 4) {
|
||||
minorRoads.push(path);
|
||||
result.added++;
|
||||
} else {
|
||||
result.failed++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
let comps = components();
|
||||
const before = comps.length;
|
||||
const mountainConnect = attemptConnectSameLandmassAdminRoadComponents(comps);
|
||||
if (mountainConnect.added > 0) comps = components();
|
||||
const pruned = { minor: 0, national: 0, external: 0, expressway: 0, externalExpressway: 0 };
|
||||
for (let pass = 0; pass < 4 && comps.length > 1; pass++) {
|
||||
const mainMask = new Uint8Array(MAP_W * MAP_H);
|
||||
|
|
@ -776,7 +953,7 @@ export function finishMapOutput({
|
|||
for (const [key, paths] of groups) {
|
||||
const kept = [];
|
||||
for (const path of paths || []) {
|
||||
if (touchesMain(path) || pathNearAdminCenter(path)) kept.push(path);
|
||||
if (touchesMain(path) || pathNearAdminCenter(path) || ((key === "expressway" || key === "externalExpressway") && pathNearRequiredExpresswayCity(path))) kept.push(path);
|
||||
else pruned[key]++;
|
||||
}
|
||||
paths.length = 0;
|
||||
|
|
@ -784,7 +961,7 @@ export function finishMapOutput({
|
|||
}
|
||||
comps = components();
|
||||
}
|
||||
debugLayers.finalOutputRoadConnectivity = { beforeComponents: before, afterComponents: comps.length, pruned };
|
||||
debugLayers.finalOutputRoadConnectivity = { beforeComponents: before, afterComponents: comps.length, pruned, mountainAdminConnections: mountainConnect };
|
||||
}
|
||||
pruneIsolatedFinalRoadComponents();
|
||||
|
||||
|
|
@ -815,6 +992,178 @@ export function finishMapOutput({
|
|||
}
|
||||
ensureAdminCenterCellsAfterOutputPrune();
|
||||
|
||||
function connectNearbyRoadEndpoints() {
|
||||
const debugLayers = transportDebug ? (transportDebug.layers || (transportDebug.layers = {})) : {};
|
||||
const ordinaryGroups = [
|
||||
{ key: "minor", paths: minorRoads || [] },
|
||||
{ key: "national", paths: nationalRoads || [] },
|
||||
{ key: "external", paths: externalRoads || [] },
|
||||
{ key: "ring", paths: ringRoads || [] },
|
||||
];
|
||||
const occ = new Uint8Array(MAP_W * MAP_H);
|
||||
function rasterize(path, fn) {
|
||||
for (let k = 1; k < (path?.length || 0); k++) {
|
||||
const [x0, y0] = path[k - 1];
|
||||
const [x1, y1] = path[k];
|
||||
const steps = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0)));
|
||||
for (let s = 0; s <= steps; s++) {
|
||||
const t = s / steps;
|
||||
const x = Math.round(x0 + (x1 - x0) * t);
|
||||
const y = Math.round(y0 + (y1 - y0) * t);
|
||||
if (inside(x, y) && !sea[indexOf(x, y)]) fn(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const group of ordinaryGroups) for (const path of group.paths || []) rasterize(path, (x, y) => { occ[indexOf(x, y)] = 1; });
|
||||
const comp = new Int32Array(MAP_W * MAP_H);
|
||||
comp.fill(-1);
|
||||
let compId = 0;
|
||||
const queue = [];
|
||||
for (let i = 0; i < occ.length; i++) {
|
||||
if (!occ[i] || comp[i] >= 0) continue;
|
||||
comp[i] = compId;
|
||||
queue.length = 0;
|
||||
queue.push(i);
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
const [x, y] = xyOf(cur);
|
||||
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
||||
if (!dx && !dy) continue;
|
||||
const nx = x + dx, ny = y + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!occ[ni] || comp[ni] >= 0) continue;
|
||||
comp[ni] = compId;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
compId++;
|
||||
}
|
||||
function endpointComponent(x, y) {
|
||||
if (!inside(x, y) || sea[indexOf(x, y)]) return -1;
|
||||
const here = comp[indexOf(x, y)];
|
||||
if (here >= 0) return here;
|
||||
for (let r = 1; r <= 2; r++) {
|
||||
for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
|
||||
const nx = x + dx, ny = y + dy;
|
||||
if (!inside(nx, ny) || sea[indexOf(nx, ny)]) continue;
|
||||
const id = comp[indexOf(nx, ny)];
|
||||
if (id >= 0) return id;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function directConnector(a, b) {
|
||||
const steps = Math.max(1, Math.ceil(Math.hypot(a.x - b.x, a.y - b.y)));
|
||||
const path = [];
|
||||
for (let s = 0; s <= steps; s++) {
|
||||
const t = s / steps;
|
||||
const x = Math.round(a.x + (b.x - a.x) * t);
|
||||
const y = Math.round(a.y + (b.y - a.y) * t);
|
||||
if (!inside(x, y) || sea[indexOf(x, y)]) return [];
|
||||
if (!path.length || path[path.length - 1][0] !== x || path[path.length - 1][1] !== y) path.push([x, y]);
|
||||
}
|
||||
return path.length >= 2 ? path : [];
|
||||
}
|
||||
const endpoints = [];
|
||||
for (const group of ordinaryGroups) {
|
||||
for (let pathIdx = 0; pathIdx < (group.paths?.length || 0); pathIdx++) {
|
||||
const path = group.paths[pathIdx];
|
||||
if (!path || path.length < 2) continue;
|
||||
for (const end of [0, 1]) {
|
||||
const raw = end === 0 ? path[0] : path[path.length - 1];
|
||||
const x = Math.round(raw[0]), y = Math.round(raw[1]);
|
||||
if (!inside(x, y) || sea[indexOf(x, y)]) continue;
|
||||
const ci = indexOf(x, y);
|
||||
const ruralBias = Math.max(0, 0.42 - (populationDensity?.[ci] || 0));
|
||||
endpoints.push({ group: group.key, pathIdx, end, x, y, comp: endpointComponent(x, y), ruralBias });
|
||||
}
|
||||
}
|
||||
}
|
||||
const pairs = [];
|
||||
for (let i = 0; i < endpoints.length; i++) {
|
||||
const a = endpoints[i];
|
||||
if (a.comp < 0) continue;
|
||||
for (let j = i + 1; j < endpoints.length; j++) {
|
||||
const b = endpoints[j];
|
||||
if (b.comp < 0 || a.comp === b.comp) continue;
|
||||
if (a.group === b.group && a.pathIdx === b.pathIdx) continue;
|
||||
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
||||
const limit = (a.ruralBias + b.ruralBias) > 0.38 ? 6.5 : 4.4;
|
||||
if (d < 1.1 || d > limit) continue;
|
||||
const path = directConnector(a, b);
|
||||
if (path.length < 2 || path.length > 9) continue;
|
||||
pairs.push({ a, b, d, path, score: d - (a.ruralBias + b.ruralBias) * 1.25 + (a.group === "minor" && b.group === "minor" ? 0.25 : 0) });
|
||||
}
|
||||
}
|
||||
pairs.sort((a, b) => a.score - b.score || a.d - b.d);
|
||||
const used = new Set();
|
||||
let added = 0;
|
||||
for (const pair of pairs) {
|
||||
if (added >= 180) break;
|
||||
const ak = `${pair.a.group}:${pair.a.pathIdx}:${pair.a.end}`;
|
||||
const bk = `${pair.b.group}:${pair.b.pathIdx}:${pair.b.end}`;
|
||||
if (used.has(ak) || used.has(bk)) continue;
|
||||
minorRoads.push(pair.path);
|
||||
used.add(ak); used.add(bk);
|
||||
added++;
|
||||
}
|
||||
debugLayers.nearbyRoadEndpointConnectorsAdded = added;
|
||||
return added;
|
||||
}
|
||||
|
||||
connectNearbyRoadEndpoints();
|
||||
|
||||
function renameInterchangesFromMunicipalities() {
|
||||
if (!interchanges?.length || !adminCenters?.length || !adminId) return 0;
|
||||
const centerByAdmin = new Map();
|
||||
for (const center of adminCenters || []) {
|
||||
if (!center || !inside(center.x, center.y)) continue;
|
||||
const id = adminId[indexOf(center.x, center.y)];
|
||||
if (id >= 0 && !centerByAdmin.has(id)) centerByAdmin.set(id, center);
|
||||
}
|
||||
const allCenters = [...centerByAdmin.values()].filter((c) => c?.name);
|
||||
const used = new Set();
|
||||
const directionNames = ["北", "東", "南", "西", "中央", "上", "下", "新"];
|
||||
let renamed = 0;
|
||||
function cleanBase(name) {
|
||||
return String(name || "").replace(/[ICインターチェンジ\s]+$/u, "").replace(/[市町村区]$/u, "");
|
||||
}
|
||||
for (const [idx, ic] of interchanges.entries()) {
|
||||
if (!ic || !inside(ic.x, ic.y)) continue;
|
||||
const cell = indexOf(ic.x, ic.y);
|
||||
const admin = adminId[cell];
|
||||
const primary = centerByAdmin.get(admin) || allCenters.slice().sort((a, b) => Math.hypot(a.x - ic.x, a.y - ic.y) - Math.hypot(b.x - ic.x, b.y - ic.y))[0];
|
||||
const nearbyCenters = allCenters
|
||||
.slice()
|
||||
.sort((a, b) => Math.hypot(a.x - ic.x, a.y - ic.y) - Math.hypot(b.x - ic.x, b.y - ic.y));
|
||||
const candidates = [];
|
||||
if (primary?.name) candidates.push(`${cleanBase(primary.municipalityName || primary.name)}IC`);
|
||||
for (const center of nearbyCenters.slice(0, 12)) {
|
||||
const base = cleanBase(center.municipalityName || center.name);
|
||||
if (base) candidates.push(`${base}IC`);
|
||||
}
|
||||
if (primary?.name) {
|
||||
const base = cleanBase(primary.municipalityName || primary.name);
|
||||
for (const dir of directionNames) candidates.push(`${base}${dir}IC`);
|
||||
}
|
||||
candidates.push(`自治${idx + 1}IC`);
|
||||
let name = candidates.find((candidate) => candidate && !used.has(candidate));
|
||||
if (!name) name = `自治${idx + 1}IC`;
|
||||
ic.name = name;
|
||||
ic.labelName = name;
|
||||
ic.municipalityNameBased = true;
|
||||
used.add(name);
|
||||
renamed++;
|
||||
}
|
||||
if (transportDebug) {
|
||||
transportDebug.layers ||= {};
|
||||
transportDebug.layers.municipalityBasedInterchangeNames = renamed;
|
||||
}
|
||||
return renamed;
|
||||
}
|
||||
renameInterchangesFromMunicipalities();
|
||||
|
||||
nameDebug.maxDerivedPerBase = 0;
|
||||
const prefectureRegions = buildPrefectureRegions(prefectureRegionId, sea, nameFields, seed, usedNames, nameDebug, { modernCities, adminCenters });
|
||||
const regionalPrefectureBorders = adminRegionalPrefectureBorders || [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue