File indexing completed on 2024-04-28 03:51:00

0001 /****************************************************************************
0002 **
0003 ** Copyright (C) 2016 by Sandro S. Andrade <sandroandrade@kde.org>
0004 **
0005 ** This program is free software; you can redistribute it and/or
0006 ** modify it under the terms of the GNU General Public License as
0007 ** published by the Free Software Foundation; either version 2 of
0008 ** the License or (at your option) version 3 or any later version
0009 ** accepted by the membership of KDE e.V. (or its successor approved
0010 ** by the membership of KDE e.V.), which shall act as a proxy
0011 ** defined in Section 14 of version 3 of the license.
0012 **
0013 ** This program is distributed in the hope that it will be useful,
0014 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
0015 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0016 ** GNU General Public License for more details.
0017 **
0018 ** You should have received a copy of the GNU General Public License
0019 ** along with this program.  If not, see <http://www.gnu.org/licenses/>.
0020 **
0021 ****************************************************************************/
0022 
0023 #include "exercisecontroller.h"
0024 
0025 #if !defined(Q_OS_ANDROID)
0026 #include <KLocalizedString>
0027 #endif
0028 
0029 #include <QDateTime>
0030 #include <QDir>
0031 #include <QJsonDocument>
0032 #include <QRandomGenerator>
0033 #include <QStandardPaths>
0034 #include <qqml.h>
0035 
0036 #include <utils/xdgdatadirs.h>
0037 
0038 namespace Minuet
0039 {
0040 ExerciseController::ExerciseController(QObject *parent)
0041     : IExerciseController(parent), m_chosenRootNote(0)
0042 {
0043     m_exercises[QStringLiteral("exercises")] = QJsonArray();
0044     m_definitions[QStringLiteral("definitions")] = QJsonArray();
0045 }
0046 
0047 bool ExerciseController::initialize(Core *core)
0048 {
0049     Q_UNUSED(core)
0050 
0051     m_errorString.clear();
0052     bool definitionsMerge = mergeJsonFiles(QStringLiteral("definitions"), m_definitions);
0053     bool exercisesMerge = mergeJsonFiles(QStringLiteral("exercises"), m_exercises, true,
0054                                          QStringLiteral("name"), QStringLiteral("children"));
0055 
0056     //    QFile file("merged-exercises.json");
0057     //    file.open(QIODevice::WriteOnly);
0058     //    file.write(QJsonDocument(m_exercises).toJson());
0059     //    file.close();
0060 
0061     return definitionsMerge & exercisesMerge;
0062 }
0063 
0064 QString ExerciseController::errorString() const
0065 {
0066     return m_errorString;
0067 }
0068 
0069 void ExerciseController::randomlySelectExerciseOptions()
0070 {
0071     while (!m_selectedExerciseOptions.isEmpty()) {
0072         m_selectedExerciseOptions.removeFirst();
0073     }
0074 
0075     int minNote = INT_MAX;
0076     int maxNote = INT_MIN;
0077     auto *generator = QRandomGenerator::global();
0078     quint8 numberOfSelectedOptions
0079         = m_currentExercise[QStringLiteral("numberOfSelectedOptions")].toInt();
0080     for (quint8 i = 0; i < numberOfSelectedOptions; ++i) {
0081         QJsonArray exerciseOptions
0082             = QJsonObject::fromVariantMap(m_currentExercise)[QStringLiteral("options")].toArray();
0083         quint8 chosenExerciseOption = generator->bounded(exerciseOptions.size());
0084 
0085         QString sequence = exerciseOptions[chosenExerciseOption]
0086                                .toObject()[QStringLiteral("sequence")]
0087                                .toString();
0088         foreach (const QString &additionalNote, sequence.split(' ')) {
0089             int note = additionalNote.toInt();
0090             if (note > maxNote) {
0091                 maxNote = note;
0092             }
0093             if (note < minNote) {
0094                 minNote = note;
0095             }
0096         }
0097         if (m_currentExercise[QStringLiteral("playMode")].toString() != QLatin1String("rhythm")) {
0098             QStringList exerciseRoots
0099                 = m_currentExercise[QStringLiteral("root")].toString().split('.');
0100             quint8 exerciseMinRoot = exerciseRoots.first().toInt();
0101             quint8 exerciseMaxRoot = exerciseRoots.last().toInt();
0102             do {
0103                 m_chosenRootNote
0104                     = exerciseMinRoot + generator->bounded(exerciseMaxRoot - exerciseMinRoot);
0105             } while (m_chosenRootNote + maxNote > 108 || m_chosenRootNote + minNote < 21);
0106         }
0107 
0108         QJsonObject jsonObject = exerciseOptions[chosenExerciseOption].toObject();
0109         jsonObject[QStringLiteral("rootNote")] = QString::number(m_chosenRootNote);
0110         exerciseOptions[chosenExerciseOption] = jsonObject;
0111         m_selectedExerciseOptions.append(exerciseOptions[chosenExerciseOption]);
0112     }
0113     emit selectedExerciseOptionsChanged(m_selectedExerciseOptions);
0114 }
0115 
0116 unsigned int ExerciseController::chosenRootNote() const
0117 {
0118     return m_chosenRootNote;
0119 }
0120 
0121 QJsonArray ExerciseController::exercises() const
0122 {
0123     return m_exercises[QStringLiteral("exercises")].toArray();
0124 }
0125 
0126 bool ExerciseController::mergeJsonFiles(const QString directoryName, QJsonObject &targetObject,
0127                                         bool applyDefinitionsFlag, QString commonKey,
0128                                         QString mergeKey)
0129 {
0130     QStringList jsonDirs;
0131 #if defined(Q_OS_ANDROID)
0132     jsonDirs << "assets:/data/" + directoryName;
0133 #elif defined(Q_OS_WIN)
0134     jsonDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation,
0135                                          QStringLiteral("minuet/") + directoryName,
0136                                          QStandardPaths::LocateDirectory);
0137 #else
0138     jsonDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, directoryName,
0139                                          QStandardPaths::LocateDirectory);
0140 #ifdef Q_OS_MACOS
0141     if (jsonDirs.isEmpty()) {
0142         const QStringList xdgDataDirs = Utils::getXdgDataDirs();
0143         for (const auto &dirPath : xdgDataDirs) {
0144             const QDir testDir(
0145                 QDir(dirPath).absoluteFilePath(QStringLiteral("minuet/") + directoryName));
0146             if (testDir.exists()) {
0147                 jsonDirs << testDir.absolutePath();
0148             }
0149         }
0150     }
0151 #endif
0152 #endif
0153     foreach (const QString &jsonDirString, jsonDirs) {
0154         QDir jsonDir(jsonDirString);
0155         foreach (const QString &json, jsonDir.entryList(QDir::Files)) {
0156             if (!json.endsWith(QLatin1String(".json"))) {
0157                 break;
0158             }
0159             QFile jsonFile(jsonDir.absoluteFilePath(json));
0160             if (!jsonFile.open(QIODevice::ReadOnly)) {
0161 #if !defined(Q_OS_ANDROID)
0162                 m_errorString
0163                     = i18n("Could not open JSON file \"%1\".", jsonDir.absoluteFilePath(json));
0164 #else
0165                 m_errorString = QStringLiteral("Couldn't open json file \"%1\".")
0166                                     .arg(jsonDir.absoluteFilePath(json));
0167 #endif
0168                 return false;
0169             }
0170             QJsonParseError error;
0171             QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonFile.readAll(), &error);
0172 
0173             if (error.error != QJsonParseError::NoError) {
0174                 m_errorString += QStringLiteral("Error when parsing JSON file '%1'. ")
0175                                      .arg(jsonDir.absoluteFilePath(json));
0176                 jsonFile.close();
0177                 return false;
0178             }
0179             QJsonObject jsonObject = jsonDocument.object();
0180             if (applyDefinitionsFlag) {
0181                 jsonObject[directoryName]
0182                     = applyDefinitions(jsonObject[directoryName].toArray(),
0183                                        m_definitions[QStringLiteral("definitions")].toArray());
0184             }
0185 
0186             targetObject[directoryName]
0187                 = mergeJsonArrays(targetObject[directoryName].toArray(),
0188                                   jsonObject[directoryName].toArray(), commonKey, mergeKey);
0189             jsonFile.close();
0190         }
0191     }
0192     return true;
0193 }
0194 
0195 QJsonArray ExerciseController::applyDefinitions(QJsonArray exercises, QJsonArray definitions,
0196                                                 QJsonObject collectedProperties)
0197 {
0198     QJsonArray::const_iterator exercisesBegin = exercises.constBegin();
0199     QJsonArray::const_iterator exercisesEnd = exercises.constEnd();
0200     for (QJsonArray::ConstIterator i1 = exercisesBegin; i1 < exercisesEnd; ++i1) {
0201         if (i1->isObject()) {
0202             QJsonObject exerciseObject = i1->toObject();
0203             QJsonArray filteredDefinitions = definitions;
0204             QStringList exerciseObjectKeys = exerciseObject.keys();
0205             if (exerciseObjectKeys.contains(QStringLiteral("and-tags"))
0206                 && exerciseObject[QStringLiteral("and-tags")].isArray()) {
0207                 filterDefinitions(filteredDefinitions, exerciseObject, QStringLiteral("and-tags"),
0208                                   AndFiltering);
0209             }
0210             if (exerciseObjectKeys.contains(QStringLiteral("or-tags"))
0211                 && exerciseObject[QStringLiteral("or-tags")].isArray()) {
0212                 filterDefinitions(filteredDefinitions, exerciseObject, QStringLiteral("or-tags"),
0213                                   OrFiltering);
0214             }
0215             if (exerciseObjectKeys.contains(QStringLiteral("children"))) {
0216                 foreach (const QString &key, exerciseObjectKeys)
0217                     if (key != QLatin1String("name") && key != QLatin1String("children")
0218                         && key != QLatin1String("and-tags") && key != QLatin1String("or-tags")
0219                         && !key.startsWith('_')) {
0220                         collectedProperties.insert(key, exerciseObject[key]);
0221                         exerciseObject.remove(key);
0222                     }
0223                 exerciseObject[QStringLiteral("children")]
0224                     = applyDefinitions(exerciseObject[QStringLiteral("children")].toArray(),
0225                                        filteredDefinitions, collectedProperties);
0226             } else {
0227                 foreach (const QString &key, collectedProperties.keys())
0228                     if (!exerciseObject.contains(key)) {
0229                         exerciseObject.insert(key, collectedProperties[key]);
0230                     }
0231                 exerciseObject.insert(QStringLiteral("options"), filteredDefinitions);
0232             }
0233             exercises[i1 - exercisesBegin] = exerciseObject;
0234         }
0235     }
0236     return exercises;
0237 }
0238 
0239 void ExerciseController::filterDefinitions(QJsonArray &definitions, QJsonObject &exerciseObject,
0240                                            const QString &filterTagsKey,
0241                                            DefinitionFilteringMode definitionFilteringMode)
0242 {
0243     QJsonArray filterTags = exerciseObject[filterTagsKey].toArray();
0244     exerciseObject.remove(filterTagsKey);
0245     for (QJsonArray::Iterator i2 = definitions.begin(); i2 < definitions.end(); ++i2) {
0246         bool remove = definitionFilteringMode != AndFiltering;
0247         QJsonArray::const_iterator filterTagsEnd = filterTags.constEnd();
0248         for (QJsonArray::ConstIterator i3 = filterTags.constBegin(); i3 < filterTagsEnd; ++i3) {
0249             QJsonArray tagArray = i2->toObject()[QStringLiteral("tags")].toArray();
0250             if (definitionFilteringMode == AndFiltering && !tagArray.contains(*i3)) {
0251                 remove = true;
0252                 break;
0253             }
0254             if (definitionFilteringMode == OrFiltering && tagArray.contains(*i3)) {
0255                 remove = false;
0256             }
0257         }
0258         if (remove) {
0259             i2 = definitions.erase(i2);
0260             i2--;
0261         }
0262     }
0263 }
0264 
0265 QJsonArray ExerciseController::mergeJsonArrays(QJsonArray oldFile, QJsonArray newFile,
0266                                                QString commonKey, QString mergeKey)
0267 {
0268     QJsonArray::const_iterator newFileEnd = newFile.constEnd();
0269     ;
0270     for (QJsonArray::ConstIterator i1 = newFile.constBegin(); i1 < newFileEnd; ++i1) {
0271         if (i1->isObject()) {
0272             QJsonArray::ConstIterator i2;
0273             QJsonArray::const_iterator oldFileEnd = oldFile.constEnd();
0274             for (i2 = oldFile.constBegin(); i2 < oldFileEnd; ++i2) {
0275                 QJsonObject newFileObject = i1->toObject();
0276                 QJsonObject oldFileObject = i2->toObject();
0277                 if (i2->isObject() && i1->isObject() && !commonKey.isEmpty()
0278                     && oldFileObject[commonKey] == newFileObject[commonKey]) {
0279                     QJsonObject jsonObject = oldFile[i2 - oldFile.constBegin()].toObject();
0280                     jsonObject[mergeKey]
0281                         = mergeJsonArrays(oldFileObject[mergeKey].toArray(),
0282                                           newFileObject[mergeKey].toArray(), commonKey, mergeKey);
0283                     oldFile[i2 - oldFile.constBegin()] = jsonObject;
0284                     break;
0285                 }
0286             }
0287             if (i2 == oldFile.constEnd()) {
0288                 oldFile.append(*i1);
0289             }
0290         }
0291     }
0292     return oldFile;
0293 }
0294 
0295 }
0296 
0297 #include "moc_exercisecontroller.cpp"