fig-laba2-profile.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. # -*- coding: utf-8 -*-
  2. """
  3. Лаба 2, рисунок «Продольный профиль трассы» (схематический).
  4. Масштабы: горизонтальный 1:5000, вертикальный 1:500 (лист 420x297).
  5. Данные (горизонтали, трасса, уклоны, тоннель) берутся из .work/laba2-plan-data.pkl.
  6. """
  7. import numpy as np, pickle
  8. with open('.work/laba2-plan-data.pkl', 'rb') as f:
  9. D = pickle.load(f)
  10. crossings, GRADES = D['crossings'], D['GRADES']
  11. S_P1, S_P2, L_total = D['S_P1'], D['S_P2'], D['L_total']
  12. S_A_Z, S_B_Z = D['S_A_Z'], D['S_B_Z']
  13. def ground(s):
  14. cs = [0.0] + [c[0] for c in crossings] + [L_total]
  15. zs = [S_A_Z] + [c[1] for c in crossings] + [S_B_Z]
  16. return float(np.interp(s, cs, zs))
  17. def red_h(s):
  18. h = 250.0
  19. for (s1, i1), (s2, _i2) in zip(GRADES, GRADES[1:]):
  20. if s1 <= s < s2:
  21. return h + (s - s1) * i1 / 1000.0
  22. h += (s2 - s1) * i1 / 1000.0
  23. return h + (s - GRADES[-1][0]) * GRADES[-1][1] / 1000.0
  24. align, s_m, curves = D['align'], D['s_m'], D['curves']
  25. RED = '#cc1f2d'
  26. # --- геометрия листа (мм), y вверх ------------------------------------------
  27. W_SH, H_SH = 357.0, 247.0
  28. X0 = 40.0 # начало ординат
  29. def X(s): return X0 + s / 5.0 # 1:5000
  30. H0 = 246.0 # отметка низа профильной зоны
  31. Y0 = 95.0 # y листа для H0
  32. def Y(h): return Y0 + (h - H0) * 2.0 # 1:500
  33. # строки сетки (снизу вверх): (имя, высота мм)
  34. ROWS = [('Километры', 7.5), ('Пикеты', 7.5), ('Расстояния', 9.0),
  35. ('Отметки земли', 9.5), ('Проект. отметки', 9.5),
  36. ('Уклоны, ‰', 11.0), ('План трассы', 12.0)]
  37. Y_GRID_BOT, Y_GRID_TOP = 15.0, 15.0 + sum(h for _, h in ROWS)
  38. fig = plt = None
  39. import matplotlib
  40. matplotlib.use('Agg')
  41. import matplotlib.pyplot as plt
  42. # Чертёжный шрифт ГОСТ 2.304-81 тип А (прямой, из системных шрифтов Windows)
  43. import matplotlib.font_manager as fm
  44. fm.fontManager.addfont('C:/Windows/Fonts/GOST2304A.ttf')
  45. matplotlib.rcParams['font.family'] = 'GOST 2.304 type A'
  46. matplotlib.rcParams['mathtext.default'] = 'regular'
  47. fig = plt.figure(figsize=(W_SH / 25.4, H_SH / 25.4), dpi=250)
  48. ax = fig.add_axes([0, 0, 1, 1])
  49. ax.set_xlim(0, W_SH); ax.set_ylim(0, H_SH)
  50. ax.axis('off')
  51. FS, FSS = 10.6, 9.6 # кегли, pt (2.83 pt = 1 мм)
  52. # --- заголовок ---------------------------------------------------------------
  53. ax.text(W_SH / 2, H_SH - 11, 'ПРОФИЛЬ ТРАССЫ АВТОМОБИЛЬНОЙ ДОРОГИ А–Б',
  54. fontsize=13.5, weight='normal', ha='center', va='center')
  55. ax.text(W_SH / 2, H_SH - 18.5, 'Масштабы: горизонтальный 1:5000, вертикальный 1:500 (схематический)',
  56. fontsize=FS, ha='center', va='center', color='0.25')
  57. # --- сетка -------------------------------------------------------------------
  58. y = Y_GRID_BOT
  59. bounds = [y]
  60. for name, h in ROWS:
  61. y += h
  62. bounds.append(y)
  63. # горизонтальные линии сетки: верхняя и внутренние жирнее
  64. X_R, X_L = X(L_total) + 14.0, X0 - 3.0
  65. for i, b in enumerate(bounds):
  66. lw = 0.9 if i in (0, len(bounds) - 1) else 0.45
  67. ax.plot([X_L, X_R], [b, b], color='black', lw=lw)
  68. ax.plot([X_L, X_L], [bounds[0], bounds[-1]], color='black', lw=0.9)
  69. ax.plot([X_R, X_R], [bounds[0], bounds[-1]], color='black', lw=0.9)
  70. # названия строк — слева
  71. for (name, h), b0, b1 in zip(ROWS, bounds, bounds[1:]):
  72. ax.text(X_L - 0.8, (b0 + b1) / 2, name, fontsize=8.8, ha='right', va='center')
  73. # вертикальные линии: пикеты (тонкие), километры (толстые), переломы красной
  74. grade_s = [g[0] for g in GRADES] + [L_total]
  75. def vline(s, y1, y2, lw=0.35, color='black'):
  76. ax.plot([X(s), X(s)], [y1, y2], color=color, lw=lw)
  77. for pk in range(0, int(L_total // 100) + 1):
  78. s = pk * 100.0
  79. if s > L_total: break
  80. vline(s, Y_GRID_TOP, bounds[0]) # ордината сквозь сетку
  81. vline(0.0, Y0, bounds[0], lw=0.9)
  82. vline(1000.0, Y_GRID_TOP, bounds[0], lw=0.9)
  83. vline(L_total, Y_GRID_TOP, bounds[0], lw=0.9)
  84. # отметки по левой шкале
  85. for h in range(245, 311, 5):
  86. ax.plot([X_L - 1.2, X_L], [Y(h), Y(h)], color='black', lw=0.5)
  87. ax.text(X_L - 1.8, Y(h), str(h), fontsize=8.6, ha='right', va='center')
  88. # горизонтальные сетки профильной зоны (каждые 5 м)
  89. for h in range(245, 311, 5):
  90. lw = 0.55 if h % 10 == 0 else 0.25
  91. ax.plot([X0, X_R], [Y(h), Y(h)], color='0.55', lw=lw, zorder=0)
  92. # --- линия поверхности земли --------------------------------------------------
  93. ss = np.linspace(0, L_total, 400)
  94. gs = [ground(s) for s in ss]
  95. ax.plot([X(s) for s in ss], [Y(g) for g in gs], color='black', lw=1.0, zorder=3)
  96. # фактические отметки на пикетах
  97. for pk in range(0, 15):
  98. s = pk * 100.0
  99. if s > L_total: break
  100. g = ground(s)
  101. ha = 'left' if s == 0 else 'center'
  102. ax.text(X(s) + (1.0 if s == 0 else 0), (bounds[3] + bounds[4]) / 2, f'{g:.1f}'.replace('.', ','),
  103. fontsize=FSS, ha=ha, va='center', zorder=5)
  104. # --- проектная (красная) линия ------------------------------------------------
  105. rr = [red_h(s) for s in ss]
  106. ax.plot([X(s) for s in ss], [Y(h) for h in rr], color=RED, lw=2.0, zorder=4)
  107. breaks = [0.0, 400.0, 700.0, 1060.0, 1265.0, L_total]
  108. def cm(v):
  109. return f'{v:.2f}'.replace('.', ',')
  110. pk_even = [pk * 100.0 for pk in range(0, 15) if pk % 2 == 0 and pk * 100 <= L_total]
  111. # проектные отметки (красные): переломы + чётные пикеты; станции ближе 14 мм
  112. # на листе разносят по ярусам, иначе соседние цифры сливаются
  113. MIN_GAP_MM = 14.0
  114. red_stations = sorted(set(breaks) | set(pk_even))
  115. red_levels, last_x, lvl = [], None, 0
  116. for s in red_stations:
  117. x = X(s)
  118. lvl = 1 - lvl if last_x is not None and x - last_x < MIN_GAP_MM else 0
  119. red_levels.append(lvl)
  120. last_x = x
  121. # над красной линией; белая подложка отделяет цифры от пересекаемых линий
  122. for s, lvl in zip(red_stations, red_levels):
  123. ha = 'left' if s == 0 else 'center'
  124. ax.text(X(s) + (1.0 if s == 0 else 0),
  125. Y(red_h(s)) + (3.4 if s in breaks else 3.0) + 4.2 * lvl, cm(red_h(s)),
  126. fontsize=8.8, color=RED, ha=ha, va='center', zorder=6,
  127. bbox=dict(boxstyle='round,pad=0.15', fc='white', ec='none'))
  128. # проектные отметки в строке сетки
  129. for s, lvl in zip(red_stations, red_levels):
  130. ha = 'left' if s == 0 else 'center'
  131. ax.text(X(s) + (1.0 if s == 0 else 0),
  132. (bounds[4] + bounds[5]) / 2 + 2.4 - 4.8 * lvl, cm(red_h(s)),
  133. fontsize=8.8, color=RED, ha=ha, va='center', zorder=6,
  134. bbox=dict(boxstyle='round,pad=0.12', fc='white', ec='none'))
  135. # --- строка уклонов -----------------------------------------------------------
  136. b0, b1 = bounds[5], bounds[6]
  137. for (s1, i1), (s2, _i2) in zip(GRADES, GRADES[1:] + [(None, None)]):
  138. s2e = L_total if s2 is None else s2
  139. x1, x2 = X(s1), X(s2e)
  140. ym = (b0 + b1) / 2
  141. L = x2 - x1
  142. dy = np.clip(L * np.tan(np.radians(6.0)) * np.sign(i1), -2.6, 2.6)
  143. ax.annotate('', xy=(x2, ym + dy), xytext=(x1, ym - dy),
  144. arrowprops=dict(arrowstyle='-|>', color=RED, lw=1.1, mutation_scale=9))
  145. ax.text((x1 + x2) / 2, ym + 3.6, ('+' if i1 > 0 else '') + f'{i1:g}',
  146. fontsize=FSS, color=RED, ha='center', va='center', zorder=6,
  147. bbox=dict(boxstyle='round,pad=0.12', fc='white', ec='none'))
  148. ax.text((x1 + x2) / 2, ym - 3.4, f'{s2e - s1:.0f}', fontsize=FSS, ha='center',
  149. va='center', zorder=6, bbox=dict(boxstyle='round,pad=0.12', fc='white', ec='none'))
  150. # --- строка расстояний (между переломами) -------------------------------------
  151. b0, b1 = bounds[2], bounds[3]
  152. for s1, s2 in zip(grade_s, grade_s[1:]):
  153. ax.text(X((s1 + s2) / 2), (b0 + b1) / 2, f'{s2 - s1:.0f}', fontsize=FSS, ha='center', va='center')
  154. # --- строка пикетов ------------------------------------------------------------
  155. b0, b1 = bounds[1], bounds[2]
  156. for pk in range(0, 15):
  157. s = pk * 100.0
  158. if s > L_total: continue
  159. ax.text(X(s), (b0 + b1) / 2 + 0.8, str(pk), fontsize=FSS, ha='center', va='center',
  160. bbox=dict(facecolor='white', edgecolor='none', pad=1.2))
  161. ax.text(X(L_total), (b0 + b1) / 2 + 0.8, '14+43', fontsize=8.6, ha='center', va='center',
  162. zorder=6, bbox=dict(boxstyle='round,pad=0.12', fc='white', ec='none'))
  163. # --- километры ------------------------------------------------------------------
  164. b0, b1 = bounds[0], bounds[1]
  165. for km, s in ((0, 0.0), (1, 1000.0)):
  166. ax.text(X(s), (b0 + b1) / 2 + 0.3, str(km), fontsize=FSS, ha='center', va='center',
  167. bbox=dict(boxstyle='circle,pad=0.18', fc='white', ec='black', lw=0.6))
  168. # --- план трассы в своей строке ---------------------------------------------------
  169. b0, b1 = bounds[-2], bounds[-1]
  170. ym = (b0 + b1) / 2
  171. ax.plot([X0, X(L_total)], [ym, ym], color='black', lw=0.9)
  172. def station_of(pt):
  173. d = np.hypot(align[:, 0] - pt[0], align[:, 1] - pt[1])
  174. return s_m[int(np.argmin(d))]
  175. for c in curves:
  176. sA, sB = station_of(c['A']), station_of(c['B'])
  177. x1, x2 = X(sA), X(sB)
  178. hh = min(4.2, c['R_m'] / 100.0)
  179. t = np.linspace(0, np.pi, 40)
  180. xs = np.linspace(x1, x2, 40)
  181. ys = ym + hh * np.sin(t) * (1 if c['right'] else -1)
  182. ax.plot(xs, ys, color='black', lw=0.9)
  183. if c['right']: # подпись сбоку от дуги, чтобы не выходить за строку сетки
  184. ax.text(x2 + 1.5, ym, f"R{c['R_m']:.0f}", fontsize=8.4, ha='left', va='center',
  185. bbox=dict(facecolor='white', edgecolor='none', pad=1.2))
  186. else:
  187. ax.text((x1 + x2) / 2, ym - hh - 1.2, f"R{c['R_m']:.0f}", fontsize=8.4,
  188. ha='center', va='top',
  189. bbox=dict(facecolor='white', edgecolor='none', pad=1.2))
  190. # тоннель в строке плана: прямоугольник
  191. x1, x2 = X(S_P1), X(S_P2)
  192. ax.plot([x1, x2, x2, x1, x1], [ym - 3.4, ym - 3.4, ym + 3.4, ym + 3.4, ym - 3.4],
  193. color='black', lw=0.8)
  194. ax.text((x1 + x2) / 2 - 1.5, ym, 'тоннель', fontsize=8.0, ha='center', va='center',
  195. rotation=0, bbox=dict(facecolor='white', edgecolor='none', pad=1.2))
  196. # --- тоннель на профиле ---------------------------------------------------------
  197. x1, x2 = X(S_P1), X(S_P2)
  198. ytop1, ytop2 = Y(red_h(S_P1)), Y(red_h(S_P2))
  199. dep = 7.0
  200. ax.plot([x1, x2, x2, x1, x1], [ytop1, ytop2, ytop2 - dep, ytop1 - dep, ytop1],
  201. color=RED, lw=1.0)
  202. ax.plot([x1, x1], [ytop1, Y(ground(S_P1))], color=RED, lw=0.9)
  203. ax.plot([x2, x2], [ytop2, Y(ground(S_P2))], color=RED, lw=0.9)
  204. ax.text((x1 + x2) / 2, (ytop1 + ytop2) / 2 - dep / 2, 'Тоннель, 205 м',
  205. fontsize=8.6, color=RED, ha='center', va='center')
  206. ax.text(x1 - 1.0, Y(ground(S_P1)) + 3.0, 'портал', fontsize=8.0, color='0.3',
  207. ha='right', va='center')
  208. ax.text(x2 + 1.0, Y(ground(S_P2)) + 3.0, 'портал', fontsize=8.0, color='0.3',
  209. ha='left', va='center')
  210. # рабочие отметки у порталов (глубина выемок)
  211. for s in (S_P1, S_P2):
  212. w = ground(s) - red_h(s)
  213. ax.text(X(s) + (2.4 if s == S_P2 else -2.4), Y(red_h(s)) - 3.2,
  214. f'{w:.1f}'.replace('.', ','), fontsize=8.6, color='0.25',
  215. ha='left' if s == S_P2 else 'right', va='center')
  216. fig.savefig('assets/images/laba2-profil.png', dpi=250, facecolor='white')
  217. print('сохранено: assets/images/laba2-profil.png')