File indexing completed on 2024-04-28 03:43:17

0001 /*
0002     SPDX-FileCopyrightText: 2021 Kwon-Young Choi <kwon-young.choi@hotmail.fr>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "placeholderpath.h"
0008 
0009 #include "sequencejob.h"
0010 #include "kspaths.h"
0011 
0012 #include <QString>
0013 #include <QStringList>
0014 
0015 #include <cmath>
0016 #include <algorithm>
0017 #include <ekos_capture_debug.h>
0018 
0019 namespace Ekos
0020 {
0021 
0022 PlaceholderPath::PlaceholderPath(const QString &seqFilename):
0023     m_frameTypes(
0024 {
0025     {FRAME_LIGHT, "Light"},
0026     {FRAME_DARK, "Dark"},
0027     {FRAME_BIAS, "Bias"},
0028     {FRAME_FLAT, "Flat"},
0029     {FRAME_NONE, ""},
0030 }),
0031 m_seqFilename(seqFilename)
0032 {
0033 }
0034 
0035 PlaceholderPath::PlaceholderPath():
0036     PlaceholderPath(QString())
0037 {
0038 }
0039 
0040 PlaceholderPath::~PlaceholderPath()
0041 {
0042 }
0043 
0044 QString PlaceholderPath::defaultFormat(bool useFilter, bool useExposure, bool useTimestamp)
0045 {
0046     QString tempFormat = QDir::separator() + "%t" + QDir::separator() + "%T" + QDir::separator();
0047     if (useFilter)
0048         tempFormat.append("%F" + QDir::separator());
0049     tempFormat.append("%t_%T_");
0050     if (useFilter)
0051         tempFormat.append("%F_");
0052     if (useExposure)
0053         tempFormat.append("%e_");
0054     if (useTimestamp)
0055         tempFormat.append("%D");
0056     return tempFormat;
0057 }
0058 
0059 void PlaceholderPath::processJobInfo(SequenceJob *job)
0060 {
0061     QString jobTargetName = job->getCoreProperty(SequenceJob::SJ_TargetName).toString();
0062     auto frameType = getFrameType(job->getFrameType());
0063     auto filterType = job->getCoreProperty(SequenceJob::SJ_Filter).toString();
0064     auto exposure    = job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
0065     const auto isDarkFlat = job->jobType() == SequenceJob::JOBTYPE_DARKFLAT;
0066 
0067     if (isDarkFlat)
0068         frameType = "DarkFlat";
0069 
0070     // Sanitize name
0071     QString tempTargetName = KSUtils::sanitize(jobTargetName);
0072 
0073     // Because scheduler sets the target name in capture module
0074     // it would be the same as the raw prefix
0075     if (tempTargetName.isEmpty() == false && jobTargetName.isEmpty())
0076         jobTargetName = tempTargetName;
0077 
0078     // Make full prefix
0079     QString imagePrefix = jobTargetName;
0080 
0081     if (imagePrefix.isEmpty() == false)
0082         imagePrefix += '_';
0083 
0084     imagePrefix += frameType;
0085 
0086     if (isFilterEnabled(job->getCoreProperty(SequenceJob::SJ_PlaceholderFormat).toString()) && filterType.isEmpty() == false &&
0087             (job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT || job->getFrameType() == FRAME_NONE
0088              || isDarkFlat))
0089     {
0090         imagePrefix += '_';
0091 
0092         imagePrefix += filterType;
0093     }
0094 
0095     // JM 2021.08.21 For flat frames with specific ADU, the exposure duration is only advisory
0096     // and the final exposure time would depend on how many seconds are needed to arrive at the
0097     // target ADU. Therefore we should add duration to the signature.
0098     //if (expEnabled && !(job->getFrameType() == FRAME_FLAT && job->getFlatFieldDuration() == DURATION_ADU))
0099     if (isExpEnabled(job->getCoreProperty(SequenceJob::SJ_PlaceholderFormat).toString()))
0100     {
0101         imagePrefix += '_';
0102 
0103         double fractpart, intpart;
0104         fractpart = std::modf(exposure, &intpart);
0105         if (fractpart == 0)
0106         {
0107             imagePrefix += QString::number(exposure, 'd', 0) + QString("_secs");
0108         }
0109         else if (exposure >= 1e-3)
0110         {
0111             imagePrefix += QString::number(exposure, 'f', 3) + QString("_secs");
0112         }
0113         else
0114         {
0115             imagePrefix += QString::number(exposure, 'f', 6) + QString("_secs");
0116         }
0117     }
0118 
0119     job->setCoreProperty(SequenceJob::SJ_FullPrefix, imagePrefix);
0120 
0121     QString signature = generateSequenceFilename(*job, true, true, 1, ".fits", "", false, true);
0122     job->setCoreProperty(SequenceJob::SJ_Signature, signature);
0123 }
0124 
0125 void PlaceholderPath::addJob(SequenceJob *job, const QString &targetName)
0126 {
0127     auto frameType = job->getFrameType();
0128     auto frameTypeString = getFrameType(job->getFrameType());
0129     QString imagePrefix = KSUtils::sanitize(targetName);
0130 
0131     const auto isDarkFlat = job->jobType() == SequenceJob::JOBTYPE_DARKFLAT;
0132     if (isDarkFlat)
0133         frameTypeString = "DarkFlat";
0134 
0135     QString tempPrefix = constructPrefix(job, imagePrefix);
0136 
0137     job->setCoreProperty(SequenceJob::SJ_FullPrefix, tempPrefix);
0138 
0139     QString directoryPostfix;
0140 
0141     const auto filterName = job->getCoreProperty(SequenceJob::SJ_Filter).toString();
0142 
0143     /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in scheduler.cpp */
0144     if (targetName.isEmpty())
0145         directoryPostfix = QDir::separator() + frameTypeString;
0146     else
0147         directoryPostfix = QDir::separator() + imagePrefix + QDir::separator() + frameTypeString;
0148 
0149 
0150     if ((frameType == FRAME_LIGHT || frameType == FRAME_FLAT || frameType == FRAME_NONE || isDarkFlat)
0151             &&  filterName.isEmpty() == false)
0152         directoryPostfix += QDir::separator() + filterName;
0153 
0154     job->setCoreProperty(SequenceJob::SJ_RemoteFormatDirectory, directoryPostfix);
0155     job->setCoreProperty(SequenceJob::SJ_RemoteFormatFilename, directoryPostfix);
0156 }
0157 
0158 QString PlaceholderPath::constructPrefix(const SequenceJob *job, const QString &imagePrefix)
0159 {
0160     CCDFrameType frameType = job->getFrameType();
0161     auto placeholderFormat = job->getCoreProperty(SequenceJob::SJ_PlaceholderFormat).toString();
0162     auto filter = job->getCoreProperty(SequenceJob::SJ_Filter).toString();
0163 
0164     double exposure = job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
0165 
0166     QString tempImagePrefix = imagePrefix;
0167     if (tempImagePrefix.isEmpty() == false)
0168         tempImagePrefix += '_';
0169 
0170     const auto isDarkFlat = job->jobType() == SequenceJob::JOBTYPE_DARKFLAT;
0171 
0172     tempImagePrefix += isDarkFlat ? "DarkFlat" : CCDFrameTypeNames[frameType];
0173 
0174     if (isFilterEnabled(placeholderFormat) && filter.isEmpty() == false &&
0175             (frameType == FRAME_LIGHT ||
0176              frameType == FRAME_FLAT ||
0177              frameType == FRAME_NONE ||
0178              isDarkFlat))
0179     {
0180         tempImagePrefix += '_';
0181         tempImagePrefix += filter;
0182     }
0183     if (isExpEnabled(placeholderFormat))
0184     {
0185         tempImagePrefix += '_';
0186 
0187         double exposureValue = job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
0188 
0189         // Don't use the locale for exposure value in the capture file name, so that we get a "." as decimal separator
0190         if (exposureValue == static_cast<int>(exposureValue))
0191             // Whole number
0192             tempImagePrefix += QString::number(exposure, 'd', 0) + QString("_secs");
0193         else
0194         {
0195             // Decimal
0196             if (exposure >= 0.001)
0197                 tempImagePrefix += QString::number(exposure, 'f', 3) + QString("_secs");
0198             else
0199                 tempImagePrefix += QString::number(exposure, 'f', 6) + QString("_secs");
0200         }
0201     }
0202     if (isTsEnabled(placeholderFormat))
0203     {
0204         tempImagePrefix += SequenceJob::ISOMarker;
0205     }
0206 
0207     return tempImagePrefix;
0208 }
0209 
0210 QString PlaceholderPath::generateSequenceFilename(const SequenceJob &job,
0211         bool local,
0212         const bool batch_mode,
0213         const int nextSequenceID,
0214         const QString &extension,
0215         const QString &filename,
0216         const bool glob,
0217         const bool gettingSignature)
0218 {
0219     QMap<PathProperty, QVariant> pathPropertyMap;
0220     setGenerateFilenameSettings(job, pathPropertyMap, local, gettingSignature);
0221 
0222     return generateFilenameInternal(pathPropertyMap, local, batch_mode, nextSequenceID, extension, filename, glob,
0223                                     gettingSignature);
0224 }
0225 
0226 QString PlaceholderPath::generateOutputFilename(const bool local, const bool batch_mode, const int nextSequenceID,
0227         const QString &extension,
0228         const QString &filename, const bool glob, const bool gettingSignature) const
0229 {
0230     return generateFilenameInternal(m_PathPropertyMap, local, batch_mode, nextSequenceID, extension, filename, glob,
0231                                     gettingSignature);
0232 }
0233 
0234 QString PlaceholderPath::generateReplacement(const QMap<PathProperty, QVariant> &pathPropertyMap, PathProperty property,
0235         bool usePattern) const
0236 {
0237     if (usePattern)
0238     {
0239         switch (propertyType(property))
0240         {
0241             case PP_TYPE_UINT:
0242             case PP_TYPE_DOUBLE:
0243                 return "-?\\d+";
0244             case PP_TYPE_BOOL:
0245                 return "(true|false)";
0246             case PP_TYPE_POINT:
0247                 return "\\d+x\\d+";
0248             default:
0249                 if (property == PP_PIERSIDE)
0250                     return "(East|West|Unknown)";
0251                 else
0252                     return "\\w+";
0253         }
0254     }
0255     else if (pathPropertyMap[property].isValid())
0256     {
0257         switch (propertyType(property))
0258         {
0259             case PP_TYPE_DOUBLE:
0260                 return QString::number(pathPropertyMap[property].toDouble(), 'd', 0);
0261             case PP_TYPE_UINT:
0262                 return QString::number(pathPropertyMap[property].toUInt());
0263             case PP_TYPE_POINT:
0264                 return QString("%1x%2").arg(pathPropertyMap[PP_BIN].toPoint().x()).arg(pathPropertyMap[PP_BIN].toPoint().y());
0265             case PP_TYPE_STRING:
0266                 if (property == PP_PIERSIDE)
0267                 {
0268                     switch (static_cast<ISD::Mount::PierSide>(pathPropertyMap[property].toInt()))
0269                     {
0270                         case ISD::Mount::PIER_EAST:
0271                             return "East";
0272                         case ISD::Mount::PIER_WEST:
0273                             return "West";
0274                         default:
0275                             return "Unknown";
0276                     }
0277                 }
0278                 else
0279                     return pathPropertyMap[property].toString();
0280             default:
0281                 return pathPropertyMap[property].toString();
0282         }
0283     }
0284     else
0285     {
0286         switch (propertyType(property))
0287         {
0288             case PP_TYPE_DOUBLE:
0289             case PP_TYPE_UINT:
0290                 return "-1";
0291             case PP_TYPE_POINT:
0292                 return "0x0";
0293             case PP_TYPE_BOOL:
0294                 return "false";
0295             default:
0296                 return "Unknown";
0297         }
0298     }
0299 }
0300 
0301 QString PlaceholderPath::generateFilenameInternal(const QMap<PathProperty, QVariant> &pathPropertyMap,
0302         const bool local,
0303         const bool batch_mode,
0304         const int nextSequenceID,
0305         const QString &extension,
0306         const QString &filename,
0307         const bool glob,
0308         const bool gettingSignature) const
0309 {
0310     QString targetNameSanitized = KSUtils::sanitize(pathPropertyMap[PP_TARGETNAME].toString());
0311     int i = 0;
0312 
0313     const QString format = pathPropertyMap[PP_FORMAT].toString();
0314     const bool isDarkFlat = pathPropertyMap[PP_DARKFLAT].isValid() && pathPropertyMap[PP_DARKFLAT].toBool();
0315     const CCDFrameType frameType = static_cast<CCDFrameType>(pathPropertyMap[PP_FRAMETYPE].toUInt());
0316     QString tempFilename = filename;
0317     QString currentDir;
0318     if (batch_mode)
0319         currentDir = pathPropertyMap[PP_DIRECTORY].toString();
0320     else
0321         currentDir = QDir::toNativeSeparators(KSPaths::writableLocation(QStandardPaths::TempLocation) + "/kstars/");
0322 
0323     // ensure, that there is exactly one separator is between non empty directory and format
0324     if(!currentDir.isEmpty() && !format.isEmpty())
0325     {
0326         if(!currentDir.endsWith(QDir::separator()) && !format.startsWith(QDir::separator()))
0327             currentDir.append(QDir::separator());
0328         if(currentDir.endsWith(QDir::separator()) && format.startsWith(QDir::separator()))
0329             currentDir = currentDir.left(currentDir.length() - 1);
0330     }
0331 
0332     QString tempFormat = currentDir + format + "_%s" + QString::number(pathPropertyMap[PP_SUFFIX].toUInt());
0333 
0334 #if defined(Q_OS_WIN)
0335     tempFormat.replace("\\", "/");
0336 #endif
0337     QRegularExpressionMatch match;
0338     QRegularExpression
0339 #if defined(Q_OS_WIN)
0340     re("(?<replace>\\%(?<name>(filename|f|Datetime|D|Type|T|exposure|e|exp|E|Filter|F|target|t|temperature|C|bin|B|gain|G|offset|O|iso|I|pierside|P|sequence|s))(?<level>\\d+)?)(?<sep>[_\\\\])?");
0341 #else
0342     re("(?<replace>\\%(?<name>(filename|f|Datetime|D|Type|T|exposure|e|exp|E|Filter|F|target|t|temperature|C|bin|B|gain|G|offset|O|iso|I|pierside|P|sequence|s))(?<level>\\d+)?)(?<sep>[_/])?");
0343 #endif
0344 
0345     while ((i = tempFormat.indexOf(re, i, &match)) != -1)
0346     {
0347         QString replacement = "";
0348         if ((match.captured("name") == "filename") || (match.captured("name") == "f"))
0349             replacement = m_seqFilename.baseName();
0350         else if ((match.captured("name") == "Datetime") || (match.captured("name") == "D"))
0351         {
0352             if (glob || gettingSignature)
0353             {
0354                 if (local)
0355                     replacement = "\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d-\\d\\d-\\d\\d";
0356                 else
0357                     replacement = "ISO8601";
0358 
0359             }
0360             else
0361                 replacement = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
0362         }
0363         else if ((match.captured("name") == "Type") || (match.captured("name") == "T"))
0364         {
0365             if (isDarkFlat)
0366                 replacement = "DarkFlat";
0367             else
0368                 replacement = getFrameType(frameType);
0369         }
0370         else if ((match.captured("name") == "exposure") || (match.captured("name") == "e") ||
0371                  (match.captured("name") == "exp") || (match.captured("name") == "E"))
0372         {
0373             double fractpart, intpart;
0374             double exposure = pathPropertyMap[PP_EXPOSURE].toDouble();
0375             fractpart = std::modf(exposure, &intpart);
0376             if (fractpart == 0)
0377                 replacement = QString::number(exposure, 'd', 0);
0378             else if (exposure >= 1e-3)
0379                 replacement = QString::number(exposure, 'f', 3);
0380             else
0381                 replacement = QString::number(exposure, 'f', 6);
0382             // append _secs for placeholders "exposure" and "e"
0383             if ((match.captured("name") == "exposure") || (match.captured("name") == "e"))
0384                 replacement += QString("_secs");
0385         }
0386         else if ((match.captured("name") == "Filter") || (match.captured("name") == "F"))
0387         {
0388             QString filter = pathPropertyMap[PP_FILTER].toString();
0389             if (filter.isEmpty() == false
0390                     && (frameType == FRAME_LIGHT
0391                         || frameType == FRAME_FLAT
0392                         || frameType == FRAME_NONE
0393                         || isDarkFlat))
0394             {
0395                 replacement = filter;
0396             }
0397         }
0398         else if ((match.captured("name") == "target") || (match.captured("name") == "t"))
0399         {
0400             replacement = targetNameSanitized;
0401         }
0402         else if (((match.captured("name") == "temperature") || (match.captured("name") == "C")))
0403         {
0404             replacement = generateReplacement(pathPropertyMap, PP_TEMPERATURE,
0405                                               (glob || gettingSignature) && pathPropertyMap[PP_TEMPERATURE].isValid() == false);
0406         }
0407         else if (((match.captured("name") == "bin") || (match.captured("name") == "B")))
0408         {
0409             replacement = generateReplacement(pathPropertyMap, PP_BIN,
0410                                               (glob || gettingSignature) && pathPropertyMap[PP_BIN].isValid() == false);
0411         }
0412         else if (((match.captured("name") == "gain") || (match.captured("name") == "G")))
0413         {
0414             replacement = generateReplacement(pathPropertyMap, PP_GAIN,
0415                                               (glob || gettingSignature) && pathPropertyMap[PP_GAIN].isValid() == false);
0416         }
0417         else if (((match.captured("name") == "offset") || (match.captured("name") == "O")))
0418         {
0419             replacement = generateReplacement(pathPropertyMap, PP_OFFSET,
0420                                               (glob || gettingSignature) && pathPropertyMap[PP_OFFSET].isValid() == false);
0421         }
0422         else if (((match.captured("name") == "iso") || (match.captured("name") == "I"))
0423                  && pathPropertyMap[PP_ISO].isValid())
0424         {
0425             replacement = generateReplacement(pathPropertyMap, PP_ISO,
0426                                               (glob || gettingSignature) && pathPropertyMap[PP_ISO].isValid() == false);
0427         }
0428         else if (((match.captured("name") == "pierside") || (match.captured("name") == "P")))
0429         {
0430             replacement = generateReplacement(pathPropertyMap, PP_PIERSIDE, glob || gettingSignature);
0431         }
0432         // Disable for now %d & %p tags to simplfy
0433         //        else if ((match.captured("name") == "directory") || (match.captured("name") == "d") ||
0434         //                 (match.captured("name") == "path") || (match.captured("name") == "p"))
0435         //        {
0436         //            int level = 0;
0437         //            if (!match.captured("level").isEmpty())
0438         //                level = match.captured("level").toInt() - 1;
0439         //            QFileInfo dir = m_seqFilename;
0440         //            for (int j = 0; j < level; ++j)
0441         //                dir = QFileInfo(dir.dir().path());
0442         //            if (match.captured("name") == "directory" || match.captured("name") == "d")
0443         //                replacement = dir.dir().dirName();
0444         //            else if (match.captured("name") == "path" || match.captured("name") == "p")
0445         //                replacement = dir.path();
0446         //        }
0447         else if ((match.captured("name") == "sequence") || (match.captured("name") == "s"))
0448         {
0449             if (glob)
0450                 replacement = "(?<id>\\d+)";
0451             else
0452             {
0453                 int level = 0;
0454                 if (!match.captured("level").isEmpty())
0455                     level = match.captured("level").toInt();
0456                 replacement = QString("%1").arg(nextSequenceID, level, 10, QChar('0'));
0457             }
0458         }
0459         else
0460             qWarning() << "Unknown replacement string: " << match.captured("replace");
0461 
0462         if (replacement.isEmpty())
0463             tempFormat = tempFormat.replace(match.capturedStart(), match.capturedLength(), replacement);
0464         else
0465             tempFormat = tempFormat.replace(match.capturedStart("replace"), match.capturedLength("replace"), replacement);
0466         i += replacement.length();
0467     }
0468 
0469     if (!gettingSignature)
0470         tempFilename = tempFormat + extension;
0471     else
0472         tempFilename = tempFormat.left(tempFormat.lastIndexOf("_"));
0473 
0474     return tempFilename;
0475 }
0476 
0477 void PlaceholderPath::setGenerateFilenameSettings(const SequenceJob &job, QMap<PathProperty, QVariant> &pathPropertyMap,
0478         const bool local, const bool gettingSignature)
0479 {
0480     setPathProperty(pathPropertyMap, PP_TARGETNAME, job.getCoreProperty(SequenceJob::SJ_TargetName));
0481     setPathProperty(pathPropertyMap, PP_FRAMETYPE, QVariant(job.getFrameType()));
0482     setPathProperty(pathPropertyMap, PP_FILTER, job.getCoreProperty(SequenceJob::SJ_Filter));
0483     setPathProperty(pathPropertyMap, PP_EXPOSURE, job.getCoreProperty(SequenceJob::SJ_Exposure));
0484     setPathProperty(pathPropertyMap, PP_DIRECTORY,
0485                     job.getCoreProperty(local ? SequenceJob::SJ_LocalDirectory : SequenceJob::SJ_RemoteDirectory));
0486     setPathProperty(pathPropertyMap, PP_FORMAT, job.getCoreProperty(SequenceJob::SJ_PlaceholderFormat));
0487     setPathProperty(pathPropertyMap, PP_SUFFIX, job.getCoreProperty(SequenceJob::SJ_PlaceholderSuffix));
0488     setPathProperty(pathPropertyMap, PP_DARKFLAT, job.jobType() == SequenceJob::JOBTYPE_DARKFLAT);
0489     setPathProperty(pathPropertyMap, PP_BIN, job.getCoreProperty(SequenceJob::SJ_Binning));
0490     setPathProperty(pathPropertyMap, PP_PIERSIDE, QVariant(job.getPierSide()));
0491     setPathProperty(pathPropertyMap, PP_ISO, job.getCoreProperty(SequenceJob::SJ_ISO));
0492 
0493     // handle optional parameters
0494     if (job.getCoreProperty(SequenceJob::SJ_EnforceTemperature).toBool())
0495         setPathProperty(pathPropertyMap, PP_TEMPERATURE, QVariant(job.getTargetTemperature()));
0496     else if (job.currentTemperature() != Ekos::INVALID_VALUE && !gettingSignature)
0497         setPathProperty(pathPropertyMap, PP_TEMPERATURE, QVariant(job.currentTemperature()));
0498     else
0499         pathPropertyMap.remove(PP_TEMPERATURE);
0500 
0501     if (job.getCoreProperty(SequenceJob::SequenceJob::SJ_Gain).toInt() >= 0)
0502         setPathProperty(pathPropertyMap, PP_GAIN, job.getCoreProperty(SequenceJob::SJ_Gain));
0503     else if (job.currentGain() >= 0 && !gettingSignature)
0504         setPathProperty(pathPropertyMap, PP_GAIN, job.currentGain());
0505     else
0506         pathPropertyMap.remove(PP_GAIN);
0507 
0508     if (job.getCoreProperty(SequenceJob::SequenceJob::SJ_Offset).toInt() >= 0)
0509         setPathProperty(pathPropertyMap, PP_OFFSET, job.getCoreProperty(SequenceJob::SJ_Offset));
0510     else if (job.currentOffset() >= 0 && !gettingSignature)
0511         setPathProperty(pathPropertyMap, PP_OFFSET, job.currentOffset());
0512     else
0513         pathPropertyMap.remove(PP_OFFSET);
0514 }
0515 
0516 QStringList PlaceholderPath::remainingPlaceholders(const QString &filename)
0517 {
0518     QList<QString> placeholders = {};
0519     QRegularExpressionMatch match;
0520 #if defined(Q_OS_WIN)
0521     QRegularExpression re("(?<replace>\\%(?<name>[a-zA-Z])(?<level>\\d+)?)(?<sep>[_\\\\])+");
0522 #else
0523     QRegularExpression re("(?<replace>%(?<name>[a-zA-Z])(?<level>\\d+)?)(?<sep>[_/])+");
0524 #endif
0525     int i = 0;
0526     while ((i = filename.indexOf(re, i, &match)) != -1)
0527     {
0528         if (match.hasMatch())
0529             placeholders.push_back(match.captured("replace"));
0530         i += match.capturedLength("replace");
0531     }
0532     return placeholders;
0533 }
0534 
0535 QList<int> PlaceholderPath::getCompletedFileIds(const SequenceJob &job)
0536 {
0537     QString path = generateSequenceFilename(job, true, true, 0, ".*", "", true);
0538     auto sanitizedPath = path;
0539 
0540     // This is needed for Windows as the regular expression confuses path search
0541     QString idRE = "(?<id>\\d+).*";
0542     QString datetimeRE = "\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d-\\d\\d-\\d\\d";
0543     sanitizedPath.replace(idRE, "{IDRE}");
0544     sanitizedPath.replace(datetimeRE, "{DATETIMERE}");
0545 
0546     // Now we can get a proper directory
0547     QFileInfo path_info(sanitizedPath);
0548     QDir dir(path_info.dir());
0549 
0550     // e.g. Light_R_(?<id>\\d+).*
0551     auto filename = path_info.fileName();
0552 
0553     // Next replace back the problematic regular expressions
0554     filename.replace("{IDRE}", idRE);
0555     filename.replace("{DATETIMERE}", datetimeRE);
0556 
0557     QStringList matchingFiles = dir.entryList(QDir::Files);
0558     QRegularExpressionMatch match;
0559     QRegularExpression re("^" + filename + "$");
0560     QList<int> ids = {};
0561     for (auto &name : matchingFiles)
0562     {
0563         match = re.match(name);
0564         if (match.hasMatch())
0565             ids << match.captured("id").toInt();
0566     }
0567 
0568     return ids;
0569 }
0570 
0571 int PlaceholderPath::getCompletedFiles(const SequenceJob &job)
0572 {
0573     return getCompletedFileIds(job).length();
0574 }
0575 
0576 int PlaceholderPath::getCompletedFiles(const QString &path)
0577 {
0578     int seqFileCount = 0;
0579 #ifdef Q_OS_WIN
0580     // Splitting directory and baseName in QFileInfo does not distinguish regular expression backslash from directory separator on Windows.
0581     // So do not use QFileInfo for the code that separates directory and basename for Windows.
0582     // Conditions for calling this function:
0583     // - Directory separators must always be "/".
0584     // - Directory separators must not contain backslash.
0585     QString sig_dir;
0586     QString sig_file;
0587     int index = path.lastIndexOf('/');
0588     if (0 <= index)
0589     {
0590         // found '/'. path has both dir and filename
0591         sig_dir = path.left(index);
0592         sig_file = path.mid(index + 1);
0593     } // not found '/'. path has only filename
0594     else
0595     {
0596         sig_file = path;
0597     }
0598     // remove extension
0599     index = sig_file.lastIndexOf('.');
0600     if (0 <= index)
0601     {
0602         // found '.', then remove extension
0603         sig_file = sig_file.left(index);
0604     }
0605     qCDebug(KSTARS_EKOS_CAPTURE) << "Scheduler::PlaceholderPath path:" << path << " sig_dir:" << sig_dir << " sig_file:" <<
0606                                  sig_file;
0607 #else
0608     QFileInfo const path_info(path);
0609     QString const sig_dir(path_info.dir().path());
0610     QString const sig_file(path_info.completeBaseName());
0611 #endif
0612     QRegularExpression re(sig_file);
0613 
0614     QDirIterator it(sig_dir, QDir::Files);
0615 
0616     /* FIXME: this counts all files with prefix in the storage location, not just captures. DSS analysis files are counted in, for instance. */
0617     while (it.hasNext())
0618     {
0619         QString const fileName = QFileInfo(it.next()).completeBaseName();
0620 
0621         QRegularExpressionMatch match = re.match(fileName);
0622         if (match.hasMatch())
0623             seqFileCount++;
0624     }
0625 
0626     return seqFileCount;
0627 }
0628 
0629 int PlaceholderPath::checkSeqBoundary(const SequenceJob &job)
0630 {
0631     auto ids = getCompletedFileIds(job);
0632     if (ids.length() > 0)
0633         return *std::max_element(ids.begin(), ids.end()) + 1;
0634     else
0635         return 1;
0636 }
0637 
0638 PlaceholderPath::PathPropertyType PlaceholderPath::propertyType(PathProperty property)
0639 {
0640     switch (property)
0641     {
0642         case PP_FORMAT:
0643         case PP_DIRECTORY:
0644         case PP_TARGETNAME:
0645         case PP_FILTER:
0646         case PP_PIERSIDE:
0647             return PP_TYPE_STRING;
0648 
0649         case PP_DARKFLAT:
0650             return PP_TYPE_BOOL;
0651 
0652         case PP_SUFFIX:
0653         case PP_FRAMETYPE:
0654         case PP_ISO:
0655             return PP_TYPE_UINT;
0656 
0657         case PP_EXPOSURE:
0658         case PP_GAIN:
0659         case PP_OFFSET:
0660         case PP_TEMPERATURE:
0661             return PP_TYPE_DOUBLE;
0662 
0663         case PP_BIN:
0664             return PP_TYPE_POINT;
0665 
0666         default:
0667             return PP_TYPE_NONE;
0668     }
0669 }
0670 
0671 // An "emergency" method--the code should not be overwriting files,
0672 // however, if we've detected an overwrite, we generate a new filename
0673 // by looking for numbers at its end (before its extension) and incrementing
0674 // that number, checking to make sure the new filename with the incremented number doesn't exist.
0675 QString PlaceholderPath::repairFilename(const QString &filename)
0676 {
0677     QRegularExpression re("^(.*[^\\d])(\\d+)\\.(\\w+)$");
0678 
0679     auto match = re.match(filename);
0680     if (match.hasMatch())
0681     {
0682         QString prefix = match.captured(1);
0683         int number = match.captured(2).toInt();
0684         int numberLength = match.captured(2).size();
0685         QString extension = match.captured(3);
0686         QString candidate = QString("%1%2.%3").arg(prefix).arg(number + 1, numberLength, 10, QLatin1Char('0')).arg(extension);
0687         int maxIterations = 2000;
0688         while (QFile::exists(candidate))
0689         {
0690             number = number + 1;
0691             candidate = QString("%1%2.%3").arg(prefix).arg(number, numberLength, 10, QLatin1Char('0')).arg(extension);
0692             if (--maxIterations <= 0)
0693                 return filename;
0694         }
0695         return candidate;
0696     }
0697     return filename;;
0698 }
0699 
0700 }
0701