Skip to content

Commit f3fc49d

Browse files
committed
Add automatic breadcrumb generation
1 parent 3a7dfea commit f3fc49d

File tree

4 files changed

+199
-7
lines changed

4 files changed

+199
-7
lines changed

src/db/database.cpp

+174-5
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@
3535

3636

3737
/**
38-
* Creates a new Database object, immediately creates the table models and initializes the
39-
* SQLite database driver.
38+
* Creates a new Database object, immediately creates the table models and initializes the SQLite
39+
* database driver.
4040
*/
4141
Database::Database() :
4242
databaseLoaded(false),
4343
tables(QList<Table*>()),
44+
breadcrumbMatrix(QMap<const NormalTable*, QMap<const Table*, Breadcrumbs>>()),
4445
mainWindowStatusBar(nullptr)
4546
{
4647
tripsTable = new TripsTable();
@@ -67,6 +68,8 @@ Database::Database() :
6768

6869
projectSettings = new ProjectSettings(settingsTable);
6970

71+
computeBreadcrumbMatrix();
72+
7073

7174
QSqlDatabase::addDatabase("QSQLITE");
7275
}
@@ -75,7 +78,7 @@ Database::Database() :
7578
* Destroys the Database object.
7679
*/
7780
Database::~Database() {
78-
qDeleteAll(getTableList());
81+
qDeleteAll(tables);
7982
}
8083

8184

@@ -244,6 +247,151 @@ void Database::populateBuffers(QWidget* parent)
244247

245248

246249

250+
/**
251+
* For all pairs of normal tables and any other table (always excluding the project settings table),
252+
* computes the breadcrumbs for the connection between them and stores them in the breadcrumbMatrix.
253+
*
254+
* The algorithm consists of three steps:
255+
* 1. Fill the diagonal with empty breadcrumbs (since connecting a table to itself does not
256+
* require any breadcrumbs).
257+
* 2. Proactively find all trivial connections by iterating through all tables and adding
258+
* breadcrumbs consisting of a single column pair for each foreign key column.
259+
* This step adds two breadcrumbs for each normal table and four for each associative table.
260+
* 3. Iteratively find the remaining connections by chaining existing ones together. During this
261+
* process, the maximum length of accepted new breadcrumb chains is gradually increased to
262+
* prevent forming connections which turn back on themselves.
263+
*/
264+
void Database::computeBreadcrumbMatrix()
265+
{
266+
const int numNormalTables = getNormalItemTableList().size();
267+
const int numCells = numNormalTables * getItemTableList().size();
268+
int numFilled = 0;
269+
270+
// Fill empty diagonal (normal table to itself)
271+
for (const NormalTable* const normalTable : getNormalItemTableList()) {
272+
breadcrumbMatrix[normalTable][normalTable] = Breadcrumbs();
273+
numFilled++;
274+
}
275+
276+
// Fill trivial connections (one crumb or two over an associative table)
277+
for (const Table* const table : getItemTableList()) {
278+
if (table->isAssociative) {
279+
// Associative table
280+
const AssociativeTable* const associativeTable = (AssociativeTable*) table;
281+
PrimaryForeignKeyColumn* const column1 = associativeTable->getColumn1();
282+
PrimaryForeignKeyColumn* const column2 = associativeTable->getColumn2();
283+
PrimaryKeyColumn* const foreignColumn1 = column1->getReferencedForeignColumn();
284+
PrimaryKeyColumn* const foreignColumn2 = column2->getReferencedForeignColumn();
285+
const NormalTable* const foreignTable1 = (NormalTable*) foreignColumn1->table;
286+
const NormalTable* const foreignTable2 = (NormalTable*) foreignColumn2->table;
287+
288+
assert(breadcrumbMatrix[foreignTable1][table].isEmpty());
289+
assert(breadcrumbMatrix[foreignTable2][table].isEmpty());
290+
assert(breadcrumbMatrix[foreignTable1][foreignTable2].isEmpty());
291+
assert(breadcrumbMatrix[foreignTable2][foreignTable1].isEmpty());
292+
293+
// Foreign table to associative table (either side)
294+
breadcrumbMatrix[foreignTable1][table] = Breadcrumbs({
295+
{foreignColumn1, column1}
296+
});
297+
breadcrumbMatrix[foreignTable2][table] = Breadcrumbs({
298+
{foreignColumn2, column2}
299+
});
300+
// Foreign table to other foreign table via associative table (either side)
301+
breadcrumbMatrix[foreignTable1][foreignTable2] = Breadcrumbs({
302+
{foreignColumn1, column1},
303+
{column2, foreignColumn2}
304+
});
305+
breadcrumbMatrix[foreignTable2][foreignTable1] = Breadcrumbs({
306+
{foreignColumn2, column2},
307+
{column1, foreignColumn1}
308+
});
309+
numFilled += 4;
310+
}
311+
312+
else {
313+
// Normal table
314+
const NormalTable* const normalTable = (NormalTable*) table;
315+
QList<const Column*> foreignKeyColumns = normalTable->getForeignKeyColumnList();
316+
for (const Column* const column : foreignKeyColumns) {
317+
ForeignKeyColumn* localColumn = (ForeignKeyColumn*) column;
318+
PrimaryKeyColumn* foreignColumn = column->getReferencedForeignColumn();
319+
const NormalTable* const foreignTable = (NormalTable*) foreignColumn->table;
320+
321+
assert(breadcrumbMatrix[normalTable][foreignTable].isEmpty());
322+
assert(breadcrumbMatrix[foreignTable][normalTable].isEmpty());
323+
324+
breadcrumbMatrix[normalTable][foreignTable] = Breadcrumbs({
325+
{localColumn, foreignColumn}
326+
});
327+
breadcrumbMatrix[foreignTable][normalTable] = Breadcrumbs({
328+
{foreignColumn, localColumn}
329+
});
330+
numFilled += 2;
331+
}
332+
}
333+
}
334+
335+
// Iteratively find the remaining connections by chaining existing ones
336+
int connectionLengthLimit = 2;
337+
// Gradually increase the maximum length of combined connections which are accepted to prevent
338+
// ending up with connections which turn back on themselves.
339+
while (numFilled < numCells) {
340+
const int numFilledBeforeIter = numFilled;
341+
342+
for (const NormalTable* const tableA : getNormalItemTableList()) {
343+
for (const NormalTable* const tableB : getNormalItemTableList()) {
344+
if (tableA == tableB) continue;
345+
if (breadcrumbMatrix[tableA][tableB].isEmpty()) continue;
346+
// We are on a connection A -> B which has already been found.
347+
// We are now looking for another existing connection B -> C. We then derive A -> C.
348+
349+
for (const Table* const tableC : getItemTableList()) {
350+
if (tableB == tableC || tableA == tableC) continue;
351+
if (breadcrumbMatrix[tableB][tableC].isEmpty()) continue;
352+
// We have found an existing connection B -> C.
353+
if (!breadcrumbMatrix[tableA][tableC].isEmpty()) continue;
354+
355+
const Breadcrumbs& breadcrumbsAtoB = breadcrumbMatrix[tableA][tableB];
356+
const Breadcrumbs& breadcrumbsBtoC = breadcrumbMatrix[tableB][tableC];
357+
358+
int candidateLength = breadcrumbsAtoB.length() + breadcrumbsBtoC.length();
359+
if (candidateLength > connectionLengthLimit) continue;
360+
361+
breadcrumbMatrix[tableA][tableC] = breadcrumbsAtoB + breadcrumbsBtoC;
362+
numFilled++;
363+
}
364+
}
365+
}
366+
367+
assert(numFilled > numFilledBeforeIter);
368+
connectionLengthLimit++;
369+
}
370+
}
371+
372+
/**
373+
* Returns the pre-computed breadcrumbs for the connection between the given tables.
374+
*
375+
* @param startTable The table to start from (e.g. where the user made a selection).
376+
* @param destinationTable The table which must be reached.
377+
* @return The breadcrumbs for the connection between the given tables.
378+
*/
379+
Breadcrumbs Database::getBreadcrumbsFor(const NormalTable* startTable, const NormalTable* destinationTable)
380+
{
381+
assert(breadcrumbMatrix.contains(startTable));
382+
assert(breadcrumbMatrix.value(startTable).contains(destinationTable));
383+
384+
return breadcrumbMatrix.value(startTable).value(destinationTable);
385+
}
386+
387+
388+
389+
/**
390+
* Translates the given string under the context "Database".
391+
*
392+
* @param string The string to translate.
393+
* @return The translated string.
394+
*/
247395
QString Database::tr(const QString& string)
248396
{
249397
return QCoreApplication::translate("Database", string.toStdString().c_str());
@@ -256,9 +404,30 @@ QString Database::tr(const QString& string)
256404
*
257405
* @return A list of all regular tables (not project settings) in the database.
258406
*/
259-
QList<Table*> Database::getTableList() const
407+
QList<Table*> Database::getItemTableList() const
408+
{
409+
QList<Table*> list = QList<Table*>();
410+
for (Table* const table : tables) {
411+
if (table == settingsTable) continue;
412+
list.append(table);
413+
}
414+
return list;
415+
}
416+
417+
/**
418+
* Returns a list of all normal tables in the database (not including the project settings table).
419+
*
420+
* @return A list of all regular normal tables (not project settings) in the database.
421+
*/
422+
QList<NormalTable*> Database::getNormalItemTableList() const
260423
{
261-
return QList<Table*>(tables);
424+
QList<NormalTable*> list = QList<NormalTable*>();
425+
for (const Table* const table : tables) {
426+
if (table == settingsTable) continue;
427+
if (table->isAssociative) continue;
428+
list.append((NormalTable*) table);
429+
}
430+
return list;
262431
}
263432

264433

src/db/database.h

+10-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#ifndef DATABASE_H
2525
#define DATABASE_H
2626

27+
#include "src/comp_tables/breadcrumbs.h"
2728
#include "src/data/ascent.h"
2829
#include "src/data/peak.h"
2930
#include "src/data/trip.h"
@@ -58,9 +59,12 @@ struct WhatIfDeleteResult;
5859
class Database {
5960
/** Whether a database is currently loaded. */
6061
bool databaseLoaded;
61-
/** The (functionally static) list of tables in any project database. */
62+
/** The (functionally static) list of tables in any project database. Caution: Contains the project settings table! */
6263
QList<Table*> tables;
6364

65+
/** A precomputed matrix of breadcrumb connections from any normal table to any table in the project (settings table always excluded). */
66+
QMap<const NormalTable*, QMap<const Table*, Breadcrumbs>> breadcrumbMatrix;
67+
6468
/** A pointer to the status bar of the main window, used to display status messages. */
6569
QStatusBar* mainWindowStatusBar;
6670

@@ -99,7 +103,8 @@ class Database {
99103
bool saveAs(QWidget* parent, const QString& filepath);
100104
QString getCurrentFilepath() const;
101105

102-
QList<Table*> getTableList() const;
106+
QList<Table*> getItemTableList() const;
107+
QList<NormalTable*> getNormalItemTableList() const;
103108

104109
Ascent* getAscent (ValidItemID ascentID) const;
105110
Peak* getPeak (ValidItemID peakID) const;
@@ -124,7 +129,10 @@ class Database {
124129

125130
void populateBuffers(QWidget* parent);
126131

132+
void computeBreadcrumbMatrix();
127133
public:
134+
Breadcrumbs getBreadcrumbsFor(const NormalTable* startTable, const NormalTable* destinationTable);
135+
128136
static QString tr(const QString& string);
129137

130138
friend class DatabaseUpgrader;

src/db/table.cpp

+14
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ QList<const Column*> Table::getPrimaryKeyColumnList() const
117117
return primaryKeyColumns;
118118
}
119119

120+
/**
121+
* Returns a list of all foreign key columns in the table.
122+
*
123+
* @return A list of all foreign key columns in the table.
124+
*/
125+
QList<const Column*> Table::getForeignKeyColumnList() const
126+
{
127+
QList<const Column*> foreignKeyColumns = QList<const Column*>();
128+
for (const Column* column : columns) {
129+
if (column->foreignColumn) foreignKeyColumns.append(column);
130+
}
131+
return foreignKeyColumns;
132+
}
133+
120134
/**
121135
* Returns a list of all non-primary-key columns in the table.
122136
*

src/db/table.h

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class Table : public QAbstractItemModel {
7575
int getNumberOfPrimaryKeyColumns() const;
7676
QList<const Column*> getColumnList() const;
7777
QList<const Column*> getPrimaryKeyColumnList() const;
78+
QList<const Column*> getForeignKeyColumnList() const;
7879
QList<const Column*> getNonPrimaryKeyColumnList() const;
7980
QString getColumnListString() const;
8081
QString getPrimaryKeyColumnListString() const;

0 commit comments

Comments
 (0)