package Demeter::UI::Artemis; =for LiteratureReference I sing of Artemis, whose shafts are of gold, who cheers on the hounds, the pure maiden, shooter of stags, who delights in archery, own sister to Apollo with the golden sword. Over the shadowy hills and windy peaks she draws her golden bow, rejoicing in the chase, and sends out grievous shafts. The tops of the high mountains tremble and the tangled wood echoes awesomely with the outcry of beasts: earthquakes and the sea also where fishes shoal. But the goddess with a bold heart turns every way destroying the race of wild beasts: and when she is satisfied and has cheered her heart, this huntress who delights in arrows slackens her supple bow and goes to the great house of her dear brother Phoebus Apollo, to the rich land of Delphi, there to order the lovely dance of the Muses and Graces. There she hangs up her curved bow and her arrows, and heads and leads the dances, gracefully arrayed, while all they utter their heavenly voice, singing how neat-ankled Leto bare children supreme among the immortals both in thought and in deed. Hail to you, children of Zeus and rich-haired Leto! And now I will remember you and another song also. Homeric Hymns XXVII Translated by H. G. EVELYN-WHITE =cut use Demeter qw(:artemis); use Demeter::UI::Atoms; use Demeter::UI::Artemis::Import; use Demeter::UI::Artemis::Project; use Demeter::UI::Common::Buffer; use Demeter::UI::Common::Cursor; use Demeter::UI::Common::ShowText; use Demeter::UI::Wx::MRU; use Demeter::UI::Wx::SpecialCharacters qw(:all); use Demeter::UI::Wx::Colours; use Demeter::UI::Artemis::DataDropTarget; use Demeter::UI::Artemis::FeffDropTarget; use Demeter::UI::Wx::VerbDialog; use Archive::Zip qw( :ERROR_CODES :CONSTANTS ); local $Archive::Zip::UNICODE = 1; #use Capture::Tiny ':all'; use Cwd; use File::Basename; use File::Copy; use File::Path; use File::Spec; use List::MoreUtils qw(any zip); use Scalar::Util qw(blessed); use YAML::Tiny; use Wx qw(:everything); ##use Wx::Html; # so we can use Wx::HtmlEasyPrinting use Wx::Event qw(EVT_MENU EVT_CLOSE EVT_ICONIZE EVT_TOOL_ENTER EVT_CHECKBOX EVT_BUTTON EVT_TOGGLEBUTTON EVT_ENTER_WINDOW EVT_LEAVE_WINDOW EVT_TOOL_RCLICKED EVT_RIGHT_UP EVT_LEFT_DOWN EVT_NOTEBOOK_PAGE_CHANGING ); use base 'Wx::App'; use Const::Fast; const my $BLANK => q{___.BLANK.___}; const my $SAVENOHIST => Wx::NewId(); const my $SAVETHIS => Wx::NewId(); const my $MRU => Wx::NewId(); const my $SHOW_BUFFER => Wx::NewId(); const my $CONFIG => Wx::NewId(); const my $CRASH => Wx::NewId(); const my $SHOW_GROUPS => Wx::NewId(); const my $SHOW_ARRAYS => Wx::NewId(); const my $SHOW_SCALARS => Wx::NewId(); const my $SHOW_STRINGS => Wx::NewId(); const my $SHOW_FEFFPATHS => Wx::NewId(); const my $SHOW_PATHS => Wx::NewId(); const my $IMPORT_DPJ => Wx::NewId(); const my $IMPORT_FEFFIT => Wx::NewId(); const my $IMPORT_FEFF => Wx::NewId(); const my $IMPORT_MOLECULE => Wx::NewId(); const my $IMPORT_OLD => Wx::NewId(); const my $IMPORT_CHI => Wx::NewId(); const my $EXPORT_IFEFFIT => Wx::NewId(); const my $EXPORT_DEMETER => Wx::NewId(); const my $FIT_YAML => Wx::NewId(); const my $PLOT_YAML => Wx::NewId(); const my $MODE_YAML => Wx::NewId(); const my $MODE_STATUS => Wx::NewId(); const my $PERL_MODULES => Wx::NewId(); const my $STATUS => Wx::NewId(); const my $DOCUMENT => Wx::NewId(); const my $DOCUMENT_PLOT => Wx::NewId(); const my $DOCUMENT_FEFF => Wx::NewId(); const my $DOCUMENT_FIT => Wx::NewId(); const my $BUG => Wx::NewId(); const my $QUESTION => Wx::NewId(); const my $CHANGELOG => Wx::NewId(); const my $PLOT_PNG => Wx::NewId(); const my $PLOT_GIF => Wx::NewId(); const my $PLOT_JPG => Wx::NewId(); const my $PLOT_PDF => Wx::NewId(); const my $PLOT_XKCD => Wx::NewId(); const my $PLOT_ALL_DATA => Wx::NewId(); const my $PLOT_NO_DATA => Wx::NewId(); const my $TERM_1 => Wx::NewId(); const my $TERM_2 => Wx::NewId(); const my $TERM_3 => Wx::NewId(); const my $TERM_4 => Wx::NewId(); const my $IFEFFIT_MEMORY => Wx::NewId(); const my $IGNORE_NIDP => Wx::NewId(); const my $IGNORE_RBKG => Wx::NewId(); const my $IGNORE_RMAX => Wx::NewId(); const my $IGNORE_DATACOLL => Wx::NewId(); use Wx::Perl::Carp qw(verbose); $SIG{__WARN__} = sub {Wx::Perl::Carp::warn($_[0])}; $SIG{__DIE__} = sub {Wx::Perl::Carp::warn($_[0])}; ##$SIG{PIPE} = 'IGNORE'; sub identify_self { my @caller = caller; return dirname($caller[1]); }; use vars qw($buffer $plotbuffer $artemis_base $icon $nset $noautosave %frames %fit_order); $fit_order{order}{current} = 0; $nset = 0; $artemis_base = identify_self(); $noautosave = 0; # set this to skip autosave, see Demeter::UI::Artemis::Import::_feffit my %hints = ( gds => "Display/hide the Guess/Def/Set parameters dialog", plot => "Display/hide the plotting controls dialog", log => "Display/hide the fit log", fit => "Display/hide the fit history dialog", journal => "Display/hide the fit journal", ); sub OnInit { my ($app) = @_; Demeter -> set_mode(backend=>1, screen=>0); Demeter -> mo -> ui('Wx'); Demeter -> mo -> identity('Artemis'); Demeter -> plot_with(Demeter->co->default(qw(plot plotwith))); Demeter -> po -> space('R'); ## -------- import all of Artemis' various parts foreach my $m (qw(GDS Plot History Journal Log Status Config Data)) { next if $INC{"Demeter/UI/Artemis/$m.pm"}; ##print "Demeter/UI/Artemis/$m.pm\n"; require "Demeter/UI/Artemis/$m.pm"; }; foreach my $m (qw(Buffer Prj)) { next if $INC{"Demeter/UI/Common/$m.pm"}; ##print "Demeter/UI/Common/$m.pm\n"; require "Demeter/UI/Common/$m.pm"; }; ## -------- create a new frame and set icon $frames{main} = Wx::Frame->new(undef, -1, 'Artemis [EXAFS data analysis] - ', [0,0], # position -- along top of screen [Wx::SystemSettings::GetMetric(wxSYS_SCREEN_X), -1] # size -- entire width of screen ); $frames{main} -> SetBackgroundColour( $wxBGC ); my $iconfile = File::Spec->catfile(dirname($INC{'Demeter/UI/Artemis.pm'}), 'Artemis', 'icons', "artemis.png"); $icon = Wx::Icon->new( $iconfile, wxBITMAP_TYPE_ANY ); $frames{main} -> SetIcon($icon); $frames{main} -> {currentfit} = q{}; $frames{main} -> {projectname} = ''; $frames{main} -> {projectpath} = q{}; $frames{main} -> {modified} = 0; $frames{main} -> {cvcount} = 0; $app->{main} = $frames{main}; ##$frames{main}->{printer} = Wx::HtmlEasyPrinting -> new("Printing", $frames{main}); $app->{main}->{prefgroups} = [sort qw(gnuplot larch indicator marker artemis feff happiness pathfinder fft plot atoms file histogram bft fit interpolation bkg fspath lcf warnings gds operations)]; ## -------- Set up menubar my $bar = Wx::MenuBar->new; my $filemenu = Wx::Menu->new; my $mrumenu = Wx::Menu->new; my $importmenu = Wx::Menu->new; $importmenu->Append($IMPORT_CHI, "$CHI(k) data", "Import $CHI(k) data from a column data file"); $importmenu->Append($IMPORT_DPJ, "Demeter fit serialization", "Import a Demeter fit serialization (.dpj) file"); #$importmenu->AppendSeparator; #$importmenu->Append($IMPORT_FEFF, "an external Feff calculation", "Import a Feff input file and the results of a calculation already made with that file"); #$importmenu->Append($IMPORT_MOLECULE, "a molecule", "Import a molecule using OpenBabel"); $importmenu->AppendSeparator; $importmenu->Append($IMPORT_OLD, "an old-style Artemis project", "Import the current fitting model from an old-style Artemis project file"); $importmenu->Append($IMPORT_FEFFIT, "a feffit.inp file", "Import a fitting model from a feffit.inp file"); #$importmenu->Enable($IMPORT_CHI, 0); $importmenu->Enable($IMPORT_MOLECULE, 0); my $exportmenu = Wx::Menu->new; $exportmenu->Append($EXPORT_IFEFFIT, "to an ".Demeter->backend_name." script", "Export the current fitting model as an ".Demeter->backend_name." script"); $exportmenu->Append($EXPORT_DEMETER, "to a Demeter script", "Export the current fitting model as a perl script using Demeter"); $filemenu->Append(wxID_OPEN, "Open project or data\tCtrl+o", "Read from a project file or import data" ); $filemenu->AppendSubMenu($mrumenu, "Recent files", "Open a submenu of recently used files" ); $filemenu->Append(wxID_SAVE, "Save project\tCtrl+s", "Save project" ); $filemenu->Append(wxID_SAVEAS, "Save project as...", "Save to a new project file" ); $filemenu->Append($SAVETHIS, "Save current fit", "Save current fit without history to a project file" ); $filemenu->AppendSeparator; $filemenu->AppendSubMenu($importmenu, "Import...", "Export a fitting model from ..." ); $filemenu->AppendSubMenu($exportmenu, "Export...", "Export the current fitting model as ..." ); $filemenu->AppendSeparator; $filemenu->Append(wxID_PREFERENCES , "Edit Preferences", "Show the preferences editing dialog"); $filemenu->AppendSeparator; $filemenu->Append(wxID_CLOSE, "&Close\tCtrl+w" ); $filemenu->Append(wxID_EXIT, "E&xit\tCtrl+q" ); $frames{main}->{filemenu} = $filemenu; $frames{main}->{mrumenu} = $mrumenu; $frames{main}->{mruartemis} = Wx::Menu->new; $frames{main}->{mrufit} = Wx::Menu->new; $frames{main}->{mruathena} = Wx::Menu->new; $frames{main}->{mrustructure} = Wx::Menu->new; $frames{main}->{mruold} = Wx::Menu->new; $mrumenu->AppendSubMenu($frames{main}->{mruartemis}, "Artemis projects" ); $mrumenu->AppendSubMenu($frames{main}->{mruathena}, "Athena projects" ); $mrumenu->AppendSubMenu($frames{main}->{mrustructure}, "Crystal/structure data" ); $mrumenu->AppendSubMenu($frames{main}->{mrufit}, "Fit serializations" ); $mrumenu->AppendSubMenu($frames{main}->{mruold}, "Old-style artemis projects" ); my $showmenu = Wx::Menu->new; $showmenu->Append($SHOW_GROUPS, "groups", "Show Ifeffit groups"); $showmenu->Append($SHOW_ARRAYS, "arrays", "Show Ifeffit arrays"); $showmenu->Append($SHOW_SCALARS, "scalars", "Show Ifeffit scalars"); $showmenu->Append($SHOW_STRINGS, "strings", "Show Ifeffit strings"); $showmenu->Append($SHOW_PATHS, "paths", "Show Ifeffit paths"); $showmenu->Append($SHOW_FEFFPATHS, "feffpaths", "Show Ifeffit feffpaths"); my $debugmenu = Wx::Menu->new; $debugmenu->Append($FIT_YAML, "Show YAML for current Fit object", "Show YAML dialog for current Fit object", wxITEM_NORMAL ); $debugmenu->Append($PLOT_YAML, "Show YAML for Plot object", "Show YAML dialog for Plot object", wxITEM_NORMAL ); #$debugmenu->Append($MODE_YAML, "Show YAML for Mode object", "Show YAML dialog for Plot object", wxITEM_NORMAL ); $debugmenu->Append($MODE_STATUS, "Show mode status", "Show mode status dialog", wxITEM_NORMAL ); $debugmenu->Append($PERL_MODULES, "Show perl modules", "Show perl module versions", wxITEM_NORMAL ); #$debugmenu->Append($CRASH, "Crash Artemis", "Force a crash of Artemis to test autosave file", wxITEM_NORMAL ); my $sanitymenu = Wx::Menu->new; $sanitymenu->AppendCheckItem($IGNORE_NIDP, "Skip Nidp check", "Skip test verifying that the number of guesses is less than Nidp (this is STRONGLY discouraged!)"); $sanitymenu->Check($IGNORE_NIDP, 0); $sanitymenu->AppendCheckItem($IGNORE_RBKG, "Skip Rmin>Rbkg check", "Skip test verifying that Rmin is equal to or greater than Rbkg (this is STRONGLY discouraged!)"); $sanitymenu->Check($IGNORE_RBKG, 0); $sanitymenu->AppendCheckItem($IGNORE_RMAX, "Skip paths within Rmax check", "Skip test verifying that no paths are much larger than Rmax (this is STRONGLY discouraged!)"); $sanitymenu->Check($IGNORE_RMAX, 0); $sanitymenu->AppendCheckItem($IGNORE_DATACOLL, "Skip data collision check", "Skip test that no data group is used more than once in the fit (this is STRONGLY discouraged!)"); $sanitymenu->Check($IGNORE_DATACOLL, 0); my $fitmenu = Wx::Menu->new; $frames{main}->{fitmenu} = $fitmenu; $fitmenu->AppendSubMenu($sanitymenu, 'Disable sanity checks', 'Disable selected sanity checks that are performed on a fit.'); my $feedbackmenu = Wx::Menu->new; $feedbackmenu->Append($SHOW_BUFFER, "Show command buffer", 'Show the '.Demeter->backend_name.' and plotting commands buffer'); $feedbackmenu->Append($STATUS, "Show status bar buffer", 'Show the buffer containing messages written to the status bars'); $feedbackmenu->AppendSubMenu($showmenu, "Show ".Demeter->backend_name." ...", 'Show variables from '.Demeter->backend_name); $feedbackmenu->AppendSubMenu($debugmenu, 'Debug options', 'Display debugging tools'); ##if (Demeter->co->default("artemis", "debug_menus")); $feedbackmenu->Append($IFEFFIT_MEMORY, "Show Ifeffit's memory use", "Show Ifeffit's memory use and remaining capacity") if (not Demeter->is_larch); #my $settingsmenu = Wx::Menu->new; my $plotmenu = Wx::Menu->new; $frames{main}->{plotmenu} = $plotmenu; $plotmenu->Append($PLOT_PNG, "Last plot to png file", "Send the last plot to a png file"); $plotmenu->Append($PLOT_PDF, "Last plot to pdf file", "Send the last plot to a pdf file"); $plotmenu->AppendCheckItem($PLOT_XKCD, 'Plot XKCD style', 'Plot more or less in the style of an XKCD cartoon'); $plotmenu->AppendSeparator; $plotmenu->AppendRadioItem($TERM_1, "Plot to terminal 1", "Plot to terminal 1"); $plotmenu->AppendRadioItem($TERM_2, "Plot to terminal 2", "Plot to terminal 2"); $plotmenu->AppendRadioItem($TERM_3, "Plot to terminal 3", "Plot to terminal 3"); $plotmenu->AppendRadioItem($TERM_4, "Plot to terminal 4", "Plot to terminal 4"); $plotmenu->AppendSeparator; $plotmenu->Append($PLOT_ALL_DATA, "Plot all data sets after fit", "Set all data sets to be plotted after a fit finishes"); $plotmenu->Append($PLOT_NO_DATA, "Plot no data sets after fit", "Set all data sets NOT to be plotted after a fit finishes"); $plotmenu->Check($PLOT_XKCD, Demeter->co->default('gnuplot', 'xkcd')); my $helpmenu = Wx::Menu->new; $helpmenu->Append($DOCUMENT, "Users' Guide" ); $helpmenu->Append($DOCUMENT_PLOT, "Documentation: Plot window" ); $helpmenu->Append($DOCUMENT_FEFF, "Documentation: Atoms and Feff" ); $helpmenu->Append($DOCUMENT_FIT, "Documentation: Running a fit" ); $helpmenu->Append($BUG, "Report a bug", "How to report a bug in Artemis" ); $helpmenu->Append($QUESTION, "Ask a question", "How to ask a question about Artemis" ); $helpmenu->AppendSeparator; $helpmenu->Append($CHANGELOG, "Demeter change log", "View the Demeter change log online" ); $helpmenu->Append(wxID_ABOUT, "&About Artemis" ); $bar->Append( $filemenu, "&File" ); $bar->Append( $feedbackmenu, "&Monitor" ); $bar->Append( $fitmenu, "Fi&t" ); $bar->Append( $plotmenu, "Plot" ) if (Demeter->co->default('plot', 'plotwith') eq 'gnuplot'); $bar->Append( $helpmenu, "&Help" ); $frames{main}->SetMenuBar( $bar ); my $hbox = Wx::BoxSizer->new( wxHORIZONTAL); ## -------- status bar $frames{main}->{statusbar} = $frames{main}->CreateStatusBar; ## -------- GDS and Plot toolbar my $vbox = Wx::BoxSizer->new( wxVERTICAL); $hbox -> Add($vbox, 0, wxALL, 5); my $toolbar = Wx::ToolBar->new($frames{main}, -1, wxDefaultPosition, wxDefaultSize, wxTB_VERTICAL|wxTB_HORZ_TEXT); $frames{main}->{toolbar} = $toolbar; $frames{main}->{gds_toggle} = $toolbar -> AddCheckTool(1, "GDS", icon("gds"), wxNullBitmap, q{}, $hints{gds} ); $frames{main}->{plot_toggle} = $toolbar -> AddCheckTool(2, "Plot", icon("plot"), wxNullBitmap, q{}, $hints{plot} ); $frames{main}->{history_toggle} = $toolbar -> AddCheckTool(3, " History", icon("history"), wxNullBitmap, q{}, $hints{fit} ); $frames{main}->{journal_toggle} = $toolbar -> AddCheckTool(4, " Journal", icon("journal"), wxNullBitmap, q{}, $hints{journal} ); $toolbar -> Realize; $vbox -> Add($toolbar, 0, wxALL, 0); ## -------- Data box $vbox = Wx::BoxSizer->new( wxVERTICAL); $hbox -> Add($vbox, 0, wxALL, 5); my $databox = Wx::StaticBox->new($frames{main}, -1, 'Data sets', wxDefaultPosition, wxDefaultSize); my $databoxsizer = Wx::StaticBoxSizer->new( $databox, wxVERTICAL ); my $datalist = Wx::ScrolledWindow->new($frames{main}, -1, wxDefaultPosition, wxDefaultSize, wxVSCROLL); $datalist->SetScrollbars(20, 20, 50, 50); my $datavbox = Wx::BoxSizer->new( wxVERTICAL ); $datalist->SetSizer($datavbox); $frames{main}->{newdata} = Wx::Button->new($datalist, wxID_ADD, "", wxDefaultPosition, wxDefaultSize, wxNO_BORDER); $datavbox -> Add($frames{main}->{newdata}, 0, wxGROW|wxRIGHT, 5); mouseover($frames{main}->{newdata}, "Import a new data set. Right click for a menu of recently used Athena project files. Drag and drop Athena or Artemis project files."); EVT_BUTTON($frames{main}->{newdata}, -1, sub{Import('prj', q{})}); $datavbox -> Add(Wx::StaticLine->new($datalist, -1, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL), 0, wxGROW|wxALL, 2); $databoxsizer -> Add($datalist, 1, wxGROW|wxALL, 0); $hbox -> Add($databoxsizer, 2, wxGROW|wxALL, 0); $frames{main}->{datalist} = $datalist; $frames{main}->{databox} = $datavbox; EVT_RIGHT_UP($frames{main}->{newdata}, \&OnDataRightClick); $databox->SetDropTarget( Demeter::UI::Artemis::DataDropTarget->new( $databox ) ); ## -------- Feff box $vbox = Wx::BoxSizer->new( wxVERTICAL); $hbox -> Add($vbox, 0, wxALL, 5); my $feffbox = Wx::StaticBox->new($frames{main}, -1, 'Feff calculations', wxDefaultPosition, wxDefaultSize); my $feffboxsizer = Wx::StaticBoxSizer->new( $feffbox, wxVERTICAL ); my $fefflist = Wx::ScrolledWindow->new($frames{main}, -1, wxDefaultPosition, wxDefaultSize, wxVSCROLL); $fefflist->SetScrollbars(20, 20, 50, 50); my $feffvbox = Wx::BoxSizer->new( wxVERTICAL); $fefflist->SetSizer($feffvbox); $frames{main}->{newfeff} = Wx::Button->new($fefflist, wxID_ADD, "", wxDefaultPosition, wxDefaultSize, wxNO_BORDER); $feffvbox -> Add($frames{main}->{newfeff}, 0, wxGROW|wxRIGHT, 5); mouseover($frames{main}->{newfeff}, "Start a new Feff calculation. Right click for a menu of recently used crystal or Feff input files or to open an empty Atoms input file. Drag and drop Feff/Atoms/CIF files."); EVT_BUTTON($frames{main}->{newfeff}, -1, sub{Import('feff')}); $feffvbox -> Add(Wx::StaticLine->new($fefflist, -1, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL), 0, wxGROW|wxALL, 2); $feffboxsizer -> Add($fefflist, 1, wxGROW|wxALL, 0); $hbox -> Add($feffboxsizer, 2, wxGROW|wxALL, 0); $frames{main}->{fefflist} = $fefflist; $frames{main}->{feffbox} = $feffvbox; EVT_RIGHT_UP($frames{main}->{newfeff}, \&OnFeffRightClick); $feffbox->SetDropTarget( Demeter::UI::Artemis::FeffDropTarget->new( $feffbox ) ); ## -------- Fit box $vbox = Wx::BoxSizer->new( wxVERTICAL); $hbox -> Add($vbox, 4, wxGROW|wxLEFT|wxRIGHT|wxTOP, 5); my $hname = Wx::BoxSizer->new( wxHORIZONTAL); $vbox -> Add($hname, 0, wxGROW|wxTOP|wxBOTTOM, 0); my $label = Wx::StaticText->new($frames{main}, -1, "Name"); $frames{main}->{name} = Wx::TextCtrl->new($frames{main}, -1, "Fit 1"); $hname -> Add($label, 0, wxALL, 5); $hname -> Add($frames{main}->{name}, 1, wxALL, 2); mouseover($frames{main}->{name}, "Provide a short description of this fitting model."); $hname -> Add(Wx::StaticLine->new($frames{main}, -1, wxDefaultPosition, [4,-1], wxLI_VERTICAL), 0, wxGROW|wxLEFT|wxRIGHT, 7); $label = Wx::StaticText->new($frames{main}, -1, "Fit space:"); my @fitspace = (Wx::RadioButton->new($frames{main}, -1, 'k', wxDefaultPosition, wxDefaultSize, wxRB_GROUP), Wx::RadioButton->new($frames{main}, -1, 'R', wxDefaultPosition, wxDefaultSize), Wx::RadioButton->new($frames{main}, -1, 'q', wxDefaultPosition, wxDefaultSize), ); $frames{main}->{fitspace} = \@fitspace; my $savebutton = Wx::Button->new($frames{main}, wxID_SAVE, q{}); EVT_BUTTON($savebutton, -1, sub{save_project(\%frames, $frames{main}->{projectpath})}); mouseover($savebutton, "One-click save this project"); $hname -> Add($label, 0, wxALL, 3); map {$hname -> Add($_, 0, wxLEFT|wxRIGHT, 2)} @fitspace; $hname -> Add(Wx::StaticLine->new($frames{main}, -1, wxDefaultPosition, [4,-1], wxLI_VERTICAL), 0, wxGROW|wxLEFT|wxRIGHT, 7); $hname -> Add($savebutton, 0, wxLEFT|wxRIGHT, 3); $fitspace[0]->SetValue(1) if (Demeter->co->default("fit", "space") eq 'k'); $fitspace[1]->SetValue(1) if (Demeter->co->default("fit", "space") eq 'r'); $fitspace[2]->SetValue(1) if (Demeter->co->default("fit", "space") eq 'q'); mouseover($fitspace[0], "Evaluate the fitting metric in k-space."); mouseover($fitspace[1], "Evaluate the fitting metric in R-space."); mouseover($fitspace[2], "Evaluate the fitting metric in q-space."); my $descbox = Wx::StaticBox->new($frames{main}, -1, 'Fit description', wxDefaultPosition, wxDefaultSize); my $descboxsizer = Wx::StaticBoxSizer->new( $descbox, wxVERTICAL ); $frames{main}->{description} = Wx::TextCtrl->new($frames{main}, -1, q{}, wxDefaultPosition, [-1, 25], wxTE_MULTILINE); $descboxsizer -> Add($frames{main}->{description}, 1, wxGROW|wxALL, 0); $vbox -> Add($descboxsizer, 1, wxGROW|wxALL, 0); mouseover($frames{main}->{description}, "Use this space to fully describe this fitting model."); $vbox = Wx::BoxSizer->new( wxVERTICAL); $hbox -> Add($vbox, 0, wxGROW|wxALL, 0); $frames{main}->{fitbutton} = Wx::Button->new($frames{main}, -1, "F&it", wxDefaultPosition, wxDefaultSize); $frames{main}->{fitbutton} -> SetForegroundColour(Wx::Colour->new("#000000")); $frames{main}->{fitbutton} -> SetBackgroundColour(Wx::Colour->new(Demeter->co->default("happiness", "average_color"))); $frames{main}->{fitbutton} -> SetFont(Wx::Font->new( 10, wxDEFAULT, wxNORMAL, wxBOLD, 0, "" ) ); $vbox->Add($frames{main}->{fitbutton}, 1, wxGROW|wxALL, 2); mouseover($frames{main}->{fitbutton}, "Start the fit."); $frames{main}->{savehist} = Wx::CheckBox->new($frames{main}, -1, "History"); $vbox -> Add($frames{main}->{savehist}, 0, wxALL, 2); $frames{main}->{savehist}->SetValue(1); mouseover($frames{main}->{savehist}, "When toggled on, the next fit will be saved in the fit hstory of this project."); $frames{main}->{log_toggle} = Wx::ToggleButton -> new($frames{main}, -1, "Show &log",); $vbox->Add($frames{main}->{log_toggle}, 0, wxGROW|wxALL, 2); mouseover($frames{main}->{log_toggle}, $hints{log}); EVT_MENU ($frames{main}, -1, sub{my ($frame, $event) = @_; OnMenuClick($frame, $event)} ); EVT_CLOSE ($frames{main}, \&on_close); EVT_MENU ($toolbar, -1, sub{my ($toolbar, $event) = @_; OnToolClick($toolbar, $event, $frames{main})} ); EVT_TOOL_ENTER ($frames{main}, $toolbar, sub{my ($toolbar, $event) = @_; OnToolEnter($toolbar, $event, 'toolbar')} ); EVT_BUTTON ($frames{main}->{fitbutton}, -1, sub{fit(@_, \%frames)}); $frames{main} -> SetSizerAndFit($hbox); ## sum of menu bar, toolbar, and statusbar + the spaceing around the bix containing the toolbar #my $h = ($toolbar->GetSizeWH)[1] + ($frames{main}->{statusbar}->GetSizeWH)[1] + ($bar->GetSizeWH)[1] + 10; #$frames{main} -> SetSize(Wx::Size->new(Wx::SystemSettings::GetMetric(wxSYS_SCREEN_X), $h)); $frames{main} -> SetSize(Wx::Size->new(Wx::SystemSettings::GetMetric(wxSYS_SCREEN_X), -1)); foreach my $part (qw(GDS Plot Log History Journal Status Config)) { my $pp = "Demeter::UI::Artemis::".$part; $app->{$part} = $pp->new($frames{main}); $frames{$part} = $app->{$part}; $frames{$part} -> SetIcon($icon); }; $app->{Buffer} = Demeter::UI::Common::Buffer->new($frames{main}); $frames{Buffer} = $app->{Buffer}; $frames{Buffer} -> SetIcon($icon); $app->{Buffer}->SetTitle("Artemis [".Demeter->backend_name." \& Plot Buffer]"); $frames{main} -> Show( 1 ); $toolbar->ToggleTool($frames{main}->{plot_toggle}->GetId,1); $frames{Plot} -> Show( 1 ); EVT_TOGGLEBUTTON($frames{main}->{log_toggle}, -1, sub{ $frames{Log}->Show($frames{main}->{log_toggle}->GetValue); $frames{Log}->Iconize(0) if $frames{main}->{log_toggle}->GetValue; }); ## -------- disk space to hold this project my $this = '_dem_' . Demeter->randomstring(8); my $project_folder = File::Spec->catfile(Demeter->stash_folder, $this); $frames{main}->{project_folder} = $project_folder; mkpath($project_folder,0); my $readme = File::Spec->catfile(Demeter->share_folder, "Readme.fit_serialization"); my $target = File::Spec->catfile($project_folder, "Readme"); copy($readme, $target); chmod(0666, $target) if Demeter->is_windows; my $orderfile = File::Spec->catfile($frames{main}->{project_folder}, "order"); $frames{main}->{order_file} = $orderfile; if (not -e $orderfile) { my $string .= YAML::Tiny::Dump(%fit_order); open(my $ORDER, '>'.$orderfile); print $ORDER $string; close $ORDER; }; $frames{main}->{plot_folder} = File::Spec->catfile($frames{main}->{project_folder}, 'plot'); mkpath($frames{main}->{plot_folder}, 0); $frames{main}->{autosave_file} = File::Spec->catfile(Demeter->stash_folder, $this.'.autosave'); #Demeter->Touch(File::Spec->catfile($frames{main}->{project_folder}, $this)); set_mru(); ## now that everything is established, set up disposal callbacks to ## display Ifeffit/Larch commands in the buffer window Demeter->set_mode(callback => \&ifeffit_buffer, plotcallback => (Demeter->mo->template_plot eq 'pgplot') ? \&ifeffit_buffer : \&plot_buffer, feedback => \&feedback, ); Demeter->dispense('fit', 'prep_fit'); $frames{main}->status("Welcome to Artemis $MDASH " . Demeter->identify . " $MDASH " . Demeter->backends); print DateTime->now, " Athena is go! ...\n" if ($ENV{DEMETER_TIMING}); 1; } sub process_argv { my ($app, @args) = @_; if (Demeter->co->default("artemis", "autosave") and autosave_exists()) { import_autosave(); } elsif ($args[0]) { # and -e $args[0]) { my $file = File::Spec->rel2abs( $args[0] ); read_project(\%frames, $file) if Demeter->is_zipproj($file, 0, 'fpj'); }; }; sub mouseover { my ($widget, $text) = @_; my $sb = $frames{main}->{statusbar}; EVT_ENTER_WINDOW($widget, sub{$sb->PushStatusText($text); $_[1]->Skip}); EVT_LEAVE_WINDOW($widget, sub{$sb->PopStatusText if ($sb->GetStatusText eq $text); $_[1]->Skip}); }; sub on_close { my ($self, $event) = @_; if ($frames{main} -> {modified}) { ## offer to save project.... my $yesno = Demeter::UI::Wx::VerbDialog->new($frames{main}, -1, "Save this project before exiting?", "Save project?", "Save", 1); my $result = $yesno->ShowModal; if ($result == wxID_CANCEL) { $frames{main}->status("Not exiting Artemis."); return 0; }; save_project(\%frames) if $result == wxID_YES; }; $frames{main}->{cvcount} = 0; rmtree($self->{project_folder}); #, {verbose=>1}); unlink $frames{main}->{autosave_file}; Demeter->mo->destroy_all; foreach my $f (keys(%frames)) { #print '>', $f, '<', $/; #next if ($f !~ m{Demeter}); next if ($f eq 'main'); $frames{$f}->Destroy; }; $frames{main}->Destroy; $event->Skip(1); }; #sub OnExit { # my ($self, $event) = @_; # Demeter->mo->destroy_all; # $event->Skip(1); #}; sub on_about { my ($self) = @_; my $info = Wx::AboutDialogInfo->new; $info->SetName( 'Artemis' ); #$info->SetVersion( Demeter->version ); $info->SetDescription( "EXAFS analysis using Feff and ".Demeter->backend_name ); $info->SetCopyright( Demeter->identify ); $info->SetWebSite( 'http://bruceravel.github.io/demeter', 'The Demeter web site' ); $info->SetDevelopers( ["Bruce Ravel (http://bruceravel.github.io/home)\n" . Demeter->backend_name." ".Demeter->backend_id."\n" . "Artemis is powered using Wx $Wx::VERSION with $Wx::wxVERSION_STRING\n" . "and Moose $Moose::VERSION"] ); $info->SetLicense( Demeter->slurp(File::Spec->catfile($artemis_base, 'Artemis', 'share', "GPL.dem")) ); my $artwork = <<'EOH' Design and layout of Artemis is the work of Bruce Ravel Some icons taken from the Fairytale icon set at Wikimedia Commons (http://commons.wikimedia.org/) and others from the Gartoon Redux icon set from http://www.gnome-look.org All other icons icons are from the Kids icon set for KDE by Everaldo Coelho, http://www.everaldo.com EOH ; $info -> AddArtist($artwork); Wx::AboutBox( $info ); }; sub heap_check { my ($app, $show) = @_; return if Demeter->is_larch; if (Demeter->mo->heap_used > 0.99) { $app->{main}->status("You have used all of Ifeffit's memory! It is likely that your data is corrupted!", "error"); } elsif (Demeter->mo->heap_used > 0.95) { $app->{main}->status("You have used more than 95% of Ifeffit's memory. Save your work!", "error"); } elsif (Demeter->mo->heap_used > 0.9) { $app->{main}->status("You have used more than 90% of Ifeffit's memory. Save your work!", "error"); } elsif ($show) { Demeter->ifeffit_heap; $app->{main}->status(sprintf("You are currently using %.1f%% of Ifeffit's %.1f Mb of memory", 100 * Demeter->mo->heap_used, Demeter->mo->heap_free/(1-Demeter->mo->heap_used)/2**20)); }; }; sub uptodate { my ($rframes) = @_; my (@data, @paths, @gds); my $abort = 0; ## do I need to take care at this point about GDS's with the same name? # my $grid = $rframes->{GDS}->{grid}; # foreach my $row (0 .. $grid->GetNumberRows) { # $grid -> SetCellValue($row, 3, q{}); # my $name = $grid -> GetCellValue($row, 1); # next if ($name =~ m{\A\s*\z}); # my $type = $grid -> GetCellValue($row, 0); # my $mathexp = $grid -> GetCellValue($row, 2); # my $thisgds = $grid->{$name} || Demeter::GDS->new(); # take care to reuse GDS objects whenever possible # $thisgds -> set(name=>$name, gds=>$type, mathexp=>$mathexp); # $grid->{$name} = $thisgds; # push @gds, $thisgds; # $thisgds->dispose($thisgds->write_gds); # }; foreach my $k (sort keys(%$rframes)) { # sorting will order them in their order in the Data list next unless ($k =~ m{\Adata}); my $this = $rframes->{$k}->{data}; ++$abort if ($rframes->{$k}->fetch_parameters == 0); push @data, $this; my $npath = $rframes->{$k}->{pathlist}->GetPageCount - 1; foreach my $p (0 .. $npath) { my $path = $rframes->{$k}->{pathlist}->GetPage($p); next if (blessed($path) !~ m{Path}); $path->fetch_parameters; push @paths, $path->{path}; }; }; $rframes->{Plot}->fetch_parameters('plot'); #modified(1); return ($abort, \@data, \@paths); }; sub fit { my ($button, $event, $rframes) = @_; my $busy = Wx::BusyCursor->new(); ## reset all relevant widgets to their initial states (i.e. assume ## that the last fit returned trouble and that the widgets ## containing the responsible data were colored in some way to ## indicate that) $rframes->{Plot}->{fileout}->SetValue(0); local $|=1; my $rgds = $rframes->{GDS}->reset_all(1, 0); my ($abort, $rdata, $rpaths) = uptodate($rframes); foreach my $p (@$rpaths) { next if not $p->sp; $p->_update("fft") }; if (($#{$rdata} == -1) or ($#{$rpaths} == -1) or ($#{$rgds} == -1)) { my $message = q{}; $message .= "You have not defined any data sets.\n" if ($#{$rdata} == -1); $message .= "You have not defined any paths.\n" if ($#{$rpaths} == -1); $message .= "You have not defined any fitting parameters.\n" if ($#{$rgds} == -1); Wx::MessageDialog->new($rframes->{main}, $message, "Fit cannot continue", wxOK|wxICON_ERROR) -> ShowModal; $rframes->{main}->status("Your fit cannot continue."); undef $busy; return; }; my @data = @$rdata; my @paths = @$rpaths; my @gds = @$rgds; if ($abort) { $rframes->{main}->status("There is a problem in your fit.", "error"); return; }; my $start = DateTime->now( time_zone => 'floating' ); $rframes->{main}->status("Fitting (please be patient, it may take a while...)", "wait"); ## get name, fom, and description + other properties $rframes->{main} -> {currentfit} = Demeter::Fit->new(interface=>"Artemis (Wx $Wx::VERSION)") if (not $rframes->{main} -> {currentfit}); my $fit = $rframes->{main} -> {currentfit}; $fit -> set(data => \@data, paths => \@paths, gds => \@gds); my $name = $rframes->{main}->{name}->GetValue || 'Fit '.$fit->mo->currentfit; my $startingname = $name; $fit->name($name); $fit->description($rframes->{main}->{description}->GetValue); $fit->fom($fit->mo->currentfit); $fit->keep($frames{main}->{savehist}->GetValue); #$fit->ignore_errors(1); $fit->ignore_nidp($frames{main}->{fitmenu}->IsChecked($IGNORE_NIDP)); $fit->ignore_rbkg($frames{main}->{fitmenu}->IsChecked($IGNORE_RBKG)); $fit->ignore_rmax($frames{main}->{fitmenu}->IsChecked($IGNORE_RMAX)); $fit->ignore_datacollision($frames{main}->{fitmenu}->IsChecked($IGNORE_DATACOLL)); $rframes->{main} -> {currentfit} = $fit; ## get fitting space my $fit_space = 'r'; $fit_space = 'k' if $frames{main}->{fitspace}->[0]->GetValue; $fit_space = 'r' if $frames{main}->{fitspace}->[1]->GetValue; $fit_space = 'q' if $frames{main}->{fitspace}->[2]->GetValue; foreach my $d (@data) { $d->fit_space($fit_space); }; $fit->set_mode(backend=>1, screen=>0); ##autosave($name); my $result = $fit->fit; my $finishtext = q{}; my $code = "normal"; if ($result eq $fit) { $fit -> serialize(tree => File::Spec->catfile($frames{main}->{project_folder}, 'fits'), folder => $fit->group, nozip => 1, copyfeff => 0, ); update_order_file(); $rframes->{Log}->{name} = $fit->name; $rframes->{Log}->Show(1) if ( ($fit->co->default("artemis", "show_after_fit") eq 'log') or (($fit->co->default("artemis", "show_after_fit") eq 'history') and not $frames{main}->{savehist}->GetValue) ); $rframes->{Log}->put_log($fit); $rframes->{Log}->SetTitle("Artemis [Log] " . $rframes->{main}->{name}->GetValue); $rframes->{Log}->Refresh; $rframes->{main}->{log_toggle}->SetValue(1) if ( ($fit->co->default("artemis", "show_after_fit") eq 'log') or (($fit->co->default("artemis", "show_after_fit") eq 'history') and not $frames{main}->{savehist}->GetValue) ); Demeter->Touch(File::Spec->catfile($frames{main}->{project_folder}, 'fits', $fit->group, 'keep')) if $fit->keep; ## fill in plotting list if (not $rframes->{Plot}->{freeze}->GetValue) { $rframes->{Plot}->{plotlist}->ClearAll; foreach my $k (sort (keys (%$rframes))) { next if ($k !~ m{data}); if ($rframes->{$k}->{include}->GetValue) { $rframes->{$k}->transfer if $rframes->{$k}->{plot_after}->GetValue; foreach my $p (0 .. $rframes->{$k}->{pathlist}->GetPageCount -1) { my $pathpage = $rframes->{$k}->{pathlist}->{LIST}->GetIndexedData($p); $pathpage->transfer if $pathpage->{plotafter}->GetValue; }; }; }; }; set_happiness_color($fit->color); $_->update_fft(1) foreach @data; $fit->po->start_plot; $rframes->{Plot}->{limits}->{fit}->SetValue(1); $fit->po->plot_fit(1); my $how = $fit->co->default("artemis", "plot_after_fit"); if ($how =~ m{\A(?:rmr|rk|r123|k123|kq)\z}) { foreach my $d (@data) { if ($d->fit_include) { $Demeter::UI::Artemis::frames{Plot}->fetch_parameters('data'); $d->plot($how); last; }; }; } elsif ($how =~ m{\A[krq]\z}) { $rframes->{Plot}->plot(q{}, $how); }; $rframes->{GDS}->fill_results(@gds); $finishtext = Demeter->howlong($start, 'Your fit'); if ($frames{main}->{savehist}->GetValue) { $rframes->{History}->{list}->AddData($fit->name, $fit); $rframes->{History}->add_plottool($fit); if ($fit->co->default("artemis", "show_after_fit") eq 'history') { $rframes->{History}->Show(1); $rframes->{History}->{list}->SetSelection($rframes->{History}->{list}->GetCount-1); $rframes->{History}->put_log($fit); $rframes->{History}->set_params($fit); }; }; } else { $rframes->{Log}->{text}->SetValue($fit->troubletext); $rframes->{Log}->Show(1); $rframes->{main}->{log_toggle}->SetValue(1); set_happiness_color($fit->co->default("happiness", "bad_color")); $finishtext = "The error report from the fit that just failed is written in the log window."; $code = "error"; }; my $this_name = $fit->name; $rframes->{main}->{name}->SetValue("Fit ". $fit->mo->currentfit) if ($this_name =~ m{\A\s*Fit\s+\d+\z}); $rframes->{main}->{description}->SetValue($fit->description); autosave($name); $rframes->{main}->status($finishtext, $code); my @saved = $rframes->{main} -> {currentfit}->get(qw(happiness fom name description)); my $newfit = Demeter::Fit->new(interface=>"Artemis (Wx $Wx::VERSION)"); $newfit->set(happiness=>$saved[0], fom=>$saved[1], description=>$saved[3]); if ($saved[2] =~ m{\AFit\s+\d+\n}) { $newfit->name("Fit " . $saved[1]); } else { $newfit->name($saved[2]); }; $rframes->{main} -> {currentfit} = $newfit; ++$fit_order{order}{current}; $fit->grabbed(1); $fit->thawed(1); modified(1); $::app->heap_check; undef $start; undef $busy; }; ## YAML::Tiny will write this hash ## %hash = (1=>'a', 2=>'b', 3=>'c') ## on linux as ## --- ## 1: a ## 2: b ## 3: c ## and on Windows as ## --- ## '1': a ## '2': b ## '3': c ## ## It will fail to read the latter on linux, even though it is valid YAML ## ## Solution #1: use YAML rather than YAML::Tiny ## Solution #2: strip the single quotes ## I opted for 2 since the YAML string is never very large ## The substitution below implements this sub update_order_file { my ($just_write) = @_; $just_write || 0; my $thisfit = $fit_order{order}{current} || 1; if (not $just_write) { $fit_order{order}{$thisfit} = $frames{main}->{currentfit}->group; $fit_order{order}{current} = $thisfit; }; my $string .= YAML::Tiny::Dump(%fit_order); $string =~ s{\'(\d+)\':}{$1:}g; open(my $ORDER, '>'.$frames{main}->{order_file}); print $ORDER $string; close $ORDER; return $thisfit; }; sub ifeffit_buffer { my ($text) = @_; foreach my $line (split(/\n/, $text)) { my ($was, $is) = $frames{Buffer}->insert('ifeffit', $line); my $color = ($line =~ m{\A\#}) ? 'comment' : 'normal'; $color = 'endblock' if ($line =~ m{\A\#end}); $frames{Buffer}->color('ifeffit', $was, $is, $color); $frames{Buffer}->insert('ifeffit', $/) }; }; sub plot_buffer { my ($text) = @_; foreach my $line (split(/\n/, $text)) { my ($was, $is) = $frames{Buffer}->insert('plot', $line); my $color = ($line =~ m{\A\#}) ? 'comment' : (Demeter->mo->template_plot eq 'singlefile') ? 'singlefile' :'normal'; $frames{Buffer}->color('plot', $was, $is, $color); $frames{Buffer}->insert('plot', $/) }; }; sub feedback { my ($text) = @_; my ($was, $is) = $frames{Buffer}->insert('ifeffit', $text); my $color = ($text =~ m{\A\s*\*}) ? 'warning' : 'feedback'; $color = 'warning' if $text =~ m{(?color('ifeffit', $was, $is, $color); }; sub set_happiness_color { my $color = $_[0] || Demeter->co->default("happiness", "average_color"); $color = $wxBGC if (not Demeter->co->default("artemis", "happiness")); $frames{main}->{fitbutton} -> SetBackgroundColour(Wx::Colour->new($color)); $frames{Plot}->{k_button} -> SetBackgroundColour(Wx::Colour->new($color)); $frames{Plot}->{r_button} -> SetBackgroundColour(Wx::Colour->new($color)); $frames{Plot}->{'q_button'} -> SetBackgroundColour(Wx::Colour->new($color)); foreach my $k (keys(%frames)) { next unless ($k =~ m{\Adata}); $frames{$k}->{'plot_k123'} -> SetBackgroundColour(Wx::Colour->new($color)); $frames{$k}->{plot_r123} -> SetBackgroundColour(Wx::Colour->new($color)); $frames{$k}->{plot_rmr} -> SetBackgroundColour(Wx::Colour->new($color)); $frames{$k}->{plot_rk} -> SetBackgroundColour(Wx::Colour->new($color)); $frames{$k}->{plot_kq} -> SetBackgroundColour(Wx::Colour->new($color)); }; }; sub button_label { my ($string) = @_; my $this = sprintf("%-40s", $string); return $string; }; sub icon { my ($which) = @_; my $icon = File::Spec->catfile($Demeter::UI::Artemis::artemis_base, 'Artemis', 'icons', "$which.png"); return wxNullBitmap if (not -e $icon); return Wx::Bitmap->new($icon, wxBITMAP_TYPE_ANY) }; sub _doublewide { my ($widget) = @_; my ($w, $h) = $widget->GetSizeWH; $widget -> SetSizeWH(2*$w, $h); }; sub set_mru { my ($self) = @_; foreach my $which (qw(artemis athena structure fit_serialization old_artemis)) { my $type = ($which eq 'fit_serialization') ? 'fit' : ($which eq 'old_artemis') ? 'old' : $which; foreach my $i (reverse (0 .. $frames{main}->{'mru'.$type}->GetMenuItemCount-1)) { $frames{main}->{'mru'.$type}->Delete($frames{main}->{'mru'.$type}->FindItemByPosition($i)); }; my @list = ($which eq 'structure') ? Demeter->get_mru_list('atoms', 'feff') : Demeter->get_mru_list($which); foreach my $f (@list) { $frames{main}->{'mru'.$type}-> Append(-1, $f->[0]); }; }; }; sub OnMenuClick { my ($self, $event) = @_; my $id = $event->GetId; my $mru = $frames{main}->{mrumenu}->GetLabel($id); #print "$id $mru\n"; $mru =~ s{__}{_}g; # wtf!?!?!? SWITCH: { ($id == wxID_ABOUT) and do { &on_about; return; }; ($id == $DOCUMENT) and do { $::app->document('index'); return; }; ($id == $DOCUMENT_PLOT) and do { $::app->document('plot'); return; }; ($id == $DOCUMENT_FEFF) and do { $::app->document('feff'); return; }; ($id == $DOCUMENT_FIT) and do { $::app->document('fit'); return; }; ($id == $BUG) and do { Wx::LaunchDefaultBrowser(q{http://bruceravel.github.io/demeter/documents/SinglePage/bugs.html}); return; }; ($id == $QUESTION) and do { Wx::LaunchDefaultBrowser(q{http://bruceravel.github.io/demeter/documents/SinglePage/help.html}); return; }; ($id == $CHANGELOG) and do { Wx::LaunchDefaultBrowser(q{http://bruceravel.github.io/demeter/Changes.html}); return; }; ($id == wxID_CLOSE) and do { close_project(\%frames); return; }; ($id == wxID_EXIT) and do { $self->Close; Demeter->stop_larch_server; return; }; ($id == wxID_OPEN) and do { read_project(\%frames); last SWITCH; }; ($id == wxID_SAVE) and do { #my $fpj = File::Spec->catfile(); save_project(\%frames, $frames{main}->{projectpath}); last SWITCH; }; ($id == wxID_SAVEAS) and do { save_project(\%frames); last SWITCH; }; ($id == $SAVETHIS) and do { if ($frames{History}->{list}->GetCount) { $frames{History}->export($frames{History}->{list}->GetCount-1); } else { $frames{main}->status("You haven't made a fit yet!") }; last SWITCH; }; ($id == $SHOW_BUFFER) and do { $frames{Buffer}->Show(1); last SWITCH; }; ($id == wxID_PREFERENCES) and do { $frames{Config}->Show(1); last SWITCH; }; ($mru) and do { ## figure out which submenu it came from... read_project(\%frames, $mru) if $frames{main}->{mruartemis} ->GetLabel($id); Import('dpj', $mru) if $frames{main}->{mrufit} ->GetLabel($id); Import('prj', $mru) if $frames{main}->{mruathena} ->GetLabel($id); Import('feff', $mru) if $frames{main}->{mrustructure}->GetLabel($id); Import('old', $mru) if $frames{main}->{mruold} ->GetLabel($id); last SWITCH; }; (($id == $SHOW_GROUPS) or ($id == $SHOW_ARRAYS) or ($id == $SHOW_SCALARS) or ($id == $SHOW_STRINGS) or ($id == $SHOW_PATHS) or ($id == $SHOW_FEFFPATHS)) and do { show_ifeffit($id); last SWITCH; }; ## -------- import submenu ($id == $IMPORT_OLD) and do { Import('old', q{}); last SWITCH; }; ($id == $IMPORT_FEFF) and do { Import('external', q{}); last SWITCH; }; ($id == $IMPORT_FEFFIT) and do { Import('feffit', q{}); last SWITCH; }; ($id == $IMPORT_CHI) and do { Import('chi', q{}); last SWITCH; }; ($id == $IMPORT_DPJ) and do { Import('dpj', q{}); last SWITCH; }; ## -------- export submenu ($id == $EXPORT_IFEFFIT) and do { export('ifeffit'); last SWITCH; }; ($id == $EXPORT_DEMETER) and do { export('demeter'); last SWITCH; }; ## -------- debug submenu ($id == $FIT_YAML) and do { my $yaml = $frames{main}->{currentfit}->serialization; my $dialog = Demeter::UI::Common::ShowText->new($frames{main}, $yaml, 'YAML of current Fit object') -> Show; last SWITCH; }; ($id == $PLOT_YAML) and do { $frames{Plot}->fetch_parameters('plot'); my $yaml = Demeter->po->serialization; my $dialog = Demeter::UI::Common::ShowText->new($frames{main}, $yaml, 'YAML of Plot object') -> Show; last SWITCH; }; ($id == $PERL_MODULES) and do { my $text = Demeter->module_environment . Demeter -> wx_environment; my $dialog = Demeter::UI::Common::ShowText->new($frames{main}, $text, 'Perl module versions') -> Show; last SWITCH; }; ($id == $MODE_STATUS) and do { my $dialog = Demeter::UI::Common::ShowText->new($frames{main}, Demeter->mo->report('all'), 'Overview of this instance of Demeter') -> Show; last SWITCH; }; #($id == $CRASH) and do { # my $x = 1/0; # last SWITCH; #}; ($id == $IFEFFIT_MEMORY) and do { $::app->heap_check(1); last SWITCH; }; ($id == $PLOT_PNG) and do { $frames{Plot}->image('png'); last SWITCH; }; ($id == $PLOT_PDF) and do { $frames{Plot}->image('pdf'); last SWITCH; }; ($id == $PLOT_XKCD) and do { if ($frames{main}->{plotmenu}->IsChecked($PLOT_XKCD)) { Demeter->xkcd(1); } else { Demeter->xkcd(0); }; }; ($id == $TERM_1) and do { Demeter->po->terminal_number(1); last SWITCH; }; ($id == $TERM_2) and do { Demeter->po->terminal_number(2); last SWITCH; }; ($id == $TERM_3) and do { Demeter->po->terminal_number(3); last SWITCH; }; ($id == $TERM_4) and do { Demeter->po->terminal_number(4); last SWITCH; }; ($id == $PLOT_ALL_DATA) and do { foreach my $k (sort (keys (%frames))) { next if ($k !~ m{data}); $frames{$k}->{plot_after}->SetValue(1); }; last SWITCH; }; ($id == $PLOT_NO_DATA) and do { foreach my $k (sort (keys (%frames))) { next if ($k !~ m{data}); $frames{$k}->{plot_after}->SetValue(0); }; last SWITCH; }; ## -------- help menu ($id == $STATUS) and do { $frames{Status} -> Show(1); last SWITCH; }; ## -------- fit menu ($id == $IGNORE_NIDP) and do { if ($frames{main}->{fitmenu}->IsChecked($IGNORE_NIDP)) { my $yesno = Demeter::UI::Wx::VerbDialog->new($frames{main}, -1, "Are you SURE you want to skip the Nidp test?", "Skip Nidp test?", 'Skip test'); if ($yesno->ShowModal == wxID_NO) { $frames{main}->{fitmenu}->Check($IGNORE_NIDP, 0); return; }; }; }; ($id == $IGNORE_RBKG) and do { if ($frames{main}->{fitmenu}->IsChecked($IGNORE_RBKG)) { my $yesno = Demeter::UI::Wx::VerbDialog->new($frames{main}, -1, "Are you SURE you want to skip the Rmin>Rbkg test?", "Skip Rbkg test?", 'Skip test'); if ($yesno->ShowModal == wxID_NO) { $frames{main}->{fitmenu}->Check($IGNORE_RBKG, 0); return; }; }; last SWITCH; }; }; }; sub show_ifeffit { my ($id) = @_; my $text = ($id =~ m{\A[a-z]+\z}) ? "\@group $id" : ($id == $SHOW_GROUPS) ? "\@groups" : ($id == $SHOW_ARRAYS) ? "\@arrays" : ($id == $SHOW_SCALARS) ? "\@scalars" : ($id == $SHOW_STRINGS) ? "\@strings" : ($id == $SHOW_PATHS) ? "\@paths" : ($id == $SHOW_FEFFPATHS) ? "\@feffpaths" : q{}; return if not $text; Demeter->dispense('process', 'show', {items=>$text}); $frames{Buffer}->Show(1); }; sub OnToolEnter { 1; }; sub OnToolClick { my ($toolbar, $event, $self) = @_; my $which = (qw(GDS Plot History Journal))[$toolbar->GetToolPos($event->GetId)]; $frames{$which}->Show($toolbar->GetToolState($event->GetId)); $frames{$which}->Iconize(0) if $toolbar->GetToolState($event->GetId); }; sub OnDataRightClick { my ($self, $event) = @_; return 0 if (not Demeter::UI::Artemis::Import::_check_number_of_data_sets()); my $dialog = Demeter::UI::Wx::MRU->new($frames{main}, 'athena', "Select a recent Athena project file", "Recent Athena project files"); $frames{main}->status("There are no recent Athena project files."), return if ($dialog == -1); if( $dialog->ShowModal == wxID_CANCEL ) { $frames{main}->status("Import canceled."); } else { Import('prj', $dialog->GetMruSelection); }; }; sub OnDataButtonRightClick { my ($self, $event) = @_; my $dnum = $self->{dnum}; my $data = $frames{$dnum}->{data}; my $menu = Wx::Menu->new(q{}); $menu->Append(0, "Rename ".$data->name); $menu->Append(1, "Discard ".$data->name); $self->PopupMenu($menu, $event->GetPosition); }; sub OnDataMenu { my ($self, $event) = @_; my $dnum = $self->{dnum}; my $data = $frames{$dnum}->{data}; if ($event->GetId == 0) { $frames{$dnum}->Rename; modified(1); } elsif ($event->GetId == 1) { $frames{$dnum}->discard_data; modified(1); }; }; sub make_data_frame { my ($self, $data) = @_; my $databox = $self->{databox}; #print join('|', split(//, emph($data->name))), $/; my $new = Wx::ToggleButton->new($self->{datalist}, -1, "Show ".emph($data->name)); #my $new = Wx::ToggleButton->new($self->{datalist}, -1, "Hide ".$data->name); $databox -> Add($new, 0, wxGROW|wxALL, 0); mouseover($self, "Display/hide this data group. Right click for a menu of options."); do_the_size_dance($self); my $idata = $new->GetId; my $dnum = sprintf("data%s", $idata); $new->{dnum} = $dnum; $self->{$dnum} = $new; EVT_TOGGLEBUTTON($new, -1, sub{ $frames{$dnum}->Show($_[0]->GetValue); $frames{$dnum}->Iconize(0) if $_[0]->GetValue; my $label = $_[0]->GetLabel; if ($_[0]->GetValue) { $label =~ s{Show}{Hide}; } else { $label =~ s{Hide}{Show}; }; $_[0]->SetLabel($label); }); EVT_MENU($new, -1, \&OnDataMenu); EVT_RIGHT_UP($new, \&OnDataButtonRightClick); ++$frames{main}->{cvcount}; $data->cv($frames{main}->{cvcount}); $frames{$dnum} = Demeter::UI::Artemis::Data->new($self, $nset++); $frames{$dnum} -> SetTitle("Artemis [Data] ".$data->name); $frames{$dnum} -> SetIcon($icon); $frames{$dnum} -> populate($data); $frames{$dnum} -> transfer; $frames{$dnum} -> {dnum} = $dnum; set_happiness_color(); $frames{$dnum} -> Show(0); $new->SetValue(0); modified(1); $::app->{$dnum} = $frames{$dnum}; $::app->heap_check; return ($dnum, $idata); }; sub OnFeffClick { my ($feffbar, $event, $self) = @_; my $which = $feffbar->GetToolPos($event->GetId); if ($which == 0) { Import('feff', q{}); } else { my $this = sprintf("feff%s", $event->GetId); return if not exists($frames{$this}); $frames{$this}->Show($feffbar->GetToolState($event->GetId)); }; }; sub OnFeffRightClick { my ($self, $event) = @_; my $dialog = Demeter::UI::Wx::MRU->new($frames{main}, ['atoms', 'feff'], "Start a new Atoms input or select a recent Feff input file, Atoms input file, or CIF file", "Recent Feff or crystal data file"); $frames{main}->status("There are no recent crystal files."), return if ($dialog == -1); if( $dialog->ShowModal == wxID_CANCEL ) { $frames{main}->status("Import canceled."); } else { my $which = $dialog->GetMruSelection; if ($which eq 'Open a blank Atoms window') { my ($fnum, $ifeff) = make_feff_frame($frames{main}, $BLANK); $frames{$fnum} -> Show(1); $frames{main}->{$fnum}->SetValue(1); } elsif (not -e $which) { $frames{main}->status("\"$which\" does not exist."); } elsif (not -r $which) { $frames{main}->status("\"$which\" cannot be read."); } else { Import('feff', $which); }; }; }; sub OnFeffButtonRightClick { my ($self, $event) = @_; #my $fnum = $self->{fnum}; my $menu = Wx::Menu->new(q{}); $menu->Append(0, "Rename this Feff object"); $menu->Append(1, "Discard this Feff object"); $self->PopupMenu($menu, $event->GetPosition); }; sub OnFeffMenu { my ($self, $event) = @_; my $fnum = $self->{fnum}; if ($event->GetId == 0) { $frames{$fnum}->on_rename; modified(1); } elsif ($event->GetId == 1) { $frames{$fnum}->on_discard; modified(1); }; }; ## name for empty feff frame... sub make_feff_frame { my ($self, $file, $name, $feffobject) = @_; my $feffbox = $self->{feffbox}; $name ||= basename($file) if $file; # ok for importing an atoms or CIF file $name ||= 'new'; $name = 'new' if ($name eq $BLANK); my $new = Wx::ToggleButton->new($self->{fefflist}, -1, "Hide ".emph($name)); my $ifeff = $new->GetId; $feffbox -> Add($new, 0, wxGROW|wxRIGHT, 5); mouseover($new, "Display/hide this Feff calculation. Right click for a menu of options."); do_the_size_dance($self); my $fnum = sprintf("feff%s", $ifeff); $new->{fnum} = $fnum; $self->{$fnum} = $new; EVT_TOGGLEBUTTON($new, -1, sub{ $frames{$fnum}->Show($_[0]->GetValue); $frames{$fnum}->Iconize(0) if $_[0]->GetValue; my $label = $_[0]->GetLabel; if ($_[0]->GetValue) { $label =~ s{Show}{Hide}; } else { $label =~ s{Hide}{Show}; }; $_[0]->SetLabel($label); }); EVT_MENU($new, -1, \&OnFeffMenu); EVT_RIGHT_UP($new, \&OnFeffButtonRightClick); my $base = File::Spec->catfile($self->{project_folder}, 'feff'); $frames{$fnum} = Demeter::UI::AtomsApp->new($base, $feffobject, $fnum); $frames{$fnum} -> SetTitle('Artemis [Feff] Atoms and Feff'); $frames{$fnum} -> SetIcon($icon); $frames{$fnum} -> {atoms_disabled} = 0; if ($file and (-e $file) and (Demeter->is_atoms($file) or Demeter->is_cif($file))) { my $result = $frames{$fnum}->{Atoms}->Demeter::UI::Atoms::Xtal::open_file($file); if (not $result) { $new -> Destroy; $frames{$fnum} -> Hide; $frames{$fnum} -> Destroy; delete $frames{$fnum}; return; }; } else { $frames{$fnum}->{Atoms}->{used} = 1; $frames{$fnum}->{Atoms}->{name}->SetValue('new'); if ($file ne $BLANK) { # $frames{$fnum}->{notebook}->DeletePage(0); # $fefftab = 0; $frames{$fnum}->{atoms_disabled} = 1; $frames{$fnum}->{notebook}->SetPageImage(0, 5); # see Demeter::UI::Atoms.pm around line 60 $frames{$fnum}->{notebook}->SetPageText(0, ''); ## The following two event handlers are used to overcome the ## fact that $event->GetPosition is unreliable on Windows -- as ## explained in the documentation: ## http://docs.wxwidgets.org/2.8.4/wx_wxnotebookevent.html#wxnotebookeventgetselection ## This solution was suggested by Mark Dootson on the wxperl mailing list ## http://www.nntp.perl.org/group/perl.wxperl.users/2011/12/msg8296.html EVT_LEFT_DOWN($frames{$fnum}->{notebook}, sub { $_[0]->{last_pos} = $_[1]->GetPosition(); $_[1]->Skip(1); }); EVT_NOTEBOOK_PAGE_CHANGING($frames{$fnum}, $frames{$fnum}->{notebook}, sub{ my($self, $event) = @_; my $notebook = $event->GetEventObject; my ($nbtab, $flags ) = $notebook->HitTest($notebook->{last_pos}); $event->Veto() if ($nbtab == 0); # veto selection of Atoms tab return; }); }; }; if ($file and (-e $file) and Demeter->is_feff($file)) { my $text = Demeter->slurp($file); $frames{$fnum}->{Atoms}->{used} = 1; $frames{$fnum}->make_page('Feff') if not $frames{$fnum}->{Feff}; $frames{$fnum}->{Feff}->{feff}->SetValue($text); $frames{$fnum}->{Feff}->{name}->SetValue(basename($file, '.inp')); $frames{$fnum}->{notebook}->ChangeSelection(1); Demeter -> push_mru("feff", $file) }; #$newtool -> SetLabel( $frames{$fnum}->{Atoms}->{name}->GetValue ); $frames{$fnum} -> {fnum} = $fnum; EVT_CLOSE($frames{$fnum}, \&Demeter::UI::AtomsApp::on_close); EVT_ICONIZE($frames{$fnum}, \&Demeter::UI::AtomsApp::on_close); $frames{$fnum} -> Show(0); $::app->{$fnum} = $frames{$fnum}; $new->SetValue(0); modified(1) if ($file); return ($fnum, $ifeff); }; ## the tool bars only seem to update after a resize. I could not ## figure out how to force an update without resizing, so this sub ## jiggles the window and voila! the new tool button appears. sub do_the_size_dance { my ($top) = @_; my @size = $top->GetSizeWH; $top -> SetSize($size[0], $size[1]+1); $top -> SetSize($size[0], $size[1]); }; sub discard_feff { my ($which, $force) = @_; my $feffobject = $frames{$which}->{feffobject}; ##my $atomsobject = $frames{$which}->{Atoms} if (not $force) { my $yesno = Demeter::UI::Wx::VerbDialog->new($frames{main}, -1, "Do you really wish to discard this Feff calculation?", "Discard?", "Discard"); return if ($yesno->ShowModal == wxID_NO); }; ## remove the button from the data tool bar my $fnum = $frames{$which}->{fnum}; (my $id = $fnum) =~ s{feff}{}; ## remove the frame with the feff calculation $frames{$fnum}->Hide; $frames{$fnum}->Destroy; delete $frames{$fnum}; ## remove the button from the feff tool bar $frames{main}->{feffbox}->Hide($frames{main}->{$fnum}); $frames{main}->{feffbox}->Detach($frames{main}->{$fnum}); $frames{main}->{feffbox}->Layout; #$frames{main}->{$fnum}->Destroy; ## destroy the ScatteringPath object ## destroy the feff object if (defined($feffobject) and (ref($feffobject) =~ m{Demeter})) { rmtree($feffobject->workspace); $feffobject->DEMOLISH; }; foreach my $obj (@{Demeter->mo->Atoms}, @{Demeter->mo->Feff}, @{Demeter->mo->External}, @{Demeter->mo->GDS}, @{Demeter->mo->ScatteringPath}, @{Demeter->mo->VPath}, @{Demeter->mo->SSPath}, @{Demeter->mo->FPath}, @{Demeter->mo->FSPath}) { $obj->remove; }; } sub export { my ($how) = @_; ## make a disposable Fit object my ($abort, $rdata, $rpaths) = uptodate(\%frames); my $rgds = $frames{GDS}->reset_all(0,0); my @data = @$rdata; my @paths = @$rpaths; my @gds = @$rgds; if ($abort) { $frames{main}->status("There is a problem in your fit."); return; }; my $fit = Demeter::Fit->new(data => \@data, paths => \@paths, gds => \@gds); my $suffix = ($how eq 'ifeffit') ? 'iff' : 'pl'; my $fd = Wx::FileDialog->new( $::app->{main}, "Export this fitting model", cwd, "artemis.$suffix", "fitting scripts (*.$suffix)|*.$suffix|All files (*)|*", wxFD_SAVE|wxFD_CHANGE_DIR|wxFD_OVERWRITE_PROMPT, wxDefaultPosition); if ($fd->ShowModal == wxID_CANCEL) { $::app->{main}->status("Exporting fitting model canceled."); return; }; my $fname = $fd->GetPath; #return if $::app->{main}->overwrite_prompt($fname); # work-around gtk's wxFD_OVERWRITE_PROMPT bug (5 Jan 2011) unlink $fname; ## save mode settings my @modes = qw(template_process template_fit backend file callback plotcallback feedback); my @values = $fit -> mo -> get(@modes); ## set mode settings appropriate to file output $fit -> mo -> template_process($how); $fit -> mo -> template_fit($how); $fit -> mo -> ifeffit(0); $fit -> mo -> file('>'.$fname); $fit -> mo -> callback(sub{}); $fit -> mo -> plotcallback(sub{}); $fit -> mo -> feedback(sub{}); ## do the fit, thus writing the script file $fit -> fit; ## restore mode settings $fit -> mo -> set(zip(@modes, @values)); undef $fit; }; sub document { my ($app, $doc, $target) = @_; my $file; my $url = Demeter->co->default('artemis', 'doc_url'); my @path = ('Demeter', 'share', 'documentation', 'Artemis'); if (any {$doc eq $_} (qw(plot fit path feff))) { push @path, $doc; $file = 'index'; $url .= $doc . '/index.html'; } elsif ($doc =~ m{\A\w+\.\w+\z}) { my ($dir, $fname) = split(/\./, $doc); push @path, $dir; $file = $fname; $url .= $dir . '/' . $fname . '.html'; } else { $file = $doc; $url .= $doc . '.html'; }; my $fname = File::Spec->catfile(dirname($INC{'Demeter.pm'}), @path, $file.'.html'); if (-e $fname) { $fname = 'file://'.$fname; $fname .= '#'.$target if $target; $::app->{main}->status("Displaying document page: $fname"); Wx::LaunchDefaultBrowser($fname); } else { $url .= '#'.$target if $target; #$::app->{main}->status("Document target not found: $fname"); $::app->{main}->status("Displaying document page: $url"); Wx::LaunchDefaultBrowser($url); }; }; =for Explain Every window in Artemis is a Wx::Frame. This inserts a method into that namespace which serves as a choke point for writing messages to the status bar. The two purposes served are (1) to apply some color to the text in the status bar and (2) to log all such messages. The neat thing about doing it this way is that each window will write to its own status bar yet all messages get captured to a common log. $wxframe->status($text, $type); where the optional $type is one of "normal", "error", or "wait", each of which corresponds to a different text style in both the status bar and the log buffer. =cut package Wx::Frame; use Demeter::UI::Wx::Colours; #use Demeter::UI::Wx::OverwritePrompt; my $normal = $wxBGC; my $wait = Wx::Colour->new("#C5E49A"); my $error = Wx::Colour->new("#FD7E6F"); my $alert = Wx::Colour->new("#FCDD9F"); my $debug = 0; sub status { my ($self, $text, $type) = @_; $type ||= 'normal'; if ($debug) { local $|=1; print $text, " -- ", join(", ", (caller)[0,2]), $/; }; my $bgcolor = ($type =~ m{normal}) ? $normal : ($type =~ m{wait}) ? $wait : ($type =~ m{alert}) ? $alert : ($type =~ m{error}) ? $error : $normal; $self->GetStatusBar->SetBackgroundColour($bgcolor); $self->GetStatusBar->Refresh; $self->GetStatusBar->SetStatusText($text); return if ($type =~ m{nobuffer}); # Demeter->trace; $Demeter::UI::Artemis::frames{Status}->put_text($text, $type) if ($text !~ m{\A\s*\z}); }; =for Explain According to the wxWidgets documentation, "Please note that wxCheckListBox uses client data in its implementation, and therefore this is not available to the application." This appears either not to be true on Linux or, perhaps, that the client data is overwritable with no ill effect. On Windows, however, attempting to set client data crashes the application. On the wxperl-users mailing list Mattia Barbon said: "It's a wxWidgets limitation: it uses the same Win32 client data slot in wxListBox to store client data, in wxCheckListBox to store the boolean state of the item." Sigh.... These methods are an attempt to replicate the effect of client data by maintaining a list of pointers to data that is indexed to the CheckListBox. This list is stored in the underlying hash of the CheckListBox object. The trick is to keep the list in sync with the displayed content of the CheckListBox at all times. Yes, this *is* much to complicated. =cut package Wx::CheckListBox; use Wx qw(:everything); sub AddData { my ($clb, $name, $data) = @_; $clb->Append($name); push @{$clb->{datalist}}, $data; }; sub InsertData { my ($clb, $name, $n, $data) = @_; $clb->Insert($name, $n); my @list = @{$clb->{datalist}}; splice(@list, $n, 0, $data); $clb->{datalist} = \@list; }; sub SetIndexedData { my ($clb, $n, $data) = @_; $clb->{datalist}->[$n]=$data; }; sub GetIndexedData { my ($clb, $n) = @_; return $clb->{datalist}->[$n] if defined($n); return $clb->{intial}; }; sub DeleteData { my ($clb, $n) = @_; ## remove from the Indexed array my @list = @{$clb->{datalist}}; my $gone = splice(@list, $n, 1); #print $gone, " ", $gone->name, $/; $clb->{datalist} = \@list; $clb->Delete($n); # this calls the selection event on the new item }; sub ClearAll { my ($clb) = @_; $clb->{datalist} = []; $clb->Clear; }; ## also need a method for reordering items on the list... 1; =head1 NAME Demeter::UI::Artemis - EXAFS analysis using Feff and Ifeffit/Larch =head1 VERSION This documentation refers to Demeter version 0.9.26. =head1 SYNOPSIS This short program launches Artemis: use Wx; use Demeter::UI::Artemis; Wx::InitAllImageHandlers(); my $window = Demeter::UI::Artemis->new; $window -> MainLoop; =head1 DESCRIPTION Artemis is a program for EXAFS data analysis using Feff. =head1 USE See the Artemis Users' Guide. =head1 CONFIGURATION Many aspects of Artemis and its UI are configurable using the configuration tool built into Artemis. =head1 DEPENDENCIES This is a Wx application. Demeter's dependencies are in the F file. =head1 BUGS AND LIMITATIONS Please report problems to the Ifeffit Mailing List (L) Patches are welcome. =head1 AUTHOR Bruce Ravel, L L =head1 LICENCE AND COPYRIGHT Copyright (c) 2006-2019 Bruce Ravel (L). All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. =cut