diff --git a/InterSpec/MakeDrfSrcDef.h b/InterSpec/MakeDrfSrcDef.h index 713ed0bb..80892704 100644 --- a/InterSpec/MakeDrfSrcDef.h +++ b/InterSpec/MakeDrfSrcDef.h @@ -34,6 +34,7 @@ #include class MaterialDB; +class PopupDivMenu; class ShieldingSelect; namespace Wt @@ -359,6 +360,9 @@ class MakeDrfSrcDef : public Wt::WContainerWidget /** Button that lets you select sources that might be in Source.lib files */ Wt::WPushButton *m_lib_src_btn; + /** Menu that pops up when `m_lib_src_btn` is clicked. */ + PopupDivMenu *m_lib_src_menu; + Wt::Signal<> m_updated; };//MakeDrfSrcDef diff --git a/src/MakeDrfSrcDef.cpp b/src/MakeDrfSrcDef.cpp index 5c78c5e9..836e07cb 100644 --- a/src/MakeDrfSrcDef.cpp +++ b/src/MakeDrfSrcDef.cpp @@ -51,6 +51,7 @@ #include "SandiaDecay/SandiaDecay.h" #include "InterSpec/PeakDef.h" +#include "InterSpec/PopupDiv.h" #include "InterSpec/InterSpec.h" #include "InterSpec/MaterialDB.h" #include "InterSpec/HelpSystem.h" @@ -141,7 +142,11 @@ vector SrcLibLineInfo::sources_in_lib( std::istream &file ) SpecUtils::trim( line ); vector fields; - SpecUtils::split( fields, line, " \t" ); + // We will allow for a space, a tab, or the U+2002 (Unicode En Space) character to separate fields + boost::algorithm::split( fields, line, + boost::is_any_of(" \t") || boost::is_any_of("\xe2\x80\x82"), + boost::token_compress_on ); + if( fields.size() < 3 ) continue; @@ -199,12 +204,13 @@ vector SrcLibLineInfo::sources_in_lib( std::istream &file ) }//if( std::regex_match( remark, dist_mtch, dist_expr ) ) std::smatch act_uncert_mtch; - std::regex act_uncert_expr( string(".+([ActivityUncertainty|ActivityUncert]\\s*\\=\\s*(") - + PhysicalUnits::sm_positiveDecimalRegex - + ")).*?", std::regex::icase ); + const string act_uncert_expr_str = string(".*((Act|Activity)(Uncert|Uncertainty)\\s*[\\=:]\\s*(") + + PhysicalUnits::sm_positiveDecimalRegex + ")).*?"; + + std::regex act_uncert_expr( act_uncert_expr_str, std::regex::icase ); if( std::regex_match( src_info.m_comments, act_uncert_mtch, act_uncert_expr ) ) { - string strval = act_uncert_mtch[2].str(); + string strval = act_uncert_mtch[4].str(); if( !SpecUtils::parse_double( strval.c_str(), strval.size(), src_info.m_activity_uncert ) ) src_info.m_activity_uncert = -1.0; }//if( std::regex_match( remark, dist_mtch, dist_expr ) ) @@ -339,6 +345,7 @@ MakeDrfSrcDef::MakeDrfSrcDef( const SandiaDecay::Nuclide *nuc, m_useShielding( nullptr ), m_shieldingSelect( nullptr ), m_lib_src_btn( nullptr ), + m_lib_src_menu( nullptr ), m_lib_srcs_for_nuc{}, m_lib_srcs_from_file{}, m_lib_srcs_added{}, @@ -362,6 +369,11 @@ MakeDrfSrcDef::MakeDrfSrcDef( const SandiaDecay::Nuclide *nuc, MakeDrfSrcDef::~MakeDrfSrcDef() { +#if( WT_VERSION >= 0x3070000 ) + if( m_lib_src_menu ) + delete m_lib_src_menu; + m_lib_src_menu = nullptr; +#endif } @@ -409,7 +421,7 @@ void MakeDrfSrcDef::updateSourceLibNuclides() if( m_lib_src_btn ) m_lib_src_btn->setHidden( m_lib_srcs_for_nuc.empty() ); - WPopupMenu *menu = m_lib_src_btn ? m_lib_src_btn->menu() : nullptr; + WPopupMenu *menu = m_lib_src_menu; if( menu ) { const vector old_items = menu->items(); @@ -632,9 +644,25 @@ void MakeDrfSrcDef::create() m_lib_src_btn->setIcon( "InterSpec_resources/images/db_small.png" ); m_lib_src_btn->setStyleClass( "LinkBtn DownloadBtn DialogFooterQrBtn" ); m_lib_src_btn->setFloatSide( Wt::Left ); - WPopupMenu *menu = new WPopupMenu(); - menu->setAutoHide( true ); - m_lib_src_btn->setMenu( menu ); + +#if( WT_VERSION < 0x3070000 ) + m_lib_src_menu = new PopupDivMenu( m_lib_src_btn, PopupDivMenu::TransientMenu ); + m_lib_src_menu->setJavaScriptMember("wtNoReparent", "true"); +#else + // If we have the button own the popup menu, the menu will be placed in our current div holding + // all src info, that may have a significant amount of scroll in it, so we will then have to + // scroll this div, to see all the menu items, which is annoying. + // So instead we'll make the menu a global widget, and just pop it up at the clicked location. + // This is a little less than optimal, and make auto-hide of menu not quite as good, but maybe + // better than the alternative. + m_lib_src_menu = new PopupDivMenu( nullptr, PopupDivMenu::TransientMenu ); + m_lib_src_menu->setMaximumSize( WLength::Auto, WLength(15, WLength::FontEm) ); + m_lib_src_btn->clicked().connect( boost::bind( + static_cast(&WPopupMenu::popup), + m_lib_src_menu, boost::placeholders::_1 ) ); +#endif + + m_lib_src_menu->setAutoHide( true, 2500 ); const bool showToolTips = InterSpecUser::preferenceValue( "ShowTooltips", InterSpec::instance() ); const char *tooltip = "Sources defined in Source.lib file in your users data directory.
" diff --git a/target/testing/CMakeLists.txt b/target/testing/CMakeLists.txt index de5262ee..86ded402 100644 --- a/target/testing/CMakeLists.txt +++ b/target/testing/CMakeLists.txt @@ -121,6 +121,14 @@ add_test( NAME TShieldingSourceFitCalc ) +add_executable( test_MakeDrfSrcDef test_MakeDrfSrcDef.cpp ) +target_link_libraries( test_MakeDrfSrcDef PRIVATE InterSpecLib ) +add_test( NAME TMakeDrfSrcDef + COMMAND $ ${BOOST_TEST_CL_ARGS} -- ${DATA_DIR_ARGS} + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" +) + + # ./test_offlineAnalysis.cpp #target_link_libraries(test_offlineAnalysis PUBLIC # debug "${Wt_TEST_DEBUG_LIBRARY}" diff --git a/target/testing/test_MakeDrfSrcDef.cpp b/target/testing/test_MakeDrfSrcDef.cpp new file mode 100644 index 00000000..95bc9394 --- /dev/null +++ b/target/testing/test_MakeDrfSrcDef.cpp @@ -0,0 +1,277 @@ +/* InterSpec: an application to analyze spectral gamma radiation data. + + Copyright 2018 National Technology & Engineering Solutions of Sandia, LLC + (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. + Government retains certain rights in this software. + For questions contact William Johnson via email at wcjohns@sandia.gov, or + alternative emails of interspec@sandia.gov. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include +#include +#include + + +//#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_MODULE testPhysicalUnits_suite +//#include +#include + +#include "SpecUtils/StringAlgo.h" +#include "SpecUtils/Filesystem.h" + +#include "InterSpec/InterSpec.h" +#include "InterSpec/MakeDrfSrcDef.h" +#include "InterSpec/PhysicalUnits.h" +#include "InterSpec/DecayDataBaseServer.h" + + +using namespace std; +using namespace boost::unit_test; + +/* This file currently just tests reading `SrcLibLineInfo` info from files. */ + +namespace SetDataDirs +{ +// The "InterSpec/data" directory that contains all the cross-sections, reactions, etc +std::string sm_static_data_dir = ""; + +// The sandia.decay.xml file in InterSpec/data is usually the minimized (~6MB) version of the file +// without coincidences and stuff - we want to use the full ~30MB version of the file that is in +// the SandiaDecay repository. +std::string sm_sandia_decay_file = ""; + +void set_data_dir() +{ + // We only need to initialize things once + static bool s_have_set = false; + if( s_have_set ) + return; + + s_have_set = true; + + int argc = boost::unit_test::framework::master_test_suite().argc; + char **argv = boost::unit_test::framework::master_test_suite().argv; + + std::string datadir, test_file_dir; + + for( int i = 1; i < argc; ++i ) + { + const std::string arg = argv[i]; + if( SpecUtils::istarts_with( arg, "--datadir=" ) ) + datadir = arg.substr( 10 ); + + if( SpecUtils::istarts_with( arg, "--testfiledir=" ) ) + test_file_dir = arg.substr( 14 ); + }//for( int arg = 1; arg < argc; ++ arg ) + + SpecUtils::ireplace_all( datadir, "%20", " " ); + SpecUtils::ireplace_all( test_file_dir, "%20", " " ); + + // Search around a little for the data directory, if it wasnt specified + if( datadir.empty() ) + { + for( const auto &d : { "data", "../data", "../../data", "../../../data", "/Users/wcjohns/rad_ana/InterSpec/data" } ) + { + if( SpecUtils::is_file( SpecUtils::append_path(d, "sandia.reactiongamma.xml") ) ) + { + datadir = d; + break; + } + }//for( loop over candidate dirs ) + }//if( datadir.empty() ) + + const std::string sandia_reaction_file = SpecUtils::append_path(datadir, "sandia.reactiongamma.xml"); + BOOST_REQUIRE_MESSAGE( SpecUtils::is_file( sandia_reaction_file ), "sandia.reactiongamma.xml not at '" << sandia_reaction_file << "'" ); + + // Set the static data directory so we have cross-sections, reactions, and all that + BOOST_REQUIRE_NO_THROW( InterSpec::setStaticDataDirectory( datadir ) ); + + // Now find the full-version of sandia.decay.xml + + // What we actually want is the InterSpec code base-path - we'll search around a little for it + const string decay_xml = "external_libs/SandiaDecay/sandia.decay.xml"; + string potential_code_dirs[] = { + SpecUtils::append_path( test_file_dir, "../../../" ), + "..", "../..", "../../..", "/Users/wcjohns/rad_ana/InterSpec/" + }; + + for( const auto &d : potential_code_dirs ) + { + const string candidate = SpecUtils::append_path( d, decay_xml ); + if( SpecUtils::is_file(candidate) ) + { + sm_sandia_decay_file = candidate; + break; + } + } + + BOOST_REQUIRE_MESSAGE( !sm_sandia_decay_file.empty() && SpecUtils::is_file(sm_sandia_decay_file), + "Error finding the full version of sandia.decay.xml" ); + + BOOST_REQUIRE_NO_THROW( DecayDataBaseServer::setDecayXmlFile( sm_sandia_decay_file ) ); + + sm_static_data_dir = datadir; + + // Make sure we can actually init the decay database + const SandiaDecay::SandiaDecayDataBase * const db = DecayDataBaseServer::database(); + BOOST_REQUIRE_MESSAGE( db, "Error initing full SandiaDecayDataBase" ); + const SandiaDecay::Nuclide * const u238 = db->nuclide("U238"); + BOOST_REQUIRE_MESSAGE( u238, "Full SandiaDecayDataBase empty?" ); + + // Now make sure we have the full database with x-rays and coincidences + bool has_coincidences = false, has_xray = false; + + SandiaDecay::NuclideMixture mixture; + mixture.addNuclideByActivity( u238, 1.0E-3 * SandiaDecay::curie ); + + for( const SandiaDecay::NuclideActivityPair &nap : mixture.activity( 20*SandiaDecay::year ) ) + { + for( const SandiaDecay::Transition *transition : nap.nuclide->decaysToChildren ) + { + for( const SandiaDecay::RadParticle &particle : transition->products ) + { + has_xray |= (particle.type == SandiaDecay::ProductType::XrayParticle); + + if( particle.type == SandiaDecay::GammaParticle ) + has_coincidences |= !particle.coincidences.empty(); + }//for( const SandiaDecay::RadParticle &particle : transition->products ) + }//for( const SandiaDecay::Transition *transition : nap.nuclide->decaysToChildren ) + }//for( const SandiaDecay::NuclideActivityPair &nap : activities ) + + BOOST_REQUIRE_MESSAGE( has_coincidences, "U238 decay in SandiaDecay didnt have coincidences" ); + BOOST_REQUIRE_MESSAGE( has_xray, "U238 decay in SandiaDecay didnt have x-rays" ); +}//void set_data_dir() + +}//namespace + + +BOOST_AUTO_TEST_CASE( sources_in_lib ) { + SetDataDirs::set_data_dir(); + + const SandiaDecay::SandiaDecayDataBase * const db = DecayDataBaseServer::database(); + BOOST_REQUIRE( db ); + + auto test_line = []( const string src_line, const SrcLibLineInfo &expected ){ + + stringstream strm(src_line); + vector info = SrcLibLineInfo::sources_in_lib( strm ); + BOOST_CHECK( info.size() == 1 ); + if( info.size() != 1 ) + return; + + const SrcLibLineInfo &src = info.at(0); + + BOOST_CHECK_CLOSE_FRACTION( src.m_activity, expected.m_activity, 1.0E-6 ); + BOOST_CHECK_EQUAL( src.m_nuclide, expected.m_nuclide ); + BOOST_CHECK_EQUAL( src.m_activity_date, expected.m_activity_date ); + BOOST_CHECK_EQUAL( src.m_source_name, expected.m_source_name ); + BOOST_CHECK_EQUAL( src.m_comments, expected.m_comments ); + BOOST_CHECK_EQUAL( src.m_line, src_line ); + + BOOST_CHECK_CLOSE_FRACTION( src.m_activity_uncert, expected.m_activity_uncert, 1.0E-6 ); + BOOST_CHECK_CLOSE_FRACTION( src.m_distance, expected.m_distance, 1.0E-6 ); + }; + + string src_line = "60Co_12916 5.03E+03   01-Jan-2024 ActivityUncert=1.0e3, Distance=5cm"; + SrcLibLineInfo expected; + expected.m_activity = 5.03E+03 * PhysicalUnits::bq; + expected.m_nuclide = db->nuclide( "Co60" ); + expected.m_activity_date = boost::posix_time::time_from_string( "2024-01-01 00:00:00.000" ); + expected.m_source_name = "60Co_12916"; + expected.m_comments = "ActivityUncert=1.0e3, Distance=5cm"; + expected.m_activity_uncert = 1.0e3 * PhysicalUnits::bq; + expected.m_distance = 5.0 * PhysicalUnits::cm; + test_line( src_line, expected ); + + src_line = "60Co_12916 5.03E+03   01-Jan-2024 ActivityUncert=1.0e3"; + expected.m_distance = -1.0; + expected.m_comments = "ActivityUncert=1.0e3"; + test_line( src_line, expected ); + + src_line = "60Co_12916 5.03E+03   01-Jan-2024 Distance=5cm"; + expected.m_distance = 5.0 * PhysicalUnits::cm; + expected.m_activity_uncert = -1.0; + expected.m_comments = "Distance=5cm"; + test_line( src_line, expected ); + + src_line = "60Co_12916 5.03E+03   01-Jan-2024"; + expected.m_distance = -1.0; + expected.m_activity_uncert = -1.0; + expected.m_comments = ""; + test_line( src_line, expected ); + + + src_line = "22NA_042713 3.600E+05 25-Apr-2020 SomeNote"; + expected.m_activity = 3.600E+05 * PhysicalUnits::bq; + expected.m_nuclide = db->nuclide( "Na22" ); + expected.m_activity_date = boost::posix_time::time_from_string( "2020-04-25 00:00:00.000" ); + expected.m_source_name = "22NA_042713"; + expected.m_comments = "SomeNote"; + expected.m_activity_uncert = -1.0; + expected.m_distance = -1.0; + test_line( src_line, expected ); + + + src_line = "54MN_041711 3.511E+05 25-Apr-2020 AStorageLocation, Dist=24 cm, ActivityUncertainty=3.582E+04"; + expected.m_activity = 3.511E+05 * PhysicalUnits::bq; + expected.m_nuclide = db->nuclide( "Mn54" ); + expected.m_activity_date = boost::posix_time::time_from_string( "2020-04-25 00:00:00.000" ); + expected.m_source_name = "54MN_041711"; + expected.m_comments = "AStorageLocation, Dist=24 cm, ActivityUncertainty=3.582E+04"; + expected.m_activity_uncert = 3.582E+04 * PhysicalUnits::bq; + expected.m_distance = 24 * PhysicalUnits::cm; + test_line( src_line, expected ); + + + src_line = "22NA_08251 4.107E+04 1-Aug-2019 SomePlace"; + expected.m_activity = 4.107E+04 * PhysicalUnits::bq; + expected.m_nuclide = db->nuclide( "Na22" ); + expected.m_activity_date = boost::posix_time::time_from_string( "2019-08-01 00:00:00.000" ); + expected.m_source_name = "22NA_08251"; + expected.m_comments = "SomePlace"; + expected.m_activity_uncert = -1.0; + expected.m_distance = -1.0; + test_line( src_line, expected ); + + + // Test some invalid cases + { + stringstream strm("Invalid Line"); + vector info = SrcLibLineInfo::sources_in_lib( strm ); + BOOST_CHECK( info.size() == 0 ); + } + + + { + stringstream strm("22NotANuc_08251 4.107E+04 1-Aug-2019 SomePlace\n"); + vector info = SrcLibLineInfo::sources_in_lib( strm ); + BOOST_CHECK( info.size() == 0 ); + } + + // Test multiple lines + { + stringstream strm( " 22NA_08251 4.107E+04 1-Aug-2019 SomePlace \n" + "\n" + " \n" + "54MN_041711 3.511E+05 25-Apr-2020 AStorageLocation, Dist=24 cm, ActivityUncertainty=3.582E+04\n" + "\n" + " 60Co_12916 5.03E+03   01-Jan-2024\n" + ); + vector info = SrcLibLineInfo::sources_in_lib( strm ); + BOOST_CHECK( info.size() == 3 ); + } +}//BOOST_AUTO_TEST_CASE( sources_in_lib )