diff --git a/src/Makefile.am b/src/Makefile.am index d87f5d469..f131e1ec4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -118,6 +118,7 @@ noinst_HEADERS = core/stopwatch.h \ gui/MySettingsPanel.h \ gui/PickingResultsPanel.h \ gui/MyParticlePositionExportDialog.h \ + gui/MyPickingJobExportDialog.h \ gui/MyFrealignExportDialog.h \ gui/MyRelionExportDialog.h \ gui/MyRefinementPackageAssetPanel.h \ @@ -722,6 +723,7 @@ cisTEM_SOURCES = programs/projectx/projectx.cpp \ gui/ActionsPanelTm.cpp \ gui/MySettingsPanel.cpp \ gui/MyParticlePositionExportDialog.cpp \ + gui/MyPickingJobExportDialog.cpp \ gui/MyFrealignExportDialog.cpp \ gui/MyRelionExportDialog.cpp \ gui/MyRefinementPackageAssetPanel.cpp \ diff --git a/src/gui/MyParticlePositionAssetPanel.cpp b/src/gui/MyParticlePositionAssetPanel.cpp index 492a74ede..bbff910a2 100644 --- a/src/gui/MyParticlePositionAssetPanel.cpp +++ b/src/gui/MyParticlePositionAssetPanel.cpp @@ -1,5 +1,6 @@ //#include "../core/core_headers.h" #include "../core/gui_core_headers.h" +#include "MyPickingJobExportDialog.h" extern MyImageAssetPanel* image_asset_panel; @@ -8,6 +9,7 @@ MyParticlePositionAssetPanel::MyParticlePositionAssetPanel(wxWindow* parent) RenameAssetButton->Show(false); DisplayButton->Show(false); DisplayButton->Enable(false); + ExportButton->Show(true); Layout( ); Label0Title->SetLabel(""); @@ -354,7 +356,7 @@ void MyParticlePositionAssetPanel::NewFromParentClick(wxCommandEvent& event) { } void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { - // Get a text file which should have asset_id x_pos y_pos + // Get a text file which should have image_asset_id x_pos y_pos wxFileDialog openFileDialog(this, _("Open TXT file"), "", "", "TXT files (*.txt)|*.txt;*.txt", wxFD_OPEN | wxFD_FILE_MUST_EXIST); @@ -369,6 +371,18 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { MyErrorDialog* my_error = new MyErrorDialog(this); long image_asset_id; + // Create a new group named after the imported file (without extension) + wxString new_group_name = openFileDialog.GetFilename( ).BeforeLast('.'); + if ( new_group_name.IsEmpty( ) ) + new_group_name = openFileDialog.GetFilename( ); + current_group_number++; + all_groups_list->AddGroup(new_group_name); + all_groups_list->groups[all_groups_list->number_of_groups - 1].id = current_group_number; + AddGroupToDatabase(current_group_number, new_group_name.ToUTF8( ).data( ), current_group_number); + long new_group_index = all_groups_list->number_of_groups - 1; + + wxArrayLong imported_asset_indices; + ParticlePositionAsset temp_asset; temp_asset.pick_job_id = -1; temp_asset.picking_id = -1; @@ -387,7 +401,6 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { if ( current_line.IsEmpty( ) == false && current_line.StartsWith("#") == false ) { current_tokenizer.SetString(current_line); - wxPrintf("Current Line = %s, number_tokens = %li\n", current_line, current_tokenizer.CountTokens( )); if ( current_tokenizer.CountTokens( ) < 3 ) { my_error->ErrorText->AppendText(wxString::Format(wxT("Line %li contains less than 3 (%li) values and will be ignored\n"), counter, current_tokenizer.CountTokens( ))); @@ -440,6 +453,11 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { AddAsset(&temp_asset); //main_frame->current_project.database.AddNextParticlePositionAsset(temp_asset.asset_id, temp_asset.parent_id, temp_asset.pick_job_id, temp_asset.x_position, temp_asset.y_position); main_frame->current_project.database.AddNextParticlePositionAsset(&temp_asset); + + // Track this asset for the new group + long new_asset_index = all_assets_list->number_of_assets - 1; + all_groups_list->groups[new_group_index].AddMember(new_asset_index); + imported_asset_indices.Add(new_asset_index); } } } @@ -452,9 +470,20 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { main_frame->current_project.database.EndParticlePositionAssetInsert( ); + // Populate the new group in the database + if ( imported_asset_indices.GetCount( ) > 0 ) { + InsertArrayofGroupMembersToDatabase(new_group_index, &imported_asset_indices, NULL); + } + else { + // Nothing was imported; remove the empty group + all_groups_list->RemoveGroup(new_group_index); + RemoveGroupFromDatabase(current_group_number); + current_group_number--; + } + my_dialog->Destroy( ); - // errros? + // errors? if ( have_errors == true ) { my_error->ShowModal( ); @@ -462,6 +491,23 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { my_error->Destroy( ); + FillGroupList( ); + FillContentsList( ); + DirtyGroups( ); + main_frame->RecalculateAssetBrowser( ); + is_dirty = true; } } + +void MyParticlePositionAssetPanel::OnExportClick(wxCommandEvent& event) { + if ( ReturnNumberOfAssets( ) == 0 ) { + wxMessageBox(wxT("No particle positions to export."), wxT("Export"), + wxOK | wxICON_INFORMATION, this); + return; + } + + MyPickingJobExportDialog* dialog = new MyPickingJobExportDialog(this); + dialog->ShowModal( ); + dialog->Destroy( ); +} diff --git a/src/gui/MyParticlePositionAssetPanel.h b/src/gui/MyParticlePositionAssetPanel.h index 6da05309c..be17f8796 100644 --- a/src/gui/MyParticlePositionAssetPanel.h +++ b/src/gui/MyParticlePositionAssetPanel.h @@ -9,6 +9,7 @@ class MyParticlePositionAssetPanel : public MyAssetPanelParent { void ImportAssetClick(wxCommandEvent& event); void NewFromParentClick(wxCommandEvent& event); + void OnExportClick(wxCommandEvent& event); void EnableNewFromParentButton( ); diff --git a/src/gui/MyPickingJobExportDialog.cpp b/src/gui/MyPickingJobExportDialog.cpp new file mode 100644 index 000000000..4ca9d86f3 --- /dev/null +++ b/src/gui/MyPickingJobExportDialog.cpp @@ -0,0 +1,222 @@ +#include "../core/gui_core_headers.h" +#include "MyPickingJobExportDialog.h" + +extern MyMainFrame* main_frame; +extern MyImageAssetPanel* image_asset_panel; +extern MyParticlePositionAssetPanel* particle_position_asset_panel; + +// --------------------------------------------------------------------------- +// Construction +// --------------------------------------------------------------------------- + +MyPickingJobExportDialog::MyPickingJobExportDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, wxT("Export Particle Positions"), + wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE) { + + wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + + main_sizer->Add(new wxStaticText(this, wxID_ANY, wxT("Select export source:")), + 0, wxALL, 10); + + JobComboBox = new wxComboBox(this, wxID_ANY, wxEmptyString, + wxDefaultPosition, wxSize(360, -1), + 0, NULL, wxCB_READONLY); + main_sizer->Add(JobComboBox, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 10); + + ParticleCountText = new wxStaticText(this, wxID_ANY, wxEmptyString); + main_sizer->Add(ParticleCountText, 0, wxLEFT | wxBOTTOM, 10); + + main_sizer->Add(new wxStaticLine(this), 0, wxEXPAND | wxLEFT | wxRIGHT, 5); + + wxBoxSizer* button_sizer = new wxBoxSizer(wxHORIZONTAL); + wxButton* cancel_btn = new wxButton(this, wxID_ANY, wxT("Cancel")); + wxButton* export_btn = new wxButton(this, wxID_ANY, wxT("Export...")); + button_sizer->Add(cancel_btn, 0, wxALL, 5); + button_sizer->Add(export_btn, 0, wxALL, 5); + main_sizer->Add(button_sizer, 0, wxALIGN_RIGHT | wxALL, 5); + + SetSizer(main_sizer); + Fit( ); + Centre( ); + + FillComboBox( ); + + JobComboBox->Bind(wxEVT_COMBOBOX, &MyPickingJobExportDialog::OnJobSelectionChange, this); + export_btn->Bind(wxEVT_BUTTON, &MyPickingJobExportDialog::OnExportButtonClick, this); + cancel_btn->Bind(wxEVT_BUTTON, &MyPickingJobExportDialog::OnCancelButtonClick, this); +} + +// --------------------------------------------------------------------------- +// Populate combo box +// --------------------------------------------------------------------------- + +void MyPickingJobExportDialog::FillComboBox( ) { + entries_.clear( ); + long total = particle_position_asset_panel->ReturnNumberOfAssets( ); + + // --- Section 1: job-based entries --- + + // "All Jobs" + entries_.push_back({EntryType::AllJobs, 0, wxT("particles_all")}); + JobComboBox->Append(wxString::Format(wxT("All Jobs (%ld particles)"), total)); + + // Collect unique pick_job_ids in order of first appearance + std::vector seen_jobs; + for ( long i = 0; i < total; i++ ) { + int jid = particle_position_asset_panel->ReturnAssetPointer(i)->pick_job_id; + bool found = false; + for ( int v : seen_jobs ) + if ( v == jid ) { + found = true; + break; + } + if ( ! found ) + seen_jobs.push_back(jid); + } + + for ( int jid : seen_jobs ) { + long count = CountForEntry({(jid == -1 ? EntryType::ManualPicks : EntryType::Job), jid, wxEmptyString}); + if ( jid == -1 ) { + entries_.push_back({EntryType::ManualPicks, -1, wxT("particles_manual")}); + JobComboBox->Append(wxString::Format(wxT("Manually picked (%ld particles)"), count)); + } + else { + entries_.push_back({EntryType::Job, jid, + wxString::Format(wxT("particles_job_%d"), jid)}); + JobComboBox->Append(wxString::Format(wxT("Job #%d (%ld particles)"), jid, count)); + } + } + + // --- Section 2: user groups (groups 1+) --- + + int num_groups = particle_position_asset_panel->ReturnNumberOfGroups( ); + if ( num_groups > 1 ) { + // Dotted separator — stored as Separator entry, not selectable + entries_.push_back({EntryType::Separator, 0, wxEmptyString}); + JobComboBox->Append(wxT(" - - - - - - - - - - - - -")); + + for ( int g = 1; g < num_groups; g++ ) { + wxString gname = particle_position_asset_panel->ReturnGroupName(g); + long count = particle_position_asset_panel->ReturnGroupSize(g); + entries_.push_back({EntryType::Group, g, + wxT("particles_") + SanitizeFilename(gname)}); + JobComboBox->Append(wxString::Format(wxT("%s (%ld particles)"), gname, count)); + } + } + + JobComboBox->SetSelection(0); + last_valid_selection_ = 0; + ParticleCountText->SetLabel(wxString::Format(wxT("%ld particles will be exported"), total)); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +long MyPickingJobExportDialog::CountForEntry(const ComboEntry& e) const { + long total = particle_position_asset_panel->ReturnNumberOfAssets( ); + if ( e.type == EntryType::AllJobs ) + return total; + if ( e.type == EntryType::Group ) + return particle_position_asset_panel->ReturnGroupSize(e.value); + // Job or ManualPicks: count by pick_job_id + long count = 0; + for ( long i = 0; i < total; i++ ) + if ( particle_position_asset_panel->ReturnAssetPointer(i)->pick_job_id == e.value ) + count++; + return count; +} + +wxString MyPickingJobExportDialog::SanitizeFilename(const wxString& name) const { + wxString result = name; + result.Replace(wxT(" "), wxT("_")); + result.Replace(wxT("/"), wxT("_")); + result.Replace(wxT("\\"), wxT("_")); + return result; +} + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +void MyPickingJobExportDialog::OnJobSelectionChange(wxCommandEvent& event) { + int sel = JobComboBox->GetSelection( ); + if ( sel == wxNOT_FOUND ) + return; + + // Separator: snap back to last valid selection, do nothing + if ( entries_[sel].type == EntryType::Separator ) { + JobComboBox->SetSelection(last_valid_selection_); + return; + } + + last_valid_selection_ = sel; + long count = CountForEntry(entries_[sel]); + ParticleCountText->SetLabel(wxString::Format(wxT("%ld particles will be exported"), count)); + Fit( ); +} + +void MyPickingJobExportDialog::OnExportButtonClick(wxCommandEvent& event) { + int sel = JobComboBox->GetSelection( ); + if ( sel == wxNOT_FOUND ) + return; + const ComboEntry& entry = entries_[sel]; + if ( entry.type == EntryType::Separator ) + return; + + wxFileDialog save_dialog(this, wxT("Export particle positions"), + wxEmptyString, entry.default_filename + wxT(".txt"), + wxT("Text files (*.txt)|*.txt|All files (*.*)|*.*"), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if ( save_dialog.ShowModal( ) == wxID_CANCEL ) + return; + + wxFile output_file; + if ( ! output_file.Open(save_dialog.GetPath( ), wxFile::write) ) { + wxMessageBox(wxT("Could not open file for writing."), wxT("Export Error"), + wxOK | wxICON_ERROR, this); + return; + } + + //output_file.Write(wxT("# Image# X Y\n")); + + if ( entry.type == EntryType::Group ) { + // Export members of a user-created group + int g = entry.value; + long gsize = particle_position_asset_panel->ReturnGroupSize(g); + for ( long m = 0; m < gsize; m++ ) { + long asset_idx = particle_position_asset_panel->ReturnGroupMember(g, m); + ParticlePositionAsset* asset = particle_position_asset_panel->ReturnAssetPointer(asset_idx); + int image_pos = image_asset_panel->ReturnArrayPositionFromAssetID(asset->parent_id); + if ( image_pos < 0 ) + continue; + ImageAsset* img = image_asset_panel->ReturnAssetPointer(image_pos); + float x_pix = float(asset->x_position) / img->pixel_size + 1.0f; + float y_pix = float(img->y_size) - float(asset->y_position) / img->pixel_size + 1.0f; + output_file.Write(wxString::Format(wxT("%d %.2f %.2f\n"), asset->parent_id, x_pix, y_pix)); + } + } + else { + // Export by pick_job_id (AllJobs, Job, or ManualPicks) + long total = particle_position_asset_panel->ReturnNumberOfAssets( ); + for ( long i = 0; i < total; i++ ) { + ParticlePositionAsset* asset = particle_position_asset_panel->ReturnAssetPointer(i); + if ( entry.type != EntryType::AllJobs && asset->pick_job_id != entry.value ) + continue; + int image_pos = image_asset_panel->ReturnArrayPositionFromAssetID(asset->parent_id); + if ( image_pos < 0 ) + continue; + ImageAsset* img = image_asset_panel->ReturnAssetPointer(image_pos); + float x_pix = float(asset->x_position) / img->pixel_size + 1.0f; + float y_pix = float(img->y_size) - float(asset->y_position) / img->pixel_size + 1.0f; + output_file.Write(wxString::Format(wxT("%d %.2f %.2f\n"), asset->parent_id, x_pix, y_pix)); + } + } + + output_file.Close( ); + Close( ); +} + +void MyPickingJobExportDialog::OnCancelButtonClick(wxCommandEvent& event) { + Close( ); +} diff --git a/src/gui/MyPickingJobExportDialog.h b/src/gui/MyPickingJobExportDialog.h new file mode 100644 index 000000000..406841fc4 --- /dev/null +++ b/src/gui/MyPickingJobExportDialog.h @@ -0,0 +1,42 @@ +#ifndef _SRC_GUI_MYPICKINGJOBEPORTDIALOG_H_ +#define _SRC_GUI_MYPICKINGJOBEPORTDIALOG_H_ + +#include +#include +#include +#include + +class MyPickingJobExportDialog : public wxDialog { + public: + MyPickingJobExportDialog(wxWindow* parent); + + ~MyPickingJobExportDialog( ) {} + + private: + enum class EntryType { AllJobs, + Job, + ManualPicks, + Separator, + Group }; + + struct ComboEntry { + EntryType type; + int value; // pick_job_id for Job, group index for Group + wxString default_filename; // pre-filled in the save dialog + }; + + wxComboBox* JobComboBox; + wxStaticText* ParticleCountText; + std::vector entries_; + int last_valid_selection_ = 0; + + void FillComboBox( ); + long CountForEntry(const ComboEntry& e) const; + wxString SanitizeFilename(const wxString& name) const; + + void OnJobSelectionChange(wxCommandEvent& event); + void OnExportButtonClick(wxCommandEvent& event); + void OnCancelButtonClick(wxCommandEvent& event); +}; + +#endif diff --git a/src/gui/ProjectX_gui_assets.cpp b/src/gui/ProjectX_gui_assets.cpp index 0a0a34601..5b2acc26b 100644 --- a/src/gui/ProjectX_gui_assets.cpp +++ b/src/gui/ProjectX_gui_assets.cpp @@ -1072,6 +1072,12 @@ AssetPanelParent::AssetPanelParent( wxWindow* parent, wxWindowID id, const wxPoi bSizer28->Add( ResampleButton, 0, wxALL|wxEXPAND, 5 ); + ExportButton = new wxButton( m_panel3, wxID_ANY, wxT("Export"), wxDefaultPosition, wxDefaultSize, 0 ); + ExportButton->Hide(); + ExportButton->SetToolTip( wxT("Export particle positions to a text file") ); + + bSizer28->Add( ExportButton, 0, wxALL|wxEXPAND, 5 ); + bSizer25->Add( bSizer28, 0, wxEXPAND, 5 ); @@ -1260,6 +1266,7 @@ AssetPanelParent::AssetPanelParent( wxWindow* parent, wxWindowID id, const wxPoi AddSelectedAssetButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::AddSelectedAssetClick ), NULL, this ); DisplayButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnDisplayButtonClick ), NULL, this ); ResampleButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnResampleClick ), NULL, this ); + ExportButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnExportClick ), NULL, this ); } AssetPanelParent::~AssetPanelParent() @@ -1305,6 +1312,7 @@ AssetPanelParent::~AssetPanelParent() AddSelectedAssetButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::AddSelectedAssetClick ), NULL, this ); DisplayButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnDisplayButtonClick ), NULL, this ); ResampleButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnResampleClick ), NULL, this ); + ExportButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnExportClick ), NULL, this ); } diff --git a/src/gui/ProjectX_gui_assets.h b/src/gui/ProjectX_gui_assets.h index 79f76bdd7..ed9af62df 100644 --- a/src/gui/ProjectX_gui_assets.h +++ b/src/gui/ProjectX_gui_assets.h @@ -344,6 +344,7 @@ class AssetPanelParent : public wxPanel wxButton* AddSelectedAssetButton; wxButton* DisplayButton; wxButton* ResampleButton; + wxButton* ExportButton; wxStaticLine* m_staticline6; wxStaticText* Label0Title; wxStaticText* Label0Text; @@ -391,6 +392,7 @@ class AssetPanelParent : public wxPanel virtual void AddSelectedAssetClick( wxCommandEvent& event ) { event.Skip(); } virtual void OnDisplayButtonClick( wxCommandEvent& event ) { event.Skip(); } virtual void OnResampleClick( wxCommandEvent& event ) { event.Skip(); } + virtual void OnExportClick( wxCommandEvent& event ) { event.Skip(); } public: diff --git a/src/gui/wxformbuilder/ProjectX_assets.fbp b/src/gui/wxformbuilder/ProjectX_assets.fbp index df333387e..cad9f5095 100644 --- a/src/gui/wxformbuilder/ProjectX_assets.fbp +++ b/src/gui/wxformbuilder/ProjectX_assets.fbp @@ -8639,6 +8639,80 @@ OnResampleClick + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 1 + + 1 + + + 0 + 1 + wxID_ANY + Export + + 0 + + 0 + + + 0 + + 1 + ExportButton + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + Export particle positions to a text file + + wxFILTER_NONE + wxDefaultValidator + + + + + OnExportClick + +