elliotchance/gedcom

View on GitHub
html/diff_page.go

Summary

Maintainability
A
2 hrs
Test Coverage
package html

import (
    "fmt"
    "io"
    "sort"

    "github.com/elliotchance/gedcom/v39"
    "github.com/elliotchance/gedcom/v39/html/core"
    "github.com/elliotchance/gedcom/v39/util"
)

// These are used for optionShow. If you update these options you will also
// need to adjust validateOptions.
const (
    DiffPageShowAll         = "all" // default
    DiffPageShowOnlyMatches = "only-matches"
    DiffPageShowSubset      = "subset"
)

// These are used for optionSort. If you update these options you will also
// need to adjust validateOptions.
const (
    DiffPageSortWrittenName       = "written-name" // default
    DiffPageSortHighestSimilarity = "highest-similarity"
)

type DiffPage struct {
    comparisons       gedcom.IndividualComparisons
    filterFlags       *gedcom.FilterFlags
    googleAnalyticsID string
    sort              string
    show              string
    progress          chan gedcom.Progress
    compareOptions    *gedcom.IndividualNodesCompareOptions
    visibility        LivingVisibility
}

func NewDiffPage(comparisons gedcom.IndividualComparisons, filterFlags *gedcom.FilterFlags, googleAnalyticsID string, show, sort string, progress chan gedcom.Progress, compareOptions *gedcom.IndividualNodesCompareOptions, visibility LivingVisibility) *DiffPage {
    return &DiffPage{
        comparisons:       comparisons,
        filterFlags:       filterFlags,
        googleAnalyticsID: googleAnalyticsID,
        show:              show,
        sort:              sort,
        progress:          progress,
        compareOptions:    compareOptions,
        visibility:        visibility,
    }
}

func (c *DiffPage) sortByWrittenName(comparisons []*IndividualCompare, i, j int) bool {
    a := comparisons[i].comparison.Left
    b := comparisons[j].comparison.Left

    if a == nil {
        a = comparisons[i].comparison.Right
    }

    if b == nil {
        b = comparisons[j].comparison.Right
    }

    aName := a.Name().String()
    bName := b.Name().String()

    return aName < bName
}

func (c *DiffPage) sortByHighestSimilarity(comparisons []*IndividualCompare, i, j int) bool {
    a := c.weightedSimilarity(comparisons[i].comparison)
    b := c.weightedSimilarity(comparisons[j].comparison)

    if a != b {
        // Greater than because we want the highest matches up the top.
        return a > b
    }

    // Fallback to sorting by name for non-matches
    return c.sortByWrittenName(comparisons, i, j)
}

func (c *DiffPage) sortComparisons(comparisons []*IndividualCompare) {
    sortFns := map[string]func(*DiffPage, []*IndividualCompare, int, int) bool{
        DiffPageSortWrittenName:       (*DiffPage).sortByWrittenName,
        DiffPageSortHighestSimilarity: (*DiffPage).sortByHighestSimilarity,
    }

    sortFn := sortFns[c.sort]
    sort.SliceStable(comparisons, func(i, j int) bool {
        return sortFn(c, comparisons, i, j)
    })
}

func (c *DiffPage) weightedSimilarity(comparison *gedcom.IndividualComparison) float64 {
    s := comparison.Similarity

    if s != nil {
        return s.WeightedSimilarity()
    }

    return 0.0
}

func (c *DiffPage) createJobs() chan *IndividualCompare {
    jobs := make(chan *IndividualCompare, 10)

    go func() {
        for _, comparison := range c.comparisons {
            jobs <- NewIndividualCompare(comparison,
                c.filterFlags, c.progress, c.compareOptions, c.visibility)
        }

        close(jobs)
    }()

    return jobs
}

func (c *DiffPage) processJobs(jobs chan *IndividualCompare) chan *IndividualCompare {
    results := make(chan *IndividualCompare, 10)

    go func() {
        util.WorkerPool(c.compareOptions.ConcurrentJobs(), func(_ int) {
            for job := range jobs {
                if c.shouldSkip(job) {
                    continue
                }

                results <- job
            }
        })

        close(results)
    }()

    return results
}

func (c *DiffPage) sortResults(in chan *IndividualCompare) chan *IndividualCompare {
    out := make(chan *IndividualCompare, 10)

    go func() {
        // We have to read all results before they can be sorted.
        all := []*IndividualCompare{}
        for comparison := range in {
            all = append(all, comparison)
        }

        c.sortComparisons(all)

        // Send all results back. We expect there is only one receiver for this
        // to work.
        for _, item := range all {
            out <- item
        }

        close(out)
    }()

    return out
}

func (c *DiffPage) WriteHTMLTo(w io.Writer) (int64, error) {
    if c.progress != nil {
        c.progress <- gedcom.Progress{
            Total: int64(len(c.comparisons)),
        }
    }

    jobs := c.createJobs()
    results := c.processJobs(jobs)
    results = c.sortResults(results)

    precalculatedComparisons := []*IndividualCompare{}

    for comparison := range results {
        precalculatedComparisons = append(precalculatedComparisons, comparison)
    }

    // The index at the top of the page.
    rows := []core.Component{}
    for _, comparison := range precalculatedComparisons {
        weightedSimilarity := c.weightedSimilarity(comparison.comparison)

        leftClass := ""
        rightClass := ""

        switch {
        case comparison.comparison.Left != nil && comparison.comparison.Right == nil:
            leftClass = "bg-warning"

        case comparison.comparison.Left == nil && comparison.comparison.Right != nil:
            rightClass = "bg-primary"

        case weightedSimilarity < 1:
            leftClass = "bg-info"
            rightClass = "bg-info"

        case c.filterFlags.HideEqual:
            continue
        }

        leftNameAndDates := NewIndividualNameAndDatesLink(comparison.comparison.Left, c.visibility, "")
        rightNameAndDates := NewIndividualNameAndDatesLink(comparison.comparison.Right, c.visibility, "")

        left := core.NewTableCell(leftNameAndDates).Class(leftClass)
        right := core.NewTableCell(rightNameAndDates).Class(rightClass)

        middle := core.NewTableCell(core.NewText(""))
        if weightedSimilarity != 0 {
            similarityString := fmt.Sprintf("%.2f%%", weightedSimilarity*100)
            middle = core.NewTableCell(core.NewText(similarityString)).
                Class("text-center " + leftClass)
        }

        tableRow := core.NewTableRow(left, middle, right)

        rows = append(rows, tableRow)
    }

    // Individual pages
    components := []core.Component{
        core.NewSpace(),
        core.NewCard(core.NewText("Individuals"), core.CardNoBadgeCount,
            core.NewTable("", rows...)),
        core.NewSpace(),
    }
    for _, comparison := range precalculatedComparisons {
        components = append(components, comparison, core.NewSpace())
    }

    return core.NewPage(
        "Comparison",
        core.NewRow(core.NewColumn(core.EntireRow, core.NewComponents(components...))),
        c.googleAnalyticsID,
    ).WriteHTMLTo(w)
}

func (c *DiffPage) shouldSkip(comparison *IndividualCompare) bool {
    switch c.show {
    case DiffPageShowAll:
        // Do nothing, we want to show all.

    case DiffPageShowSubset:
        if gedcom.IsNil(comparison.comparison.Right) {
            return true
        }

    case DiffPageShowOnlyMatches:
        if gedcom.IsNil(comparison.comparison.Left) || gedcom.IsNil(comparison.comparison.Right) {
            return true
        }
    }

    return comparison.isEmpty()
}