1use crate::color::Color;
24use crate::data_config::{DataRenderType, ScatterShape, SeriesConfig};
25use crate::line::LineStylePreset;
26use crate::text::{RichSegment, RichText, rich_segments_from_text};
27use std::ops::Range;
28
29#[derive(Debug, Clone, Copy, PartialEq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub enum LegendCorner {
32 TopLeft,
33 TopRight,
34 BottomLeft,
35 BottomRight,
36}
37
38#[derive(Debug, Clone, PartialEq)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub enum LegendEntryKind {
42 Line,
44 Scatter,
46 LineScatter,
48}
49
50pub fn scatter_shape_char(shape: &ScatterShape) -> char {
54 match shape {
55 ScatterShape::Circle => '○',
56 ScatterShape::CircleFilled => '●',
57 ScatterShape::Square => '□',
58 ScatterShape::SquareFilled => '■',
59 ScatterShape::Triangle => '▲',
60 ScatterShape::TriangleFilled => '▲',
61 ScatterShape::TriangleDown => '▼',
62 ScatterShape::TriangleDownFilled => '▼',
63 ScatterShape::TriangleLeft => '◄',
64 ScatterShape::TriangleLeftFilled => '◄',
65 ScatterShape::TriangleRight => '►',
66 ScatterShape::TriangleRightFilled => '►',
67 ScatterShape::Diamond => '◊',
68 ScatterShape::DiamondFilled => '♦',
69 ScatterShape::Cross => '×',
70 ScatterShape::CrossFilled => '×',
71 ScatterShape::Plus => '+',
72 ScatterShape::PlusFilled => '+',
73 ScatterShape::Pentagon => 'p',
74 ScatterShape::PentagonFilled => 'p',
75 ScatterShape::Hexagon => 'h',
76 ScatterShape::HexagonFilled => 'h',
77 ScatterShape::Octagon => '8',
78 ScatterShape::OctagonFilled => '8',
79 ScatterShape::Star => '*',
80 ScatterShape::StarFilled => '*',
81 }
82}
83
84pub const SYMBOL_FIELD_EM: f32 = 2.0;
88const LEGEND_DASH_NOMINAL_FONT_PX: f32 = 14.0;
89
90fn rule_dash_em(style: LineStylePreset) -> Option<Vec<f32>> {
91 let pattern = style.pattern();
92 if pattern.is_empty() {
93 None
94 } else {
95 Some(
96 pattern
97 .iter()
98 .map(|v| v / LEGEND_DASH_NOMINAL_FONT_PX)
99 .collect(),
100 )
101 }
102}
103
104#[cfg(test)]
105mod legend_update_tests {
106 use super::*;
107 use crate::data_config::DataLineStyleConfig;
108 use crate::line::LineStylePreset;
109 use crate::text::TextAlign;
110
111 fn line_cfg(color: Color, line_style: LineStylePreset) -> SeriesConfig {
112 SeriesConfig {
113 series_id: "s".into(),
114 source_id: None,
115 label: None,
116 x_column: "x".into(),
117 y_column: "y".into(),
118 render_type: DataRenderType::Line {
119 line: DataLineStyleConfig {
120 line_style,
121 line_color: color,
122 line_width: 1.0,
123 },
124 },
125 }
126 }
127
128 fn empty_content() -> RichText {
129 RichText {
130 segments: Vec::new(),
131 color: Color::BLACK,
132 font_size: 14.0,
133 font: String::new(),
134 align: TextAlign::Left,
135 }
136 }
137
138 #[test]
139 fn dashed_series_symbol_carries_rule_dash() {
140 let red = Color::new(1.0, 0.0, 0.0, 1.0);
141 let segs = series_symbol_segments(&line_cfg(red, LineStylePreset::Dash));
142 assert_eq!(segs.len(), 1);
143 assert!(segs[0].rule);
144 assert_eq!(segs[0].color, Some(red));
145 assert_eq!(
146 segs[0].rule_dash.as_deref(),
147 Some(&[8.0 / 14.0, 4.0 / 14.0][..])
148 );
149 }
150
151 #[test]
152 fn symbol_update_preserves_user_text_and_label_newlines() {
153 let red = Color::new(1.0, 0.0, 0.0, 1.0);
154 let blue = Color::new(0.0, 0.0, 1.0, 1.0);
155 let green = Color::new(0.0, 0.8, 0.0, 1.0);
156 let mut content = empty_content();
157 append_legend_entry(
158 &mut content,
159 series_symbol_segments(&line_cfg(red, LineStylePreset::Solid)),
160 "line 1\nline 2",
161 );
162 append_legend_entry(
163 &mut content,
164 series_symbol_segments(&line_cfg(blue, LineStylePreset::Solid)),
165 "beta",
166 );
167
168 let series = [
169 line_cfg(green, LineStylePreset::Dash),
170 line_cfg(red, LineStylePreset::Dot),
171 ];
172 assert_eq!(
173 update_legend_symbols_preserving_text(&mut content, &series),
174 2
175 );
176 let text: String = content.segments.iter().map(|s| s.text).collect();
177
178 assert_eq!(legend_entry_count(&content), 2);
179 assert!(text.contains("line 1\nline 2"));
180 assert!(text.contains("beta"));
181 assert_eq!(content.segments[0].color, Some(green));
182 assert!(content.segments[0].rule_dash.is_some());
183 }
184
185 #[test]
186 fn label_update_and_remove_touch_only_target_entry() {
187 let mut content = empty_content();
188 append_legend_entry(
189 &mut content,
190 series_symbol_segments(&line_cfg(Color::BLACK, LineStylePreset::Solid)),
191 "alpha",
192 );
193 append_legend_entry(
194 &mut content,
195 series_symbol_segments(&line_cfg(Color::BLACK, LineStylePreset::Solid)),
196 "beta",
197 );
198
199 set_legend_entry_label(
200 &mut content,
201 1,
202 series_symbol_segments(&line_cfg(Color::BLACK, LineStylePreset::Solid)),
203 rich_segments_from_text("gamma"),
204 );
205 let text: String = content.segments.iter().map(|s| s.text).collect();
206 assert!(text.contains("alpha"));
207 assert!(text.contains("gamma"));
208 assert!(!text.contains("beta"));
209
210 assert!(remove_legend_entry(&mut content, 0));
211 let text: String = content.segments.iter().map(|s| s.text).collect();
212 assert!(!text.contains("alpha"));
213 assert!(text.contains("gamma"));
214 assert_eq!(legend_entry_count(&content), 1);
215 }
216
217 #[test]
218 fn custom_legend_without_auto_entry_shape_is_left_untouched() {
219 let mut content = RichText::plain("custom legend text", Color::BLACK, 14.0, "");
220 let before = content.clone();
221 assert_eq!(
222 update_legend_symbols_preserving_text(
223 &mut content,
224 &[line_cfg(Color::BLACK, LineStylePreset::Solid)]
225 ),
226 0
227 );
228 assert_eq!(content, before);
229 assert!(!remove_legend_entry(&mut content, 0));
230 }
231}
232
233fn line_symbol(color: Color, style: LineStylePreset) -> Vec<RichSegment> {
236 let seg = match rule_dash_em(style) {
237 Some(dash) => RichSegment::dashed_rule(SYMBOL_FIELD_EM, Some(color), dash),
238 None => RichSegment::rule(SYMBOL_FIELD_EM, Some(color)),
239 };
240 vec![seg]
241}
242
243fn scatter_symbol(shape: &ScatterShape, color: Color) -> Vec<RichSegment> {
245 vec![RichSegment::fielded(
246 scatter_shape_char(shape),
247 SYMBOL_FIELD_EM,
248 Some(color),
249 )]
250}
251
252fn line_scatter_symbol(shape: &ScatterShape, color: Color) -> Vec<RichSegment> {
256 line_scatter_symbol_with_style(shape, color, LineStylePreset::Solid)
257}
258
259fn line_scatter_symbol_with_style(
260 shape: &ScatterShape,
261 color: Color,
262 style: LineStylePreset,
263) -> Vec<RichSegment> {
264 let glyph_em = 0.7;
265 let rule_em = (SYMBOL_FIELD_EM - glyph_em) * 0.5;
266 let rule = |width_em| match rule_dash_em(style) {
267 Some(dash) => RichSegment::dashed_rule(width_em, Some(color), dash),
268 None => RichSegment::rule(width_em, Some(color)),
269 };
270 vec![
271 rule(rule_em),
272 RichSegment::fielded(scatter_shape_char(shape), glyph_em, Some(color)),
273 rule(rule_em),
274 ]
275}
276
277pub fn symbol_segments(kind: &LegendEntryKind, color: Color) -> Vec<RichSegment> {
281 match kind {
282 LegendEntryKind::Line => line_symbol(color, LineStylePreset::Solid),
283 LegendEntryKind::Scatter => scatter_symbol(&ScatterShape::CircleFilled, color),
284 LegendEntryKind::LineScatter => line_scatter_symbol(&ScatterShape::CircleFilled, color),
285 }
286}
287
288fn blank_symbol() -> Vec<RichSegment> {
298 vec![RichSegment::fielded(' ', SYMBOL_FIELD_EM, None)]
299}
300
301pub fn series_symbol_segments(cfg: &SeriesConfig) -> Vec<RichSegment> {
304 match &cfg.render_type {
305 DataRenderType::Line { line } => line_symbol(line.line_color, line.line_style),
306 DataRenderType::Histogram { bar } => {
309 scatter_symbol(&ScatterShape::SquareFilled, bar.fill_color)
310 }
311 DataRenderType::Contour { contour, .. }
314 | DataRenderType::HeatmapContour { contour, .. } => {
315 line_symbol(contour.line.line_color, contour.line.line_style)
316 }
317 DataRenderType::Heatmap { .. } => blank_symbol(),
318 DataRenderType::Scatter { scatter }
319 | DataRenderType::ScatterErrorbarX { scatter, .. }
320 | DataRenderType::ScatterErrorbarY { scatter, .. }
321 | DataRenderType::ScatterErrorbarXY { scatter, .. } => {
322 scatter_symbol(&scatter.point_shape, scatter.point_color)
323 }
324 DataRenderType::ScatterLine { scatter, line }
325 | DataRenderType::LineScatterErrorbarX { scatter, line, .. }
326 | DataRenderType::LineScatterErrorbarY { scatter, line, .. }
327 | DataRenderType::LineScatterErrorbarXY { scatter, line, .. } => {
328 line_scatter_symbol_with_style(&scatter.point_shape, line.line_color, line.line_style)
329 }
330 }
331}
332
333pub fn append_legend_entry(content: &mut RichText, symbol: Vec<RichSegment>, label: &str) {
344 append_legend_entry_rich(content, symbol, rich_segments_from_text(label));
345}
346
347pub fn append_legend_entry_rich(
349 content: &mut RichText,
350 symbol: Vec<RichSegment>,
351 label: Vec<RichSegment>,
352) {
353 if !content.segments.is_empty() {
354 content.segments.push(RichSegment::plain('\n'));
355 }
356 content.segments.extend(symbol);
357 content.segments.push(RichSegment::plain(' '));
358 content.segments.push(RichSegment::plain('\t'));
359 content.segments.extend(label);
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
363struct LegendEntrySpan {
364 symbol: Range<usize>,
365 text: Range<usize>,
366 remove: Range<usize>,
367}
368
369fn is_plain_space(seg: &RichSegment) -> bool {
370 seg.text == ' '
371 && !seg.bold
372 && !seg.italic
373 && !seg.underline
374 && !seg.superscript
375 && !seg.subscript
376 && !seg.greek
377 && seg.color.is_none()
378 && seg.font_size.is_none()
379 && seg.field_em.is_none()
380 && !seg.rule
381 && seg.rule_dash.is_none()
382}
383
384fn symbol_anchor(segments: &[RichSegment], start: usize) -> Option<(usize, usize)> {
385 if start >= segments.len() || segments[start].field_em.is_none() {
386 return None;
387 }
388
389 let mut i = start;
390 while i < segments.len() && segments[i].field_em.is_some() {
391 i += 1;
392 }
393 let symbol_end = i;
394 while i < segments.len() && is_plain_space(&segments[i]) {
395 i += 1;
396 }
397 (i < segments.len() && segments[i].text == '\t').then_some((symbol_end, i))
398}
399
400fn legend_entry_spans(content: &RichText) -> Vec<LegendEntrySpan> {
401 let segments = &content.segments;
402 let mut anchors = Vec::new();
403 let mut i = 0;
404 while i < segments.len() {
405 if let Some((symbol_end, tab)) = symbol_anchor(segments, i) {
406 anchors.push((i, symbol_end, tab));
407 i = tab + 1;
408 } else {
409 i += 1;
410 }
411 }
412
413 let mut spans = Vec::with_capacity(anchors.len());
414 for (idx, &(symbol_start, symbol_end, tab)) in anchors.iter().enumerate() {
415 let next_start = anchors.get(idx + 1).map(|(start, _, _)| *start);
416 let mut text_end = next_start.unwrap_or(segments.len());
417 if next_start.is_some() && text_end > tab + 1 && segments[text_end - 1].text == '\n' {
418 text_end -= 1;
419 }
420
421 let remove_start = if symbol_start > 0 && segments[symbol_start - 1].text == '\n' {
422 symbol_start - 1
423 } else {
424 symbol_start
425 };
426 let remove_end = if idx == 0 {
427 next_start
428 .filter(|&start| start > 0 && segments[start - 1].text == '\n')
429 .unwrap_or(text_end)
430 } else {
431 text_end
432 };
433
434 spans.push(LegendEntrySpan {
435 symbol: symbol_start..symbol_end,
436 text: tab + 1..text_end,
437 remove: remove_start..remove_end,
438 });
439 }
440 spans
441}
442
443pub fn legend_entry_count(content: &RichText) -> usize {
445 legend_entry_spans(content).len()
446}
447
448pub fn update_legend_symbols_preserving_text(
452 content: &mut RichText,
453 series: &[SeriesConfig],
454) -> usize {
455 let spans = legend_entry_spans(content);
456 let mut replaced = 0;
457 for (span, cfg) in spans.into_iter().zip(series.iter()).rev() {
458 content
459 .segments
460 .splice(span.symbol, series_symbol_segments(cfg));
461 replaced += 1;
462 }
463 replaced
464}
465
466pub fn set_legend_entry_label(
469 content: &mut RichText,
470 entry_index: usize,
471 symbol: Vec<RichSegment>,
472 label: Vec<RichSegment>,
473) {
474 let spans = legend_entry_spans(content);
475 if let Some(span) = spans.get(entry_index) {
476 content.segments.splice(span.text.clone(), label);
477 } else {
478 append_legend_entry_rich(content, symbol, label);
479 }
480}
481
482pub fn remove_legend_entry(content: &mut RichText, entry_index: usize) -> bool {
485 let spans = legend_entry_spans(content);
486 let Some(span) = spans.get(entry_index) else {
487 return false;
488 };
489 content.segments.drain(span.remove.clone());
490 true
491}
492
493#[derive(Debug, Clone, PartialEq)]
495#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
496pub struct Legend {
497 pub visible: bool,
498 pub content: RichText,
505 pub corner: LegendCorner,
506 pub offset_x: f32,
508 pub offset_y: f32,
509 pub padding: f32,
511 pub bg_color: Color,
513 pub border_color: Color,
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use crate::data_config::DataLineStyleConfig;
520 use crate::line::LineStylePreset;
521 use crate::text::TextAlign;
522
523 fn line_cfg(color: Color) -> SeriesConfig {
524 line_cfg_style(color, LineStylePreset::Solid)
525 }
526
527 fn line_cfg_style(color: Color, line_style: LineStylePreset) -> SeriesConfig {
528 SeriesConfig {
529 series_id: "s".into(),
530 source_id: None,
531 label: None,
532 x_column: "x".into(),
533 y_column: "y".into(),
534 render_type: DataRenderType::Line {
535 line: DataLineStyleConfig {
536 line_style,
537 line_color: color,
538 line_width: 1.0,
539 },
540 },
541 }
542 }
543
544 fn empty_content() -> RichText {
545 RichText {
546 segments: Vec::new(),
547 color: Color::BLACK,
548 font_size: 14.0,
549 font: String::new(),
550 align: TextAlign::Left,
551 }
552 }
553
554 #[test]
555 fn series_symbol_takes_series_color_as_override() {
556 let red = Color::new(1.0, 0.0, 0.0, 1.0);
557 let segs = series_symbol_segments(&line_cfg(red));
558 assert_eq!(segs.len(), 1); assert_eq!(segs[0].text, '—');
560 assert_eq!(segs[0].color, Some(red));
561 assert_eq!(segs[0].font_size, None); }
563
564 #[test]
565 fn scatter_symbols_map_every_shape_to_a_glyph() {
566 for shape in [
567 ScatterShape::Circle,
568 ScatterShape::Square,
569 ScatterShape::Triangle,
570 ScatterShape::TriangleDown,
571 ScatterShape::TriangleLeft,
572 ScatterShape::TriangleRight,
573 ScatterShape::Diamond,
574 ScatterShape::Cross,
575 ScatterShape::Plus,
576 ScatterShape::Pentagon,
577 ScatterShape::Hexagon,
578 ScatterShape::Octagon,
579 ScatterShape::Star,
580 ScatterShape::CircleFilled,
581 ScatterShape::SquareFilled,
582 ScatterShape::TriangleFilled,
583 ScatterShape::TriangleDownFilled,
584 ScatterShape::TriangleLeftFilled,
585 ScatterShape::TriangleRightFilled,
586 ScatterShape::DiamondFilled,
587 ScatterShape::CrossFilled,
588 ScatterShape::PlusFilled,
589 ScatterShape::PentagonFilled,
590 ScatterShape::HexagonFilled,
591 ScatterShape::OctagonFilled,
592 ScatterShape::StarFilled,
593 ] {
594 let c = scatter_shape_char(&shape);
595 assert!(!c.is_control(), "{shape:?} → {c}");
596 }
597 }
598
599 #[test]
600 fn kind_symbols_carry_color_override_on_every_segment() {
601 let blue = Color::new(0.0, 0.0, 1.0, 1.0);
602 for kind in [
603 LegendEntryKind::Line,
604 LegendEntryKind::Scatter,
605 LegendEntryKind::LineScatter,
606 ] {
607 let segs = symbol_segments(&kind, blue);
608 assert!(!segs.is_empty());
609 assert!(segs.iter().all(|s| s.color == Some(blue)), "{kind:?}");
610 }
611 }
612
613 #[test]
614 fn two_appends_put_explicit_newline_between_entries() {
615 let mut content = empty_content();
616 append_legend_entry(
617 &mut content,
618 symbol_segments(&LegendEntryKind::Line, Color::BLACK),
619 "a",
620 );
621 append_legend_entry(
622 &mut content,
623 symbol_segments(&LegendEntryKind::Line, Color::BLACK),
624 "b",
625 );
626
627 let newlines = content.segments.iter().filter(|s| s.text == '\n').count();
628 assert_eq!(newlines, 1);
629 let texts: String = content.segments.iter().map(|s| s.text).collect();
632 assert_eq!(texts, "— \ta\n— \tb");
633 assert!(
635 content
636 .segments
637 .iter()
638 .filter(|s| s.text == 'a')
639 .all(|s| s.color.is_none())
640 );
641 }
642
643 #[test]
644 fn label_newlines_stay_explicit_in_content() {
645 let mut content = empty_content();
646 append_legend_entry(
647 &mut content,
648 symbol_segments(&LegendEntryKind::Line, Color::BLACK),
649 "line 1\nline 2",
650 );
651 let newlines = content.segments.iter().filter(|s| s.text == '\n').count();
652 assert_eq!(newlines, 1);
653 }
654
655 #[test]
656 fn one_line_composition_when_caller_joins_without_newline() {
657 let red = Color::new(1.0, 0.0, 0.0, 1.0);
660 let mut content = empty_content();
661 append_legend_entry(
662 &mut content,
663 symbol_segments(&LegendEntryKind::Line, red),
664 "a",
665 );
666 content.segments.push(RichSegment::plain(' '));
667 content
668 .segments
669 .extend(symbol_segments(&LegendEntryKind::Scatter, red));
670 content.segments.push(RichSegment::plain(' '));
671 content.segments.extend(rich_segments_from_text("b"));
672
673 assert!(content.segments.iter().all(|s| s.text != '\n'));
674 let texts: String = content.segments.iter().map(|s| s.text).collect();
675 assert_eq!(texts, "— \ta ● b");
676 }
677
678 fn field_series(render_type: DataRenderType) -> SeriesConfig {
679 SeriesConfig {
680 series_id: "f".into(),
681 source_id: None,
682 label: None,
683 x_column: "x".into(),
684 y_column: "y".into(),
685 render_type,
686 }
687 }
688
689 fn bar(color: Color) -> DataRenderType {
690 DataRenderType::Histogram {
691 bar: crate::data_config::DataBarStyleConfig {
692 fill_color: color,
693 border_color: Color::BLACK,
694 border_width: 1.0,
695 baseline: 0.0,
696 gap_px: 1.0,
697 width_ratio: 1.0,
698 orientation: crate::data_config::BarOrientation::Vertical,
699 bar_style_overrides: None,
700 },
701 }
702 }
703
704 fn contour(color: Color) -> crate::data_config::ContourConfig {
705 crate::data_config::ContourConfig {
706 levels: vec![1.0],
707 line: DataLineStyleConfig {
708 line_style: LineStylePreset::Dash,
709 line_color: color,
710 line_width: 1.0,
711 },
712 per_level_color: None,
713 labels: None,
714 }
715 }
716
717 fn matrix() -> crate::data_config::MatrixRef {
718 crate::data_config::MatrixRef {
719 columns: vec!["z0".into()],
720 orientation: crate::data_config::MatrixOrientation::ColumnsAreX,
721 grid_layout: crate::data_config::GridLayout::Centers,
722 }
723 }
724
725 fn fill() -> crate::data_config::FieldFillConfig {
726 crate::data_config::FieldFillConfig {
727 mode: crate::data_config::FillMode::Continuous,
728 shading: crate::data_config::Shading::Flat,
729 opacity: 1.0,
730 }
731 }
732
733 #[test]
736 fn field_series_symbols_come_from_the_primitive_that_is_drawn() {
737 let swatch = series_symbol_segments(&field_series(bar(Color::from_rgb8(1, 2, 3))));
738 assert_eq!(swatch.len(), 1);
739 assert_eq!(swatch[0].color, Some(Color::from_rgb8(1, 2, 3)));
740 assert_eq!(
741 swatch[0].text,
742 scatter_shape_char(&crate::data_config::ScatterShape::SquareFilled)
743 );
744
745 let level_color = Color::from_rgb8(4, 5, 6);
746 for render_type in [
747 DataRenderType::Contour {
748 matrix: matrix(),
749 contour: contour(level_color),
750 },
751 DataRenderType::HeatmapContour {
752 matrix: matrix(),
753 fill: fill(),
754 contour: contour(level_color),
755 },
756 ] {
757 let mark = series_symbol_segments(&field_series(render_type));
758 assert_eq!(mark.len(), 1, "a contour mark is one rule");
759 assert_eq!(mark[0].color, Some(level_color));
760 assert!(mark[0].rule, "a contour mark is a drawn rule");
763 assert!(mark[0].rule_dash.is_some(), "dashed contour, dashed mark");
764 }
765 }
766
767 #[test]
772 fn a_heatmap_gets_a_blank_full_width_mark_not_no_entry() {
773 let heatmap = field_series(DataRenderType::Heatmap {
774 matrix: matrix(),
775 fill: fill(),
776 });
777 let mark = series_symbol_segments(&heatmap);
778 assert_eq!(mark.len(), 1);
779 assert_eq!(mark[0].field_em, Some(SYMBOL_FIELD_EM));
780 assert_eq!(mark[0].text, ' ');
781
782 let series = [
784 line_cfg(Color::from_rgb8(10, 0, 0)),
785 heatmap,
786 field_series(bar(Color::from_rgb8(0, 10, 0))),
787 ];
788 let mut content = RichText {
789 segments: Vec::new(),
790 color: Color::BLACK,
791 font_size: 12.0,
792 font: String::new(),
793 align: TextAlign::Left,
794 };
795 for cfg in &series {
796 append_legend_entry(&mut content, series_symbol_segments(cfg), "label");
797 }
798 assert_eq!(legend_entry_count(&content), series.len());
799 assert_eq!(
800 update_legend_symbols_preserving_text(&mut content, &series),
801 series.len(),
802 "every series must own exactly one entry, or later symbols shift"
803 );
804 }
805}