17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 08:27:17 +01:00

refactor(tools): the grading tool in smaller functions

As Sonar asks, and a regular expression with no backtracking. The results stay
the same byte for byte on baltyk_skm1, baltyk_cargo, td and lk357.
This commit is contained in:
maj00r
2026-10-31 20:17:47 +01:00
parent 9364b03e1a
commit 32fadd634d

View File

@@ -43,7 +43,7 @@ STRAY_PER_MM, DIP_SHARE, DIP_REACH = 0.00145, 0.4, 3.0
GOLDEN = math.pi * (3.0 - math.sqrt(5.0))
UP_WAVES = [(2 * math.pi / length * math.cos(GOLDEN * i), 2 * math.pi / length * math.sin(GOLDEN * i), GOLDEN * (i + 1), share) for i, (length, share) in enumerate(zip((8.3, 11.9, 16.7, 23.9, 37.1, 47.3, 61.9, 83.3), (.14, .19, .22, .22, .23, .23, .23, .23)))]
TOKEN = re.compile(r"//[^\n]*|\S+")
NUMBER = re.compile(r"^[-+]?\d*\.?\d+([eE][-+]?\d+)?$")
NUMBER = re.compile(r"^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$")
def limit(speed):
@@ -79,38 +79,9 @@ class Scenery:
def read(self, path, offset=(0.0, 0.0, 0.0)):
path = path.resolve()
if path in self.seen:
return
self.seen.add(path)
text = path.read_bytes().decode("latin-1")
self.texts[path] = text
matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")]
words = [m.group(0) for m in matches]
lower = [w.lower() for w in words]
origins = [offset]
i = 0
while i < len(words):
word = lower[i]
if word == "include" and i + 1 < len(words):
end = i + 1
while end < len(words) and lower[end] != "end":
end += 1
self.include(words[i + 1], words[i + 2:end], origins[-1])
i = end
elif word == "origin" and i + 3 < len(words) and all(NUMBER.match(w) for w in words[i + 1:i + 4]):
origins.append(tuple(o + float(w) for o, w in zip(origins[-1], words[i + 1:i + 4])))
i += 3
elif word == "endorigin" and len(origins) > 1:
origins.pop()
elif word == "rotate" and i + 3 < len(words) and any(w.strip("0.-") for w in words[i + 1:i + 4]):
self.notes["rotate"] += 1
elif word == "node" and i + 4 < len(words):
kind = lower[i + 4]
if kind == "track":
i = self.track(path, matches, words, lower, i, origins[-1])
elif kind == "model" and i + 9 < len(words) and GRASS.search(lower[i + 9]) and all(NUMBER.match(w) for w in words[i + 5:i + 8]):
self.plant(float(words[i + 5]) + origins[-1][0], float(words[i + 7]) + origins[-1][2])
i += 1
if path not in self.seen:
self.seen.add(path)
FileReader(self, path, offset).run()
def include(self, name, params, origin):
path = self.root / name.replace("\\", "/")
@@ -118,21 +89,90 @@ class Scenery:
numbers = [float(w) for w in params if NUMBER.match(w)]
if GRASS.search(name.lower()) and len(numbers) >= 3:
self.plant(numbers[0] + origin[0], numbers[2] + origin[2])
return
if not path.is_file():
elif not path.is_file():
self.notes["brak pliku " + name] += 1
return
if params:
elif not params:
self.read(path, origin)
elif path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I):
# the same file can come many times with other parameters: its tracks can't be told apart in one copy
if path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I):
self.notes["pliki z parametrami, tory pominiete"] += 1
return
self.read(path, origin)
self.notes["pliki z parametrami, tory pominiete"] += 1
def plant(self, x, z):
self.plants[(round(x / 10), round(z / 10))].append((x, z))
def track(self, path, matches, words, lower, i, origin):
class FileReader:
"""Goes through the words of one file. Each handler takes the place of its word and gives the place of the last word it used."""
def __init__(self, scenery, path, offset):
self.scenery, self.path = scenery, path
text = path.read_bytes().decode("latin-1")
scenery.texts[path] = text
self.matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")]
self.words = [m.group(0) for m in self.matches]
self.lower = [w.lower() for w in self.words]
self.origins = [offset]
self.handlers = {"include": self.include, "origin": self.origin, "endorigin": self.endorigin, "rotate": self.rotate, "node": self.node}
def run(self):
i = 0
while i < len(self.words):
handler = self.handlers.get(self.lower[i])
i = (handler(i) if handler else i) + 1
def include(self, i):
if i + 1 >= len(self.words):
return i
end = i + 1
while end < len(self.words) and self.lower[end] != "end":
end += 1
self.scenery.include(self.words[i + 1], self.words[i + 2:end], self.origins[-1])
return end
def origin(self, i):
values = self.words[i + 1:i + 4]
if len(values) < 3 or not all(NUMBER.match(w) for w in values):
return i
self.origins.append(tuple(o + float(w) for o, w in zip(self.origins[-1], values)))
return i + 3
def endorigin(self, i):
if len(self.origins) > 1:
self.origins.pop()
return i
def rotate(self, i):
if i + 3 < len(self.words) and any(w.strip("0.-") for w in self.words[i + 1:i + 4]):
self.scenery.notes["rotate"] += 1
return i
def node(self, i):
if i + 4 >= len(self.words):
return i
kind = self.lower[i + 4]
if kind == "track":
return self.track(i)
if kind == "model" and i + 9 < len(self.words) and GRASS.search(self.lower[i + 9]) and all(NUMBER.match(w) for w in self.words[i + 5:i + 8]):
self.scenery.plant(float(self.words[i + 5]) + self.origins[-1][0], float(self.words[i + 7]) + self.origins[-1][2])
return i
def paths_from(self, cursor, count):
paths = []
origin = self.origins[-1]
for _ in range(count):
values = self.words[cursor:cursor + 15]
if len(values) < 15 or not all(NUMBER.match(v) for v in values):
return None
n = [float(v) for v in values]
part = {"p1": [n[j] + origin[j] for j in range(3)], "c1": n[4:7], "c2": n[7:10], "p2": [n[10 + j] + origin[j] for j in range(3)], "radius": abs(n[14])}
part["line"] = line_of(part)
part["length"] = sum(math.dist(a, b) for a, b in zip(part["line"], part["line"][1:]))
paths.append(part)
cursor += 15
return paths
def track(self, i):
words, lower = self.words, self.lower
at = i + 4
end = at
while end < len(words) and lower[end] != "endtrack":
@@ -142,33 +182,30 @@ class Scenery:
kind = lower[at + 1]
if kind not in ("normal", "switch", "road"):
return end
cursor = at + 10
rail = ballast = None
if lower[at + 9] == "vis":
rail, ballast = lower[cursor], lower[cursor + 2]
cursor += 6
paths = []
for _ in range(2 if kind == "switch" else 1):
values = words[cursor:cursor + 15]
if len(values) < 15 or not all(NUMBER.match(v) for v in values):
self.notes["tory z parametrami, pominiete"] += 1
return end
n = [float(v) for v in values]
part = dict(p1=[n[j] + origin[j] for j in range(3)], c1=n[4:7], c2=n[7:10], p2=[n[10 + j] + origin[j] for j in range(3)], radius=abs(n[14]))
part["line"] = line_of(part)
part["length"] = sum(math.dist(a, b) for a, b in zip(part["line"], part["line"][1:]))
paths.append(part)
cursor += 15
if kind == "road":
self.roads.append(paths[0])
return end
block = lower[at:end]
taken = any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip"))
velocity = float(words[at + block.index("velocity") + 1]) if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]) else 0.0
sound = float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0
self.tracks.append(dict(name=words[i + 3], kind=kind, rail=rail, ballast=ballast, sound=sound, paths=paths, speed=velocity if velocity > 0 else None, source="wpis" if velocity > 0 else None, file=path, offset=matches[end].start(), taken=taken))
visible = lower[at + 9] == "vis"
paths = self.paths_from(at + (16 if visible else 10), 2 if kind == "switch" else 1)
if paths is None:
self.scenery.notes["tory z parametrami, pominiete"] += 1
elif kind == "road":
self.scenery.roads.append(paths[0])
else:
self.add_track(i, at, end, kind, visible, paths)
return end
def add_track(self, i, at, end, kind, visible, paths):
words = self.words
block = self.lower[at:end]
velocity = 0.0
if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]):
velocity = float(words[at + block.index("velocity") + 1])
self.scenery.tracks.append({
"name": words[i + 3], "kind": kind,
"rail": self.lower[at + 10] if visible else None, "ballast": self.lower[at + 12] if visible else None,
"sound": float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0,
"paths": paths, "speed": velocity if velocity > 0 else None, "source": "wpis" if velocity > 0 else None,
"file": self.path, "offset": self.matches[end].start(),
"taken": any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip"))})
def bezier(part, t):
a = [part["p1"][i] + part["c1"][i] for i in range(3)]
@@ -220,50 +257,67 @@ class Joints:
return found
def plain_neighbours(t, joints):
return [other for key in ("p1", "p2") for other, _, _ in joints.at(t["paths"][0][key]) if other is not t and other["kind"] == "normal"]
def plain_past_switches(t, point, joints):
# the plain tracks reached from a point of a track along the straight paths of switches
points, seen, found = [point], set(), []
for _ in range(6):
onward = []
for reached in points:
for other, part, side in joints.at(reached):
if other is t or id(part) in seen:
continue
seen.add(id(part))
if other["kind"] != "switch":
found.append(other)
elif other["paths"].index(part) != other["bent"]:
onward.append(part["p2" if side == "p1" else "p1"])
points = onward
return found
def short_pieces_past_switches(t, joints):
return [o for key in ("p1", "p2") for o in plain_past_switches(t, t["paths"][0][key], joints) if o["paths"][0]["length"] < PIECE]
def spread_speed(tracks, joints, reached):
heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"])
while heads:
t = heads.popleft()
for other in reached(t, joints):
if other["speed"] is None:
other["speed"], other["source"] = t["speed"], "sasiad"
heads.append(other)
def settle_speeds(tracks, joints):
# from the nearest track on the stretch between switches, a short piece also along the straight path of a switch
heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"])
while heads:
t = heads.popleft()
for key in ("p1", "p2"):
for other, _, _ in joints.at(t["paths"][0][key]):
if other is not t and other["kind"] == "normal" and other["speed"] is None:
other["speed"], other["source"] = t["speed"], "sasiad"
heads.append(other)
heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"])
while heads:
t = heads.popleft()
for key in ("p1", "p2"):
points, seen = [t["paths"][0][key]], set()
for _ in range(6):
onward = []
for point in points:
for other, part, side in joints.at(point):
if other is t or id(part) in seen:
continue
seen.add(id(part))
if other["kind"] == "switch":
if other["paths"].index(part) != other["bent"]:
onward.append(part["p2" if side == "p1" else "p1"])
elif other["speed"] is None and other["paths"][0]["length"] < PIECE:
other["speed"], other["source"] = t["speed"], "sasiad"
heads.append(other)
points = onward
spread_speed(tracks, joints, plain_neighbours)
spread_speed(tracks, joints, short_pieces_past_switches)
for t in tracks:
if t["speed"] is None:
t["speed"], t["source"] = NO_SPEED, "brak"
def plain_ends(tracks):
for t in tracks:
if t["kind"] == "normal":
for part in t["paths"]:
yield part["p1"]
yield part["p2"]
def distances(tracks, joints, station):
best, heap = {}, []
for t in tracks:
for part in t["paths"]:
for key in ("p1", "p2"):
gap = math.dist((part[key][0], part[key][2]), station)
node = (round(part[key][0], 1), round(part[key][2], 1))
if gap < 300 and t["kind"] == "normal" and gap < best.get(node, math.inf):
best[node] = gap
heapq.heappush(heap, (gap, node, part[key]))
for point in plain_ends(tracks):
gap = math.dist((point[0], point[2]), station)
node = (round(point[0], 1), round(point[2], 1))
if gap < 300 and gap < best.get(node, math.inf):
best[node] = gap
heapq.heappush(heap, (gap, node, point))
while heap:
distance, node, point = heapq.heappop(heap)
if distance > best.get(node, math.inf):
@@ -293,30 +347,34 @@ def grass_on(part, plants):
return len(near) * 100.0 / max(part["length"], 5.0)
def cells_under(a, b):
for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1):
for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1):
yield x, z
def road_cells(roads):
cells = collections.defaultdict(list)
for road in roads:
for a, b in zip(road["line"], road["line"][1:]):
for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1):
for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1):
cells[(x, z)].append((a, b))
for cell in cells_under(a, b):
cells[cell].append((a, b))
return cells
def cross_in_plan(a, b, c, d):
rx, rz, sx, sz = b[0] - a[0], b[2] - a[2], d[0] - c[0], d[2] - c[2]
k = rx * sz - rz * sx
if abs(k) < 1e-9:
return False
t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k
u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k
return 0 <= t <= 1 and 0 <= u <= 1
def hits_road(part, cells):
for a, b in zip(part["line"], part["line"][1:]):
for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1):
for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1):
for c, d in cells.get((x, z), ()):
rx, rz, sx, sz = b[0] - a[0], b[2] - a[2], d[0] - c[0], d[2] - c[2]
k = rx * sz - rz * sx
if abs(k) < 1e-9:
continue
t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k
u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k
if 0 <= t <= 1 and 0 <= u <= 1:
return True
return False
line = part["line"]
return any(cross_in_plan(a, b, c, d) for a, b in zip(line, line[1:]) for cell in cells_under(a, b) for c, d in cells.get(cell, ()))
def heights_per_mm(t):
@@ -423,17 +481,19 @@ def fit_under_pantograph(normal):
return low
def joint_curve_changes(t, part, key, joints):
own = end_curve(part, key)
for other, theirs, side in joints.at(part[key]):
if other is not t and other["kind"] == "normal":
their = end_curve(theirs, side)
if abs(own - (-their if side == key else their)) > CURVE_STEP:
return True
return False
def curve_changes(t, joints):
part = t["paths"][0]
if abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP:
return True
for key in ("p1", "p2"):
for other, theirs, side in joints.at(part[key]):
if other is not t and other["kind"] == "normal":
their = end_curve(theirs, side)
if abs(end_curve(part, key) - (-their if side == key else their)) > CURVE_STEP:
return True
return False
return abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP or any(joint_curve_changes(t, part, key, joints) for key in ("p1", "p2"))
def move_near_limit(normal, joints, share):
@@ -458,24 +518,32 @@ def move_near_limit(normal, joints, share):
return chosen
def value_from_plain(t, index, part, joints, product):
found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT]
if not found:
return None
speed = max(o["speed"] for o in found)
if index == t["bent"]:
speed = min(speed, DIVERGING)
return min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed)
def value_from_switches(t, part, joints):
found = []
for key in ("p1", "p2"):
for other, theirs, _ in joints.at(part[key]):
if other["kind"] == "switch" and other is not t:
found += [other["values"][i] for i, q in enumerate(other["paths"]) if q is theirs and other["values"][i] is not None]
return max(found) if found else None
def grade_switches(switches, joints, product):
# each path by the plain tracks it leads to, the worst of them; those only among switches from their neighbours
for t in switches:
t["values"] = [None] * len(t["paths"])
for index, part in enumerate(t["paths"]):
found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT]
if found:
speed = max(o["speed"] for o in found)
if index == t["bent"]:
speed = min(speed, DIVERGING)
t["values"][index] = min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed)
t["values"] = [value_from_plain(t, index, part, joints, product) for index, part in enumerate(t["paths"])]
for _ in range(5):
for t in switches:
for index, part in enumerate(t["paths"]):
if t["values"][index] is None:
found = [o["values"][i] for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "switch" and o is not t for i, q in enumerate(o["paths"]) if q is theirs and o["values"][i] is not None]
if found:
t["values"][index] = max(found)
t["values"] = [value_from_switches(t, part, joints) if value is None else value for value, part in zip(t["values"], t["paths"])]
for t in switches:
t["values"] = [min(SWITCH_MOST, product / NO_SPEED) if v is None else v for v in t["values"]]