To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Revision:

root / common / Objects.cpp @ 52:e0e12bd2978d

History | View | Annotate | Download (23.3 KB)

1
/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
2

    
3
#include "Objects.h"
4

    
5
//#include <dataquay/Debug.h>
6

    
7
#include <cstdlib>
8
#include <iostream>
9

    
10
#include "EditDistance.h"
11

    
12
#include <QHash> // to ensure correct qHash(const QString &) is found
13
#include <QFile>
14
#include <QFileInfo>
15
#include <QCryptographicHash>
16

    
17
namespace ClassicalData {
18

    
19
QMap<QString, Form *> Form::m_map;
20
QMutex Form::m_mutex;
21

    
22
QString
23
Composition::getComposerName() const
24
{
25
    if (m_composer) return m_composer->name();
26
    return m_cname;
27
}
28

    
29
bool
30
Composer::matchDates(const Composer *b) const
31
{
32
    const Composer *a = this;
33
    
34
    if (a->birth() && b->birth()) {
35
        int ay = a->birth()->year(), by = b->birth()->year();
36
        if (ay < 1800 || // birth dates before 1700 tend to be vague!
37
            a->birth()->approximate() ||
38
            b->birth()->approximate()) {
39
            if (abs(ay - by) > 25) return false;
40
        } else {
41
            if (abs(ay - by) > 1) {
42
                return false;
43
            }
44
        }
45
    }
46
    if (a->death() && b->death()) {
47
        int ay = a->death()->year(), by = b->death()->year();
48
        if (a->death()->approximate() || b->death()->approximate()) {
49
            if (abs(ay - by) > 10) return false;
50
        } else if (ay < 1700) {
51
            if (abs(ay - by) > 25) return false;
52
        } else if (ay < 1800) {
53
            // cut a bit of slack, but not as much as for birth date
54
            if (abs(ay - by) > 10) return false;
55
        } else {
56
            if (abs(ay - by) > 1) return false;
57
        }
58
    }
59
    return true;
60
}
61

    
62
void
63
Composer::cacheNames() const
64
{
65
    if (m_namesCached) return;
66

    
67
    QString n = name();
68
    QStringList pl = n.split(", ");
69

    
70
    if (pl.size() == 1) {
71
        QStringList pl2;
72
        pl = n.split(' ');
73
        pl2.push_back(pl[pl.size()-1]);
74
        pl2.push_back("");
75
        for (int i = 0; i+1 < pl.size(); ++i) {
76
            if (i > 0) pl2[1] += " ";
77
            pl2[1] += pl[i];
78
        }
79
        pl = pl2;
80
    }
81

    
82
    m_surname = pl[0];
83

    
84
    n = "";
85
    for (int i = 1; i < pl.size(); ++i) {
86
        if (i > 1) n += ", ";
87
        n += pl[i];
88
    }
89

    
90
    m_forenames = n;
91

    
92
    m_surnameElements.clear();
93
    m_connectiveElements.clear();
94
    m_forenameElements.clear();
95
    m_otherElements.clear();
96
    m_reducedSurnameElements.clear();
97
    m_reducedForenameElements.clear();
98
    
99
    static QRegExp sre("[\\., -]+");
100

    
101
    foreach (QString s, m_surname.split(sre, QString::SkipEmptyParts)) {
102
        if (s[0].isUpper()) {
103
            m_surnameElements.push_back(s.toLower());
104
            m_reducedSurnameElements.push_back(reduceName(s));
105
        } else if (s.length() > 1) {
106
            m_connectiveElements.push_back(s.toLower());
107
        }
108
    }
109

    
110
    foreach (QString s, m_forenames.split(sre, QString::SkipEmptyParts)) {
111
        if (s[0].isUpper()) {
112
            m_forenameElements.push_back(s.toLower());
113
            m_reducedForenameElements.push_back(reduceName(s));
114
        } else if (s.length() > 1) {
115
            m_connectiveElements.push_back(s.toLower());
116
        }
117
    }
118

    
119
    foreach (QString a, m_aliases) {
120
        foreach (QString ae, a.split(sre, QString::SkipEmptyParts)) {
121
            m_otherElements.push_back(ae.toLower());
122
        }
123
    }
124

    
125
    m_namesCached = true;
126
}
127

    
128
QString
129
Composer::getSortName(bool caps) const
130
{
131
    QString surname = getSurname();
132
    QString forenames = getForenames();
133
    if (caps) surname = surname.toUpper();
134
    if (forenames != "") return surname + ", " + forenames;
135
    else return surname;
136
}
137

    
138
QString
139
Composer::getSurname() const
140
{
141
    cacheNames();
142
    return m_surname;
143
}
144

    
145
QString
146
Composer::getForenames() const
147
{
148
    cacheNames();
149
    return m_forenames;
150
}
151

    
152
QString
153
Composer::getDisplayDates() const
154
{
155
    QString s;
156
    if (birth() || death()) {
157
        bool showApprox = false;
158
        if ((birth() && birth()->approximate()) ||
159
            (death() && death()->approximate())) {
160
            showApprox = true;
161
        }
162
        if (birth()) {
163
            if (birth()->place() != "") {
164
                s += birth()->place() + ", ";
165
            }
166
            if (showApprox) {
167
                s += "c. ";
168
                showApprox = false;
169
            }
170
            s += QString("%1").arg(birth()->year().toInt());
171
        }
172
        s += "-";
173
        if (death()) {
174
            if (death()->place() != "") {
175
                s += death()->place() + ", ";
176
            }
177
            if (showApprox) {
178
                s += "c. ";
179
                showApprox = false;
180
            }
181
            s += QString("%1").arg(death()->year().toInt());
182
        }
183
    }
184

    
185
    return s;
186
}
187
   
188
static QString
189
asciify(QString field)
190
{
191
    QString ascii;
192
    for (int i = 0; i < field.length(); ++i) {
193
        QString dc = field[i].decomposition();
194
        if (dc != "") ascii += dc[0];
195
        else if (field[i] == QChar(0x00DF)) {
196
            ascii += "ss";
197
        } else {
198
            ascii += field[i];
199
        }
200
    }
201
    ascii.replace(QString::fromUtf8("\342\200\231"), "'"); // apostrophe
202
    ascii.replace(QString::fromUtf8("\342\200\222"), "-");
203
    ascii.replace(QString::fromUtf8("\342\200\223"), "-");
204
    ascii.replace(QString::fromUtf8("\342\200\224"), "-");
205
    ascii.replace(QString::fromUtf8("\342\200\225"), "-");
206
    return ascii;
207
}
208

    
209
QString
210
Composer::reduceName(QString name)
211
{
212
    QString key = asciify(name).toLower()
213
        .replace("'", "")
214
        .replace("x", "ks")
215
        .replace("y", "i")
216
        .replace("ie", "i")
217
        .replace("ei", "i")
218
        .replace("ii", "i")
219
        .replace("k", "c")
220
        .replace("aa", "a")
221
        .replace("a", "e")
222
        .replace("ee", "e")
223
        .replace("v", "f")
224
        .replace("ph", "f")
225
        .replace("ff", "f")
226
        .replace("th", "t")
227
        .replace("tch", "ch")
228
        .replace("ch", "c")
229
        .replace("cc", "c")
230
        .replace("er", "r");
231
    return key;
232
}
233

    
234
bool
235
Composer::matchCatalogueName(QString an) const
236
{
237
    // ew!
238

    
239
    QString bn = name();
240
    if (bn == an) return true;
241
    if (aliases().contains(an)) return true;
242

    
243
    int aSurnameIndex = 0, bSurnameIndex = 0;
244
    if (an.contains(",")) {
245
        an.replace(",", "");
246
    } else {
247
        aSurnameIndex = -1;
248
    }
249
    if (bn.contains(",")) {
250
        bn.replace(",", "");
251
    } else {
252
        bSurnameIndex = -1;
253
    }
254
    QStringList nl = an.split(QRegExp("[ -]"));
255
    QStringList bnl = reduceName(bn).split(QRegExp("[ -]"));
256
    int matchCount = 0;
257
    QString surnameMatch = "";
258
    if (aSurnameIndex == -1) aSurnameIndex = nl.size()-1;
259
    if (bSurnameIndex == -1) bSurnameIndex = bnl.size()-1;
260
    if (nl[aSurnameIndex][0].isUpper() &&
261
        nl[aSurnameIndex] != "Della" &&
262
        reduceName(nl[aSurnameIndex]) == bnl[bSurnameIndex]) {
263
        surnameMatch = nl[aSurnameIndex];
264
    }
265
    int tested = 0;
266
    foreach (QString elt, nl) {
267
        if (!elt[0].isUpper() || elt == "Della") continue;
268
        QString k = reduceName(elt);
269
        if (bnl.contains(k)) {
270
            ++matchCount;
271
        }
272
        if (++tested == 2 && matchCount == 0) {
273
            return false;
274
        }
275
    }
276
    if (surnameMatch != "") {
277
//        DEBUG << "namesFuzzyMatch: note: surnameMatch = " << surnameMatch << endl;
278
        if (matchCount > 1) {
279
            return true;
280
        } else {
281
//            DEBUG << "(but not enough else matched)" << endl;
282
            return false;
283
        }
284
    }
285
    return false;
286
}    
287

    
288
float
289
Composer::matchFuzzyName(QString n) const
290
{
291
    int fameBonus = m_pages.size();
292
    if (n == name()) return 100 + fameBonus;
293
    static QRegExp sre("[\\., -]+");
294
    return matchFuzzyName(n.toLower().split(sre, QString::SkipEmptyParts));
295
}
296

    
297
static int
298
calculateThresholdedDistance(EditDistance &ed, const QString &user,
299
                             const QString &machine)
300
{
301
    int threshold = machine.length()/3;
302
    int dist;
303
    if (threshold == 0) dist = (user == machine ? 0 : -1);
304
    else {
305
        dist = ed.calculate(user, machine, threshold);
306
        if (dist > threshold) dist = -1;
307
    }
308
    return dist;
309
}
310

    
311
float
312
Composer::matchFuzzyName(QStringList elements) const
313
{
314
    if (elements.empty()) return 0;
315

    
316
    cacheNames();
317
    int fameBonus = m_pages.size();
318
    
319
    EditDistance ed(EditDistance::RestrictedTransposition);
320
    
321
    int score = 0;
322
    bool haveSurname = false;
323

    
324
    // We aim to scale the eventual result such that a score of 1.0 or
325
    // more indicates near-certainty that this is a correct match
326
    // (i.e. that it is properly matched -- not that it is the only
327
    // possible match).  To achieve this score, we need to have
328
    // matched with reasonable confidence every element in the passed
329
    // elements list, and to have matched at least one of them to a
330
    // part of our surname.
331

    
332
    int matched = 0;
333
    int unmatched = 0;
334

    
335
    foreach (QString elt, elements) {
336

    
337
        bool accept = false;
338

    
339
        if (elt.length() == 1) {
340
            // An initial: search forenames only, ignoring
341
            // connectives.  The score contribution here is low, but
342
            // they do not count to matched which means the score can
343
            // only enhance whatever happens elsewhere.  They can
344
            // however seriously damage our score if unmatched, which
345
            // is as it should be.
346
            foreach (QString s, m_forenameElements) {
347
                if (s[0] == elt[0]) {
348
                    score += 2;
349
                    accept = true;
350
                    break;
351
                }
352
            }
353
            if (!accept) {
354
                foreach (QString s, m_connectiveElements) {
355
                    if (s[0] == elt[0]) {
356
                        score += 1;
357
                        accept = true;
358
                        break;
359
                    }
360
                }
361
            }
362
            if (!accept) {
363
                foreach (QString s, m_surnameElements) {
364
                    if (s[0] == elt[0]) {
365
                        // no score, but don't call it unmatched
366
                        accept = true;
367
                        break;
368
                    }
369
                }
370
            }
371
            if (!accept) ++unmatched;
372
            continue;
373
        }
374
        
375
        foreach (QString s, m_surnameElements) {
376
            int dist = calculateThresholdedDistance(ed, elt, s);
377
            if (dist >= 0) {
378
                score += 22 - dist*2;
379
                if (elt[0] != s[0]) score -= 10;
380
                accept = true;
381
//                std::cerr << "[surname: " << s.toStdString() << "]" << std::endl;
382
                break;
383
            }
384
        }
385
        if (accept) {
386
            haveSurname = true;
387
            ++matched;
388
            continue;
389
        }
390

    
391
        foreach (QString s, m_forenameElements) {
392
            int dist = calculateThresholdedDistance(ed, elt, s);
393
            if (dist >= 0) {
394
                score += 22 - dist*2;
395
                if (elt[0] != s[0]) score -= 10;
396
                accept = true;
397
//                std::cerr << "[forename: " << s.toStdString() << "]" << std::endl;
398
                break;
399
            }
400
        }
401
        if (accept) {
402
            ++matched;
403
            continue;
404
        }
405

    
406
        foreach (QString s, m_connectiveElements) {
407
            // treated much like initials
408
            int dist = calculateThresholdedDistance(ed, elt, s);
409
            if (dist == 0) {
410
                score += 2;
411
                accept = true;
412
            } else if (dist == 1) {
413
                score += 1;
414
                accept = true;
415
            }
416
            if (accept) {
417
//                std::cerr << "[connective: " << s.toStdString() << "]" << std::endl;
418
                break;
419
            }
420
        }
421
        if (accept) {
422
            continue;
423
        }
424

    
425
        QString reduced = reduceName(elt);
426

    
427
        //!!! these don't seem to match often...
428

    
429
        if (m_reducedSurnameElements.contains(reduced)) {
430
            score += 10;
431
            haveSurname = true;
432
            ++matched;
433
            std::cerr << "[reduced surname: " << elt.toStdString() << "]" << std::endl;
434
            continue;
435
        }
436

    
437
        if (m_reducedForenameElements.contains(reduced)) {
438
            score += 7;
439
            ++matched;
440
            std::cerr << "[reduced forename: " << elt.toStdString() << "]" << std::endl;
441
            continue;
442
        }
443

    
444
        foreach (QString s, m_otherElements) {
445
            int dist = calculateThresholdedDistance(ed, elt, s);
446
            if (dist >= 0) {
447
                score += 22 - dist*2;
448
                if (elt[0] != s[0]) score -= 10;
449
                accept = true;
450
//                std::cerr << "[other: " << s.toStdString() << "]" << std::endl;
451
                break;
452
            }
453
        }
454
        if (accept) {
455
            ++matched;
456
            continue;
457
        }
458

    
459
        ++unmatched;
460
    }
461

    
462
//    if (fameBonus > 0) std::cerr << "[fame: " << fameBonus << "]" << std::endl;
463
    score += fameBonus;
464
        
465
    if (matched == 0) {
466
        if (unmatched == 0) {
467
            return float(score) / 20.f;
468
        } else {
469
            return 0;
470
        }
471
    }
472

    
473
    float fscore = score;
474
    float divisor = (matched + unmatched) * 20;
475

    
476
    if (!haveSurname) fscore /= 2;
477
    if (unmatched > 0) fscore /= 1.5;
478

    
479
    fscore /= divisor;
480

    
481
    if (matched > 0) {
482
//        std::cerr << "[score " << score << " with divisor " << divisor << " for " << name().toStdString() << " adjusted to " << fscore << "]" << std::endl;
483
    }
484

    
485
    return fscore;
486
}
487

    
488
float
489
Composer::matchTyping(QString t) const
490
{
491
    return doMatchTyping(t, false);
492
}
493

    
494
float
495
Composer::matchTypingQuick(QString t) const
496
{
497
    return doMatchTyping(t, true);
498
}
499

    
500
float
501
Composer::doMatchTyping(QString t, bool quick) const
502
{
503
    if (t == "") return 0;
504

    
505
    cacheNames();
506
    float fameBonus = m_pages.size() / 400.f;
507
    
508
    QString n = name().toLower();
509
    t = t.toLower();
510

    
511
    if (n == t) return 1.f + fameBonus;
512
    if (n.startsWith(t)) return 0.8f + fameBonus;
513

    
514
    QSet<QString> sl;
515
    QSet<QString> nl;
516
    foreach (QString s, m_surnameElements) {
517
        sl.insert(s.toLower());
518
        nl.insert(s.toLower());
519
    }
520
    foreach (QString s, m_forenameElements) {
521
        nl.insert(s.toLower());
522
    }
523
    if (!quick) {
524
        foreach (QString s, m_otherElements) {
525
            nl.insert(s.toLower());
526
        }
527
        foreach (QString s, m_connectiveElements) {
528
            nl.insert(s.toLower());
529
        }
530
    }
531

    
532
    static QRegExp sre("[\\., -]+");
533
    QStringList tl = t.split(sre, QString::SkipEmptyParts);
534
    
535
    float score = 0.f;
536

    
537
    if (nl.empty() || tl.empty()) return 0.f;
538
    
539
    int unmatched = 0;
540

    
541
    for (int i = 0; i < tl.size(); ++i) {
542

    
543
        QString tel = tl[i];
544
        float component = 0.f;
545
        float max = 0.f;
546

    
547
        for (QSet<QString>::const_iterator ni = nl.begin();
548
             ni != nl.end(); ++ni) {
549

    
550
            QString nel = ni->toLower();
551

    
552
            if (tel == nel) {
553
                if (tel.length() > 1) {
554
                    component = 0.2;
555
                } else {
556
                    component = 0.1;
557
                }
558
                if (sl.contains(nel)) component *= 1.5;
559
                goto calculated;
560
            }
561

    
562
            if (nel.startsWith(tel)) {
563
                component = 0.1;
564
                if (sl.contains(nel)) component *= 1.5;
565
                goto calculated;
566
            }
567

    
568
            if (!quick) {
569
                if (tel.length() > 3) {
570
                    EditDistance ed(EditDistance::RestrictedTransposition);
571
                    int dist = calculateThresholdedDistance
572
                        (ed, nel.left(tel.length()), tel);
573
                    if (dist >= 0) {
574
                        component = 0.08 - dist * 0.01;
575
                        if (sl.contains(nel)) component *= 1.5;
576
                    }
577
                }
578
                if (component > 0.f) goto calculated;
579
            }
580

    
581
            if (nel.startsWith(tel[0])) {
582
                component += 0.02;
583
            }
584

    
585
        calculated:
586
            if (component > max) max = component;
587
        }
588

    
589
        score += max;
590
    }
591

    
592
    if (!quick) {
593
        if (t.contains(" ")) {
594
            float fuzzyScore = matchFuzzyName(t);
595
            if (fuzzyScore >= 0.4f) {
596
                score += fuzzyScore / 3.f;
597
            }
598
        }
599
    }
600

    
601
    if (score > 0.f) score += fameBonus;
602
    return score;
603
}
604

    
605
void
606
Composer::mergeFrom(Composer *c)
607
{
608
    QSet<QString> allNames = c->aliases();
609
    allNames.insert(c->name());
610
        
611
    foreach (QString n, allNames) {
612
        if (n != m_name && !m_aliases.contains(n)) {
613
            m_aliases.insert(n);
614
            m_namesCached = false;
615
        }
616
    }
617

    
618
    if (!m_birth) {
619
        if (c->birth()) {
620
            m_birth = new Birth(*c->birth());
621
            emit birthChanged(m_birth);
622
        }
623
    }
624

    
625
    if (!m_death) {
626
        if (c->death()) {
627
            m_death = new Death(*c->death());
628
            emit deathChanged(m_death);
629
        }
630
    }
631
        
632
    if (c->gender() != "") {
633
        if (m_gender == "") {
634
            m_gender = c->gender();
635
            emit genderChanged(m_gender);
636
        } else if (c->gender() != m_gender) {
637
            std::cerr << "WARNING: Composer::mergeFrom: Gender mismatch! Composer " << c->name().toStdString() << " has gender " << c->gender().toStdString() << ", but target composer " << m_name.toStdString() << " has gender " << m_gender.toStdString() << std::endl;
638
        }
639
    }
640

    
641
    m_nationality.unite(c->nationality());
642
    m_geonameURIs.unite(c->geonameURIs());
643
    m_otherURIs.unite(c->otherURIs());
644

    
645
    foreach (Document *d, c->pages()) {
646
/*
647
        Document *dd = new Document;
648
        dd->setUri(d->uri());
649
        dd->setSiteName(d->siteName());
650
        dd->setTopic(this);
651
        m_pages.insert(dd);
652
*/
653
        d->setTopic(this);
654
        m_pages.insert(d);
655
    }
656
    
657
    if (m_period == "") m_period = c->period();
658
    if (m_remarks == "") m_remarks = c->remarks();
659

    
660
    emit nationalityChanged(m_nationality);
661
    emit geonameURIsChanged(m_geonameURIs);
662
    emit otherURIsChanged(m_otherURIs);
663
    emit pagesChanged(m_pages);
664
    emit periodChanged(m_period);
665
    emit remarksChanged(m_remarks);
666
    emit aliasesChanged(m_aliases);
667
}
668

    
669
QString
670
Work::getComposerName() const
671
{
672
    Composer *c = getComposer();
673
    if (c) return c->name();
674
    else return "";
675
}
676

    
677
static int
678
compare(QString a, QString b)
679
{
680
    if (a < b) {
681
        return -1;
682
    } else if (a > b) {
683
        return 1;
684
    } else {
685
        return 0;
686
    }
687
}
688

    
689
int
690
Work::compareCatalogueNumberTexts(QString a, QString b)
691
{
692
//    std::cout << "compare " << a.toStdString()
693
//              << " :: " << b.toStdString() << std::endl;
694

    
695
    if (a == b) return 0;
696

    
697
    if (!a[0].isDigit()) {
698
        a.replace(QRegExp("^[^\\d]+"), "");
699
    }
700

    
701
    if (!b[0].isDigit()) {
702
        b.replace(QRegExp("^[^\\d]+"), "");
703
    }
704

    
705
    QStringList al = a.split(QRegExp("\\b[^\\d]*"), QString::SkipEmptyParts);
706
    QStringList bl = b.split(QRegExp("\\b[^\\d]*"), QString::SkipEmptyParts);
707
    if (al.size() != bl.size()) return int(al.size()) - int(bl.size());
708

    
709
/*    if (al.size() < 2 || bl.size() < 2 || al.size() != bl.size()) {
710
        if (a < b) return -1;
711
        else if (a > b) return 1;
712
        else return 0;
713
    }
714
*/
715
    for (int i = 0; i < al.size(); ++i) {
716
        if (al[i] != bl[i]) {
717
            // use atoi instead of toInt() because we want it to succeed even
718
            // if the text is not only an integer (e.g. 35a)
719
            int aoi = atoi(al[i].toLocal8Bit().data());
720
            int boi = atoi(bl[i].toLocal8Bit().data());
721
            if (aoi != boi) return aoi - boi;
722
            else return compare(al[i], bl[i]);
723
        }
724
    }
725
    return 0;
726
}
727

    
728
QStringList
729
Work::extractCatalogueNumberTexts(QString text)
730
{
731
    //!!! test this
732
    QStringList results;
733
    std::cerr << "Work::extractCatalogueNumberTexts(" << text.toStdString() << ")" << std::endl;
734

    
735
    // Note we explicitly exclude "catalogue identifiers" beginning
736
    // with N, because we don't want to treat e.g. "Symphony No. 8"
737
    // as catalogue number 8.  What a fine hack.
738

    
739
    QRegExp catre("\\b([Oo]pu?s?|[A-MP-Z]+)\\.?[\\s_]*(\\d+\\w*)(\\s+[Nn]([OoRrBb]?|umber)(\\.\\s*|\\s+)(\\d+\\w*))?\\b");
740
    int ix = 0;
741
    while ((ix = catre.indexIn(text, ix+1)) >= 0) {
742
        std::cerr << "extractCatalogueNumberTexts: found match \"" << catre.cap(0).toStdString() << "\"" << std::endl;
743
        QString cat = catre.cap(0);
744
        // ensure space before digit
745
        for (int i = 0; i+1 < cat.length(); ++i) {
746
            if (!cat[i].isDigit() && !cat[i].isSpace() && cat[i+1].isDigit()) {
747
                QString spaced = cat.left(i+1) + " " + cat.right(cat.length()-i-1);
748
                std::cerr << "spaced out from " << cat.toStdString() << " to "
749
                          << spaced.toStdString() << std::endl;
750
                cat = spaced;
751
                break;
752
            }
753
        }
754
        results.push_back(cat);
755
    }
756
    return results;
757
}
758

    
759
bool
760
Work::Ordering::operator()(Work *a, Work *b)
761
{
762
    if (!a) {
763
        if (!b) return false;
764
        else return true;
765
    } else {
766
        if (!b) {
767
            return false;
768
        }
769
    }
770
/*
771
    QString ao = a->catalogue();
772
    if (ao == "") ao = a->opus();
773

774
    QString bo = b->catalogue();
775
    if (bo == "") bo = b->opus();
776

777
    std::cout << "ao " << ao.toStdString() << ", bo " << bo.toStdString() << std::endl;
778
*/
779
    int c = 0;
780
    if (a->catalogue() != "" && b->catalogue() != "") {
781
        c = compareCatalogueNumberTexts(a->catalogue(), b->catalogue());
782
    }
783
    if (c == 0 && a->opus() != "" && b->opus() != "") {
784
        c = compareCatalogueNumberTexts(a->opus(), b->opus());
785
    }
786
    if (c == 0 && a->partOf() == b->partOf() &&
787
        a->number() != "" && b->number() != "") {
788
        c = compareCatalogueNumberTexts(a->number(), b->number());
789
    }
790

    
791
    bool rv = false;
792

    
793
    if (c == 0) {
794
        if (a->name() == b->name()) rv = (a < b);
795
        else rv = (a->name() < b->name());
796
    } else {
797
        rv = (c < 0);
798
    }
799

    
800
//    std::cout << "result = " << rv << std::endl;
801
    return rv;
802
}
803

    
804
QString
805
Work::getDisplayName() const
806
{
807
    QString suffix;
808

    
809
    if (catalogue() != "") {
810
        suffix = catalogue();
811
    } else if (opus() != "") {
812
        suffix = QString("Op. %1").arg(opus());
813
    }
814
    if (suffix != "" && number() != "") {
815
        suffix = QString("%1 no. %2").arg(suffix).arg(number());
816
    }
817
    if (suffix != "") {
818
        if (name() != "") {
819
            return QString("%1, %2").arg(name()).arg(suffix);
820
        } else {
821
            return suffix;
822
        }
823
    } else {
824
        return name();
825
    }
826
}
827

    
828
AudioFile::AudioFile(QObject *parent) :
829
    QObject(parent)
830
{
831
}
832

    
833
AudioFile::AudioFile(FileSource source, QObject *parent) :
834
    QObject(parent)
835
{
836
    if (source.isAvailable()) {
837
        QFile f(source.getLocalFilename());
838
        f.open(QIODevice::ReadOnly);
839
        //!!! stream this!
840
        QByteArray ba = f.readAll();
841
        m_hash = QString::fromLatin1
842
            (QCryptographicHash::hash(ba, QCryptographicHash::Sha1).toHex());
843
    }
844
    QString location = source.getLocation();
845
    if (source.isRemote()) {
846
        m_uri = Dataquay::Uri(location);
847
    } else {
848
        if (location.contains("://")) {
849
            m_uri = Dataquay::Uri(location);
850
        } else if (location.startsWith('/')) {
851
            m_uri = Dataquay::Uri("file://" + location);
852
        } else {
853
            m_uri = Dataquay::Uri("file://" + QFileInfo(location).canonicalFilePath());
854
        }
855
    }
856

    
857
    std::cerr << "AudioFile::AudioFile: hash = " << m_hash.toStdString()
858
              << ", uri = " << m_uri.toString().toStdString() << std::endl;
859
}
860

    
861
AudioFile::~AudioFile()
862
{
863
    foreach (AudioFileTag *t, m_tags) delete t;
864
}
865

    
866
void
867
AudioFile::setTags(QSet<AudioFileTag *> tt)
868
{
869
    foreach (AudioFileTag *t, m_tags) {
870
        if (!tt.contains(t)) delete t;
871
    }
872
    m_tags = tt;
873
}
874

    
875
void
876
AudioFile::addTag(AudioFileTag *t)
877
{
878
    m_tags.insert(t);
879
}
880

    
881
}
882