X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=lily%2Fbeam.cc;h=a9dd24bda379306a3309e03769e52b883e604fab;hb=9efbad2d9487a05b04423e7e9f062968e8f8eaf4;hp=64f3e3cf775aa721316b216328443bcc8b468584;hpb=daee8243ff1c66bc714455a2d265ab02f03546ec;p=lilypond.git diff --git a/lily/beam.cc b/lily/beam.cc index 64f3e3cf77..a9dd24bda3 100644 --- a/lily/beam.cc +++ b/lily/beam.cc @@ -1,295 +1,718 @@ -#include "varray.hh" +/* + beam.cc -- implement Beam + + source file of the GNU LilyPond music typesetter + + (c) 1997--1998, 1998 Han-Wen Nienhuys + Jan Nieuwenhuizen + +*/ -#include "dimen.hh" + +/* + [TODO] + * centre beam symbol + * less hairy code + * redo grouping + */ + +#include + +#include "p-col.hh" +#include "array.hh" +#include "proto.hh" +#include "dimensions.hh" #include "beam.hh" +#include "abbreviation-beam.hh" #include "misc.hh" #include "debug.hh" -#include "symbol.hh" +#include "atom.hh" #include "molecule.hh" #include "leastsquares.hh" -#include "pcol.hh" #include "stem.hh" #include "paper-def.hh" #include "lookup.hh" #include "grouping.hh" +#include "stem-info.hh" +IMPLEMENT_IS_TYPE_B1 (Beam, Spanner); -struct Stem_info { - Real x; - Real idealy; - Real miny; - int no_beams; +Beam::Beam () +{ + slope_f_ = 0; + solved_slope_f_ = 0; + left_y_ = 0; + damping_i_ = 1; + quantisation_ = NORMAL; + multiple_i_ = 0; +} - - Stem_info(){} - Stem_info(const Stem*); -}; +void +Beam::add_stem (Stem*s) +{ + stems_.push (s); + s->add_dependency (this); + s->beam_l_ = this; + + if (!spanned_drul_[LEFT]) + set_bounds (LEFT,s); + else + set_bounds (RIGHT,s); +} -Stem_info::Stem_info(const Stem*s) +Molecule* +Beam::brew_molecule_p () const { - x = s->hindex(); - int dir = s->dir; - idealy = max(dir*s->top, dir*s->bot); - miny = max(dir*s->minnote, dir*s-> maxnote); - assert(miny <= idealy); + Molecule *mol_p = new Molecule; -} + Real internote_f = paper ()->internote_f (); + + Real x0 = stems_[0]->hpos_f (); + for (int j=0; j 0)? stems_[j-1] : 0; + Stem * next = (j < stems_.size ()-1) ? stems_[j+1] :0; + + Molecule sb = stem_beams (i, next, prev); + Real x = i->hpos_f ()-x0; + sb.translate (Offset (x, (x * slope_f_ + left_y_) * internote_f)); + mol_p->add_molecule (sb); + } + mol_p->translate_axis (x0 + - spanned_drul_[LEFT]->absolute_coordinate (X_AXIS), X_AXIS); -/* *************** */ + return mol_p; +} Offset -Beam::center()const +Beam::center () const { - assert(status >= POSTCALCED); + Real w= (paper ()->note_width () + width ().length ())/2.0; + return Offset (w, (left_y_ + w* slope_f_)*paper ()->internote_f ()); +} - Real w=(paper()->note_width() + width().length())/2.0; - return Offset(w, (left_pos + w* slope)*paper()->internote()); +void +Beam::do_pre_processing () +{ + if (!dir_) + set_default_dir (); } +void +Beam::do_print () const +{ +#ifndef NPRINT + DOUT << "slope_f_ " << slope_f_ << "left ypos " << left_y_; + Spanner::do_print (); +#endif +} -Beam::Beam() +void +Beam::do_post_processing () { - slope = 0; - left_pos = 0.0; + if (stems_.size () < 2) + { + warning (_ ("beam with less than two stems")); + transparent_b_ = true; + return ; + } + solve_slope (); + set_stemlens (); } void -Beam::add(Stem*s) +Beam::do_substitute_dependent (Score_element*o,Score_element*n) +{ + if (o->is_type_b (Stem::static_name ())) + stems_.substitute ((Stem*)dynamic_cast (o), n? (Stem*) dynamic_cast (n):0); +} + +Interval +Beam::do_width () const { - stems.bottom().add(s); - s->add_dependency(this); - s->print_flag = false; + return Interval (stems_[0]->hpos_f (), + stems_.top ()->hpos_f ()); } void -Beam::set_default_dir() +Beam::set_default_dir () { - int dirs[2]; - dirs[0]=0; dirs[1] =0; - for (iter_top(stems,i); i.ok(); i++) { - int d = i->get_default_dir(); - dirs[(d+1)/2] ++; + Drul_array total; + total[UP] = total[DOWN] = 0; + Drul_array count; + count[UP] = count[DOWN] = 0; + Direction d = DOWN; + + for (int i=0; i dir_ + ? (1 + d * s->dir_)/2 + : s->get_center_distance ((Direction)-d); + + if (current) + { + total[d] += current; + count[d] ++; + } + + } while (flip(&d) != DOWN); + + do { + if (!total[d]) + count[d] = 1; + } while (flip(&d) != DOWN); + + /* + [Ross] states that the majority of the notes dictates the + direction (and not the mean of "center distance") + + But is that because it really looks better, or because he + wants to provide some real simple hands-on rules. + + We have our doubts, so we simply provide all sensible alternatives. + */ + + Dir_algorithm a = (Dir_algorithm)rint(paper ()->get_var ("beam_dir_algorithm")); + switch (a) + { + case MAJORITY: + dir_ = (count[UP] > count[DOWN]) ? UP : DOWN; + break; + case MEAN: + // mean centre distance + dir_ = (total[UP] > total[DOWN]) ? UP : DOWN; + break; + default: + case MEDIAN: + // median centre distance + if (!count[UP]) + dir_ = DOWN; + else if (!count[DOWN]) + dir_ = UP; + else + dir_ = (total[UP] / count[UP] > total[DOWN] / count[DOWN]) ? UP : DOWN; + break; } - dir_i_ = (dirs[0] > dirs[1]) ? -1 : 1; - for (iter_top(stems,i); i.ok(); i++) { - i->dir = dir_i_; + + for (int i=0; i beam_dir_ = dir_; + if (!s->dir_forced_b_) + s->dir_ = dir_; } } /* - should use minimum energy formulation (cf linespacing) - */ + See Documentation/tex/fonts.doc + */ void -Beam::solve_slope() +Beam::solve_slope () { - Array sinfo; - for (iter_top(stems,i); i.ok(); i++) { - i->set_default_extents(); - Stem_info info(i); - sinfo.push(info); + /* + should use minimum energy formulation (cf linespacing) + */ + + assert (multiple_i_); + Array sinfo; + DOUT << "Beam::solve_slope: \n"; + for (int j=0; j mult_i_ = multiple_i_; + i->set_default_extents (); + if (i->invisible_b ()) + continue; + + Stem_info info (i); + sinfo.push (info); } - Real leftx = sinfo[0].x; - Least_squares l; - for (int i=0; i < sinfo.size(); i++) { - sinfo[i].x -= leftx; - l.input.push(Offset(sinfo[i].x, sinfo[i].idealy)); + if (! sinfo.size ()) + slope_f_ = left_y_ = 0; + else if (sinfo.size () == 1) + { + slope_f_ = 0; + left_y_ = sinfo[0].idealy_f_; } + else + { + Real leftx = sinfo[0].x_; + Least_squares l; + for (int i=0; i < sinfo.size (); i++) + { + sinfo[i].x_ -= leftx; + l.input.push (Offset (sinfo[i].x_, sinfo[i].idealy_f_)); + } + + l.minimise (slope_f_, left_y_); + + } + + solved_slope_f_ = dir_ * slope_f_; + + /* + This neat trick is by Werner Lemberg, damped = tanh (slope_f_) corresponds + with some tables in [Wanske] + */ + if (damping_i_) + slope_f_ = 0.6 * tanh (slope_f_) / damping_i_; + + /* + [TODO] + think - l.minimise(slope, left_pos); - Real dy = 0.0; - for (int i=0; i < sinfo.size(); i++) { - Real y = sinfo[i].x * slope + left_pos; - Real my = sinfo[i].miny; + dropping lq for stemlengths solves [d d d] [d g d] "bug..." - if (my - y > dy) - dy = my -y; + but may be a bit too crude, and result in lots of + too high beams... + + perhaps only if slope = 0 ? + */ + +// left_y_ = sinfo[0].minyf_; + + if (sinfo.size () >= 1) + { + Real staffline_f = paper ()->rule_thickness (); + Real epsilon_f = staffline_f / 8; + if (abs (slope_f_) < epsilon_f) + left_y_ = (sinfo[0].idealy_f_ + sinfo.top ().idealy_f_) / 2; + else + /* + symmetrical, but results often in having stemlength = minimal + + left_y_ = sinfo[0].dir_ == dir_ ? sinfo[0].miny_f_ : sinfo[0].maxy_f_; + + what about + */ + { + Real dx = stems_.top ()->hpos_f () - stems_[0]->hpos_f (); + if (sinfo[0].dir_ == sinfo.top ().dir_) + left_y_ = sinfo[0].idealy_f_ >? sinfo.top ().idealy_f_ - slope_f_ * dx; + // knee + else + left_y_ = sinfo[0].idealy_f_; + } } - left_pos += dy; - left_pos *= dir_i_; - slope *= dir_i_; - - // URG - Real sl = slope*paper()->internote(); - paper()->lookup_p_->beam(sl, 20 PT); - slope = sl /paper()->internote(); -} -void -Beam::set_stemlens() -{ - iter_top(stems,s); - Real x0 = s->hindex(); - for (; s.ok() ; s++) { - Real x = s->hindex()-x0; - s->set_stemend(left_pos + slope * x); + // uh? + Real dy = 0.0; + for (int i=0; i < sinfo.size (); i++) + { + Real y = sinfo[i].x_ * slope_f_ + left_y_; + Real my = sinfo[i].miny_f_; + + if (my - y > dy) + dy = my -y; } -} + left_y_ += dy; + left_y_ *= dir_; + slope_f_ *= dir_; + quantise_dy (); +} void -Beam::do_post_processing() +Beam::quantise_dy () { - solve_slope(); - set_stemlens(); + /* + [Ross] (simplification of) + Try to set slope_f_ complying with y-span of: + - zero + - beam_f / 2 + staffline_f / 2 + - beam_f + staffline_f + + n * interline + */ + + if (quantisation_ <= NONE) + return; + + Real interline_f = paper ()->interline_f (); + Real internote_f = interline_f / 2; + Real staffline_f = paper ()->rule_thickness (); + Real beam_f = paper ()->beam_thickness_f (); + + Real dx_f = stems_.top ()->hpos_f () - stems_[0]->hpos_f (); + + // dim(y) = internote; so slope = (y/internote)/x + Real dy_f = dx_f * abs (slope_f_ * internote_f); + + Real quanty_f = 0.0; + + /* UGR. ICE in 2.8.1; bugreport filed. */ + Array allowed_fraction (3); + allowed_fraction[0] = 0; + allowed_fraction[1] = (beam_f / 2 + staffline_f / 2); + allowed_fraction[2] = (beam_f + staffline_f); + + + Interval iv = quantise_iv (allowed_fraction, interline_f, dy_f); + quanty_f = (dy_f - iv.min () <= iv.max () - dy_f) + ? iv.min () + : iv.max (); + + + slope_f_ = (quanty_f / dx_f) / internote_f * sign (slope_f_); } +static int test_pos = 0; + + +/* + + Prevent interference from stafflines and beams. See Documentation/tex/fonts.doc + + */ void -Beam::set_grouping(Rhythmic_grouping def, Rhythmic_grouping cur) +Beam::quantise_left_y (bool extend_b) { - def.OK(); - cur.OK(); - assert(cur.children.size() == stems.size()); - - cur.split(def); - - Array b; + /* + we only need to quantise the start of the beam as dy is quantised too + if extend_b then stems must *not* get shorter + */ + + if (quantisation_ <= NONE) + return; + + /* + ---------------------------------------------------------- + ######## + ######## + ######## + --------------########------------------------------------ + ######## + + hang straddle sit inter hang + */ + + Real interline_f = paper ()->interline_f (); + Real internote_f = paper ()->internote_f (); + Real staffline_f = paper ()->rule_thickness (); + Real beam_f = paper ()->beam_thickness_f (); + + /* + [TODO] + it would be nice to have all allowed positions in a runtime matrix: + (multiplicity, minimum_beam_dy, maximum_beam_dy) + */ + + Real straddle = 0; + Real sit = beam_f / 2 - staffline_f / 2; + Real inter = interline_f / 2; + Real hang = interline_f - beam_f / 2 + staffline_f / 2; + + /* + Put all allowed positions into an array. + Whether a position is allowed or not depends on + strictness of quantisation, multiplicity and direction. + + For simplicity, we'll assume dir = UP and correct if + dir = DOWN afterwards. + */ + + // dim(left_y_) = internote + Real dy_f = dir_ * left_y_ * internote_f; + + Real beamdx_f = stems_.top ()->hpos_f () - stems_[0]->hpos_f (); + Real beamdy_f = beamdx_f * slope_f_ * internote_f; + + Array allowed_position; + if (quantisation_ != TEST) { - iter_top(stems,s); - Array flags; - for (; s.ok(); s++) { - int f = intlog2(abs(s->flag))-2; - assert(f>0); - flags.push(f); + if (quantisation_ <= NORMAL) + { + if ((multiple_i_ <= 2) || (abs (beamdy_f) >= staffline_f / 2)) + allowed_position.push (straddle); + if ((multiple_i_ <= 1) || (abs (beamdy_f) >= staffline_f / 2)) + allowed_position.push (sit); + allowed_position.push (hang); + } + else + // TODO: check and fix TRADITIONAL + { + if ((multiple_i_ <= 2) || (abs (beamdy_f) >= staffline_f / 2)) + allowed_position.push (straddle); + if ((multiple_i_ <= 1) && (beamdy_f <= staffline_f / 2)) + allowed_position.push (sit); + if (beamdy_f >= -staffline_f / 2) + allowed_position.push (hang); } - int fi =0; - b= cur.generate_beams(flags, fi); - b.insert(0,0); - b.push(0); - assert(stems.size() == b.size()/2); } - - iter_top(stems,s); - for (int i=0; i < b.size() && s.ok(); i+=2, s++) { - s->beams_left = b[i]; - s->beams_right = b[i+1]; + else + { + if (test_pos == 0) + { + allowed_position.push (hang); + cout << "hang" << hang << endl; + } + else if (test_pos==1) + { + allowed_position.push (straddle); + cout << "straddle" << straddle << endl; + } + else if (test_pos==2) + { + allowed_position.push (sit); + cout << "sit" << sit << endl; + } + else if (test_pos==3) + { + allowed_position.push (inter); + cout << "inter" << inter << endl; + } } -} +#if 0 + // this currently never happens + Real q = (dy_f / interline_f - dy_i) * interline_f; + if ((quantisation_ < NORMAL) && (q < interline_f / 3 - beam_f / 2)) + allowed_position.push (inter); +#endif -// todo. -Spanner * -Beam::do_break_at( PCol *, PCol *) const -{ - Beam *beam_p= new Beam(*this); - - return beam_p; + Interval iv = quantise_iv (allowed_position, interline_f, dy_f); + + Real quanty_f = dy_f - iv.min () <= iv.max () - dy_f ? iv.min () : iv.max (); + if (extend_b) + quanty_f = iv.max (); + + // dim(left_y_) = internote + left_y_ = dir_ * quanty_f / internote_f; } void -Beam::do_pre_processing() +Beam::set_stemlens () { - left = (*stems.top()) ->pcol_l_; - right = (*stems.bottom())->pcol_l_; - assert(stems.size()>1); - if (!dir_i_) - set_default_dir(); + Real staffline_f = paper ()->rule_thickness (); + Real interbeam_f = paper ()->interbeam_f (multiple_i_); + Real internote_f = paper ()->internote_f (); + Real beam_f = paper ()->beam_thickness_f (); + + // enge floots + Real epsilon_f = staffline_f / 8; + + /* + + Damped and quantised slopes, esp. in monotone scales such as + + [c d e f g a b c] + + will soon produce the minimal stem-length for one of the extreme + stems, which is wrong (and ugly). The minimum stemlength should + be kept rather small, in order to handle extreme beaming, such as + + [c c' 'c] %assuming no knee + + correctly. + To avoid these short stems for normal cases, we'll correct for + the loss in slope, if necessary. + + [TODO] + ugh, another hack. who's next? + Writing this all down, i realise (at last) that the Right Thing to + do is to assign uglyness to slope and stem-lengths and then minimise + the total uglyness of a beam. + Steep slopes are ugly, shortened stems are ugly, lengthened stems + are ugly. + How to do this? + + */ + + Real dx_f = stems_.top ()->hpos_f () - stems_[0]->hpos_f (); + Real damp_correct_f = paper ()->get_var ("beam_slope_damp_correct_factor"); + Real damped_slope_dy_f = (solved_slope_f_ - slope_f_) * dx_f + * sign (slope_f_); + damped_slope_dy_f *= damp_correct_f; + if (damped_slope_dy_f <= epsilon_f) + damped_slope_dy_f = 0; + + DOUT << "Beam::set_stemlens: \n"; + Real x0 = stems_[0]->hpos_f (); + Real dy_f = 0; + // urg + for (int jj = 0; jj < 10; jj++) + { + left_y_ += dy_f * dir_; + quantise_left_y (dy_f); + dy_f = 0; + for (int i=0; i < stems_.size (); i++) + { + Stem *s = stems_[i]; + if (s->transparent_b_) + continue; + + Real x = s->hpos_f () - x0; + // urg move this to stem-info + Real sy = left_y_ + slope_f_ * x; + if (dir_ != s->dir_) + sy -= dir_ * (beam_f / 2 + + (s->mult_i_ - 1) * interbeam_f) / internote_f; + s->set_stemend (sy); + Real y = s->stem_end_f () * dir_; + Stem_info info (s); + if (y > info.maxy_f_) + dy_f = dy_f ? info.miny_f_ - y; + } + } + if (damped_slope_dy_f && (dy_f >= 0)) + dy_f += damped_slope_dy_f; + damped_slope_dy_f = 0; + if (abs (dy_f) <= epsilon_f) + { + DOUT << "Beam::set_stemlens: " << jj << " iterations\n"; + break; + } + } + test_pos++; + test_pos %= 4; } - -Interval -Beam::width() const +/* + FIXME + ugh. this is broken and should be rewritten. + - [c8. c32 c32] + */ +void +Beam::set_grouping (Rhythmic_grouping def, Rhythmic_grouping cur) { - Beam * me = (Beam*) this; // ugh - return Interval( (*me->stems.top()) ->hindex(), - (*me->stems.bottom()) ->hindex() ); + def.OK (); + cur.OK (); + assert (cur.children.size () == stems_.size ()); + + cur.split (def); + + Array b; + { + Array flags; + for (int j=0; j flag_i_ - 2; + assert (f>0); + flags.push (f); + } + int fi =0; + b= cur.generate_beams (flags, fi); + b.insert (0,0); + b.push (0); + assert (stems_.size () == b.size ()/2); + } + + for (int j=0, i=0; i < b.size () && j beams_left_i_ = b[i]; + s->beams_right_i_ = b[i+1]; + multiple_i_ = multiple_i_ >? (b[i] >? b[i+1]); + } } /* beams to go with one stem. */ Molecule -Beam::stem_beams(Stem *here, Stem *next, Stem *prev)const +Beam::stem_beams (Stem *here, Stem *next, Stem *prev) const { - assert( !next || next->hindex() > here->hindex() ); - assert( !prev || prev->hindex() < here->hindex() ); - Real dy=paper()->internote()*2; - Real stemdx = paper()->rule_thickness(); - Real sl = slope*paper()->internote(); - paper()->lookup_p_->beam(sl, 20 PT); - - Molecule leftbeams; - Molecule rightbeams; - - /* half beams extending to the left. */ - if (prev) { - int lhalfs= lhalfs = here->beams_left - prev->beams_right ; - int lwholebeams= here->beams_left beams_right ; - Real w = (here->hindex() - prev->hindex())/4; - Symbol dummy; - Atom a(dummy); - if (lhalfs) // generates warnings if not - a = paper()->lookup_p_->beam(sl, w); - a.translate(Offset (-w, -w * sl)); - for (int j = 0; j < lhalfs; j++) { - Atom b(a); - b.translate(Offset(0, -dir_i_ * dy * (lwholebeams+j))); - leftbeams.add( b ); + assert (!next || next->hpos_f () > here->hpos_f ()); + assert (!prev || prev->hpos_f () < here->hpos_f ()); + + Real staffline_f = paper ()->rule_thickness (); + Real interbeam_f = paper ()->interbeam_f (multiple_i_); + Real internote_f = paper ()->internote_f (); + Real beam_f = paper ()->beam_thickness_f (); + + Real dy = interbeam_f; + Real stemdx = staffline_f; + Real sl = slope_f_* internote_f; + lookup_l ()->beam (sl, 20 PT, 1 PT); + + Molecule leftbeams; + Molecule rightbeams; + + /* half beams extending to the left. */ + if (prev) + { + int lhalfs= lhalfs = here->beams_left_i_ - prev->beams_right_i_ ; + int lwholebeams= here->beams_left_i_ beams_right_i_ ; + /* + Half beam should be one note-width, + but let's make sure two half-beams never touch + */ + Real w = here->hpos_f () - prev->hpos_f (); + w = w/2 note_width (); + Atom a; + if (lhalfs) // generates warnings if not + a = lookup_l ()->beam (sl, w, beam_f); + a.translate (Offset (-w, -w * sl)); + for (int j = 0; j < lhalfs; j++) + { + Atom b (a); + b.translate_axis (-dir_ * dy * (lwholebeams+j), Y_AXIS); + leftbeams.add_atom (b); } } - - if (next){ - int rhalfs = here->beams_right - next->beams_left; - int rwholebeams = here->beams_right beams_left; - - Real w = next->hindex() - here->hindex(); - Atom a = paper()->lookup_p_->beam(sl, w + stemdx); - - int j = 0; - for (; j < rwholebeams; j++) { - Atom b(a); - b.translate(Offset(0, -dir_i_ * dy * j)); - rightbeams.add( b ); + + if (next) + { + int rhalfs = here->beams_right_i_ - next->beams_left_i_; + int rwholebeams = here->beams_right_i_ beams_left_i_; + + Real w = next->hpos_f () - here->hpos_f (); + Atom a = lookup_l ()->beam (sl, w + stemdx, beam_f); + a.translate_axis( - stemdx/2, X_AXIS); + int j = 0; + Real gap_f = 0; + if (here->beam_gap_i_) + { + int nogap = rwholebeams - here->beam_gap_i_; + for (; j < nogap; j++) + { + Atom b (a); + b.translate_axis (-dir_ * dy * j, Y_AXIS); + rightbeams.add_atom (b); + } + // TODO: notehead widths differ for different types + gap_f = paper ()->note_width () / 2; + w -= 2 * gap_f; + a = lookup_l ()->beam (sl, w + stemdx, beam_f); } - w /= 4; - if (rhalfs) - a = paper()->lookup_p_->beam(sl, w); - - for (; j < rwholebeams + rhalfs; j++) { - Atom b(a); - b.translate(Offset(0, -dir_i_ * dy * j)); - rightbeams.add(b ); + for (; j < rwholebeams; j++) + { + Atom b (a); + b.translate (Offset (gap_f, -dir_ * dy * j)); + rightbeams.add_atom (b); } - - } - leftbeams.add(rightbeams); - return leftbeams; -} + w = w/2 note_width (); + if (rhalfs) + a = lookup_l ()->beam (sl, w, beam_f); + + for (; j < rwholebeams + rhalfs; j++) + { + Atom b (a); + b.translate_axis (-dir_ * dy * j, Y_AXIS); + rightbeams.add_atom (b); + } -Molecule* -Beam::brew_molecule_p() const return out; -{ - Real inter=paper()->internote(); - out = new Molecule; - Real x0 = stems.top()->hindex(); - - for (iter_top(stems,i); i.ok(); i++) { - PCursor p(i-1); - PCursor n(i+1); - Stem * prev = p.ok() ? p.ptr() : 0; - Stem * next = n.ok() ? n.ptr() : 0; - - Molecule sb = stem_beams(i, next, prev); - Real x = i->hindex()-x0; - sb.translate(Offset(x, (x * slope + left_pos)* inter)); - out->add(sb); } - out->translate(Offset(x0 - left->hpos,0)); -} + leftbeams.add_molecule (rightbeams); -void -Beam::do_print()const -{ -#ifndef NPRINT - mtor << "slope " <