chore(tools): AD-vejnet-værktøjer (45°-kurver, parkering, reparationer) brugt på savegame2 30/9
This commit is contained in:
@@ -0,0 +1,222 @@
|
|||||||
|
"""Fjern sving > MAX_TURN på AD-ruter mellem markører ved at tilføje kurve-omveje.
|
||||||
|
|
||||||
|
Kun nye waypoints tilføjes; eksisterende id'er, koordinater og forbindelser bevares
|
||||||
|
(kanter der deles får et mellempunkt på samme linje). AD's pathfinder vælger selv
|
||||||
|
kurven, fordi den er kortere og har mindre vinkel-straf.
|
||||||
|
|
||||||
|
Brug: adfix.py <ind.xml> <ud.xml> <log.json>
|
||||||
|
"""
|
||||||
|
import json, math, re, sys, collections
|
||||||
|
sys.path.insert(0, __file__.rsplit("/", 1)[0])
|
||||||
|
from adsmooth import read, write, dist, new_node, link, apply_splits
|
||||||
|
from adroutes import transit, dijkstra, angle, is_reverse
|
||||||
|
|
||||||
|
MAX_TURN = 45.5 # grænse (45° præcis er tilladt)
|
||||||
|
CURVE_STEP = 7.9 # max knæk i kurven (<8° => AD regner 27 km/h, så kurven vinder)
|
||||||
|
R_TRY = (8.0, 6.0, 4.5, 3.0, 2.0, 1.5, 1.0)
|
||||||
|
TARGET_EDGE = 7.9 # max knæk overalt i kurven inkl. ind-/udgang
|
||||||
|
|
||||||
|
|
||||||
|
def markers(doc):
|
||||||
|
return [int(float(i)) for i in re.findall(r"<mm\d+>\s*<id>([\d.]+)</id>", doc["s"])]
|
||||||
|
|
||||||
|
|
||||||
|
def node_path(best, pre, target):
|
||||||
|
s = best.get(target); seq = []
|
||||||
|
while s:
|
||||||
|
seq.append(s); s = pre[s]
|
||||||
|
seq.reverse()
|
||||||
|
return [seq[0][0]] + [b for _, b in seq] if seq else []
|
||||||
|
|
||||||
|
|
||||||
|
def collect(N, mks):
|
||||||
|
"""Returnér {(p,b,c): (antal, path, index_of_b)} for sving > MAX_TURN på ruter."""
|
||||||
|
T = transit(N); found = {}
|
||||||
|
cnt = collections.Counter()
|
||||||
|
for st in mks:
|
||||||
|
best, pre = dijkstra(N, T, st)
|
||||||
|
for tg in mks:
|
||||||
|
if tg == st or tg not in best:
|
||||||
|
continue
|
||||||
|
path = node_path(best, pre, tg)
|
||||||
|
backing = False
|
||||||
|
for i in range(1, len(path) - 1):
|
||||||
|
p, b, c = path[i - 1], path[i], path[i + 1]
|
||||||
|
a = angle(N, p, b, c)
|
||||||
|
# bakning: starter på en bakforbindelse (p ikke i b.incoming), slutter når retningen vender (>90°)
|
||||||
|
if p not in N[b]["inc"]:
|
||||||
|
backing = True
|
||||||
|
if backing:
|
||||||
|
if a > 90:
|
||||||
|
backing = False
|
||||||
|
continue
|
||||||
|
# vendepunktet lige før baklængs-kørsel (bakforbindelse c -> næste) er bevidst skarpt
|
||||||
|
if i + 2 < len(path) and c not in N[path[i + 2]]["inc"]:
|
||||||
|
continue
|
||||||
|
if a > MAX_TURN and not is_reverse(N, p, b, c):
|
||||||
|
key = (p, b, c) + window(N, path, i)
|
||||||
|
cnt[key] += 1
|
||||||
|
back = sum(dist(N[path[j]], N[path[j + 1]]) for j in range(i))
|
||||||
|
fwd = sum(dist(N[path[j]], N[path[j + 1]]) for j in range(i, len(path) - 1))
|
||||||
|
cur = found.get(key)
|
||||||
|
if cur is None:
|
||||||
|
found[key] = [(path, i, back), (path, i, fwd)]
|
||||||
|
else:
|
||||||
|
if back < cur[0][2]: cur[0] = (path, i, back)
|
||||||
|
if fwd < cur[1][2]: cur[1] = (path, i, fwd)
|
||||||
|
return {k: (cnt[k], v[0], v[1]) for k, v in found.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def window(N, path, i, reach=8.5):
|
||||||
|
"""Rutens noder inden for `reach` meter før og efter path[i] (skelner grene)."""
|
||||||
|
lo = i; acc = 0.0
|
||||||
|
while lo > 0 and acc < reach:
|
||||||
|
acc += dist(N[path[lo - 1]], N[path[lo]]); lo -= 1
|
||||||
|
hi = i; acc = 0.0
|
||||||
|
while hi < len(path) - 1 and acc < reach:
|
||||||
|
acc += dist(N[path[hi]], N[path[hi + 1]]); hi += 1
|
||||||
|
return (tuple(path[lo:i]), tuple(path[i + 1:hi + 1]))
|
||||||
|
|
||||||
|
|
||||||
|
def point_along(N, path, i, R, direction):
|
||||||
|
"""Gå R meter fra path[i] bagud (-1) eller fremad (+1). Returnér
|
||||||
|
(u, v, d_from_u) for punktet på kanten u->v i kørselsretning, eller None."""
|
||||||
|
acc = 0.0; j = i
|
||||||
|
while True:
|
||||||
|
k = j + direction
|
||||||
|
if k < 0 or k >= len(path):
|
||||||
|
return None
|
||||||
|
a, b = (path[k], path[j]) if direction < 0 else (path[j], path[k])
|
||||||
|
L = dist(N[a], N[b])
|
||||||
|
if acc + L >= R:
|
||||||
|
rest = R - acc # meter fra path[j] mod path[k]
|
||||||
|
return (a, b, L - rest) if direction < 0 else (a, b, rest)
|
||||||
|
acc += L; j = k
|
||||||
|
|
||||||
|
|
||||||
|
def tangent(N, u, v):
|
||||||
|
L = dist(N[u], N[v])
|
||||||
|
return ((N[v]["x"] - N[u]["x"]) / L, (N[v]["z"] - N[u]["z"]) / L)
|
||||||
|
|
||||||
|
|
||||||
|
def pos_on(N, u, v, d):
|
||||||
|
t = d / dist(N[u], N[v])
|
||||||
|
return {a: N[u][a] + (N[v][a] - N[u][a]) * t for a in "xyz"}
|
||||||
|
|
||||||
|
|
||||||
|
def ang2(v1, v2):
|
||||||
|
l1 = math.hypot(*v1); l2 = math.hypot(*v2)
|
||||||
|
if l1 < 1e-6 or l2 < 1e-6:
|
||||||
|
return 0.0
|
||||||
|
return math.degrees(math.acos(max(-1, min(1, (v1[0] * v2[0] + v1[1] * v2[1]) / (l1 * l2)))))
|
||||||
|
|
||||||
|
|
||||||
|
def design(N, back, fwd, R):
|
||||||
|
"""Kubisk Bézier fra punkt R før b til R efter b. back/fwd = (path, i, plads) for
|
||||||
|
de ruter der har mindst plads før hhv. efter b. Returnér plan eller None."""
|
||||||
|
a = point_along(N, back[0], back[1], R, -1); b = point_along(N, fwd[0], fwd[1], R, +1)
|
||||||
|
if not a or not b:
|
||||||
|
return None
|
||||||
|
(u1, v1, d1), (u2, v2, d2) = a, b
|
||||||
|
# ligger punktet (næsten) oven i en eksisterende node, bruges noden selv
|
||||||
|
snap = lambda u, v, d: ("node", u) if d < 0.3 else ("node", v) if dist(N[u], N[v]) - d < 0.3 else ("split", u, v, d)
|
||||||
|
e_in, e_out = snap(u1, v1, d1), snap(u2, v2, d2)
|
||||||
|
if e_in[0] == "node":
|
||||||
|
d1 = 0.0 if e_in[1] == u1 else dist(N[u1], N[v1])
|
||||||
|
if e_out[0] == "node":
|
||||||
|
d2 = 0.0 if e_out[1] == u2 else dist(N[u2], N[v2])
|
||||||
|
P0 = pos_on(N, u1, v1, d1); P3 = pos_on(N, u2, v2, d2)
|
||||||
|
t0 = tangent(N, u1, v1); t3 = tangent(N, u2, v2)
|
||||||
|
# snappet til en node: retningen skal matche rutens kant IND i (start) / UD af (slut) noden
|
||||||
|
bp, fp = back[0], fwd[0]
|
||||||
|
if e_in[0] == "node" and e_in[1] == u1:
|
||||||
|
k = bp.index(u1)
|
||||||
|
if k == 0:
|
||||||
|
return None
|
||||||
|
t0 = tangent(N, bp[k - 1], u1)
|
||||||
|
if e_out[0] == "node" and e_out[1] == v2:
|
||||||
|
k = fp.index(v2)
|
||||||
|
if k >= len(fp) - 1:
|
||||||
|
return None
|
||||||
|
t3 = tangent(N, v2, fp[k + 1])
|
||||||
|
chord = math.hypot(P3["x"] - P0["x"], P3["z"] - P0["z"])
|
||||||
|
k = chord * 0.4
|
||||||
|
P1 = {"x": P0["x"] + t0[0] * k, "z": P0["z"] + t0[1] * k, "y": P0["y"] + (P3["y"] - P0["y"]) / 3}
|
||||||
|
P2 = {"x": P3["x"] - t3[0] * k, "z": P3["z"] - t3[1] * k, "y": P0["y"] + 2 * (P3["y"] - P0["y"]) / 3}
|
||||||
|
total = ang2(t0, t3)
|
||||||
|
n = max(3, math.ceil(total / CURVE_STEP) + 2)
|
||||||
|
while True:
|
||||||
|
pts = []
|
||||||
|
for s in range(n + 1):
|
||||||
|
t = s / n
|
||||||
|
pts.append({ax: (1 - t) ** 3 * P0[ax] + 3 * (1 - t) ** 2 * t * P1[ax] + 3 * (1 - t) * t * t * P2[ax] + t ** 3 * P3[ax] for ax in "xyz"})
|
||||||
|
# tjek alle knæk inkl. ind- og udgang
|
||||||
|
dirs = [t0] + [(pts[s + 1]["x"] - pts[s]["x"], pts[s + 1]["z"] - pts[s]["z"]) for s in range(n)] + [t3]
|
||||||
|
worst = max(ang2(dirs[s], dirs[s + 1]) for s in range(len(dirs) - 1))
|
||||||
|
if worst <= TARGET_EDGE:
|
||||||
|
break
|
||||||
|
n += 1
|
||||||
|
if n > 80 or chord / n < 0.15:
|
||||||
|
return None
|
||||||
|
return dict(end_in=e_in, end_out=e_out, split_in=(u1, v1, d1), split_out=(u2, v2, d2), inner=pts[1:-1], R=R, total=round(total, 1), worst=round(worst, 1))
|
||||||
|
|
||||||
|
|
||||||
|
def run(src, dst, logp):
|
||||||
|
doc = read(src); N = doc["N"]; n0 = len(N)
|
||||||
|
mks = [m for m in markers(doc) if m in N]
|
||||||
|
turns = collect(N, mks)
|
||||||
|
order = sorted(turns.items(), key=lambda kv: -kv[1][0])
|
||||||
|
plans = []; covered = set(); skipped = []
|
||||||
|
for key, (cnt, back, fwd) in order:
|
||||||
|
p, b, c = key[:3]
|
||||||
|
if key in covered:
|
||||||
|
continue
|
||||||
|
path, i = back[0], back[1]
|
||||||
|
plan = None
|
||||||
|
for R in R_TRY:
|
||||||
|
if R > back[2] - 0.3 or R > fwd[2] - 0.3:
|
||||||
|
continue
|
||||||
|
plan = design(N, back, fwd, R)
|
||||||
|
if plan:
|
||||||
|
break
|
||||||
|
if not plan:
|
||||||
|
skipped.append([p, b, c, cnt, round(angle(N, p, b, c), 1)]); continue
|
||||||
|
# marker alle sving i vinduet som dækket (fx 63°+27° i samme hjørne)
|
||||||
|
lo = path.index(plan["split_in"][1]) if plan["split_in"][1] in path else i
|
||||||
|
hi = path.index(plan["split_out"][0]) if plan["split_out"][0] in path else i
|
||||||
|
for j in range(max(1, lo), min(len(path) - 1, hi + 1)):
|
||||||
|
covered.add((path[j - 1], path[j], path[j + 1]) + window(N, path, j))
|
||||||
|
plan.update(turn=[p, b, c], count=cnt); plans.append(plan)
|
||||||
|
# 1) del kanter (samlet pr. kant), 2) byg kurver
|
||||||
|
req = {}; keyof = []
|
||||||
|
for pl in plans:
|
||||||
|
ks = []
|
||||||
|
for e in (pl["end_in"], pl["end_out"]):
|
||||||
|
if e[0] == "node":
|
||||||
|
ks.append(("node", e[1])); continue
|
||||||
|
_, u, v, d = e
|
||||||
|
lo, hi = min(u, v), max(u, v)
|
||||||
|
dd = round(d if u == lo else dist(N[u], N[v]) - d, 3)
|
||||||
|
req.setdefault((lo, hi), set()).add(dd); ks.append((lo, hi, dd))
|
||||||
|
keyof.append(ks)
|
||||||
|
made = apply_splits(N, req)
|
||||||
|
for pl, ks in zip(plans, keyof):
|
||||||
|
res = lambda k: k[1] if k[0] == "node" else made[k]
|
||||||
|
a, b = res(ks[0]), res(ks[1])
|
||||||
|
# bivej (bit 1) kun hvis begge ender er bivej — ellers ganger AD kurven med 20
|
||||||
|
sub = N[a]["fl"] & N[b]["fl"] & 1
|
||||||
|
fl = (N[pl["turn"][1]]["fl"] & ~1) | sub
|
||||||
|
chain = [a] + [new_node(N, q["x"], q["y"], q["z"], fl) for q in pl["inner"]] + [b]
|
||||||
|
for x, y in zip(chain, chain[1:]):
|
||||||
|
link(N, x, y)
|
||||||
|
pl["nodes"] = chain
|
||||||
|
del pl["inner"]
|
||||||
|
write(doc, dst)
|
||||||
|
json.dump(dict(plans=plans, skipped=skipped), open(logp, "w"))
|
||||||
|
print(f"sving>{MAX_TURN}°: {len(turns)} kurver: {len(plans)} dækket af andre kurver: {len(turns)-len(plans)-len(skipped)}"
|
||||||
|
f" sprunget over: {len(skipped)} nye noder: {len(N)-n0}")
|
||||||
|
return len(turns)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run(*sys.argv[1:4])
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Fjern kurver som ingen markør-rute bruger, og omnummerér nye noder fortløbende."""
|
||||||
|
import sys, json, re, collections
|
||||||
|
sys.path.insert(0, __file__.rsplit("/", 1)[0])
|
||||||
|
import adfix
|
||||||
|
from adsmooth import read, write
|
||||||
|
from adroutes import transit, dijkstra
|
||||||
|
|
||||||
|
src, dst, n0 = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
||||||
|
logs = sys.argv[4:]
|
||||||
|
doc = read(src); N = doc["N"]
|
||||||
|
mks = [m for m in adfix.markers(doc) if m in N]
|
||||||
|
T = transit(N); used = collections.Counter()
|
||||||
|
for st in mks:
|
||||||
|
best, pre = dijkstra(N, T, st)
|
||||||
|
for tg in mks:
|
||||||
|
if tg != st and tg in best:
|
||||||
|
for x in adfix.node_path(best, pre, tg):
|
||||||
|
if x > n0:
|
||||||
|
used[x] += 1
|
||||||
|
drop = set()
|
||||||
|
for lp in logs:
|
||||||
|
for p in json.load(open(lp))["plans"]:
|
||||||
|
inner = p["nodes"][1:-1]
|
||||||
|
if inner and not any(used[x] for x in inner):
|
||||||
|
drop.update(inner)
|
||||||
|
for i in drop:
|
||||||
|
del N[i]
|
||||||
|
for n in N.values():
|
||||||
|
n["out"] = [o for o in n["out"] if o not in drop]
|
||||||
|
n["inc"] = [o for o in n["inc"] if o not in drop]
|
||||||
|
old_new = sorted(i for i in N if i > n0)
|
||||||
|
remap = {o: n0 + 1 + k for k, o in enumerate(old_new)}
|
||||||
|
f = lambda i: remap.get(i, i)
|
||||||
|
doc["N"] = {f(i): dict(n, out=[f(o) for o in n["out"]], inc=[f(o) for o in n["inc"]]) for i, n in N.items()}
|
||||||
|
write(doc, dst)
|
||||||
|
print(f"fjernet {len(drop)} ubrugte kurvenoder; noder nu {len(doc['N'])} (nye: {len(doc['N']) - n0})")
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import re,math,heapq,collections,sys,json
|
||||||
|
sys.path.insert(0, __file__.rsplit("/", 1)[0])
|
||||||
|
from adturns import load
|
||||||
|
def angle(N,p,b,c):
|
||||||
|
v1=(N[b]["x"]-N[p]["x"],N[b]["z"]-N[p]["z"]); v2=(N[c]["x"]-N[b]["x"],N[c]["z"]-N[b]["z"])
|
||||||
|
l1=math.hypot(*v1); l2=math.hypot(*v2)
|
||||||
|
if l1<1e-6 or l2<1e-6: return 0.0
|
||||||
|
return math.degrees(math.acos(max(-1,min(1,(v1[0]*v2[0]+v1[1]*v2[1])/(l1*l2)))))
|
||||||
|
def speed(a):
|
||||||
|
for lim,sp in ((3,50),(5,38),(8,27),(12,20),(15,13),(20,10),(30,7)):
|
||||||
|
if a<lim: return sp
|
||||||
|
return 4
|
||||||
|
def is_reverse(N,p,b,c):
|
||||||
|
return (b not in N[c]["inc"]) or (b in N[c]["inc"] and p not in N[b]["inc"])
|
||||||
|
def transit(N):
|
||||||
|
T={}
|
||||||
|
for b,n in N.items():
|
||||||
|
for p in n["inc"]:
|
||||||
|
if p not in N: continue
|
||||||
|
T[(p,b)]=[c for c in n["out"] if c in N and (angle(N,p,b,c)<=80 or is_reverse(N,p,b,c))]
|
||||||
|
return T
|
||||||
|
def dijkstra(N,T,start):
|
||||||
|
# tilstand = (forrige, node); returnerer bedste forgænger-tilstand pr node
|
||||||
|
dist={};pre={};best={}
|
||||||
|
h=[]
|
||||||
|
for c in N[start]["out"]:
|
||||||
|
if c not in N: continue
|
||||||
|
d=math.hypot(N[c]["x"]-N[start]["x"],N[c]["z"]-N[start]["z"])/(50/3.6)*(20 if N[c]["fl"]&1 else 1)
|
||||||
|
s=(start,c); dist[s]=d; pre[s]=None; heapq.heappush(h,(d,s))
|
||||||
|
while h:
|
||||||
|
d,s=heapq.heappop(h)
|
||||||
|
if d>dist.get(s,1e18): continue
|
||||||
|
p,b=s
|
||||||
|
if b not in best or d<dist[best[b]]: best[b]=s
|
||||||
|
# AD: efter en bakforbindelse (p ikke i b.incoming) findes ingen transitMapping -> alle udgange er tilladt
|
||||||
|
for c in T.get(s,N[b]["out"]):
|
||||||
|
a=angle(N,p,b,c); l=math.hypot(N[c]["x"]-N[b]["x"],N[c]["z"]-N[b]["z"])
|
||||||
|
nd=d+l/(speed(a)/3.6)*(20 if N[c]["fl"]&1 else 1)
|
||||||
|
ns=(b,c)
|
||||||
|
if nd<dist.get(ns,1e18): dist[ns]=nd; pre[ns]=s; heapq.heappush(h,(nd,ns))
|
||||||
|
return best,pre
|
||||||
|
def path_turns(N,best,pre,target):
|
||||||
|
s=best.get(target); turns=[]
|
||||||
|
seq=[]
|
||||||
|
while s: seq.append(s); s=pre[s]
|
||||||
|
seq.reverse()
|
||||||
|
for i in range(1,len(seq)):
|
||||||
|
p,b=seq[i-1]; _,c=seq[i]
|
||||||
|
turns.append((p,b,c))
|
||||||
|
return turns
|
||||||
|
if __name__=="__main__":
|
||||||
|
s,N,_=load(sys.argv[1])
|
||||||
|
mm=[(int(float(i)),n,g) for i,n,g in re.findall(r"<mm\d+>\s*<id>([\d.]+)</id>\s*<name>([^<]*)</name>\s*<group>([^<]*)</group>",s)]
|
||||||
|
T=transit(N)
|
||||||
|
ids=[m[0] for m in mm if m[0] in N]
|
||||||
|
sharp=collections.Counter(); rev=0
|
||||||
|
for k,st in enumerate(ids):
|
||||||
|
best,pre=dijkstra(N,T,st)
|
||||||
|
for tg in ids:
|
||||||
|
if tg==st or tg not in best: continue
|
||||||
|
for (p,b,c) in path_turns(N,best,pre,tg):
|
||||||
|
a=angle(N,p,b,c)
|
||||||
|
if a>45:
|
||||||
|
if is_reverse(N,p,b,c): rev+=1; continue
|
||||||
|
sharp[(p,b,c)]+=1
|
||||||
|
json.dump([[p,b,c,n,round(angle(N,p,b,c),1)] for (p,b,c),n in sharp.items()],open(sys.argv[2],"w"))
|
||||||
|
print("markører",len(ids),"forskellige skarpe sving på ruter:",len(sharp),"bakke-sving sprunget over:",rev)
|
||||||
|
print(collections.Counter(int(angle(N,*k)//5*5) for k in sharp))
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""Blødgør skarpe sving (>45°) i et AutoDrive-vejnet ved at tilføje kurve-omveje.
|
||||||
|
|
||||||
|
Eksisterende waypoints og id'er røres ikke (kun nye noder tilføjes), så markører og
|
||||||
|
mod-referencer holder. For hvert sving p->b->c:
|
||||||
|
- kanten p-b deles i Pin (r meter før b), kanten b-c deles i Pout (r meter efter b)
|
||||||
|
- Pin -> kvadratisk Bézier (kontrolpunkt b) -> Pout, med nok punkter til at hvert
|
||||||
|
knæk er <= MAX_STEP grader
|
||||||
|
- er det omvendte sving (c,b,p) også på listen, bliver kurven tovejs
|
||||||
|
"""
|
||||||
|
import json, math, re, sys
|
||||||
|
|
||||||
|
MAX_STEP = 20.0 # grader pr. knæk inde i kurven
|
||||||
|
R_CAP = 10.0 # max benlængde (m) -> max afvigelse fra hjørnet ~3,2 m
|
||||||
|
R_FRAC = 0.45 # max andel af en kant der bruges til benet
|
||||||
|
R_MIN = 1.5 # kortere ben end dette -> spring over
|
||||||
|
|
||||||
|
|
||||||
|
def read(path):
|
||||||
|
raw = open(path, "rb").read()
|
||||||
|
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||||
|
s = raw.decode("utf-8-sig")
|
||||||
|
w0 = s.index("<waypoints>"); w1 = s.index("</waypoints>")
|
||||||
|
w = s[w0:w1]
|
||||||
|
g = lambda t: re.search(rf"<{t}>(.*?)</{t}>", w, re.S).group(1)
|
||||||
|
ids = [int(v) for v in g("id").split(",")]
|
||||||
|
assert ids == list(range(1, len(ids) + 1)), "id'er er ikke 1..N"
|
||||||
|
xs = [float(v) for v in g("x").split(",")]
|
||||||
|
ys = [float(v) for v in g("y").split(",")]
|
||||||
|
zs = [float(v) for v in g("z").split(",")]
|
||||||
|
lst = lambda t: [[int(v) for v in e.split(",") if v != "-1"] for e in g(t).split(";")]
|
||||||
|
out, inc = lst("out"), lst("incoming")
|
||||||
|
fl = [int(v) for v in g("flags").split(",")]
|
||||||
|
N = {i: dict(x=xs[k], y=ys[k], z=zs[k], out=out[k], inc=inc[k], fl=fl[k]) for k, i in enumerate(ids)}
|
||||||
|
return dict(s=s, bom=bom, w0=w0, w1=w1, w=w, N=N)
|
||||||
|
|
||||||
|
|
||||||
|
def write(doc, path):
|
||||||
|
N = doc["N"]; ids = sorted(N)
|
||||||
|
f3 = lambda v: "%.3f" % v
|
||||||
|
joinl = lambda L: ",".join(map(str, L)) if L else "-1"
|
||||||
|
parts = {
|
||||||
|
"id": ",".join(map(str, ids)),
|
||||||
|
"x": ",".join(f3(N[i]["x"]) for i in ids),
|
||||||
|
"y": ",".join(f3(N[i]["y"]) for i in ids),
|
||||||
|
"z": ",".join(f3(N[i]["z"]) for i in ids),
|
||||||
|
"out": ";".join(joinl(N[i]["out"]) for i in ids),
|
||||||
|
"incoming": ";".join(joinl(N[i]["inc"]) for i in ids),
|
||||||
|
"flags": ",".join(str(N[i]["fl"]) for i in ids),
|
||||||
|
}
|
||||||
|
w = doc["w"]
|
||||||
|
for t, v in parts.items():
|
||||||
|
w = re.sub(rf"<{t}>.*?</{t}>", lambda m, v=v, t=t: f"<{t}>{v}</{t}>", w, count=1, flags=re.S)
|
||||||
|
s = doc["s"][:doc["w0"]] + w + doc["s"][doc["w1"]:]
|
||||||
|
data = s.encode("utf-8")
|
||||||
|
open(path, "wb").write((b"\xef\xbb\xbf" if doc["bom"] else b"") + data)
|
||||||
|
|
||||||
|
|
||||||
|
def xz(n):
|
||||||
|
return (n["x"], n["z"])
|
||||||
|
|
||||||
|
|
||||||
|
def dist(a, b):
|
||||||
|
return math.hypot(a["x"] - b["x"], a["z"] - b["z"])
|
||||||
|
|
||||||
|
|
||||||
|
def new_node(N, x, y, z, fl):
|
||||||
|
i = max(N) + 1
|
||||||
|
N[i] = dict(x=x, y=y, z=z, out=[], inc=[], fl=fl)
|
||||||
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
def link(N, a, b):
|
||||||
|
if b not in N[a]["out"]:
|
||||||
|
N[a]["out"].append(b)
|
||||||
|
if a not in N[b]["inc"]:
|
||||||
|
N[b]["inc"].append(a)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_splits(N, requests):
|
||||||
|
"""requests: {(u,v) u<v: set(afstand fra u)} -> {(u,v,afstand): ny node}."""
|
||||||
|
made = {}
|
||||||
|
for (u, v), ds in requests.items():
|
||||||
|
L = dist(N[u], N[v])
|
||||||
|
pts = []
|
||||||
|
for d in sorted(ds):
|
||||||
|
if pts and d - pts[-1] < 0.5:
|
||||||
|
made[(u, v, d)] = made[(u, v, pts[-1])]; continue
|
||||||
|
t = d / L
|
||||||
|
m = new_node(N, *(N[u][a] + (N[v][a] - N[u][a]) * t for a in "xyz"),
|
||||||
|
N[v]["fl"] if t > 0.5 else N[u]["fl"])
|
||||||
|
made[(u, v, d)] = m; pts.append(d)
|
||||||
|
chain = [u] + [made[(u, v, d)] for d in pts] + [v]
|
||||||
|
for a, b in ((u, v), (v, u)):
|
||||||
|
if b in N[a]["out"]:
|
||||||
|
seq = chain if a == u else chain[::-1]
|
||||||
|
N[a]["out"] = [seq[1] if o == b else o for o in N[a]["out"]]
|
||||||
|
N[b]["inc"] = [seq[-2] if o == a else o for o in N[b]["inc"]]
|
||||||
|
for x, y in zip(seq[1:-1], seq[2:]):
|
||||||
|
link(N, x, y)
|
||||||
|
N[seq[1]]["inc"].append(a) if a not in N[seq[1]]["inc"] else None
|
||||||
|
return made
|
||||||
|
|
||||||
|
|
||||||
|
def smooth(doc, turns):
|
||||||
|
N = doc["N"]; want = set(turns); plan = []; req = {}; seen = set()
|
||||||
|
for p, b, c in turns:
|
||||||
|
if (p, b, c) in seen:
|
||||||
|
continue
|
||||||
|
dual = (c, b, p) in want
|
||||||
|
seen.add((p, b, c)); seen.add((c, b, p)) if dual else None
|
||||||
|
r = min(R_CAP, R_FRAC * dist(N[p], N[b]), R_FRAC * dist(N[b], N[c]))
|
||||||
|
if r < R_MIN:
|
||||||
|
continue
|
||||||
|
keys = []
|
||||||
|
for o in (p, c):
|
||||||
|
u, v = min(o, b), max(o, b)
|
||||||
|
d = r if u == b else dist(N[u], N[v]) - r
|
||||||
|
req.setdefault((u, v), set()).add(round(d, 3)); keys.append((u, v, round(d, 3)))
|
||||||
|
plan.append((p, b, c, dual, r, keys))
|
||||||
|
orig = {i: dict(N[i]) for i in N}
|
||||||
|
made_split = apply_splits(N, req)
|
||||||
|
made = []
|
||||||
|
for p, b, c, dual, r, keys in plan:
|
||||||
|
pin, pout = made_split[keys[0]], made_split[keys[1]]
|
||||||
|
P0, P1, P2 = N[pin], N[b], N[pout]
|
||||||
|
th = turn_angle(orig[p], orig[b], orig[c])
|
||||||
|
n = max(2, math.ceil(th / MAX_STEP) + 1)
|
||||||
|
chain = [pin]
|
||||||
|
for k in range(1, n):
|
||||||
|
t = k / n
|
||||||
|
q = lambda a: (1 - t) ** 2 * P0[a] + 2 * (1 - t) * t * P1[a] + t * t * P2[a]
|
||||||
|
chain.append(new_node(N, q("x"), q("y"), q("z"), N[b]["fl"]))
|
||||||
|
chain.append(pout)
|
||||||
|
for a, bb in zip(chain, chain[1:]):
|
||||||
|
link(N, a, bb)
|
||||||
|
if dual:
|
||||||
|
link(N, bb, a)
|
||||||
|
made.append(dict(turn=[p, b, c], dual=dual, angle=round(th, 1), r=round(r, 1), nodes=chain))
|
||||||
|
return made
|
||||||
|
|
||||||
|
|
||||||
|
def turn_angle(A, B, C):
|
||||||
|
v1 = (B["x"] - A["x"], B["z"] - A["z"]); v2 = (C["x"] - B["x"], C["z"] - B["z"])
|
||||||
|
l1 = math.hypot(*v1); l2 = math.hypot(*v2)
|
||||||
|
if l1 < 1e-6 or l2 < 1e-6:
|
||||||
|
return 0.0
|
||||||
|
return math.degrees(math.acos(max(-1, min(1, (v1[0] * v2[0] + v1[1] * v2[1]) / (l1 * l2)))))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
src, sharp_json, dst, log = sys.argv[1:5]
|
||||||
|
doc = read(src)
|
||||||
|
n0 = len(doc["N"])
|
||||||
|
turns = [tuple(x[:3]) for x in json.load(open(sharp_json)) if x[4] > 45.5]
|
||||||
|
made = smooth(doc, turns)
|
||||||
|
write(doc, dst)
|
||||||
|
json.dump(made, open(log, "w"), indent=0)
|
||||||
|
print(f"sving: {len(turns)} kurver lavet: {len(made)} (tovejs {sum(m['dual'] for m in made)})"
|
||||||
|
f" nye noder: {len(doc['N']) - n0} sprunget over: {len(turns) - len(made) - sum(m['dual'] for m in made)}")
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import re,math,collections,sys
|
||||||
|
def load(path):
|
||||||
|
s=open(path,encoding="utf-8-sig").read()
|
||||||
|
w=re.search(r"<waypoints>(.*?)</waypoints>",s,re.S).group(1)
|
||||||
|
g=lambda t:re.search(rf"<{t}>(.*?)</{t}>",w,re.S).group(1)
|
||||||
|
ids=[int(v) for v in g("id").split(",")]
|
||||||
|
X=[float(v) for v in g("x").split(",")]; Z=[float(v) for v in g("z").split(",")]
|
||||||
|
sp=lambda t:[[int(v) for v in e.split(",") if v not in("","-1")] for e in g(t).split(";")]
|
||||||
|
out=sp("out"); inc=sp("incoming"); fl=[int(v) for v in g("flags").split(",")]
|
||||||
|
N={i:dict(x=X[k],z=Z[k],out=out[k],inc=inc[k],fl=fl[k]) for k,i in enumerate(ids)}
|
||||||
|
mk=[(int(a),b) for a,b in re.findall(r'<mm\d+ id="(\d+)" name="([^"]*)"',s)]
|
||||||
|
return s,N,mk
|
||||||
|
def ang(N,a,b,c):
|
||||||
|
A,B,C=N[a],N[b],N[c]
|
||||||
|
v1=(B["x"]-A["x"],B["z"]-A["z"]); v2=(C["x"]-B["x"],C["z"]-B["z"])
|
||||||
|
l1=math.hypot(*v1); l2=math.hypot(*v2)
|
||||||
|
if l1<1e-3 or l2<1e-3: return 0,l1,l2
|
||||||
|
d=(v1[0]*v2[0]+v1[1]*v2[1])/(l1*l2)
|
||||||
|
return math.degrees(math.acos(max(-1,min(1,d)))),l1,l2
|
||||||
|
if __name__=="__main__":
|
||||||
|
s,N,mk=load(sys.argv[1])
|
||||||
|
print("noder",len(N),"markører",len(mk),"flags",collections.Counter(n["fl"] for n in N.values()))
|
||||||
|
sharp=[];rev=0
|
||||||
|
for b,n in N.items():
|
||||||
|
for a in n["inc"]:
|
||||||
|
for c in n["out"]:
|
||||||
|
if a==c or a not in N or c not in N: continue
|
||||||
|
# samme retning som forbindelse? tjek at a->b og b->c er reelle kanter
|
||||||
|
if b not in N[a]["out"]: continue
|
||||||
|
t,l1,l2=ang(N,a,b,c)
|
||||||
|
if t>135: rev+=1; continue # U-vending / bakke-par, ikke et sving
|
||||||
|
if t>45: sharp.append((round(t),a,b,c,len(n["inc"]),len(n["out"]),round(l1,1),round(l2,1)))
|
||||||
|
kinds=collections.Counter("kæde" if (x[4]==1 and x[5]==1) else "kryds" for x in sharp)
|
||||||
|
print("sving >45°:",len(sharp),kinds,"(>135° udeladt:",rev,")")
|
||||||
|
print("fordeling:",collections.Counter(min(x[0]//15*15,135) for x in sharp))
|
||||||
|
nodes={x[2] for x in sharp}; print("unikke knæk-noder",len(nodes))
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""Savegame2 30/9: reparationer af AD-vejnettet fundet ved gennemgangen.
|
||||||
|
|
||||||
|
1. Punkt 19564 (spor til 'P Forage Wagon') lå 5,6 km væk -> midt mellem naboerne.
|
||||||
|
2. Biogas Drop 1/2/4/5 var blindgyder (kun vej ind) -> sidste kant gøres tovejs som Drop 3.
|
||||||
|
3. Chicken Wait 5: 81° knæk ved 12808 (AD tillader 80°) -> privat spor 12808..12815 lagt om i en blød kurve.
|
||||||
|
4. 'Food And More' lå på en afbrudt stump oven på vejen -> markøren flyttes til vejpunkt 8416.
|
||||||
|
|
||||||
|
Brug: netfix.py <ind.xml> <ud.xml>
|
||||||
|
"""
|
||||||
|
import math, re, sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from adsmooth import read, write, link, dist
|
||||||
|
|
||||||
|
src, dst = sys.argv[1], sys.argv[2]
|
||||||
|
doc = read(src); N = doc["N"]
|
||||||
|
mk = {m.group(2): int(float(m.group(1))) for m in re.finditer(r"<mm\d+>\s*<id>([\d.]+)</id>\s*<name>([^<]*)</name>", doc["s"])}
|
||||||
|
|
||||||
|
# 1
|
||||||
|
a, b = N[19563], N[19565]
|
||||||
|
assert dist(N[19564], a) > 1000, "19564 ser allerede rigtig ud"
|
||||||
|
for ax in "xyz":
|
||||||
|
N[19564][ax] = (a[ax] + b[ax]) / 2
|
||||||
|
print("19564 ->", round(N[19564]["x"], 2), round(N[19564]["z"], 2))
|
||||||
|
|
||||||
|
# 2
|
||||||
|
for name in ("Biogas Drop 1", "Biogas Drop 2", "Biogas Drop 4", "Biogas Drop 5"):
|
||||||
|
w = mk[name]; assert not N[w]["out"], name
|
||||||
|
for p in list(N[w]["inc"]):
|
||||||
|
link(N, w, p)
|
||||||
|
print(name, "tovejs mod", N[w]["out"])
|
||||||
|
|
||||||
|
# 3 kurve fra 12802 (retning fra 12801) til 12697 (retning mod 12698)
|
||||||
|
chain = [12808, 12809, 12810, 12811, 12812, 12813, 12814, 12815]
|
||||||
|
for i in chain:
|
||||||
|
assert len(N[i]["inc"]) == 1 and len(N[i]["out"]) == 1, i
|
||||||
|
P0, P3 = N[12802], N[12697]
|
||||||
|
def unit(u, v):
|
||||||
|
L = dist(u, v); return ((v["x"] - u["x"]) / L, (v["z"] - u["z"]) / L)
|
||||||
|
t0, t3 = unit(N[12801], P0), unit(P3, N[12698])
|
||||||
|
seq = [12801, 12802] + chain + [12697, 12698]
|
||||||
|
def ang(p, b, c):
|
||||||
|
v1 = (N[b]["x"] - N[p]["x"], N[b]["z"] - N[p]["z"]); v2 = (N[c]["x"] - N[b]["x"], N[c]["z"] - N[b]["z"])
|
||||||
|
return math.degrees(math.acos(max(-1, min(1, (v1[0] * v2[0] + v1[1] * v2[1]) / (math.hypot(*v1) * math.hypot(*v2))))))
|
||||||
|
def shape(k0, k3, g):
|
||||||
|
P1 = {"x": P0["x"] + t0[0] * k0, "z": P0["z"] + t0[1] * k0}
|
||||||
|
P2 = {"x": P3["x"] - t3[0] * k3, "z": P3["z"] - t3[1] * k3}
|
||||||
|
n = len(chain) + 1
|
||||||
|
for j, i in enumerate(chain, 1):
|
||||||
|
t = (j / n) ** g # g skubber punkterne mod den skarpe ende
|
||||||
|
for ax in "xz":
|
||||||
|
N[i][ax] = (1 - t) ** 3 * P0[ax] + 3 * (1 - t) ** 2 * t * P1[ax] + 3 * (1 - t) * t * t * P2[ax] + t ** 3 * P3[ax]
|
||||||
|
N[i]["y"] = P0["y"] + (P3["y"] - P0["y"]) * t
|
||||||
|
return max(ang(*seq[i:i + 3]) for i in range(len(seq) - 2))
|
||||||
|
D = dist(P0, P3)
|
||||||
|
best = min(((shape(a * D, b * D, g), a, b, g) for a in (0.2, 0.35, 0.5, 0.7, 0.9, 1.2) for b in (0.2, 0.35, 0.5, 0.7, 0.9, 1.2) for g in (0.7, 0.85, 1.0, 1.2)))
|
||||||
|
worst = shape(best[1] * D, best[2] * D, best[3])
|
||||||
|
print("Chicken Wait 5-kurve: største knæk %.1f°" % worst)
|
||||||
|
assert worst <= 45
|
||||||
|
|
||||||
|
write(doc, dst)
|
||||||
|
# 4 markør
|
||||||
|
s = open(dst, encoding="utf-8-sig").read()
|
||||||
|
s, cnt = re.subn(r"(<id>)8481\.000000(</id>\s*<name>Food And More</name>)", r"\g<1>8416.000000\g<2>", s)
|
||||||
|
assert cnt == 1
|
||||||
|
open(dst, "wb").write(b"\xef\xbb\xbf" + s.encode("utf-8"))
|
||||||
|
print("Food And More -> 8416")
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
"""Savegame2 30/9: parkering til alle køretøjer + Sell Slurry-vognfejlen.
|
||||||
|
|
||||||
|
- REX-vintraktorer: parkDestination = deres vinredskabs parkering (faste sæt).
|
||||||
|
- Motorkøretøjer uden plads (FH16 x3, Finsnitter): ledig bås omdøbes "P <navn>", parkDestination sættes.
|
||||||
|
Køretøjet flyttes ikke — AutoDrive bakker det ind, når det parkeres.
|
||||||
|
- Frakoblede vogne uden plads: ledig bås + "Connect P <navn>" (nyt punkt delt ind på bakkesporet ved
|
||||||
|
vognens trækøje) og vognen stilles ind i båsen, bagende 0,5 m fra P.
|
||||||
|
- Sell Slurry 2: ploven kobles af og stilles på "P Plov"; flowets vogn bliver gylletanken på
|
||||||
|
"P Sell Slurry vogn 2".
|
||||||
|
|
||||||
|
Skriver <fil>.new i savegame-mappen. Brug: parkall.py <savegame-mappe>
|
||||||
|
"""
|
||||||
|
import math, os, re, sys, zipfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from adsmooth import read, write, apply_splits, dist
|
||||||
|
|
||||||
|
SG = sys.argv[1].rstrip("/") + "/"
|
||||||
|
GAME = os.path.expanduser("~/.local/share/Steam/steamapps/common/Farming Simulator 25/")
|
||||||
|
MODS = os.path.expanduser("~/FS25-data/mods/")
|
||||||
|
REAR_GAP = 0.5 # vognens bagende så langt fra P (m)
|
||||||
|
KINGPIN_BACK = 1.2 # sættevogn: kongebolt bag forenden (ukalibreret skøn)
|
||||||
|
|
||||||
|
# motorkøretøjer: driverName -> kolonne; vogne: fil -> (navn, kolonne, sættevogn?)
|
||||||
|
MOTOR = ["FH16 780 1", "FH16 780 2", "Milk Sale", "Finsnitter"]
|
||||||
|
TRAILERS = {"dpw1800": ("DPW 1800", "syd", False), "bigBodyS750": ("Big Body 750", "syd", False),
|
||||||
|
"gtw330": ("Overlæssevogn GTW 330", "syd", False), "steelDropDeck": ("Steel Drop Deck", "vest", True)}
|
||||||
|
|
||||||
|
|
||||||
|
def xmltext(fn):
|
||||||
|
if fn.startswith("data/"):
|
||||||
|
return open(GAME + fn, encoding="utf-8", errors="ignore").read()
|
||||||
|
mod, rest = fn.replace("$moddir$", "").split("/", 1)
|
||||||
|
return zipfile.ZipFile(MODS + mod + ".zip").read(rest).decode("utf-8", "ignore")
|
||||||
|
|
||||||
|
|
||||||
|
def size_of(fn):
|
||||||
|
m = re.search(r"<size [^>]*>", xmltext(fn)).group(0)
|
||||||
|
L = float(re.search(r'length="([\d.]+)"', m).group(1))
|
||||||
|
lo = re.search(r'lengthOffset="(-?[\d.]+)"', m)
|
||||||
|
return L, float(lo.group(1)) if lo else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def heading(el):
|
||||||
|
r = [float(t) for t in el.find("component").get("rotation").split()]
|
||||||
|
return math.radians(r[1] + (180 if abs(r[0]) > 90 else 0))
|
||||||
|
|
||||||
|
|
||||||
|
def angn(a):
|
||||||
|
return (a + math.pi) % (2 * math.pi) - math.pi
|
||||||
|
|
||||||
|
|
||||||
|
doc = read(SG + "AutoDrive_config.xml"); N = doc["N"]
|
||||||
|
mk = {} # mm-index -> dict(wp, name, group)
|
||||||
|
for m in re.finditer(r"<mm(\d+)>\s*<id>([\d.]+)</id>\s*<name>([^<]*)</name>\s*<group>([^<]*)</group>", doc["s"]):
|
||||||
|
mk[int(m.group(1))] = dict(wp=int(float(m.group(2))), name=m.group(3), group=m.group(4))
|
||||||
|
byname = {m["name"]: i for i, m in mk.items()}
|
||||||
|
|
||||||
|
VT = ET.parse(SG + "vehicles.xml"); VR = VT.getroot()
|
||||||
|
veh = {v.get("uniqueId"): v for v in VR.iter("vehicle")}
|
||||||
|
def adname(v):
|
||||||
|
ad = v.find("AutoDrive"); return ad.get("driverName") if ad is not None else None
|
||||||
|
def park_of(v):
|
||||||
|
ad = v.find("AutoDrive"); p = ad.get("parkDestination") if ad is not None else None
|
||||||
|
return int(p) if p and p.lstrip("-").isdigit() and int(p) > 0 else None
|
||||||
|
def set_park(v, idx):
|
||||||
|
ad = v.find("AutoDrive")
|
||||||
|
if ad is None:
|
||||||
|
last = list(v)[-1]; ad = ET.SubElement(v, "AutoDrive"); ad.tail = last.tail; last.tail = "\n "
|
||||||
|
ad.set("parkDestination", str(idx))
|
||||||
|
parent = {a.get("attachedVehicleUniqueId"): v.get("uniqueId") for v in VR.iter("vehicle") for a in v.iter("attachedImplement")}
|
||||||
|
|
||||||
|
|
||||||
|
def bay_axis(p_wp):
|
||||||
|
"""Retning fra P ud mod båsens munding, og sporets noder sorteret efter afstand fra P."""
|
||||||
|
P = N[p_wp]
|
||||||
|
# nabo-kæden fra P: gå til nabo længst væk, 6 skridt, og brug den retning
|
||||||
|
cur, prev = p_wp, None
|
||||||
|
for _ in range(6):
|
||||||
|
nb = [x for x in set(N[cur]["out"]) | set(N[cur]["inc"]) if x != prev]
|
||||||
|
if not nb: break
|
||||||
|
nxt = max(nb, key=lambda x: dist(N[x], P)); prev, cur = cur, nxt
|
||||||
|
e = math.atan2(N[cur]["x"] - P["x"], N[cur]["z"] - P["z"])
|
||||||
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
def along_lat(p_wp, e, x, z):
|
||||||
|
dx, dz = x - N[p_wp]["x"], z - N[p_wp]["z"]
|
||||||
|
return dx * math.sin(e) + dz * math.cos(e), dx * math.cos(e) - dz * math.sin(e)
|
||||||
|
|
||||||
|
|
||||||
|
def split_point_on_track(p_wp, e, d):
|
||||||
|
"""(u, v, afstand fra min(u,v)) for punktet d m fra P på bakkesporet."""
|
||||||
|
track = []
|
||||||
|
for i, n in N.items():
|
||||||
|
a, l = along_lat(p_wp, e, n["x"], n["z"])
|
||||||
|
if abs(l) < 0.35 and -0.1 < a < 26:
|
||||||
|
track.append((a, i))
|
||||||
|
track.sort()
|
||||||
|
for (a1, i1), (a2, i2) in zip(track, track[1:]):
|
||||||
|
linked = i2 in N[i1]["out"] or i1 in N[i2]["out"]
|
||||||
|
if linked and a1 <= d <= a2:
|
||||||
|
u, v = min(i1, i2), max(i1, i2)
|
||||||
|
du = d - (a1 if u == i1 else a2)
|
||||||
|
return u, v, round(abs(du), 3)
|
||||||
|
raise SystemExit(f"fandt ikke sporet {d:.1f} m fra P (wp {p_wp})")
|
||||||
|
|
||||||
|
|
||||||
|
def ground_offset(v):
|
||||||
|
c = v.find("component"); x, y, z = [float(t) for t in c.get("position").split()]
|
||||||
|
near = min(N.values(), key=lambda n: (n["x"] - x) ** 2 + (n["z"] - z) ** 2)
|
||||||
|
return y - near["y"]
|
||||||
|
|
||||||
|
|
||||||
|
def place(v, p_wp, e, root_along):
|
||||||
|
"""Stil køretøjet med roden root_along m fra P langs e, front mod mundingen."""
|
||||||
|
tx = N[p_wp]["x"] + root_along * math.sin(e); tz = N[p_wp]["z"] + root_along * math.cos(e)
|
||||||
|
ty = N[p_wp]["y"] + ground_offset(v)
|
||||||
|
cs = v.findall("component"); p0 = [float(t) for t in cs[0].get("position").split()]
|
||||||
|
d = angn(e - heading(v))
|
||||||
|
for c in cs:
|
||||||
|
q = [float(t) for t in c.get("position").split()]; r = [float(t) for t in c.get("rotation").split()]
|
||||||
|
rx, rz = q[0] - p0[0], q[2] - p0[2]
|
||||||
|
nx = tx + rx * math.cos(d) + rz * math.sin(d); nz = tz - rx * math.sin(d) + rz * math.cos(d)
|
||||||
|
c.set("position", "%.3f %.3f %.3f" % (nx, q[1] - p0[1] + ty, nz))
|
||||||
|
r[1] = (r[1] + math.degrees(d) + 180) % 360 - 180
|
||||||
|
c.set("rotation", "%.2f %.2f %.2f" % tuple(r))
|
||||||
|
|
||||||
|
|
||||||
|
free = {"syd": [], "vest": []}
|
||||||
|
for i, m in mk.items():
|
||||||
|
if re.fullmatch(r"P Ledig \d+", m["name"]):
|
||||||
|
x, z = N[m["wp"]]["x"], N[m["wp"]]["z"]
|
||||||
|
if -2328 < z < -2320 and x > 1003: free["syd"].append(i)
|
||||||
|
elif 905 < x < 920: free["vest"].append(i)
|
||||||
|
free["syd"].sort(key=lambda i: N[mk[i]["wp"]]["x"])
|
||||||
|
free["vest"].sort(key=lambda i: N[mk[i]["wp"]]["z"])
|
||||||
|
renames, connects, log = {}, [], []
|
||||||
|
|
||||||
|
# 1) REX -> vinredskabets plads
|
||||||
|
for v in VR.iter("vehicle"):
|
||||||
|
if v.get("filename", "").endswith("seriesREX4.xml") and park_of(v) is None:
|
||||||
|
kids = [a.get("attachedVehicleUniqueId") for a in v.iter("attachedImplement")]
|
||||||
|
pk = [park_of(veh[k]) for k in kids if k in veh and park_of(veh[k])]
|
||||||
|
if pk:
|
||||||
|
set_park(v, pk[0]); log.append(f"{adname(v)} -> {mk[pk[0]]['name']} (fast sæt)")
|
||||||
|
|
||||||
|
# 2) motorkøretøjer
|
||||||
|
for v in VR.iter("vehicle"):
|
||||||
|
if adname(v) in MOTOR and park_of(v) is None:
|
||||||
|
slot = free["vest"].pop(0); renames[slot] = "P " + adname(v); set_park(v, slot)
|
||||||
|
log.append(f"{adname(v)} -> {mk[slot]['name']} => 'P {adname(v)}'")
|
||||||
|
|
||||||
|
# 3) frakoblede vogne
|
||||||
|
req, pending = {}, []
|
||||||
|
for v in VR.iter("vehicle"):
|
||||||
|
f = v.get("filename", "").split("/")[-1].replace(".xml", "")
|
||||||
|
if f in TRAILERS and park_of(v) is None and v.get("uniqueId") not in parent:
|
||||||
|
name, col, semi = TRAILERS[f]
|
||||||
|
slot = free[col].pop(0); pw = mk[slot]["wp"]; e = bay_axis(pw)
|
||||||
|
L, lo = size_of(v.get("filename"))
|
||||||
|
root = REAR_GAP - lo + L / 2
|
||||||
|
d = REAR_GAP + L - (KINGPIN_BACK if semi else 0.0)
|
||||||
|
u, w, du = split_point_on_track(pw, e, d)
|
||||||
|
req.setdefault((u, w), set()).add(du)
|
||||||
|
place(v, pw, e, root); set_park(v, slot)
|
||||||
|
renames[slot] = "P " + name
|
||||||
|
pending.append((slot, name, (u, w, du), d))
|
||||||
|
log.append(f"{name} ({f}) -> {mk[slot]['name']} => 'P {name}', Connect {d:.2f} m, rod {root:.2f} m")
|
||||||
|
|
||||||
|
# 4) Sell Slurry: plov af, gylletank som flowets vogn
|
||||||
|
plow_park = byname["P Plov"]; tank_park = byname["P Sell Slurry vogn 2"]
|
||||||
|
plow = next(v for v in VR.iter("vehicle") if park_of(v) == plow_park)
|
||||||
|
tank = next(v for v in VR.iter("vehicle") if park_of(v) == tank_park)
|
||||||
|
tractor = veh[parent[plow.get("uniqueId")]]
|
||||||
|
aj = tractor.find("attacherJoints")
|
||||||
|
for a in list(aj):
|
||||||
|
if a.get("attachedVehicleUniqueId") == plow.get("uniqueId"):
|
||||||
|
aj.remove(a)
|
||||||
|
pw = mk[plow_park]["wp"]; e = bay_axis(pw)
|
||||||
|
cw = mk[byname["Connect P Plov"]]["wp"]; dcon, _ = along_lat(pw, e, N[cw]["x"], N[cw]["z"])
|
||||||
|
L, lo = size_of(plow.get("filename"))
|
||||||
|
place(plow, pw, e, dcon - 0.1 - lo - L / 2) # liftmonteret: forkant + 0,1 m = Connect
|
||||||
|
log.append(f"plov koblet af '{adname(tractor)}' og stillet på P Plov (Connect {dcon:.2f} m)")
|
||||||
|
RP = SG + "ADSmartPickup_runs.xml"; runs = open(RP, encoding="utf-8-sig", newline="").read() # bevar CRLF
|
||||||
|
m = re.search(r'(<run [^>]*name="Sell Slurry"[^>]*wagons=")([^"]*)(")', runs)
|
||||||
|
pairs = dict(p.split("=") for p in m.group(2).split(";") if "=" in p)
|
||||||
|
pairs[tractor.get("uniqueId")] = tank.get("uniqueId")
|
||||||
|
runs = runs[:m.start(2)] + ";".join(f"{k}={pairs[k]}" for k in sorted(pairs)) + runs[m.end(2):]
|
||||||
|
log.append(f"Sell Slurry: '{adname(tractor)}' får gylletanken på P Sell Slurry vogn 2")
|
||||||
|
|
||||||
|
# skriv AD-config: del spor, så markørnavne + nye Connect-markører
|
||||||
|
made = apply_splits(N, req)
|
||||||
|
write(doc, SG + "AutoDrive_config.xml.new")
|
||||||
|
s = open(SG + "AutoDrive_config.xml.new", encoding="utf-8-sig").read()
|
||||||
|
bom = open(SG + "AutoDrive_config.xml", "rb").read(3) == b"\xef\xbb\xbf"
|
||||||
|
for slot, new in renames.items():
|
||||||
|
s, n = re.subn(r"(<mm%d>\s*<id>[\d.]+</id>\s*<name>)[^<]*(</name>\s*<group>)[^<]*(</group>)" % slot,
|
||||||
|
r"\g<1>%s\g<2>Parkering\g<3>" % new, s)
|
||||||
|
assert n == 1, slot
|
||||||
|
nxt = max(mk) + 1; add = ""
|
||||||
|
for slot, name, key, d in pending:
|
||||||
|
add += (" <mm%d>\n <id>%d.000000</id>\n <name>Connect P %s</name>\n"
|
||||||
|
" <group>Connect</group>\n </mm%d>\n") % (nxt, made[key], name, nxt)
|
||||||
|
nxt += 1
|
||||||
|
i = s.rfind(" </mapmarker>"); s = s[:i] + add + s[i:]
|
||||||
|
open(SG + "AutoDrive_config.xml.new", "wb").write((b"\xef\xbb\xbf" if bom else b"") + s.encode("utf-8"))
|
||||||
|
body = ET.tostring(VR, encoding="unicode")
|
||||||
|
open(SG + "vehicles.xml.new", "w", encoding="utf-8").write('<?xml version="1.0" encoding="utf-8" standalone="no"?>\n' + body + "\n")
|
||||||
|
open(SG + "ADSmartPickup_runs.xml.new", "w", encoding="utf-8", newline="").write("\ufeff" + runs)
|
||||||
|
print("\n".join(log)); print("skrevet .new")
|
||||||
Reference in New Issue
Block a user