diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 075c56d..40d4b50 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -7,6 +7,7 @@ set(NCVIS_SOURCE_FILES
wxNcVisFrame.cpp
wxNcVisOptionsDialog.cpp
wxNcVisExportDialog.cpp
+ wxNcVisLinePlot.cpp
wxImagePanel.cpp
GridDataSampler.cpp
ColorMap.cpp
diff --git a/src/ncvis.cpp b/src/ncvis.cpp
index 5fac5f1..e5a4c31 100644
--- a/src/ncvis.cpp
+++ b/src/ncvis.cpp
@@ -43,6 +43,10 @@ wxIMPLEMENT_APP(wxNcVisApp);
bool wxNcVisApp::OnInit() {
+ // PNG handler
+ wxInitAllImageHandlers();
+ wxImage::AddHandler(new wxPNGHandler);
+
// Turn off fatal errors in NetCDF
NcError error(NcError::silent_nonfatal);
diff --git a/src/wxImagePanel.cpp b/src/wxImagePanel.cpp
index cfdf89b..9fb89de 100644
--- a/src/wxImagePanel.cpp
+++ b/src/wxImagePanel.cpp
@@ -23,6 +23,7 @@ EVT_PAINT(wxImagePanel::OnPaint)
EVT_SIZE(wxImagePanel::OnSize)
EVT_IDLE(wxImagePanel::OnIdle)
EVT_LEFT_DCLICK(wxImagePanel::OnMouseLeftDoubleClick)
+EVT_LEFT_DOWN(wxImagePanel::OnMouseLeftDown)
EVT_MOTION(wxImagePanel::OnMouseMotion)
EVT_LEAVE_WINDOW(wxImagePanel::OnMouseLeaveWindow)
END_EVENT_TABLE()
@@ -442,6 +443,36 @@ void wxImagePanel::OnMouseLeftDoubleClick(wxMouseEvent & evt) {
////////////////////////////////////////////////////////////////////////////////
+void wxImagePanel::OnMouseLeftDown(wxMouseEvent & evt) {
+
+ if (!evt.ShiftDown()) {
+ evt.Skip();
+ return;
+ }
+
+ wxPoint pos = evt.GetPosition();
+
+ wxSize wxsMap;
+ wxPosition wxpMap;
+ GetMapPositionSize(wxsMap, wxpMap);
+
+ pos.x -= wxpMap.GetCol();
+ pos.y -= wxpMap.GetRow();
+
+ if ((pos.x < 0) || (pos.x >= (int)m_dSampleX.size())) return;
+ if ((pos.y < 0) || (pos.y >= (int)m_dSampleY.size())) return;
+
+ const double dLon = m_dSampleX[pos.x];
+ const double dLat = m_dSampleY[m_dSampleY.size() - pos.y - 1];
+
+ _ASSERT(m_pncvisparent != NULL);
+
+ // Open/update the time series plot
+ m_pncvisparent->OnShiftClickTimeSeries(dLat, dLon);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
void wxImagePanel::FormatLabelBarLabel(
double dValue,
std::string & str
diff --git a/src/wxImagePanel.h b/src/wxImagePanel.h
index ec63847..1bd36a7 100644
--- a/src/wxImagePanel.h
+++ b/src/wxImagePanel.h
@@ -91,6 +91,11 @@ class wxImagePanel : public wxPanel {
///
void OnMouseLeftDoubleClick(wxMouseEvent & evt);
+ ///
+ /// Callback for when Shift + mouse left-clicked.
+ ///
+ void OnMouseLeftDown(wxMouseEvent & evt);
+
public:
///
/// Format a label bar label from a value.
diff --git a/src/wxNcVisFrame.cpp b/src/wxNcVisFrame.cpp
index a9ede95..26b408b 100644
--- a/src/wxNcVisFrame.cpp
+++ b/src/wxNcVisFrame.cpp
@@ -11,11 +11,17 @@
#include "wxNcVisOptionsDialog.h"
#include "wxNcVisExportDialog.h"
+#include "wxNcVisLinePlot.h"
#include "STLStringHelper.h"
#include "ShpFile.h"
#include "TimeObj.h"
#include
+#include
+#include
#include
+#include
+#include
+#include
////////////////////////////////////////////////////////////////////////////////
@@ -96,6 +102,7 @@ wxNcVisFrame::wxNcVisFrame(
m_rightsizer(NULL),
m_vardimsizer(NULL),
m_imagepanel(NULL),
+ m_pLinePlot(NULL),
m_wxNcVisExportDialog(NULL),
m_wxDimTimer(this,ID_DIMTIMER),
m_varActive(NULL),
@@ -1084,6 +1091,297 @@ void wxNcVisFrame::MapSampleCoords1DFromActiveVar(
////////////////////////////////////////////////////////////////////////////////
+bool wxNcVisFrame::GetTimeDimensionForActiveVar(long & lTimeDim) const {
+
+ lTimeDim = -1;
+ if (m_varActive == NULL) {
+ return false;
+ }
+
+ const int nDims = m_varActive->num_dims();
+ if (nDims <= 0) {
+ return false;
+ }
+
+ for (int d = 0; d < nDims; ++d) {
+ NcDim * pDim = m_varActive->get_dim(d);
+ if (pDim == NULL) continue;
+
+ std::string name = pDim->name();
+ std::transform(name.begin(), name.end(), name.begin(), ::tolower);
+
+ if ((name == "time") ||
+ (name == "times") ||
+ (name == "t") ||
+ (name.find("time") != std::string::npos))
+ {
+ lTimeDim = d;
+ return true;
+ }
+ }
+
+ // Fallback assumption (common): first dimension is time
+ lTimeDim = 0;
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool wxNcVisFrame::BuildTimeSeriesAtLatLon(
+ double dLat, double dLon,
+ std::vector & vecTime,
+ std::vector & vecValue,
+ double & dLatNearest,
+ double & dLonNearest,
+ wxString & outTimeUnits,
+ time_t & outBaseEpoch
+) {
+ vecTime.clear();
+ vecValue.clear();
+ dLatNearest = dLat;
+ dLonNearest = dLon;
+
+ if (m_varActive == NULL) return false;
+
+ // ------------------------------------------------------------
+ // Find which NcFile contains the active variable (ncvis style)
+ // ------------------------------------------------------------
+ NcFile * pFile = NULL;
+ const std::string strVarName = m_varActive->name();
+
+ for (int vc = 0; vc < NcVarMaximumDimensions; ++vc) {
+ auto itVar = m_mapVarNames[vc].find(strVarName);
+ if (itVar != m_mapVarNames[vc].end()) {
+ const long ixFile = itVar->second[0];
+ if ((ixFile < 0) || (ixFile >= (long)m_vecpncfiles.size())) return false;
+ pFile = m_vecpncfiles[ixFile];
+ break;
+ }
+ }
+ if (pFile == NULL) return false;
+
+ // ------------------------------------------------------------
+ // Identify time / lat / lon dimensions of the active variable
+ // ------------------------------------------------------------
+ long lTimeDim = -1;
+ if (!GetTimeDimensionForActiveVar(lTimeDim)) return false;
+
+ const int nDims = m_varActive->num_dims();
+ if (nDims < 3) return false; // expecting (time,lat,lon) style
+
+ long lLatDim = -1;
+ long lLonDim = -1;
+
+ for (int d = 0; d < nDims; ++d) {
+ NcDim * pDim = m_varActive->get_dim(d);
+ if (pDim == NULL) continue;
+
+ std::string name = pDim->name();
+ std::transform(name.begin(), name.end(), name.begin(), ::tolower);
+
+ if (name == "lat" || name == "latitude" || name.find("lat") != std::string::npos) {
+ lLatDim = d;
+ }
+ if (name == "lon" || name == "longitude" || name.find("lon") != std::string::npos) {
+ lLonDim = d;
+ }
+ }
+
+ // Fallback for typical ordering if names weren't found
+ if (lLatDim < 0 || lLonDim < 0) {
+ if (lTimeDim == 0 && nDims >= 3) {
+ lLatDim = 1;
+ lLonDim = 2;
+ } else {
+ return false;
+ }
+ }
+
+ const long nT = m_varActive->get_dim((int)lTimeDim)->size();
+ const long nLat = m_varActive->get_dim((int)lLatDim)->size();
+ const long nLon = m_varActive->get_dim((int)lLonDim)->size();
+
+ if (nT <= 0 || nLat <= 0 || nLon <= 0) return false;
+
+ // ------------------------------------------------------------
+ // Read 1D coordinate variables for lat/lon from this file
+ // ------------------------------------------------------------
+ NcVar * varLat = pFile->get_var("lat");
+ if (varLat == NULL) varLat = pFile->get_var("latitude");
+ if (varLat == NULL) varLat = pFile->get_var(m_varActive->get_dim((int)lLatDim)->name());
+
+ NcVar * varLon = pFile->get_var("lon");
+ if (varLon == NULL) varLon = pFile->get_var("longitude");
+ if (varLon == NULL) varLon = pFile->get_var(m_varActive->get_dim((int)lLonDim)->name());
+
+ if (varLat == NULL || varLon == NULL) return false;
+
+ std::vector lat(nLat), lon(nLon);
+ if (!varLat->get(&lat[0], (long)nLat)) return false;
+ if (!varLon->get(&lon[0], (long)nLon)) return false;
+
+ auto nearestIndex = [](const std::vector & a, double x) -> long {
+ long best = 0;
+ double bestd = std::numeric_limits::infinity();
+ for (long i = 0; i < (long)a.size(); ++i) {
+ double d = std::fabs(a[i] - x);
+ if (d < bestd) { bestd = d; best = i; }
+ }
+ return best;
+ };
+
+ const long iLat = nearestIndex(lat, dLat);
+ const long iLon = nearestIndex(lon, dLon);
+
+ dLatNearest = lat[iLat];
+ dLonNearest = lon[iLon];
+
+ // ------------------------------------------------------------
+ // Time coordinate: try "time" variable
+ // ------------------------------------------------------------
+ vecTime.resize(nT);
+
+ for (long it = 0; it < nT; ++it) {
+ vecTime[it] = (double)it;
+ }
+
+ NcVar * varTime = pFile->get_var("time");
+
+ outTimeUnits = "time";
+ outBaseEpoch = 0;
+
+ if (varTime != NULL) {
+
+ // ---- Read time coordinate values robustly (works with netcdfcpp.h types) ----
+ NcValues * pTimeVals = varTime->values();
+ if (pTimeVals != NULL) {
+ long n = pTimeVals->num();
+ if (n > nT) n = nT;
+ for (long it = 0; it < n; ++it) {
+ vecTime[it] = pTimeVals->as_double(it);
+ }
+ delete pTimeVals;
+ }
+
+ // ---- Read units attribute and parse base date ----
+ NcAtt * attUnits = varTime->get_att("units");
+ if (attUnits != NULL) {
+ char buf[256];
+ buf[0] = '\0';
+
+ NcValues * pVals = attUnits->values();
+ if (pVals != NULL) {
+ const char * psz = pVals->as_string(0);
+ if (psz != NULL) {
+ std::strncpy(buf, psz, sizeof(buf)-1);
+ buf[sizeof(buf)-1] = '\0';
+ }
+ delete pVals;
+ }
+ delete attUnits;
+
+ outTimeUnits = wxString::Format("time (%s)", buf);
+
+ // Parse "days since YYYY-MM-DD HH:MM:SS" (time optional)
+ std::string s(buf);
+ size_t p = s.find("since");
+ if (p != std::string::npos) {
+ std::string rest = s.substr(p + 5);
+ while (!rest.empty() && (rest[0] == ' ' || rest[0] == '\t')) rest.erase(rest.begin());
+
+ int Y=0,M=0,D=0,h=0,m=0,sec=0;
+ int nn = std::sscanf(rest.c_str(), "%d-%d-%d %d:%d:%d", &Y,&M,&D,&h,&m,&sec);
+ if (nn >= 3) {
+ std::tm tmv;
+ std::memset(&tmv, 0, sizeof(tmv));
+ tmv.tm_year = Y - 1900;
+ tmv.tm_mon = M - 1;
+ tmv.tm_mday = D;
+ tmv.tm_hour = (nn >= 4) ? h : 0;
+ tmv.tm_min = (nn >= 5) ? m : 0;
+ tmv.tm_sec = (nn >= 6) ? sec : 0;
+
+ outBaseEpoch = std::mktime(&tmv);
+ }
+ }
+ }
+ }
+
+ // ------------------------------------------------------------
+ // Extract scalar value at (time,it) & fixed (lat,lon)
+ // ------------------------------------------------------------
+ vecValue.resize(nT);
+
+ std::vector cur(nDims, 0);
+ cur[(int)lLatDim] = iLat;
+ cur[(int)lLonDim] = iLon;
+
+ for (long it = 0; it < nT; ++it) {
+ cur[(int)lTimeDim] = it;
+
+ bool ok = false;
+ if (nDims == 3) {
+ ok = m_varActive->set_cur(cur[0], cur[1], cur[2]);
+ } else if (nDims == 4) {
+ ok = m_varActive->set_cur(cur[0], cur[1], cur[2], cur[3]);
+ } else if (nDims == 5) {
+ ok = m_varActive->set_cur(cur[0], cur[1], cur[2], cur[3], cur[4]);
+ } else {
+ return false;
+ }
+ if (!ok) return false;
+
+ float val = std::numeric_limits::quiet_NaN();
+
+ if (nDims == 3) {
+ if (!m_varActive->get(&val, 1, 1, 1)) return false;
+ } else if (nDims == 4) {
+ if (!m_varActive->get(&val, 1, 1, 1, 1)) return false;
+ } else if (nDims == 5) {
+ if (!m_varActive->get(&val, 1, 1, 1, 1, 1)) return false;
+ }
+
+ vecValue[it] = val;
+ }
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void wxNcVisFrame::OnShiftClickTimeSeries(double dLat, double dLon) {
+
+ std::vector vecTime;
+ std::vector vecValue;
+ double dLatNearest = dLat;
+ double dLonNearest = dLon;
+
+ wxString timeUnits("time");
+ time_t baseEpoch = 0;
+
+ if (!BuildTimeSeriesAtLatLon(dLat, dLon, vecTime, vecValue, dLatNearest, dLonNearest, timeUnits, baseEpoch)) {
+ SetStatusMessage("Shift+click: failed to extract time series", false);
+ return;
+ }
+
+ wxString title = wxString::Format(
+ "%s time series (lat,lon)=(%.4f, %.4f)",
+ GetVarActiveTitle().c_str(), dLatNearest, dLonNearest);
+
+ wxString yLabel = wxString::Format("%s (%s)",
+ GetVarActiveTitle().c_str(), GetVarActiveUnits().c_str());
+
+ if (m_pLinePlot == NULL) {
+ m_pLinePlot = new wxNcVisLinePlotFrame(this, title, vecTime, vecValue, timeUnits, baseEpoch, yLabel);
+ m_pLinePlot->Show();
+ } else {
+ m_pLinePlot->UpdatePlot(title, vecTime, vecValue, timeUnits, baseEpoch, yLabel);
+ if (!m_pLinePlot->IsShown()) m_pLinePlot->Show();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
void wxNcVisFrame::SampleData(
const std::vector & dSampleX,
const std::vector & dSampleY,
diff --git a/src/wxNcVisFrame.h b/src/wxNcVisFrame.h
index ac85ded..13fb6bf 100644
--- a/src/wxNcVisFrame.h
+++ b/src/wxNcVisFrame.h
@@ -20,11 +20,13 @@
#include