From f2ebd720040d3ed8cc60540f845c77df75f007c1 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Wed, 13 Jan 2021 17:15:32 +0000 Subject: [PATCH] ADS-B demodulator updates. Use message pipes for target. Send aircraft to Map feature. All selection of flight paths for all aircraft, or just the selected aircraft. Do not display demod stats by default. --- plugins/channelrx/demodadsb/adsbdemod.cpp | 26 ++ plugins/channelrx/demodadsb/adsbdemod.h | 7 +- plugins/channelrx/demodadsb/adsbdemodgui.cpp | 250 +++++++++++------- plugins/channelrx/demodadsb/adsbdemodgui.h | 24 +- plugins/channelrx/demodadsb/adsbdemodgui.ui | 32 ++- .../channelrx/demodadsb/adsbdemodsettings.cpp | 5 +- .../channelrx/demodadsb/adsbdemodsettings.h | 1 + plugins/channelrx/demodadsb/icons.qrc | 1 + .../demodadsb/icons/allflightpaths.png | Bin 0 -> 2025 bytes 9 files changed, 235 insertions(+), 111 deletions(-) create mode 100644 plugins/channelrx/demodadsb/icons/allflightpaths.png diff --git a/plugins/channelrx/demodadsb/adsbdemod.cpp b/plugins/channelrx/demodadsb/adsbdemod.cpp index 3cd87e795..41a1a6cb7 100644 --- a/plugins/channelrx/demodadsb/adsbdemod.cpp +++ b/plugins/channelrx/demodadsb/adsbdemod.cpp @@ -34,12 +34,14 @@ #include "SWGADSBDemodSettings.h" #include "SWGChannelReport.h" #include "SWGADSBDemodReport.h" +#include "SWGTargetAzimuthElevation.h" #include "dsp/dspengine.h" #include "dsp/dspcommands.h" #include "dsp/devicesamplemimo.h" #include "device/deviceapi.h" #include "util/db.h" +#include "maincore.h" #include "adsbdemod.h" #include "adsbdemodworker.h" @@ -471,3 +473,27 @@ void ADSBDemod::networkManagerFinished(QNetworkReply *reply) reply->deleteLater(); } + +void ADSBDemod::setTarget(const QString& name, float targetAzimuth, float targetElevation) +{ + m_targetAzimuth = targetAzimuth; + m_targetElevation = targetElevation; + m_targetAzElValid = true; + + // Send to Rotator Controllers + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(this, "target"); + if (mapMessageQueues) + { + QList::iterator it = mapMessageQueues->begin(); + + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = new SWGSDRangel::SWGTargetAzimuthElevation(); + swgTarget->setName(new QString(name)); + swgTarget->setAzimuth(targetAzimuth); + swgTarget->setElevation(targetElevation); + (*it)->push(MainCore::MsgTargetAzimuthElevation::create(this, swgTarget)); + } + } +} diff --git a/plugins/channelrx/demodadsb/adsbdemod.h b/plugins/channelrx/demodadsb/adsbdemod.h index 326831f62..104259541 100644 --- a/plugins/channelrx/demodadsb/adsbdemod.h +++ b/plugins/channelrx/demodadsb/adsbdemod.h @@ -119,12 +119,7 @@ public: m_basebandSink->setMessageQueueToGUI(queue); } - void setTarget(float targetAzimuth, float targetElevation) - { - m_targetAzimuth = targetAzimuth; - m_targetElevation = targetElevation; - m_targetAzElValid = true; - } + void setTarget(const QString& name, float targetAzimuth, float targetElevation); void clearTarget() { m_targetAzElValid = false; } uint32_t getNumberOfDeviceStreams() const; diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index 8571c9f84..abd6963c2 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -29,6 +29,8 @@ #include #include +#include "SWGMapItem.h" + #include "ui_adsbdemodgui.h" #include "channel/channelwebapiutils.h" #include "plugin/pluginapi.h" @@ -148,6 +150,104 @@ static Real modulus(double x, double y) return x - y * std::floor(x/y); } +QString Aircraft::getImage() +{ + if (m_emitterCategory.length() > 0) + { + if (!m_emitterCategory.compare("Heavy")) + return QString("aircraft_4engine.png"); + else if (!m_emitterCategory.compare("Large")) + return QString("aircraft_2engine.png"); + else if (!m_emitterCategory.compare("Small")) + return QString("aircraft_2enginesmall.png"); + else if (!m_emitterCategory.compare("Rotorcraft")) + return QString("aircraft_helicopter.png"); + else if (!m_emitterCategory.compare("High performance")) + return QString("aircraft_fighter.png"); + else if (!m_emitterCategory.compare("Light") + || !m_emitterCategory.compare("Ultralight") + || !m_emitterCategory.compare("Glider/sailplane")) + return QString("aircraft_light.png"); + else if (!m_emitterCategory.compare("Space vehicle")) + return QString("aircraft_space.png"); + else if (!m_emitterCategory.compare("UAV")) + return QString("aircraft_drone.png"); + else if (!m_emitterCategory.compare("Emergency vehicle") + || !m_emitterCategory.compare("Service vehicle")) + return QString("truck.png"); + else + return QString("aircraft_2engine.png"); + } + else + return QString("aircraft_2engine.png"); +} + +QString Aircraft::getText(bool all) +{ + QStringList list; + if (m_flight.length() > 0) + { + list.append(QString("Flight: %1").arg(m_flight)); + } + else + { + list.append(QString("ICAO: %1").arg(m_icao, 1, 16)); + } + if (m_showAll || m_isHighlighted || all) + { + if (m_aircraftInfo != nullptr) + { + if (m_aircraftInfo->m_model.size() > 0) + { + list.append(QString("Aircraft: %1").arg(m_aircraftInfo->m_model)); + } + } + if (m_altitudeValid) + { + if (m_gui->useSIUints()) + list.append(QString("Altitude: %1 (m)").arg(Units::feetToIntegerMetres(m_altitude))); + else + list.append(QString("Altitude: %1 (ft)").arg(m_altitude)); + } + if (m_speedValid) + { + if (m_gui->useSIUints()) + list.append(QString("%1: %2 (kph)").arg(m_speedTypeNames[m_speedType]).arg(Units::knotsToIntegerKPH(m_speed))); + else + list.append(QString("%1: %2 (kn)").arg(m_speedTypeNames[m_speedType]).arg(m_speed)); + } + if (m_verticalRateValid) + { + QString desc; + Real rate; + QString units; + + if (m_gui->useSIUints()) + { + rate = Units::feetPerMinToIntegerMetresPerSecond(m_verticalRate); + units = QString("m/s"); + } + else + { + rate = m_verticalRate; + units = QString("ft/min"); + } + if (m_verticalRate == 0) + desc = "Level flight"; + else if (rate > 0) + desc = QString("Climbing: %1 (%2)").arg(rate).arg(units); + else + desc = QString("Descending: %1 (%2)").arg(rate).arg(units); + list.append(QString(desc)); + } + if ((m_status.length() > 0) && m_status.compare("No emergency")) + { + list.append(m_status); + } + } + return list.join("\n"); +} + QVariant AircraftModel::data(const QModelIndex &index, int role) const { int row = index.row(); @@ -170,101 +270,12 @@ QVariant AircraftModel::data(const QModelIndex &index, int role) const else if (role == AircraftModel::adsbDataRole) { // Create the text to go in the bubble next to the aircraft - QStringList list; - if (m_aircrafts[row]->m_flight.length() > 0) - { - list.append(QString("Flight: %1").arg(m_aircrafts[row]->m_flight)); - } - else - { - list.append(QString("ICAO: %1").arg(m_aircrafts[row]->m_icao, 1, 16)); - } - if (m_aircrafts[row]->m_showAll || m_aircrafts[row]->m_isHighlighted) - { - if (m_aircrafts[row]->m_aircraftInfo != nullptr) - { - if (m_aircrafts[row]->m_aircraftInfo->m_model.size() > 0) - { - list.append(QString("Aircraft: %1").arg(m_aircrafts[row]->m_aircraftInfo->m_model)); - } - } - if (m_aircrafts[row]->m_altitudeValid) - { - if (m_aircrafts[row]->m_gui->useSIUints()) - list.append(QString("Altitude: %1 (m)").arg(Units::feetToIntegerMetres(m_aircrafts[row]->m_altitude))); - else - list.append(QString("Altitude: %1 (ft)").arg(m_aircrafts[row]->m_altitude)); - } - if (m_aircrafts[row]->m_speedValid) - { - if (m_aircrafts[row]->m_gui->useSIUints()) - list.append(QString("%1: %2 (kph)").arg(m_aircrafts[row]->m_speedTypeNames[m_aircrafts[row]->m_speedType]).arg(Units::knotsToIntegerKPH(m_aircrafts[row]->m_speed))); - else - list.append(QString("%1: %2 (kn)").arg(m_aircrafts[row]->m_speedTypeNames[m_aircrafts[row]->m_speedType]).arg(m_aircrafts[row]->m_speed)); - } - if (m_aircrafts[row]->m_verticalRateValid) - { - QString desc; - Real rate; - QString units; - - if (m_aircrafts[row]->m_gui->useSIUints()) - { - rate = Units::feetPerMinToIntegerMetresPerSecond(m_aircrafts[row]->m_verticalRate); - units = QString("m/s"); - } - else - { - rate = m_aircrafts[row]->m_verticalRate; - units = QString("ft/min"); - } - if (m_aircrafts[row]->m_verticalRate == 0) - desc = "Level flight"; - else if (rate > 0) - desc = QString("Climbing: %1 (%2)").arg(rate).arg(units); - else - desc = QString("Descending: %1 (%2)").arg(rate).arg(units); - list.append(QString(desc)); - } - if ((m_aircrafts[row]->m_status.length() > 0) && m_aircrafts[row]->m_status.compare("No emergency")) - { - list.append(m_aircrafts[row]->m_status); - } - } - QString data = list.join("\n"); - return QVariant::fromValue(data); + return QVariant::fromValue(m_aircrafts[row]->getText()); } else if (role == AircraftModel::aircraftImageRole) { // Select an image to use for the aircraft - if (m_aircrafts[row]->m_emitterCategory.length() > 0) - { - if (!m_aircrafts[row]->m_emitterCategory.compare("Heavy")) - return QVariant::fromValue(QString("aircraft_4engine.png")); - else if (!m_aircrafts[row]->m_emitterCategory.compare("Large")) - return QVariant::fromValue(QString("aircraft_2engine.png")); - else if (!m_aircrafts[row]->m_emitterCategory.compare("Small")) - return QVariant::fromValue(QString("aircraft_2enginesmall.png")); - else if (!m_aircrafts[row]->m_emitterCategory.compare("Rotorcraft")) - return QVariant::fromValue(QString("aircraft_helicopter.png")); - else if (!m_aircrafts[row]->m_emitterCategory.compare("High performance")) - return QVariant::fromValue(QString("aircraft_fighter.png")); - else if (!m_aircrafts[row]->m_emitterCategory.compare("Light") - || !m_aircrafts[row]->m_emitterCategory.compare("Ultralight") - || !m_aircrafts[row]->m_emitterCategory.compare("Glider/sailplane")) - return QVariant::fromValue(QString("aircraft_light.png")); - else if (!m_aircrafts[row]->m_emitterCategory.compare("Space vehicle")) - return QVariant::fromValue(QString("aircraft_space.png")); - else if (!m_aircrafts[row]->m_emitterCategory.compare("UAV")) - return QVariant::fromValue(QString("aircraft_drone.png")); - else if (!m_aircrafts[row]->m_emitterCategory.compare("Emergency vehicle") - || !m_aircrafts[row]->m_emitterCategory.compare("Service vehicle")) - return QVariant::fromValue(QString("truck.png")); - else - return QVariant::fromValue(QString("aircraft_2engine.png")); - } - else - return QVariant::fromValue(QString("aircraft_2engine.png")); + return QVariant::fromValue(m_aircrafts[row]->getImage()); } else if (role == AircraftModel::bubbleColourRole) { @@ -280,7 +291,7 @@ QVariant AircraftModel::data(const QModelIndex &index, int role) const } else if (role == AircraftModel::aircraftPathRole) { - if (m_flightPaths) + if ((m_flightPaths && m_aircrafts[row]->m_isHighlighted) || m_allFlightPaths) return m_aircrafts[row]->m_coordinates; else return QVariantList(); @@ -407,7 +418,7 @@ bool AirportModel::setData(const QModelIndex &index, const QVariant& value, int else if (idx == m_airports[row]->m_frequencies.size()) { // Set airport as target - m_gui->target(m_azimuth[row], m_elevation[row]); + m_gui->target(m_airports[row]->m_name, m_azimuth[row], m_elevation[row]); emit dataChanged(index, index); } return true; @@ -442,7 +453,29 @@ void ADSBDemodGUI::updatePosition(Aircraft *aircraft) aircraft->m_rangeItem->setText(QString::number(aircraft->m_range/1000.0, 'f', 1)); aircraft->m_azElItem->setText(QString("%1/%2").arg(std::round(aircraft->m_azimuth)).arg(std::round(aircraft->m_elevation))); if (aircraft == m_trackAircraft) - m_adsbDemod->setTarget(aircraft->m_azimuth, aircraft->m_elevation); + m_adsbDemod->setTarget(aircraft->targetName(), aircraft->m_azimuth, aircraft->m_elevation); + + // Send to Map feature + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_adsbDemod, "mapitems"); + if (mapMessageQueues) + { + QList::iterator it = mapMessageQueues->begin(); + + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(QString("%1").arg(aircraft->m_icao, 0, 16))); + swgMapItem->setLatitude(aircraft->m_latitude); + swgMapItem->setLongitude(aircraft->m_longitude); + swgMapItem->setImage(new QString(QString("qrc:///map/%1").arg(aircraft->getImage()))); + swgMapItem->setImageRotation(aircraft->m_heading); + swgMapItem->setText(new QString(aircraft->getText(true))); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_adsbDemod, swgMapItem); + (*it)->push(msg); + } + } } // Called when we have lat & long from local decode and we need to check if it is in a valid range (<180nm/333km) @@ -1359,6 +1392,12 @@ void ADSBDemodGUI::on_flightPaths_clicked(bool checked) m_aircraftModel.setFlightPaths(checked); } +void ADSBDemodGUI::on_allFlightPaths_clicked(bool checked) +{ + m_settings.m_allFlightPaths = checked; + m_aircraftModel.setAllFlightPaths(checked); +} + QString ADSBDemodGUI::getDataDir() { // Get directory to store app data in (aircraft & airport databases and user-definable icons) @@ -1633,7 +1672,7 @@ void ADSBDemodGUI::updateAirports() } // Set a static target, such as an airport -void ADSBDemodGUI::target(float az, float el) +void ADSBDemodGUI::target(const QString& name, float az, float el) { if (m_trackAircraft) { @@ -1642,7 +1681,7 @@ void ADSBDemodGUI::target(float az, float el) m_aircraftModel.aircraftUpdated(m_trackAircraft); m_trackAircraft = nullptr; } - m_adsbDemod->setTarget(az, el); + m_adsbDemod->setTarget(name, az, el); } void ADSBDemodGUI::targetAircraft(Aircraft *aircraft) @@ -1658,7 +1697,7 @@ void ADSBDemodGUI::targetAircraft(Aircraft *aircraft) // Track this aircraft m_trackAircraft = aircraft; if (aircraft->m_positionValid) - m_adsbDemod->setTarget(aircraft->m_azimuth, aircraft->m_elevation); + m_adsbDemod->setTarget(aircraft->targetName(), aircraft->m_azimuth, aircraft->m_elevation); // Change colour of new target aircraft->m_isTarget = true; m_aircraftModel.aircraftUpdated(aircraft); @@ -1922,6 +1961,8 @@ void ADSBDemodGUI::displaySettings() ui->flightPaths->setChecked(m_settings.m_flightPaths); m_aircraftModel.setFlightPaths(m_settings.m_flightPaths); + ui->allFlightPaths->setChecked(m_settings.m_allFlightPaths); + m_aircraftModel.setAllFlightPaths(m_settings.m_allFlightPaths); displayStreamIndex(); @@ -2035,6 +2076,21 @@ void ADSBDemodGUI::tick() ui->adsbData->removeRow(aircraft->m_icaoItem->row()); // Remove aircraft from hash i = m_aircraft.erase(i); + // Remove from map feature + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_adsbDemod, "mapitems"); + if (mapMessageQueues) + { + QList::iterator it = mapMessageQueues->begin(); + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(QString("%1").arg(aircraft->m_icao, 0, 16))); + swgMapItem->setImage(new QString("")); + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_adsbDemod, swgMapItem); + (*it)->push(msg); + } + } // And finally free its memory delete aircraft; } diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.h b/plugins/channelrx/demodadsb/adsbdemodgui.h index 512dd8ed7..352fc926d 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.h +++ b/plugins/channelrx/demodadsb/adsbdemodgui.h @@ -34,6 +34,7 @@ #include "util/azel.h" #include "util/movingaverage.h" #include "util/httpdownloadmanager.h" +#include "maincore.h" #include "adsbdemodsettings.h" #include "ourairportsdb.h" @@ -207,6 +208,19 @@ struct Aircraft { m_correlationItem = new QTableWidgetItem(); m_rssiItem = new QTableWidgetItem(); } + + QString getImage(); + QString getText(bool all=false); + + // Name to use when selected as a target + QString targetName() + { + if (!m_flight.isEmpty()) + return QString("Flight: %1").arg(m_flight); + else + return QString("ICAO: %1").arg(m_icao, 0, 16); + } + }; // Aircraft data model used by QML map item @@ -300,9 +314,16 @@ public: allAircraftUpdated(); } + void setAllFlightPaths(bool allFlightPaths) + { + m_allFlightPaths = allFlightPaths; + allAircraftUpdated(); + } + private: QList m_aircrafts; bool m_flightPaths; + bool m_allFlightPaths; }; // Airport data model used by QML map item @@ -445,7 +466,7 @@ public: virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } void highlightAircraft(Aircraft *aircraft); void targetAircraft(Aircraft *aircraft); - void target(float az, float el); + void target(const QString& name, float az, float el); bool setFrequency(float frequency); bool useSIUints() { return m_settings.m_siUnits; } @@ -547,6 +568,7 @@ private slots: void on_getOSNDB_clicked(); void on_getAirportDB_clicked(); void on_flightPaths_clicked(bool checked); + void on_allFlightPaths_clicked(bool checked); void onWidgetRolled(QWidget* widget, bool rollDown); void onMenuDialogCalled(const QPoint& p); void handleInputMessages(); diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.ui b/plugins/channelrx/demodadsb/adsbdemodgui.ui index 95886ee4e..5a0a8d312 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.ui +++ b/plugins/channelrx/demodadsb/adsbdemodgui.ui @@ -546,7 +546,7 @@ - Display flight paths + Display flight path for selected aircraft ^ @@ -563,6 +563,26 @@ + + + + Display flight paths for all aircraft + + + ^ + + + + :/icons/allflightpaths.png:/icons/allflightpaths.png + + + true + + + true + + + @@ -945,17 +965,17 @@ QWidget
QtQuickWidgets/QQuickWidget
- - ButtonSwitch - QToolButton -
gui/buttonswitch.h
-
RollupWidget QWidget
gui/rollupwidget.h
1
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
LevelMeterSignalDB QWidget diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp index c8edf7997..2f608063d 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp @@ -53,10 +53,11 @@ void ADSBDemodSettings::resetToDefaults() m_airportMinimumSize = AirportType::Medium; m_displayHeliports = false; m_flightPaths = true; + m_allFlightPaths = false; m_siUnits = false; m_tableFontName = "Liberation Sans"; m_tableFontSize = 9; - m_displayDemodStats = true; + m_displayDemodStats = false; m_correlateFullPreamble = true; m_demodModeS = false; m_deviceIndex = -1; @@ -109,6 +110,7 @@ QByteArray ADSBDemodSettings::serialize() const s.writeBool(30, m_autoResizeTableColumns); s.writeS32(31, m_interpolatorPhaseSteps); s.writeFloat(32, m_interpolatorTapsPerPhase); + s.writeBool(33, m_allFlightPaths); for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) s.writeS32(100 + i, m_columnIndexes[i]); @@ -188,6 +190,7 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data) d.readBool(30, &m_autoResizeTableColumns, false); d.readS32(31, &m_interpolatorPhaseSteps, 4); d.readFloat(32, &m_interpolatorTapsPerPhase, 3.5f); + d.readBool(33, &m_allFlightPaths, false); for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) d.readS32(100 + i, &m_columnIndexes[i], i); diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.h b/plugins/channelrx/demodadsb/adsbdemodsettings.h index 081e5934d..e3191632b 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.h +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.h @@ -66,6 +66,7 @@ struct ADSBDemodSettings } m_airportMinimumSize; //!< What's the minimum size airport that should be displayed bool m_displayHeliports; //!< Whether to display heliports on the map bool m_flightPaths; //!< Whether to display flight paths + bool m_allFlightPaths; //!< Whether to display flight paths for all aircraft bool m_siUnits; //!< Uses m,kph rather than ft/knts QString m_tableFontName; //!< Font to use for table int m_tableFontSize; diff --git a/plugins/channelrx/demodadsb/icons.qrc b/plugins/channelrx/demodadsb/icons.qrc index 4bafcd1f5..6923f2feb 100644 --- a/plugins/channelrx/demodadsb/icons.qrc +++ b/plugins/channelrx/demodadsb/icons.qrc @@ -2,5 +2,6 @@ icons/aircraft.png icons/controltower.png + icons/allflightpaths.png diff --git a/plugins/channelrx/demodadsb/icons/allflightpaths.png b/plugins/channelrx/demodadsb/icons/allflightpaths.png new file mode 100644 index 0000000000000000000000000000000000000000..e75c45a8c0bc7ca6eb48745e87c00fe8b1c43cc3 GIT binary patch literal 2025 zcmbVN2~ZPP7>_WofQEM$? zTeafRidN8iQYx*eEl|Lg@hCWI2OT>V588p&sZhoWqo94^$k+~Tr|#_Td;8w^zW@Gr zYeIa~M3FY=dKjX#a(H=wTBv3eq@Ie-HEUIXfUyuRSpmGVqk7tMqp?V!ez_haGO9VRh9G(C`HVX=besOHxJ-%K~3=SkG|` zh9DM;MPLyNXmcil%49M`C_+RcK0xr7rkFq1e( zn>93T7ztFuh{}*iBtRkm<&;TFTi69J9UzrBN6KLkHIy#|cc2E9Vj>YHk*yd)sc9{x z%Nq?P2H_Qrpg^K%aSs1CoR+|JwAqLQ3n?R>Ng|9X6Nc=5!xXfEHUn`$9r4TSDup7! zOzS8E*kF^QmO!cqg%p)ZrF>x2ppt4erZTY{ZX!sPQVs)e2q;R6p#iu|D3eL}5&{+T z#ZpNiKS+v8`6MpZWd!Jifq_!d$ay7AF9|tgkWhMXTi;6cqVYv zf94nh^hRvOIb;Izck2fCUMl1fIOqO}b57OUg;r`>`XFHgpGX|44tuH}aZ&o38W zFS*xV<>fcA@M*W#i!$fBpycg`rmmU$d0NE$FPI~|!lJOXGoq?I*W70-8akD4YJF5S*2D@v+AYf1N!womqHa42=@;~V0Vy-$!KZ5N&MUSFKh zc(F5PdcAYM;N~Q_$tCp49dX~0oyBc2lb;J_o_`o{W_4qa_UZV$CAarDue`86*r`|8 z72uo|TKQnXdcp400w%J2M_h_-<2c{WZ*ObY`4GQOkr0Uu1&m)^>gL3^+~XRk$L(H` zw|qN`D(|~YI$G&>`b@1Rv*q%EP@gPK`Ofou6wkWntiKSJcy?YO)L{>pg8}QuwiL686ym=3cKW&a@ z&$Zw6?>y&(rC(d|z>kp+`1c;2d+M95Jvua19+4g^a@?IYec6ti<&7QL4abPFDSkb^ zbF4nCk2~sI_h;n0{w}~yE;jD;>`!ukw`q^t-8&_n;r;btIU7no{Bqa8qv%}kqbYB0 zX^#}FTS;WBIp+4lxm)$Mh1gKuwh^fczVhs~m9d-7y0zw2oLX?bIq0|9h1mOVT&}3E_a^9smFU literal 0 HcmV?d00001