| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- # -*- coding: utf-8 -*-
- """
- Лаба 2, рисунок «План трассы». Горизонтали векторизуются из скана варианта 6
- (.work/laba2-contours-paths.pkl, координаты в пикселях кадра карты),
- затем на карту наносится трасса А–Б: тангенсы с круговыми кривыми,
- пикетаж, тоннель у водораздела.
- Схема трассы задаётся списком вершин углов поворота ВУ (пиксели кадра) и
- радиусами кривых; всё остальное (пикетаж, пересечения горизонталей, элементы
- кривых) вычисляется. Данные профиля сохраняются в .work/laba2-profile-data.pkl.
- """
- import numpy as np, pickle, json, math
- with open('.work/laba2-contours-paths.pkl', 'rb') as f:
- raw = pickle.load(f)
- # --- 1. Склейка разорванных горизонталей и упорядочивание -------------------
- # (пути 11+10 и 12+13 разорваны маркером пункта А)
- contours = [
- np.vstack([raw[11], raw[10]]), # 250 (через пункт А)
- np.vstack([raw[12], raw[13]]), # 240 (нижняя)
- ] + [raw[i] for i in range(10)] # остальные, с юга на север: p9..p0
- # юг -> север по положению на западном крае (x < 170)
- def west_y(p):
- m = (p[:, 0] >= 20) & (p[:, 0] <= 170)
- return np.mean(p[m, 1]) if m.any() else p[:, 1].mean()
- contours.sort(key=west_y, reverse=True) # южные первыми (y вниз)
- # южный склон 240->290, водораздел 300 (седьмая линия), северный склон 290->250
- ELEV = [240, 250, 260, 270, 280, 290, 300, 290, 280, 270, 260, 250]
- # проверка унимодальности отметок на вертикальных разрезах
- for xq in (60, 200, 400, 600, 760):
- seq = []
- for p, z in zip(contours, ELEV):
- m = np.abs(p[:, 0] - xq) < 30
- if m.any():
- seq.append((np.mean(p[m, 1]), z))
- seq.sort()
- zs = [z for _, z in seq]
- peak = zs.index(max(zs))
- ok = zs[:peak+1] == sorted(zs[:peak+1]) and zs[peak:] == sorted(zs[peak:], reverse=True)
- print(f'x={xq}: {zs} {"OK" if ok else "!! НЕ унимодально"}')
- # --- 2. Масштаб --------------------------------------------------------------
- PX_PER_KM = 262.7 # кадр карты 788 px = 3.0 км (масштаб 1:10 000)
- M_PER_PX = 1000.0 / PX_PER_KM
- # --- 3. Сглаживание горизонталей --------------------------------------------
- def smooth(p, k=7):
- if len(p) < 2 * k + 1:
- return p
- ker = np.hanning(k)
- ker /= ker.sum()
- sm = np.empty_like(p)
- for c in (0, 1):
- sm[:, c] = np.convolve(p[:, c], ker, mode='same')
- sm[:k//2] = p[:k//2]; sm[-(k//2):] = p[-(k//2):]
- return sm
- contours = [smooth(p) for p in contours]
- # --- 4. Трасса: тангенсы и круговые кривые ----------------------------------
- # Вершины углов поворота (пиксели кадра, y вниз); НТ = начало трассы (А),
- # КТ = конец трассы (Б). (A, R): None для первого/последнего участка
- VUS = [
- ((260.0, 402.0), None), # НТ = А
- ((150.0, 372.0), 300.0), # ВУ1
- ((105.0, 255.0), 600.0), # ВУ2
- ((55.0, 165.0), 250.0), # ВУ3
- ((18.0, 142.0), None), # КТ = Б
- ]
- def build_alignment(vus):
- pts = [] # плотная полилиния оси (px)
- pieces = [] # (тип, геометрия, длина_м)
- P = [np.array(v, float) for v, _ in vus]
- R = [r for _, r in vus]
- elem = [] # элементы кривых для надписей
- # 1) тангенсы и дуги
- segs = [] # последовательность ('l', A, B) | ('a', C, A, B, R, ccw)
- cur = P[0]
- for i in range(1, len(P) - 1):
- V, Pn = P[i], P[i + 1]
- u1 = (V - cur) / np.linalg.norm(V - cur)
- u2 = (Pn - V) / np.linalg.norm(Pn - V)
- cosA = np.clip(np.dot(u1, u2), -1, 1)
- d = math.acos(cosA) # угол поворота
- Rp = R[i] / M_PER_PX # радиус в px
- T = Rp * math.tan(d / 2)
- A = V - u1 * T
- B = V + u2 * T
- cross = u1[0] * u2[1] - u1[1] * u2[0] # >0: поворот по часовой (y вниз)
- n = np.array([-u1[1], u1[0]]) * np.sign(cross)
- C = A + n * Rp
- segs.append(('l', cur, A))
- segs.append(('a', C, A, B, Rp, cross < 0))
- elem.append({'i': i, 'V': V, 'A': A, 'B': B, 'C': C, 'R_px': Rp,
- 'd': math.degrees(d), 'T_px': T,
- 'K_m': Rp * d * M_PER_PX, 'T_m': T * M_PER_PX,
- 'R_m': R[i], 'right': cross < 0})
- cur = B
- segs.append(('l', cur, P[-1]))
- # 2) плотная выборка и длины
- total_px = 0.0
- dense = []
- for s in segs:
- if s[0] == 'l':
- _, a, b = s
- L = np.linalg.norm(b - a)
- n = max(2, int(L))
- t = np.linspace(0, 1, n)
- pts.append(a[None, :] + t[:, None] * (b - a)[None, :])
- total_px += L
- else:
- _, C, a, b, Rp, ccw = s
- a0 = math.atan2(a[1] - C[1], a[0] - C[0])
- a1 = math.atan2(b[1] - C[1], b[0] - C[0])
- da = a1 - a0
- if ccw:
- while da > 0: da -= 2 * math.pi
- else:
- while da < 0: da += 2 * math.pi
- n = max(4, int(abs(da) * Rp))
- t = np.linspace(a0, a0 + da, n)
- pts.append(C[None, :] + Rp * np.column_stack([np.cos(t), np.sin(t)]))
- total_px += abs(da) * Rp
- align = np.vstack(pts)
- s_m = np.r_[0, np.cumsum(np.hypot(*np.diff(align, axis=0).T))] * M_PER_PX
- return align, s_m, segs, elem
- align, s_m, segs, curves = build_alignment(VUS)
- L_total = s_m[-1]
- print(f'длина трассы: {L_total:.0f} м; кривых: {len(curves)}')
- for c in curves:
- print(f" ВУ{c['i']}: У={c['d']:.1f}° R={c['R_m']} T={c['T_m']:.1f} K={c['K_m']:.1f} "
- f"{'вправо' if c['right'] else 'влево'}")
- # --- 5. Пересечения трассы с горизонталями ----------------------------------
- from scipy.spatial import cKDTree
- trees = [cKDTree(p) for p in contours]
- def elev_along(points):
- """ближайшая горизонталь -> отметка (ступенчатая), для контроля"""
- z = np.empty(len(points))
- for j, tree in enumerate(trees):
- pass
- d = np.stack([tree.query(points)[0] for tree in trees])
- return np.array(ELEV)[np.argmin(d, axis=0)]
- crossings = [] # (s, z)
- zone_prev = False
- idx = trees_ind = np.zeros(len(align), int)
- D = np.stack([tree.query(align)[0] for tree in trees])
- nearest = np.argmin(D, axis=0)
- near = D.min(axis=0)
- inzone = near < 1.1
- runs = []
- i = 0
- while i < len(align):
- if inzone[i]:
- j = i
- while j < len(align) and inzone[j]:
- j += 1
- runs.append((i, j))
- i = j
- else:
- i += 1
- for a, b in runs:
- mid = (a + b) // 2
- crossings.append((s_m[mid], ELEV[nearest[mid]], a, b))
- print('пересечения с горизонталями:')
- for s, z, a, b in crossings:
- print(f' s={s:7.1f} м z={z} м (px {a}..{b})')
- # --- 6. Поверхность земли по пересечениям -----------------------------------
- # отметки на концах трассы: А стоит на горизонтали 250, Б — между 280 и 270
- S_A_Z, S_B_Z = 250.0, 277.5
- def ground(s):
- cs = [0.0] + [c[0] for c in crossings] + [L_total]
- zs = [S_A_Z] + [c[1] for c in crossings] + [S_B_Z]
- return float(np.interp(s, cs, zs))
- # --- 7. Проектная (красная) линия: участки уклонов ---------------------------
- # (s_начало, уклон ‰): 2 выемки у порталов тоннеля 1060->1265
- GRADES = [(0.0, 27.0), (400.0, 45.0), (700.0, 10.0), (1060.0, 10.0), (1265.0, -10.0)]
- S_P1, S_P2 = 1060.0, 1265.0 # порталы тоннеля
- def red_h(s):
- h = 250.0
- for (s1, i1), (s2, _i2) in zip(GRADES, GRADES[1:]):
- if s1 <= s < s2:
- return h + (s - s1) * i1 / 1000.0
- h += (s2 - s1) * i1 / 1000.0
- return h + (s - GRADES[-1][0]) * GRADES[-1][1] / 1000.0
- # --- 8. Сохранение данных ---------------------------------------------------
- with open('.work/laba2-plan-data.pkl', 'wb') as f:
- pickle.dump({
- 'contours': contours, 'ELEV': ELEV, 'align': align, 's_m': s_m,
- 'curves': curves, 'crossings': crossings, 'M_PER_PX': M_PER_PX,
- 'A': np.array(VUS[0][0]), 'B': np.array(VUS[-1][0]),
- 'GRADES': GRADES, 'S_P1': S_P1, 'S_P2': S_P2,
- 'S_A_Z': S_A_Z, 'S_B_Z': S_B_Z, 'L_total': L_total,
- }, f)
- print('данные сохранены: .work/laba2-plan-data.pkl')
- # --- 9. Чертёж плана --------------------------------------------------------
- import matplotlib
- matplotlib.use('Agg')
- import matplotlib.pyplot as plt
- import matplotlib.patheffects as pe
- from matplotlib.patches import Circle
- # Чертёжный шрифт ГОСТ 2.304-81 тип А (прямой, из системных шрифтов Windows)
- import matplotlib.font_manager as fm
- fm.fontManager.addfont('C:/Windows/Fonts/GOST2304A.ttf')
- matplotlib.rcParams['font.family'] = 'GOST 2.304 type A'
- matplotlib.rcParams['mathtext.default'] = 'regular'
- RED = '#cc1f2d'
- W, H_PX = 788.0, 468.0 # кадр карты, px
- ML, MR, MT, MB = 12.0, 12.0, 62.0, 96.0 # поля листа вокруг рамки карты
- fig = plt.figure(figsize=((W + ML + MR) / 50, (H_PX + MT + MB) / 50), dpi=200)
- ax = fig.add_axes([0, 0, 1, 1])
- ax.set_xlim(-ML, W + MR)
- ax.set_ylim(H_PX + MB, -MT) # y вниз, как на плане
- ax.axis('off')
- ax.set_facecolor('white')
- fig.patches.append(plt.Rectangle((0, 0), 1, 1, transform=fig.transFigure,
- fill=False, ec='black', lw=1.4))
- # рамка карты
- ax.add_patch(plt.Rectangle((0, 0), W, H_PX, fill=False, ec='black', lw=1.1))
- def m2px(pt):
- return np.asarray(pt, float)
- # километровая сетка через 0,5 км
- step = 500.0 / M_PER_PX
- xg = np.arange(step, W, step)
- yg = np.arange(step, H_PX, step)
- for x in xg:
- ax.plot([x, x], [0, H_PX], color='0.55', lw=0.4, alpha=0.55, zorder=0.5)
- for y in yg:
- ax.plot([0, W], [y, y], color='0.55', lw=0.4, alpha=0.55, zorder=0.5)
- # горизонтали с подписями отметок
- for p, z in zip(contours, ELEV):
- ax.plot(p[:, 0], p[:, 1], color='black', lw=0.75, zorder=1)
- for xq in (120, 360, 600, 740):
- i = np.argmin(np.abs(p[:, 0] - xq))
- if abs(p[i, 0] - xq) > 90 or 4 < p[i, 1] < H_PX - 4:
- pass
- ax.text(p[i, 0] + 3, p[i, 1] - 4, str(z), fontsize=9.5, color='black',
- zorder=3, path_effects=[pe.withStroke(linewidth=2.4, foreground='white')])
- # трасса: участки вне тоннеля
- def s_to_pts(s1, s2):
- i1 = np.searchsorted(s_m, s1)
- i2 = min(len(s_m) - 1, np.searchsorted(s_m, s2))
- return align[i1:i2 + 1]
- tun = s_to_pts(S_P1, S_P2)
- s_tun = s_m[np.searchsorted(s_m, S_P1):np.searchsorted(s_m, S_P2) + 1]
- open1 = s_to_pts(0, S_P1)
- open2 = s_to_pts(S_P2, s_m[-1])
- for seg in (open1, open2):
- ax.plot(seg[:, 0], seg[:, 1], color=RED, lw=2.0, zorder=4, solid_capstyle='round')
- # тоннель: две параллельные линии со штрихами
- d = np.gradient(tun, axis=0)
- nrm = np.column_stack([d[:, 1], -d[:, 0]])
- nrm /= np.hypot(nrm[:, 0], nrm[:, 1])[:, None] + 1e-9
- for sgn in (+1, -1):
- off = tun + sgn * 3.2 * nrm
- ax.plot(off[:, 0], off[:, 1], color=RED, lw=1.3, zorder=4)
- for s in (S_P1, S_P2):
- k = np.searchsorted(s_tun, s)
- base = tun[min(k, len(tun) - 1)]
- ax.plot([base[0] - 6.5 * nrm[k, 0], base[0] + 6.5 * nrm[k, 0]],
- [base[1] - 6.5 * nrm[k, 1], base[1] + 6.5 * nrm[k, 1]],
- color=RED, lw=3.2, zorder=4)
- ax.text(96, 180, 'Тоннель, 205 м',
- fontsize=11, color=RED, zorder=6, path_effects=[pe.withStroke(linewidth=2.6, foreground='white')])
- # пикеты: each 100 м, подпись each 2
- pk = 0
- while pk * 100.0 <= s_m[-1]:
- s = pk * 100.0
- k = np.searchsorted(s_m, s)
- if k >= len(s_m) - 1:
- break
- base = align[k]
- tang = align[min(k + 2, len(s_m) - 1)] - align[max(k - 2, 0)]
- tang = tang / (np.hypot(*tang) + 1e-9)
- nr = np.array([-tang[1], tang[0]]) # вправо по ходу (y вниз)
- L = 7.0 if pk % 2 == 0 else 4.5
- ax.plot([base[0], base[0] + L * nr[0]], [base[1], base[1] + L * nr[1]],
- color=RED, lw=1.4, zorder=4)
- if pk % 2 == 0:
- ax.text(base[0] + (L + 2.5) * nr[0], base[1] + (L + 2.5) * nr[1], str(pk),
- fontsize=9.5, color=RED, ha='center', va='center', zorder=5,
- path_effects=[pe.withStroke(linewidth=2.2, foreground='white')])
- pk += 1
- # километровые знаки
- for km, s in ((1, 1000.0),):
- k = np.searchsorted(s_m, s)
- base = align[k]
- tang = align[min(k + 2, len(s_m) - 1)] - align[max(k - 2, 0)]
- tang /= np.hypot(*tang)
- nr = np.array([tang[1], -tang[0]]) # влево по ходу
- c = base + 20 * nr
- ax.add_patch(Circle(c, 8.5, fill=True, fc='white', ec=RED, lw=1.3, zorder=5))
- ax.text(c[0], c[1], str(km), fontsize=11, color=RED, ha='center', va='center', zorder=6)
- # пункты А и Б
- for pt, name, dx, dy in ((np.array(VUS[0][0]), 'А', 10, 14), (np.array(VUS[-1][0]), 'Б', 9, -2)):
- ax.plot(pt[0], pt[1], marker='o', ms=5, color='black', zorder=6)
- ax.text(pt[0] + dx, pt[1] + dy, name, fontsize=15, weight='normal', color='black', zorder=6,
- path_effects=[pe.withStroke(linewidth=2.4, foreground='white')])
- # вершины углов поворота
- for c in curves:
- ax.plot(c['V'][0], c['V'][1], marker='+', ms=7, color=RED, mew=1.1, zorder=5)
- ax.text(c['V'][0] + 5, c['V'][1] - 5, f"ВУ{c['i']}", fontsize=8.5, color=RED, zorder=6,
- path_effects=[pe.withStroke(linewidth=2.2, foreground='white')])
- # выноски элементов кривых
- def fmt_deg(x):
- dd = int(x); mm = int(round((x - dd) * 60))
- return f"{dd}°{mm:02d}'" if mm else f'{dd}°'
- slots = {1: (240, 470), 2: (300, 290), 3: (140, 210)}
- for c in curves:
- tx, ty = slots[c['i']]
- mid = c['A'] * 0.35 + c['B'] * 0.65
- # начало линии под рамкой выноски: конец скрывается белой подложкой бокса
- ax.annotate('', xy=(mid[0], mid[1]), xytext=(tx + 60, ty + 6),
- arrowprops=dict(arrowstyle='-', color=RED, lw=0.7, shrinkA=0, shrinkB=0))
- ax.text(tx, ty, f"Кривая {c['i']}: У = {fmt_deg(c['d'])}; R = {c['R_m']:.0f} м\n"
- f"Т = {c['T_m']:.1f} м; К = {c['K_m']:.1f} м".replace('.', ','),
- fontsize=9.5, color=RED, va='top', zorder=6,
- bbox=dict(boxstyle='square,pad=0.25', fc='white', ec='0.6', lw=0.5, alpha=0.92))
- # стрелка севера
- nx, ny = W - 34, 34
- ax.annotate('', xy=(nx, ny - 26), xytext=(nx, ny + 8),
- arrowprops=dict(arrowstyle='-|>', color='black', lw=1.4, mutation_scale=16))
- ax.text(nx, ny + 20, 'С', fontsize=12, ha='center', weight='normal')
- # заголовок и подписи под рамкой
- ax.text((W + ML + MR) / 2 - ML, -34, 'ПЛАН ТРАССЫ АВТОМОБИЛЬНОЙ ДОРОГИ А–Б',
- fontsize=15.5, weight='normal', ha='center', va='center')
- ax.text((W + ML + MR) / 2 - ML, -13, '(трассирование по плану в горизонталях, вариант 6)',
- fontsize=11, ha='center', va='center', color='0.25')
- # условные обозначения (двумя рядами) и масштаб (справа)
- ly = H_PX + 26
- ax.text(6, ly - 16, 'Условные обозначения:', fontsize=10.5, weight='normal')
- ax.text(W - 4, ly - 16, 'Масштаб 1:10 000', fontsize=11.5, ha='right', weight='normal')
- # ряд 1
- ax.plot([10, 52], [ly + 2, ly + 2], color=RED, lw=2.0)
- ax.text(58, ly + 2, 'трасса (принятый вариант)', fontsize=10, va='center')
- ax.plot([268, 298], [ly - 2, ly - 2], color=RED, lw=1.1)
- ax.plot([268, 298], [ly + 4, ly + 4], color=RED, lw=1.1)
- ax.text(304, ly + 2, 'тоннель', fontsize=10, va='center')
- ax.plot([392, 392], [ly - 4, ly + 6], color=RED, lw=1.4)
- ax.text(399, ly + 2, 'пикет', fontsize=10, va='center')
- ax.add_patch(Circle((475, ly + 2), 7, fc='white', ec=RED, lw=1.2))
- ax.text(475, ly + 2, '1', fontsize=9.5, color=RED, ha='center', va='center')
- ax.text(487, ly + 2, 'километровый знак', fontsize=10, va='center')
- # ряд 2
- ax.plot([10, 60], [ly + 26, ly + 26], color='black', lw=0.75)
- ax.text(66, ly + 26, 'горизонталь и её отметка, м', fontsize=10, va='center')
- ax.text(W - 4, ly + 26, 'горизонтали проведены через 10 м; координатная сетка через 500 м; пикеты через 100 м',
- fontsize=10, ha='right')
- fig.savefig('assets/images/laba2-plan-trassy.png', dpi=200, facecolor='white')
- print('сохранено: assets/images/laba2-plan-trassy.png')
|