qmlmathplot 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qmlmathplot/__init__.py +3 -0
- qmlmathplot/curve.py +5 -0
- qmlmathplot/export.py +5 -1
- qmlmathplot/figure.py +88 -14
- qmlmathplot/model.py +36 -8
- qmlmathplot/mpl.py +67 -6
- qmlmathplot/overlay.py +426 -0
- qmlmathplot/plot.py +232 -3
- qmlmathplot/pyplot.py +40 -0
- qmlmathplot/qml/PlotFurniture.qml +156 -20
- qmlmathplot/qml/PlotInput.qml +4 -0
- qmlmathplot/qml/PlotLegend.qml +66 -0
- qmlmathplot/qml/PlotOverlay.qml +111 -0
- qmlmathplot/qml/PlotView.qml +14 -3
- qmlmathplot/qsb.py +6 -0
- qmlmathplot/widget.py +3 -3
- {qmlmathplot-0.4.1.dist-info → qmlmathplot-0.4.3.dist-info}/METADATA +115 -24
- {qmlmathplot-0.4.1.dist-info → qmlmathplot-0.4.3.dist-info}/RECORD +20 -17
- {qmlmathplot-0.4.1.dist-info → qmlmathplot-0.4.3.dist-info}/WHEEL +0 -0
- {qmlmathplot-0.4.1.dist-info → qmlmathplot-0.4.3.dist-info}/entry_points.txt +0 -0
qmlmathplot/__init__.py
CHANGED
|
@@ -54,6 +54,7 @@ from .camera import HOME_SIZE, HOME_VIEW, Camera, nice_ticks
|
|
|
54
54
|
from .curve import Curve, CurveListModel, ImplicitCurve
|
|
55
55
|
from .figure import Axes, Figure
|
|
56
56
|
from .input import ViewInput
|
|
57
|
+
from .overlay import Annotation, Guide
|
|
57
58
|
from .plot import Plot
|
|
58
59
|
from .qsb import cache_dir, cache_info, clear_cache, prune_cache
|
|
59
60
|
from .view import QML_MAJOR, QML_MINOR, QML_URI, qml_component_path, register_qml_types
|
|
@@ -63,6 +64,7 @@ if TYPE_CHECKING: # only for type checkers; at runtime the module-level __getat
|
|
|
63
64
|
from .widget import MathPlotWidget
|
|
64
65
|
|
|
65
66
|
__all__ = [
|
|
67
|
+
"Annotation",
|
|
66
68
|
"Axes",
|
|
67
69
|
"cache_dir",
|
|
68
70
|
"cache_info",
|
|
@@ -72,6 +74,7 @@ __all__ = [
|
|
|
72
74
|
"CurveListModel",
|
|
73
75
|
"Figure",
|
|
74
76
|
"FigureCanvasQt",
|
|
77
|
+
"Guide",
|
|
75
78
|
"HOME_SIZE",
|
|
76
79
|
"HOME_VIEW",
|
|
77
80
|
"ImplicitCurve",
|
qmlmathplot/curve.py
CHANGED
|
@@ -431,6 +431,7 @@ class CurveListModel(QAbstractListModel):
|
|
|
431
431
|
VisibleRole = CurveRole + 4
|
|
432
432
|
VertexShaderRole = CurveRole + 5
|
|
433
433
|
FragmentShaderRole = CurveRole + 6
|
|
434
|
+
LabelRole = CurveRole + 7
|
|
434
435
|
|
|
435
436
|
_ROLES = {
|
|
436
437
|
CurveRole: b"curve",
|
|
@@ -440,6 +441,7 @@ class CurveListModel(QAbstractListModel):
|
|
|
440
441
|
VisibleRole: b"visible",
|
|
441
442
|
VertexShaderRole: b"vertexShader",
|
|
442
443
|
FragmentShaderRole: b"fragmentShader",
|
|
444
|
+
LabelRole: b"label",
|
|
443
445
|
}
|
|
444
446
|
|
|
445
447
|
def __init__(
|
|
@@ -478,6 +480,8 @@ class CurveListModel(QAbstractListModel):
|
|
|
478
480
|
return curve.vertexShader
|
|
479
481
|
if role == self.FragmentShaderRole:
|
|
480
482
|
return curve.fragmentShader
|
|
483
|
+
if role == self.LabelRole:
|
|
484
|
+
return curve.label
|
|
481
485
|
return None
|
|
482
486
|
|
|
483
487
|
def _notify(self, curve: Curve, roles: tuple[int, ...]) -> None:
|
|
@@ -498,6 +502,7 @@ class CurveListModel(QAbstractListModel):
|
|
|
498
502
|
lambda c=curve: self._notify(c, (self.ColorRole, self.LineWidthRole))
|
|
499
503
|
)
|
|
500
504
|
curve.visibleChanged.connect(lambda c=curve: self._notify(c, (self.VisibleRole,)))
|
|
505
|
+
curve.labelChanged.connect(lambda c=curve: self._notify(c, (self.LabelRole,)))
|
|
501
506
|
|
|
502
507
|
# ---------------------------------------------------------------- public
|
|
503
508
|
def _defaults(self, color: object, line_width: object) -> tuple[QColor, float]:
|
qmlmathplot/export.py
CHANGED
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
from collections.abc import Sequence
|
|
15
15
|
from typing import TYPE_CHECKING
|
|
16
16
|
|
|
17
|
-
from PySide6.QtCore import QSize, Qt, QUrl
|
|
17
|
+
from PySide6.QtCore import QCoreApplication, QSize, Qt, QUrl
|
|
18
18
|
from PySide6.QtGui import QColor, QImage
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING: # pragma: no cover - typing only; the real imports stay lazy
|
|
@@ -155,6 +155,10 @@ def render(
|
|
|
155
155
|
|
|
156
156
|
camera.set_viewport(logical[0], logical[1])
|
|
157
157
|
camera.set_view(centre[0], centre[1], scale[0], scale[1])
|
|
158
|
+
# Let QML's Canvas-based furniture (grid, guides) handle its queued paint before the
|
|
159
|
+
# synchronous readback; the scene-graph items are already ready, Canvas is not.
|
|
160
|
+
for _ in range(3):
|
|
161
|
+
QCoreApplication.processEvents()
|
|
158
162
|
image = widget.grabFramebuffer()
|
|
159
163
|
finally:
|
|
160
164
|
# Restore before the event loop runs again, so no live view repaints the export state.
|
qmlmathplot/figure.py
CHANGED
|
@@ -213,7 +213,13 @@ class Axes:
|
|
|
213
213
|
|
|
214
214
|
# -------------------------------------------------------------- dressing
|
|
215
215
|
def grid(self, visible: bool | None = True, **kwargs: object) -> None:
|
|
216
|
-
|
|
216
|
+
axis = str(kwargs.pop("axis", "both")).lower()
|
|
217
|
+
enabled = True if visible is None else bool(visible)
|
|
218
|
+
self.model.grid = enabled
|
|
219
|
+
if axis in ("x", "both"):
|
|
220
|
+
self.model.grid_x = enabled
|
|
221
|
+
if axis in ("y", "both"):
|
|
222
|
+
self.model.grid_y = enabled
|
|
217
223
|
if "color" in kwargs:
|
|
218
224
|
self.model.grid_color = kwargs["color"] # type: ignore[assignment]
|
|
219
225
|
width = kwargs.get("linewidth", kwargs.get("lw"))
|
|
@@ -255,11 +261,19 @@ class Axes:
|
|
|
255
261
|
def clear(self) -> None:
|
|
256
262
|
self._figure.clf()
|
|
257
263
|
|
|
258
|
-
#
|
|
264
|
+
# ------------------------------------------------------- text and annotation
|
|
259
265
|
def legend(self, *args: object, **kwargs: object) -> None:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
266
|
+
if args and args[0] in (False, None):
|
|
267
|
+
self.model.legend = False
|
|
268
|
+
return None
|
|
269
|
+
loc = kwargs.get("loc", kwargs.get("location"))
|
|
270
|
+
if loc is not None:
|
|
271
|
+
text = str(loc).replace("_", " ").lower()
|
|
272
|
+
if text not in self.model._LEGEND_POSITIONS:
|
|
273
|
+
raise ValueError(f"unsupported legend location {loc!r}")
|
|
274
|
+
self.model.legend_position = text
|
|
275
|
+
self.model.legend = True
|
|
276
|
+
return None
|
|
263
277
|
|
|
264
278
|
def set_xscale(self, value: object, **kwargs: object) -> None:
|
|
265
279
|
raise _unsupported("log axes", "the shader mapping is linear today")
|
|
@@ -267,17 +281,77 @@ class Axes:
|
|
|
267
281
|
def set_yscale(self, value: object, **kwargs: object) -> None:
|
|
268
282
|
raise _unsupported("log axes", "the shader mapping is linear today")
|
|
269
283
|
|
|
270
|
-
def annotate(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
284
|
+
def annotate(
|
|
285
|
+
self,
|
|
286
|
+
text: object,
|
|
287
|
+
xy: object,
|
|
288
|
+
xytext: object = (24, -18),
|
|
289
|
+
arrowprops: dict[str, object] | None = None,
|
|
290
|
+
**kwargs: object,
|
|
291
|
+
):
|
|
292
|
+
color = kwargs.pop("color", None)
|
|
293
|
+
fontsize = kwargs.pop("fontsize", kwargs.pop("size", None))
|
|
294
|
+
if arrowprops and "color" in arrowprops:
|
|
295
|
+
color = arrowprops["color"]
|
|
296
|
+
return self.model.add_annotation(
|
|
297
|
+
"" if text is None else str(text),
|
|
298
|
+
xy,
|
|
299
|
+
xytext,
|
|
300
|
+
arrowprops is not None,
|
|
301
|
+
color,
|
|
302
|
+
None if fontsize is None else float(fontsize), # type: ignore[arg-type]
|
|
303
|
+
)
|
|
275
304
|
|
|
276
|
-
def
|
|
277
|
-
|
|
305
|
+
def text(self, x: float, y: float, s: object, fontdict: object = None, **kwargs: object):
|
|
306
|
+
color = kwargs.pop("color", None)
|
|
307
|
+
size = kwargs.pop("fontsize", kwargs.pop("size", None))
|
|
308
|
+
return self.model.add_text(x, y, "" if s is None else str(s), color=color,
|
|
309
|
+
font_size=None if size is None else float(size))
|
|
310
|
+
|
|
311
|
+
def set_xticks(self, ticks: object, labels: object = None, **kwargs: object) -> None:
|
|
312
|
+
self.model.set_xticks(ticks, labels)
|
|
313
|
+
|
|
314
|
+
def set_yticks(self, ticks: object, labels: object = None, **kwargs: object) -> None:
|
|
315
|
+
self.model.set_yticks(ticks, labels)
|
|
316
|
+
|
|
317
|
+
def tick_params(self, axis: str = "both", **kwargs: object) -> None:
|
|
318
|
+
axis = str(axis).lower()
|
|
319
|
+
if "length" in kwargs:
|
|
320
|
+
self.model.tick_length = float(kwargs["length"]) # type: ignore[arg-type]
|
|
321
|
+
if "width" in kwargs:
|
|
322
|
+
self.model.tick_width = float(kwargs["width"]) # type: ignore[arg-type]
|
|
323
|
+
if "pad" in kwargs:
|
|
324
|
+
self.model.tick_pad = float(kwargs["pad"]) # type: ignore[arg-type]
|
|
325
|
+
if "labelsize" in kwargs:
|
|
326
|
+
self.model.tick_font_size = float(kwargs["labelsize"]) # type: ignore[arg-type]
|
|
327
|
+
colors = kwargs.get("colors", kwargs.get("color"))
|
|
328
|
+
if colors is not None:
|
|
329
|
+
self.model.tick_color = colors # type: ignore[assignment]
|
|
330
|
+
labelcolor = kwargs.get("labelcolor")
|
|
331
|
+
if labelcolor is not None:
|
|
332
|
+
self.model.text_color = labelcolor # type: ignore[assignment]
|
|
333
|
+
if "top" in kwargs and axis in ("x", "both"):
|
|
334
|
+
self.model.ticks_top = bool(kwargs["top"])
|
|
335
|
+
if "right" in kwargs and axis in ("y", "both"):
|
|
336
|
+
self.model.ticks_right = bool(kwargs["right"])
|
|
337
|
+
if str(kwargs.get("which", "")).lower() == "minor":
|
|
338
|
+
self.model.minor_ticks = True
|
|
339
|
+
|
|
340
|
+
def axhline(self, y: float = 0.0, **kwargs: object):
|
|
341
|
+
return self.model.add_hline(
|
|
342
|
+
y,
|
|
343
|
+
color=kwargs.get("color", kwargs.get("c")),
|
|
344
|
+
width=float(kwargs.get("linewidth", kwargs.get("lw", 1.0))), # type: ignore[arg-type]
|
|
345
|
+
style=str(kwargs.get("linestyle", kwargs.get("ls", "--"))),
|
|
346
|
+
)
|
|
278
347
|
|
|
279
|
-
def
|
|
280
|
-
|
|
348
|
+
def axvline(self, x: float = 0.0, **kwargs: object):
|
|
349
|
+
return self.model.add_vline(
|
|
350
|
+
x,
|
|
351
|
+
color=kwargs.get("color", kwargs.get("c")),
|
|
352
|
+
width=float(kwargs.get("linewidth", kwargs.get("lw", 1.0))), # type: ignore[arg-type]
|
|
353
|
+
style=str(kwargs.get("linestyle", kwargs.get("ls", "--"))),
|
|
354
|
+
)
|
|
281
355
|
|
|
282
356
|
|
|
283
357
|
class Figure:
|
qmlmathplot/model.py
CHANGED
|
@@ -440,21 +440,43 @@ void main() {
|
|
|
440
440
|
float y_bot = camera.y - 0.5 * spany;
|
|
441
441
|
|
|
442
442
|
#if USE_INTERVAL
|
|
443
|
+
// Conservative reject: every sample/segment used below lies within ±2 columns, so the
|
|
444
|
+
// interval over that window bounds the curve's y values there. A pixel farther than the
|
|
445
|
+
// stroke radius from that range cannot be painted; skip the 14 F evaluations and the
|
|
446
|
+
// column/stroke-footprint intervals. This is the cheap analogue of a column cache: one
|
|
447
|
+
// interval evaluation instead of the whole per-pixel sample loop. Whole/infinite columns
|
|
448
|
+
// do not reject (the envelope may fill the viewport); empty columns have no curve at all.
|
|
449
|
+
IVal wide = FI(x - 2.0 * dx, x + 2.0 * dx);
|
|
450
|
+
if (wide.kind == IK_EMPTY) {
|
|
451
|
+
fragColor = vec4(0.0);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (wide.kind == IK_CONNECTED && wide.lo > -IINF && wide.hi < IINF) {
|
|
455
|
+
float pad = R * camera.w;
|
|
456
|
+
if (y < wide.lo - pad || y > wide.hi + pad) {
|
|
457
|
+
fragColor = vec4(0.0);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
443
462
|
IVal own = FI(x - 0.5 * dx, x + 0.5 * dx);
|
|
444
|
-
bool own_empty = own.kind == IK_EMPTY;
|
|
445
463
|
IVal col = own;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
//
|
|
449
|
-
|
|
464
|
+
if (col.kind == IK_EMPTY) {
|
|
465
|
+
// The stroke's outer half may cross the domain boundary by up to one radius. Only a
|
|
466
|
+
// column with no in-domain value needs this wider footprint; skipping it for every
|
|
467
|
+
// other pixel removes one interval evaluation from the common path.
|
|
468
|
+
IVal gate = FI(x - R * dx, x + R * dx);
|
|
469
|
+
if (gate.kind != IK_EMPTY) {
|
|
470
|
+
col = gate;
|
|
471
|
+
}
|
|
450
472
|
}
|
|
451
473
|
|
|
452
474
|
// ---- fixed-x sub-pixel polyline (GeoGebra's sampled polyline, on the GPU) ----
|
|
453
|
-
//
|
|
475
|
+
// 14 samples over ±2 columns (0.286-column spacing) cover the perpendicular foot even at
|
|
454
476
|
// 89°: a ±1-column window lost 42 % of the width at 84°. Joining the samples gives a
|
|
455
477
|
// polyline whose segments are diagonal, so the stroke is smooth at any zoom. The interval
|
|
456
478
|
// band stays separate (below); it no longer has to be inferred from this window.
|
|
457
|
-
const int NS =
|
|
479
|
+
const int NS = 14;
|
|
458
480
|
float smp[NS];
|
|
459
481
|
float smpx[NS];
|
|
460
482
|
bool ok[NS];
|
|
@@ -1265,8 +1287,14 @@ void main() {
|
|
|
1265
1287
|
// such as x² + y² = 0 is not left to the projection alone.
|
|
1266
1288
|
// GGB measures the singular cell in *world* derivative units (|Fx| + |Fy| < 0.001);
|
|
1267
1289
|
// the per-pixel gradient of an ordinary circle is ~0.01 and must not qualify.
|
|
1290
|
+
// The distance gate is essential for a scaled expression: (x+y)/(x*y) has |F| < 0.05 and
|
|
1291
|
+
// |Fx|+|Fy| < 0.001 over huge regions around ±50, although the level set is far away. A
|
|
1292
|
+
// singular point must also be *on* the set to the first order: |F| ~ R*|grad F|. The
|
|
1293
|
+
// ratio is scale-invariant, unlike the two absolute thresholds above.
|
|
1268
1294
|
float gw_s = abs(FX(x, y)) + abs(FY(x, y));
|
|
1269
|
-
|
|
1295
|
+
float gnorm_s = max(sqrt(gx * gx + gy * gy), 1e-9);
|
|
1296
|
+
if (dom && (f == f) && (abs(f) < 1e30) && gw_s < 1e-3 && abs(f) < 0.05
|
|
1297
|
+
&& abs(f) < R * gnorm_s) {
|
|
1270
1298
|
march_cov = 1.0;
|
|
1271
1299
|
}
|
|
1272
1300
|
|
qmlmathplot/mpl.py
CHANGED
|
@@ -149,31 +149,92 @@ def figure_for(
|
|
|
149
149
|
axes.spines["bottom"].set_position("zero")
|
|
150
150
|
axes.spines["top"].set_visible(False)
|
|
151
151
|
axes.spines["right"].set_visible(False)
|
|
152
|
+
elif plot.axes_position == "box":
|
|
153
|
+
for side, visible in (
|
|
154
|
+
("top", plot.spine_top), ("bottom", plot.spine_bottom),
|
|
155
|
+
("left", plot.spine_left), ("right", plot.spine_right),
|
|
156
|
+
):
|
|
157
|
+
axes.spines[side].set_visible(visible)
|
|
152
158
|
for spine in axes.spines.values():
|
|
153
159
|
spine.set_color(plot.axis_color.name())
|
|
154
160
|
spine.set_linewidth(plot.axis_width * PX_TO_PT)
|
|
155
161
|
|
|
162
|
+
if plot.xticks:
|
|
163
|
+
axes.set_xticks([float(value) for value, _label in plot.xticks],
|
|
164
|
+
labels=[label for _value, label in plot.xticks])
|
|
165
|
+
if plot.yticks:
|
|
166
|
+
axes.set_yticks([float(value) for value, _label in plot.yticks],
|
|
167
|
+
labels=[label for _value, label in plot.yticks])
|
|
168
|
+
|
|
156
169
|
axes.set_axisbelow(True) # the Qt view paints its grid below the curves
|
|
170
|
+
grid_kwargs = {
|
|
171
|
+
"color": plot.grid_color.name(),
|
|
172
|
+
"linewidth": plot.grid_width * PX_TO_PT,
|
|
173
|
+
"alpha": plot.grid_alpha,
|
|
174
|
+
"linestyle": plot.grid_style,
|
|
175
|
+
}
|
|
157
176
|
if plot.grid:
|
|
158
|
-
axes.grid(
|
|
159
|
-
|
|
160
|
-
|
|
177
|
+
axes.grid(False)
|
|
178
|
+
if plot.grid_x:
|
|
179
|
+
axes.xaxis.grid(True, **grid_kwargs)
|
|
180
|
+
if plot.grid_y:
|
|
181
|
+
axes.yaxis.grid(True, **grid_kwargs)
|
|
161
182
|
else:
|
|
162
|
-
# Passing line properties together with `False` makes matplotlib *enable* the grid.
|
|
163
183
|
axes.grid(False)
|
|
164
|
-
axes.tick_params(
|
|
165
|
-
|
|
184
|
+
axes.tick_params(
|
|
185
|
+
colors=plot.tick_color.name(),
|
|
186
|
+
labelcolor=plot.text_color.name(),
|
|
187
|
+
labelsize=plot.tick_font_size * PX_TO_PT,
|
|
188
|
+
width=plot.tick_width * PX_TO_PT,
|
|
189
|
+
pad=plot.tick_pad * PX_TO_PT,
|
|
190
|
+
top=plot.ticks_top,
|
|
191
|
+
right=plot.ticks_right,
|
|
192
|
+
)
|
|
193
|
+
if plot.minor_ticks:
|
|
194
|
+
axes.minorticks_on()
|
|
166
195
|
axes.xaxis.label.set_fontsize(plot.font_size * PX_TO_PT)
|
|
167
196
|
axes.yaxis.label.set_fontsize(plot.font_size * PX_TO_PT)
|
|
197
|
+
if plot.xlabel:
|
|
198
|
+
axes.set_xlabel(plot.xlabel, color=plot.text_color.name())
|
|
199
|
+
if plot.ylabel:
|
|
200
|
+
axes.set_ylabel(plot.ylabel, color=plot.text_color.name())
|
|
168
201
|
if plot.title:
|
|
169
202
|
axes.set_title(plot.title, color=plot.title_color.name(),
|
|
170
203
|
fontsize=plot.title_font_size * PX_TO_PT,
|
|
171
204
|
fontweight="bold" if plot.title_bold else "normal")
|
|
172
205
|
|
|
206
|
+
for index in range(plot.annotations.rowCount()):
|
|
207
|
+
annotation = plot.annotations.at(index)
|
|
208
|
+
if not annotation.visible:
|
|
209
|
+
continue
|
|
210
|
+
options: dict[str, object] = {
|
|
211
|
+
"xycoords": "data",
|
|
212
|
+
"textcoords": "offset points",
|
|
213
|
+
"xytext": (annotation.xytext.x(), annotation.xytext.y()),
|
|
214
|
+
"color": annotation.color.name(),
|
|
215
|
+
"fontsize": annotation.fontSize * PX_TO_PT,
|
|
216
|
+
}
|
|
217
|
+
if annotation.arrow:
|
|
218
|
+
options["arrowprops"] = {"arrowstyle": "->", "color": annotation.color.name()}
|
|
219
|
+
axes.annotate(annotation.text, (annotation.xy.x(), annotation.xy.y()), **options)
|
|
220
|
+
|
|
221
|
+
for index in range(plot.guides.rowCount()):
|
|
222
|
+
guide = plot.guides.at(index)
|
|
223
|
+
if not guide.visible:
|
|
224
|
+
continue
|
|
225
|
+
if guide.kind == "h":
|
|
226
|
+
axes.axhline(guide.value, color=guide.color.name(),
|
|
227
|
+
linewidth=guide.width * PX_TO_PT, linestyle=guide.style)
|
|
228
|
+
else:
|
|
229
|
+
axes.axvline(guide.value, color=guide.color.name(),
|
|
230
|
+
linewidth=guide.width * PX_TO_PT, linestyle=guide.style)
|
|
231
|
+
|
|
173
232
|
for curve in _visible_curves(plot):
|
|
174
233
|
xs, ys = _sample(curve, x_range[0], x_range[1], samples)
|
|
175
234
|
axes.plot(xs, ys, color=curve.color.name(), linewidth=curve.lineWidth * PX_TO_PT,
|
|
176
235
|
label=curve.label or curve.expression)
|
|
236
|
+
if plot.legend and any(curve.label for curve in _visible_curves(plot)):
|
|
237
|
+
axes.legend(loc=plot.legend_position)
|
|
177
238
|
return figure
|
|
178
239
|
|
|
179
240
|
|