opensheetmusicdisplay/opensheetmusicdisplay

View on GitHub
src/MusicalScore/ScoreIO/MusicSymbolModules/RepetitionInstructionReader.ts

Summary

Maintainability
F
6 days
Test Coverage
import {MusicSheet} from "../../MusicSheet";
import {IXmlElement} from "../../../Common/FileIO/Xml";
import {SourceMeasure} from "../../VoiceData/SourceMeasure";
import {RepetitionInstruction, RepetitionInstructionEnum, AlignmentType} from "../../VoiceData/Instructions/RepetitionInstruction";
import {RepetitionInstructionComparer} from "../../VoiceData/Instructions/RepetitionInstruction";
import {StringUtil} from "../../../Common/Strings/StringUtil";
export class RepetitionInstructionReader {
  /**
   * A global list of all repetition instructions in the musicsheet.
   */
  public repetitionInstructions: RepetitionInstruction[];
  public xmlMeasureList: IXmlElement[][];
  private musicSheet: MusicSheet;
  private currentMeasureIndex: number;

  public set MusicSheet(value: MusicSheet) {
    this.musicSheet = value;
    this.xmlMeasureList = new Array(this.musicSheet.Instruments.length);
    this.repetitionInstructions = [];
  }

  /**
   * is called when starting reading an xml measure
   * @param measure
   * @param currentMeasureIndex
   */
  public prepareReadingMeasure(measure: SourceMeasure, currentMeasureIndex: number): void {
    this.currentMeasureIndex = currentMeasureIndex;
  }

  public handleLineRepetitionInstructions(barlineNode: IXmlElement): boolean {
    let pieceEndingDetected: boolean = false;
    if (barlineNode.elements().length > 0) {
      let location: string = "";
      let hasRepeat: boolean = false;
      let direction: string = "";
      let type: string = "";
      let style: string = "";
      const endingIndices: number[] = [];

      // read barline style
      const styleNode: IXmlElement = barlineNode.element("bar-style");

      // if location is ommited in Xml, right is implied (from documentation)
      if (styleNode) {
        style = styleNode.value;
      }
      if (barlineNode.attributes().length > 0 && barlineNode.attribute("location")) {
        location = barlineNode.attribute("location").value;
      } else {
        location = "right";
      }
      const barlineNodeElements: IXmlElement[] = barlineNode.elements();

      // read repeat- or ending line information
      for (let idx: number = 0, len: number = barlineNodeElements.length; idx < len; ++idx) {
        const childNode: IXmlElement = barlineNodeElements[idx];
        if ("repeat" === childNode.name && childNode.hasAttributes) {
          hasRepeat = true;
          direction = childNode.attribute("direction").value;
        } else if ( "ending" === childNode.name && childNode.hasAttributes &&
                    childNode.attribute("type") !== undefined && childNode.attribute("number")) {
          if (childNode.attribute("print-object")?.value === "no") {
            continue;
            // Finale only puts print-object="no" at the (duplicated) <ending> node at thestart of measure barline,
            //   making this uneffective. Unclear whether the intention is to render the volta or not. (See #1367)
          }
          type = childNode.attribute("type").value;
          let num: string = childNode.attribute("number").value;
          if (childNode.value) {
            num = childNode.value;
            // MusicXML spec: "The element text is used when the text displayed in the ending is different than what appears in the number attribute."
            //   Finale v27.3 accordingly seems to put the desired printed number here instead of in "number" (#1367)
          }

          // Parse the given ending indices:
          // handle cases like: "1, 2" or "1 + 2" or even "1 - 3, 6"
          const separatedEndingIndices: string[] = num.split("[,+]");
          for (let idx2: number = 0, len2: number = separatedEndingIndices.length; idx2 < len2; ++idx2) {
            const separatedEndingIndex: string = separatedEndingIndices[idx2];
            const indices: string[] = separatedEndingIndex.match("[0-9]");

            // check if possibly something like "1-3" is given..
            if (separatedEndingIndex.search("-") !== -1 && indices.length === 2) {
              const startIndex: number = parseInt(indices[0], 10);
              const endIndex: number = parseInt(indices[1], 10);
              for (let index: number = startIndex; index <= endIndex; index++) {
                endingIndices.push(index);
              }
            } else {
              for (let idx3: number = 0, len3: number = indices.length; idx3 < len3; ++idx3) {
                const index: string = indices[idx3];
                endingIndices.push(parseInt(index, 10));
              }
            }
          }
        }
      }

      // reset measure counter if not lastMeasure
      if (style === "light-heavy" && endingIndices.length === 0 && !hasRepeat) {
        pieceEndingDetected = true;
      }
      if (hasRepeat || endingIndices.length > 0) {
        if (location === "left") {
          if (type === "start") {
            const newInstruction: RepetitionInstruction = new RepetitionInstruction(this.currentMeasureIndex, RepetitionInstructionEnum.Ending,
                                                                                    AlignmentType.Begin, undefined, endingIndices);
            this.addInstruction(this.repetitionInstructions, newInstruction);
          }
          if (direction === "forward") {
            // start new Repetition
            const newInstruction: RepetitionInstruction = new RepetitionInstruction(this.currentMeasureIndex, RepetitionInstructionEnum.StartLine);
            this.addInstruction(this.repetitionInstructions, newInstruction);
          }
        } else { // location right
          if (type === "stop") {
            const newInstruction: RepetitionInstruction = new RepetitionInstruction(this.currentMeasureIndex, RepetitionInstructionEnum.Ending,
                                                                                    AlignmentType.End, undefined, endingIndices);
            this.addInstruction(this.repetitionInstructions, newInstruction);
          }
          if (direction === "backward") {
            const newInstruction: RepetitionInstruction = new RepetitionInstruction(this.currentMeasureIndex, RepetitionInstructionEnum.BackJumpLine);
            this.addInstruction(this.repetitionInstructions, newInstruction);
          }
        }
      }
    }
    return pieceEndingDetected;
  }

  public handleRepetitionInstructionsFromWordsOrSymbols(directionTypeNode: IXmlElement, relativeMeasurePosition: number): boolean {
    const wordsNode: IXmlElement = directionTypeNode.element("words");
    const measureIndex: number = this.currentMeasureIndex;
    if (wordsNode) {
      const dsRegEx: string = "d\\s?\\.s\\."; // Input for new RegExp(). TS eliminates the first \
      // must Trim string and ToLower before compare
      const innerText: string = wordsNode.value.trim().toLowerCase();
      if (StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + " al fine", true)) {
        // @correctness i don't think we should manipulate the measure index by relative position [ssch]
        //   it's clearly assigned a measure in the xml
        //   this has misfired in the past, see test_staverepetitions_coda_etc_positioning.musicxml
        //   there, it put the 'To Coda' in measure 1, same as the 'Signo', which was not correct.
        // if (relativeMeasurePosition < 0.5 && this.currentMeasureIndex < this.xmlMeasureList[0].length - 1) { // not in last measure
        //   measureIndex--;
        // }
        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.DalSegnoAlFine);
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
      const dcRegEx: string = "d\\.\\s?c\\.";
      if (StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al coda", true)) {
        // if (relativeMeasurePosition < 0.5) {
        //   measureIndex--;
        // }
        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.DaCapoAlCoda);
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
      if (StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al fine", true)) {
        // if (relativeMeasurePosition < 0.5 && this.currentMeasureIndex < this.xmlMeasureList[0].length - 1) { // not in last measure
        //   measureIndex--;
        // }
        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.DaCapoAlFine);
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
      if (StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al coda", true)) {
        // if (relativeMeasurePosition < 0.5) {
        //   measureIndex--;
        // }
        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.DaCapoAlCoda);
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
      if (StringUtil.StringContainsSeparatedWord(innerText, dcRegEx) ||
        StringUtil.StringContainsSeparatedWord(innerText, "da\\s?capo", true)) {
        // if (relativeMeasurePosition < 0.5 && this.currentMeasureIndex < this.xmlMeasureList[0].length - 1) { // not in last measure
        //   measureIndex--;
        // }
        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.DaCapo);
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
      if (StringUtil.StringContainsSeparatedWord(innerText, dsRegEx, true) ||
        StringUtil.StringContainsSeparatedWord(innerText, "dal\\s?segno", true)) {
        // if (relativeMeasurePosition < 0.5 && this.currentMeasureIndex < this.xmlMeasureList[0].length - 1) { // not in last measure
        //   measureIndex--;
        // }
        let newInstruction: RepetitionInstruction;
        if (StringUtil.StringContainsSeparatedWord(innerText, "al\\s?coda", true)) {
          newInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.DalSegnoAlCoda);
        } else {
          newInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.DalSegno);
        }
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
      if (StringUtil.StringContainsSeparatedWord(innerText, "to\\s?coda", true) ||
        StringUtil.StringContainsSeparatedWord(innerText, "a (la )?coda", true)) {
        // if (relativeMeasurePosition < 0.5) {
        //   measureIndex--;
        // }
        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.ToCoda);
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
      if (StringUtil.StringContainsSeparatedWord(innerText, "fine", true)) {
        // if (relativeMeasurePosition < 0.5) {
        //   measureIndex--;
        // }
        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.Fine);
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
      if (StringUtil.StringContainsSeparatedWord(innerText, "coda", true)) {
        // if (relativeMeasurePosition > 0.5) {
        //   measureIndex++;
        // }
        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.Coda);
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
      if (StringUtil.StringContainsSeparatedWord(innerText, "segno", true)) {
        // if (relativeMeasurePosition > 0.5) {
        //   measureIndex++;
        // }
        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.Segno);
        this.addInstruction(this.repetitionInstructions, newInstruction);
        return true;
      }
    } else if (directionTypeNode.element("segno")) {
      // if (relativeMeasurePosition > 0.5) {
      //   measureIndex++;
      // }
      const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.Segno);
      this.addInstruction(this.repetitionInstructions, newInstruction);
      return true;
    } else if (directionTypeNode.element("coda")) {
      // if (relativeMeasurePosition > 0.5) {
      //   measureIndex++;
      // }
      const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.Coda);
      this.addInstruction(this.repetitionInstructions, newInstruction);
      return true;
    }
    return false;
  }

  public removeRedundantInstructions(): void {
    let segnoCount: number = 0;
    let codaCount: number = 0;
    //const fineCount: number = 0;
    let toCodaCount: number = 0;
    let dalSegnaCount: number = 0;
    for (let index: number = 0; index < this.repetitionInstructions.length; index++) {
      const instruction: RepetitionInstruction = this.repetitionInstructions[index];
      switch (instruction.type) {
        case RepetitionInstructionEnum.Coda:
          if (toCodaCount > 0) {
            if (this.findInstructionInPreviousMeasure(index, instruction.measureIndex, RepetitionInstructionEnum.ToCoda)) {
              instruction.type = RepetitionInstructionEnum.None;
            }
          }
          // TODO this prevents a piece consisting of a single coda sign showing coda (will show To Coda)
          // if (codaCount === 0 && toCodaCount === 0) {
          //   instruction.type = RepetitionInstructionEnum.ToCoda;
          //   instruction.alignment = AlignmentType.End;
          //   instruction.measureIndex--;
          // }
          break;
        case RepetitionInstructionEnum.Segno:
          if (segnoCount - dalSegnaCount > 0) { // two segnos in a row
            let foundInstruction: boolean = false;
            for (let idx: number = 0, len: number = this.repetitionInstructions.length; idx < len; ++idx) {
              const instr: RepetitionInstruction = this.repetitionInstructions[idx];
              if (instruction.measureIndex - instr.measureIndex === 1) {
                switch (instr.type) {
                  case RepetitionInstructionEnum.BackJumpLine:
                    if (toCodaCount - codaCount > 0) { // open toCoda existing
                      instr.type = RepetitionInstructionEnum.DalSegnoAlCoda;
                    } else {
                      instr.type = RepetitionInstructionEnum.DalSegno;
                    }
                    instruction.type = RepetitionInstructionEnum.None;
                    foundInstruction = true;
                    break;
                  case RepetitionInstructionEnum.DalSegno:
                  case RepetitionInstructionEnum.DalSegnoAlFine:
                  case RepetitionInstructionEnum.DalSegnoAlCoda:
                    instruction.type = RepetitionInstructionEnum.None;
                    foundInstruction = true;
                    break;
                  default:
                    break;
                }
              }
              if (foundInstruction) {
                break;
              }
            }
            if (foundInstruction) {
              break;
            }
            // convert to dal segno instruction:
            if (toCodaCount - codaCount > 0) { // open toCoda existing
              instruction.type = RepetitionInstructionEnum.DalSegnoAlCoda;
            } else {
              instruction.type = RepetitionInstructionEnum.DalSegno;
            }
            instruction.alignment = AlignmentType.End;
            instruction.measureIndex--;
          }
          break;
        default:
          break;
      }

      // check if this  instruction already exists or is otherwise redundant:
      if (this.backwardSearchForPreviousIdenticalInstruction(index, instruction) || instruction.type === RepetitionInstructionEnum.None) {
        this.repetitionInstructions.splice(index, 1);
        index--;
      } else {
        switch (instruction.type) {
          case RepetitionInstructionEnum.Fine:
            //fineCount++;
            break;
          case RepetitionInstructionEnum.ToCoda:
            toCodaCount++;
            break;
          case RepetitionInstructionEnum.Coda:
            codaCount++;
            break;
          case RepetitionInstructionEnum.Segno:
            segnoCount++;
            break;
          case RepetitionInstructionEnum.DalSegnoAlFine:
          case RepetitionInstructionEnum.DalSegnoAlCoda:
            dalSegnaCount++;
            break;
          default:
            break;
        }
      }
    }
    this.repetitionInstructions.sort(RepetitionInstructionComparer.Compare);
  }

  private findInstructionInPreviousMeasure(currentInstructionIndex: number, currentMeasureIndex: number, searchedType: RepetitionInstructionEnum): boolean {
    for (let index: number = currentInstructionIndex - 1; index >= 0; index--) {
      const instruction: RepetitionInstruction = this.repetitionInstructions[index];
      if (currentMeasureIndex - instruction.measureIndex === 1 && instruction.type === searchedType) {
        return true;
      }
    }
    return false;
  }

  private backwardSearchForPreviousIdenticalInstruction(currentInstructionIndex: number, currentInstruction: RepetitionInstruction): boolean {
    for (let index: number = currentInstructionIndex - 1; index >= 0; index--) {
      const instruction: RepetitionInstruction = this.repetitionInstructions[index];
      if (instruction.equals(currentInstruction)) {
        return true;
      }
    }
    return false;
  }

  private addInstruction(currentRepetitionInstructions: RepetitionInstruction[], newInstruction: RepetitionInstruction): void {
    let addInstruction: boolean = true;
    for (let idx: number = 0, len: number = currentRepetitionInstructions.length; idx < len; ++idx) {
      const repetitionInstruction: RepetitionInstruction = currentRepetitionInstructions[idx];
      if (newInstruction.equals(repetitionInstruction)) {
        addInstruction = false;
        break;
      }
    }
    if (addInstruction) {
      currentRepetitionInstructions.push(newInstruction);
    }
  }
}