diff --git a/apps/gui-qt/scolv/descriptions/scolv.xml b/apps/gui-qt/scolv/descriptions/scolv.xml
index d385e06e3..1b7fda05f 100644
--- a/apps/gui-qt/scolv/descriptions/scolv.xml
+++ b/apps/gui-qt/scolv/descriptions/scolv.xml
@@ -1028,6 +1028,43 @@
+
+
+ Configuration of the Nearby Cities tab in scolv. The tab lists
+ cities and locations near the current origin, sorted by distance.
+ Cities are sourced from the built-in cities.xml (via SCApp) and
+ optionally from a user-supplied extended locations file.
+
+
+
+ Maximum search radius in km around the origin. Cities beyond
+ this distance are not shown.
+
+
+
+
+ Maximum number of cities displayed. When more cities fall
+ within the search radius, the closest ones are kept.
+
+
+
+
+ Minimum population for a location to appear in the table.
+ Capital cities from cities.xml (category "C") are
+ always shown regardless of this threshold. Entries in the
+ extended locations file that have no population value (or
+ zero) are also always included.
+
+
+
+
+ When enabled, the full administrative region name is shown in the
+ Region column (e.g. "New South Wales") instead of the
+ abbreviation (e.g. "NSW"). If no region is available, the
+ full country name is shown as fallback.
+
+
+
diff --git a/apps/gui-qt/scolv/mainframe.cpp b/apps/gui-qt/scolv/mainframe.cpp
index 2f5d8dc60..ec4f837c4 100644
--- a/apps/gui-qt/scolv/mainframe.cpp
+++ b/apps/gui-qt/scolv/mainframe.cpp
@@ -56,9 +56,16 @@
#include
#include
#include
+#include
+
+#include
+#include
+#include
#include
#include
+#include
+#include
//#include
#define WITH_SMALL_SUMMARY
@@ -137,6 +144,31 @@ string trim(const std::string &str) {
}
+// Numeric QTableWidgetItem so distance/population columns sort correctly
+class NumericItem : public QTableWidgetItem {
+ public:
+ NumericItem(double value, const QString &text)
+ : QTableWidgetItem(text), _value(value) {}
+
+ bool operator<(const QTableWidgetItem &other) const override {
+ const NumericItem *o = dynamic_cast(&other);
+ return o ? _value < o->_value : QTableWidgetItem::operator<(other);
+ }
+ private:
+ double _value;
+};
+
+
+static const char *compassDir(double az) {
+ static const char *dirs[16] = {
+ "N","NNE","NE","ENE","E","ESE","SE","SSE",
+ "S","SSW","SW","WSW","W","WNW","NW","NNW"
+ };
+ return dirs[static_cast((az + 11.25) / 22.5) % 16];
+}
+
+
+
}
@@ -709,7 +741,7 @@ MainFrame::MainFrame(){
layoutMagnitudes->addWidget(_magnitudes);
_tabEventEdit = new QWidget;
- _ui.tabWidget->insertTab(_ui.tabWidget->count()-1, _tabEventEdit, "Event");
+ _ui.tabWidget->insertTab(_ui.tabWidget->indexOf(_ui.tabNearbyCities), _tabEventEdit, "Event");
_eventEdit = new EventEdit(SCApp->query(), mapTree.get(), _tabEventEdit);
QLayout* layoutEventEdit = new QVBoxLayout(_tabEventEdit);
@@ -990,6 +1022,26 @@ MainFrame::MainFrame(){
}
catch ( ... ) {}
+ // Nearby Cities config
+ try { _citiesMaxDist = SCApp->configGetDouble("cities.maxDist"); } catch ( ... ) {}
+ try { _citiesMaxCount = SCApp->configGetInt("cities.maxCount"); } catch ( ... ) {}
+ if ( _citiesMaxCount <= 0 ) _citiesMaxCount = 20;
+ try { _citiesMinPopulation = SCApp->configGetInt("cities.minPopulation"); } catch ( ... ) {}
+ try { _citiesUseFullState = SCApp->configGetBool("cities.useFullState"); } catch ( ... ) {}
+
+ _ui.citiesUseFullState->setChecked(_citiesUseFullState);
+
+ connect(_ui.citiesTable->selectionModel(),
+ &QItemSelectionModel::selectionChanged,
+ this, &MainFrame::onCitySelectionChanged);
+ connect(_ui.setRegionBtn, &QPushButton::clicked,
+ this, &MainFrame::onSetRegionName);
+ connect(_ui.regionFormatCombo,
+ QOverload::of(&QComboBox::currentIndexChanged),
+ this, &MainFrame::onRegionFormatChanged);
+ connect(_ui.citiesUseFullState, &QCheckBox::toggled,
+ this, [this](bool){ updateCitiesTab(_currentOrigin.get()); });
+
SCApp->settings().endGroup();
}
@@ -1295,7 +1347,7 @@ void MainFrame::setOrigin(Seiscomp::DataModel::Origin *o,
Seiscomp::DataModel::Event *e,
bool newOrigin, bool relocated) {
_currentOrigin = o;
- _eventID = "";
+ _eventID = e ? e->publicID() : "";
_magnitudes->setOrigin(o, e);
@@ -1306,6 +1358,9 @@ void MainFrame::setOrigin(Seiscomp::DataModel::Origin *o,
_eventSmallSummaryCurrent->setEvent(e, o, true);
#endif
+ updateCitiesTab(o);
+ updateCurrentRegionLabel(e);
+
if ( newOrigin && relocated && _computeMagnitudesAutomatically && (o->magnitudeCount() == 0) )
_originLocator->computeMagnitudes();
}
@@ -1348,6 +1403,9 @@ bool MainFrame::populateOrigin(Seiscomp::DataModel::Origin *org, Seiscomp::DataM
#endif
_eventEdit->setEvent(ev, org);
+ updateCitiesTab(org);
+ updateCurrentRegionLabel(ev);
+
return true;
}
@@ -1877,5 +1935,260 @@ void MainFrame::trayIconMessageClicked() {
}
+
+
+void MainFrame::updateCitiesTab(DataModel::Origin *origin) {
+ QTableWidget *t = _ui.citiesTable;
+ t->setSortingEnabled(false);
+ t->setRowCount(0);
+ _ui.regionPreview->clear();
+ _ui.setRegionBtn->setEnabled(false);
+
+ if ( !origin ) return;
+
+ double originLat = origin->latitude().value();
+ double originLon = origin->longitude().value();
+ double maxDistDeg = Math::Geo::km2deg(_citiesMaxDist);
+
+ struct Entry {
+ double distKm;
+ double az;
+ QString name;
+ QString type;
+ QString state;
+ QString country;
+ double population;
+ bool isCapital;
+ };
+ std::vector entries;
+
+ bool useFullState = _ui.citiesUseFullState->isChecked();
+
+ for ( const auto &city : SCApp->cities() ) {
+ double dist, az;
+ Math::Geo::delazi(originLat, originLon, city.lat, city.lon,
+ &dist, &az);
+ if ( dist > maxDistDeg ) continue;
+
+ bool isCapital = city.category() == "C";
+ if ( !isCapital &&
+ static_cast(city.population()) < _citiesMinPopulation )
+ continue;
+
+ const auto ®ion = city.adminRegion();
+ QString regionDisplay;
+ if ( !region.name.empty() )
+ regionDisplay = useFullState
+ ? QString::fromStdString(region.name)
+ : (region.abbr.empty()
+ ? QString::fromStdString(region.name)
+ : QString::fromStdString(region.abbr));
+ else
+ regionDisplay = QString::fromStdString(city.country());
+
+ entries.push_back({
+ Math::Geo::deg2km(dist), az,
+ QString::fromStdString(city.name()),
+ city.type() == Math::Geo::CITYTYPE_UNKNOWN ? QString("-") : QString(city.type().toString()),
+ regionDisplay,
+ QString::fromStdString(city.country()),
+ city.population(),
+ isCapital
+ });
+ }
+
+ std::sort(entries.begin(), entries.end(),
+ [](const Entry &a, const Entry &b){ return a.distKm < b.distKm; });
+
+ if ( static_cast(entries.size()) > _citiesMaxCount )
+ entries.resize(static_cast(_citiesMaxCount));
+
+ t->setRowCount(static_cast(entries.size()));
+
+ for ( int i = 0; i < static_cast(entries.size()); ++i ) {
+ const Entry &e = entries[i];
+
+ auto boldify = [&](QTableWidgetItem *item) {
+ if ( e.isCapital ) {
+ QFont f = item->font();
+ f.setBold(true);
+ item->setFont(f);
+ }
+ return item;
+ };
+
+ t->setItem(i, 0, boldify(new QTableWidgetItem(e.name)));
+ t->setItem(i, 1, boldify(new QTableWidgetItem(e.type)));
+ t->setItem(i, 2, boldify(new QTableWidgetItem(e.state)));
+ t->setItem(i, 3, boldify(new QTableWidgetItem(e.country)));
+ t->setItem(i, 4, boldify(new NumericItem(
+ e.distKm, QString::number(static_cast(e.distKm + 0.5)))));
+ t->setItem(i, 5, boldify(new QTableWidgetItem(
+ QString::fromLatin1(compassDir(e.az)))));
+ t->setItem(i, 6, boldify(new NumericItem(
+ e.population,
+ QString::number(static_cast(e.population)))));
+
+ auto *capItem = new QTableWidgetItem(
+ e.isCapital ? QString("\u2605") : QString());
+ capItem->setTextAlignment(Qt::AlignCenter);
+ t->setItem(i, 7, capItem);
+ }
+
+ t->setSortingEnabled(true);
+ t->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
+}
+
+
+void MainFrame::updateCurrentRegionLabel(DataModel::Event *event) {
+ if ( !event ) {
+ _ui.currentRegionLabel->setText(tr("Region: \u2014"));
+ return;
+ }
+ for ( size_t i = 0; i < event->eventDescriptionCount(); ++i ) {
+ auto *d = event->eventDescription(i);
+ if ( d->type() == DataModel::REGION_NAME ) {
+ _ui.currentRegionLabel->setText(
+ tr("Region: %1").arg(d->text().c_str()));
+ return;
+ }
+ }
+ _ui.currentRegionLabel->setText(tr("Region: \u2014"));
+}
+
+
+QString MainFrame::formatRegionName(const QString &name, const QString &state,
+ const QString &country, int distKm,
+ const QString &dir) const {
+ QString loc = !state.isEmpty() ? state : country;
+
+ switch ( _ui.regionFormatCombo->currentIndex() ) {
+ case 0: return QString("%1 of %2%3")
+ .arg(dir, name, loc.isEmpty() ? "" : ", " + loc);
+ case 1: return QString("%1, %2 at %3 km")
+ .arg(name, dir).arg(distKm);
+ case 2: return QString("%1 km %2 of %3")
+ .arg(distKm).arg(dir, name);
+ case 3: return QString("%1 (%2, %3 km)")
+ .arg(name, dir).arg(distKm);
+ case 4: return QString("%1 km %2 of %3%4")
+ .arg(distKm).arg(dir, name,
+ loc.isEmpty() ? "" : ", " + loc);
+ default: return name;
+ }
+}
+
+
+void MainFrame::updateRegionPreview() {
+ int row = _ui.citiesTable->currentRow();
+ if ( row < 0 ) {
+ _ui.regionPreview->clear();
+ _ui.setRegionBtn->setEnabled(false);
+ return;
+ }
+
+ auto cell = [&](int col) -> QString {
+ auto *item = _ui.citiesTable->item(row, col);
+ return item ? item->text() : QString();
+ };
+
+ QString name = cell(0);
+ QString state = cell(2);
+ QString country = cell(3);
+ int distKm = static_cast(cell(4).toDouble() + 0.5);
+ QString dir = cell(5);
+
+ _ui.regionPreview->setText(
+ formatRegionName(name, state, country, distKm, dir));
+ _ui.setRegionBtn->setEnabled(!_eventID.empty());
+}
+
+
+void MainFrame::onCitySelectionChanged() {
+ updateRegionPreview();
+}
+
+
+void MainFrame::onRegionFormatChanged() {
+ updateRegionPreview();
+}
+
+
+void MainFrame::onSetRegionName() {
+ QString regionText = _ui.regionPreview->text().trimmed();
+ if ( regionText.isEmpty() ) return;
+ if ( _eventID.empty() ) {
+ QMessageBox::warning(this, tr("No Event"),
+ tr("No event is currently loaded."));
+ return;
+ }
+
+ if ( QMessageBox::question(
+ this, tr("Set Region Name"),
+ tr("Set region name for event %1 to:\n\n\"%2\"?")
+ .arg(_eventID.c_str(), regionText),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes )
+ return;
+
+ DataModel::EventPtr evt = DataModel::Event::Find(_eventID);
+ if ( !evt && SCApp->query() )
+ evt = DataModel::Event::Cast(
+ SCApp->query()->loadObject(
+ DataModel::Event::TypeInfo(), _eventID));
+ if ( !evt ) {
+ QMessageBox::critical(this, tr("Error"),
+ tr("Could not load event '%1'.")
+ .arg(_eventID.c_str()));
+ return;
+ }
+
+ if ( SCApp->query() )
+ SCApp->query()->loadEventDescriptions(evt.get());
+
+ DataModel::EventDescription *desc = nullptr;
+ DataModel::Operation op = DataModel::OP_ADD;
+ DataModel::EventDescriptionPtr newDesc;
+
+ for ( size_t i = 0; i < evt->eventDescriptionCount(); ++i ) {
+ if ( evt->eventDescription(i)->type() == DataModel::REGION_NAME ) {
+ desc = evt->eventDescription(i);
+ op = DataModel::OP_UPDATE;
+ break;
+ }
+ }
+
+ if ( !desc ) {
+ newDesc = new DataModel::EventDescription;
+ newDesc->setType(DataModel::REGION_NAME);
+ evt->add(newDesc.get());
+ desc = newDesc.get();
+ }
+
+ desc->setText(regionText.toStdString());
+
+ DataModel::NotifierMessagePtr msg = new DataModel::NotifierMessage;
+ msg->attach(new DataModel::Notifier(
+ "EventParameters", DataModel::OP_UPDATE, evt.get()));
+ msg->attach(new DataModel::Notifier(evt->publicID(), op, desc));
+
+ if ( !SCApp->sendMessage(SCApp->messageGroups().event.c_str(), msg.get()) ) {
+ QMessageBox::critical(this, tr("Error"),
+ tr("Failed to send region name update.\n"
+ "Check messaging connection."));
+ return;
+ }
+
+ for ( DataModel::NotifierMessage::iterator it = msg->begin();
+ it != msg->end(); ++it )
+ SCApp->emitNotifier(it->get());
+
+ _ui.currentRegionLabel->setText(tr("Region: %1").arg(regionText));
+
+ QMessageBox::information(this, tr("Region Name Set"),
+ tr("Region name updated to:\n\"%1\"")
+ .arg(regionText));
+}
+
+
}
}
diff --git a/apps/gui-qt/scolv/mainframe.h b/apps/gui-qt/scolv/mainframe.h
index a99182787..362afe638 100644
--- a/apps/gui-qt/scolv/mainframe.h
+++ b/apps/gui-qt/scolv/mainframe.h
@@ -64,6 +64,9 @@ class MainFrame : public MainWindow {
private slots:
void configureAcquisition();
+ void onCitySelectionChanged();
+ void onSetRegionName();
+ void onRegionFormatChanged();
void messageAvailable(Seiscomp::Core::Message*, Seiscomp::Client::Packet*);
@@ -95,6 +98,12 @@ class MainFrame : public MainWindow {
private:
bool populateOrigin(Seiscomp::DataModel::Origin*, Seiscomp::DataModel::Event*, bool);
+ void updateCitiesTab(Seiscomp::DataModel::Origin*);
+ void updateRegionPreview();
+ void updateCurrentRegionLabel(Seiscomp::DataModel::Event*);
+ QString formatRegionName(const QString &name, const QString &state,
+ const QString &country, int distKm,
+ const QString &dir) const;
// This creates an EventParameters instance containing copies
// of all event attributes relevant for publication incl.
@@ -132,6 +141,12 @@ class MainFrame : public MainWindow {
bool _exportScriptTerminate;
QWidget *_currentTabWidget;
QProcess _exportProcess;
+
+ // Nearby Cities tab
+ double _citiesMaxDist{1000.0};
+ int _citiesMaxCount{20};
+ int _citiesMinPopulation{10000};
+ bool _citiesUseFullState{true};
};
diff --git a/apps/gui-qt/scolv/mainframe.ui b/apps/gui-qt/scolv/mainframe.ui
index 7eb09a17b..fb1c08918 100644
--- a/apps/gui-qt/scolv/mainframe.ui
+++ b/apps/gui-qt/scolv/mainframe.ui
@@ -67,6 +67,183 @@
Magnitudes
+
+
+ Nearby Cities
+
+
+
+ 4
+
+ -
+
+
-
+
+
+ Full state name
+
+
+ Show full state name (e.g. Queensland) instead of abbreviation (e.g. QLD)
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Region: —
+
+
+
+
+
+ -
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ QAbstractItemView::SelectRows
+
+
+ QAbstractItemView::SingleSelection
+
+
+ true
+
+
+ true
+
+
+
+ Name
+
+
+
+
+ Type
+
+
+
+
+ Region
+
+
+
+
+ Country
+
+
+
+
+ Distance (km)
+
+
+
+
+ Direction
+
+
+
+
+ Population
+
+
+
+
+ ★
+
+
+
+
+ -
+
+
+ Set Region Name
+
+
+
-
+
+
-
+
+
+ Format:
+
+
+
+ -
+
+
-
+
+ DIRECTION of NAME, STATE
+
+
+ -
+
+ NAME, DIRECTION at DISTANCE km
+
+
+ -
+
+ DISTANCE km DIRECTION of NAME
+
+
+ -
+
+ NAME (DIRECTION, DISTANCE km)
+
+
+ -
+
+ DISTANCE km DIRECTION of NAME, STATE
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+ Select a city to preview the region name...
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Set as Region Name
+
+
+ false
+
+
+
+
+
+
+
+
+
+
Events