fig-laba2-profile.py 13 KB

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