KWeatherCore

capparser.cpp
1/*
2 * SPDX-FileCopyrightText: 2021 Anjani Kumar <anjanik012@gmail.com>
3 * SPDX-FileCopyrightText: 2021 Han Young <hanyoung@protonmail.com>
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7#include "capparser.h"
8#include "capalertinfo.h"
9#include "capalertmessage.h"
10#include "caparea.h"
11#include "capnamedvalue.h"
12#include "capreference.h"
13
14#include <KLocalizedString>
15
16#include <QDateTime>
17#include <QDebug>
18#include <QStringTokenizer>
19
20#include <optional>
21
22using namespace Qt::Literals;
23
24namespace KWeatherCore
25{
26
27template<typename T>
28struct MapEntry {
29 const char *name;
30 T value;
31};
32
33template<typename QStringT, typename EnumT, std::size_t N>
34static std::optional<EnumT> stringToValue(const QStringT &s, const MapEntry<EnumT> (&map)[N])
35{
36 const auto it = std::lower_bound(std::begin(map), std::end(map), s, [](auto lhs, auto rhs) {
37 return QLatin1String(lhs.name) < rhs;
38 });
39 if (it != std::end(map) && QLatin1String((*it).name) == s) {
40 return (*it).value;
41 }
42 return {};
43}
44
45// ### important: keep all the following tables sorted by name!
46static constexpr const MapEntry<CAPAlertInfo::Category> category_map[] = {
47 {"CBRNE", CAPAlertInfo::Category::CBRNE},
48 {"Env", CAPAlertInfo::Category::Environmental},
49 {"Fire", CAPAlertInfo::Category::Fire},
50 {"Geo", CAPAlertInfo::Category::Geophysical},
51 {"Health", CAPAlertInfo::Category::Health},
52 {"Infra", CAPAlertInfo::Category::Infrastructure},
53 {"Met", CAPAlertInfo::Category::Meteorological},
54 {"Other", CAPAlertInfo::Category::Other},
55 {"Rescue", CAPAlertInfo::Category::Rescue},
56 {"Safety", CAPAlertInfo::Category::Safety},
57 {"Security", CAPAlertInfo::Category::Security},
58 {"Transport", CAPAlertInfo::Category::Transport},
59};
60
61enum class Tags { ALERT, IDENTIFIER, SENDER, SENT_TIME, STATUS, MSG_TYPE, SCOPE, NOTE, INFO, REFERENCES };
62
63static constexpr const MapEntry<Tags> tag_map[] = {
64 {"alert", Tags::ALERT},
65 {"identifier", Tags::IDENTIFIER},
66 {"info", Tags::INFO},
67 {"msgType", Tags::MSG_TYPE},
68 {"note", Tags::NOTE},
69 {"references", Tags::REFERENCES},
70 {"scope", Tags::SCOPE},
71 {"sender", Tags::SENDER},
72 {"sent", Tags::SENT_TIME},
73 {"status", Tags::STATUS},
74};
75
76enum class InfoTags {
77 HEADLINE,
78 DESCRIPTION,
79 EVENT,
80 EVENTCODE,
81 EFFECTIVE_TIME,
82 ONSET_TIME,
83 EXPIRE_TIME,
84 CATEGORY,
85 INSTRUCTION,
86 URGENCY,
87 SEVERITY,
88 CERTAINITY,
89 PARAMETER,
90 AREA,
91 SENDERNAME,
92 LANGUAGE,
93 RESPONSETYPE,
94 CONTACT,
95 WEB,
96};
97
98static constexpr const MapEntry<InfoTags> info_tag_map[] = {
99 {"area", InfoTags::AREA},
100 {"category", InfoTags::CATEGORY},
101 {"certainty", InfoTags::CERTAINITY},
102 {"contact", InfoTags::CONTACT},
103 {"description", InfoTags::DESCRIPTION},
104 {"effective", InfoTags::EFFECTIVE_TIME},
105 {"event", InfoTags::EVENT},
106 {"eventCode", InfoTags::EVENTCODE},
107 {"expires", InfoTags::EXPIRE_TIME},
108 {"headline", InfoTags::HEADLINE},
109 {"instruction", InfoTags::INSTRUCTION},
110 {"language", InfoTags::LANGUAGE},
111 {"onset", InfoTags::ONSET_TIME},
112 {"parameter", InfoTags::PARAMETER},
113 {"responseType", InfoTags::RESPONSETYPE},
114 {"senderName", InfoTags::SENDERNAME},
115 {"severity", InfoTags::SEVERITY},
116 {"urgency", InfoTags::URGENCY},
117 {"web", InfoTags::WEB},
118};
119
120static constexpr const MapEntry<CAPAlertMessage::Status> status_map[] = {
124 {"System", CAPAlertMessage::Status::System},
126};
127
128static constexpr const MapEntry<CAPAlertMessage::MessageType> msgtype_map[] = {
134};
135
136static constexpr const MapEntry<CAPAlertMessage::Scope> scope_map[] = {
140};
141
142static constexpr const MapEntry<CAPAlertInfo::ResponseType> response_type_map[] = {
143 {"AllClear", CAPAlertInfo::ResponseType::AllClear},
144 {"Assess", CAPAlertInfo::ResponseType::Assess},
145 {"Avoid", CAPAlertInfo::ResponseType::Avoid},
146 {"Evacuate", CAPAlertInfo::ResponseType::Evacuate},
147 {"Execute", CAPAlertInfo::ResponseType::Execute},
148 {"Monitor", CAPAlertInfo::ResponseType::Monitor},
149 {"None", CAPAlertInfo::ResponseType::None},
150 {"Prepare", CAPAlertInfo::ResponseType::Prepare},
151 {"Shelter", CAPAlertInfo::ResponseType::Shelter},
152};
153
154static constexpr const MapEntry<CAPAlertInfo::Urgency> urgency_map[] = {
155 {"Expected", CAPAlertInfo::Urgency::Expected},
156 {"Future", CAPAlertInfo::Urgency::Future},
157 {"Immediate", CAPAlertInfo::Urgency::Immediate},
158 {"Past", CAPAlertInfo::Urgency::Past},
159 {"Unknown", CAPAlertInfo::Urgency::UnknownUrgency},
160};
161
162static constexpr const MapEntry<CAPAlertInfo::Severity> severity_map[] = {
163 {"Extreme", CAPAlertInfo::Severity::Extreme},
164 {"Minor", CAPAlertInfo::Severity::Minor},
165 {"Moderate", CAPAlertInfo::Severity::Moderate},
166 {"Severe", CAPAlertInfo::Severity::Severe},
167 {"Unknown", CAPAlertInfo::Severity::UnknownSeverity},
168};
169
170static constexpr const MapEntry<CAPAlertInfo::Certainty> certainty_map[] = {
171 {"Likely", CAPAlertInfo::Certainty::Likely},
172 {"Observed", CAPAlertInfo::Certainty::Observed},
173 {"Possible", CAPAlertInfo::Certainty::Possible},
174 {"Unknown", CAPAlertInfo::Certainty::UnknownCertainty},
175 {"Unlikely", CAPAlertInfo::Certainty::Unlikely},
176};
177
178[[nodiscard]] static CAPPolygon stringToPolygon(QStringView str)
179{
180 CAPPolygon res;
181
182 for (auto coordinate : QStringTokenizer(str, ' '_L1, Qt::SkipEmptyParts)) {
183 const auto idx = coordinate.indexOf(','_L1);
184 if (idx < 0) {
185 continue;
186 }
187 bool latOk = false, lonOk = false;
188 res.push_back({coordinate.left(idx).toFloat(&latOk), coordinate.mid(idx + 1).toFloat(&lonOk)});
189 if (!latOk || !lonOk) {
190 res.pop_back();
191 }
192 }
193 return res;
194}
195
196CAPParser::CAPParser(const QByteArray &data)
197 : m_xml(data)
198{
199 bool flag = false;
200 if (!data.isEmpty()) {
201 while (m_xml.readNextStartElement()) {
202 if (m_xml.name() == QStringLiteral("alert")) {
203 flag = true;
204 break;
205 }
206 }
207 if (!flag) {
208 qWarning() << "Not a CAP XML";
209 }
210 }
211}
212
213CAPAlertMessage CAPParser::parse()
214{
215 CAPAlertMessage entry;
216 while (m_xml.readNextStartElement()) {
217 const auto tag = stringToValue(m_xml.name(), tag_map);
218 if (!tag) {
219 m_xml.skipCurrentElement();
220 continue;
221 }
222 switch (*tag) {
223 case Tags::IDENTIFIER:
224 entry.setIdentifier(m_xml.readElementText());
225 break;
226 case Tags::SENDER:
227 entry.setSender(m_xml.readElementText());
228 break;
229 case Tags::SENT_TIME:
230 entry.setSentTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
231 break;
232 case Tags::STATUS: {
233 const auto elementText = m_xml.readElementText();
234 const auto status = stringToValue(elementText, status_map);
235 if (status) {
236 entry.setStatus(*status);
237 } else {
238 qWarning() << "Unknown status field" << elementText;
239 }
240 break;
241 }
242 case Tags::MSG_TYPE: {
243 const auto elementText = m_xml.readElementText();
244 const auto msgType = stringToValue(elementText, msgtype_map);
245 if (msgType) {
246 entry.setMessageType(*msgType);
247 } else {
248 qWarning() << "Unknown msgType field" << elementText;
249 }
250 break;
251 }
252 case Tags::SCOPE: {
253 const auto elementText = m_xml.readElementText();
254 const auto scope = stringToValue(elementText, scope_map);
255 if (scope) {
256 entry.setScope(*scope);
257 } else {
258 qWarning() << "Unknown scope field" << elementText;
259 }
260 break;
261 }
262 case Tags::NOTE:
263 entry.setNote(m_xml.readElementText());
264 break;
265 case Tags::INFO: {
266 auto info = parseInfo();
267 entry.addInfo(std::move(info));
268 break;
269 }
270 case Tags::REFERENCES:
271 entry.setReferences(parseReferences(m_xml.readElementText()));
272 break;
273 default:
274 m_xml.skipCurrentElement();
275 }
276 }
277 return entry;
278}
279
280CAPAlertInfo CAPParser::parseInfo()
281{
282 CAPAlertInfo info;
283
284 if (m_xml.name() == QLatin1String("info")) {
285 while (!m_xml.atEnd() && !(m_xml.isEndElement() && m_xml.name() == QLatin1String("info"))) {
286 m_xml.readNext();
287 if (!m_xml.isStartElement()) {
288 continue;
289 }
290 const auto tag = stringToValue(m_xml.name(), info_tag_map);
291 if (tag) {
292 switch (*tag) {
293 case InfoTags::CATEGORY: {
294 const auto s = m_xml.readElementText();
295 const auto category = stringToValue(s, category_map);
296 if (category) {
297 info.addCategory(*category);
298 }
299 break;
300 }
301 case InfoTags::EVENT:
302 info.setEvent(m_xml.readElementText());
303 break;
304 case InfoTags::URGENCY: {
305 const auto s = m_xml.readElementText();
306 if (const auto urgency = stringToValue(s, urgency_map); urgency) {
307 info.setUrgency(*urgency);
308 } else {
309 qWarning() << "Unknown urgency type:" << s;
310 }
311 break;
312 }
313 case InfoTags::SEVERITY: {
314 const auto s = m_xml.readElementText();
315 if (const auto severity = stringToValue(s, severity_map); severity) {
316 info.setSeverity(*severity);
317 } else {
318 qWarning() << "Unknown severity type:" << s;
319 }
320 break;
321 }
322 case InfoTags::CERTAINITY: {
323 const auto s = m_xml.readElementText();
324 if (const auto certainty = stringToValue(s, certainty_map); certainty) {
325 info.setCertainty(*certainty);
326 } else {
327 qWarning() << "Unknown certainty type:" << s;
328 }
329 break;
330 }
331 case InfoTags::EFFECTIVE_TIME:
332 info.setEffectiveTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
333 break;
334 case InfoTags::ONSET_TIME:
335 info.setOnsetTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
336 break;
337 case InfoTags::EXPIRE_TIME:
338 info.setExpireTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
339 break;
340 case InfoTags::HEADLINE:
341 info.setHeadline(m_xml.readElementText());
342 break;
343 case InfoTags::DESCRIPTION:
344 info.setDescription(m_xml.readElementText());
345 break;
346 case InfoTags::INSTRUCTION:
347 info.setInstruction(m_xml.readElementText());
348 break;
349 case InfoTags::PARAMETER: {
350 info.addParameter(parseNamedValue());
351 break;
352 }
353 case InfoTags::AREA: {
354 info.addArea(parseArea());
355 break;
356 }
357 case InfoTags::SENDERNAME: {
358 info.setSender(m_xml.readElementText());
359 break;
360 }
361 case InfoTags::LANGUAGE:
362 info.setLanguage(m_xml.readElementText());
363 break;
364 case InfoTags::RESPONSETYPE: {
365 const auto elementText = m_xml.readElementText();
366 if (const auto respType = stringToValue(elementText, response_type_map)) {
367 info.addResponseType(*respType);
368 } else {
369 qWarning() << "Unknown respone type value" << elementText;
370 }
371 break;
372 }
373 case InfoTags::CONTACT:
374 info.setContact(m_xml.readElementText());
375 break;
376 case InfoTags::WEB:
377 info.setWeb(m_xml.readElementText());
378 break;
379 case InfoTags::EVENTCODE:
380 info.addEventCode(parseNamedValue());
381 break;
382 }
383 } else {
384 if (m_xml.isStartElement()) {
385 qWarning() << "unknown element: " << m_xml.name();
386 }
387 }
388 }
389 }
390 return info;
391}
392
393CAPArea CAPParser::parseArea()
394{
395 CAPArea area;
396 while (!(m_xml.isEndElement() && m_xml.name() == QLatin1String("area"))) {
397 m_xml.readNext();
398 if (m_xml.name() == QLatin1String("areaDesc") && !m_xml.isEndElement()) {
399 area.setDescription(m_xml.readElementText());
400 } else if (m_xml.name() == QLatin1String("geocode") && !m_xml.isEndElement()) {
401 area.addGeoCode(parseNamedValue());
402 } else if (m_xml.name() == QLatin1String("polygon") && !m_xml.isEndElement()) {
403 area.addPolygon(stringToPolygon(m_xml.readElementText()));
404 } else if (m_xml.name() == QLatin1String("circle") && !m_xml.isEndElement()) {
405 const auto t = m_xml.readElementText();
406 const auto commaIdx = t.indexOf(QLatin1Char(','));
407 const auto spaceIdx = t.indexOf(QLatin1Char(' '));
408 if (commaIdx > 0 && spaceIdx > commaIdx && commaIdx < t.size()) {
409 CAPCircle circle;
410 circle.latitude = QStringView(t).left(commaIdx).toFloat();
411 circle.longitude = QStringView(t).mid(commaIdx + 1, spaceIdx - commaIdx - 1).toFloat();
412 circle.radius = QStringView(t).mid(spaceIdx).toFloat();
413 area.addCircle(std::move(circle));
414 }
415 } else if (m_xml.name() == QLatin1String("altitude") && !m_xml.isEndElement()) {
416 area.setAltitude(m_xml.readElementText().toFloat());
417 } else if (m_xml.name() == QLatin1String("ceiling") && !m_xml.isEndElement()) {
418 area.setCeiling(m_xml.readElementText().toFloat());
419 } else if (m_xml.isStartElement()) {
420 qDebug() << "unknown area element:" << m_xml.name();
421 }
422 }
423 return area;
424}
425
426CAPNamedValue CAPParser::parseNamedValue()
427{
428 CAPNamedValue value;
429 const auto elementName = m_xml.name().toString();
430 while (!m_xml.isEndElement() || m_xml.name() != elementName) {
431 m_xml.readNext();
432 if (m_xml.isStartElement() && m_xml.name() == QLatin1String("valueName")) {
433 value.name = m_xml.readElementText();
434 } else if (m_xml.isStartElement() && m_xml.name() == QLatin1String("value")) {
435 value.value = m_xml.readElementText();
436 } else if (m_xml.isStartElement()) {
437 qDebug() << "unknown named value element:" << m_xml.name();
438 }
439 }
440 return value;
441}
442
443std::vector<CAPReference> CAPParser::parseReferences(const QString &refsString)
444{
445 std::vector<CAPReference> refs;
446 // TODO for Qt 6: use QStringTokenizer
447 const auto refsSplit = refsString.split(QLatin1Char(' '), Qt::SkipEmptyParts);
448 refs.reserve(refsSplit.size());
449 for (const auto &refString : refsSplit) {
450 const auto refSplit = refString.split(QLatin1Char(','));
451 if (refSplit.size() != 3) {
452 qDebug() << "failed to parse CAP reference:" << refString;
453 continue;
454 }
455 refs.emplace_back(refSplit.at(0), refSplit.at(1), QDateTime::fromString(refSplit.at(2), Qt::ISODate));
456 }
457
458 return refs;
459}
460}
@ Test
Technical testing only, all recipients disregard.
@ Actual
Actionable by all targeted recipients.
@ Exercise
Actionable only by designated exercise participants.
@ Draft
A preliminary template or draft, not actionable in its current form.
@ Public
For general dissemination to unrestricted audiences.
@ Private
For dissemination only to specified addresses.
@ Restricted
For dissemination only to users with a known operational requirement.
@ Update
Updates and supercedes the earlier message(s) identified in references()
@ Error
Indicates rejection of the message(s) identified in references()
@ Alert
Initial information requiring attention by targeted recipients.
@ Acknowledge
Acknowledges receipt and acceptance of the message(s) identified in references()
@ Cancel
Cancels the earlier message(s) identified in references()
Q_SCRIPTABLE CaptureState status()
Category category(StandardShortcut id)
bool isEmpty() const const
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
void reserve(qsizetype size)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QStringView left(qsizetype length) const const
QStringView mid(qsizetype start, qsizetype length) const const
float toFloat(bool *ok) const const
SkipEmptyParts
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 31 2025 12:11:37 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.