diff --git a/adapter/diagnostic.go b/adapter/diagnostic.go new file mode 100644 index 0000000..f16340e --- /dev/null +++ b/adapter/diagnostic.go @@ -0,0 +1,152 @@ +package adapter + +import ( + "encoding/json" + "io" + "log" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" + + svg "github.com/ajstarks/svgo" + "github.com/google/uuid" + "github.com/jodi-ivan/numbered-notation-xml/internal/utils" + "github.com/jodi-ivan/numbered-notation-xml/svc/repository" + "github.com/jodi-ivan/numbered-notation-xml/svc/usecase" + "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" + "github.com/julienschmidt/httprouter" +) + +type DiagnosticHTTP struct { + Usecase usecase.Usecase + Interrupt chan os.Signal + Repo repository.Repository +} + +// SSEvent represents a single server-sent event packet. +type SSEvent struct { + ID string `json:"id"` // Optional: Unique identifier for the event + Event string `json:"event"` // Optional: Custom event name (e.g., "update", "chat_message") + Data []byte `json:"data"` // Required: The main payload + Retry int `json:"retry"` // Optional: Reconnection timeout value in milliseconds +} + +func (dh *DiagnosticHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + target := svg.New(io.Discard) + + canv := canvas.NewCanvasWithDelegator(target, &CanvasDelegator{}) + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.WriteHeader(http.StatusOK) + + no, vars, err := utils.ParseHymnWithVariant(ps.ByName("scope")) + if err != nil { + w.Write([]byte("Invalid param" + err.Error())) + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + return + } + + mode := r.FormValue("focus") + + focusMode, err := strconv.ParseBool(mode) + if mode != "" && err != nil { + log.Printf("[ServeHTTP] invalid mode: %v", err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid URL")) + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + return + } + + timeout := time.NewTimer(2 * time.Second) + defer timeout.Stop() + + dig := ¶ms.DiagParam{ + VerseSyllMatch: make(chan map[int]bool, 1), + VerseDiagnostic: make(chan params.VerseDiagnostic), + Finish: make(chan bool, 1), + Mu: &sync.RWMutex{}, + MapMtx: &sync.Map{}, + } + + go func() { + prm := ¶ms.Param{ + Verse: 3, // hardcoded to trigger the load verse mechanism + SingleVerseMode: focusMode, + Diagnostic: dig, + } + rctx := params.NewParamContext(r.Context(), prm) + v := []string{} + if vars != "" { + v = append(v, vars) + } + err := dh.Usecase.RenderHymn(rctx, canv, no, v...) + log.Println("try render", err) + }() + + for { + select { + + case <-timeout.C: + log.Println("timeout") + body := []string{ + "id: " + uuid.NewString() + "\n", + "event: close\n", + "data: stream_finished \n\n", + } + // body, _ := json.Marshal(SSEvent{ + // ID: uuid.NewString(), + // Event: "data", + // Data: s, + // }) + + w.Write([]byte(strings.Join(body, ""))) + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + return + + case intr := <-dh.Interrupt: + log.Println("inside the server", intr) + return + case <-dig.VerseDiagnostic: + + syncMap := map[int]interface{}{} + dig.MapMtx.Range(func(key, value any) bool { + syncMap[key.(int)] = value + return true + }) + + s, _ := json.Marshal(syncMap) + body := []string{ + "id: " + uuid.NewString() + "\n", + "event: data\n", + "data: " + string(s) + "\n\n", + } + + w.Write([]byte(strings.Join(body, ""))) + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + // time.Sleep(1 * time.Second) + // timeout.Reset(time.Millisecond * 100) + + // case <-dig.Finish: + // log.Println("finishing") + // // time.Sleep(1 * time.Second) + // if flusher, ok := w.(http.Flusher); ok { + // flusher.Flush() + // } + // return + } + } +} diff --git a/adapter/render_to_string.go b/adapter/render_to_string.go index be42a47..038e964 100644 --- a/adapter/render_to_string.go +++ b/adapter/render_to_string.go @@ -9,12 +9,12 @@ import ( "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" ) -type RenderStringCanvasDelegator struct{} +type CanvasDelegator struct{} -func (rscd *RenderStringCanvasDelegator) OnBeforeStartWrite() { +func (rscd *CanvasDelegator) OnBeforeStartWrite() { // no pre operation needed for string operation } -func (rscd *RenderStringCanvasDelegator) OnError(err error) canvas.DelegatorErrorFlowControl { +func (rscd *CanvasDelegator) OnError(err error) canvas.DelegatorErrorFlowControl { return canvas.DelegatorErrorFlowControlStop } @@ -30,7 +30,7 @@ func NewRenderString(u usecase.Usecase) *RenderString { } func (rs *RenderString) RenderHymn(ctx context.Context, buf *bytes.Buffer, number int, variant ...string) (string, error) { - canv := canvas.NewCanvasWithDelegator(svg.New(buf), &RenderStringCanvasDelegator{}) + canv := canvas.NewCanvasWithDelegator(svg.New(buf), &CanvasDelegator{}) err := rs.usecase.RenderHymn(ctx, canv, number, variant...) if err != nil { return "", err diff --git a/adapter/renderer.go b/adapter/renderer.go index a75b4f5..7c2428d 100644 --- a/adapter/renderer.go +++ b/adapter/renderer.go @@ -10,6 +10,7 @@ import ( "github.com/jodi-ivan/numbered-notation-xml/svc/repository" "github.com/jodi-ivan/numbered-notation-xml/svc/usecase" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" "github.com/julienschmidt/httprouter" ) @@ -30,8 +31,8 @@ func (cdh *CanvasDelegatorHTTP) OnError(err error) canvas.DelegatorErrorFlowCont } else if errors.Is(err, repository.ErrHymnHasMoreThanOneVariant) { // Perform the redirect - http.Redirect(cdh.w, cdh.r, cdh.r.URL.Path+"a", http.StatusSeeOther) - + cdh.r.URL.Path += "a" + http.Redirect(cdh.w, cdh.r, cdh.r.URL.RequestURI(), http.StatusSeeOther) return canvas.DelegatorErrorFlowControlStop } @@ -60,14 +61,14 @@ func (rh *RenderHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httpr num, err := strconv.Atoi(raw) if err != nil { if len(raw) == 0 { - log.Printf("invalid number: %v", err.Error()) + log.Printf("[ServeHTTP] invalid number: %v", err.Error()) w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Invalid URL")) return } num, err = strconv.Atoi(raw[0 : len(raw)-1]) if err != nil { - log.Printf("invalid number: %v", err.Error()) + log.Printf("[ServeHTTP] invalid number: %v", err.Error()) w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Invalid URL")) return @@ -75,7 +76,32 @@ func (rh *RenderHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httpr variant = []string{string(raw[len(raw)-1])} } - err = rh.usecase.RenderHymn(r.Context(), canv, num, variant...) + verseRaw := r.FormValue("verse") + + verseNo, err := strconv.Atoi(verseRaw) + if verseRaw != "" && err != nil { + log.Printf("[ServeHTTP] invalid verse: %v", err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid URL")) + return + } + + mode := r.FormValue("focus") + + focusMode, err := strconv.ParseBool(mode) + if mode != "" && err != nil { + log.Printf("[ServeHTTP] invalid mode: %v", err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid URL")) + return + } + + prm := ¶ms.Param{ + Verse: verseNo, + SingleVerseMode: focusMode, + } + + err = rh.usecase.RenderHymn(params.NewParamContext(r.Context(), prm), canv, num, variant...) if err != nil { delegator.OnError(err) } diff --git a/cmd/rest/app.go b/cmd/rest/app.go index e406ebc..6d73be7 100644 --- a/cmd/rest/app.go +++ b/cmd/rest/app.go @@ -63,6 +63,14 @@ func main() { Db: db, }) + sigs := make(chan os.Signal, 1) + + ws.Register("GET", "/internal/diagnostic/verse/:scope", &adapter.DiagnosticHTTP{ + Usecase: usecaseMod, + Interrupt: sigs, + Repo: repo, + }) + err = ws.Serve(cfg.Webserver.Port) if err != nil { log.Printf("Failed to start the server. Err: %s", err.Error()) @@ -70,7 +78,6 @@ func main() { return } - sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) log.Println(<-sigs) diff --git a/diagnostics/verse_diag.go b/diagnostics/verse_diag.go new file mode 100644 index 0000000..e01e95f --- /dev/null +++ b/diagnostics/verse_diag.go @@ -0,0 +1,97 @@ +package diagnostics + +import ( + "context" + "log" + "time" + + "github.com/jodi-ivan/numbered-notation-xml/internal/entity" + "github.com/jodi-ivan/numbered-notation-xml/internal/musicxml" + "github.com/jodi-ivan/numbered-notation-xml/internal/verse" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" +) + +var verseDiagnostic *VerseDiagnostic + +type VerseDiagnostic struct { + Matcher verse.SyllableMatch +} + +func (vd *VerseDiagnostic) IsVowel(char rune) bool { + return vd.Matcher.IsVowel(char) +} +func (vd *VerseDiagnostic) ApplyElision(syllText string, combine bool) []musicxml.LyricText { + return vd.Matcher.ApplyElision(syllText, combine) +} +func (vd *VerseDiagnostic) LoadOtherVerse(ctx context.Context, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, offset map[int]int, prevRepeatInfos []*musicxml.RepeatInfo) (map[int]int, int) { + all := map[int]int{} + // singleResult := map[int]bool{} + + timeout := time.NewTimer(10 * time.Second) + defer timeout.Stop() + + prm, _ := params.GetParamFromContext(ctx) + go func() { + + for { + select { + case data := <-prm.Diagnostic.VerseSyllMatch: + currentData := data + prm.Diagnostic.Mu.RLock() + for k, v := range currentData { + prm.Diagnostic.MapMtx.Store(k, v) + } + + prm.Diagnostic.Mu.RUnlock() + + prm.Diagnostic.VerseDiagnostic <- params.VerseDiagnostic{ + SingleMode: currentData, + } + + case <-timeout.C: + log.Println("timeout") + + prm.Diagnostic.Finish <- true + return + + } + + } + }() + + allOffset := map[int]map[int]int{} + for i := 2; i <= len(metadata.Verse)+1; i++ { + newParam := ¶ms.Param{ + Verse: i, + SingleVerseMode: prm.SingleVerseMode, + Diagnostic: prm.Diagnostic, + } + + rctx := params.NewParamContext(ctx, newParam) + allOffset[i], _ = vd.Matcher.LoadOtherVerse(rctx, notes, metadata, startPos, offset, prevRepeatInfos) + + for i, v := range allOffset { + for _, off := range v { + all[i] = off + } + } + + } + // prm.Diagnostic.Finish <- true + + return all, 0 + +} +func (vd *VerseDiagnostic) LoadVerse(ctx context.Context, targetVerse int, clear bool, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, prevRepeatInfos []*musicxml.RepeatInfo) (int, int) { + return vd.Matcher.LoadVerse(ctx, targetVerse, clear, notes, metadata, startPos, prevRepeatInfos) +} + +func GetVerseDiagnostic(matcher verse.SyllableMatch) *VerseDiagnostic { + if verseDiagnostic == nil { + verseDiagnostic = &VerseDiagnostic{ + Matcher: matcher, + } + } + + return verseDiagnostic +} diff --git a/files/var/www/html/viewer.html b/files/var/www/html/viewer.html new file mode 100644 index 0000000..e1171c6 --- /dev/null +++ b/files/var/www/html/viewer.html @@ -0,0 +1,182 @@ + + + + + + SVG Local Viewer + + + + + +

Local SVG Navigator

+
Current: file-001.svg
+ +
+ + +
+
+ +
+ + + +
+
+
+ + + +
+ + + + + + \ No newline at end of file diff --git a/internal/entity/metadata.go b/internal/entity/metadata.go index c6b71f1..4f18371 100644 --- a/internal/entity/metadata.go +++ b/internal/entity/metadata.go @@ -15,6 +15,7 @@ type LyricPartVerse struct { Type musicxml.LyricSyllabic `json:"type"` Combine bool `json:"combine"` Breakdown []LyricStylePart `json:"breakdown"` + Offset int `json:"offset"` } type LyricWordVerse struct { diff --git a/internal/lyric/coloring.go b/internal/lyric/coloring.go new file mode 100644 index 0000000..13e6181 --- /dev/null +++ b/internal/lyric/coloring.go @@ -0,0 +1,14 @@ +package lyric + +var coloringOpacity = map[int]string{ + 0: `style="opacity:0.6"`, +} + +func getColoringStyle(verse, totalLyric int) string { + if totalLyric == 1 { + return "" + } + + return coloringOpacity[verse] + +} diff --git a/internal/lyric/elision.go b/internal/lyric/elision.go index 8e00891..e97a934 100644 --- a/internal/lyric/elision.go +++ b/internal/lyric/elision.go @@ -2,24 +2,29 @@ package lyric import ( "context" + "strings" "github.com/jodi-ivan/numbered-notation-xml/internal/entity" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" ) -func (li *lyricInteractor) RenderElision(ctx context.Context, canv canvas.Canvas, text []entity.Text, lyricPart int, pos entity.Coordinate) { +func (li *lyricInteractor) RenderElision(ctx context.Context, canv canvas.Canvas, text []entity.Text, lyricPart int, pos entity.Coordinate, style ...string) { offsetLyric := "" yPos := int(pos.Y) + 2 for _, t := range text { if t.Underline == 1 { + styleStr := "fill:none;stroke:#000000;stroke-linecap:round;stroke-width:1.1;" + if len(style) > 0 { + styleStr += strings.Join(style, ";") + } currTextLength := li.CalculateLyricWidth(t.Value) offset := li.CalculateLyricWidth(offsetLyric) canv.Qbez( int(pos.X+offset), yPos, int(pos.X+offset+(currTextLength/2)), yPos+6, int(pos.X+offset+currTextLength), yPos, - "fill:none;stroke:#000000;stroke-linecap:round;stroke-width:1.1", + styleStr, ) } else { offsetLyric += t.Value diff --git a/internal/lyric/hypen.go b/internal/lyric/hypen.go index 631b483..4bd0565 100644 --- a/internal/lyric/hypen.go +++ b/internal/lyric/hypen.go @@ -8,9 +8,10 @@ import ( "github.com/jodi-ivan/numbered-notation-xml/internal/entity" "github.com/jodi-ivan/numbered-notation-xml/internal/musicxml" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" ) -func (li *lyricInteractor) CalculateHypen(ctx context.Context, prevLyric, currentLyric *LyricPosition) (location []entity.Coordinate) { +func (li *lyricInteractor) CalculateHypen(ctx context.Context, prevLyric, currentLyric *LyricPosition) (location []HyphenPosition) { if prevLyric.Lyrics.Syllabic == musicxml.LyricSyllabicTypeEnd || prevLyric.Lyrics.Syllabic == musicxml.LyricSyllabicTypeSingle { return nil @@ -28,8 +29,12 @@ func (li *lyricInteractor) CalculateHypen(ctx context.Context, prevLyric, curren // force add hyphen at the end if the lyric near the end margin if endPostion == float64(constant.LAYOUT_WIDTH-constant.LAYOUT_INDENT_LENGTH) { - return []entity.Coordinate{ - entity.NewCoordinate(startPosition+2, currentLyric.Coordinate.Y), + return []HyphenPosition{ + { + Coordinate: entity.NewCoordinate(startPosition+2, currentLyric.Coordinate.Y), + Verse: currentLyric.Lyrics.Verse, + TotalLyric: currentLyric.TotalLyric, + }, } } return nil @@ -42,21 +47,36 @@ func (li *lyricInteractor) CalculateHypen(ctx context.Context, prevLyric, curren if offset < 0 { offset = 0 } - return []entity.Coordinate{ - entity.NewCoordinate(startPosition+offset, currentLyric.Coordinate.Y), + return []HyphenPosition{ + { + Coordinate: entity.NewCoordinate(startPosition+offset, currentLyric.Coordinate.Y), + Verse: currentLyric.Lyrics.Verse, + TotalLyric: currentLyric.TotalLyric, + }, } } else { - result := []entity.Coordinate{} + result := []HyphenPosition{} totalContainer := math.Ceil((distance - (2 * hypenWidth)) / float64(container)) totalHypen := (totalContainer * 2) if lyricText == "" { - result = append(result, entity.NewCoordinate(HYPHEN_LEFT_INDENT, currentLyric.Coordinate.Y)) + result = append(result, + HyphenPosition{ + Coordinate: entity.NewCoordinate(HYPHEN_LEFT_INDENT, currentLyric.Coordinate.Y), + Verse: currentLyric.Lyrics.Verse, + TotalLyric: currentLyric.TotalLyric, + }, + ) totalHypen += 1 } startPosition += (distance / totalHypen) for i := float64(0); i < totalHypen-1; i++ { result = append(result, - entity.NewCoordinate(startPosition+(i*(distance/totalHypen)), currentLyric.Coordinate.Y)) + HyphenPosition{ + Coordinate: entity.NewCoordinate(startPosition+(i*(distance/totalHypen)), currentLyric.Coordinate.Y), + Verse: currentLyric.Lyrics.Verse, + TotalLyric: currentLyric.TotalLyric, + }) + } return result @@ -72,7 +92,7 @@ func (li *lyricInteractor) RenderHypen(ctx context.Context, y, offsetCenter int, hs := NewHypenStack() baseYPos := map[int]float64{} var lastLyric []entity.Lyric - hypenLocation := []entity.Coordinate{} + hypenLocation := []HyphenPosition{} hasLyricBefore := false // filter notes that has lyric only notes := []*entity.NoteRenderer{} @@ -174,6 +194,9 @@ func (li *lyricInteractor) RenderHypen(ctx context.Context, y, offsetCenter int, } } if pair[1] == nil { + if len(l.Text) == 0 || (len(l.Text) == 1 && l.Text[0].Value == "") { + continue + } pair[1] = &LyricPosition{ TotalLyric: len(n.Lyric), Coordinate: entity.NewCoordinate(float64(n.PositionX), pair[0].Coordinate.Y), @@ -208,9 +231,16 @@ func (li *lyricInteractor) RenderHypen(ctx context.Context, y, offsetCenter int, } } } + + prm, _ := params.GetParamFromContext(ctx) + canv.Group("hyphens") for _, hl := range hypenLocation { - canv.TextUnescaped(hl.X, hl.Y, "-") // use the Unescaped because of the floating number + sty := getColoringStyle(hl.Verse, hl.TotalLyric) + if prm.Verse < 2 { + sty = "" + } + canv.TextUnescaped(hl.Coordinate.X, hl.Coordinate.Y, "-", sty) // use the Unescaped because of the floating number } canv.Gend() } diff --git a/internal/lyric/lyric.go b/internal/lyric/lyric.go index 872a580..2953b33 100644 --- a/internal/lyric/lyric.go +++ b/internal/lyric/lyric.go @@ -12,6 +12,7 @@ import ( "github.com/jodi-ivan/numbered-notation-xml/internal/entity" "github.com/jodi-ivan/numbered-notation-xml/internal/musicxml" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" ) var ( @@ -33,9 +34,9 @@ func init() { type Lyric interface { CalculateLyricWidth(string) float64 SetLyricRenderer(noteRenderer *entity.NoteRenderer, rawLyric []musicxml.Lyric) VerseInfo - CalculateHypen(ctx context.Context, prevLyric, currentLyric *LyricPosition) (location []entity.Coordinate) + CalculateHypen(ctx context.Context, prevLyric, currentLyric *LyricPosition) (location []HyphenPosition) RenderHypen(ctx context.Context, y, offsetCenter int, canv canvas.Canvas, measure []*entity.NoteRenderer) - RenderElision(ctx context.Context, canv canvas.Canvas, text []entity.Text, lyricPart int, pos entity.Coordinate) + RenderElision(ctx context.Context, canv canvas.Canvas, text []entity.Text, lyricPart int, pos entity.Coordinate, sty ...string) CalculateMarginLeft(txt string) float64 CalculateOverallWidth(ls []entity.Lyric) float64 SplitLyricPrefix(note *entity.NoteRenderer, y int, part int, leftBarline *entity.NoteRenderer) []LyricPosition @@ -113,6 +114,9 @@ func (li *lyricInteractor) SetLyricRenderer(noteRenderer *entity.NoteRenderer, r } func (li *lyricInteractor) RenderLyrics(ctx context.Context, y int, canv canvas.Canvas, measure []*entity.NoteRenderer, prevNote ...*entity.NoteRenderer) int { + + prm, _ := params.GetParamFromContext(ctx) + prefixes := map[string]LyricPosition{} offsetCenterVal := 0 @@ -171,7 +175,7 @@ func (li *lyricInteractor) RenderLyrics(ctx context.Context, y int, canv canvas. minPrefix = math.Min(minPrefix, float64(n.PositionX)-prefixWidth-constant.LOWERCASE_LENGTH) prefixes[n.LeadingHeader] = LyricPosition{ Coordinate: entity.NewCoordinate(float64(n.PositionX), float64(y)), - Lyrics: entity.Lyric{Text: []entity.Text{{Value: n.LeadingHeader}}}, + Lyrics: entity.Lyric{Text: []entity.Text{{Value: n.LeadingHeader}}, Verse: l.Verse}, } } @@ -182,9 +186,16 @@ func (li *lyricInteractor) RenderLyrics(ctx context.Context, y int, canv canvas. if strings.HasPrefix(lyricVal, "*") { xPos -= int(li.CalculateLyricWidth("*")) } - - canv.Text(xPos, int(yPos), lyricVal) - li.RenderElision(ctx, canv, text, i, entity.Coordinate{X: float64(xPos), Y: yPos}) + sty := getColoringStyle(l.Verse, len(n.Lyric)) + if prm.Verse < 2 { + sty = "" + } + canv.Text(xPos, int(yPos), lyricVal, sty) + elisionOpacity := "stroke-opacity:0.6" + if sty == "" { + elisionOpacity = "" + } + li.RenderElision(ctx, canv, text, i, entity.Coordinate{X: float64(xPos), Y: yPos}, elisionOpacity) n.Lyric[i] = l } @@ -200,6 +211,11 @@ func (li *lyricInteractor) RenderLyrics(ctx context.Context, y int, canv canvas. } minPrefix += li.CalculateLyricWidth(prefixVal) * 0.1 } + sty := getColoringStyle(p.Lyrics.Verse, p.TotalLyric) + if prm.Verse < 2 { + sty = "" + } + style = append(style, sty) canv.Text(int(minPrefix), int(p.Coordinate.Y), prefixVal, style...) } prefixes = map[string]LyricPosition{} diff --git a/internal/lyric/prefix.go b/internal/lyric/prefix.go index 0b1c014..c258a23 100644 --- a/internal/lyric/prefix.go +++ b/internal/lyric/prefix.go @@ -20,7 +20,8 @@ func (li *lyricInteractor) CalculateMarginLeft(txt string) float64 { return 0 } -func (li *lyricInteractor) SplitLyricPrefix(note *entity.NoteRenderer, y int, part int, leftBarline *entity.NoteRenderer) []LyricPosition { +func (li *lyricInteractor) SplitLyricPrefix(note *entity.NoteRenderer, y, part int, leftBarline *entity.NoteRenderer) []LyricPosition { + verse := note.Lyric[part].Verse lyricVal := entity.LyricVal(note.Lyric[part].Text).String() xPos := float64(note.PositionX) @@ -54,6 +55,7 @@ func (li *lyricInteractor) SplitLyricPrefix(note *entity.NoteRenderer, y int, pa Lyrics: entity.Lyric{ Syllabic: musicxml.LyricSyllabicTypeSingle, Text: []entity.Text{text}, + Verse: verse, }, } @@ -62,6 +64,7 @@ func (li *lyricInteractor) SplitLyricPrefix(note *entity.NoteRenderer, y int, pa Lyrics: entity.Lyric{ Syllabic: musicxml.LyricSyllabicTypeBegin, Text: []entity.Text{{Value: parts[1]}}, + Verse: verse, }, } mainLyric.Lyrics.Text[0] = entity.Text{Value: strings.ReplaceAll(mainLyric.Lyrics.Text[0].Value, parts[0]+".", "")} diff --git a/internal/lyric/type.go b/internal/lyric/type.go index 21a0533..7d3831c 100644 --- a/internal/lyric/type.go +++ b/internal/lyric/type.go @@ -10,6 +10,12 @@ type LyricPosition struct { TotalLyric int } +type HyphenPosition struct { + Coordinate entity.Coordinate + Verse int + TotalLyric int +} + type VerseInfo struct { MarginBottom int HasLyric bool diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index 3d3c8e5..a1ec71b 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -15,7 +15,9 @@ import ( "github.com/jodi-ivan/numbered-notation-xml/internal/staff" "github.com/jodi-ivan/numbered-notation-xml/internal/timesig" "github.com/jodi-ivan/numbered-notation-xml/internal/verse" + "github.com/jodi-ivan/numbered-notation-xml/svc/repository" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" ) type Renderer interface { @@ -59,6 +61,17 @@ func (ir *rendererInteractor) Render(ctx context.Context, music musicxml.MusicXM relativeY := ir.Staff.Render(ctx, canv, music.Part, keySignature, timeSignature, metadata) if metadata != nil { + prm, _ := params.GetParamFromContext(ctx) + if prm.Verse > 2 || (prm.Verse > 1 && prm.SingleVerseMode) { + firstVerse := verse.BuildContent(music, metadata) + metadata.ParsedVerse[1] = firstVerse + verseInfo := repository.HymnVerse{} + if otherVerse, ok := metadata.Verse[2]; ok { + verseInfo.StyleRow = otherVerse.StyleRow + } + metadata.Verse[1] = verseInfo + } + ir.Footnote.RenderMusicFootnotes(ctx, canv, metadata.HymnMetadata, relativeY) verseInfo := ir.Verse.RenderVerse(ctx, canv, relativeY, metadata) diff --git a/internal/staff/render.go b/internal/staff/render.go index ff02cf7..73a6994 100644 --- a/internal/staff/render.go +++ b/internal/staff/render.go @@ -26,6 +26,7 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part x := staffLines.GetLeftIndentWithTimeSignature() info := StaffInfo{ NextLineRenderer: []*entity.NoteRenderer{}, + SyllableOffset: map[int]int{}, } oldMarginButtom := 0 for i, st := range staffes { @@ -37,6 +38,8 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part IndexStart: info.EndIndex, ReffAtStart: info.StartRenderOtherNotes, RepeatInfo: info.RepeatInfo, + + SyllableOffset: info.SyllableOffset, } info = si.RenderStaff(ctx, canv, x, relativeY, i, metadata, st, data) info.RepeatInfo = append(data.RepeatInfo, info.RepeatInfo...) @@ -72,6 +75,8 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part IndexStart: info.EndIndex, ReffAtStart: info.StartRenderOtherNotes, RepeatInfo: info.RepeatInfo, + + SyllableOffset: info.SyllableOffset, } x = staffLines.GetLeftIndent(info.NextLineRenderer[0].MeasureNumber) idx := len(staffes) - 1 diff --git a/internal/staff/staff.go b/internal/staff/staff.go index e86b97a..105a56d 100644 --- a/internal/staff/staff.go +++ b/internal/staff/staff.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" + "github.com/jodi-ivan/numbered-notation-xml/diagnostics" "github.com/jodi-ivan/numbered-notation-xml/internal/barline" "github.com/jodi-ivan/numbered-notation-xml/internal/breathpause" "github.com/jodi-ivan/numbered-notation-xml/internal/constant" @@ -22,6 +23,7 @@ import ( "github.com/jodi-ivan/numbered-notation-xml/internal/timesig" "github.com/jodi-ivan/numbered-notation-xml/internal/verse" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" ) type Staff interface { @@ -32,24 +34,26 @@ type Staff interface { } type staffInteractor struct { - Barline barline.Barline - Lyric lyric.Lyric - Numbered numbered.Numbered - BreathPause breathpause.BreathPause - Rhythm rhythm.Rhythm - RenderAlign RenderStaffWithAlign + Barline barline.Barline + Lyric lyric.Lyric + Numbered numbered.Numbered + BreathPause breathpause.BreathPause + Rhythm rhythm.Rhythm + RenderAlign RenderStaffWithAlign + SyllableMatch verse.SyllableMatch } func NewStaff() Staff { barlineInteractor := barline.NewBarline() lyricInteractor := lyric.NewLyric() return &staffInteractor{ - Barline: barlineInteractor, - Lyric: lyricInteractor, - Numbered: numbered.New(lyricInteractor, barlineInteractor), - BreathPause: breathpause.New(), - Rhythm: rhythm.New(splitter.New()), - RenderAlign: NewRenderAlign(), + Barline: barlineInteractor, + Lyric: lyricInteractor, + Numbered: numbered.New(lyricInteractor, barlineInteractor), + BreathPause: breathpause.New(), + Rhythm: rhythm.New(splitter.New()), + RenderAlign: NewRenderAlign(), + SyllableMatch: verse.NewSyllableMatcher(), } } @@ -127,16 +131,15 @@ func (si *staffInteractor) RenderStaff(ctx context.Context, canv canvas.Canvas, renderer := &entity.NoteRenderer{ UUID: uuid.New().String(), PositionX: x, PositionY: int(y), - Note: n, NoteLength: note.Type, Octave: octave, Strikethrough: strikethrough, - NoteValue: noteLength, - IsRest: (note.Rest != nil), + Note: n, NoteLength: note.Type, Octave: octave, + Strikethrough: strikethrough, NoteValue: noteLength, + IsRest: (note.Rest != nil), Beam: map[int]entity.Beam{}, IsNewLine: measure.NewLineIndex[notePos], MeasureNumber: measure.Number, - AbsoluteNote: note.Pitch.Step, - AbsoluteOctave: note.Pitch.Octave, + AbsoluteNote: note.Pitch.Step, AbsoluteOctave: note.Pitch.Octave, AbsoluteAccidental: note.Accidental, TimeModifications: note.TimeModification, @@ -228,7 +231,13 @@ func (si *staffInteractor) RenderStaff(ctx context.Context, canv canvas.Canvas, staffInfo.RepeatInfo = append(staffInfo.RepeatInfo, measure.RepeatInfo) repeatInfo = staffInfo.RepeatInfo } - marginBottom := verse.LoadOtherVerse(notes, metadata, start, repeatInfo) + + matcher := si.SyllableMatch + if p, ok := params.GetParamFromContext(ctx); ok && p.Diagnostic != nil { + matcher = diagnostics.GetVerseDiagnostic(matcher) + } + var marginBottom int + staffInfo.SyllableOffset, marginBottom = matcher.LoadOtherVerse(ctx, notes, metadata, start, data.SyllableOffset, repeatInfo) if staffInfo.MarginBottom < marginBottom { staffInfo.MarginBottom = marginBottom } diff --git a/internal/staff/type.go b/internal/staff/type.go index b1118b9..48d747c 100644 --- a/internal/staff/type.go +++ b/internal/staff/type.go @@ -18,6 +18,7 @@ type StaffInfo struct { ForceNewLine bool SyllableCount int RepeatInfo []*musicxml.RepeatInfo + SyllableOffset map[int]int } type StaffData struct { @@ -28,6 +29,8 @@ type StaffData struct { IndexStart int ReffAtStart bool RepeatInfo []*musicxml.RepeatInfo + + SyllableOffset map[int]int } type CoordinateWithTuplet struct { entity.Coordinate diff --git a/internal/utils/map.go b/internal/utils/map.go new file mode 100644 index 0000000..18f621c --- /dev/null +++ b/internal/utils/map.go @@ -0,0 +1,13 @@ +package utils + +import "slices" + +// GetMapSortedKeys returns a sorted slice of keys for any map with string keys. +func GetMapSortedKeys[V any](m map[int]V) []int { + keys := make([]int, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + slices.Sort(keys) + return keys +} diff --git a/internal/verse/counter_match.go b/internal/verse/counter_match.go index 2bb3b00..94653d8 100644 --- a/internal/verse/counter_match.go +++ b/internal/verse/counter_match.go @@ -1,19 +1,37 @@ package verse import ( + "context" + "fmt" "strings" + "github.com/jodi-ivan/numbered-notation-xml/internal/breathpause" "github.com/jodi-ivan/numbered-notation-xml/internal/entity" "github.com/jodi-ivan/numbered-notation-xml/internal/lyric" "github.com/jodi-ivan/numbered-notation-xml/internal/musicxml" "github.com/jodi-ivan/numbered-notation-xml/internal/utils" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" ) -func IsVowel(char rune) bool { +type SyllableMatch interface { + IsVowel(char rune) bool + ApplyElision(syllText string, combine bool) []musicxml.LyricText + LoadOtherVerse(ctx context.Context, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, offset map[int]int, prevRepeatInfos []*musicxml.RepeatInfo) (map[int]int, int) + LoadVerse(ctx context.Context, targetVerse int, clear bool, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, prevRepeatInfos []*musicxml.RepeatInfo) (int, int) +} + +func NewSyllableMatcher() SyllableMatch { + return &matcher{} +} + +type matcher struct { +} + +func (m *matcher) IsVowel(char rune) bool { return utils.Contains([]string{"a", "i", "u", "e", "o"}, strings.ToLower(string(char))) >= 0 } -func ApplyElision(syllText string, combine bool) []musicxml.LyricText { +func (m *matcher) ApplyElision(syllText string, combine bool) []musicxml.LyricText { start, end := -1, -1 if !combine { @@ -22,17 +40,29 @@ func ApplyElision(syllText string, combine bool) []musicxml.LyricText { } } - for ir, r := range syllText { - if IsVowel(r) || (r == 'h' && start != -1) { - if start == -1 { - start = ir - } else { - end = ir + if defElistion, ok := defaultElision[syllText]; ok { + start, end = defElistion[0], defElistion[1] + } else { + for ir, r := range syllText { + if m.IsVowel(r) || (r == 'h' && start != -1) { + if start == -1 { + start = ir + } else { + end = ir + } } } + + } + + if end < start { + return []musicxml.LyricText{ + {Value: syllText}, + } } partBreakdown := []musicxml.LyricText{} + if start == 0 { partBreakdown = []musicxml.LyricText{ {Underline: 1, Value: syllText[start : end+1]}, @@ -55,14 +85,48 @@ func ApplyElision(syllText string, combine bool) []musicxml.LyricText { return partBreakdown } +func (m *matcher) LoadOtherVerse(ctx context.Context, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, offset map[int]int, prevRepeatInfos []*musicxml.RepeatInfo) (map[int]int, int) { + prm, _ := params.GetParamFromContext(ctx) + + if offset == nil { + offset = map[int]int{} + } + + if prm.Verse < 2 { + return offset, 0 + } + targetVerse := 2 + if prm.Verse > 1 { + targetVerse = prm.Verse + } + + if targetVerse > 2 && !prm.SingleVerseMode { + // load previous verse + prvOffset, _ := m.LoadVerse(ctx, targetVerse-1, true, notes, metadata, startPos+offset[targetVerse-1], prevRepeatInfos) + offset[targetVerse-1] += prvOffset + } + + targetVerseOffset, margin := m.LoadVerse(ctx, targetVerse, prm.SingleVerseMode, notes, metadata, startPos+offset[targetVerse], prevRepeatInfos) + offset[targetVerse] += targetVerseOffset + return offset, margin -func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, prevRepeatInfos []*musicxml.RepeatInfo) int { +} - verse, ok := metadata.ParsedVerse[2] // for now hardcoded two for testing visual +func (m *matcher) fillableByLyric(n *entity.NoteRenderer) bool { + return !(n.Barline != nil || breathpause.IsBreathMark(n) || n.IsDotted) +} + +func (m *matcher) LoadVerse(ctx context.Context, targetVerse int, clear bool, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, prevRepeatInfos []*musicxml.RepeatInfo) (int, int) { + + prm, _ := params.GetParamFromContext(ctx) + + verse, ok := metadata.ParsedVerse[targetVerse] if !ok { - return 0 + return 0, 0 } + offset := 0 + totalSyllable := 0 flattenSyll := []entity.LyricPartVerse{} @@ -93,31 +157,78 @@ func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, insert := true lyricNum := 0 firstNote := false - li := lyric.NewLyric() for i := 0; i < len(notes) && syll < len(flattenSyll); i++ { note := notes[i] + + if !m.fillableByLyric(note) { + continue + } if len(note.Lyric) == 0 { + if flattenSyll[syll].Offset == 1 { + // fill up the empty notes with current syllable (shift left) + offset++ + } else { + continue + + } + } else if flattenSyll[syll].Offset == 1 { + offset++ + } + if flattenSyll[syll].Offset == -1 { // fill the current notes with empty syllable. shift right + flattenSyll[syll].Offset = 0 + // syll-- + offset-- + appendedLyric := lyric.GetMusicxmlLyric(note) + if clear { + appendedLyric = []musicxml.Lyric{} + } + lyricVerse := 0 + if len(appendedLyric) > 0 { + lyricVerse = 2 + } + appendedLyric = append(appendedLyric, musicxml.Lyric{ + Number: len(appendedLyric) + 1, + Syllabic: musicxml.LyricSyllabicTypeMiddle, + Verse: lyricVerse, + }) + + li.SetLyricRenderer(note, appendedLyric) continue } - appendedLyric := lyric.GetMusicxmlLyric(note) // load the lyric on the current music + appendedLyric := []musicxml.Lyric{} + if !clear { + appendedLyric = lyric.GetMusicxmlLyric(note) // load the lyric on the current music + } if lyricNum == 0 { lyricNum = len(appendedLyric) + 1 } txt := flattenSyll[syll].Text - if lyric.HasPrefix(note) { - txt = "2." + txt // for now, hardcoded. if the lyric has prefix. + hasPrefix := lyric.HasPrefix(note) + if hasPrefix || syll == 0 { + txt = fmt.Sprintf("%d. %s", targetVerse, txt) + if len(appendedLyric) > 0 && !hasPrefix { + lastLyric := appendedLyric[0] + lastLyric.Text[0].Value = fmt.Sprintf("%d. %s", targetVerse-1, lastLyric.Text[0].Value) + appendedLyric[0] = lastLyric + } + } + + verseIndicator := 2 + if targetVerse != 2 && prm.Verse == verseIndicator-1 { + verseIndicator = 0 + } newLyric := []musicxml.Lyric{ { - Text: ApplyElision(txt, flattenCombine[syll]), + Text: m.ApplyElision(txt, flattenCombine[syll]), Syllabic: flattenSyll[syll].Type, Number: lyricNum, - Verse: 2, + Verse: verseIndicator, }, } insertLastMeasure := syll < lastOffset*2 @@ -149,9 +260,8 @@ func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, newLyric = []musicxml.Lyric{ { - Number: lyricNum, Verse: 2, - Text: ApplyElision(flattenSyll[syll].Text, flattenCombine[syll]), - Syllabic: flattenSyll[syll].Type, + Number: lyricNum, Verse: 2, Syllabic: flattenSyll[syll].Type, + Text: m.ApplyElision(flattenSyll[syll].Text, flattenCombine[syll]), }, } insert = false @@ -168,7 +278,7 @@ func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, if syllRepeat < len(flattenSyll) { newLyric = append(newLyric, musicxml.Lyric{ Number: newLyric[0].Number + 1, Verse: 2, - Text: ApplyElision(flattenSyll[syllRepeat].Text, flattenCombine[syllRepeat]), + Text: m.ApplyElision(flattenSyll[syllRepeat].Text, flattenCombine[syllRepeat]), Syllabic: flattenSyll[syllRepeat].Type, }) } @@ -187,6 +297,12 @@ func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, } - return marginBottom + if prm.Diagnostic != nil { + res := map[int]bool{targetVerse: syll == len(flattenSyll)} + prm.Diagnostic.VerseSyllMatch <- res + + } + + return offset, marginBottom } diff --git a/internal/verse/firstverse.go b/internal/verse/firstverse.go new file mode 100644 index 0000000..88834bf --- /dev/null +++ b/internal/verse/firstverse.go @@ -0,0 +1,137 @@ +package verse + +import ( + "unicode" + + "github.com/jodi-ivan/numbered-notation-xml/internal/entity" + "github.com/jodi-ivan/numbered-notation-xml/internal/musicxml" +) + +func GetLineSyllableBound(metadata *entity.HymnMetaData) []int { + result := []int{} + verse, ok := metadata.ParsedVerse[2] + if !ok { + return result + } + + syllLine := make([]int, len(verse)) + for i, line := range verse { + totalSyllablePerLine := 0 + for _, syll := range line { + totalSyllablePerLine += len(syll.Breakdown) + } + syllLine[i] = totalSyllablePerLine + if i > 0 { + syllLine[i] += syllLine[i-1] + } + } + + return syllLine +} + +func BuildContent(music musicxml.MusicXML, metadata *entity.HymnMetaData) [][]entity.LyricWordVerse { + + lMapper := map[int][]entity.LyricWordVerse{} + + prevTotalLyric := -1 + wordVerses := map[int]entity.LyricWordVerse{} + for _, measure := range music.Part.Measures { + measure.Build() + for _, note := range measure.Notes { + if len(note.Lyric) == 0 { + continue + } + + if prevTotalLyric != len(note.Lyric) && prevTotalLyric != -1 { + lMapper[1] = append(lMapper[1], lMapper[2]...) + lMapper[2] = []entity.LyricWordVerse{} + } + + for _, l := range note.Lyric { + + syl := "" + + part := entity.LyricPartVerse{ + Text: syl, + Type: l.Syllabic, + } + + if len(l.Text) > 1 { + part.Combine = true + part.Breakdown = []entity.LyricStylePart{} + } + for _, s := range l.Text { + syl += s.Value + part.Text += s.Value + + part.Breakdown = append(part.Breakdown, entity.LyricStylePart{ + Text: s.Value, + Underline: s.Underline != 0, + }) + } + + // li := lyric.NewLyric() + if unicode.IsDigit(rune(syl[0])) { + syl = syl[2:] + } + wordVerse := wordVerses[l.Number] + if len(wordVerse.Breakdown) == 0 { + wordVerse.Breakdown = []entity.LyricPartVerse{} + } + wordVerse.Word += syl + wordVerse.Breakdown = append(wordVerse.Breakdown, part) + + wordVerses[l.Number] = wordVerse + + if l.Syllabic == musicxml.LyricSyllabicTypeEnd || l.Syllabic == musicxml.LyricSyllabicTypeSingle { + lMapper[l.Number] = append(lMapper[l.Number], wordVerse) + wordVerses[l.Number] = entity.LyricWordVerse{ + Breakdown: []entity.LyricPartVerse{}, + } + + } + + } + + prevTotalLyric = len(note.Lyric) + } + } + + for part := 2; part <= 4; part++ { + if len(lMapper[part]) > 0 { + lMapper[1] = append(lMapper[1], lMapper[part]...) + } + } + + return SplitLyricsIntoLines(lMapper[1], GetLineSyllableBound(metadata)) + +} + +func SplitLyricsIntoLines(words []entity.LyricWordVerse, maxSyllables []int) [][]entity.LyricWordVerse { + result := make([][]entity.LyricWordVerse, len(maxSyllables)) + + wordIdx := 0 // index into words[] + consumed := 0 // syllables consumed across all lines so far + + for lineIdx, ceiling := range maxSyllables { + var lineWords []entity.LyricWordVerse + + for wordIdx < len(words) { + w := words[wordIdx] + syllableCount := len(w.Breakdown) + + // Stop if adding this word exceeds the line's ceiling + if consumed+syllableCount > ceiling { + break + } + + lineWords = append(lineWords, w) + consumed += syllableCount + wordIdx++ + } + + result[lineIdx] = lineWords + } + + return result +} diff --git a/internal/verse/type.go b/internal/verse/type.go index 963194a..acd6ae3 100644 --- a/internal/verse/type.go +++ b/internal/verse/type.go @@ -11,10 +11,9 @@ type VerseInfo struct { type VerseRowStyle int type versePosition struct { - Col int - Row int - RowWidth int - Style VerseRowStyle + Col int + Row int + Style VerseRowStyle } type ParsedVerse struct { @@ -30,3 +29,7 @@ type ParsedVerseWithInfo struct { MaxRightPos float64 RowPositionY map[int]int } + +var defaultElision = map[string][2]int{ + "rasy": [2]int{0, 3}, +} diff --git a/internal/verse/verse.go b/internal/verse/verse.go index b2592df..11f5b17 100644 --- a/internal/verse/verse.go +++ b/internal/verse/verse.go @@ -10,7 +10,9 @@ import ( "github.com/jodi-ivan/numbered-notation-xml/internal/entity" "github.com/jodi-ivan/numbered-notation-xml/internal/footnote" "github.com/jodi-ivan/numbered-notation-xml/internal/lyric" + "github.com/jodi-ivan/numbered-notation-xml/internal/utils" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" ) type Verse interface { @@ -53,7 +55,36 @@ func (v *verseInteractor) elisionPosition(p entity.LyricPartVerse, y int, lineBe } } -func (v *verseInteractor) parse(y int, metadata *entity.HymnMetaData) ParsedVerseWithInfo { +func getPos(idx, style, totalVerse int) (row, col, newStyle int) { + if (idx == 0 && totalVerse == 1) || style == 0 || style == 12 { + return idx + 1, 1, 12 + } + + half := totalVerse / 2 + col = 1 + if (idx+1)%2 == 1 && idx+1 == totalVerse { + col = 1 + row = half + 1 // Placed at the bottom across both columns + style = 12 + } else { + // 2. Vertical Column Logic + if idx < half { + // First Column (Vertical flow) + col = 1 + row = idx + 1 + } else { + // Second Column (Starts at Verse 4 if count is 5) + col = 2 + row = (idx - half) + 1 + } + } + + return row, col, style +} + +func (v *verseInteractor) parse(ctx context.Context, y int, metadata *entity.HymnMetaData) ParsedVerseWithInfo { + + prm, _ := params.GetParamFromContext(ctx) result := ParsedVerseWithInfo{ Verses: map[int]ParsedVerse{}, @@ -61,26 +92,49 @@ func (v *verseInteractor) parse(y int, metadata *entity.HymnMetaData) ParsedVers RowPositionY: map[int]int{}, } - for i := 2; i <= len(metadata.Verse)+1; i++ { + if prm.Verse > 1 { + delete(metadata.Verse, prm.Verse) + delete(metadata.ParsedVerse, prm.Verse) + + if !prm.SingleVerseMode { + delete(metadata.Verse, prm.Verse-1) + delete(metadata.ParsedVerse, prm.Verse-1) + } + } + + if len(metadata.Verse) == 0 { + return result + } + versesNo := utils.GetMapSortedKeys(metadata.Verse) + + for idx, i := range versesNo { + verse := metadata.Verse[i] whole := metadata.ParsedVerse[i] - if _, ok := result.RowPositionY[int(verse.Row.Int16)]; !ok { - result.RowPositionY[int(verse.Row.Int16)] = y + (LINE_DISTANCE * len(whole) * (int(verse.Row.Int16) - 1)) + ((int(verse.Row.Int16) - 1) * VERSE_SEPARATOR) - } + row := int(verse.Row.Int16) + col := int(verse.Col.Int16) + style := int(verse.StyleRow.Int32) - style := VerseRowStyle(verse.StyleRow.Int32) + if prm.Verse > 1 { + // get the 1st style instead, since there is cases when the verse is last verse + // style can be modified. + style = int(metadata.Verse[versesNo[0]].StyleRow.Int32) + if style == 0 { + style = int(VerseRowStyleSingleColumn) + } + row, col, style = getPos(idx, style, len(versesNo)) + } - if int(style) == 0 { - style = VerseRowStyleSingleColumn + if _, ok := result.RowPositionY[row]; !ok { + result.RowPositionY[row] = y + (LINE_DISTANCE * len(whole) * (row - 1)) + ((row - 1) * VERSE_SEPARATOR) } + parsedVerse := ParsedVerse{ ElisionMarks: [][2]entity.Coordinate{}, Position: versePosition{ - Col: int(verse.Col.Int16), - RowWidth: int(verse.StyleRow.Int32), - Row: int(verse.Row.Int16), - Style: style, + Col: col, Row: row, + Style: VerseRowStyle(style), }, } @@ -98,13 +152,13 @@ func (v *verseInteractor) parse(y int, metadata *entity.HymnMetaData) ParsedVers lineText = lineText + " " + word.Word } result.MaxLineWidth = math.Max(result.MaxLineWidth, v.Lyric.CalculateLyricWidth(lineText)) - if verse.Col.Int16 == 2 { + if col == 2 { result.MaxRightPos = math.Max(result.MaxRightPos, result.MaxLineWidth) result.IsMultiColumn = result.IsMultiColumn || true } parsedVerse.Verse = append(parsedVerse.Verse, lineText) } - result.Verses[int(verse.VerseNum.Int32)] = parsedVerse + result.Verses[i] = parsedVerse } return result @@ -113,7 +167,9 @@ func (v *verseInteractor) parse(y int, metadata *entity.HymnMetaData) ParsedVers func (v *verseInteractor) RenderVerse(ctx context.Context, canv canvas.Canvas, y int, metadata *entity.HymnMetaData) VerseInfo { canv.Group("class='verses'", "style='font-family:Caladea'") - parsedVerse := v.parse(y, metadata) + prm, _ := params.GetParamFromContext(ctx) + + parsedVerse := v.parse(ctx, y, metadata) defaultX := int(math.Round((constant.LAYOUT_WIDTH / 2) - (parsedVerse.MaxLineWidth / 2))) x := defaultX @@ -121,25 +177,33 @@ func (v *verseInteractor) RenderVerse(ctx context.Context, canv canvas.Canvas, y x = constant.LAYOUT_INDENT_LENGTH * 2 } totalVerse := len(parsedVerse.Verses) + if prm.Verse != 0 { + totalVerse -= 2 + } offset := 0.0 maxY := float64(0) - for i := 1; i < totalVerse+1; i++ { + versesNo := utils.GetMapSortedKeys(parsedVerse.Verses) + for _, i := range versesNo { - canv.Group("class='verse'", fmt.Sprintf("number='%d'", i+1)) + canv.Group("class='verse'", fmt.Sprintf("number='%d'", i)) yVerse := y - currentVerse := parsedVerse.Verses[i+1] + currentVerse := parsedVerse.Verses[i] + + row := currentVerse.Position.Row + col := currentVerse.Position.Col + style := currentVerse.Position.Style // number verse margin := 0 if parsedVerse.IsMultiColumn { - if currentVerse.Position.Col == 2 { + if col == 2 { margin = constant.LAYOUT_WIDTH - (constant.LAYOUT_INDENT_LENGTH * 3.5) - int(parsedVerse.MaxRightPos) - yVerse = parsedVerse.RowPositionY[currentVerse.Position.Row] + yVerse = parsedVerse.RowPositionY[row] y = yVerse - } else if currentVerse.Position.Style == VerseRowStyleSingleColumn { + } else if style == VerseRowStyleSingleColumn { margin = -1 * (constant.LAYOUT_INDENT_LENGTH / 4) } } @@ -149,23 +213,23 @@ func (v *verseInteractor) RenderVerse(ctx context.Context, canv canvas.Canvas, y offset = parsedVerse.MaxLineWidth / 2 } - if currentVerse.Position.Col == 1 { + if col == 1 { offset = math.Abs(offset) } else { offset = math.Abs(offset) * -1 } - if currentVerse.Position.Style == VerseRowStyleSingleColumn { + if style == VerseRowStyleSingleColumn { offset = (constant.LAYOUT_INDENT_LENGTH / 4) } } - if currentVerse.Position.Style == VerseRowStyleSingleColumn { + if style == VerseRowStyleSingleColumn { x = defaultX + int(constant.LAYOUT_INDENT_LENGTH/2) } xPos := x + margin + int(offset) - prefixNum := fmt.Sprintf("%d. ", i+1) + prefixNum := fmt.Sprintf("%d. ", i) canv.Text(xPos-5-int(v.Lyric.CalculateLyricWidth(prefixNum)), y, prefixNum) for line, liveVerse := range currentVerse.Verse { if strings.HasPrefix(liveVerse, " ") { @@ -173,7 +237,7 @@ func (v *verseInteractor) RenderVerse(ctx context.Context, canv canvas.Canvas, y } canv.TextUnescaped(float64(xPos), float64(y), liveVerse) cursor := footnote.VerseLineCursor{ - VerseNo: i + 1, + VerseNo: i, LinePos: line + 1, Leftmargin: margin + int(offset), LineText: liveVerse, diff --git a/internal/verse/verse_test.go b/internal/verse/verse_test.go index 099b085..a19851f 100644 --- a/internal/verse/verse_test.go +++ b/internal/verse/verse_test.go @@ -136,7 +136,7 @@ func Test_verseInteractor_parse(t *testing.T) { var v verseInteractor v.Lyric = tt.lyricMock(ctrl) - got := v.parse(tt.y, &entity.HymnMetaData{HymnMetadata: &repository.HymnMetadata{Verse: tt.verses}}) + got := v.parse(context.Background(), tt.y, &entity.HymnMetaData{HymnMetadata: &repository.HymnMetadata{Verse: tt.verses}}) assert.Equal(t, tt.want, got, "parse()") }) diff --git a/utils/params/diagnostic.go b/utils/params/diagnostic.go new file mode 100644 index 0000000..cdbf659 --- /dev/null +++ b/utils/params/diagnostic.go @@ -0,0 +1,16 @@ +package params + +import "sync" + +type VerseDiagnostic struct { + SingleMode map[int]bool + StackMode map[int]bool +} + +type DiagParam struct { + Mu *sync.RWMutex + MapMtx *sync.Map + VerseSyllMatch chan map[int]bool + VerseDiagnostic chan VerseDiagnostic + Finish chan bool +} diff --git a/utils/params/paramctx.go b/utils/params/paramctx.go new file mode 100644 index 0000000..14b8953 --- /dev/null +++ b/utils/params/paramctx.go @@ -0,0 +1,61 @@ +package params + +import ( + "context" + "time" +) + +const PARAM_CTX_KEY = "param" + +type Param struct { + DisableGregorian bool + Verse int + SingleVerseMode bool + + Diagnostic *DiagParam +} + +type paramCtx struct { + ctx context.Context + Param *Param +} + +func (pc *paramCtx) Deadline() (deadline time.Time, ok bool) { + return pc.ctx.Deadline() +} +func (pc *paramCtx) Done() <-chan struct{} { + return pc.ctx.Done() +} +func (pc *paramCtx) Err() error { + return pc.ctx.Err() +} +func (pc *paramCtx) Value(key any) any { + strKey, ok := key.(string) + if ok && strKey == PARAM_CTX_KEY { + return pc.Param + } + + return pc.ctx.Value(key) +} + +func NewParamContext(ctx context.Context, param *Param) context.Context { + ctx = context.WithValue(ctx, PARAM_CTX_KEY, param) + return ¶mCtx{ + ctx: ctx, + Param: param, + } +} +func GetParamFromContext(ctx context.Context) (*Param, bool) { + pCtx, ok := ctx.(*paramCtx) + if ok && pCtx != nil { + return pCtx.Param, true + } + + param, ok := ctx.Value(PARAM_CTX_KEY).(*Param) + if !ok { + return &Param{}, false + } + + return param, true + +} diff --git a/utils/webserver/webserver.go b/utils/webserver/webserver.go index 7aa0ecb..41ea864 100644 --- a/utils/webserver/webserver.go +++ b/utils/webserver/webserver.go @@ -53,7 +53,7 @@ type HTTPAdapter interface { func commonMiddleware(wg *sync.WaitGroup, next httprouter.Handle) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - path := r.URL.Path + path := r.RequestURI defer func() { err := recover() if err != nil { @@ -72,7 +72,11 @@ func commonMiddleware(wg *sync.WaitGroup, next httprouter.Handle) httprouter.Han next(responseWriter, r, ps) - log.Printf("[Webserver][%d ms][%d bytes] %s %s -> Status code: %d\n", time.Since(t).Milliseconds(), responseWriter.size, strings.ToUpper(r.Method), path, responseWriter.status) + log.Printf("[Webserver][Status: %d][%d ms][%d bytes] %s %s\n", + responseWriter.status, + time.Since(t).Milliseconds(), + responseWriter.size, + strings.ToUpper(r.Method), path) } }