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};
160
161static constexpr const MapEntry<CAPAlertInfo::Severity> severity_map[] = {
162 {"Extreme", CAPAlertInfo::Severity::Extreme},
163 {"Minor", CAPAlertInfo::Severity::Minor},
164 {"Moderate", CAPAlertInfo::Severity::Moderate},
165 {"Severe", CAPAlertInfo::Severity::Severe},
166};
167
168static constexpr const MapEntry<CAPAlertInfo::Certainty> certainty_map[] = {
169 {"Likely", CAPAlertInfo::Certainty::Likely},
170 {"Observed", CAPAlertInfo::Certainty::Observed},
171 {"Possible", CAPAlertInfo::Certainty::Possible},
172 {"Unlikely", CAPAlertInfo::Certainty::Unlikely},
173};
174
175[[nodiscard]] static CAPPolygon stringToPolygon(QStringView str)
176{
177 CAPPolygon res;
178
179 for (auto coordinate : QStringTokenizer(str, ' '_L1, Qt::SkipEmptyParts)) {
180 const auto idx = coordinate.indexOf(','_L1);
181 if (idx < 0) {
182 continue;
183 }
184 bool latOk = false, lonOk = false;
185 res.push_back({coordinate.left(idx).toFloat(&latOk), coordinate.mid(idx + 1).toFloat(&lonOk)});
186 if (!latOk || !lonOk) {
187 res.pop_back();
188 }
189 }
190 return res;
191}
192
193CAPParser::CAPParser(const QByteArray &data)
194 : m_xml(data)
195{
196 bool flag = false;
197 if (!data.isEmpty()) {
198 while (m_xml.readNextStartElement()) {
199 if (m_xml.name() == QStringLiteral("alert")) {
200 flag = true;
201 break;
202 }
203 }
204 if (!flag) {
205 qWarning() << "Not a CAP XML";
206 }
207 }
208}
209
210CAPAlertMessage CAPParser::parse()
211{
212 CAPAlertMessage entry;
213 while (m_xml.readNextStartElement()) {
214 const auto tag = stringToValue(m_xml.name(), tag_map);
215 if (!tag) {
216 m_xml.skipCurrentElement();
217 continue;
218 }
219 switch (*tag) {
220 case Tags::IDENTIFIER:
221 entry.setIdentifier(m_xml.readElementText());
222 break;
223 case Tags::SENDER:
224 entry.setSender(m_xml.readElementText());
225 break;
226 case Tags::SENT_TIME:
227 entry.setSentTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
228 break;
229 case Tags::STATUS: {
230 const auto elementText = m_xml.readElementText();
231 const auto status = stringToValue(elementText, status_map);
232 if (status) {
233 entry.setStatus(*status);
234 } else {
235 qWarning() << "Unknown status field" << elementText;
236 }
237 break;
238 }
239 case Tags::MSG_TYPE: {
240 const auto elementText = m_xml.readElementText();
241 const auto msgType = stringToValue(elementText, msgtype_map);
242 if (msgType) {
243 entry.setMessageType(*msgType);
244 } else {
245 qWarning() << "Unknown msgType field" << elementText;
246 }
247 break;
248 }
249 case Tags::SCOPE: {
250 const auto elementText = m_xml.readElementText();
251 const auto scope = stringToValue(elementText, scope_map);
252 if (scope) {
253 entry.setScope(*scope);
254 } else {
255 qWarning() << "Unknown scope field" << elementText;
256 }
257 break;
258 }
259 case Tags::NOTE:
260 entry.setNote(m_xml.readElementText());
261 break;
262 case Tags::INFO: {
263 auto info = parseInfo();
264 entry.addInfo(std::move(info));
265 break;
266 }
267 case Tags::REFERENCES:
268 entry.setReferences(parseReferences(m_xml.readElementText()));
269 break;
270 default:
271 m_xml.skipCurrentElement();
272 }
273 }
274 return entry;
275}
276
277CAPAlertInfo CAPParser::parseInfo()
278{
279 CAPAlertInfo info;
280
281 if (m_xml.name() == QLatin1String("info")) {
282 while (!m_xml.atEnd() && !(m_xml.isEndElement() && m_xml.name() == QLatin1String("info"))) {
283 m_xml.readNext();
284 if (!m_xml.isStartElement()) {
285 continue;
286 }
287 const auto tag = stringToValue(m_xml.name(), info_tag_map);
288 if (tag) {
289 switch (*tag) {
290 case InfoTags::CATEGORY: {
291 const auto s = m_xml.readElementText();
292 const auto category = stringToValue(s, category_map);
293 if (category) {
294 info.addCategory(*category);
295 }
296 break;
297 }
298 case InfoTags::EVENT:
299 info.setEvent(m_xml.readElementText());
300 break;
301 case InfoTags::URGENCY: {
302 const auto s = m_xml.readElementText();
303 if (const auto urgency = stringToValue(s, urgency_map); urgency) {
304 info.setUrgency(*urgency);
305 } else {
306 qWarning() << "Unknown urgency type:" << s;
307 }
308 break;
309 }
310 case InfoTags::SEVERITY: {
311 const auto s = m_xml.readElementText();
312 if (const auto severity = stringToValue(s, severity_map); severity) {
313 info.setSeverity(*severity);
314 } else {
315 qWarning() << "Unknown severity type:" << s;
316 }
317 break;
318 }
319 case InfoTags::CERTAINITY: {
320 const auto s = m_xml.readElementText();
321 if (const auto certainty = stringToValue(s, certainty_map); certainty) {
322 info.setCertainty(*certainty);
323 } else {
324 qWarning() << "Unknown certainty type:" << s;
325 }
326 break;
327 }
328 case InfoTags::EFFECTIVE_TIME:
329 info.setEffectiveTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
330 break;
331 case InfoTags::ONSET_TIME:
332 info.setOnsetTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
333 break;
334 case InfoTags::EXPIRE_TIME:
335 info.setExpireTime(QDateTime::fromString(m_xml.readElementText(), Qt::ISODate));
336 break;
337 case InfoTags::HEADLINE:
338 info.setHeadline(m_xml.readElementText());
339 break;
340 case InfoTags::DESCRIPTION:
341 info.setDescription(m_xml.readElementText());
342 break;
343 case InfoTags::INSTRUCTION:
344 info.setInstruction(m_xml.readElementText());
345 break;
346 case InfoTags::PARAMETER: {
347 info.addParameter(parseNamedValue());
348 break;
349 }
350 case InfoTags::AREA: {
351 info.addArea(parseArea());
352 break;
353 }
354 case InfoTags::SENDERNAME: {
355 info.setSender(m_xml.readElementText());
356 break;
357 }
358 case InfoTags::LANGUAGE:
359 info.setLanguage(m_xml.readElementText());
360 break;
361 case InfoTags::RESPONSETYPE: {
362 const auto elementText = m_xml.readElementText();
363 if (const auto respType = stringToValue(elementText, response_type_map)) {
364 info.addResponseType(*respType);
365 } else {
366 qWarning() << "Unknown respone type value" << elementText;
367 }
368 break;
369 }
370 case InfoTags::CONTACT:
371 info.setContact(m_xml.readElementText());
372 break;
373 case InfoTags::WEB:
374 info.setWeb(m_xml.readElementText());
375 break;
376 case InfoTags::EVENTCODE:
377 info.addEventCode(parseNamedValue());
378 break;
379 }
380 } else {
381 if (m_xml.isStartElement()) {
382 qWarning() << "unknown element: " << m_xml.name();
383 }
384 }
385 }
386 }
387 return info;
388}
389
390CAPArea CAPParser::parseArea()
391{
392 CAPArea area;
393 while (!(m_xml.isEndElement() && m_xml.name() == QLatin1String("area"))) {
394 m_xml.readNext();
395 if (m_xml.name() == QLatin1String("areaDesc") && !m_xml.isEndElement()) {
396 area.setDescription(m_xml.readElementText());
397 } else if (m_xml.name() == QLatin1String("geocode") && !m_xml.isEndElement()) {
398 area.addGeoCode(parseNamedValue());
399 } else if (m_xml.name() == QLatin1String("polygon") && !m_xml.isEndElement()) {
400 area.addPolygon(stringToPolygon(m_xml.readElementText()));
401 } else if (m_xml.name() == QLatin1String("circle") && !m_xml.isEndElement()) {
402 const auto t = m_xml.readElementText();
403 const auto commaIdx = t.indexOf(QLatin1Char(','));
404 const auto spaceIdx = t.indexOf(QLatin1Char(' '));
405 if (commaIdx > 0 && spaceIdx > commaIdx && commaIdx < t.size()) {
406 CAPCircle circle;
407 circle.latitude = QStringView(t).left(commaIdx).toFloat();
408 circle.longitude = QStringView(t).mid(commaIdx + 1, spaceIdx - commaIdx - 1).toFloat();
409 circle.radius = QStringView(t).mid(spaceIdx).toFloat();
410 area.addCircle(std::move(circle));
411 }
412 } else if (m_xml.name() == QLatin1String("altitude") && !m_xml.isEndElement()) {
413 area.setAltitude(m_xml.readElementText().toFloat());
414 } else if (m_xml.name() == QLatin1String("ceiling") && !m_xml.isEndElement()) {
415 area.setCeiling(m_xml.readElementText().toFloat());
416 } else if (m_xml.isStartElement()) {
417 qDebug() << "unknown area element:" << m_xml.name();
418 }
419 }
420 return area;
421}
422
423CAPNamedValue CAPParser::parseNamedValue()
424{
425 CAPNamedValue value;
426 const auto elementName = m_xml.name().toString();
427 while (!m_xml.isEndElement() || m_xml.name() != elementName) {
428 m_xml.readNext();
429 if (m_xml.isStartElement() && m_xml.name() == QLatin1String("valueName")) {
430 value.name = m_xml.readElementText();
431 } else if (m_xml.isStartElement() && m_xml.name() == QLatin1String("value")) {
432 value.value = m_xml.readElementText();
433 } else if (m_xml.isStartElement()) {
434 qDebug() << "unknown named value element:" << m_xml.name();
435 }
436 }
437 return value;
438}
439
440std::vector<CAPReference> CAPParser::parseReferences(const QString &refsString)
441{
442 std::vector<CAPReference> refs;
443 // TODO for Qt 6: use QStringTokenizer
444 const auto refsSplit = refsString.split(QLatin1Char(' '), Qt::SkipEmptyParts);
445 refs.reserve(refsSplit.size());
446 for (const auto &refString : refsSplit) {
447 const auto refSplit = refString.split(QLatin1Char(','));
448 if (refSplit.size() != 3) {
449 qDebug() << "failed to parse CAP reference:" << refString;
450 continue;
451 }
452 refs.emplace_back(refSplit.at(0), refSplit.at(1), QDateTime::fromString(refSplit.at(2), Qt::ISODate));
453 }
454
455 return refs;
456}
457}
@ 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-2024 The KDE developers.
Generated on Fri Dec 27 2024 11:55:53 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.