v2.0.0
Loading...
Searching...
No Matches
fiff_annotations.cpp
Go to the documentation of this file.
1//=============================================================================================================
34
35//=============================================================================================================
36// INCLUDES
37//=============================================================================================================
38
39#include "fiff_annotations.h"
40
41//=============================================================================================================
42// QT INCLUDES
43//=============================================================================================================
44
45#include <QFile>
46#include <QJsonDocument>
47#include <QJsonObject>
48#include <QJsonArray>
49#include <QTextStream>
50#include <QDebug>
51#include <QFileInfo>
52
53//=============================================================================================================
54// USED NAMESPACES
55//=============================================================================================================
56
57using namespace FIFFLIB;
58
59//=============================================================================================================
60// DEFINE MEMBER METHODS
61//=============================================================================================================
62
66
67//=============================================================================================================
68
70{
71 return m_annotations.size();
72}
73
74//=============================================================================================================
75
77{
78 return m_annotations.isEmpty();
79}
80
81//=============================================================================================================
82
84{
85 return m_annotations[index];
86}
87
88//=============================================================================================================
89
91{
92 return m_annotations[index];
93}
94
95//=============================================================================================================
96
98{
99 m_annotations.append(annotation);
100}
101
102//=============================================================================================================
103
104void FiffAnnotations::append(double onset, double duration, const QString& description,
105 const QStringList& channelNames, const QString& comment)
106{
108 a.onset = onset;
109 a.duration = duration;
110 a.description = description;
111 a.channelNames = channelNames;
112 a.comment = comment;
113 m_annotations.append(a);
114}
115
116//=============================================================================================================
117
119{
120 m_annotations.remove(index);
121}
122
123//=============================================================================================================
124
126{
127 m_annotations.clear();
128}
129
130//=============================================================================================================
131
132const QVector<FiffAnnotation>& FiffAnnotations::toVector() const
133{
134 return m_annotations;
135}
136
137//=============================================================================================================
138
139int FiffAnnotations::onsetToSample(int index, double sfreq, int firstSample) const
140{
141 return static_cast<int>(m_annotations[index].onset * sfreq) + firstSample;
142}
143
144//=============================================================================================================
145
146int FiffAnnotations::endToSample(int index, double sfreq, int firstSample) const
147{
148 const FiffAnnotation& a = m_annotations[index];
149 return static_cast<int>((a.onset + a.duration) * sfreq) + firstSample;
150}
151
152//=============================================================================================================
153
154FiffAnnotations FiffAnnotations::select(const QString& descriptionFilter) const
155{
156 FiffAnnotations result;
157 for (const FiffAnnotation& a : m_annotations) {
158 if (a.description.startsWith(descriptionFilter)) {
159 result.append(a);
160 }
161 }
162 return result;
163}
164
165//=============================================================================================================
166
167FiffAnnotations FiffAnnotations::selectByChannel(const QString& channelName) const
168{
169 FiffAnnotations result;
170 for (const FiffAnnotation& a : m_annotations) {
171 if (a.channelNames.isEmpty() || a.channelNames.contains(channelName)) {
172 result.append(a);
173 }
174 }
175 return result;
176}
177
178//=============================================================================================================
179
180FiffAnnotations FiffAnnotations::crop(double tmin, double tmax) const
181{
182 FiffAnnotations result;
183 for (const FiffAnnotation& a : m_annotations) {
184 double aEnd = a.onset + a.duration;
185
186 // Check overlap: annotation interval [onset, aEnd] vs window [tmin, tmax]
187 if (a.onset > tmax || aEnd < tmin) {
188 continue;
189 }
190
191 FiffAnnotation clipped = a;
192 if (clipped.onset < tmin) {
193 double shift = tmin - clipped.onset;
194 clipped.onset = tmin;
195 clipped.duration -= shift;
196 }
197 double clippedEnd = clipped.onset + clipped.duration;
198 if (clippedEnd > tmax) {
199 clipped.duration = tmax - clipped.onset;
200 }
201 result.append(clipped);
202 }
203 return result;
204}
205
206//=============================================================================================================
207
209{
210 FiffAnnotations annot;
211
212 QFile file(path);
213 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
214 qWarning() << "[FiffAnnotations::readJson] Cannot open file:" << path;
215 return annot;
216 }
217
218 QByteArray data = file.readAll();
219 file.close();
220
221 QJsonParseError parseError;
222 QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
223 if (parseError.error != QJsonParseError::NoError) {
224 qWarning() << "[FiffAnnotations::readJson] JSON parse error:" << parseError.errorString();
225 return annot;
226 }
227
228 QJsonObject root = doc.object();
229 QJsonArray arr = root.value("annotations").toArray();
230
231 for (int i = 0; i < arr.size(); ++i) {
232 QJsonObject obj = arr[i].toObject();
234 a.onset = obj.value("onset").toDouble(0.0);
235 a.duration = obj.value("duration").toDouble(0.0);
236 a.description = obj.value("description").toString();
237
238 QJsonArray chArr = obj.value("channel_names").toArray();
239 for (int j = 0; j < chArr.size(); ++j) {
240 a.channelNames.append(chArr[j].toString());
241 }
242
243 a.comment = obj.value("comment").toString();
244 a.extras = obj.value("extras").toObject().toVariantMap();
245
246 annot.append(a);
247 }
248
249 return annot;
250}
251
252//=============================================================================================================
253
254bool FiffAnnotations::writeJson(const QString& path, const FiffAnnotations& annot)
255{
256 QJsonArray arr;
257 for (const FiffAnnotation& a : annot.toVector()) {
258 QJsonObject obj;
259 obj["onset"] = a.onset;
260 obj["duration"] = a.duration;
261 obj["description"] = a.description;
262
263 QJsonArray chArr;
264 for (const QString& ch : a.channelNames) {
265 chArr.append(ch);
266 }
267 obj["channel_names"] = chArr;
268 obj["comment"] = a.comment;
269 obj["extras"] = QJsonObject::fromVariantMap(a.extras);
270 arr.append(obj);
271 }
272
273 QJsonObject root;
274 root["annotations"] = arr;
275 QJsonDocument doc(root);
276
277 QFile file(path);
278 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
279 qWarning() << "[FiffAnnotations::writeJson] Cannot open file for writing:" << path;
280 return false;
281 }
282 file.write(doc.toJson(QJsonDocument::Indented));
283 file.close();
284 return true;
285}
286
287//=============================================================================================================
288
290{
291 FiffAnnotations annot;
292
293 QFile file(path);
294 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
295 qWarning() << "[FiffAnnotations::readCsv] Cannot open file:" << path;
296 return annot;
297 }
298
299 QTextStream in(&file);
300 // Skip header line
301 if (!in.atEnd()) {
302 in.readLine();
303 }
304
305 while (!in.atEnd()) {
306 QString line = in.readLine().trimmed();
307 if (line.isEmpty()) {
308 continue;
309 }
310
311 // Parse: onset,duration,description
312 // Description may contain commas, so split only the first two commas
313 int firstComma = line.indexOf(',');
314 if (firstComma < 0) continue;
315 int secondComma = line.indexOf(',', firstComma + 1);
316 if (secondComma < 0) continue;
317
318 bool onsetOk = false, durationOk = false;
319 double onset = line.left(firstComma).toDouble(&onsetOk);
320 double duration = line.mid(firstComma + 1, secondComma - firstComma - 1).toDouble(&durationOk);
321 QString description = line.mid(secondComma + 1);
322
323 if (!onsetOk || !durationOk) {
324 continue;
325 }
326
327 annot.append(onset, duration, description);
328 }
329
330 file.close();
331 return annot;
332}
333
334//=============================================================================================================
335
336bool FiffAnnotations::writeCsv(const QString& path, const FiffAnnotations& annot)
337{
338 QFile file(path);
339 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
340 qWarning() << "[FiffAnnotations::writeCsv] Cannot open file for writing:" << path;
341 return false;
342 }
343
344 QTextStream out(&file);
345 out << "onset,duration,description\n";
346
347 for (const FiffAnnotation& a : annot.toVector()) {
348 out << a.onset << "," << a.duration << "," << a.description << "\n";
349 }
350
351 file.close();
352 return true;
353}
354
355//=============================================================================================================
356
358{
359 QString ext = QFileInfo(path).suffix().toLower();
360
361 if (ext == "json") {
362 return readJson(path);
363 } else if (ext == "csv") {
364 return readCsv(path);
365 }
366
367 // Unknown extension: try JSON first, then CSV
368 FiffAnnotations annot = readJson(path);
369 if (!annot.isEmpty()) {
370 return annot;
371 }
372 return readCsv(path);
373}
374
375//=============================================================================================================
376
377bool FiffAnnotations::write(const QString& path, const FiffAnnotations& annot)
378{
379 QString ext = QFileInfo(path).suffix().toLower();
380
381 if (ext == "csv") {
382 return writeCsv(path, annot);
383 }
384 // Default to JSON
385 return writeJson(path, annot);
386}
FiffAnnotations class declaration.
FIFF file I/O and data structures (raw, epochs, evoked, covariance, forward).
Single annotation entry.
int onsetToSample(int index, double sfreq, int firstSample=0) const
static FiffAnnotations readCsv(const QString &path)
const QVector< FiffAnnotation > & toVector() const
FiffAnnotations selectByChannel(const QString &channelName) const
FiffAnnotations select(const QString &descriptionFilter) const
static FiffAnnotations read(const QString &path)
static bool write(const QString &path, const FiffAnnotations &annot)
static bool writeJson(const QString &path, const FiffAnnotations &annot)
int endToSample(int index, double sfreq, int firstSample=0) const
static bool writeCsv(const QString &path, const FiffAnnotations &annot)
static FiffAnnotations readJson(const QString &path)
void append(const FiffAnnotation &annotation)
FiffAnnotations crop(double tmin, double tmax) const
const FiffAnnotation & operator[](int index) const