# -*- coding: utf-8 -*- """ Лаба 2, рисунок «Продольный профиль трассы» (схематический). Масштабы: горизонтальный 1:5000, вертикальный 1:500 (лист 420x297). Данные (горизонтали, трасса, уклоны, тоннель) берутся из .work/laba2-plan-data.pkl. """ import numpy as np, pickle with open('.work/laba2-plan-data.pkl', 'rb') as f: D = pickle.load(f) crossings, GRADES = D['crossings'], D['GRADES'] S_P1, S_P2, L_total = D['S_P1'], D['S_P2'], D['L_total'] S_A_Z, S_B_Z = D['S_A_Z'], D['S_B_Z'] 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)) 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 align, s_m, curves = D['align'], D['s_m'], D['curves'] RED = '#cc1f2d' # --- геометрия листа (мм), y вверх ------------------------------------------ W_SH, H_SH = 357.0, 247.0 X0 = 40.0 # начало ординат def X(s): return X0 + s / 5.0 # 1:5000 H0 = 246.0 # отметка низа профильной зоны Y0 = 95.0 # y листа для H0 def Y(h): return Y0 + (h - H0) * 2.0 # 1:500 # строки сетки (снизу вверх): (имя, высота мм) ROWS = [('Километры', 7.5), ('Пикеты', 7.5), ('Расстояния', 9.0), ('Отметки земли', 9.5), ('Проект. отметки', 9.5), ('Уклоны, ‰', 11.0), ('План трассы', 12.0)] Y_GRID_BOT, Y_GRID_TOP = 15.0, 15.0 + sum(h for _, h in ROWS) fig = plt = None import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt # Чертёжный шрифт ГОСТ 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' fig = plt.figure(figsize=(W_SH / 25.4, H_SH / 25.4), dpi=250) ax = fig.add_axes([0, 0, 1, 1]) ax.set_xlim(0, W_SH); ax.set_ylim(0, H_SH) ax.axis('off') FS, FSS = 10.6, 9.6 # кегли, pt (2.83 pt = 1 мм) # --- заголовок --------------------------------------------------------------- ax.text(W_SH / 2, H_SH - 11, 'ПРОФИЛЬ ТРАССЫ АВТОМОБИЛЬНОЙ ДОРОГИ А–Б', fontsize=13.5, weight='normal', ha='center', va='center') ax.text(W_SH / 2, H_SH - 18.5, 'Масштабы: горизонтальный 1:5000, вертикальный 1:500 (схематический)', fontsize=FS, ha='center', va='center', color='0.25') # --- сетка ------------------------------------------------------------------- y = Y_GRID_BOT bounds = [y] for name, h in ROWS: y += h bounds.append(y) # горизонтальные линии сетки: верхняя и внутренние жирнее X_R, X_L = X(L_total) + 14.0, X0 - 3.0 for i, b in enumerate(bounds): lw = 0.9 if i in (0, len(bounds) - 1) else 0.45 ax.plot([X_L, X_R], [b, b], color='black', lw=lw) ax.plot([X_L, X_L], [bounds[0], bounds[-1]], color='black', lw=0.9) ax.plot([X_R, X_R], [bounds[0], bounds[-1]], color='black', lw=0.9) # названия строк — слева for (name, h), b0, b1 in zip(ROWS, bounds, bounds[1:]): ax.text(X_L - 0.8, (b0 + b1) / 2, name, fontsize=8.8, ha='right', va='center') # вертикальные линии: пикеты (тонкие), километры (толстые), переломы красной grade_s = [g[0] for g in GRADES] + [L_total] def vline(s, y1, y2, lw=0.35, color='black'): ax.plot([X(s), X(s)], [y1, y2], color=color, lw=lw) for pk in range(0, int(L_total // 100) + 1): s = pk * 100.0 if s > L_total: break vline(s, Y_GRID_TOP, bounds[0]) # ордината сквозь сетку vline(0.0, Y0, bounds[0], lw=0.9) vline(1000.0, Y_GRID_TOP, bounds[0], lw=0.9) vline(L_total, Y_GRID_TOP, bounds[0], lw=0.9) # отметки по левой шкале for h in range(245, 311, 5): ax.plot([X_L - 1.2, X_L], [Y(h), Y(h)], color='black', lw=0.5) ax.text(X_L - 1.8, Y(h), str(h), fontsize=8.6, ha='right', va='center') # горизонтальные сетки профильной зоны (каждые 5 м) for h in range(245, 311, 5): lw = 0.55 if h % 10 == 0 else 0.25 ax.plot([X0, X_R], [Y(h), Y(h)], color='0.55', lw=lw, zorder=0) # --- линия поверхности земли -------------------------------------------------- ss = np.linspace(0, L_total, 400) gs = [ground(s) for s in ss] ax.plot([X(s) for s in ss], [Y(g) for g in gs], color='black', lw=1.0, zorder=3) # фактические отметки на пикетах for pk in range(0, 15): s = pk * 100.0 if s > L_total: break g = ground(s) ha = 'left' if s == 0 else 'center' ax.text(X(s) + (1.0 if s == 0 else 0), (bounds[3] + bounds[4]) / 2, f'{g:.1f}'.replace('.', ','), fontsize=FSS, ha=ha, va='center', zorder=5) # --- проектная (красная) линия ------------------------------------------------ rr = [red_h(s) for s in ss] ax.plot([X(s) for s in ss], [Y(h) for h in rr], color=RED, lw=2.0, zorder=4) breaks = [0.0, 400.0, 700.0, 1060.0, 1265.0, L_total] def cm(v): return f'{v:.2f}'.replace('.', ',') pk_even = [pk * 100.0 for pk in range(0, 15) if pk % 2 == 0 and pk * 100 <= L_total] # проектные отметки (красные): переломы + чётные пикеты; станции ближе 14 мм # на листе разносят по ярусам, иначе соседние цифры сливаются MIN_GAP_MM = 14.0 red_stations = sorted(set(breaks) | set(pk_even)) red_levels, last_x, lvl = [], None, 0 for s in red_stations: x = X(s) lvl = 1 - lvl if last_x is not None and x - last_x < MIN_GAP_MM else 0 red_levels.append(lvl) last_x = x # над красной линией; белая подложка отделяет цифры от пересекаемых линий for s, lvl in zip(red_stations, red_levels): ha = 'left' if s == 0 else 'center' ax.text(X(s) + (1.0 if s == 0 else 0), Y(red_h(s)) + (3.4 if s in breaks else 3.0) + 4.2 * lvl, cm(red_h(s)), fontsize=8.8, color=RED, ha=ha, va='center', zorder=6, bbox=dict(boxstyle='round,pad=0.15', fc='white', ec='none')) # проектные отметки в строке сетки for s, lvl in zip(red_stations, red_levels): ha = 'left' if s == 0 else 'center' ax.text(X(s) + (1.0 if s == 0 else 0), (bounds[4] + bounds[5]) / 2 + 2.4 - 4.8 * lvl, cm(red_h(s)), fontsize=8.8, color=RED, ha=ha, va='center', zorder=6, bbox=dict(boxstyle='round,pad=0.12', fc='white', ec='none')) # --- строка уклонов ----------------------------------------------------------- b0, b1 = bounds[5], bounds[6] for (s1, i1), (s2, _i2) in zip(GRADES, GRADES[1:] + [(None, None)]): s2e = L_total if s2 is None else s2 x1, x2 = X(s1), X(s2e) ym = (b0 + b1) / 2 L = x2 - x1 dy = np.clip(L * np.tan(np.radians(6.0)) * np.sign(i1), -2.6, 2.6) ax.annotate('', xy=(x2, ym + dy), xytext=(x1, ym - dy), arrowprops=dict(arrowstyle='-|>', color=RED, lw=1.1, mutation_scale=9)) ax.text((x1 + x2) / 2, ym + 3.6, ('+' if i1 > 0 else '') + f'{i1:g}', fontsize=FSS, color=RED, ha='center', va='center', zorder=6, bbox=dict(boxstyle='round,pad=0.12', fc='white', ec='none')) ax.text((x1 + x2) / 2, ym - 3.4, f'{s2e - s1:.0f}', fontsize=FSS, ha='center', va='center', zorder=6, bbox=dict(boxstyle='round,pad=0.12', fc='white', ec='none')) # --- строка расстояний (между переломами) ------------------------------------- b0, b1 = bounds[2], bounds[3] for s1, s2 in zip(grade_s, grade_s[1:]): ax.text(X((s1 + s2) / 2), (b0 + b1) / 2, f'{s2 - s1:.0f}', fontsize=FSS, ha='center', va='center') # --- строка пикетов ------------------------------------------------------------ b0, b1 = bounds[1], bounds[2] for pk in range(0, 15): s = pk * 100.0 if s > L_total: continue ax.text(X(s), (b0 + b1) / 2 + 0.8, str(pk), fontsize=FSS, ha='center', va='center', bbox=dict(facecolor='white', edgecolor='none', pad=1.2)) ax.text(X(L_total), (b0 + b1) / 2 + 0.8, '14+43', fontsize=8.6, ha='center', va='center', zorder=6, bbox=dict(boxstyle='round,pad=0.12', fc='white', ec='none')) # --- километры ------------------------------------------------------------------ b0, b1 = bounds[0], bounds[1] for km, s in ((0, 0.0), (1, 1000.0)): ax.text(X(s), (b0 + b1) / 2 + 0.3, str(km), fontsize=FSS, ha='center', va='center', bbox=dict(boxstyle='circle,pad=0.18', fc='white', ec='black', lw=0.6)) # --- план трассы в своей строке --------------------------------------------------- b0, b1 = bounds[-2], bounds[-1] ym = (b0 + b1) / 2 ax.plot([X0, X(L_total)], [ym, ym], color='black', lw=0.9) def station_of(pt): d = np.hypot(align[:, 0] - pt[0], align[:, 1] - pt[1]) return s_m[int(np.argmin(d))] for c in curves: sA, sB = station_of(c['A']), station_of(c['B']) x1, x2 = X(sA), X(sB) hh = min(4.2, c['R_m'] / 100.0) t = np.linspace(0, np.pi, 40) xs = np.linspace(x1, x2, 40) ys = ym + hh * np.sin(t) * (1 if c['right'] else -1) ax.plot(xs, ys, color='black', lw=0.9) if c['right']: # подпись сбоку от дуги, чтобы не выходить за строку сетки ax.text(x2 + 1.5, ym, f"R{c['R_m']:.0f}", fontsize=8.4, ha='left', va='center', bbox=dict(facecolor='white', edgecolor='none', pad=1.2)) else: ax.text((x1 + x2) / 2, ym - hh - 1.2, f"R{c['R_m']:.0f}", fontsize=8.4, ha='center', va='top', bbox=dict(facecolor='white', edgecolor='none', pad=1.2)) # тоннель в строке плана: прямоугольник x1, x2 = X(S_P1), X(S_P2) ax.plot([x1, x2, x2, x1, x1], [ym - 3.4, ym - 3.4, ym + 3.4, ym + 3.4, ym - 3.4], color='black', lw=0.8) ax.text((x1 + x2) / 2 - 1.5, ym, 'тоннель', fontsize=8.0, ha='center', va='center', rotation=0, bbox=dict(facecolor='white', edgecolor='none', pad=1.2)) # --- тоннель на профиле --------------------------------------------------------- x1, x2 = X(S_P1), X(S_P2) ytop1, ytop2 = Y(red_h(S_P1)), Y(red_h(S_P2)) dep = 7.0 ax.plot([x1, x2, x2, x1, x1], [ytop1, ytop2, ytop2 - dep, ytop1 - dep, ytop1], color=RED, lw=1.0) ax.plot([x1, x1], [ytop1, Y(ground(S_P1))], color=RED, lw=0.9) ax.plot([x2, x2], [ytop2, Y(ground(S_P2))], color=RED, lw=0.9) ax.text((x1 + x2) / 2, (ytop1 + ytop2) / 2 - dep / 2, 'Тоннель, 205 м', fontsize=8.6, color=RED, ha='center', va='center') ax.text(x1 - 1.0, Y(ground(S_P1)) + 3.0, 'портал', fontsize=8.0, color='0.3', ha='right', va='center') ax.text(x2 + 1.0, Y(ground(S_P2)) + 3.0, 'портал', fontsize=8.0, color='0.3', ha='left', va='center') # рабочие отметки у порталов (глубина выемок) for s in (S_P1, S_P2): w = ground(s) - red_h(s) ax.text(X(s) + (2.4 if s == S_P2 else -2.4), Y(red_h(s)) - 3.2, f'{w:.1f}'.replace('.', ','), fontsize=8.6, color='0.25', ha='left' if s == S_P2 else 'right', va='center') fig.savefig('assets/images/laba2-profil.png', dpi=250, facecolor='white') print('сохранено: assets/images/laba2-profil.png')