road tweak
This commit is contained in:
parent
a6e66f2838
commit
0359bb2445
5 changed files with 1266 additions and 200 deletions
1161
mapFeatures.js
1161
mapFeatures.js
File diff suppressed because it is too large
Load diff
166
mapOutput.js
166
mapOutput.js
|
|
@ -1,6 +1,7 @@
|
|||
import { createNameDebug } from "./names.js";
|
||||
import { CELL_SIZE, INF, MAP_H, MAP_W, clamp, indexOf, inside, rand } from "./mapUtils.js";
|
||||
import { CELL_SIZE, INF, MAP_H, MAP_W, MinHeap, clamp, indexOf, inside, rand, xyOf } from "./mapUtils.js";
|
||||
import { applyOutputOptions, attachIdsAndNames, influenceFromPaths, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
|
||||
import { routeQualityAcceptable } from "./mapTransport.js";
|
||||
|
||||
const MUNICIPAL_SUFFIX_RE = /[市町村区]$/u;
|
||||
|
||||
|
|
@ -475,65 +476,133 @@ export function finishMapOutput({
|
|||
]);
|
||||
|
||||
function addMunicipalCenterLocalAccess() {
|
||||
if (!transportDebug) return;
|
||||
const debugLayers = transportDebug.layers || (transportDebug.layers = { repairedSegments: [], unservedSettlements: [] });
|
||||
const debugLayers = transportDebug ? (transportDebug.layers || (transportDebug.layers = { repairedSegments: [], unservedSettlements: [] })) : { repairedSegments: [], unservedSettlements: [] };
|
||||
debugLayers.repairedSegments ||= [];
|
||||
debugLayers.unservedSettlements ||= [];
|
||||
const accessInfluence = influenceFromPaths([...nationalRoads, ...railways, ...externalRoads, ...minorRoads], 7);
|
||||
const localPenalty = influenceFromPaths(minorRoads, 4);
|
||||
const influenceCache = new Map();
|
||||
const signature = (paths) => `${paths?.length || 0}:${(paths || []).reduce((sum, path) => sum + (path?.length || 0), 0)}`;
|
||||
const cachedInfluence = (paths, radius, label) => {
|
||||
const key = `${label}:${radius}:${signature(paths)}`;
|
||||
let grid = influenceCache.get(key);
|
||||
if (!grid) {
|
||||
grid = influenceFromPaths(paths, radius);
|
||||
influenceCache.set(key, grid);
|
||||
}
|
||||
return grid;
|
||||
};
|
||||
const accessInfluence = cachedInfluence([...nationalRoads, ...railways, ...externalRoads, ...minorRoads], 8, "municipal-access");
|
||||
const localPenalty = cachedInfluence(minorRoads, 4, "municipal-minor");
|
||||
const perPrefectureQuota = new Map();
|
||||
const candidates = adminCenters
|
||||
.filter((p) => inside(p.x, p.y) && !sea[indexOf(p.x, p.y)] && accessInfluence[indexOf(p.x, p.y)] < 0.16)
|
||||
.sort((a, b) => (b.municipalityPopulation || 0) - (a.municipalityPopulation || 0))
|
||||
.slice(0, 45);
|
||||
.filter((p) => {
|
||||
if (!inside(p.x, p.y) || sea[indexOf(p.x, p.y)]) return false;
|
||||
const i = indexOf(p.x, p.y);
|
||||
const meaningful = (p.municipalityPopulation || 0) >= 4500 || (populationDensity?.[i] || 0) > 0.08 || (p.representativeFeatureName && accessInfluence[i] < 0.22);
|
||||
return meaningful && accessInfluence[i] < 0.42;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const ai = indexOf(a.x, a.y);
|
||||
const bi = indexOf(b.x, b.y);
|
||||
const as = (a.municipalityPopulation || 0) + Math.max(0, 0.42 - accessInfluence[ai]) * 145000;
|
||||
const bs = (b.municipalityPopulation || 0) + Math.max(0, 0.42 - accessInfluence[bi]) * 145000;
|
||||
return bs - as;
|
||||
})
|
||||
.filter((p) => {
|
||||
const prefId = municipalityToPrefectureId?.[p.municipalityId] ?? -1;
|
||||
const used = perPrefectureQuota.get(prefId) || 0;
|
||||
if (used >= 18) return false;
|
||||
perPrefectureQuota.set(prefId, used + 1);
|
||||
return true;
|
||||
})
|
||||
.slice(0, 95);
|
||||
|
||||
function routeAccess(start) {
|
||||
const maxSteps = 72;
|
||||
const out = [];
|
||||
const visited = new Set([`${start.x},${start.y}`]);
|
||||
let x = start.x;
|
||||
let y = start.y;
|
||||
let lastKey = "";
|
||||
for (let step = 0; step < maxSteps; step++) {
|
||||
const i = indexOf(x, y);
|
||||
if (step > 3 && (accessInfluence[i] > 0.20 || localPenalty[i] > 0.10)) return out;
|
||||
let best = null;
|
||||
let bestScore = INF;
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
if (!dx && !dy) continue;
|
||||
function addLocalPenalty(path, radius = 4, strength = 0.20) {
|
||||
for (const [x, y] of path || []) {
|
||||
for (let dy = -radius; dy <= radius; dy++) {
|
||||
for (let dx = -radius; dx <= radius; dx++) {
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > radius) continue;
|
||||
const i = indexOf(nx, ny);
|
||||
if (sea[i]) continue;
|
||||
localPenalty[i] = Math.max(localPenalty[i], strength * (1 - d / radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function routeAccess(start) {
|
||||
const startIndex = indexOf(start.x, start.y);
|
||||
if (sea[startIndex]) return [];
|
||||
const score = new Float32Array(MAP_W * MAP_H);
|
||||
const cameFrom = new Int32Array(MAP_W * MAP_H);
|
||||
const closed = new Uint8Array(MAP_W * MAP_H);
|
||||
score.fill(INF);
|
||||
cameFrom.fill(-1);
|
||||
const heap = new MinHeap();
|
||||
score[startIndex] = 0;
|
||||
heap.push({ i: startIndex, f: 0 });
|
||||
const maxExpanded = Math.min(MAP_W * MAP_H, 24000);
|
||||
let goal = -1;
|
||||
let expanded = 0;
|
||||
while (heap.length && expanded++ < maxExpanded) {
|
||||
const current = heap.pop();
|
||||
if (!current || closed[current.i]) continue;
|
||||
closed[current.i] = 1;
|
||||
const [cx, cy] = xyOf(current.i);
|
||||
const straightDistance = Math.hypot(cx - start.x, cy - start.y);
|
||||
if (straightDistance > 3 && (accessInfluence[current.i] > 0.11 || localPenalty[current.i] > 0.06)) {
|
||||
goal = current.i;
|
||||
break;
|
||||
}
|
||||
if (straightDistance > 150) continue;
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
if (!dx && !dy) continue;
|
||||
const nx = cx + dx;
|
||||
const ny = cy + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (sea[ni]) continue;
|
||||
if (visited.has(`${nx},${ny}`)) continue;
|
||||
const cost =
|
||||
1.1 +
|
||||
slope[ni] * 1.4 +
|
||||
ridgeField[ni] * 0.55 +
|
||||
Math.max(0, elevation[ni] - 0.70) * 1.2 -
|
||||
Math.max(roadInfluence[ni], railInfluence2[ni]) * 2.8 -
|
||||
plain[ni] * 0.28 -
|
||||
valleyField[ni] * 0.18 -
|
||||
coastalLowland[ni] * 0.16 +
|
||||
localPenalty[ni] * 0.55;
|
||||
if (cost < bestScore) {
|
||||
bestScore = cost;
|
||||
best = [nx, ny];
|
||||
if (closed[ni] || sea[ni]) continue;
|
||||
const step = Math.hypot(dx, dy);
|
||||
const highPenalty = Math.max(0, elevation[ni] - 0.64);
|
||||
const targetAttraction = Math.max(accessInfluence[ni] * 2.4, localPenalty[ni] * 1.25, roadInfluence[ni] * 1.8, railInfluence2[ni] * 1.2);
|
||||
const terrainCost =
|
||||
1.0 +
|
||||
slope[ni] * 1.20 +
|
||||
ridgeField[ni] * 0.52 +
|
||||
highPenalty * 1.55 -
|
||||
plain[ni] * 0.38 -
|
||||
valleyField[ni] * 0.42 -
|
||||
coastalLowland[ni] * 0.18 +
|
||||
Math.max(0, localPenalty[ni] - 0.18) * 0.38 -
|
||||
targetAttraction;
|
||||
const nd = score[current.i] + Math.max(0.16, terrainCost) * step;
|
||||
if (nd < score[ni]) {
|
||||
score[ni] = nd;
|
||||
cameFrom[ni] = current.i;
|
||||
heap.push({ i: ni, f: nd });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!best) break;
|
||||
x = best[0];
|
||||
y = best[1];
|
||||
const key = `${x},${y}`;
|
||||
visited.add(key);
|
||||
if (key !== lastKey) {
|
||||
out.push([x, y]);
|
||||
lastKey = key;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
if (goal < 0) return [];
|
||||
const path = [];
|
||||
for (let p = goal; p >= 0; p = cameFrom[p]) {
|
||||
path.push(xyOf(p));
|
||||
if (p === startIndex) break;
|
||||
}
|
||||
path.reverse();
|
||||
if (path.length < 4 || path.length > 112) return [];
|
||||
return routeQualityAcceptable(path, { sea, elevation, slope, potential: populationDensity, penalty: localPenalty, highElevationThreshold: 0.78, steepThreshold: 0.52 }, {
|
||||
minLength: 4,
|
||||
maxLength: 112,
|
||||
maxCompactness: 4.0,
|
||||
maxHighElevationShare: 0.22,
|
||||
maxSteepShare: 0.50,
|
||||
}) ? path : [];
|
||||
}
|
||||
|
||||
for (const center of candidates) {
|
||||
|
|
@ -541,6 +610,7 @@ export function finishMapOutput({
|
|||
debugLayers.unservedSettlements.push({ x: center.x, y: center.y, kind: "Municipal Center", mode: "municipal-access", repaired: path.length >= 4 });
|
||||
if (path.length < 4) continue;
|
||||
minorRoads.push(path);
|
||||
addLocalPenalty(path);
|
||||
debugLayers.repairedSegments.push({ mode: "municipal-local", path, from: "municipal-center", to: "network" });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
99
mapTransport.js
Normal file
99
mapTransport.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { SIZE, clamp, indexOf, inside } from "./mapUtils.js";
|
||||
|
||||
export function pathSetSignature(paths) {
|
||||
let cells = 0;
|
||||
let endpoints = 0;
|
||||
for (const path of paths || []) {
|
||||
cells += path?.length || 0;
|
||||
const a = path?.[0];
|
||||
const b = path?.[path.length - 1];
|
||||
if (a) endpoints = (endpoints + a[0] * 17 + a[1] * 31) | 0;
|
||||
if (b) endpoints = (endpoints + b[0] * 47 + b[1] * 73) | 0;
|
||||
}
|
||||
return `${paths?.length || 0}:${cells}:${endpoints >>> 0}`;
|
||||
}
|
||||
|
||||
export function createPathInfluenceCache(influenceFromPaths) {
|
||||
const cache = new Map();
|
||||
return (paths, radius, label = "paths") => {
|
||||
const key = `${label}:${radius}:${pathSetSignature(paths)}`;
|
||||
let grid = cache.get(key);
|
||||
if (!grid) {
|
||||
grid = influenceFromPaths(paths, radius);
|
||||
cache.set(key, grid);
|
||||
}
|
||||
return grid;
|
||||
};
|
||||
}
|
||||
|
||||
export function packDebugField(field) {
|
||||
const out = new Uint8Array(SIZE);
|
||||
for (let i = 0; i < SIZE; i++) out[i] = Math.round(clamp(field?.[i] || 0) * 255);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function pathLengthCells(path) {
|
||||
let total = 0;
|
||||
for (let i = 1; i < (path?.length || 0); i++) {
|
||||
total += Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
export function pathAverageField(path, field) {
|
||||
if (!path?.length || !field) return 0;
|
||||
let sum = 0;
|
||||
let n = 0;
|
||||
for (const [x, y] of path) {
|
||||
if (!inside(x, y)) continue;
|
||||
sum += field[indexOf(x, y)] || 0;
|
||||
n++;
|
||||
}
|
||||
return n ? sum / n : 0;
|
||||
}
|
||||
|
||||
export function routeQualityStats(path, fields = {}) {
|
||||
if (!path?.length) return { length: 0, compactness: Infinity, highElevationShare: 1, steepShare: 1, waterShare: 1, avgPotential: 0, avgPenalty: 0 };
|
||||
const length = pathLengthCells(path);
|
||||
const first = path[0];
|
||||
const last = path[path.length - 1];
|
||||
const direct = first && last ? Math.hypot(first[0] - last[0], first[1] - last[1]) : 0;
|
||||
let high = 0;
|
||||
let steep = 0;
|
||||
let water = 0;
|
||||
let potential = 0;
|
||||
let penalty = 0;
|
||||
let n = 0;
|
||||
for (const [x, y] of path) {
|
||||
if (!inside(x, y)) continue;
|
||||
const i = indexOf(x, y);
|
||||
if (fields.sea?.[i]) water++;
|
||||
if ((fields.elevation?.[i] || 0) > (fields.highElevationThreshold ?? 0.72)) high++;
|
||||
if ((fields.slope?.[i] || 0) > (fields.steepThreshold ?? 0.44)) steep++;
|
||||
potential += fields.potential?.[i] || 0;
|
||||
penalty += fields.penalty?.[i] || 0;
|
||||
n++;
|
||||
}
|
||||
return {
|
||||
length,
|
||||
compactness: direct > 0.001 ? length / direct : Infinity,
|
||||
highElevationShare: high / Math.max(1, n),
|
||||
steepShare: steep / Math.max(1, n),
|
||||
waterShare: water / Math.max(1, n),
|
||||
avgPotential: potential / Math.max(1, n),
|
||||
avgPenalty: penalty / Math.max(1, n),
|
||||
};
|
||||
}
|
||||
|
||||
export function routeQualityAcceptable(path, fields = {}, limits = {}) {
|
||||
const q = routeQualityStats(path, fields);
|
||||
if (q.length < (limits.minLength ?? 2)) return false;
|
||||
if (q.length > (limits.maxLength ?? Infinity)) return false;
|
||||
if (q.compactness > (limits.maxCompactness ?? 3.2)) return false;
|
||||
if (q.highElevationShare > (limits.maxHighElevationShare ?? 0.0)) return false;
|
||||
if (q.steepShare > (limits.maxSteepShare ?? 0.38)) return false;
|
||||
if (q.waterShare > (limits.maxWaterShare ?? 0)) return false;
|
||||
if (q.avgPenalty > (limits.maxAvgPenalty ?? Infinity) && q.avgPotential < (limits.minPotentialIfPenalty ?? 0.35)) return false;
|
||||
if (q.avgPotential < (limits.minAvgPotential ?? 0)) return false;
|
||||
return true;
|
||||
}
|
||||
23
names.js
23
names.js
|
|
@ -1,6 +1,6 @@
|
|||
import { MAP_H, MAP_W, hash2, indexOf, inside } from "./mapUtils.js";
|
||||
|
||||
export const CUSTOM_NAME_LIST = ["加茂", "春日", "新庄", "常盤", "追分", "清水", "曙", "暁", "旭", "鳥羽", "長門", "吉野", "吉田", "美郷", "美里", "美山"];
|
||||
export const CUSTOM_NAME_LIST = ["加茂", "春日", "新庄", "常盤", "追分", "清水", "曙", "暁", "旭", "鳥羽", "長門", "吉野", "吉田", "美郷", "美里", "美山", "八幡", "相生",];
|
||||
|
||||
function nameCharCount(value) {
|
||||
return Array.from(String(value || "")).length;
|
||||
|
|
@ -18,7 +18,7 @@ export const NAME_KANJI_POOLS = {
|
|||
"大", "小", "上", "下", "中", "奥", "脇",
|
||||
"東", "西", "南", "北",
|
||||
"新", "古", "本",
|
||||
"高", "長", "広", "深", "浅",
|
||||
"高", "長", "広", "深", "浅", "明", "重", "荒",
|
||||
"白", "黒", "青", "赤", "藍",
|
||||
"奥", "前", "後", "内", "外",
|
||||
"美", "吉", "福", "幸", "徳",
|
||||
|
|
@ -26,7 +26,7 @@ export const NAME_KANJI_POOLS = {
|
|||
"霞", "朝", "日", "天",
|
||||
"土", "砂", "石", "岩",
|
||||
"卯", "辰",
|
||||
"荒", "永", "早", "清", "芳", "安", "泰", "真", "丸", "平", "勝", "吹", "舞", "鎌", "釜", "笠", "掘", "鎚", "綾", "槌", "徳", "栄"
|
||||
"駒", "羽", "鳴", "横", "永", "早", "清", "芳", "安", "泰", "真", "丸", "平", "勝", "吹", "舞", "鎌", "釜", "笠", "掘", "鎚", "綾", "槌", "徳", "栄"
|
||||
],
|
||||
|
||||
inlandTerrain: [
|
||||
|
|
@ -37,7 +37,7 @@ export const NAME_KANJI_POOLS = {
|
|||
"塚", "牧", "畑", "田", "森", "幡多", "幡", "畠", "秦", "植", "生",
|
||||
"郷", "里",
|
||||
"馬", "鹿", "亀", "鷲", "鷹", "鶴", "鴨", "竜", "龍", "牛", "鳥",
|
||||
"湯",
|
||||
"湯", "宍",
|
||||
],
|
||||
|
||||
waterTerrain: [
|
||||
|
|
@ -54,16 +54,15 @@ export const NAME_KANJI_POOLS = {
|
|||
"州", "洲", "瀬", "砂", "潮", "塩", "浜",
|
||||
"泊", "江", "浦", "灘", "入",
|
||||
"戸", "門",
|
||||
"鯵", "鰐", "漁", "魚", "鮫", "鮎",
|
||||
],
|
||||
|
||||
plants: [
|
||||
"松", "杉", "桜", "梅", "栗",
|
||||
"竹", "楠", "藤", "萩", "葦",
|
||||
"菅", "榎", "椿", "桐", "柳",
|
||||
"橘", "柏", "槙", "柿", "桃",
|
||||
"梨", "桑", "麻", "芦", "茅",
|
||||
"粟", "稲", "稗", "米", "飯", "糠", "茜", "葵",
|
||||
"橘", "柏", "槙", "柿", "桃", "稲", "花", "草", "菊",
|
||||
"梨", "桑", "麻", "芦", "茅", "根",
|
||||
"粟", "稲", "稗", "米", "飯", "糠", "茜", "葵", "篠",
|
||||
"榊", "楢", "檜", "椎", "茨", "葛", "蘆", "菖", "蒲", "蓮", "桑", "瓜"
|
||||
],
|
||||
|
||||
|
|
@ -72,9 +71,9 @@ export const NAME_KANJI_POOLS = {
|
|||
"島",
|
||||
"江", "瀬", "井", "戸", "口",
|
||||
"辺", "里", "郷", "村", "町",
|
||||
"宿", "庄", "台", "坂", "橋",
|
||||
"本", "内", "窪", "平", "塚",
|
||||
"畑", "牧", "前", "見", "中", "羽", "生", "塚", "部", "栄", "永",
|
||||
"宿", "庄", "台", "坂", "橋", "明",
|
||||
"本", "内", "窪", "平", "塚", "根",
|
||||
"畑", "牧", "前", "見", "中", "羽", "生", "駒", "塚", "部", "栄", "永", "平",
|
||||
],
|
||||
|
||||
archaicPrefixes: [
|
||||
|
|
@ -118,7 +117,7 @@ export const NAME_KANJI_POOLS = {
|
|||
],
|
||||
|
||||
settlementWords: [
|
||||
"里", "郷", "村", "町", "宿", "垣", "坪", "軒", "惣",
|
||||
"里", "郷", "村", "町", "宿", "垣", "坪", "軒", "惣", "條",
|
||||
"庄", "ノ庄", "之庄", "院", "宮", "ノ宮", "之宮", "寺", "社", "堂",
|
||||
"城", "館", "屋", "家", "所",
|
||||
"市", "場", "関", "地蔵", "辻", "角", "堰", "通", "路", "道", "筋",
|
||||
|
|
|
|||
21
renderer.js
21
renderer.js
|
|
@ -208,10 +208,13 @@ function vectorPath(path) {
|
|||
if (cached) return cached;
|
||||
|
||||
const points = path.map(([x, y]) => [x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2]);
|
||||
const simplified = simplifyRdp(points, CELL_SIZE * 0.34);
|
||||
const smoothed = chaikin(simplified, path.length > 6 ? 1 : 0, false);
|
||||
pathVectorCache.set(path, smoothed);
|
||||
return smoothed;
|
||||
// Transport routes are already cost-routed on the raster grid. A large RDP
|
||||
// tolerance erases those small valley/contour bends and makes roads look like
|
||||
// ruler-straight overlays, so smooth first and simplify only lightly.
|
||||
const smoothedBase = chaikin(points, path.length > 8 ? 1 : 0, false);
|
||||
const simplified = simplifyRdp(smoothedBase, CELL_SIZE * 0.14);
|
||||
pathVectorCache.set(path, simplified);
|
||||
return simplified;
|
||||
}
|
||||
|
||||
function drawPolylinePoints(ctx, points) {
|
||||
|
|
@ -636,7 +639,8 @@ function drawDebugCells(ctx, map, field, color) {
|
|||
const i = indexOf(x, y);
|
||||
const debugMask = map.humanRegionMask || map.prefectureMask;
|
||||
if (!debugMask[i] || map.sea[i]) continue;
|
||||
const v = clamp(field[i] || 0, 0, 1);
|
||||
const raw = field[i] || 0;
|
||||
const v = clamp(raw > 1 ? raw / 255 : raw, 0, 1);
|
||||
if (v <= 0.12) continue;
|
||||
ctx.fillStyle = color(v);
|
||||
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||
|
|
@ -917,7 +921,10 @@ export function drawMap(canvas, map, options) {
|
|||
for (const path of map.premodernRoads) drawPath(ctx, path, "rgba(210, 210, 210, 0.7)", 2.5);
|
||||
}
|
||||
if (showMinorRoads) {
|
||||
for (const path of map.minorRoads || []) drawPath(ctx, path, "rgba(205, 205, 205, 0.60)", 2.35);
|
||||
// Local roads need a visible casing on pale green lowland/farmland tiles.
|
||||
// Keep the fill light, but use a warmer grey outline rather than a nearly
|
||||
// invisible white-on-green stroke.
|
||||
for (const path of map.minorRoads || []) drawPath(ctx, path, "rgba(132, 126, 112, 0.72)", 2.75);
|
||||
}
|
||||
if (showRoads) {
|
||||
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
|
||||
|
|
@ -939,7 +946,7 @@ export function drawMap(canvas, map, options) {
|
|||
for (const path of map.premodernRoads) drawPath(ctx, path, "rgba(255, 255, 255, 1)", 1.2, false);
|
||||
}
|
||||
if (showMinorRoads) {
|
||||
for (const path of map.minorRoads || []) drawPath(ctx, path, "rgba(255, 255, 255, 0.94)", 1.1, false);
|
||||
for (const path of map.minorRoads || []) drawPath(ctx, path, "rgba(255, 253, 244, 0.98)", 1.25, false);
|
||||
}
|
||||
if (showRoads) {
|
||||
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue