/*
    This file is part of Msc-generator.
    Copyright (C) 2008-2024 Zoltan Turanyi
    Distributed under GNU Affero General Public License.

    Msc-generator is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Msc-generator is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/

/** @file boxes.cpp The basic definitions of boxes, pipes, dividers and parallel constructs
 * @ingroup libmscgen_files */

#include <math.h>
#include <cassert>
#include <iostream>
#include <algorithm>
#include "msc.h"

using namespace msc;


template<> const char EnumEncapsulator<EBoxCollapseType>::names[][ENUM_STRING_LEN] =
{"invalid", "no", "yes", "arrow", ""};

template<> const char * const EnumEncapsulator<EBoxCollapseType>::descriptions[] =
{"", "Do not collapse this box or entity.", "Collapse this box or entity.",
"Collapse the box to a block arrow.", ""};

/** Return the arc in the catalog with the signature most similar to 'cat'.
 * We score how many of 'file_pos', 'label', 'src', 'dst' are equal
 * (or in case of file_pos overlapping). On ties, we break on where is the
 * larger overlap.*/
BoxSignatureCatalog::iterator BoxSignature::WhichIsSimilar(BoxSignatureCatalog &cat) const
{
    BoxSignatureCatalog::iterator ret = cat.end();
    int match = 0;
    FileLineColDiff diff;
    for (auto i = cat.begin(); cat.end()!=i; i++) {
        //If all line and col numbers match that is 2 points.
        //if any one line or col mismatches, that is -1 point
        //if the number of levels ismatch, that is 0 points
        int local_match = 2;
        for (const FileLineCol *f = &i->first.file_pos.start, *g = &file_pos.start; f && g; f = f->GetNext(), g = g->GetNext()) {
            if ((g->GetNext()==nullptr)!=(f->GetNext()==nullptr)) {
                local_match = 0;
                break;
            } else {
                if (f->line != g->line)
                    local_match--;
                if (f->col == g->col)
                    local_match--;
            }
        }
        local_match = std::max(local_match, 0); //dont go negative
        if (i->first.label == label) local_match++;
        if (i->first.src == src) local_match++;
        if (i->first.dst == dst) local_match++;
        if (i->first.content_len == content_len) local_match++;
        if (local_match < match) continue;
        if (local_match == match && diff>=file_pos.start - i->first.file_pos.start)
            continue;
        ret = i;
        diff = file_pos.start - i->first.file_pos.start;
        match = local_match;
    }
    //dont return a match if at least three pieces equal.
    return match>=4 ? ret : cat.end();
}

/**Convert EBoxCollapseType to a char for serializing.*/
char msc::EBoxCollapseTypeToChar(EBoxCollapseType t)
{
    switch (t) {
    default:
    case EBoxCollapseType::INVALID:     return '0';
    case EBoxCollapseType::EXPAND:      return '1';
    case EBoxCollapseType::COLLAPSE:    return '2';
    case EBoxCollapseType::BLOCKARROW:  return '3';
    }
}

/**Convert a char to EBoxCollapseType for deserializing.*/
EBoxCollapseType msc::CharToEBoxCollapseType(char c)
{
    switch (c) {
    default:
    case '0': return EBoxCollapseType::INVALID;
    case '1': return EBoxCollapseType::EXPAND;
    case '2': return EBoxCollapseType::COLLAPSE;
    case '3': return EBoxCollapseType::BLOCKARROW;
    }
}

/** Serialize the signature to a string*/
std::string BoxSignature::Serialize() const
{
    std::string s("1\n"); //file format version #1
    s << file_pos.Print() << "\n";
    s << label << "\n";
    s << src << "\n";
    s << dst << "\n";
    s << content_len << "\n";
    return s;
}

/** Deserialize the signature from a string. Sets 's' to the first char after us in the string.*/
bool BoxSignature::Deserialize(std::string_view &s)
{
    auto ver = DeserializeInt(s);
    if (!ver.first || ver.second>1) return false;
    DeserializeLine(s); //eat the rest till the lineend
    if (s.length()==0 || s.front()==0) {
        _ASSERT(0);
        return false;
    }
    if (!file_pos.Read(s)) return false;
    //version #0 stored the position in the file only
    if (ver.second==0) return true;
    if (!DeserializeLine(s, true).first) return false; //newline must follow version
    //version #1 stores label and src/dst, too.
    label = DeserializeLine(s).second;
    if (s.length()==0 || s.front()==0) return false;
    src = DeserializeLine(s).second;
    if (s.length()==0 || s.front()==0) return false;
    dst = DeserializeLine(s).second;
    if (s.length()==0 || s.front()==0) return false;
    auto l = DeserializeInt(s);
    if (!l.first) return false;
    content_len = l.second;
    DeserializeLine(s); //eat away any newline that follows.
    _ASSERT(ver.second==1);
    return true;
}

/** Serialize the signature to a string */
std::string BoxSignatureCatalog::Serialize() const
{
    const size_t num = std::ranges::count_if(*this, [](const value_type& e) { return EBoxCollapseTypeToChar(e.second)!='0'; });
    std::string s("0\n"); //version
    s << num << "\n"; //size
    for (auto &i : *this)
        if (EBoxCollapseTypeToChar(i.second)!='0')
            s << EBoxCollapseTypeToChar(i.second) << ' ' << i.first.Serialize(); //BoxSignature::Serialize() appends a '\n'
    return s;
}

/** Deserialize the signature from a string. Sets 's' to the first char after us in the string.*/
bool BoxSignatureCatalog::Deserialize(std::string_view & s)
{
    //always accept empty string
    if (s.length()==0 || s.front()==0) {
        clear();
        return true;
    }
    BoxSignatureCatalog cat;

    auto ver = DeserializeInt(s);
    if (!ver.first || ver.second!=0) return false;
    DeserializeLine(s); //eat the rest till the lineend
    if (s.length()==0 || s.front()==0) {
        _ASSERT(0);
        return false;
    }
    auto num = DeserializeInt(s);
    if (!num.first || num.second<0) return false;
    DeserializeLine(s); //eat the rest till the lineend
    for (; s.length() && s.front() && num.second>0; num.second--) {
        EBoxCollapseType type = CharToEBoxCollapseType(s.front());
        if (s.length()>1 && s[1]!=' ') {
            _ASSERT(0);
            return false;
        }
        s.remove_prefix(2);
        BoxSignature sig;
        if (!sig.Deserialize(s)) {
            _ASSERT(0);
            return false;
        }
        cat[sig] = type;
    }
    operator=(std::move(cat));
    return true;
}


/** Standard constructor of boxes. */
Box::Box(EArcSymbol t, const FileLineColRange &l,
    std::string_view s, const FileLineColRange &sl,
    std::string_view d, const FileLineColRange &dl, MscChart *msc) :
    LabelledArc(EArcCategory::BOX, msc),
    type(t), collapsed(EBoxCollapseType::EXPAND),
    drawEntityLines(true), mscgen_compat(MSCGEN_COMPAT_NONE)
{
    _ASSERT(IsArcSymbolBox(type));
    src = chart->FindAllocEntity(s, sl);
    dst = chart->FindAllocEntity(d, dl);

    //fill in part of the signature (label comes later)
    sig.file_pos = l;
    if (src) sig.src = src->name;
    if (dst) sig.dst = dst->name;

    //If both src and dst specified, order them
    if (src && dst && src->pos > dst->pos) {
        std::swap(src, dst);
        sig.src.swap(sig.dst);
    }
    keep_together = true;
}

/** Constructor of mscgen-style boxes. */
Box::Box(Emscgen_compat c, const FileLineColRange &l,
    std::string_view s, const FileLineColRange &sl,
    std::string_view d, const FileLineColRange &dl, MscChart *msc) :
    LabelledArc(EArcCategory::BOX, msc),
    type(EArcSymbol::BOX_SOLID), collapsed(EBoxCollapseType::EXPAND),
    drawEntityLines(true), mscgen_compat(c)
{
    _ASSERT(IsArcSymbolBox(type));
    src = chart->FindAllocEntity(s, sl);
    dst = chart->FindAllocEntity(d, dl);

    //fill in part of the signature (label comes later)
    sig.file_pos = l;
    if (src) sig.src = src->name;
    if (dst) sig.dst = dst->name;

    //If both src and dst specified, order them
    if (src && dst && src->pos > dst->pos) {
        std::swap(src, dst);
        sig.src.swap(sig.dst);
    }
    keep_together = true;
}

/** Return the refinement style for a box symbol. */
const MscStyleCoW *Box::GetRefinementStyle4BoxSymbol(EArcSymbol t) const
{
    //refinement for boxes
    switch (t) {
    case EArcSymbol::BOX_SOLID:
        return (MscStyleCoW*)&chart->MyCurrentContext().styles["--"];
    case EArcSymbol::BOX_DASHED:
        return (MscStyleCoW*)&chart->MyCurrentContext().styles["++"];
    case EArcSymbol::BOX_DOTTED:
        return (MscStyleCoW*)&chart->MyCurrentContext().styles[".."];
    case EArcSymbol::BOX_DOUBLE:
        return (MscStyleCoW*)&chart->MyCurrentContext().styles["=="];
    case EArcSymbol::BOX_UNDETERMINED_FOLLOW:
    default:
        _ASSERT(0);
        return nullptr; /*do nothing*/
    };
}



BoxSeries::BoxSeries(gsl::owner<Box *> first, gsl::owner<AttributeList *>l) :
    ArcBase(EArcCategory::BOX_SERIES, first->chart),
    drawing_variant(1), left_space(10), right_space(10) //These two will be needed in calls to GetEdgeXPos() *before* Width()
{
    AddBox(first, l);
    keep_together = true; //we will do our own pagination if needed, we shall not be cut in half by MscChart::ArcHeightList()
}

/** Adds content to a box.*/
Box* Box::AddArcList(gsl::owner<ArcList*>l)
{
    if (valid && l && l->size())
        content.splice(content.end(), *l);
    delete l;
    keep_together = false;
    return this;
}

bool BoxSeries::StoreNoteTemporarily(Note* cn) {
    //Set the target to the last Box
    //We only process comments targeting a Box and not a BoxSeries
    cn->SetTarget(series.back().get());
    tmp_stored_notes.emplace_back(std::unique_ptr<Note>(cn), "");
    return true;
}

void BoxSeries::ReinsertTmpStoredNotes(ArcList& list, ArcList::iterator after) {
    ArcList::iterator before = std::next(after);
    for (auto& [pNote, _] : tmp_stored_notes)
        list.insert(before, std::move(pNote));
    tmp_stored_notes.clear();
}

//It can be called even if the series is empty.
//If we add subsequent boxes, this should be called after adding content
//to "f', but before adding any attributes to it. (so that it can inherit
//unset attributes from the first element in the series.)
BoxSeries* BoxSeries::AddBox(gsl::owner<Box*> f, gsl::owner<AttributeList*> l)
{
    if (f==nullptr) {
        delete l;
        return this;
    }
    if (series.size())
        draw_pass = f->draw_pass;
    if (f->type==EArcSymbol::BOX_UNDETERMINED_FOLLOW) {
        _ASSERT(series.size());
        f->type = series.size() ? series.back()->type : EArcSymbol::BOX_SOLID;
    }
    valid &= f->valid;
    collected_attrs.Append(l);
    delete l;
    f->AddAttributeList(Duplicate(collected_attrs).release());
    series.Append(f);
    return this;
}


void Box::AddAttributeList(gsl::owner<AttributeList*> l)
{
    //First check for a collapse attribute to see what state we are of
    //also set label, so we can add it to signature
    if (l)
        for (auto i = l->begin(); i!=l->end(); /*nope*/) {
            if ((*i)->Is("collapsed") || (*i)->Is("label")) {
                AddAttribute(**i);
                //Delete any collapsed attribute
                l->erase(i++);
            } else {
                i++;
            }
        }
    //fill in rest of signature
    sig.label = label;
    sig.content_len = content.size();
    StringFormat::RemovePosEscapes(sig.label); //since these would make labels different at different location

    //Search for instructions (from GUI) if we should collapse or expand this box
    //these override any attribute setting
    if (!chart->SkipContent()) {
        auto force = sig.WhichIsSimilar(chart->force_box_collapse);
        if (force != chart->force_box_collapse.end() && content.size() &&
            collapsed != force->second && EBoxCollapseType::INVALID != force->second) {
            //keep forced stuff only if we have content and can be collapsed at all
            collapsed = force->second;
            chart->force_box_collapse_instead[sig] = force->second;
        } else {
            if (force != chart->force_box_collapse.end())
                //remove the entry we have applied - not to match to something else accidentally
                chart->force_box_collapse.erase(force);
            if (chart->prepare_element_controls)
                chart->force_box_collapse_instead[sig] = EBoxCollapseType::INVALID;
        }
    }
    //Now the 'collapsed' member is final, but no (other) attribute has been set.

    //Set style
    const char *st;
    switch (collapsed) {
    case EBoxCollapseType::COLLAPSE:
        st = "box_collapsed"; break;
    case EBoxCollapseType::BLOCKARROW:
        st = "box_collapsed_arrow"; break;
    default:
        _ASSERT(0);
        FALLTHROUGH;
    case EBoxCollapseType::EXPAND:
        st = content.size() ? "box" : "emptybox";
    }
    style = chart->MyCurrentContext().styles[st];
    switch (mscgen_compat) {
    default: _ASSERT(0); FALLTHROUGH;
    case MSCGEN_COMPAT_NONE: break;
    case MSCGEN_COMPAT_BOX: break;
    case MSCGEN_COMPAT_ABOX:
        style.write().line.corner = ECornerType::BEVEL;
        style.write().line.radius = 10;
        break;
    case MSCGEN_COMPAT_RBOX:
        style.write().line.corner = ECornerType::ROUND;
        style.write().line.radius = 10;
        break;
    case MSCGEN_COMPAT_NOTE:
        style.write().line.corner = ECornerType::NOTE;
        style.write().line.radius = 10;
        break;
    }
    //Add the global text format
    SetStyleWithText(static_cast<MscStyleCoW*>(nullptr), nullptr);
    //Turn on word wrapping for mscgen-defined boxes
    if (mscgen_compat!=MSCGEN_COMPAT_NONE)
        style.write().text.AddAttribute(Attribute("text.wrap", "yes", FileLineColRange(), FileLineColRange()), chart, EStyleType::ELEMENT);
    //Add refinement style
    const MscStyleCoW * pStyle = GetRefinementStyle4BoxSymbol(type);
    if (pStyle)
        style += *pStyle;

    //Find position of tag label attribute (if any), prepend it via an escape
    FileLineCol tag_label_pos;
    if (l)
        for (auto &pAttr : *l)
            if (pAttr->Is("tag"))
                tag_label_pos = pAttr->linenum_value.start;
    //Now set the attributes
    LabelledArc::AddAttributeList(l);
    //Convert color and style names in labels
    if (tag_label.length()>0) {
        StringFormat basic = style.read().tag_text;
        StringFormat::ExpandReferences(tag_label, chart, tag_label_pos,
            &basic, false, true, StringFormat::LABEL, true);
        //re-insert position, so that FinalizeLabels has one
        tag_label.insert(0, tag_label_pos.Print());
    }
}

bool Box::AddAttribute(const Attribute &a)
{
    if (a.type == EAttrType::STYLE) return LabelledArc::AddAttribute(a);
    if (a.Is("collapsed")) {
        if (content.size()==0) {
            chart->Error.Error(a, false, "The 'collapsed' attribute can only be specified for boxes with content.",
                "Ignoring attribute.");
            return true;
        }
        if (!a.EnsureNotClear(chart->Error, EStyleType::ELEMENT))
            return true;
        if (a.type == EAttrType::BOOL) {
            collapsed = a.yes ? EBoxCollapseType::COLLAPSE : EBoxCollapseType::EXPAND;
            return true;
        }
        if (a.type == EAttrType::STRING && Convert(a.value, collapsed))
            return true;
        a.InvalidValueError(CandidatesFor(collapsed), chart->Error);
        return true;
    }
    if (a.Is("tag")) {
        if (!a.CheckType(EAttrType::STRING, chart->Error)) return true;
        //EAttrType::CLEAR is OK above with value = ""
        tag_label = a.value;
        return true;
    }
    if (mscgen_compat!=MSCGEN_COMPAT_NONE &&
        (a.Is("textbgcolour") || a.Is("textbgcolor"))) {
        chart->Error.WarnMscgenAttr(a, false, "fill.color");
        return LabelledArc::AddAttribute(Attribute("fill.color", a));
    }   //else fallthrough
    return LabelledArc::AddAttribute(a);
}

void Box::AttributeNames(Csh &csh)
{
    LabelledArc::AttributeNames(csh);
    csh.AttributeNamesForStyle("box");
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"collapsed",
        "Turn this on to collapse the box (hiding its content).",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"tag",
        "Specify the tag label with this attribute. Needed if you want the box to have a tag.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"vident",
        "In case of boxes larger than their content, this sets vertical alignment.",
        EHintType::ATTR_NAME));
}

/** Callback for drawing a symbol for the values of the 'collapsed' attribute of boxes.
* @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForBoxCollapsed(Canvas *canvas, CshHintGraphicParam p, CshHintStore &csh)
{
    if (!canvas) return false;
    EBoxCollapseType t = (EBoxCollapseType)(int)p;
    switch (t) {
    case EBoxCollapseType::COLLAPSE:
        return CshHintGraphicCallbackForYesNo(canvas, CshHintGraphicParam(1), csh);
    case EBoxCollapseType::EXPAND:
        return CshHintGraphicCallbackForYesNo(canvas, CshHintGraphicParam(0), csh);
    case EBoxCollapseType::BLOCKARROW:
        return CshHintGraphicCallbackForSide(canvas, CshHintGraphicParam(int(ESide::LEFT)), csh);
    default:
        _ASSERT(0);
    }
    return true;
}

bool Box::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEqual(attr, "collapsed")) {
        csh.AddToHints(EnumEncapsulator<EBoxCollapseType>::names,
            EnumEncapsulator<EBoxCollapseType>::descriptions,
            csh.HintPrefix(COLOR_ATTRVALUE),
            EHintType::ATTR_VALUE, CshHintGraphicCallbackForBoxCollapsed);
        return true;
    }
    if (csh.AttributeValuesForStyle(attr, "box")) return true;
    if (CaseInsensitiveEqual(attr, "tag"))
        return true;
    if (CaseInsensitiveEqual(attr, "vident")) {
        csh.AddToHints(EnumEncapsulator<EVIdentType>::names, nullptr,
            csh.HintPrefix(COLOR_ATTRVALUE), EHintType::ATTR_VALUE,
            CshHintGraphicCallbackForVIdent);
        return true;
    }
    if (CaseInsensitiveEqual(attr, "vident")) {
        static const char * const names_descriptions[] = {"", nullptr,
            "content", "The box will be as high as its content.",
            "auto", "The box will be as high as its content or higher if arrows refer to it.",
            "full", "The box will be as high as the whole of the inline construct.",
            ""
        };
        csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_LABEL_ESCAPE), EHintType::ATTR_VALUE);
        return true;
    }
    if (LabelledArc::AttributeValues(attr, csh)) return true;
    return false;
}

EDirType BoxSeries::GetToucedEntities(NEntityList &el) const
{
    _ASSERT(!chart->IsVirtualEntity(series.front()->src));
    _ASSERT(!chart->IsVirtualEntity(series.front()->dst));
    el.push_back(series.front()->src);
    el.push_back(series.front()->dst);
    return EDirType::INDETERMINATE;
}

EndConnection BoxSeries::GetConnection(bool is_src) const
{
    EndConnection ret(GetSidePos(is_src));
    if (chart->IsVirtualEntity(ret.entity)) {
        ret.error_msg_no_join = is_src ? "Specify the side (left in this case) of a box you want to join to." :
            "Specify the side (right in this case) of a box you want to join to.";
        ret.error_msg_no_join_add = "Try using, e.g., 'A--B' instead of just '--' or 'A--'.";
    } else
        ret.error_msg_no_join = nullptr;
    ret.is_src = is_src;
    ret.body_is_left = !is_src;
    ret.accepts_curly_join = false;
    ret.can_curly_join = false;
    ret.is_unspecified = false;
    ret.is_indicator = false;
    return ret;
}

ArrowEnding BoxSeries::GetSidePos(bool left) const
{
    _ASSERT(series.size());
    //This is imprecise before Width(), since left/right_space is not calculated
    ArrowEnding ret;
    if (left) ret = ArrowEnding(series.front()->src, -left_space);
    else      ret = ArrowEnding(series.front()->dst, right_space);
    //dont return null but LSide/RSide instead,
    //So that the resulting ArrowEntity sorts well
    if (chart->IsVirtualEntity(ret.entity)) {
        ret.entity = left ? chart->LSide : chart->RSide;
        ret.offset = 0;
    } else
        //If we are before PostParseProcess(), we need to return a non-group entity
        if (ret.entity->children_names.size())
            ret.entity = chart->FindWhoIsShowingInsteadOf(ret.entity, left);
    return ret;
}

ArcBase* Box::PostParseProcess(Canvas &canvas, bool hide, EntityRef &left, EntityRef &right,
                               Numbering& number, MscElement** target, ArcBase* byref_target)
{
    ArcBase *ret = this;
    if (collapsed == EBoxCollapseType::BLOCKARROW) {
        NEntityList el;
        //replace us with a block arrow - here we are sure to be alone in the series
        if (!chart->IsVirtualEntity(src)) el.Append(src);
        if (!chart->IsVirtualEntity(dst)) el.Append(dst);
        EDirType dir = chart->GetTouchedEntitiesArcList(content, el);
        if (el.size()<2) {
            collapsed = EBoxCollapseType::COLLAPSE; //no actual content, no guiding src and dst - switch to regular collapse
            return PostParseProcess(canvas, hide, left, right, number, target, byref_target); //recursive call - start over
        }
        if (dir == EDirType::INDETERMINATE) dir = EDirType::BIDIR;
        //el.erase(std::remove_if(el.begin(), el.end(), [&](auto &e) {return chart->IsVirtualEntity(e); }), el.end());
        el.sort([](auto &a, auto &b) {return a->pos_exp<b->pos_exp; });
        if (dir == EDirType::LEFT)
            std::reverse(el.begin(), el.end());
        BlockArrow *aba = new BlockArrow(el, dir == EDirType::BIDIR, type, *this, GetSignature());
        aba->AddAttributeList(nullptr);
        aba->CombineComments(this); //we pass on our notes to the block arrow
        MscElement *const old_target = *target;
        ret = aba->PostParseProcess(canvas, hide, left, right, number, target, byref_target);
        if (ret != aba) delete aba;
        if (old_target != *target && ret == nullptr)
            *target = DELETE_NOTE;
    } else
        ret = LabelledArc::PostParseProcess(canvas, hide, left, right, number, target, byref_target);
    //Add numbering, if needed
    EntityRef left_content=nullptr, right_content=nullptr;
    if (content.size()) {
        //If we have attached a number to the label of a box, set flag on number,
        //so that if we add levels inside the content of the box (before displaying any number)
        //we continue from present value
        if (*style.read().numbering && label.length()!=0)
            number.SetDecrementOnSizeIncrease(numberingStyle.Last().increment);
        const bool hide_i = hide || (collapsed!=EBoxCollapseType::EXPAND);
        chart->PostParseProcessArcList(canvas, hide_i, content, false,
            left_content, right_content, number, target);
        //If we are collapsed (but not to blockarrow), but not hidden and "indicator" attribute is set,
        //then add an indicator to the end of the list (which will have only elements
        //with zero height here, the rest removed themselves due to hide_i==true
        if (collapsed==EBoxCollapseType::COLLAPSE && style.read().indicator.value() && !hide) {
            //src and dst of the Indicator will be set in BoxSeries::PostParseProcess
            content.Append(std::make_unique<Indicator>(chart, indicator_style, file_pos));
            chart->Progress.DoneItem(EArcSection::POST_PARSE, EArcCategory::INDICATOR);
        }
        number.CancelDecrementOnSizeIncrease();
    }
    //left & right will not expand if src and dst is unspecified
    left = chart->EntityMinByPos(left, src);
    right = chart->EntityMaxByPos(right, dst);
    if (src == nullptr) src = left_content;
    if (dst == nullptr) dst = right_content;
    return ret;
}

void Box::FinalizeLabels(Canvas &canvas)
{
    chart->FinalizeLabelsArcList(content, canvas);
    LabelledArc::FinalizeLabels(canvas);
    //Now finalize tag label
    if (tag_label.length()==0) return;
    //We add reference numbers to tag labels, and also kill off any \s or similar
    //We can start with a dummy pos, since the label's pos is prepended
    //during AddAttributeList. Note that with this calling
    //(references parameter true), ExpandReferences will only emit errors
    //to missing references - all other errors were already emitted in
    //the call in AddAttributeList()
    StringFormat basic = style.read().tag_text;
    basic.Apply(tag_label.c_str()); //do not change 'tag_label'
    StringFormat::ExpandReferences(tag_label, chart, FileLineCol(),
        &basic, true, true, StringFormat::LABEL, true);
    parsed_tag_label.Set(tag_label, canvas, chart->Shapes, style.read().tag_text);
}

ArcBase* BoxSeries::PostParseProcess(Canvas &canvas, bool hide, EntityRef &left, EntityRef &right,
                                     Numbering& number, MscElement** target, ArcBase* byref_target)
{
    if (!valid || series.size()==0) return nullptr;
    //If first segment is compressed or parallel, copy that to full series
    vspacing = series.front()->vspacing;
    //parallel = (*series.begin())->parallel;
    keep_with_next = series.back()->keep_with_next;
    draw_pass = series.front()->draw_pass;

    ArcBase *ret = this;
    EntityRef src=nullptr, dst=nullptr;
    for (auto &pBox : series) {
        if (pBox!=series.front() && pBox->parallel) {
            chart->Error.Error(pBox->file_pos.start,
                "Attribute 'parallel' can only be specified in the first "
                "element in a box series. Ignoring it in subsequent ones.");
        }
        if (pBox!=series.front() && pBox->draw_pass!=EMscDrawPass::DEFAULT) {
            chart->Error.Error(pBox->file_pos.start,
                "Attribute 'draw_time' can only be specified in the first "
                "element in a box series. Ignoring it in subsequent ones.");
        }
        pBox->draw_pass = draw_pass; //ensure all have the same value (the one from the first)
        if (pBox->content.size()) {
            if (pBox->collapsed == EBoxCollapseType::BLOCKARROW && series.size()>1) {
                chart->Error.Error(pBox->file_pos.start, "Only single boxes (and not box series) can be collapsed to a block arrow.",
                    "Collapsing to a box instead.");
                pBox->collapsed = EBoxCollapseType::COLLAPSE;
            }
        } else if (pBox->collapsed != EBoxCollapseType::EXPAND) {
            chart->Error.Warning(file_pos.start, "Cannot collapse an empty box.",
                "Ignoring 'collapsed' attribute.");
            pBox->collapsed = EBoxCollapseType::EXPAND;
        }
        //set the target to the Box (if a first note comes in the content)
        *target = pBox.get();
        //Add numbering, do content, add nullptr for indicators to "content", adjust src/dst,
        //and collect left and right if needed
        ret = pBox->PostParseProcess(canvas, hide, src, dst, number, target, byref_target); //ret is an arcblockarrow if we need to collapse
        //Check if we are collapsed to a block arrow
        if (pBox->collapsed == EBoxCollapseType::BLOCKARROW) {
            _ASSERT(series.size()==1);
            if (ret == nullptr) *target = DELETE_NOTE;
            else if (ret->CanBeNoted()) *target = ret;
            else *target = DELETE_NOTE; //Box can be noted, so if replacement cannot, we shall silently delete note
            if (BlockArrow *ba = dynamic_cast<BlockArrow*>(ret))
                ba->SetTemporarilyStoredNotes(std::move(tmp_stored_notes));
            return ret;
        }
        chart->Progress.DoneItem(EArcSection::POST_PARSE, EArcCategory::BOX);
        _ASSERT(pBox.get() == ret);
    }
    //parallel flag can be either on the series or on the first element
    parallel |= series.front()->parallel;
    //Set the target to the last Box (for comments coming afterwards).
    //We only process comments targeting a Box and not a BoxSeries
    *target = series.back().get();
    const EntityRef src_save = src, dst_save = dst;
    //src and dst can be NoEntity here if none of the series specified a left or a right entity
    //Go through and use content to adjust to content
    if (src==nullptr)
        for (auto &pBox : series)
            src = chart->EntityMinByPos(src, pBox->src);
    if (dst==nullptr)
        for (auto &pBox : series)
            dst = chart->EntityMaxByPos(dst, pBox->dst);

    //Src and dst can still be == nullptr, if no arcs specified
    //inside the content and no entity specified at box declaration.
    //In this case emph box spans to leftmost and rightmost entity in chart.
    //At PostParse chart->AllEntities is already sorted by pos values
    //we only do this step if we are the first in a box series.
again:
    if (src==nullptr) {
        //find leftmost non-virtual entity
        auto i = std::find_if(chart->AllEntities.begin(), chart->AllEntities.end(),
            [this](auto &e) {return !chart->IsVirtualEntity(e); });
        //If we end up in end() it means we only have virtual entities.
        //In this case an automatic box has no meaning, we drop it
        if (i==chart->AllEntities.end()) {
            chart->Error.Error(file_pos.start, "The chart has no entities I can adjust the size of this box to. Ignoring the box.");
            src = dst; //do not let this to point to no entity. dst must == nullptr here.
            return nullptr;
        } else
            src = &*i;
    }
    if (dst==nullptr) {
        //find rightmost non-virtual entity
        auto i = std::find_if(chart->AllEntities.rbegin(), chart->AllEntities.rend(),
            [this](auto &e) {return !chart->IsVirtualEntity(e); });
        dst = &*i;
        _ASSERT(dst); //No need to check dst validity, we have ensured we have valid entities above.
    }
    //Now see how entities change due to entity collapse
    EntityRef sub1 = chart->FindWhoIsShowingInsteadOf(src, true);
    EntityRef sub2 = chart->FindWhoIsShowingInsteadOf(dst, false);

    //if box spans a single entity, which is an actual parent of both src and dst, we kill this box
    const bool we_disappear = sub1==sub2 && chart->IsMyParentEntity(src, sub1) && chart->IsMyParentEntity(dst, sub2);

    src = sub1;
    dst = sub2;
    _ASSERT(src);
    _ASSERT(dst);

    if (src != dst && chart->EntityMinByPos(src, dst)==dst) {
        //OK, src and dst are in opposite order
        if (src_save && dst_save) {
            chart->Error.Error(file_pos.start, StrCat("Entities '", src_save->name, "' and '", dst_save->name,
                                                      "' are in the opposite order. Swapping them."));
            std::swap(src, dst);
        } else if (src_save) {
            chart->Error.Error(file_pos.start, StrCat("Entity '", src_save->name, 
                                                      "' is right of all the content in this box. Using left side of the chart instead."));
            src = nullptr;
            goto again;
        } else if (dst_save) {
            chart->Error.Error(file_pos.start, StrCat("Entity '", dst_save->name,
                                                      "' is left of all the content in this box. Using right side of the chart instead."));
            dst = nullptr;
            goto again;
        } else {
            chart->Error.Error(file_pos.start, StrCat("Internal error, Content entities '", src->name, "' and '", dst->name,
                                                      "' are in the opposite order. Ignoring box/box series."));
            return nullptr;
        }
    }

    //Now copy src, dst and indicator to all boxes
    //And to indicators if any
    for (auto &s : series) {
        if (s->content.size()) {
            Indicator*ai = dynamic_cast<Indicator*>(s->content.rbegin()->get());
            if (ai && !ai->IsComplete()) //if ai is complete it may come from an entity disappearing due to entity collapse
                ai->SetEntities(src, dst);
        }
        s->src = src;
        s->dst = dst;
    }
    left = chart->EntityMinByPos(chart->EntityMinByPos(left, sub1), sub2);
    right = chart->EntityMaxByPos(chart->EntityMaxByPos(right, sub1), sub2);
    //if we disappear merge our content (what remains of it)
    if (hide || we_disappear) {
        bool have_indicator = false;
        ArcList *list = new ArcList;
        for (auto &s : series) {
            list->splice(list->end(), s->content);
            have_indicator |= we_disappear && list->size() && dynamic_cast<Indicator*>(list->back().get());
        }
        //if we disappear, need to add an indicator, and the list does not yet contain one,
        //then add an indicator
        if (!have_indicator && we_disappear && sub1->running_style.read().indicator.value())
            list->Append(std::make_unique<Indicator>(chart, src, indicator_style, file_pos));
        ParallelBlocks *pb = new ParallelBlocks(chart, list, nullptr, false, false); //'list' is taken over by ParallelBlocks...
        pb->layout = ParallelBlocks::ONE_BY_ONE;
        return pb;
    }
    return this;
}

void BoxSeries::FinalizeLabels(Canvas &canvas)
{
    for (auto i = series.begin(); i!=series.end(); i++) {
        (*i)->FinalizeLabels(canvas);
        chart->Progress.DoneItem(EArcSection::FINALIZE_LABELS, EArcCategory::BOX);
    }
    ArcBase::FinalizeLabels(canvas);
}

//will only be called for the first box of a multi-segment box series
void BoxSeries::Width(Canvas &canvas, DistanceRequirements &vdist)
{
    if (!valid) return;
    const MscStyleCoW &overall_style = series.front()->style;
    const EntityRef src = series.front()->src;
    const EntityRef dst = series.front()->dst;

    //Add a new element to vdist
    auto top_vdist_iterator = vdist.AddElementTop(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
    vdist.InsertEntity(src);
    vdist.InsertEntity(dst);

    const auto last_marker = vdist.GetIteratorLast();
    double max_width = 0; //the widest label plus margins
    for (auto &pBox : series) {
        //Add a new element to vdist
        vdist.AddElementTop(pBox.get());
        //Add activation status right away
        AddEntityLineWidths(vdist);
        vdist.InsertEntity(src);
        vdist.InsertEntity(dst);

        if (pBox->content.size())
            chart->WidthArcList(canvas, (pBox->content), vdist);
        //Add a new element to vdist
        vdist.AddElementBottom(pBox.get());
        //Add activation status right away
        AddEntityLineWidths(vdist);

        //calculate tag size and its label
        double tag_width = 0;
        if (pBox->tag_label.length()) {
            //We ignore word wrapping for tags
            const XY twh = pBox->parsed_tag_label.getTextWidthHeight();
            Contour tcov = pBox->parsed_tag_label.Cover(chart->Shapes, 0, twh.x, overall_style.read().line.LineWidth()+chart->boxVGapInside);
            pBox->sx_tag = overall_style.read().line.CalculateTextMargin(tcov, 0).left;
            //now pBox->sx_tag contains the left margin from the outside of the overall frame line
            tcov.Shift(XY(0, -overall_style.read().line.LineWidth()-chart->boxVGapInside));
            //now the top of tcov is at zero
            tcov.RotateAround(twh/2, 180);
            tcov.Shift(XY(0, pBox->style.read().tag_line.LineWidth() + chart->boxVGapInside));
            pBox->dx_tag = pBox->style.read().tag_line.CalculateTextMargin(tcov, 0).left;
            //now pBox->dx_tag contains right tag margins, including the linewidth of the tag box on its left side
            tag_width = pBox->sx_tag + twh.x + pBox->dx_tag;
        } else
            tag_width = pBox->sx_tag = pBox->dx_tag = 0;
        const double space_needed = chart->mscgen_compat == EMscgenCompat::FORCE_MSCGEN ? 0.80 : 0.95;
        double width = pBox->parsed_label.getSpaceRequired(chart->XCoord(space_needed));
        //calculated margins (only for first segment) and save them
        if (pBox==series.front()) {
            const Contour tcov = pBox->parsed_label.Cover(chart->Shapes, 0, width, overall_style.read().line.LineWidth()+chart->boxVGapInside);
            DoublePair margins = overall_style.read().line.CalculateTextMargin(tcov, 0);
            //if we have a tag, use its size as left margin (includes linewidths)
            if (tag_width)
                margins.left = tag_width;
            width += margins.left + margins.right;
            pBox->sx_text = margins.left;
            pBox->dx_text = margins.right;
        } else {
            pBox->dx_text = overall_style.read().line.LineWidth();
            pBox->sx_text = tag_width ? tag_width : pBox->dx_text;  //use tag size as margin, if any. Else use linewidth only
        }
        max_width = std::max(max_width, width);
        chart->Progress.DoneItem(EArcSection::WIDTH, EArcCategory::BOX);
    }

    //Now d contains distance requirements within this emph box series
    //And "max_width" contains the widest
    double left_space_inside = vdist.Query(src->index, DISTANCE_LEFT, top_vdist_iterator);
    double right_space_inside = vdist.Query(dst->index, DISTANCE_RIGHT, top_vdist_iterator);

    if (src == dst) {
        //ensure that text fits
        left_space_inside = std::max(max_width/2, left_space_inside);
        right_space_inside = std::max(max_width/2, right_space_inside);
    } else {
        //do a default margin and ensure that internals fit
        const double def_margin = chart->XCoord(0.25);
        left_space_inside = std::max(def_margin, left_space_inside);
        right_space_inside = std::max(def_margin, right_space_inside);
    }

    ////Check box_side requirements
    //const std::pair<double, double> l_tmp = d.QueryBoxSide(src->index-1, false);
    //const std::pair<double, double> r_tmp = d.QueryBoxSide(dst->index, true);
    //left_space_inside = std::max(left_space_inside, l_tmp.second);
    //right_space_inside = std::max(right_space_inside, r_tmp.first);

    //add gap and linewidth
    max_width += 2*chart->boxVGapInside;
    left_space = left_space_inside +  chart->boxVGapInside + overall_style.read().line.LineWidth();
    right_space = right_space_inside + chart->boxVGapInside + overall_style.read().line.LineWidth();

    //if we span multiple entities ensure that text fits
    if (src!=dst && max_width > left_space + right_space)
        vdist.Insert(src->index, dst->index, max_width - left_space - right_space);

    //Add side distances
    vdist.Insert(src->index, DISTANCE_LEFT, left_space, last_marker);
    vdist.Insert(dst->index, DISTANCE_RIGHT, right_space, last_marker);

    //Add a new element to vdist
    vdist.AddElementBottom(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
}

void BoxSeries::Layout(Canvas &canvas, AreaList *cover)
{
    height = 0;
    if (!valid) return;
    //A few explanations of the variables exact meaning
    //the upper edge of the upper line of each segment is at yPos
    //total_height includes linewidths and shadow, but not boxVGapOutside (contrary for pipes)
    //left_space and right_space includes linewidth
    //height includes the upper linewidth, emphvgapinside, content, lower emphvgapinside, but not lower lw
    //sx and dx are the inner edges of the lines of the whole box
    MscStyleCoW &main_style = series.front()->style;
    const double lw = main_style.read().line.LineWidth();
    const double sx = chart->XCoord(series.front()->src) - left_space + lw;
    const double dx = chart->XCoord(series.front()->dst) + right_space - lw;

    double y = chart->boxVGapOutside;
    yPos = y;
    double comment_end = y;
    AreaList combined_content_cover;
    Box *pPreviousBox = nullptr;
    for (auto &pBox : series) {
        pBox->yPos = y; //"y" now points to the *top* of the line of the top edge of this box
        //Place side comments. This will update "cover" and thus force the content
        //downward if the content also has side notes
        double l = y, r = y;
        pBox->LayoutCommentsHelper(canvas, cover, l, r);
        comment_end = pBox->comment_height;

        const XY tag_wh = pBox->parsed_tag_label.getTextWidthHeight();
        const double tag_lw = pBox->style.read().tag_line.LineWidth();

        //Advance upper line and spacing
        y += pBox->style.read().line.LineWidth() + chart->boxVGapInside;
        //Calculate tag parameters and coverage
        if (pBox->tag_label.length()) {
            pBox->y_tag = y;
            pBox->sx_tag = sx + pBox->sx_tag - lw + chart->boxVGapInside;  //both sx and sx_text includes a lw
            //left upper corner of outer edge is way left and above of upper-left
            //corner of the box itself.
            pBox->tag_outer_edge.x.from = sx - lw - pBox->sx_tag - chart->boxVGapInside -
                pBox->style.read().tag_line.radius.value()*2 - pBox->style.read().tag_line.LineWidth()*2;
            pBox->tag_outer_edge.y.from = y - pBox->style.read().line.LineWidth() - chart->boxVGapInside -
                pBox->style.read().tag_line.radius.value()*2 - pBox->style.read().tag_line.LineWidth()*2;
            pBox->tag_outer_edge.x.till = pBox->sx_tag + tag_wh.x + pBox->dx_tag;
            pBox->tag_outer_edge.y.till = y + tag_wh.y + chart->boxVGapInside + tag_lw;
            pBox->dx_tag = pBox->sx_tag + tag_wh.x;
            //tag_cover will be calculated below
        }
        pBox->y_text = y;
        pBox->sx_text = sx + pBox->sx_text - lw + chart->boxVGapInside;  //both sx and sx_text includes a lw
        pBox->dx_text = dx - pBox->dx_text + lw - chart->boxVGapInside;
        //reflow label if necessary
        if (pBox->parsed_label.IsWordWrap()) {
            const double overflow = pBox->parsed_label.Reflow(canvas, chart->Shapes, pBox->dx_text - pBox->sx_text);
            pBox->OverflowWarning(overflow, "", series.front()->src, series.front()->dst);
        } else {
            pBox->CountOverflow(pBox->dx_text - pBox->sx_text);
        }
        //Calculate text cover
        pBox->text_cover = pBox->parsed_label.Cover(chart->Shapes, pBox->sx_text, pBox->dx_text, pBox->y_text);
        //Advance label height
        const double th = pBox->parsed_label.getTextWidthHeight().y;
        //Position arrows if any under the label
        AreaList content_cover = Area(pBox->text_cover, pBox.get());
        if (pBox->content.size()) {
            Area limit(pBox->text_cover, pBox.get());
            if (pBox->tag_label.length())
                limit += pBox->tag_outer_edge;  //we have not yet calculated tag_cover here. This will do.
            if (pBox==series.front() && main_style.read().line.corner != ECornerType::NONE && main_style.read().line.radius.value()>0) {
                //Funnily shaped box, prevent content from hitting it
                LineAttr limiter_line(main_style.read().line);
                *limiter_line.radius += chart->compressGap;
                const Block b(sx-lw/2, dx+lw/2, y+lw/2, y + std::max(lw+limiter_line.radius.value()*4, dx-sx)); //at midpoint of line
                limit += Contour(sx-lw/2, dx+lw/2, 0, y+lw+limiter_line.radius.value()) -
                    limiter_line.CreateRectangle_InnerEdge(b);
            }
            chart->PlaceListUnder(canvas, pBox->content, y+th, y, limit, IsCompressed(), &content_cover);  //no extra margin below text
            //to set the bottom of the box, use the visible bottom of the context, not the formal height,
            //since that includes the comments
            y = std::max(y+th, content_cover.GetBoundingBox().y.till);
        } else {
            //no content, just add text height
            y += th;
        }
        //Make sure the tag fits
        if (pBox->tag_label.length() && y < pBox->tag_outer_edge.y.till)
            y = pBox->tag_outer_edge.y.till + chart->compressGap;

        if (pBox==series.back() && !content_cover.IsEmpty() &&
            main_style.read().line.corner.value() != ECornerType::NONE &&
            main_style.read().line.radius.value()>0) {
            //Funnily shaped box, prevent it from hitting the bottom of the content
            LineAttr limiter_line(main_style.read().line);
            *limiter_line.radius += chart->compressGap;
            const Block b(sx-lw/2, dx+lw/2, -limiter_line.radius.value()*2, y);
            const Contour bottom = Contour(sx-lw/2, dx+lw/2, y-limiter_line.radius.value(), y+1) -
                limiter_line.CreateRectangle_InnerEdge(b);
            double tp;
            double off = content_cover.OffsetBelow(bottom, tp);
            if (off != CONTOUR_INFINITY) {
                if (off>0 && IsCompressed()) y -= off;
                if (off<0) y -= off;
            }
        }
        y += chart->boxVGapInside;
        //increase the size of the box by the side notes, except for the last box
        if (pBox.get() != series.back().get()) y = std::max(y, comment_end);
        //Make segment as tall as needed to accommodate curvature
        //if (style.read().line.radius.value>0) {
        //    double we_need_this_much_for_radius = pBox->style.read().line.LineWidth();
        //    if (i==follow.begin())
        //        we_need_this_much_for_radius += style.read().line.radius.value;
        //    if (i==--follow.end())
        //        we_need_this_much_for_radius += style.read().line.radius.value;
        //    y = std::max(y, pBox->yPos + we_need_this_much_for_radius);
        //}
        y = ceil(y);
        pBox->height = y - pBox->yPos;  //for boxes "height is meant without the lower line

        pBox->height_w_lower_line = pBox->height;
        //Add the linewidth of the next box or the final one
        if (pBox==series.back())
            pBox->height_w_lower_line += lw;
        //add our linewidth to the height of the previous box (if any)
        if (pPreviousBox)
            pPreviousBox->height_w_lower_line += pBox->style.read().line.LineWidth();
        pPreviousBox = pBox.get();
        if (cover)
            combined_content_cover += std::move(content_cover);
        chart->Progress.DoneItem(EArcSection::LAYOUT, EArcCategory::BOX);
    } /* for cycle through segments */
      //Final advance of linewidth, the inner edge (y) is on integer
    total_height = y + lw - yPos;

    Block b(sx-lw/2, dx+lw/2, yPos+lw/2, yPos + total_height - lw/2);  //The midpoint of us
                                                                       //update the style so that it's radius is not bigger than it can be
    const double sr = main_style.read().line.SaneRadius(b);
    if (main_style.read().line.radius.value() > sr)
        main_style.write().line.radius.value() = sr;

    Area overall_box(main_style.read().line.CreateRectangle_OuterEdge(b), this);
    const Contour overall_box_inside(main_style.read().line.CreateRectangle_InnerEdge(b));
    // now we have all geometries correct, now calculate areas and covers
    for (auto &pBox : series) {
        pBox->area = Contour(sx-lw, dx+lw, pBox->yPos, pBox->yPos + pBox->height_w_lower_line) * overall_box;
        pBox->area.arc = pBox.get();
        if (pBox->tag_label.length()) {
            const Block tag_midline(pBox->tag_outer_edge.CreateExpand(-pBox->style.read().tag_line.LineWidth()/2));
            const Contour box_inside = Contour(sx, dx, pBox->yPos+pBox->style.read().line.LineWidth(), pBox->yPos + pBox->height_w_lower_line) * overall_box_inside;
            pBox->tag_cover = pBox->style.read().tag_line.CreateRectangle_OuterEdge(tag_midline) * box_inside;
        }
        if (pBox->content.size() && pBox->collapsed==EBoxCollapseType::EXPAND) {
            //Make a frame, add the tag area and the label area
            pBox->area_draw = pBox->area.CreateExpand(chart->trackFrameWidth) - pBox->area;
            if (pBox->tag_label.length())
                pBox->area_draw += pBox->tag_cover;
            pBox->area_draw += pBox->text_cover.CreateExpand(chart->trackExpandBy);
            pBox->draw_is_different = true;
            pBox->area_draw_is_frame = true;
        } else {
            pBox->area_draw.clear();
            pBox->draw_is_different = false;
            pBox->area_draw_is_frame = false;
        }
        pBox->area_important = pBox->text_cover;
        if (pBox->tag_label.length())
            pBox->area_important += pBox->tag_cover;
        chart->NoteBlockers.Append(pBox.get());
    }
    const double &offset = main_style.read().shadow.offset.value();
    if (offset)
        overall_box += overall_box.CreateShifted(XY(offset, offset));
    overall_box.mainline = Block(chart->GetDrawing().x, b.y);
    if (cover) {
        *cover += GetCover4Compress(overall_box);
        //See if some of the content lies outside this box series - then add it to cover
        std::list<Area> alist = combined_content_cover.EmptyToList(); //convert to list so that we can move elements out
        for (auto area : alist)
            if (!overall_box.GetBoundingBox().IsWithinBool(area.GetBoundingBox().UpperLeft()) ||
                !overall_box.GetBoundingBox().IsWithinBool(area.GetBoundingBox().LowerRight()))
                *cover += std::move(area);
    }
    height = yPos + total_height + offset + chart->boxVGapOutside;
    //We do not call CommentHeight for "this" since a box series cannot take notes, only its
    //box elements do and those were handled above
    comment_height = comment_end;
}

/** Re-layout the box to a particular height (larger than now).
*  Can only be called after Layout() with a tot_height larger
*  than total_height.
*  Currently works only for single-box series with no content.*/
void BoxSeries::ReLayout(double tot_height, Canvas &, AreaList *cover)
{
    _ASSERT(total_height<=tot_height);
    if (total_height+series.size()>tot_height)
        return; //nothing to do if we do not increase the size by 1 pixel for each
    if (series.size()!=1) {
        chart->Error.Error(file_pos.start, "Cannot make a multi-element box series to adjust its size.");
        return;
    }
    if (series.front()->content.size()) {
        chart->Error.Error(file_pos.start, "Cannot make box with content to adjust its size.");
        return;
    }
    //change height, height_w_lower_line, y_text and text_cover of the box
    //and total_height of the series
    Box * const pBox = series.front().get();
    double diff = tot_height - total_height;
    pBox->height += diff;
    pBox->height_w_lower_line += diff;
    total_height += diff;
    pBox->y_text += diff;
    pBox->text_cover.Shift(XY(0, diff));
    //recalc area and cover
    MscStyleCoW &main_style = series.front()->style;
    const double lw = main_style.read().line.LineWidth();
    const double sx = chart->XCoord(series.front()->src) - left_space + lw;
    const double dx = chart->XCoord(series.front()->dst) + right_space - lw;
    Block b(sx-lw/2, dx+lw/2, yPos+lw/2, yPos + total_height - lw/2);  //The midpoint of us

    Area overall_box(main_style.read().line.CreateRectangle_OuterEdge(b), this);
    pBox->area = Contour(sx-lw, dx+lw, pBox->yPos, pBox->yPos + pBox->height_w_lower_line) * overall_box;
    pBox->area.arc = pBox;
    //area_draw, draw_is_different and area_draw_is_frame remains clear & false
    //tag_area is also kept
    pBox->area_important = pBox->text_cover;
    if (pBox->tag_label.length())
        pBox->area_important += pBox->tag_cover;
    const double &offset = main_style.read().shadow.offset.value();
    if (offset)
        overall_box += overall_box.CreateShifted(XY(offset, offset));
    overall_box.mainline = Block(chart->GetDrawing().x, b.y);
    if (cover)
        *cover += GetCover4Compress(overall_box);
}

void Box::ShiftBy(double y)
{
    if (!valid) return;
    if (y==0) return;
    y_text += y;
    y_tag += y;
    text_cover.Shift(XY(0, y));
    tag_outer_edge.Shift(XY(0, y));
    tag_cover.Shift(XY(0, y));
    LabelledArc::ShiftBy(y);
    if (content.size())
        chart->ShiftByArcList(content, y);
}

Range BoxSeries::GetVisualYExtent(bool include_comments) const
{
    const double h = valid ? std::max(0., height - 2*chart->boxVGapOutside) : 0;
    Range ret(yPos+chart->boxVGapOutside, yPos+chart->boxVGapOutside+h);
    if (include_comments && valid)
        ret += yPos+comment_height;
    return ret;
}


void BoxSeries::ShiftBy(double y)
{
    if (!valid) return;
    if (y==0) return;
    for (auto &pBox : series)
        pBox->ShiftBy(y);
    ArcBase::ShiftBy(y);
}

void BoxSeries::CollectPageBreak()
{
    if (!valid) return;
    for (auto &pBox : series)
        chart->CollectPageBreakArcList(pBox->content);
}

double BoxSeries::SplitByPageBreak(Canvas &canvas, double netPrevPageSize,
    double pageBreak, bool &addCommandNewpage,
    bool addHeading, ArcList &res)
{
    if (series.size()==0) return -1; //we cannot split if no content
    if (GetVisualYExtent(true).IsWithin(pageBreak) != contour::WI_INSIDE) return -1; //pb does not cut us
    //if pageBreak goes through a label (or is above) we cannot split there
    auto kwn_from = series.end();
    for (auto i = series.begin(); i!=series.end(); i++) {
        const Range visualYExtent = (*i)->GetVisualYExtent(true);
        if (pageBreak >= visualYExtent.till) {
            if ((*i)->IsKeepWithNext()) {
                if (kwn_from == series.end())
                    kwn_from = i;
            } else
                kwn_from = series.end();
            continue;
        }
        //We need to break in 'i'
        //if we have content and the pageBreak goes through the content
        //(top: if we have a label or tag, use the bottom of the lowermost,
        //      if not then the top of the visual extent)
        //(bottom: use the bottom of the visual extent)
        const double top = std::max((*i)->text_cover.IsEmpty() ? visualYExtent.till : (*i)->text_cover.GetBoundingBox().y.till,
            (*i)->tag_label.length() ? (*i)->tag_cover.GetBoundingBox().y.till : visualYExtent.till);
        if ((*i)->content.size() && !(*i)->keep_together &&
            top <= pageBreak && visualYExtent.till >= pageBreak) {
            //break the list, but do not shift all of it to the next page
            const double ret = chart->PageBreakArcList(canvas, (*i)->content, netPrevPageSize,
                pageBreak, addCommandNewpage, addHeading,
                false, true);
            if (ret>=0) {
                //if we did split the list somewhere in its middle
                //enlarge us
                (*i)->height_w_lower_line += ret;
                (*i)->height += ret;
                //shift the remaining
                while (++i!=series.end())
                    (*i)->ShiftBy(ret);
                height += ret;
                total_height += ret;
                return ret;
            }
            //else we cannot break somewhere in `i`
        }
        //we cannot break inside 'i'
        //break just before 'kwn_from' or 'i'
        if (kwn_from == series.end()) kwn_from = i;
        if (kwn_from == series.begin()) return -1; //we cannot break
        //move the remainder of the series to a new object
        std::unique_ptr<BoxSeries> abs =
            std::make_unique<BoxSeries>(kwn_from->release(), nullptr);
        series.erase(kwn_from++); //delete the empty unique ptr
        abs->series.splice(abs->series.end(), series, kwn_from, series.end());
        //compute shift. Box::yPos points to the "top" of the upper line
        //"height" is meant without the lower line
        const double shift_top = series.front()->style.read().line.LineWidth();
        const double increase_top = shift_top - (*abs->series.begin())->style.read().line.LineWidth();
        const double shift_rest = shift_top + increase_top;
        //copy line style to the first one
        (*abs->series.begin())->style.write().line = series.front()->style.read().line;
        (*--series.end())->height_w_lower_line += increase_top;
        (*abs->series.begin())->height += increase_top;
        (*abs->series.begin())->height_w_lower_line += increase_top;
        (*abs->series.begin())->ShiftBy(shift_rest);
        (*abs->series.begin())->yPos -= shift_top;
        abs->yPos = (*abs->series.begin())->yPos;
        abs->AddAttributeList(nullptr);
        abs->vspacing = vspacing;
        abs->parallel = parallel;
        abs->keep_together = keep_together;
        abs->keep_with_next = keep_with_next;
        abs->left_space = left_space;
        abs->right_space = right_space;
        abs->drawing_variant = drawing_variant;

        total_height = shift_top;
        for (auto ii = series.begin(); ii!=series.end(); ii++)
            total_height += (*ii)->height;
        abs->total_height = shift_top;
        for (auto ii = abs->series.begin(); ii!=abs->series.end(); ii++)
            abs->total_height += (*ii)->height;
        for (auto ii = ++abs->series.begin(); ii!=abs->series.end(); ii++)
            (*ii)->ShiftBy(shift_rest);
        res.Append(std::move(abs));
        return shift_rest;
    }
    //if we got here, the pb did not go through any member
    //we must be at the very end...
    return -1;
}




void BoxSeries::PlaceWithMarkers(Canvas &canvas)
{
    for (auto &pBox : series) {
        chart->Progress.DoneItem(EArcSection::PLACEWITHMARKERS, EArcCategory::BOX);
        if (pBox->valid && pBox->content.size())
            chart->PlaceWithMarkersArcList(canvas, pBox->content);
    }
}


void BoxSeries::PostPosProcess(Canvas &canvas, Chart *ch)
{
    if (!valid) return;
    //For boxes we always add the background cover first then the content
    //And we do this for each segment sequentially
    for (auto &pBox : series)
        if (pBox->valid) {
            if (pBox->content.size())
                switch (pBox->collapsed) {
                case EBoxCollapseType::EXPAND:
                    pBox->controls.push_back(EGUIControlType::COLLAPSE);
                    if (series.size()==1)
                        pBox->controls.push_back(EGUIControlType::ARROW);
                    break;
                case EBoxCollapseType::COLLAPSE:
                    pBox->controls.push_back(EGUIControlType::EXPAND);
                    if (series.size()==1)
                        pBox->controls.push_back(EGUIControlType::ARROW);
                    break;
                case EBoxCollapseType::BLOCKARROW:
                default:
                    _ASSERT(0); //should not happen here
                    break;
                }
            pBox->entityLineRange = pBox->area.GetBoundingBox().y;
            pBox->LabelledArc::PostPosProcess(canvas, ch);
            chart->Progress.DoneItem(EArcSection::POST_POS, EArcCategory::BOX);
            if (pBox->content.size())
                chart->PostPosProcessArcList(canvas, pBox->content, ch);
        }

    //Hide entity lines during the lines inside the box
    const MscStyleCoW &main_style = series.front()->style;
    const double lw = main_style.read().line.LineWidth();
    const double src_x = chart->XCoord(series.front()->src);
    const double dst_x = chart->XCoord(series.front()->dst);
    const double sx = src_x - left_space + lw;
    const double dx = dst_x + right_space - lw;
    for (auto &pBox : series)
        if (pBox.get() != series.front().get() && pBox->style.read().line.IsDoubleOrTriple()) {
            const Block r(sx, dx, pBox->yPos, pBox->yPos + pBox->style.read().line.LineWidth());
            chart->HideEntityLines(r);
        }

    //hide the entity lines under the labels & tags
    for (auto &pBox : series) {
        chart->HideEntityLines(pBox->text_cover);
        chart->HideEntityLines(pBox->tag_cover);
    }
    //hide top and bottom line if double
    if (main_style.read().line.IsDoubleOrTriple()) {
        Block b(src_x - left_space + lw/2, dst_x + right_space - lw/2,
            yPos + lw/2, yPos+total_height - lw/2); //The midpoint of the lines
        //The radius specified in style.read().line will be that of the midpoint of the line
        chart->HideEntityLines(main_style.read().line.CreateRectangle_OuterEdge(b) -
            main_style.read().line.CreateRectangle_InnerEdge(b));
    }
}

void BoxSeries::RegisterLabels()
{
    //We do not register tag labels on purpose for now.
    const double mid = (chart->XCoord(series.front()->src) + chart->XCoord(series.front()->dst))/2;
    for (auto &pBox : series) {
        chart->RegisterLabel(pBox->parsed_label, pBox->content.size() ? LabelInfo::BOX : LabelInfo::EMPTYBOX,
            pBox->sx_text, pBox->dx_text, pBox->y_text, mid);
        if (pBox->content.size())
            chart->RegisterLabelArcList(pBox->content);
    }
}

void BoxSeries::CollectIsMapElements(Canvas &canvas)
{
    //We also collect links from the tag label
    const double mid = (chart->XCoord(series.front()->src) + chart->XCoord(series.front()->dst))/2;
    for (auto &pBox : series) {
        if (pBox->tag_label.length())
            pBox->parsed_tag_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
                pBox->sx_tag, pBox->dx_tag, pBox->y_tag);
        pBox->parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
            pBox->sx_text, pBox->dx_text, pBox->y_text, mid);
        if (pBox->content.size())
            chart->CollectIsMapElementsArcList(pBox->content, canvas);
    }
}


void BoxSeries::RegisterCover(EMscDrawPass pass) const
{
    for (auto &pBox : series) {
        //transparent boxes with content go to the background
        EMscDrawPass effective_pass = pBox->draw_pass;
        if (!pBox->style.read().fill.IsFullyOpaque() && pBox->content.size())
            effective_pass = EMscDrawPass::BEFORE_ENTITY_LINES;
        if (pass == effective_pass)
            chart->AllCovers += pBox->area;
        chart->RegisterCoverArcList(pBox->content, pass);
    }
}

void BoxSeries::Draw(Canvas &canvas, EMscDrawPass pass) const
{
    if (!valid || series.size()==0) return;
    //For boxes draw background for each segment, then separator lines, then bounding rectangle lines, then content
    const MscStyleCoW &main_style = series.front()->style;
    const double lw = main_style.read().line.LineWidth();
    const double src_x = chart->XCoord(series.front()->src);
    const double dst_x = chart->XCoord(series.front()->dst);
    //The midpoint of the lines
    const Block r(src_x - left_space + lw/2, dst_x + right_space - lw/2,
        yPos + lw/2, yPos+total_height - lw/2);
    //The radius specified in main_style.line will be that of the midpoint of the line
    //fast path for PPT
    bool added_main_box = false, added_label = false;
    if (pass==series.front()->draw_pass && !canvas.does_graphics()
        && std::ranges::all_of(series, [this](const std::unique_ptr<Box>& b) {return b->style.read().fill==series.front()->style.read().fill;})) {
        if (series.size()==1 && series.front()->content.empty()) {
            //simple, one element box with no content. Render as a single labelled box
            canvas.Add(GSBox(r, main_style.read().line, main_style.read().fill, main_style.read().shadow, series.front()->parsed_label));
            added_label = true;
        } else
            //All segments have the same fill, render as one box
            canvas.Add(GSBox(r, main_style.read().line, main_style.read().fill, main_style.read().shadow));
        added_main_box = true;
    }
    //First draw the shadow.
    if (pass==series.front()->draw_pass) {
        if (canvas.does_graphics())
            canvas.Shadow(r, main_style.read().line, main_style.read().shadow);
        else if (!added_main_box)
            //just line and shadow - fills added below
            canvas.Add(GSBox(r, main_style.read().line, FillAttr(ColorType::none(), EGradientType::NONE), main_style.read().shadow));
    }
    //Do a clip region for the overall box (for round/bevel/note corners)
    //at half a linewidth from the inner edge (use the width of a single line!)
    const Contour clip = main_style.read().line.CreateRectangle_ForFill(r);
    for (auto i = series.begin(); i!=series.end(); i++) {
        //Overall rule for background fill:
        //for single line borders we fill up to the middle of the border
        //for double line borders we fill up to the middle of the inner line of the border
        //style.read().line.LineWidth() gives the full width of the (double) line, width.value is just one line of it
        //for single lines style.read().line.LineWidth()==style.read().line.width.value
        double sy = (*i)->yPos + (*i)->style.read().line.LineWidth() - (*i)->style.read().line.width.value()/2.;
        double dy = (*i)->yPos + (*i)->height;
        //decrease upper limit for the first one (note+triple line has areas higher than this to cover)
        //clip will cut away the not needed areas
        if (i==series.begin())
            sy -= lw;
        //Increase the fill area downward by half of the linewidth below us
        auto next = std::next(i);
        if (next==series.end())
            dy += main_style.read().line.width.value()/2.;
        else
            dy += (*next)->style.read().line.width.value()/2.;
        canvas.Clip(clip);
        //fill wider than r.x - note+triple line has wider areas to cover, clip will cut away excess
        if (pass==(*i)->draw_pass) {
            const Block b(r.x.from, r.x.till+lw, sy, dy);
            if (canvas.does_graphics())
                canvas.Fill(b, (*i)->style.read().fill);
            else if (!added_main_box)
                canvas.Add(GSBox(b.CreateExpand(-lw/2), LineAttr(ELineType::NONE, ColorType::none()), (*i)->style.read().fill));
        }
        //if there are contained entities, draw entity lines, strictly from inside of line
        if ((*i)->content.size()) {
            if (pass==(*i)->draw_pass && (*i)->drawEntityLines &&
                (*i)->collapsed==EBoxCollapseType::EXPAND)
                chart->DrawEntityLinesIncludeLast(canvas, (*i)->yPos, (*i)->height + (*i)->style.read().line.LineWidth(), (*i)->src, (*i)->dst);
            canvas.UnClip();
            chart->DrawArcList(canvas, (*i)->content, chart->GetTotal().y, pass);
        } else
            canvas.UnClip();
    }

    //Draw box lines - Cycle only for subsequent boxes
    canvas.Clip(clip);
    for (auto i = ++series.begin(); i!=series.end(); i++) {
        if (pass!=(*i)->draw_pass) continue;
        const double y = (*i)->yPos + (*i)->style.read().line.LineWidth()/2;
        if (canvas.does_graphics())
            canvas.Line(XY(r.x.from, y), XY(r.x.till, y), (*i)->style.read().line);
        else
            canvas.Add(GSPath({XY(r.x.from, y), XY(r.x.till, y)}, (*i)->style.read().line));
    }
    canvas.UnClip();
    //Finally draw the overall line around the box
    if (pass==series.front()->draw_pass)
        if (canvas.does_graphics())
            canvas.Line(r, main_style.read().line);
    //XXX double line joints: fix it
    for (auto &pBox : series)
        if (pass==pBox->draw_pass) {
            if (pBox->tag_label.length()) {
                const Block tag_midline(pBox->tag_outer_edge.CreateExpand(-pBox->style.read().tag_line.LineWidth()/2));
                const Contour fill = pBox->style.read().tag_line.CreateRectangle_ForFill(tag_midline);
                const Contour line = pBox->style.read().tag_line.CreateRectangle_Midline(tag_midline);
                if (canvas.does_graphics()) {
                    canvas.Clip(pBox->tag_cover);
                    canvas.Fill(fill, pBox->style.read().tag_fill);
                    canvas.Line(line, pBox->style.read().tag_line);
                    pBox->parsed_tag_label.Draw(canvas, chart->Shapes, pBox->sx_tag, pBox->dx_tag, pBox->y_tag);
                    canvas.UnClip();
                } else {
                    canvas.Add(GSShape(fill * pBox->tag_cover, LineAttr(ELineType::NONE, ColorType::none()), pBox->style.read().tag_fill));
                    Path path = line;
                    path.ClipRemove(pBox->tag_cover);
                    canvas.Add(GSPath(path, pBox->style.read().tag_line));
                    pBox->parsed_tag_label.Draw(canvas, chart->Shapes, pBox->sx_tag, pBox->dx_tag, pBox->y_tag);
                }
            }
            if (!added_label)
                pBox->parsed_label.Draw(canvas, chart->Shapes, pBox->sx_text, pBox->dx_text, pBox->y_text, r.x.MidPoint());
        }
}

/////////////////////////////////////////////////////////////////

Pipe::Pipe(gsl::owner<Box*> box) :
    LabelledArc(EArcCategory::PIPE, box->chart),
    src(box->src), dst(box->dst), drawEntityLines(false), type(box->type)
{
    delete box;
}

/** Return the refinement style for a pipe. */
const MscStyleCoW *Pipe::GetRefinementStyle4PipeSymbol(EArcSymbol t) const
{
    //refinement for boxes
    switch (t) {
    case EArcSymbol::BOX_SOLID:
        return (MscStyleCoW*)&chart->MyCurrentContext().styles["pipe--"];
    case EArcSymbol::BOX_DASHED:
        return (MscStyleCoW*)&chart->MyCurrentContext().styles["pipe++"];
    case EArcSymbol::BOX_DOTTED:
        return (MscStyleCoW*)&chart->MyCurrentContext().styles["pipe.."];
    case EArcSymbol::BOX_DOUBLE:
        return (MscStyleCoW*)&chart->MyCurrentContext().styles["pipe=="];
    default:
        _ASSERT(0);
        return nullptr;
    }
}

void Pipe::SetStyleBeforeAttributes(AttributeList *)
{
    SetStyleWithText("pipe", GetRefinementStyle4PipeSymbol(type));
}


PipeSeries::PipeSeries(gsl::owner<Pipe*> first) :
    ArcBase(EArcCategory::PIPE_SERIES, first->chart),
    drawing_variant(1)
{
    series.Append(first);
    keep_together = false; //we can be cut in half
}

PipeSeries* PipeSeries::AddArcList(gsl::owner<ArcList*> l)
{
    if (!valid) return this;
    if (l!=nullptr && l->size()>0) {
        content.splice(content.end(), *l);
        delete l;
    }
    return this;
}

bool Pipe::AddAttribute(const Attribute &a)
{
    return LabelledArc::AddAttribute(a);
}

void Pipe::AttributeNames(Csh &csh)
{
    LabelledArc::AttributeNames(csh);
    csh.AttributeNamesForStyle("pipe");
}

bool Pipe::AttributeValues(std::string_view attr, Csh &csh)
{
    if (csh.AttributeValuesForStyle(attr, "pipe")) return true;
    if (LabelledArc::AttributeValues(attr, csh)) return true;
    return false;
}

//Should be called before AddAttributeList for "f" is called
PipeSeries* PipeSeries::AddFollowWithAttributes(gsl::owner<Pipe*> f, gsl::owner<AttributeList*> l)
{
    _ASSERT(f);
    if (f==nullptr) return this;
    if (f->valid) {
        //we set style and clear it immediately - the reason is to get
        //f_values and color_meaning right in the style
        f->SetStyleBeforeAttributes(l);
        f->style.write().Empty();
        const FileLineCol line = f->AddAttributeListStep1(l);
        if (f->style.read().side) {
            f->style.write().side.reset();
            chart->Error.Error(f->file_pos.start,
                "Attribute 'side' can only be specified in the first "
                "element in a pipe series. Ignoring it in subsequent ones.");
        }
        if (f->style.read().line.radius) {
            f->style.write().line.radius.reset();
            chart->Error.Error(f->file_pos.start,
                "Attribute 'line.radius' can only be specified in the first "
                "element in a pipe series. Ignoring it in subsequent ones.");
        }
        if (f->parallel) {
            chart->Error.Error(f->file_pos.start,
                "Attribute 'parallel' can only be specified in the first "
                "element in a pipe series. Ignoring it in subsequent ones.");
        }
        //Use the style of the first box in the series as a base
        MscStyleCoW s = series.front()->style;
        //Override with the line type specified (if any)
        _ASSERT(f->type != EArcSymbol::BOX_UNDETERMINED_FOLLOW);
        const MscStyleCoW * const ref = f->GetRefinementStyle4PipeSymbol(f->type);
        if (ref)
            s += *ref;
        s += f->style; //add the result of attributes
        f->style = s;
        f->AddAttributeListStep2(line);
        //AddAttributeList will NOT be called for "f" after this function
    } else
        valid = false;
    series.Append(f);
    return this;
}

EDirType PipeSeries::GetToucedEntities(NEntityList &el) const
{
    if (content.size())
        return chart->GetTouchedEntitiesArcList(content, el);
    //If no content, add leftmost and rightmost
    _ASSERT(!chart->IsVirtualEntity(series.front()->src));
    _ASSERT(!chart->IsVirtualEntity(series.front()->dst));
    _ASSERT(!chart->IsVirtualEntity(series.back()->src));
    _ASSERT(!chart->IsVirtualEntity(series.back()->dst));
    el.push_back(chart->EntityMinByPos(series.front()->src, series.back()->src));
    el.push_back(chart->EntityMaxByPos(series.front()->dst, series.back()->dst));
    return EDirType::INDETERMINATE;
}

namespace msc {
/** A functor struct to compare two pipes, which is leftmost or rightmost.*/
struct pipe_compare
{
    const MscChart *chart;
    bool fromright;
    pipe_compare(const MscChart *c, bool fr) : chart(c), fromright(fr) {}
    bool operator ()(const std::unique_ptr<Pipe> &p1, const std::unique_ptr<Pipe> &p2) const
    {
        EntityRef min1 = chart->EntityMinMaxByPos(p1->src, p1->dst, fromright);
        EntityRef min2 = chart->EntityMinMaxByPos(p2->src, p2->dst, fromright);
        if (min1==min2) return false; //equals are not less
        return min1 == chart->EntityMinMaxByPos(min1, min2, fromright);
    }
};
}


ArcBase* PipeSeries::PostParseProcess(Canvas &canvas, bool hide, EntityRef &left, EntityRef &right,
                                      Numbering& number, MscElement** target, ArcBase* byref_target)
{
    if (!valid) return nullptr;

    //Add numbering, if needed
    for (auto i = series.begin(); i!=series.end(); i++) {
        (*i)->PostParseProcess(canvas, hide, left, right, number, target, byref_target);
        chart->Progress.DoneItem(EArcSection::POST_PARSE, EArcCategory::PIPE);
    }
    //Postparse the content;
    EntityRef content_left=nullptr, content_right=nullptr;
    //set the first element as the note target (first in the {})
    *target = series.front().get();
    chart->PostParseProcessArcList(canvas, hide, content, false, content_left, content_right, number, target);

    //parallel flag can be either on the series or on the first element
    parallel |= series.front()->parallel;
    //copy keep_with_next to Series
    for (auto i = series.begin(); i!=series.end(); i++)
        keep_with_next |= (*i)->keep_with_next;
    //set the last element as a note target (coming after us)
    *target = series.back().get();

    //Check that all pipe segments are fully specified, non-overlapping and sort them

    //first sort from left to right
    pipe_compare comp(chart, true);
    series.sort(comp);
    //Make both src and dst specified and ordered in all segments
    //The leftmost and the rightmost segment can auto-adjust to the content
    //others can snap left and right if not specified
    if (series.front()->src == nullptr)
        series.front()->src = content_left;
    //If the left side is still unspecified, we had no content, return an error
    if (series.front()->src == nullptr) {
        chart->Error.Error(series.front()->file_pos.start, "The left side of the leftmost pipe segment in an empty pipe must be specified.",
            "Ignoring this pipe.");
        return nullptr;
    }
    if (series.back()->dst == nullptr)
        series.back()->dst = content_right;
    //If the left side is still unspecified, we had no content, return an error
    if (series.back()->dst==nullptr) {
        chart->Error.Error(series.front()->file_pos.start, "The right side of the rightmost pipe segment in an empty pipe must be specified.",
            "Ignoring this pipe.");
        return nullptr;
    }
    if (series.size()>1) {
        for (auto i = series.begin(); i!=--series.end(); i++) {
            auto i_next = i; i_next++;

            if ((*i_next)->src == nullptr) (*i_next)->src = (*i)->dst;
            if ((*i)->dst      == nullptr) (*i)->dst = (*i_next)->src;

            if ((*i)->dst == nullptr || (*i_next)->src == nullptr) {
                chart->Error.Error((*i)->file_pos.start, "Could not figure out the extent of this pipe segment. Specify right entity.");
                return nullptr;
            }
        }
        draw_pass = series.front()->draw_pass;
        for (auto i = series.begin(); i!=series.end(); /*nope*/) {
            const EntityRef loc_src = chart->FindLeftRightDescendant((*i)->src, true, false);
            const EntityRef loc_dst = chart->FindLeftRightDescendant((*i)->dst, false, false);
            if (i != --series.end()) {
                auto i_next = i; i_next++;
                const EntityRef next_src = chart->FindLeftRightDescendant((*i_next)->src, true, false);
                if (loc_src == loc_dst && loc_dst == next_src) {
                    chart->Error.Error((*i)->file_pos.start, "This pipe segment is attaches to the next segment but spans only a single entity."
                        "Segment will not be shown.");
                    series.erase(i++);
                    continue;
                }
            }
            if (i != series.begin()) {
                auto i_prev = i; i_prev--;
                const EntityRef prev_dst = chart->FindLeftRightDescendant((*i_prev)->dst, false, false);
                if (loc_src == loc_dst && loc_src == prev_dst) {
                    chart->Error.Error((*i)->file_pos.start, "This pipe segment is attaches to the previous segment but spans only a single entity."
                        "Segment will not be shown.");
                    series.erase(i++);
                    continue;
                }
                if (chart->EntityMaxByPos(prev_dst, loc_src) != loc_src)
                    chart->Error.Warning((*i)->file_pos.start, "This pipe segment overlaps the previousl one. It may not look so good.",
                        "Encapsulate one in the other if you want that effect.");
            }
            if (i!=series.begin() && (*i)->draw_pass!=EMscDrawPass::DEFAULT) {
                chart->Error.Error((*i)->file_pos.start,
                    "Attribute 'draw_time' can only be specified in the first "
                    "element in a pipe series. Ignoring it in subsequent ones.");
            }
            (*i)->draw_pass = draw_pass; //ensure all segments are of same draw_time (that of the first)
            i++;
        }
    }
    if (series.size()==0) return nullptr;

    //All the above operations were checked on AllEntities. We have accepted the pipe
    //as valid here and should not complain no matter what entities are collapsed or not

    //Now change src:s and dst:s to active entities.
    //First look up their active parent in AllEntities (or if they are
    //visible group entities find, the left/right-most children)
    //Then convert to ActiveEntities iterators
    //If may happen that a pipe segment disappears.
    //If the first one disappears (==this), we need to do a replacement
    //If all disappear, we just return the content in an ParallelBlocks
    //Do one pass of checking
    EntityRef ei_if_disappear=nullptr;
    for (auto i = series.begin(); i!=series.end(); /*none*/) {
        EntityRef sub1 = chart->FindWhoIsShowingInsteadOf((*i)->src, true);
        EntityRef sub2 = chart->FindWhoIsShowingInsteadOf((*i)->dst, false);

        //if pipe segment spans a single entity and both ends have changed,
        //we kill this segment
        if (sub1==sub2 && sub1!=(*i)->src && sub2!=(*i)->dst) {
            series.erase(i++);
            ei_if_disappear = sub1;
        } else {
            (*i)->src = sub1;
            (*i)->dst = sub2;
            _ASSERT((*i)->src);
            _ASSERT((*i)->dst);
            i++;
        }
    }
    //now ei_if_disappear is guaranteed to be non-null if we have a zero series
    _ASSERT(ei_if_disappear || series.size());
    //Do second pass: a single entity segment is kept only if it does not
    //connect to the previous or next segments
    //Also collect thickest linewidth of remaining segments
    double lw_max = 0;
    for (auto i = series.begin(); i!=series.end(); /*nope*/) {
        if ((*i)->src == (*i)->dst) {
            auto j = i;
            auto k = i;
            if ((i!=series.begin() && (*--j)->dst == (*i)->src) ||
                (i!=--series.end() && (*++k)->src == (*i)->dst)) {
                series.erase(i++);
                continue;
            }
        }
        lw_max = std::max(lw_max, (*i)->style.read().line.LineWidth());
        i++;
    }

    //see if we have any segments left, if not return only content
    if (series.size()==0) {
        if (content.size()) {
            //pipes disappeared, but content remains. Return as an arclist
            ArcList *al = new ArcList;
            al->swap(content);
            //Note content is already PostParseProcessed.
            ParallelBlocks *par = new ParallelBlocks(chart, al, nullptr, false, false); //this will do a "delete al;"
            //Do not use ONE_BY_ONE merge as we would not have that either
            //even if we have pipes in 'series'. This also allows
            //MscChart::LayoutParallelArcLists() to avoid taking cover in some cases.
            par->layout = ParallelBlocks::ONE_BY_ONE;
            return par;
        }
        //We completely disappear due to entity collapses and have no (remaining) content
        if (ei_if_disappear->running_style.read().indicator.value()) {
            //leave an indicator, but update left and right
            left = chart->EntityMinByPos(ei_if_disappear, left);
            right = chart->EntityMaxByPos(ei_if_disappear, right);
            ei_if_disappear = chart->FindWhoIsShowingInsteadOf(ei_if_disappear, true); //result points to ActiveEntities
            return new Indicator(chart, ei_if_disappear, indicator_style, file_pos);
        } else
            return nullptr;
    }

    //increase the radius everywhere by the thickest lw (if it is not zero)
    if (series.front()->style.read().line.radius.value()>0) {
        const double radius = series.front()->style.read().line.radius.value() + lw_max;
        for (auto i = series.begin(); i!=series.end(); i++)
            (*i)->style.write().line.radius.value() = radius;
    }

    //Sort according to fromright: begin() should point to the leftmost pipe if side==right,
    //and to the rightmost if side=left
    if (series.front()->style.read().side.value() == ESide::LEFT) {
        comp.fromright = false;
        series.sort(comp);
    }
    //Now series.begin does not point to the leftmost, but to the one showing the hole
    //Also set pipe_connect_back/forw flags
    //Terminology: backwards means left if "fromright" and right otherwise

    //Fill in pipe_connect vlaues
    series.front()->pipe_connect_back = false;
    series.back()->pipe_connect_forw = false;
    for (auto i = ++series.begin(); i!=series.end(); i++) {
        (*i)->pipe_connect_back = (*i)->pipe_connect_forw = false;
        //Set flags if we are adjacent to previous one
        auto i_prev = i;
        i_prev--;
        if ((series.front()->style.read().side.value() == ESide::RIGHT && (*i_prev)->dst == (*i)->src) ||
            (series.front()->style.read().side.value() == ESide::LEFT  && (*i_prev)->src == (*i)->dst)) {
            (*i)->pipe_connect_back = true;
            (*i_prev)->pipe_connect_forw = true;
        } else {
            (*i)->pipe_connect_back = false;
            (*i_prev)->pipe_connect_forw = false;
        }
    }

    //set return value
    left = chart->EntityMinByPos(left, series.front()->src);
    left = chart->EntityMinByPos(left, content_left);
    right = chart->EntityMaxByPos(right, series.back()->dst);
    right = chart->EntityMaxByPos(right, content_right);

    if (hide) return nullptr;
    //if there is no content, no need to draw a transparent cover
    //save drawing cost (and potential fallback img)
    //only first pipe can have content (which becomes the content of all pipe)
    if (content.size() == 0)
        for (auto i = series.begin(); i!=series.end(); i++)
            (*i)->style.write().solid = 255;
    else if (canvas.AvoidTransparency())
        for (auto i = series.begin(); i!=series.end(); i++)
            //disallow transparency if too low power
            if ((*i)->style.read().solid!=255)
                (*i)->style.write().solid = 0;

    return this;
}

void PipeSeries::FinalizeLabels(Canvas &canvas)
{
    for (auto i = series.begin(); i!=series.end(); i++) {
        (*i)->FinalizeLabels(canvas);
        chart->Progress.DoneItem(EArcSection::FINALIZE_LABELS, EArcCategory::PIPE);
    }
    chart->FinalizeLabelsArcList(content, canvas);
    ArcBase::FinalizeLabels(canvas);
}

//will only be called for the first box of a multi-segment box series
void PipeSeries::Width(Canvas &canvas, DistanceRequirements &vdist)
{
    if (!valid) return;
    //Add a new element to vdist
    const auto top_vdist_iterator = vdist.AddElementTop(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
    for (auto &pPipe : series) {         ///XXX DO we need this???
        //Add a new element to vdist
        vdist.AddElementTop(pPipe.get());
        //Add activation status right away
        AddEntityLineWidths(vdist);
        vdist.InsertEntity(pPipe->src);
        vdist.InsertEntity(pPipe->dst);
    }
    if (content.size())
        chart->WidthArcList(canvas, content, vdist);

    const ESide side = series.front()->style.read().side.value();
    const double radius = series.front()->style.read().line.radius.value();

    //(*i)->src and dst contain the left and right end of a pipe
    //The order of the pipe segments in follow depends on style.read().side
    for (auto &pPipe : series) {
        const double ilw = pPipe->style.read().line.LineWidth();
        const double width = pPipe->parsed_label.getSpaceRequired() + 2*chart->boxVGapInside;
        pPipe->left_space = chart->boxVGapInside +
                      vdist.Query(pPipe->src->index, DISTANCE_LEFT, top_vdist_iterator);
        pPipe->right_space = chart->boxVGapInside +
                      vdist.Query(pPipe->dst->index, DISTANCE_RIGHT, top_vdist_iterator);

        //Add extra space for curvature
        if (side == ESide::RIGHT)
            pPipe->right_space += radius;
        else
            pPipe->left_space += radius;

        //The style.read().line.radius.value is understood to be the radius of the hole of the _outer edge_
        if (pPipe->src==pPipe->dst) {
            pPipe->left_space = std::max(width/2, pPipe->left_space);
            pPipe->right_space = std::max(width/2, pPipe->right_space);
        } else {
            //keep a big enough space between src and dst for the text + curvature of pipe
            //the text can go out of the entity lines, all the way to the left-space
            vdist.Insert(pPipe->src->index, pPipe->dst->index,
                width - pPipe->left_space - pPipe->right_space +
                radius);
        }
        pPipe->left_space = ceil(pPipe->left_space);
        pPipe->right_space = ceil(pPipe->right_space);
        const bool connect_left = side == ESide::RIGHT ? pPipe->pipe_connect_back : pPipe->pipe_connect_forw;
        const bool connect_right = side == ESide::RIGHT ? pPipe->pipe_connect_forw : pPipe->pipe_connect_back;
        //Check if we are connecting to a neighbour pipe segment
        if (connect_left)
            pPipe->left_space = 0;
        else {
            vdist.Insert(pPipe->src->index, DISTANCE_LEFT,
                pPipe->left_space + ilw + radius);
        }
        //add shadow to the right size only if we are the rightmost entity
        double shadow_to_add = 0;
        if ((side == ESide::RIGHT && pPipe==series.back()) ||
            (side == ESide::LEFT  && pPipe==series.front()))
            shadow_to_add = pPipe->style.read().shadow.offset.value();
        if (connect_right)
            pPipe->right_space = 0;
        else {
            vdist.Insert(pPipe->dst->index, DISTANCE_RIGHT,
                pPipe->right_space + ilw + radius + shadow_to_add);
        }
        chart->Progress.DoneItem(EArcSection::WIDTH, EArcCategory::PIPE);
    }

    for (auto &pPipe : series) {
        //Add a new element to vdist
        vdist.AddElementBottom(pPipe.get());
        //Add activation status right away
        AddEntityLineWidths(vdist);
    }
    //Add a new element to vdist
    vdist.AddElementBottom(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
}

/** Takes the pipe_body member of each pipe and calculates their contours used for drawing.
* Returns the total body cover in 'pipe_body_cover' if not nullptr*/
void PipeSeries::CalculateContours(Area *pipe_body_cover)
{
    //A few shortcuts. "side" and "radius" must be the same in any pipe element, so we take the first
    const ESide side = series.front()->style.read().side.value();
    const double radius = series.front()->style.read().line.radius.value();
    //the largest of the shadow offsets
    for (auto &pPipe : series) {
        //No need to clean up. If any of the pipe_* or area, area_draw, area_important
        //has values here, they will get simply overwritten
        const double ilw = pPipe->style.read().line.LineWidth();

        XY cs(pPipe->pipe_block.x.from, pPipe->pipe_block.y.MidPoint());
        XY cd(pPipe->pipe_block.x.till, pPipe->pipe_block.y.MidPoint());
        const XY rad(radius, pPipe->pipe_block.y.Spans()/2); //we use the first pipe's line.radius
        if (side == ESide::LEFT) std::swap(cs, cd); //use the first pipe's fromright, not pPipe->fromright
                                                    //now cd is the one with the hole
        const Contour back_end(cs, rad.x, rad.y);
        const Contour forw_end(cd, rad.x, rad.y);
        //pPipe->pipe_block, back_end and forw_end are now all outer edge
        //we need to shrink by ilw/2 to get the line contour and by ilw/2-line.width/2 to get the fill contour
        const double gap_for_line = -ilw/2;
        const double gap_for_fill = -ilw + pPipe->style.read().line.width.value()/2;
        pPipe->pipe_body_fill = Block(pPipe->pipe_block.x, Range(pPipe->pipe_block.y).Expand(gap_for_fill));
        pPipe->pipe_body_line = Block(pPipe->pipe_block.x, Range(pPipe->pipe_block.y).Expand(gap_for_line));
        pPipe->pipe_shadow = pPipe->pipe_block;
        //Do back end
        if (rad.x>0) {
            pPipe->pipe_body_fill += back_end.CreateExpand(gap_for_fill);
            pPipe->pipe_body_line += back_end.CreateExpand(gap_for_line);
            pPipe->pipe_shadow += back_end;
        } else {
            //square end: chop off from fill and line
            pPipe->pipe_body_fill -= Block(Range(cs.x, cs.x), pPipe->pipe_block.y).Expand(gap_for_fill);
            pPipe->pipe_body_line -= Block(Range(cs.x, cs.x), pPipe->pipe_block.y).Expand(gap_for_line);
        }
        //Do front end, and whole_line
        if (pPipe->pipe_connect_forw) {
            pPipe->area = pPipe->pipe_shadow;
            if (content.size() && pPipe->style.read().solid.value() < 255)
                pPipe->area_draw = pPipe->area.CreateExpand(chart->trackFrameWidth);
            //We take a big, unshrunken back-end out of fill: (this will be the next segments outer edge)
            if (rad.x>0) {
                pPipe->pipe_body_fill -= forw_end;
                pPipe->area -= forw_end;
                //below we need to first expand forw_end before substracting it
                //the other way is not ok: Expand fails in expanding negative arcs
                if (content.size() && pPipe->style.read().solid.value() < 255) {
                    pPipe->area_draw -= forw_end.CreateExpand(-chart->trackFrameWidth);
                    pPipe->area_draw *= Contour(side == ESide::RIGHT ? chart->GetDrawing().x.from : chart->GetDrawing().x.till, cd.x,
                        -chart->trackFrameWidth-1, total_height+chart->trackFrameWidth+1);
                }
            }
            //Line shall fall entirely under the next segment, so we add a small block to it
            pPipe->pipe_body_line += Block(Range(cd.x-ilw/2, cd.x+ilw/2), Range(pPipe->pipe_block.y).Expand(gap_for_line));
            pPipe->pipe_whole_line = pPipe->pipe_body_line;
            //shadow need no adjustment, as it will be merged with next segment
            //we clear the holes: no need
            pPipe->pipe_hole_fill.clear();
            pPipe->pipe_hole_line.clear();
            pPipe->pipe_hole_curve.clear();
        } else {
            //No connection, we draw this end, too
            if (rad.x>0) {
                pPipe->pipe_body_fill -= forw_end.CreateExpand(-pPipe->style.read().line.width.value()/2);
                pPipe->pipe_whole_line = pPipe->pipe_body_line + forw_end.CreateExpand(gap_for_line);
                pPipe->pipe_body_line -= forw_end.CreateExpand(gap_for_line);

                //for shadow we add
                pPipe->pipe_shadow += forw_end;
                pPipe->pipe_hole_line = forw_end.CreateExpand(gap_for_line);
                pPipe->pipe_hole_fill = forw_end.CreateExpand(gap_for_fill);
                //const Edge hole_line[2] = {Edge(cd, rad.x+gap_for_line, rad.y+gap_for_line, 0, 90, 270),
                //                           Edge(cd, rad.x+gap_for_line, rad.y+gap_for_line, 0, 270, 90)};
                //pPipe->pipe_hole_curve.assign_dont_check(hole_line);
                ////this is only half of the hole ellipsis
                //pPipe->pipe_hole_curve[0][side == ESide::RIGHT ? 0 : 1].visible = false;
                Path tmp;
                tmp.reserve(5);
                tmp.AppendEllipse(cd, rad.x+gap_for_line, rad.y+gap_for_line,
                    side == ESide::RIGHT ? 0 : 180, 270, 90);
                const XY s = tmp.front().GetStart();
                const XY e = tmp.back().GetEnd();
                const XY off(side == ESide::RIGHT ? -rad.x : rad.x, 0);
                tmp.emplace_back(e, e + off, false);
                tmp.emplace_back(e + off, s + off, false);
                tmp.emplace_back(s + off, s, false);
                pPipe->pipe_hole_curve.assign_dont_check(tmp);
            } else {
                //just chop off from fill and line
                pPipe->pipe_body_fill -= Block(Range(cd.x, cd.x), pPipe->pipe_block.y).Expand(gap_for_fill);
                pPipe->pipe_body_line -= Block(Range(cd.x, cd.x), pPipe->pipe_block.y).Expand(gap_for_line);
                pPipe->pipe_whole_line = pPipe->pipe_body_line;
                //we clear the holes: no need, body will draw the line we need
                pPipe->pipe_hole_fill.clear();
                pPipe->pipe_hole_line.clear();
                pPipe->pipe_hole_curve.clear(); //wont draw anything
            }
            pPipe->area = pPipe->pipe_shadow;
            if (content.size() && pPipe->style.read().solid.value() < 255)
                pPipe->area_draw = pPipe->area.CreateExpand(chart->trackFrameWidth);
        }
        //Finalize covers
        pPipe->area.arc = pPipe.get();
        if (content.size() && pPipe->style.read().solid.value() < 255) {
            //Make a frame, add it to the already added label
            pPipe->area_draw -= pPipe->area;
            pPipe->area_draw += pPipe->text_cover.CreateExpand(chart->trackExpandBy);
            pPipe->area_draw_is_frame = true;
            pPipe->draw_is_different = true;
        }
        if (pipe_body_cover) {
            //now determine the cover to be used for placement
            const double offset = pPipe->style.read().shadow.offset.value();
            if (offset)
                *pipe_body_cover += pPipe->pipe_shadow + pPipe->pipe_shadow.CreateShifted(XY(offset, offset));
            else
                *pipe_body_cover += pPipe->pipe_shadow;
        }
        pPipe->area_important = pPipe->text_cover;
        //avoid duplicating the ovals in a pipe series: only add the forward end, if no upcoming segment
        if (!pPipe->pipe_connect_forw)
            pPipe->area_important += forw_end;
        pPipe->area_important += back_end;
    }
    //merge shadows of connected subsequent segments to the first of the connected segments
    //This is to draw a contiguous shadow for connected segments and draw it when (before)
    //the first segment is drawn.
    for (auto i = series.rbegin(); i!=series.rend(); i++) //reverse traversal
        if ((*i)->pipe_connect_forw) {
            _ASSERT(i!=series.rbegin());
            const auto i_after = std::prev(i);
            (*i)->pipe_shadow += std::move((*i_after)->pipe_shadow);
            (*i_after)->pipe_shadow.clear();
        }
}

void PipeSeries::Layout(Canvas &canvas, AreaList *cover)
{
    height = 0;
    if (!valid) return;
    //Collect cover information from labels and linewidth, so compression of content arrows can be done
    //Determine thickest line for precise pipe alignment
    double max_lineWidth = 0;
    for (auto &pPipe : series)
        max_lineWidth = std::max(max_lineWidth, pPipe->style.read().line.LineWidth());
    double lowest_line_bottom = max_lineWidth + chart->boxVGapInside;
    //Determine highest label and collect all text covers
    //Also calculate all x positioning
    double lowest_label_on_transculent_bottom = lowest_line_bottom;
    double lowest_label_on_opaque_segments_bottom = lowest_line_bottom;
    Area label_covers(this);
    //A few shortcuts. "side" and "radius" must be the same in any pipe element, so we take the first
    const ESide side = series.front()->style.read().side.value();
    const double radius = series.front()->style.read().line.radius.value();
    double note_l = 0, note_r = 0;
    for (auto &pPipe : series) {
        //Variables already set (all of them rounded):
        //pipe_connect true if a segment connects to us directly
        //left_space, right_space contains how much our content expands beyond the entity line,
        pPipe->yPos = 0;
        pPipe->area.clear();
        pPipe->area_draw.clear();
        pPipe->draw_is_different = false;

        //Place side comments. This will update "cover" and thus force the content
        //downward if the content also has side notes
        pPipe->LayoutCommentsHelper(canvas, cover, note_l, note_r);

        //Set pipe_block.x, sx_text, dx_text in each segment, in the meantime
        //pipe_block contains the outside of the pipe, with the exception of the curvature (since it is a rect)
        pPipe->y_text = ceil(chart->boxVGapOutside + pPipe->style.read().line.LineWidth() +
            chart->boxVGapInside);
        pPipe->area.clear();
        pPipe->pipe_block.x.from = chart->XCoord(pPipe->src) - pPipe->left_space; //already rounded
        pPipe->pipe_block.x.till = chart->XCoord(pPipe->dst) + pPipe->right_space;
        pPipe->sx_text = pPipe->pipe_block.x.from + pPipe->style.read().line.LineWidth() + chart->boxVGapInside; //not rounded
        pPipe->dx_text = pPipe->pipe_block.x.till - pPipe->style.read().line.LineWidth() - chart->boxVGapInside;
        switch (side) {
        case ESide::RIGHT: pPipe->dx_text -= radius; break;
        case ESide::LEFT:  pPipe->sx_text += radius; break;
        default: _ASSERT(0);
        }
        //reflow label if necessary
        if (pPipe->parsed_label.IsWordWrap()) {
            const double overflow = pPipe->parsed_label.Reflow(canvas, chart->Shapes, pPipe->dx_text - pPipe->sx_text);
            pPipe->OverflowWarning(overflow, "", series.front()->src, series.front()->dst);
        } else {
            pPipe->CountOverflow(pPipe->dx_text - pPipe->sx_text);
        }
        pPipe->text_cover = pPipe->parsed_label.Cover(chart->Shapes, pPipe->sx_text, pPipe->dx_text, pPipe->y_text);
        // omit text cover for pipes if the pipe is fully opaque,
        // in that case content can be drawn at same position as label - opaque pipe will cover anyway
        double y = pPipe->y_text + pPipe->parsed_label.getTextWidthHeight().y;
        if (y == pPipe->y_text && content.size()==0)
            y += pPipe->style.read().text.getCharHeight(canvas);
        if (pPipe->style.read().solid.value() < 255) {
            label_covers += pPipe->text_cover;
            lowest_label_on_transculent_bottom = std::max(lowest_label_on_transculent_bottom, y);
        } else {
            //collect the highest label of opaque segments for later use
            lowest_label_on_opaque_segments_bottom = std::max(lowest_label_on_opaque_segments_bottom, y);
        }
        chart->Progress.DoneItem(EArcSection::LAYOUT, EArcCategory::PIPE);
    }
    double y = lowest_label_on_transculent_bottom;
    //Calculate the Height of the content
    AreaList content_cover;
    if (content.size())
        y = ceil(chart->PlaceListUnder(canvas, content, ceil(y), lowest_line_bottom,
            label_covers, false, &content_cover));
    //now y contains the bottom of the content arrows (if any),
    //adjust if an opaque pipe's label was not yet considered in y
    y = std::max(y, lowest_label_on_opaque_segments_bottom);
    y += chart->boxVGapInside + max_lineWidth;
    //now y contains the bottommost pixel of the pipe itself
    total_height = y = ceil(y);
    //Now set the y coordinate in all segments
    double max_shadow_offset = 0;
    for (auto &pPipe : series) {
        //fill in pipe_block.y (both are integer)
        pPipe->pipe_block.y.from = chart->boxVGapOutside;
        pPipe->pipe_block.y.till = y;
        chart->NoteBlockers.Append(pPipe.get());
        max_shadow_offset = std::max(max_shadow_offset, pPipe->style.read().shadow.offset.value());
    }
    //Calculate contours in all pipes from pipe_block
    Area pipe_body_cover(this);
    CalculateContours(&pipe_body_cover);
    //Add content to cover (may "come out" from pipe)
    if (cover)
        *cover += content_cover;
    //If we have no valid content, set mainline to that of pipe, else the content's mainline will be used
    if (content_cover.mainline.IsEmpty())
        pipe_body_cover.mainline = Block(chart->GetDrawing().x.from, chart->GetDrawing().x.till, //totalheight includes the top emphvgapoutside
                                         chart->boxVGapOutside, total_height);                   //Expand cover, but not content (that is already expanded)
    if (cover)
        *cover += GetCover4Compress(pipe_body_cover);
    height = yPos + total_height + max_shadow_offset + chart->boxVGapOutside;
    for (auto &pPipe : series)
        pPipe->height = height; //so that if a vertical is following pPipe we get the right height
                                //We do not call NoteHeight here as a PipeSeries will not have notes, only its elements
    comment_height = std::max(note_l, note_r);
}

Range PipeSeries::GetVisualYExtent(bool include_comments) const
{
    const double h = valid ? std::max(0., height - 2*chart->boxVGapOutside) : 0;
    Range ret(yPos+chart->boxVGapOutside, yPos+chart->boxVGapOutside+h);
    if (include_comments && valid)
        ret += yPos+comment_height;
    return ret;
}


void Pipe::ShiftBy(double y)
{
    if (!valid) return;
    if (y==0) return;
    LabelledArc::ShiftBy(y);
    y_text += y;
    text_cover.Shift(XY(0, y));
    pipe_block.Shift(XY(0, y));
    pipe_shadow.Shift(XY(0, y));
    pipe_body_line.Shift(XY(0, y));
    pipe_whole_line.Shift(XY(0, y));
    pipe_hole_line.Shift(XY(0, y));
    pipe_body_fill.Shift(XY(0, y));
    pipe_hole_fill.Shift(XY(0, y));
    pipe_hole_curve.Shift(XY(0, y));
}


void PipeSeries::ShiftBy(double y)
{
    if (!valid) return;
    if (y==0) return;
    for (auto &pPipe : series)
        pPipe->ShiftBy(y);
    if (content.size())
        chart->ShiftByArcList(content, y);
    ArcBase::ShiftBy(y);
}

void PipeSeries::CollectPageBreak()
{
    chart->CollectPageBreakArcList(content);
}

double PipeSeries::SplitByPageBreak(Canvas &canvas, double netPrevPageSize,
    double pageBreak, bool &addCommandNewpage,
    bool addHeading, ArcList &/*res*/)
{
    if (content.size()==0 || keep_together) return -1; //we cannot split if no content
                                                       //if pageBreak goes through a label (or is above) we cannot split there
    for (auto& pPipe : series)
        if (pPipe->text_cover.GetBoundingBox().y.till > pageBreak || pPipe->keep_together)
            return -1;
    const double ret = chart->PageBreakArcList(canvas, content, netPrevPageSize,
        pageBreak, addCommandNewpage, addHeading,
        false, true);
    //if pb makes *all* of the list go to the next page, just shift the
    //whole pipe series to the next page
    if (ret<0) return -1;
    height += ret;
    total_height += ret;
    for (auto& pPipe : series)
        pPipe->pipe_block.y.till += ret;
    CalculateContours();
    return ret;
}


void PipeSeries::PlaceWithMarkers(Canvas &canvas)
{
    if (content.size())
        chart->PlaceWithMarkersArcList(canvas, content);
    for (unsigned u = 0; u<series.size(); u++)
        chart->Progress.DoneItem(EArcSection::PLACEWITHMARKERS, EArcCategory::PIPE);
}

void PipeSeries::PostPosProcess(Canvas &canvas, Chart *ch)
{
    if (!valid) return;
    //For pipes we first add those covers to chart->AllCovers that are not fully opaque,
    //then the content (only in the first segment)
    //then those segments, which are fully opaque
    //(this is because search is backwards and this arrangement fits the visual best
    for (auto &pPipe : series)
        if (pPipe->valid && pPipe->style.read().solid.value() < 255) {
            pPipe->entityLineRange = pPipe->area.GetBoundingBox().y;
            pPipe->LabelledArc::PostPosProcess(canvas, ch);
            chart->Progress.DoneItem(EArcSection::POST_POS, EArcCategory::PIPE);
        }
    if (content.size())
        chart->PostPosProcessArcList(canvas, content, ch);
    for (auto &pPipe : series)
        if (pPipe->valid && pPipe->style.read().solid.value() == 255) {
            pPipe->entityLineRange = pPipe->area.GetBoundingBox().y;
            pPipe->LabelledArc::PostPosProcess(canvas, ch);
            chart->Progress.DoneItem(EArcSection::POST_POS, EArcCategory::PIPE);
        }
    for (auto &pPipe : series)
        if (pPipe->draw_pass != EMscDrawPass::BEFORE_ENTITY_LINES)
            chart->HideEntityLines(pPipe->pipe_shadow);
}

void PipeSeries::RegisterLabels()
{
    //Register labels
    for (auto &pPipe : series)
        chart->RegisterLabel(pPipe->parsed_label, LabelInfo::PIPE,
            pPipe->sx_text, pPipe->dx_text, pPipe->y_text);
    if (content.size())
        chart->RegisterLabelArcList(content);
}

void PipeSeries::CollectIsMapElements(Canvas &canvas)
{
    for (auto &pPipe : series)
        pPipe->parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
            pPipe->sx_text, pPipe->dx_text, pPipe->y_text);
    if (content.size())
        chart->CollectIsMapElementsArcList(content, canvas);
}


//Draw a pipe, this is called for each segment, bool params dictate which part
//topside is the bigger part of the pipe
//backside is the small oval visible form the back of the pipe. Includes shadow
//this->yPos is the outer edge of the top line
//this->left_space and right_space includes linewidth
void Pipe::DrawPipe(Canvas &canvas, EMscDrawPass pass,
                    bool topSideFill, bool topSideLine, bool backSide,
                    bool text, double next_lw,
                    int drawing_variant)
{
    if (pass!=draw_pass)
        return;
    if (backSide) {
        //backside is always filled fully opaque
        FillAttr fill = style.read().fill;
        fill.color->a = 255;
        if (fill.color2) fill.color2->a = 255;
        if (canvas.does_graphics()) {
            //Shadow under the whole pipe.
            //(For connected pipe segments, only one contains any area in 'pipe_shadows')
            canvas.Shadow(pipe_shadow, style.read().shadow);
            //The back of the main pipe
            canvas.Fill(pipe_body_fill, fill);
            //the back of the small ellipsis visible from the side
            fill.FlipGradient(true);
            canvas.Fill(pipe_hole_fill, fill);
            //Draw the backside line
            canvas.Line(pipe_hole_curve, style.read().line);
        } else {
            canvas.Add(GSShape(pipe_shadow, LineAttr::None(), FillAttr::None(), style.read().shadow));
            canvas.Add(GSShape(pipe_body_fill, LineAttr::None(), fill));
            fill.FlipGradient(true);
            canvas.Add(GSShape(pipe_hole_fill, LineAttr::None(), fill));
            canvas.Add(GSShape(pipe_hole_curve, style.read().line));
        }
    }
    if (topSideFill) {
        //apply the transparency of the solid attribute
        FillAttr fill = style.read().fill;
        fill.color->a = unsigned(style.read().solid.value()) * unsigned(fill.color->a) / 255;
        if (canvas.does_graphics())
            canvas.Fill(pipe_body_fill, fill);
        else
            canvas.Add(GSShape(pipe_body_fill, LineAttr::None(), fill));
    }
    if (topSideLine) {
        const double x = style.read().side == ESide::RIGHT ? pipe_block.x.till : pipe_block.x.from;
        Contour clip(x, style.read().side == ESide::LEFT ? chart->GetDrawing().x.till : chart->GetDrawing().x.from,
                     chart->GetDrawing().y.from, chart->GetDrawing().y.till);
        if (style.read().line.radius.value()>0 && pipe_connect_forw) {
            const XY c(x, pipe_block.y.MidPoint());
            clip -= Contour(c, style.read().line.radius.value()-next_lw/2, pipe_block.y.Spans()/2.-next_lw/2);
        }
        if (canvas.does_graphics()) {
            cairo_line_join_t t = canvas.SetLineJoin(CAIRO_LINE_JOIN_BEVEL);
            canvas.Clip(clip);
            const double spacing = style.read().line.Spacing();
            if (!style.read().line.IsDoubleOrTriple() || drawing_variant==0)
                canvas.Line(pipe_body_line, style.read().line); //basic variant, in case of double & triple, lines cross
            else if (style.read().line.IsDouble()) {
                canvas.SetLineAttr(style.read().line);
                if (drawing_variant==1) { //advanced: lines do not cross
                    canvas.singleLine(pipe_whole_line.CreateExpand(spacing), style.read().line);
                    canvas.singleLine(pipe_hole_line.CreateExpand(-spacing), style.read().line);
                    canvas.SetLineJoin(CAIRO_LINE_JOIN_MITER);
                    canvas.singleLine(pipe_body_line.CreateExpand(-spacing), style.read().line);
                } else { //very advanced: proper double line joint
                    canvas.singleLine(pipe_whole_line.CreateExpand(spacing), style.read().line); //outer
                    canvas.SetLineJoin(CAIRO_LINE_JOIN_MITER);
                    canvas.singleLine(pipe_hole_line.CreateExpand(-spacing), style.read().line); //inner hole
                    canvas.singleLine(pipe_body_line.CreateExpand(-spacing) -
                                      pipe_hole_line.CreateExpand(spacing), style.read().line);  //inner body
                }
            } else if (style.read().line.IsTriple()) {
                canvas.SetLineAttr(style.read().line);
                //here variant 1 and 2 result in the same
                canvas.singleLine(pipe_whole_line.CreateExpand(spacing), style.read().line);  //outer
                canvas.SetLineJoin(CAIRO_LINE_JOIN_MITER);
                canvas.singleLine(pipe_body_line.CreateExpand(-spacing), style.read().line);  //inner body
                                                                                              //pipe_hole_line.CreateExpand(spacing), style.read().line);  //inner body
                canvas.SetLineJoin(CAIRO_LINE_JOIN_BEVEL);
                canvas.singleLine(pipe_hole_line.CreateExpand(-spacing), style.read().line);   //inner hole
                canvas.SetLineAttr(LineAttr(style.read().line.TripleMiddleWidth()));
                canvas.singleLine(pipe_body_line, style.read().line); //middle line
            }
            canvas.UnClip();
            canvas.SetLineJoin(t);
        } else {
            Path line = pipe_body_line;
            std::erase_if(line, [](const Edge& e) { return e.IsVertical(); });
            canvas.Add(GSPath(std::move(line), style.read().line)); //We use the basic variant to generate as few shapes, as possible
        }
    }
    if (text)
        parsed_label.Draw(canvas, chart->Shapes, sx_text, dx_text, y_text);
}

void PipeSeries::RegisterCover(EMscDrawPass pass) const
{
    //first place pipes that are not fully opaque
    for (auto &pPipe : series)
        if (pPipe->style.read().solid.value() < 255) {
            //pipes with (semi)transparent backsides and content go to the background
            EMscDrawPass effective_pass = pPipe->draw_pass;
            if (!pPipe->style.read().fill.IsFullyOpaque() && content.size())
                effective_pass = EMscDrawPass::BEFORE_ENTITY_LINES;
            if (pass == effective_pass)
                chart->AllCovers += pPipe->area;
        }
    //then the content
    chart->RegisterCoverArcList(content, pass);
    //finally pipes that *are* fully opaque
    for (auto &pPipe : series)
        if (pPipe->style.read().solid.value() == 255) {
            EMscDrawPass effective_pass = pPipe->draw_pass;
            if (!pPipe->style.read().fill.IsFullyOpaque() && content.size())
                effective_pass = EMscDrawPass::BEFORE_ENTITY_LINES;
            if (pass == effective_pass)
                chart->AllCovers += pPipe->area;
        }
}



void PipeSeries::Draw(Canvas &canvas, EMscDrawPass pass) const
{
    if (!valid) return;
    for (auto i = series.begin(); i!=series.end(); i++) {
        //Don't draw the topside fill
        //Draw the topside line only if pipe is fully transparent. Else we may cover the line.
        //Draw the backside and shadow in any case.
        //Do not draw text
        auto i_next = i; i_next++;
        const double next_linewidth = i_next!=series.end() ? (*i_next)->style.read().line.width.value() : 0;
        (*i)->DrawPipe(canvas, pass, false, (*i)->style.read().solid == 0, true, false, next_linewidth, drawing_variant);
    }
    if (content.size()) {
        if (pass==EMscDrawPass::AFTER_ENTITY_LINES)
            for (auto i = series.begin(); i!=series.end(); i++)
                if ((*i)->drawEntityLines)
                    chart->DrawEntityLinesIncludeLast(canvas, yPos, total_height, (*i)->src, (*i)->dst);
        chart->DrawArcList(canvas, content, chart->GetTotal().y, pass);
    }
    for (auto i = series.begin(); i!=series.end(); i++) {
        //Draw the topside fill only if the pipe is not fully transparent.
        //Draw the topside line in any case
        //Do not draw the backside (that may content arrow lines already drawn)
        //Draw the text
        auto i_next = i; i_next++;
        const double next_linewidth = i_next!=series.end() ? (*i_next)->style.read().line.width.value() : 0;
        (*i)->DrawPipe(canvas, pass, (*i)->style.read().solid.value() > 0, true, false, true, next_linewidth, drawing_variant);
    }
}

//////////////////////////////////////////////////////////////////////////////////////

/** Standard constructor. */
Divider::Divider(EArcSymbol t, MscChart *msc) :
    LabelledArc(EArcCategory::DIVIDER, msc),
    type(t), nudge(t==EArcSymbol::DIV_NUDGE),
    title(t==EArcSymbol::DIV_TITLE || t==EArcSymbol::DIV_SUBTITLE),
    wide(false),
    extra_space(t==EArcSymbol::DIV_DISCO ? msc->discoVgap :
        t==EArcSymbol::DIV_TITLE ? msc->titleVgap :
        t==EArcSymbol::DIV_SUBTITLE ? msc->subtitleVgap :
        0)
{
    _ASSERT(IsArcSymbolDivider(type));
}


void Divider::SetStyleBeforeAttributes(AttributeList *)
{
    switch (type) {
    default:
        _ASSERT(0);
        FALLTHROUGH;
    case EArcSymbol::DIV_DISCO:
        SetStyleWithText("divider", (MscStyleCoW*)&chart->MyCurrentContext().styles["..."]); break;
    case EArcSymbol::DIV_DIVIDER:
        SetStyleWithText("divider", (MscStyleCoW*)&chart->MyCurrentContext().styles["---"]); break;
    case EArcSymbol::DIV_VSPACE:
    case EArcSymbol::DIV_NUDGE:
        SetStyleWithText("divider", nullptr); break;
    case EArcSymbol::DIV_TITLE:
        SetStyleWithText("title", nullptr); break;
    case EArcSymbol::DIV_SUBTITLE:
        SetStyleWithText("subtitle", nullptr); break;
    }
}

bool Divider::AddAttribute(const Attribute &a)
{
    if (a.Is("_wide")) {
        if (a.CheckType(EAttrType::BOOL, chart->Error))
            wide = a.yes;
        return true;
    }
    return LabelledArc::AddAttribute(a);
};

void Divider::AttributeNames(Csh &csh, bool nudge, bool title)
{
    if (nudge) return;
    LabelledArc::AttributeNames(csh);
    csh.AttributeNamesForStyle(title ? "title" : "divider");
}

bool Divider::AttributeValues(std::string_view attr, Csh &csh, bool nudge, bool title)
{
    if (nudge) return false;
    if (csh.AttributeValuesForStyle(attr, title ? "title" : "divider")) return true;
    if (LabelledArc::AttributeValues(attr, csh)) return true;
    return false;
}

ArcBase* Divider::PostParseProcess(Canvas &canvas, bool hide, EntityRef &left, EntityRef &right,
                                   Numbering& number, MscElement** target, ArcBase* byref_target)
{
    if (!valid) return nullptr;
    string ss;
    switch (type) {
    case EArcSymbol::DIV_VSPACE:   ss = "This element";  break;
    case EArcSymbol::DIV_DISCO:    ss = "'...'"; break;
    case EArcSymbol::DIV_DIVIDER:  ss = "'---'"; break;
    case EArcSymbol::DIV_TITLE:    ss = "Titles"; break;
    case EArcSymbol::DIV_SUBTITLE: ss = "Subtitles"; break;
    case EArcSymbol::DIV_NUDGE:    ss = "Nudges"; break;
    default: _ASSERT(0); break;
    }

    if (title && label.length()==0) {
        chart->Error.Error(file_pos.start, ss + " must have a label. Ignoring this command.");
        return nullptr;
    }

    //Add numbering, if needed
    LabelledArc::PostParseProcess(canvas, hide, left, right, number, target, byref_target);

    if (hide) return nullptr;
    return this;
}

void Divider::Width(Canvas &, DistanceRequirements &vdist)
{
    if (!valid) return;
    //Add a new element to vdist
    vdist.AddElementTop(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
    //Now, if we have text or a line, we add LSide and RSide as touched entities
    if (parsed_label.getTextWidthHeight().y>0 || !style.read().line.IsFullyTransparent()) {
        vdist.InsertEntity(chart->LSide->index);
        vdist.InsertEntity(chart->RSide->index);
    }
    //Add a new element to vdist
    vdist.AddElementBottom(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
    if (nudge || !valid || parsed_label.getTextWidthHeight().y==0)
        return;
    //Get marging from lside and rside
    text_margin = wide ? 0 : chart->XCoord(MARGIN)*2;
    if (title)
        text_margin += style.read().line.LineWidth();
    //calculate space requirement between lside and rside
    const double width = 2*text_margin + parsed_label.getSpaceRequired();
    if (width>0)
        vdist.Insert(chart->LSide->index, chart->RSide->index, width);

}

//Dividers that are there just for vspace (without text or line or disco)
//do not have any cover - only mainline. This will make them
//allocate space correctly (even with compress) even if used inside a parallel
//block (there the mainlines affect only elements in the same column, whereas
//covers affect all - using the new parallel layout implemented in
//MscChart::PlaceArcLists().
void Divider::Layout(Canvas &canvas, AreaList *cover)
{
    height = 0;
    if (!valid) return;
    yPos = 0;
    if (nudge) {
        Block b(chart->GetDrawing().x.from, chart->GetDrawing().x.till, 0, chart->nudgeSize);
        area.mainline = area = b;
        area.arc = this;
        if (cover) {
            Area cover_area(this);
            cover_area.mainline = b;
            //nothing in "cover_area" just a mainline
            *cover = cover_area;
        }
        height = chart->nudgeSize;
        LayoutComments(canvas, cover);
        return;
    }
    double y = wide ? 0 : chart->arcVGapAbove;
    y += extra_space;
    if (title)
        y += style.read().line.LineWidth();
    const double charheight = style.read().text.getCharHeight(canvas);
    //reflow text if needed
    if (parsed_label.size()) {
        if (parsed_label.IsWordWrap()) {
            const double overflow = parsed_label.Reflow(canvas, chart->Shapes, chart->GetDrawing().x.Spans() - 2*text_margin);
            OverflowWarning(overflow, "", &chart->AllEntities.front(), &chart->AllEntities.back());
        } else {
            CountOverflow(chart->GetDrawing().x.Spans() - 2*text_margin);
        }
    }
    XY wh = parsed_label.getTextWidthHeight();
    if (!wh.y) wh.y = charheight;
    centerline = y+wh.y/2;
    line_margin = chart->XCoord(MARGIN);
    text_cover = parsed_label.Cover(chart->Shapes, chart->GetDrawing().x.from + text_margin, chart->GetDrawing().x.till-text_margin, y);
    area = text_cover;
    area_important = area;
    const double lw = style.read().line.LineWidth();
    //Add a cover block for the line, if one exists
    if (!title && style.read().line.type.value() != ELineType::NONE &&
        style.read().line.color->type!=ColorType::INVALID &&
        style.read().line.color->a>0)
        area += Block(chart->GetDrawing().x.from + line_margin, chart->GetDrawing().x.till - line_margin,
            centerline - style.read().line.LineWidth()*2, centerline + style.read().line.LineWidth()*2);
    else if (title && (style.read().line.type.value() != ELineType::NONE || style.read().fill.color->type!=ColorType::INVALID))
        area += Block(chart->GetDrawing().x.from + text_margin-lw, chart->GetDrawing().x.till - text_margin+lw,
            y-lw, y+wh.y+lw);
    else if (area.IsEmpty() && type!=EArcSymbol::DIV_VSPACE)
        //for VSPACE with no text and line (and thus empty 'area' so far) we only add a mainline
        area = Block(chart->GetDrawing().x.from, chart->GetDrawing().x.till,
            centerline-chart->nudgeSize/2, centerline+chart->nudgeSize/2);
    area.arc = this;

    height = wh.y + 2*extra_space + (wide ? 0 : chart->arcVGapAbove+chart->arcVGapBelow);
    if (title)
        height += 2*style.read().line.LineWidth() + style.read().shadow.offset.value();
    //Discontinuity lines cannot be compressed much
    if (type==EArcSymbol::DIV_DISCO || title)
        area.mainline = Block(chart->GetDrawing().x.from, chart->GetDrawing().x.till,
            wide ? 0 : chart->arcVGapAbove, height- (wide ? 0 : chart->arcVGapBelow));
    else
        area.mainline = Block(chart->GetDrawing().x.from, chart->GetDrawing().x.till,
            centerline-charheight/2, centerline+charheight/2);
    chart->NoteBlockers.Append(this);
    if (cover)
        *cover = GetCover4Compress(area);
    LayoutComments(canvas, cover);
}

void Divider::ShiftBy(double y)
{
    if (!valid) return;
    if (!nudge)
        text_cover.Shift(XY(0, y));
    LabelledArc::ShiftBy(y);
}

void Divider::PostPosProcess(Canvas &canvas, Chart *ch)
{
    if (!valid) return;
    if (!nudge)
        chart->HideEntityLines(text_cover);
    if (type == EArcSymbol::DIV_DISCO)
        entityLineRange = area.mainline.GetBoundingBox().y; //will never be compressed away
    else
        entityLineRange = area.GetBoundingBox().y;
    LabelledArc::PostPosProcess(canvas, ch);
}

void Divider::RegisterLabels()
{
    if (!wide)
        chart->RegisterLabel(parsed_label, LabelInfo::DIVIDER,
            chart->GetDrawing().x.from + text_margin,
            chart->GetDrawing().x.till - text_margin,
            yPos + chart->arcVGapAbove + extra_space);
}

void Divider::CollectIsMapElements(Canvas &canvas)
{
    if (!wide)
        parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
            chart->GetDrawing().x.from + text_margin,
            chart->GetDrawing().x.till - text_margin,
            yPos + chart->arcVGapAbove + extra_space);
}


void Divider::Draw(Canvas &canvas, EMscDrawPass pass) const
{
    if (!valid) return;
    if (pass!=draw_pass) return;
    if (nudge) return;
    if (title) {
        const Block outer(chart->GetDrawing().x.from + text_margin, chart->GetDrawing().x.till - text_margin,
            yPos+extra_space, yPos+height-style.read().shadow.offset.value()-extra_space);
        if (canvas.does_graphics()) {
            canvas.Shadow(outer, style.read().line, style.read().shadow);
            canvas.Fill(outer.CreateExpand(-style.read().line.LineWidth()/2-style.read().line.Spacing()), style.read().line, style.read().fill);
            canvas.Line(outer.CreateExpand(-style.read().line.LineWidth()/2), style.read().line);
        } else
            canvas.Entity(outer, parsed_label, style.read(), chart->Shapes, -1);
    }
    if (!title || canvas.does_graphics())
        parsed_label.Draw(canvas, chart->Shapes, chart->GetDrawing().x.from + text_margin, chart->GetDrawing().x.till - text_margin,
                          yPos + (wide ? 0 : chart->arcVGapAbove+extra_space));
    if (title) return;
    //determine widest extent for coverage at the centerline+-style.read().line.LineWidth()/2;
    const double lw2 = ceil(style.read().line.LineWidth()/2.);
    const Block b(chart->GetDrawing().x.from+line_margin, chart->GetDrawing().x.till-line_margin,
                  yPos + centerline - lw2, yPos + centerline + lw2);
    const Range r = (text_cover * b).GetBoundingBox().x;
    std::array<std::array<XY, 2>, 2> P;
    const size_t e = r.IsInvalid() ? 0 : 1;
    P[0][0] = { chart->GetDrawing().x.from+line_margin, yPos + centerline };
    P[e][1] = { chart->GetDrawing().x.till-line_margin, yPos + centerline };
    if (e) {
        P[0][1] = { r.from-chart->boxVGapInside, yPos + centerline };
        P[1][0] = { r.till+chart->boxVGapInside, yPos + centerline };
    }
    for (size_t i = 0; i<=e; i++)
        if (canvas.does_graphics())
            canvas.Line(P[i][0], P[i][1], style.read().line);
        else
            canvas.Add(GSPath({P[i][0], P[i][1]}, style.read().line));
}

//////////////////////////////////////////////////////////////////////////////////////

ParallelBlocks::ParallelBlocks(MscChart *msc, ArcList*l, AttributeList *al, bool before, bool u) :
    ArcBase(EArcCategory::PARALLEL, msc), before_autogen_entities(before),
    unroll(u), layout(chart->simple_arc_parallel_layout ? OVERLAP : ONE_BY_ONE_MERGE)
{
    AddArcList(l, al);
}

ParallelBlocks* ParallelBlocks::AddArcList(ArcList*l, AttributeList *al)
{
    _ASSERT(!unroll|| blocks.size()==0);
    if (l) {
        const EVerticalIdent i = blocks.size() ? blocks.front().ident : NOT_SET;
        blocks.emplace_back(std::move(*l), i);
        delete l;
        AddAttributeList(al);
        keep_together = false;
    }
    return this;
}

/** Possible values for parallel layout types*/
template<> const char EnumEncapsulator<ParallelBlocks::EParallelLayoutType>::names[][ENUM_STRING_LEN] =
{"invalid", "overlap", "one_by_one", "one_by_one_merge", ""};

/** Descriptions for parallel layout types*/
template<> const char * const EnumEncapsulator<ParallelBlocks::EParallelLayoutType>::descriptions[] =
{"", "Lay out each block independently even if they end up overlapping.",
"Place elements from each block one-by-one avoiding overlap.",
"Place elements from each block one-by-one avoiding overlap. Merge these blocks with "
"surrounding parallel blocks. Same as 'one_by_one' for non-nested parallel blocks.",
""};

/** Possible values for a vertical ident types*/
template<> const char EnumEncapsulator<ParallelBlocks::EVerticalIdent>::names[][ENUM_STRING_LEN] =
{"invalid", "top", "middle", "bottom", ""};

/** Descriptions for a vertical ident types*/
template<> const char * const EnumEncapsulator<ParallelBlocks::EVerticalIdent>::descriptions[] =
{"", "Align this parallel block to the top of the tallest parallel block.",
"Align this parallel block to the middle of the tallest parallel block.",
"Align this parallel block to the bottom of the tallest parallel block.",
""};


bool ParallelBlocks::AddAttribute(const Attribute &a)
{
    if (a.Is("layout")) {
        if (blocks.size()==1) {
            if (!a.EnsureNotClear(chart->Error, EStyleType::ELEMENT)) return true;
            if (a.type == EAttrType::STRING && Convert(a.value, layout)) return true;
            a.InvalidValueError(CandidatesFor(layout), chart->Error);
        } else
            chart->Error.Error(a, false,
                "You can set the layout attribute for a series of parallel blocks only before the first block. Ignoring attribute.");
        return true;
    }
    if (a.Is("vertical_ident")) {
        blocks.back().ident_pos = a.linenum_attr.start;
        if (!a.EnsureNotClear(chart->Error, EStyleType::ELEMENT)) return true;
        if (a.type == EAttrType::STRING && Convert(a.value, blocks.back().ident)) return true;
        a.InvalidValueError(CandidatesFor(blocks.back().ident), chart->Error);
        return true;
    }
    if (a.Is("compress") || a.Is("vspacing") ||
        a.Is("parallel") || a.Is("overlap") ||
        a.Is("keep_together") || a.Is("keep_with_next"))
        return ArcBase::AddAttribute(a); //Only these of the ArcBase attributes apply
    return false;
}

void ParallelBlocks::AttributeNames(Csh &csh, bool first)
{
    if (first) {
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "layout",
            "Specify what algorithm is used to lay out parallel blocks.",
            EHintType::ATTR_NAME));
        ArcBase::AttributeNamesSet1(csh);
    }
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "vertical_ident",
        "For the 'overlap' layout algorithm specify vertical alignment.",
        EHintType::ATTR_NAME));
}

bool ParallelBlocks::AttributeValues(std::string_view attr, Csh &csh, bool first)
{
    if (first && ArcBase::AttributeValuesSet1(attr, csh)) return true;
    if (first && CaseInsensitiveEqual(attr, "layout")) {
        csh.AddToHints(EnumEncapsulator<ParallelBlocks::EParallelLayoutType>::names,
            EnumEncapsulator<ParallelBlocks::EParallelLayoutType>::descriptions,
            csh.HintPrefix(COLOR_ATTRVALUE),
            EHintType::ATTR_VALUE);
        return true;
    }
    if (CaseInsensitiveEqual(attr, "vertical_ident")) {
        csh.AddToHints(EnumEncapsulator<ParallelBlocks::EVerticalIdent>::names,
            EnumEncapsulator<ParallelBlocks::EVerticalIdent>::descriptions,
            csh.HintPrefix(COLOR_ATTRVALUE),
            EHintType::ATTR_VALUE);
        return true;
    }
    return false;
}

EDirType ParallelBlocks::GetToucedEntities(NEntityList &el) const
{
    EDirType dir = EDirType::INDETERMINATE;
    for (auto &col : blocks)
        dir = chart->GetTouchedEntitiesArcList(col.arcs, el, dir);
    return dir;
}

ArcBase* ParallelBlocks::PostParseProcess(Canvas &canvas, bool hide,
    EntityRef &left, EntityRef &right,
    Numbering &number, MscElement **target,
    ArcBase * /*byref_target*/)
{
    _ASSERT(!unroll);
    if (!valid) return nullptr;
    //finalize ident
    if (layout==OVERLAP) {
        for (auto &col:blocks)
            if (col.ident==NOT_SET)
                col.ident = TOP; //top is default
    } else
        for (auto &col:blocks)
            if (col.ident!=NOT_SET) {
                chart->Error.Warning(col.ident_pos, "Attribute 'vertical_ident' has impact only if layout is set to 'overlap'. Ignoring attribute.",
                    "Layout is now set to '"+string(EnumEncapsulator<ParallelBlocks::EParallelLayoutType>::names[layout])+"'.");
                break;
            }

    for (auto &col : blocks)
        chart->PostParseProcessArcList(canvas, hide, col.arcs, false,
            left, right, number, target);
    if (hide) return nullptr;
    //prune empty blocks
    for (auto i = blocks.begin(); i != blocks.end(); /*nope*/)
        if (i->IsEmpty())
            blocks.erase(i++);
        else
            i++;

    return blocks.size() ? this : nullptr;
}

void ParallelBlocks::FinalizeLabels(Canvas &canvas)
{
    _ASSERT(!unroll);
    for (auto &col : blocks)
        chart->FinalizeLabelsArcList(col.arcs, canvas);
    ArcBase::FinalizeLabels(canvas);
}

void ParallelBlocks::Width(Canvas &canvas, DistanceRequirements &vdist)
{
    _ASSERT(!unroll);
    if (!valid) return;
    //Add a new element to vdist
    vdist.AddElementTop(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
    for (auto &col : blocks)
        chart->WidthArcList(canvas, col.arcs, vdist);
    //Add a new element to vdist
    vdist.AddElementBottom(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
}

void ParallelBlocks::Layout(Canvas &canvas, AreaList *cover)
{
    _ASSERT(!unroll);
    height = 0;
    if (!valid) return;
    switch (layout) {
    default:
        _ASSERT(0);
        break;
    case OVERLAP:
        for (auto &col : blocks) {
            //Each parallel block is compressed without regard to the others
            col.height = chart->LayoutArcList(canvas, col.arcs, cover);
            height = std::max(height, col.height);
        }
        //Now do vertical alignment
        for (auto &col : blocks)
            switch (col.ident) {
            default:
                _ASSERT(0);
                FALLTHROUGH;
            case TOP:
                break;
            case MIDDLE:
                chart->ShiftByArcList(col.arcs, floor((height-col.height)/2));
                break;
            case BOTTOM:
                chart->ShiftByArcList(col.arcs, floor(height-col.height));
                break;
            }
        break;
    //one_by_one_merge can happen if MscChart::LayoutArcListsParallel() decided that 
    //this particular ParallelBlocks would be laid out exactly as one_by_one and opted
    //to do that algorithm.
    case ONE_BY_ONE_MERGE: 
    case ONE_BY_ONE: 
        std::list<LayoutColumn> y;
        unsigned c = 0;
        for (auto &col : blocks)
            y.emplace_back(&col.arcs, c++);
        height = chart->LayoutParallelArcLists(canvas, y, cover);
        break;
    }
    //Do not expand cover, it has already been expanded
    LayoutComments(canvas, cover);
}

Range ParallelBlocks::GetVisualYExtent(bool include_comments) const
{
    Range ret(yPos, yPos);
    for (auto &col : blocks)
        for (auto &pArc : col.arcs)
            ret += pArc->GetVisualYExtent(include_comments);
    if (include_comments && valid)
        ret += yPos+comment_height;
    return ret;
}



void ParallelBlocks::ShiftBy(double y)
{
    if (!valid) return;
    for (auto &col : blocks)
        chart->ShiftByArcList(col.arcs, y);
    ArcBase::ShiftBy(y);
}

void ParallelBlocks::CollectPageBreak()
{
    if (!valid) return;
    for (auto &col : blocks)
        chart->CollectPageBreakArcList(col.arcs);
}


/** Find the first arc starting from 'e' in 'list' of nonzero height. */
[[nodiscard]] ArcList::const_iterator FindFirstNonZero(ArcList::const_iterator e, const ArcList *list)
{
    return std::find_if(e, list->end(), [](const std::unique_ptr<ArcBase>& a) { return a->GetVisualYExtent(true).Spans() != 0; });
}


double ParallelBlocks::SplitByPageBreak(Canvas &canvas, double netPrevPageSize,
    double pageBreak, bool &addCommandNewpage,
    bool addHeading, ArcList &/*res*/)
{
    if (keep_together) return -1;
    //First merge our content to a single list
    if (blocks.size()>1) {
        //We cannot use simple merge here, because in case of compress
        //a zero-height element between two compressed non-zero
        //may start later (at the touchpoint) than the subsequent element.
        //The rule here is to move zero-height elements along with the
        //subsequent non-zero element after them. If they are at the end
        //of a block, then move them with the non-zero element before them.
        for (auto b = ++blocks.begin(); b!=blocks.end(); b++) {
            ArcList result;
            ArcList *list[2] = {&blocks.front().arcs, &b->arcs};
            ArcList::const_iterator e[2] = {list[0]->begin(), list[1]->begin()};
            e[0] = FindFirstNonZero(e[0], list[0]);
            if (e[0] == list[0]->end()) result.Append(list[0]);
            e[1] = FindFirstNonZero(e[1], list[1]);
            if (e[1] == list[1]->end()) result.Append(list[1]);
            while (e[0] != list[0]->end() && e[1] != list[1]->end()) {
                unsigned u = (*e[0])->GetVisualYExtent(true).from > (*e[1])->GetVisualYExtent(true).from; // cppcheck-suppress eraseDereference
                result.splice(result.end(), *list[u], list[u]->begin(), ++e[u]);
                e[u] = FindFirstNonZero(e[u], list[u]);
                if (e[u] == list[u]->end()) result.Append(list[u]);
            }
            result.Append(list[0]);
            result.Append(list[1]);
            blocks.front().arcs.swap(result); //move the result to "blocks.front()"
        }
        blocks.erase(++blocks.begin(), blocks.end());
    }
    //We may get -1 if all of us would move to the next page -> we pass that to our caller, too
    const double incr = chart->PageBreakArcList(canvas, blocks.front().arcs, netPrevPageSize,
        pageBreak, addCommandNewpage, addHeading,
        false, true);
    //if we have increased in size, adjust our height (so that verticals attached to us
    //scale correctly and also we get drawn on both pages.
    if (incr>0) height += incr;
    return incr;
}

void ParallelBlocks::PlaceWithMarkers(Canvas &canvas)
{
    if (!valid) return;
    for (auto &col : blocks)
        chart->PlaceWithMarkersArcList(canvas, col.arcs);
}

void ParallelBlocks::PostPosProcess(Canvas &canvas, Chart *ch)
{
    _ASSERT(!unroll);
    if (!valid) return;
    ArcBase::PostPosProcess(canvas, ch);
    for (auto &col : blocks)
        chart->PostPosProcessArcList(canvas, col.arcs, ch);
}

void ParallelBlocks::RegisterLabels()
{
    if (valid)
        for (auto &col : blocks)
            chart->RegisterLabelArcList(col.arcs);
}

void ParallelBlocks::CollectIsMapElements(Canvas &canvas)
{
    if (valid)
        for (auto &col : blocks)
            chart->CollectIsMapElementsArcList(col.arcs, canvas);
}

void ParallelBlocks::RegisterCover(EMscDrawPass pass) const
{
    if (valid)
        for (auto &col : blocks)
            chart->RegisterCoverArcList(col.arcs, pass);
}


void ParallelBlocks::Draw(Canvas &canvas, EMscDrawPass pass) const
{
    if (valid)
        for (auto &col : blocks)
            chart->DrawArcList(canvas, col.arcs, chart->GetTotal().y, pass);
}



///////////////////////////////////////////////////////////////

/** Construct a join series with its initial element.
 * @param msc The chart we belong to
 * @param first The initial element. We take ownership of it.*/
JoinSeries::JoinSeries(MscChart * msc, ArcBase *first) :
    ArcBase(EArcCategory::PARALLEL, msc)
{
    AddAttributeList(nullptr); //We will never add attributes to JoinSeries elements. They are created in MscChart::PostParseProcessArcList().
    content.Append(first);
    first->join_pos.MakeInvalid();
}

void JoinSeries::Append(ArcBase *a)
{
    content.Append(a);
    a->join_pos.MakeInvalid();
}

void JoinSeries::ReinsertTmpStoredNotes(ArcList& list, ArcList::iterator after) {
    ArcList::iterator before = std::next(after);
    for (auto& p: content)
        p->ReinsertTmpStoredNotes(list, std::prev(before));
}


EDirType JoinSeries::GetToucedEntities(NEntityList &el) const
{
    return chart->GetTouchedEntitiesArcList(content, el);
}


/** Tests if two elements can join.
* Emits errors if not, makes unspecified endings specific, if needed.
* This is called twice. Once at the beginning of MscChart::PostParseProcess() when we setup the join
* series'. Second just before Width(), to cater for entities disappearing due to entity collapse and
* to set unspecified endings precisely.
* @param [in] a The first element
* @param [in] b The second element
* @param [in] side A requirement, which side of 'a' shall 'b' come. This is used, when 'a' has already
*                  something joined to one of its sides. It may be 'END', in which case we dont care.
* @param [in] Error If non-null, we emit error messages on error.
* @returns a JoinableResult structure with the details of the join. Its 'type' can either be
*          CURLY, SETTLED or ERROR_OVERLAP (for all errors). */
JoinableResult JoinSeries::CanJoinTo(ArcBase * a, ArcBase * b, ESide side, MscError *Error)
{
    _ASSERT(a);
    _ASSERT(b);
    //If 'a' is a join series, take the last of its elements.
    JoinSeries *ja = dynamic_cast<JoinSeries*>(a);
    if (ja) a = ja->content.back().get();
    //Test if the type of the two elements are capable of joining
    if (!a->CanJoin()) {
        if (Error) Error->Error(a->file_pos.start, "This element cannot be joined to.");
        if (Error) Error->Error(b->join_pos, a->file_pos.start, "The offending (and ignored) 'join' keyword.");
        return JoinableResult(JoinableResult::ERROR_OVERLAP);
    }
    if (!b->CanJoin()) {
        if (Error) Error->Error(b->join_pos, "This element cannot be joined. Ignoring 'join' keyword.");
        return JoinableResult(JoinableResult::ERROR_OVERLAP);
    }
    //Get ending info
    std::list<EndConnection> a_ends = {a->GetConnection(true), a->GetConnection(false)};
    std::list<EndConnection> b_ends = {b->GetConnection(true), b->GetConnection(false)};
    //Sort them, so front() is left and back() is right side
    a_ends.sort();
    b_ends.sort();
    //if one of them is a '->' both ends unspecified, sort them in reverse order, so that they connect to everything
    if (a_ends.front().is_unspecified && a_ends.back().is_unspecified) a_ends.reverse();
    if (b_ends.front().is_unspecified && b_ends.back().is_unspecified) b_ends.reverse();
    //now see if we can connect. List all 4 possible ways to connect
    //This categorization assumes side==RIGHT (in which case we shall connect only to the left side of 'a')
    std::list<JoinableResult> res_good_dir = {a_ends.back().CanJoinTo(b_ends.front()),
                                              a_ends.back().CanJoinTo(b_ends.back())};
    std::list<JoinableResult> res_bad_dir = {a_ends.front().CanJoinTo(b_ends.front()),
                                             a_ends.front().CanJoinTo(b_ends.back())};
    if (side==ESide::END || dynamic_cast<SelfArrow*>(a))
        res_good_dir.splice(res_good_dir.end(), res_bad_dir); //in this case there is no bad dir: we accept all joins
    else if (side==ESide::LEFT)
        res_good_dir.swap(res_bad_dir); //in this case it is the opposite
    res_good_dir.sort();
    res_bad_dir.sort();
    if (!res_good_dir.front().IsError()) {
        //We found a good way to connect!!
        //Now see if we need to set the endpoint of an unspecified ending.
        //If we fail, set res_good_dir.front() to an error, else to SETTLED
        if (res_good_dir.front().type==JoinableResult::NEED_UPDATE_FIRST) {
            bool success = a->SetEndingDueToJoin(res_good_dir.front().is_src.first, b->GetSidePos(res_good_dir.front().is_src.second), false);
            if (Error && !success) {
                Error->Error(a->file_pos.start, "This arrow becomes degenerate (zero length) if you join to it. Ignoring 'join'.");
                Error->Error(b->join_pos, a->file_pos.start, "The offending (and ignored) 'join' keyword.");
            }
            res_good_dir.front().type = success ? JoinableResult::SETTLED : JoinableResult::ERROR_OVERLAP;
        } else if (res_good_dir.front().type==JoinableResult::NEED_UPDATE_SECOND) {
            bool success = b->SetEndingDueToJoin(res_good_dir.front().is_src.second, a->GetSidePos(res_good_dir.front().is_src.first), false);
            if (Error && !success) {
                Error->Error(b->join_pos, "This arrow becomes degenerate (zero length) if you join it. Ignoring 'join'.");
            }
            res_good_dir.front().type = success ? JoinableResult::SETTLED : JoinableResult::ERROR_OVERLAP;
        } else if (res_good_dir.front().type==JoinableResult::CURLY_NEED_UPDATE_SECOND) {
            bool success = b->SetEndingDueToJoin(res_good_dir.front().is_src.second, a->GetSidePos(res_good_dir.front().is_src.first), false);
            if (Error && !success) {
                Error->Error(b->join_pos, "This arrow becomes degenerate (zero length) if you join it. Ignoring 'join'.");
            }
            res_good_dir.front().type = success ? JoinableResult::CURLY : JoinableResult::ERROR_OVERLAP;
        }
        //OK, now see that the other end of 'b' requires LEFT or RIGHT next element
        //Find if the good match is at front() of b_ends...?
        if (res_good_dir.front().is_src.second == b_ends.front().is_src)
            //...if so use the side from back() of b_ends
            res_good_dir.front().left_to_right = b_ends.back().body_is_left;
        else
            res_good_dir.front().left_to_right = b_ends.front().body_is_left;
    } else if (Error) { //we have not found a good result in the right direction, generate error msg
                        // try the reverse one (to provide the right error msg)
        if (res_bad_dir.size() && !res_bad_dir.front().IsError()) {
            //uh, we could connect to the other side...
            Error->Error(b->join_pos, "This element cannot be joined to the previous one, because something is already joined on its "
                +PrintEnum(Opposite(side))+ " side. Ignoring 'join' keyword.");
        } else switch (res_good_dir.front().type) {
        case JoinableResult::ERROR_BAD_END_FIRST:
            Error->Error(a->file_pos.start, res_good_dir.front().error_msg_no_join, res_good_dir.front().error_msg_no_join_add);
            Error->Error(b->join_pos, a->file_pos.start, "The offending (and ignored) 'join' keyword.");
            break;
        case JoinableResult::ERROR_BAD_END_SECOND:
            Error->Error(b->join_pos, res_good_dir.front().error_msg_no_join, res_good_dir.front().error_msg_no_join_add);
            break;
        case JoinableResult::ERROR_BOTH_UNSPECIFIED:
            Error->Error(b->join_pos, "You need to specify at least one of the endpoints to join two arrows. Ignoring 'join' keyword.");
            break;
        case JoinableResult::ERROR_FIRST_CAN_ONLY_TAKE_SPEC:
            Error->Error(b->join_pos, "You can only connect an unspecified arrow to this entity. Ignoring 'join' keyword.");
            Error->Error(b->join_pos, a->file_pos.start, "The offending (and ignored) 'join' keyword.");
            break;
        case JoinableResult::ERROR_SECOND_CAN_ONLY_TAKE_SPEC:
            Error->Error(b->join_pos, "You can only connect an unspecified arrow to this entity. Ignoring 'join' keyword.");
            break;
        default:
            _ASSERT(0); //we cannot have any other error from EndConnection::CanJoinTo().
            FALLTHROUGH;
        case JoinableResult::ERROR_OVERLAP:
            Error->Error(b->join_pos, "This element overlaps with the previous and cannot join. Ignoring 'join' keyword.");
            break;
        }
        //set to generic error, I am not sure why erase detailed error info. Well to make sure it is not used outside this func.
        res_good_dir.front().type = JoinableResult::ERROR_OVERLAP;
    }
    return res_good_dir.front();
}

ArcBase* JoinSeries::PostParseProcess(Canvas &canvas, bool hide,
    EntityRef &left, EntityRef &right,
    Numbering &number, MscElement **note_target, ArcBase * /*byref_target*/)
{
    //Copy the 'compress, parallel, overlap' of the first element
    //(This is the first function that is called after 'content' is filled up in MscChart::PostParseProcessArcList())
    //We also clear parallel and overlap of the members, so that they are never laid out overlaid
    if (content.size()) {
        vspacing = content.front()->IsCompressed() ? DBL_MIN : content.front()->GetVSpacing();
        overlap = content.front()->IsOverlap();
        parallel = content.front()->IsParallel();
        for (auto &a: content) {
            a->SetOverlap(false);
            a->SetParallel(true);
        }
    }
    for (ArcList::iterator a = content.begin(); a != content.end(); /*none*/) {
        ArcBase *replace = (*a)->PostParseProcess(canvas, hide, left, right, number, note_target, nullptr);
        Indicator *ai = dynamic_cast<Indicator*>(replace);
        if (ai && (*a)->RemoveIndicator()) {
            delete replace;
            replace = nullptr;
        } else if (ai && a!=content.begin()) {
            _ASSERT(replace != a->get()); //this is to make sure, 'replace' owns the object it points to, see above
            Indicator *ai_prev = dynamic_cast<Indicator*>(std::prev(a)->get());
            if (ai_prev && ai_prev->Combine(ai)) {
                delete replace;
                replace = nullptr;
            }
        }
        if (replace==nullptr)
            content.erase(a++);
        else {
            if (replace != a->get())
                a->reset(replace); //this kills the old element (we probably replace to Indicator)
            a++;
            chart->Progress.DoneItem(EArcSection::POST_PARSE, replace->myProgressCategory);
        }
    }
    if (content.size()==0) return nullptr; //we disappear, not even an indicator
    if (content.size()==1) return content.front().release(); //only one element left
    return this;
}

void JoinSeries::FinalizeLabels(Canvas &canvas)
{
    if (!valid) return;
    chart->FinalizeLabelsArcList(content, canvas);
}

void JoinSeries::Width(Canvas &canvas, DistanceRequirements &vdist)
{
    if (!valid) return;
    //First do boxes
    for (auto &pArc : content)
        if (dynamic_cast<BoxSeries*>(pArc.get())) {
            pArc->Width(canvas, vdist);
            chart->Progress.DoneItem(EArcSection::WIDTH, pArc->myProgressCategory);
        }

    //Now do the actual connecting
    //Here we create a series of arclists. Within each list we have non-curly joins, between the lists
    //we have curly joins.
    //During the process, we set the ends of arrows that need setting and connect curly joins.
    std::list<ArcList> lists(1);
    lists.back().emplace_back(content.front().release());
    std::list<std::list<std::pair<bool, bool>>> is_srcs(1);
    ESide side = ESide::END;
    for (auto a = ++content.begin(); a!=content.end(); a++) {
        JoinableResult res = CanJoinTo(lists.back().back().get(), a->get(), side);
        switch (res.type) {
        case JoinableResult::CURLY:
            a->get()->SetCurlyJoin(res.is_src.second, lists.back().back().get(), res.is_src.first);
            //start new series
            lists.emplace_back();
            is_srcs.emplace_back();
            break;
        case JoinableResult::SETTLED:
            //nothing to do
            break;
        case JoinableResult::ERROR_OVERLAP:
            //Too bad we get this.
#ifdef _DEBUG
            chart->Error.Error((*a)->join_pos, "Internal Error: Could not join in Width() due to entity collapse.");
#endif // DEBUG
            //start new series - in the lack of alternative options.
            lists.emplace_back();
            is_srcs.emplace_back();
            break;
        case JoinableResult::NEED_UPDATE_FIRST: //should never receive it
             //lists.back().back().get()->SetEndingDueToJoin(res.is_src.first, a->get()->GetSidePos(res.is_src.second), false);
        case JoinableResult::NEED_UPDATE_SECOND: //should never receive it
             //a->get()->SetEndingDueToJoin(res.is_src.second, lists.back().back().get()->GetSidePos(res.is_src.first), false);
        default: //all other errors
            _ASSERT(0);
            break;
        }
        lists.back().emplace_back(a->release());
        if (lists.back().size()>1)
            is_srcs.back().push_back(res.is_src); //for non curly joins, store which end of the elements we join.
        side = res.left_to_right ? ESide::RIGHT : ESide::LEFT; //side of the next element
    }
    //now all elements in 'content' have been moved to 'lists', so content only has empty pointers
    content.clear();

    //now go through the list again and do Width() for the non-boxes
    for (auto &l : lists)
        for (auto &a : l)
            if (dynamic_cast<BoxSeries*>(a.get())==nullptr) {
                a->Width(canvas, vdist);
                chart->Progress.DoneItem(EArcSection::WIDTH, a->myProgressCategory);
            }

    //Now repack everything into a neat bunch
    if (lists.size()==1) {
        //one list on top level - no curly join: this JoinSeries will represent a non-curly series...
        _ASSERT(lists.front().size()>1);
        _ASSERT(is_srcs.front().size());
        content.swap(lists.front());
        is_src.swap(is_srcs.front()); //...so we have is_src non-empty.
    } else {
        //Copy the lists
        auto i = is_srcs.begin();
        for (auto l = lists.begin(); l!=lists.end(); l++, i++)
            if (l->size()==0) {
                _ASSERT(0); continue;
            } else if (l->size()==1) {
                content.emplace_back(std::move(l->front()));
            } else {
                JoinSeries *js = new JoinSeries(chart, std::move(*l), std::move(*i));
                //set the compress of this join series to that of its first member
                js->vspacing = js->content.front()->IsCompressed() ? DBL_MIN : js->content.front()->GetVSpacing();
                js->SetOverlap(false);
                js->SetParallel(true);
                content.Append(js);
                chart->Progress.DoneItem(EArcSection::POST_PARSE, content.back()->myProgressCategory);
            }
    }
}

void JoinSeries::Layout(Canvas &canvas, AreaList *cover)
{
    //we dont actually have comments to JoinSeries
    if (!valid) return;
    if (content.size()==0) return;
    if (is_src.empty()) { //this means we store curly joins
        //for curly joins we can use standard one-below-the other layout
        height = chart->LayoutArcList(canvas, content, cover);
        return;
    }
    //for side-by-side joins, we have to align by centerline & take slant into account
    AreaList mycover;
    content.front()->Layout(canvas, cover);
    chart->Progress.DoneItem(EArcSection::LAYOUT, content.front()->myProgressCategory);
    //We ignore compress, parallel, vspace and other kind of shit here.
    auto is_src_i = is_src.begin();
    for (auto i = ++content.begin(); i!=content.end(); i++, is_src_i++) {
        (*i)->Layout(canvas, cover ? &mycover : nullptr);
        const double prev_centerline = std::prev(i)->get()->GetCenterline(is_src_i->first ? CenterlineSelector::AT_SRC : CenterlineSelector::AT_DST);
        const double my_centerline = i->get()->GetCenterline(is_src_i->second ? CenterlineSelector::AT_SRC : CenterlineSelector::AT_DST);
        (*i)->ShiftBy(prev_centerline - my_centerline);
        chart->Progress.DoneItem(EArcSection::LAYOUT, (*i)->myProgressCategory);
        if (cover) {
            mycover.Shift(XY(0, prev_centerline - my_centerline));
            *cover += std::move(mycover);
            mycover.clear();
        }
    }
    //Some elements may have ended up above y=0
    Range yrange(false);
    for (auto &a : content)
        yrange += a->GetFormalYExtent();
    if (yrange.from) {
        chart->ShiftByArcList(content, -yrange.from);
        if (cover)
            cover->Shift(XY(0, -yrange.from));
    }
    height = yrange.Spans();
}

void JoinSeries::FinalizeLayout(Canvas & canvas, AreaList * cover)
{
    if (is_src.size()) //For curly joins we have called FinalizeLayout() in LayoutArcList above
        for (auto &a: content) //for non-curly joins Call FinalizeLayout for all members.
            a->FinalizeLayout(canvas, cover);
}



Range JoinSeries::GetVisualYExtent(bool include_comments) const
{
    Range ret(false);
    if (valid)
        for (auto &pArc : content)
            ret += pArc->GetVisualYExtent(include_comments);
    if (include_comments && valid)
        ret += yPos+comment_height;
    return ret;
}


void JoinSeries::ShiftBy(double y)
{
    if (!valid) return;
    ArcBase::ShiftBy(y);
    chart->ShiftByArcList(content, y);
}

void JoinSeries::CollectPageBreak()
{
    if (!valid) return;
    chart->CollectPageBreakArcList(content);
    ///There should be no page breaks in this list - but we do it nevertheless...
}

double JoinSeries::SplitByPageBreak(Canvas &/*canvas*/, double /*netPrevPageSize*/,
                                    double /*pageBreak*/, bool&/*addCommandNewpage*/,
                                    bool /*addHeading*/, ArcList&/*res*/)
{
    //never break inside a join series - even though boxes may have content, which would allow that.
    return -1;
}

void JoinSeries::PlaceWithMarkers(Canvas &canvas)
{
    if (!valid) return;
    //Boxes may have such elements as content that need to be placed after markers...
    chart->PlaceWithMarkersArcList(canvas, content);
}

void JoinSeries::PostPosProcess(Canvas &canvas, Chart *ch)
{
    if (!valid) return;
    chart->PostPosProcessArcList(canvas, content, ch);
}

void JoinSeries::RegisterLabels()
{
    if (!valid) return;
    chart->RegisterLabelArcList(content);
}

void JoinSeries::CollectIsMapElements(Canvas &canvas)
{
    if (!valid) return;
    chart->CollectIsMapElementsArcList(content, canvas);
}

void JoinSeries::RegisterCover(EMscDrawPass pass) const
{
    if (!valid) return;
    chart->RegisterCoverArcList(content, pass);
}

void JoinSeries::Draw(Canvas &canvas, EMscDrawPass pass) const
{
    if (!valid) return;
    chart->DrawArcList(canvas, content, chart->GetTotal().y, pass);
}



