1 /*
2     SPDX-FileCopyrightText: 2021 Kwon-Young Choi <kwon-young.choi@hotmail.fr>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "placeholderpath.h"
8 
9 #include "ekos/scheduler/schedulerjob.h"
10 #include "sequencejob.h"
11 #include "Options.h"
12 #include "kspaths.h"
13 
14 #include <QString>
15 #include <QStringList>
16 
17 #include <cmath>
18 
19 namespace Ekos
20 {
21 
PlaceholderPath(QString seqFilename)22 PlaceholderPath::PlaceholderPath(QString seqFilename):
23     m_frameTypes(
24 {
25     {FRAME_LIGHT, "Light"},
26     {FRAME_DARK, "Dark"},
27     {FRAME_BIAS, "Bias"},
28     {FRAME_FLAT, "Flat"},
29     {FRAME_NONE, ""},
30 }),
31 m_seqFilename(seqFilename)
32 {
33 }
34 
PlaceholderPath()35 PlaceholderPath::PlaceholderPath():
36     PlaceholderPath("")
37 {
38 }
39 
~PlaceholderPath()40 PlaceholderPath::~PlaceholderPath()
41 {
42 }
43 
processJobInfo(SequenceJob * job,QString targetName)44 void PlaceholderPath::processJobInfo(SequenceJob *job, QString targetName)
45 {
46     job->setTargetName(targetName);
47 
48     QString frameType = getFrameType(job->getFrameType());
49     QString rawPrefix, filterType = job->getFilterName();
50     double exposure    = job->getExposure();
51     bool filterEnabled = false, expEnabled = false, tsEnabled = false;
52     job->getPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled);
53 
54     // Sanitize name
55     //QString targetName = schedJob->getName();
56     targetName = targetName.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" )
57                  // Remove any two or more __
58                  .replace( QRegularExpression("_{2,}"), "_")
59                  // Remove any _ at the end
60                  .replace( QRegularExpression("_$"), "");
61 
62     // Because scheduler sets the target name in capture module
63     // it would be the same as the raw prefix
64     if (targetName.isEmpty() == false && rawPrefix.isEmpty())
65         rawPrefix = targetName;
66 
67     // Make full prefix
68     QString imagePrefix = rawPrefix;
69 
70     if (imagePrefix.isEmpty() == false)
71         imagePrefix += '_';
72 
73     imagePrefix += frameType;
74 
75     if (filterEnabled && filterType.isEmpty() == false &&
76             (job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT || job->getFrameType() == FRAME_NONE))
77     {
78         imagePrefix += '_';
79 
80         imagePrefix += filterType;
81     }
82 
83     // JM 2021.08.21 For flat frames with specific ADU, the exposure duration is only advisory
84     // and the final exposure time would depend on how many seconds are needed to arrive at the
85     // target ADU. Therefore we should add duration to the signature.
86     if (expEnabled && !(job->getFrameType() == FRAME_FLAT && job->getFlatFieldDuration() == DURATION_ADU))
87     {
88         imagePrefix += '_';
89 
90         double fractpart, intpart;
91         fractpart = std::modf(exposure, &intpart);
92         if (fractpart == 0)
93         {
94             imagePrefix += QString::number(exposure, 'd', 0) + QString("_secs");
95         }
96         else if (exposure >= 1e-3)
97         {
98             imagePrefix += QString::number(exposure, 'f', 3) + QString("_secs");
99         }
100         else
101         {
102             imagePrefix += QString::number(exposure, 'f', 6) + QString("_secs");
103         }
104     }
105 
106     job->setFullPrefix(imagePrefix);
107 
108     // Directory postfix
109     QString directoryPostfix;
110 
111     /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in capture.cpp */
112     if (targetName.isEmpty())
113         directoryPostfix = QLatin1String("/") + frameType;
114     else
115         directoryPostfix = QLatin1String("/") + targetName + QLatin1String("/") + frameType;
116     if ((job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT || job->getFrameType() == FRAME_NONE)
117             && filterType.isEmpty() == false)
118         directoryPostfix += QLatin1String("/") + filterType;
119 
120     job->setDirectoryPostfix(directoryPostfix);
121 }
122 
addJob(SequenceJob * job,QString targetName)123 void PlaceholderPath::addJob(SequenceJob *job, QString targetName)
124 {
125     job->setTargetName(targetName);
126 
127     CCDFrameType frameType = job->getFrameType();
128     QString frameTypeStr = CCDFrameTypeNames[frameType];
129     QString imagePrefix;
130     QString rawFilePrefix;
131     bool filterEnabled, exposureEnabled, tsEnabled;
132     job->getPrefixSettings(rawFilePrefix, filterEnabled, exposureEnabled, tsEnabled);
133 
134     imagePrefix = rawFilePrefix;
135 
136     // JM 2019-11-26: In case there is no raw prefix set
137     // BUT target name is set, we update the prefix to include
138     // the target name, which is usually set by the scheduler.
139     if (imagePrefix.isEmpty() && !targetName.isEmpty())
140     {
141         imagePrefix = targetName;
142     }
143 
144     constructPrefix(job, imagePrefix);
145 
146     job->setFullPrefix(imagePrefix);
147 
148     QString directoryPostfix;
149 
150     /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in scheduler.cpp */
151     if (targetName.isEmpty())
152         directoryPostfix = QLatin1String("/") + frameTypeStr;
153     else
154         directoryPostfix = QLatin1String("/") + targetName + QLatin1String("/") + frameTypeStr;
155     if ((frameType == FRAME_LIGHT || frameType == FRAME_FLAT || frameType == FRAME_NONE)
156             &&  job->getFilterName().isEmpty() == false)
157         directoryPostfix += QLatin1String("/") + job->getFilterName();
158 
159     job->setDirectoryPostfix(directoryPostfix);
160 }
161 
constructPrefix(SequenceJob * job,QString & imagePrefix)162 void PlaceholderPath::constructPrefix(SequenceJob *job, QString &imagePrefix)
163 {
164     CCDFrameType frameType = job->getFrameType();
165     QString filter = job->getFilterName();
166     QString rawFilePrefix;
167     bool filterEnabled, exposureEnabled, tsEnabled;
168     job->getPrefixSettings(rawFilePrefix, filterEnabled, exposureEnabled, tsEnabled);
169     double exposure = job->getExposure();
170 
171     if (imagePrefix.isEmpty() == false)
172         imagePrefix += '_';
173 
174     imagePrefix += CCDFrameTypeNames[frameType];
175 
176     /*if (fileFilterS->isChecked() && captureFilterS->currentText().isEmpty() == false &&
177             captureTypeS->currentText().compare("Bias", Qt::CaseInsensitive) &&
178             captureTypeS->currentText().compare("Dark", Qt::CaseInsensitive))*/
179     if (filterEnabled && filter.isEmpty() == false &&
180             (frameType == FRAME_LIGHT || frameType == FRAME_FLAT || frameType == FRAME_NONE))
181     {
182         imagePrefix += '_';
183         imagePrefix += filter;
184     }
185     if (exposureEnabled)
186     {
187         //if (imagePrefix.isEmpty() == false || frameTypeCheck->isChecked())
188         imagePrefix += '_';
189 
190         double exposureValue = job->getExposure();
191 
192         // Don't use the locale for exposure value in the capture file name, so that we get a "." as decimal separator
193         if (exposureValue == static_cast<int>(exposureValue))
194             // Whole number
195             imagePrefix += QString::number(exposure, 'd', 0) + QString("_secs");
196         else
197         {
198             // Decimal
199             if (exposure >= 0.001)
200                 imagePrefix += QString::number(exposure, 'f', 3) + QString("_secs");
201             else
202                 imagePrefix += QString::number(exposure, 'f', 6) + QString("_secs");
203         }
204     }
205     if (tsEnabled)
206     {
207         imagePrefix += SequenceJob::ISOMarker;
208     }
209 }
210 
generateFilenameOld(const QString & format,bool batch_mode,QString * filename,QString fitsDir,QString seqPrefix,int nextSequenceID)211 void PlaceholderPath::generateFilenameOld(
212     const QString &format, bool batch_mode, QString *filename,
213     QString fitsDir, QString seqPrefix, int nextSequenceID
214 )
215 {
216     QString currentDir;
217     if (batch_mode)
218         currentDir = fitsDir.isEmpty() ? Options::fitsDir() : fitsDir;
219     else
220         currentDir = KSPaths::writableLocation(QStandardPaths::TempLocation) + "/kstars";
221 
222     /*
223     if (QDir(currentDir).exists() == false)
224         QDir().mkpath(currentDir);
225     */
226 
227     if (currentDir.endsWith('/') == false)
228         currentDir.append('/');
229 
230     // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
231     // The timestamp is no longer ISO8601 but it should solve interoperality issues
232     // between different OS hosts
233     QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
234 
235     if (seqPrefix.contains("_ISO8601"))
236     {
237         QString finalPrefix = seqPrefix;
238         finalPrefix.replace("ISO8601", ts);
239         *filename = currentDir + finalPrefix +
240                     QString("_%1%2").arg(QString().asprintf("%03d", nextSequenceID), format);
241     }
242     else
243         *filename = currentDir + seqPrefix + (seqPrefix.isEmpty() ? "" : "_") +
244                     QString("%1%2").arg(QString().asprintf("%03d", nextSequenceID), format);
245 }
246 
generateFilename(QString format,SequenceJob & job,QString targetName,bool batch_mode,int nextSequenceID,const QString & extension,QString * filename) const247 void PlaceholderPath::generateFilename(
248     QString format, SequenceJob &job, QString targetName, bool batch_mode, int nextSequenceID, const QString &extension,
249     QString *filename) const
250 {
251     QString rawFilePrefix;
252     bool filterEnabled, exposureEnabled, tsEnabled;
253     job.getPrefixSettings(rawFilePrefix, filterEnabled, exposureEnabled, tsEnabled);
254 
255     generateFilename(format, rawFilePrefix, filterEnabled, exposureEnabled,
256                      tsEnabled, job.getFilterName(), job.getFrameType(), job.getExposure(),
257                      targetName, batch_mode, nextSequenceID, extension, filename);
258 }
259 
generateFilename(QString format,bool tsEnabled,bool batch_mode,int nextSequenceID,const QString & extension,QString * filename) const260 void PlaceholderPath::generateFilename(QString format, bool tsEnabled, bool batch_mode,
261                                        int nextSequenceID, const QString &extension, QString *filename) const
262 {
263     generateFilename(format, m_RawPrefix, m_filterPrefixEnabled, m_expPrefixEnabled,
264                      tsEnabled, m_filter, m_frameType, m_exposure, m_targetName, batch_mode,
265                      nextSequenceID, extension, filename);
266 }
267 
generateFilename(QString format,QString rawFilePrefix,bool filterEnabled,bool exposureEnabled,bool tsEnabled,QString filter,CCDFrameType frameType,double exposure,QString targetName,bool batch_mode,int nextSequenceID,const QString & extension,QString * filename) const268 void PlaceholderPath::generateFilename(
269     QString format, QString rawFilePrefix, bool filterEnabled, bool exposureEnabled,
270     bool tsEnabled, QString filter, CCDFrameType frameType, double exposure, QString targetName,
271     bool batch_mode, int nextSequenceID,  const QString &extension, QString *filename) const
272 {
273     targetName = targetName.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" )
274                  // Remove any two or more __
275                  .replace( QRegularExpression("_{2,}"), "_")
276                  // Remove any _ at the end
277                  .replace( QRegularExpression("_$"), "");
278     int i = 0;
279 
280     QString currentDir;
281     if (batch_mode)
282     {
283         currentDir = m_seqFilename.path().isEmpty() ? Options::fitsDir() : currentDir;
284     }
285     else
286     {
287         currentDir = KSPaths::writableLocation(QStandardPaths::TempLocation) + "/kstars";
288     }
289 
290     if (currentDir.endsWith('/') == true)
291         currentDir.chop(1);
292 
293     if (!currentDir.isEmpty())
294         format = currentDir + "/" + format.section("/", -1);
295 
296     QRegularExpressionMatch match;
297     QRegularExpression re("(?<replace>\\%(?<name>[f,D,T,e,F,t,d,p,s])(?<level>\\d+)?)(?<sep>[_/])?");
298     while ((i = format.indexOf(re, i, &match)) != -1)
299     {
300         QString replacement = "";
301         if (match.captured("name") == "f")
302         {
303             replacement = m_seqFilename.baseName();
304         }
305         else if (match.captured("name") == "D")
306         {
307             if (tsEnabled)
308             {
309                 replacement = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
310             }
311         }
312         else if (match.captured("name") == "T")
313         {
314             replacement = getFrameType(frameType);
315         }
316         else if (match.captured("name") == "e")
317         {
318             if (exposureEnabled)
319             {
320                 double fractpart, intpart;
321                 fractpart = std::modf(exposure, &intpart);
322                 if (fractpart == 0)
323                 {
324                     replacement = QString::number(exposure, 'd', 0) + QString("_secs");
325                 }
326                 else if (exposure >= 1e-3)
327                 {
328                     replacement = QString::number(exposure, 'f', 3) + QString("_secs");
329                 }
330                 else
331                 {
332                     replacement = QString::number(exposure, 'f', 6) + QString("_secs");
333                 }
334             }
335         }
336         else if (match.captured("name") == "F")
337         {
338             if (format.indexOf("/", match.capturedStart()) == -1)
339             {
340                 // in the basename part of the path
341                 if (filterEnabled && filter.isEmpty() == false
342                         && (frameType == FRAME_LIGHT
343                             || frameType == FRAME_FLAT
344                             || frameType == FRAME_NONE))
345                 {
346                     replacement = filter;
347                 }
348             }
349             else
350             {
351                 // in the directory part of the path
352                 if (filter.isEmpty() == false
353                         && (frameType == FRAME_LIGHT
354                             || frameType == FRAME_FLAT
355                             || frameType == FRAME_NONE))
356                 {
357                     replacement = filter;
358                 }
359             }
360         }
361         else if (match.captured("name") == "t")
362         {
363             if (format.indexOf("/", match.capturedStart()) != -1)
364             {
365                 // in the directory part of the path
366                 replacement = targetName;
367             }
368             else
369             {
370                 // in the basename part of the path
371                 replacement = rawFilePrefix;
372                 if (replacement.isEmpty() && !targetName.isEmpty())
373                 {
374                     replacement = targetName;
375                 }
376             }
377         }
378         else if (match.captured("name") == "d" || match.captured("name") == "p")
379         {
380             int level = 0;
381             if (!match.captured("level").isEmpty())
382             {
383                 level = match.captured("level").toInt() - 1;
384             }
385             QFileInfo dir = m_seqFilename;
386             for (int j = 0; j < level; ++j)
387             {
388                 dir = QFileInfo(dir.dir().path());
389             }
390             if (match.captured("name") == "d")
391             {
392                 replacement = dir.dir().dirName();
393             }
394             else if (match.captured("name") == "p")
395             {
396                 replacement = dir.path();
397             }
398         }
399         else if (match.captured("name") == "s")
400         {
401             int level = 1;
402             if (!match.captured("level").isEmpty())
403             {
404                 level = match.captured("level").toInt();
405             }
406             replacement = QString("%1").arg(nextSequenceID, level, 10, QChar('0'));
407         }
408         else
409         {
410             qWarning() << "Unknown replacement string: " << match.captured("replace");
411         }
412         if (replacement.isEmpty())
413         {
414             format = format.replace(match.capturedStart(), match.capturedLength(), replacement);
415         }
416         else
417         {
418             format = format.replace(match.capturedStart("replace"), match.capturedLength("replace"), replacement);
419         }
420         i += replacement.length();
421     }
422     *filename = format + extension;
423 }
424 
setGenerateFilenameSettings(const SequenceJob & job)425 void PlaceholderPath::setGenerateFilenameSettings(const SequenceJob &job)
426 {
427     m_RawPrefix           = job.property("rawPrefix").toString();
428     m_filterPrefixEnabled = job.isFilterPrefixEnabled();
429     m_expPrefixEnabled    = job.isExposurePrefixEnabled();
430     m_filter              = job.getFilterName();
431     m_frameType           = job.getFrameType();
432     m_exposure            = job.getExposure();
433     m_targetName          = job.getTargetName();
434 }
435 
remainingPlaceholders(QString filename)436 QStringList PlaceholderPath::remainingPlaceholders(QString filename)
437 {
438     QList<QString> placeholders = {};
439     QRegularExpressionMatch match;
440     QRegularExpression re("(?<replace>\\%(?<name>[a-z])(?<level>\\d+)?)(?<sep>[_/])?");
441     int i = 0;
442     while ((i = filename.indexOf(re, i, &match)) != -1)
443     {
444         if (match.hasMatch())
445         {
446             placeholders.push_back(match.captured("replace"));
447         }
448         i += match.capturedLength("replace");
449     }
450     return placeholders;
451 }
452 
453 }
454 
455