diff --git a/.gitignore b/.gitignore index 7dfbfb95..f9c18d5f 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ _site/ .idea/ *.pyc +webserver/molovol_wasm.* # Virtual python environment venv*/ @@ -61,3 +62,6 @@ external/VTK/build* external/VTK/out/build* notforcommit/ +webserver/static/molovol_wasm.data +webserver/static/molovol_wasm.js +webserver/static/molovol_wasm.wasm diff --git a/CMakeLists.txt b/CMakeLists.txt index f2226415..73fdbbc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ endif() set(CMAKE_OSX_DEPLOYMENT_TARGET 10.11 CACHE STRING "Minimum macOS deployment version" FORCE) # Set name and version -project(MoloVol VERSION 1.2.0) +project(MoloVol VERSION 1.2.1) if(NOT APPLE) # Strip binary for release build @@ -27,25 +27,54 @@ endif() include(ExecutableName) # EXE_NAME +if(EMSCRIPTEN) + set(EXE_NAME molovol_wasm) +endif() # Specify C++ standard set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -# wxWidgets -set(wxWidgets_USE_STATIC=ON) -# Not ideal to use these absolute paths here -if (MSVC) - set(wxWidgets_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/wxWidgets") - set(wxWidgets_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/wxWidgets/lib/vc_x64_lib-x64-Release-MT") +find_program(CCACHE_PROGRAM ccache) +if(CCACHE_PROGRAM) + set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") endif() -find_package(wxWidgets REQUIRED core base gl OPTIONAL_COMPONENTS net) -include(${wxWidgets_USE_FILE}) +if(APPLE) + if(CMAKE_C_COMPILER_ID MATCHES "Clang") + set(OpenMP_C_FLAGS "-Xpreprocessor -fopenmp") + set(OpenMP_C_LIB_NAMES "omp") + set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) + endif() + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp") + set(OpenMP_CXX_LIB_NAMES "omp") + set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) + endif() +endif() -#find_package(OpenMP) -if(MOLOVOL_RENDERER) - include(VTKRenderer) + +# wxWidgets +if(MOLOVOL_BUILD_GUI) + set(wxWidgets_USE_STATIC=ON) + # Not ideal to use these absolute paths here + if (MSVC) + set(wxWidgets_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/wxWidgets") + set(wxWidgets_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/wxWidgets/lib/vc_x64_lib-x64-Release-MT") + endif() + + if(MOLOVOL_RENDERER) + find_package(wxWidgets REQUIRED core base gl OPTIONAL_COMPONENTS net) + else() + find_package(wxWidgets REQUIRED core base OPTIONAL_COMPONENTS net) + endif() + include(${wxWidgets_USE_FILE}) + + if(MOLOVOL_RENDERER) + include(VTKRenderer) + endif() endif() # Add include path, so that header files can be found @@ -68,11 +97,10 @@ endif() include(Sources) # SOURCES -if(MOLOVOL_RENDERER) +if(MOLOVOL_RENDERER AND MOLOVOL_BUILD_GUI) list(APPEND SOURCES "src/render_frame.cpp") endif() - include(Resources) # ELEM_FILE # SPACEGROUP_FILE @@ -99,45 +127,54 @@ endif() #set(CMAKE_INSTALL_RPATH "@executable_path/Frameworks") # Target MoloVol -if (MSVC) - #target_sources(${EXE_NAME} PRIVATE ${WIN_RESOURCE_FILES}) - add_executable(${EXE_NAME} WIN32 ${SOURCES} ${WIN_RESOURCE_FILES}) +if(EMSCRIPTEN) + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/wasm.cmake) +elseif(MSVC) + #target_sources(${EXE_NAME} PRIVATE ${WIN_RESOURCE_FILES}) + add_executable(${EXE_NAME} WIN32 ${SOURCES} ${WIN_RESOURCE_FILES}) else() - add_executable(${EXE_NAME} ${SOURCES} ${OSX_RESOURCE_FILES}) + add_executable(${EXE_NAME} ${SOURCES} ${OSX_RESOURCE_FILES}) endif() - # XCode, app bundle and libtiff include(MacSpecific) - -target_link_libraries(${EXE_NAME} ${wxWidgets_LIBRARIES}) -if(MOLOVOL_RENDERER) - target_link_libraries(${EXE_NAME} ${WXVTK_LIB}) - target_compile_definitions(${EXE_NAME} PRIVATE MOLOVOL_RENDERER) +target_link_libraries(${EXE_NAME} PRIVATE molovol_lib) + +if(MOLOVOL_BUILD_GUI) + target_link_libraries(${EXE_NAME} ${wxWidgets_LIBRARIES}) + target_compile_definitions(${EXE_NAME} PRIVATE MOLOVOL_GUI) + if(MOLOVOL_RENDERER) + target_link_libraries(${EXE_NAME} ${WXVTK_LIB}) + target_compile_definitions(${EXE_NAME} PRIVATE MOLOVOL_RENDERER) + endif() endif() - # Add custom flag if(MOLOVOL_ABS_RESOURCE_PATH) target_compile_definitions(${EXE_NAME} PUBLIC -DABS_PATH) endif() -# Keeping this around just so I don't forget the syntax -# if(OpenMP_CXX_FOUND) -# target_link_libraries(target PUBLIC OpenMP::OpenMP_CXX) -# endif() - -# Tests -if (MOLOVOL_BUILD_TESTING AND BUILD_TESTING) - include(Testing) -endif() - # Installation instructions for debian package -if (UNIX AND NOT APPLE) - include(DebInstall) -elseif(APPLE) - # This is needed for generation of the dmg file - install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/MoloVol.app DESTINATION "." - FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) +if (NOT EMSCRIPTEN) # Only do native installations + if (UNIX AND NOT APPLE) + include(DebInstall) + elseif(APPLE) + # This is needed for generation of the dmg file + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/MoloVol.app DESTINATION "." + FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) + endif() endif() include(Packing) + +#this openMP section is currently commented out as it leads to runtime crashed because of some memory issue +find_package(OpenMP) +if(OpenMP_CXX_FOUND) + target_link_libraries(${EXE_NAME} PUBLIC OpenMP::OpenMP_CXX) + target_compile_definitions(${EXE_NAME} PUBLIC HAVE_OPENMP) +endif() + + +# Ensure molovol_test is built if MOLOVOL_BUILD_TESTING is ON +if(MOLOVOL_BUILD_TESTING) + include(Testing) # Include Testing.cmake here +endif() diff --git a/README.md b/README.md index de52d626..6cf076a6 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,16 @@ software for your own purposes or propose changes to the developers and take par Dependencies needed for compilation: - Any C++ compiler -- [wxWidgets 3.1.5](https://www.wxwidgets.org) +- cmake +- Optional for native GUI: [wxWidgets 3.1.5](https://www.wxwidgets.org) + +You can build without the desktop native GUI by setting it up with +`cmake -DMOLOVOL_BUILD_GUI=0` + +You can build for wasm with set-up +`emcmake cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DMOLOVOL_BUILD_GUI=0 ..` +then compile with +`emmake make` ### MoloVol Web @@ -58,7 +67,7 @@ front-end wrapping the MoloVol CLI interface. To launch, first change the FLASK_ the command `export FLASK_APP=./webserver/app.py` from the project's root directory. Then execute `flask run`. For hosting on a web server check out the next section. -### Containerized application +### Containerized cli application Instead of compiling or running the binaries you can also use a containerized version (for instance using docker or podman) to access the CLI or web interface. @@ -72,7 +81,7 @@ below with your local image name. Running a container: - For a short-lived container: Pass the CLI arguments in the run command: `docker run -it bsvogler/molovol ./launch_headless.sh ` -- To run web application http://localhost:80: run +- To run web application http://localhost:5000: run `docker run -dt --restart=always -p 5000:5000 --name molovol bsvogler/molovol`. When not otherwise specified the default port of a flask instance is 5000. diff --git a/cmake/DebInstall.cmake b/cmake/DebInstall.cmake index eaee6db1..9fa50b10 100644 --- a/cmake/DebInstall.cmake +++ b/cmake/DebInstall.cmake @@ -1,10 +1,8 @@ - include(GNUInstallDirs) -# Compress changelog +# Compress changelog and man pages (needed for both GUI and non-GUI) set(DEB_CHANGELOG_COMPRESSED "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz") set(DEB_MAN_COMPRESSED "${CMAKE_CURRENT_BINARY_DIR}/molovol.1.gz") -set(HICOLOR_DIR "${CMAKE_CURRENT_BINARY_DIR}/hicolor") add_custom_command( OUTPUT ${DEB_CHANGELOG_COMPRESSED} ${DEB_MAN_COMPRESSED} @@ -15,23 +13,24 @@ add_custom_command( COMMENT "Compressing changelog and manual file" ) -add_custom_command( - OUTPUT ${HICOLOR_DIR} - COMMAND bash ${DEB_RES_DIR}/shell/resize_icon ${DEB_ICON} ./ - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS ${DEB_ICON} - COMMENT "Resizing icon to supported sizes" -) - add_custom_target(compress ALL DEPENDS ${DEB_CHANGELOG_COMPRESSED} ${DEB_MAN_COMPRESSED}) -add_custom_target(resize_icon ALL DEPENDS ${HICOLOR_DIR}) - -# -execute_process(COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE LINUX_ARCHITECTURE) +if(MOLOVOL_BUILD_GUI) + # GUI-specific icon processing + set(HICOLOR_DIR "${CMAKE_CURRENT_BINARY_DIR}/hicolor") + + add_custom_command( + OUTPUT ${HICOLOR_DIR} + COMMAND bash ${DEB_RES_DIR}/shell/resize_icon ${DEB_ICON} ./ + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${DEB_ICON} + COMMENT "Resizing icon to supported sizes" + ) + + add_custom_target(resize_icon ALL DEPENDS ${HICOLOR_DIR}) +endif() # Set directory permissions to 0755 -# Must come before install commands set( CPACK_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE @@ -39,15 +38,21 @@ set( WORLD_READ WORLD_EXECUTE ) -# Install commands so that these files get added to the deb file +# Install commands for both GUI and non-GUI install(TARGETS ${EXE_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES ${ELEM_FILE} ${SPACEGROUP_FILE} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${EXE_NAME}) install(FILES ${DEB_COPYRIGHT_FILE} ${DEB_CHANGELOG_COMPRESSED} DESTINATION ${CMAKE_INSTALL_DATADIR}/doc/${EXE_NAME}) -install(FILES ${DEB_DESKTOP_FILE} - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) -install(FILES ${DEB_MAN_COMPRESSED} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) -install(FILES ${DEB_ICON} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pixmaps) -install(DIRECTORY ${HICOLOR_DIR} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons) - +install(FILES ${DEB_MAN_COMPRESSED} + DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) + +# GUI-specific install commands +if(MOLOVOL_BUILD_GUI) + install(FILES ${DEB_DESKTOP_FILE} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) + install(FILES ${DEB_ICON} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pixmaps) + install(DIRECTORY ${HICOLOR_DIR} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons) +endif() \ No newline at end of file diff --git a/cmake/MacSpecific.cmake b/cmake/MacSpecific.cmake index d2698054..0bed5119 100644 --- a/cmake/MacSpecific.cmake +++ b/cmake/MacSpecific.cmake @@ -1,15 +1,20 @@ - # XCode compatibility set_target_properties(${EXE_NAME} PROPERTIES XCODE_GENERATE_SCHEME TRUE XCODE_SCHEME_WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) -# macOS Bundle -set_target_properties(${EXE_NAME} PROPERTIES - MACOSX_BUNDLE_BUNDLE_NAME ${EXE_NAME} - MACOSX_BUNDLE_EXECUTABLE_NAME ${EXE_NAME} - MACOSX_BUNDLE_BUNDLE_VERSION ${CMAKE_PROJECT_VERSION} - MACOSX_BUNDLE_ICON_FILE icon -) +if(MOLOVOL_BUILD_GUI) + # macOS Bundle + set_target_properties(${EXE_NAME} PROPERTIES + MACOSX_BUNDLE_BUNDLE_NAME ${EXE_NAME} + MACOSX_BUNDLE_EXECUTABLE_NAME ${EXE_NAME} + MACOSX_BUNDLE_BUNDLE_VERSION ${CMAKE_PROJECT_VERSION} + MACOSX_BUNDLE_ICON_FILE icon + ) + # Library shenanigans for wxWidgets + if(APPLE) + string(REPLACE "-ltiff" "/usr/local/opt/libtiff/lib/libtiff.a" wxWidgets_LIBRARIES "${wxWidgets_LIBRARIES}") + endif() +endif() diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 6e238df4..08459b2b 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -21,6 +21,11 @@ if (CMAKE_MACOSX_BUNDLE AND NOT MOLOVOL_ABS_RESOURCE_PATH) set(MOLOVOL_ABS_RESOURCE_PATH TRUE) endif() +option( + MOLOVOL_BUILD_GUI + "Build MoloVol with GUI support (requires wxWidgets)" + ON +) option( MOLOVOL_RENDERER "Enable compilation and linking to wxVTK24" diff --git a/cmake/Resources.cmake b/cmake/Resources.cmake index d4d508a1..b1d24dbb 100644 --- a/cmake/Resources.cmake +++ b/cmake/Resources.cmake @@ -1,50 +1,58 @@ # RESOURCE FILES -# Universal resource files +# Universal resource files (needed for both GUI and non-GUI) set(ELEM_FILE "${CMAKE_CURRENT_SOURCE_DIR}/inputfile/elements.txt") set(SPACEGROUP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/inputfile/space_groups.txt") set(LICENSE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") set(README_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README.md") +# Example files +set(INPUTDIR inputfile) +set(EXAMPLE_FILES ${INPUTDIR}/example_C60.cif ${INPUTDIR}/example_C60.xyz ${INPUTDIR}/example_C60.pdb) + # Third party licenses and copyright notices -if(MOLOVOL_RENDERER) +if(MOLOVOL_RENDERER AND MOLOVOL_BUILD_GUI) set(TPL_VTK "${CMAKE_CURRENT_SOURCE_DIR}/external/VTK/Copyright.txt") set(TPL_VTK_COPY "${CMAKE_BINARY_DIR}/VTK.txt") configure_file(${TPL_VTK} ${TPL_VTK_COPY} COPYONLY) endif() -# Resource files for macOS Bundle -set(OSX_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/macOS") -set(OSX_ICON_FILE "${OSX_RES_DIR}/icon.icns") -set(OSX_LICENSE_RTF "${OSX_RES_DIR}/LICENSE.rtf") -set(OSX_DMG_BACKGROUND "${OSX_RES_DIR}/background.png") -set(OSX_DMG_DSSTORE "${OSX_RES_DIR}/DS_Store/.DS_Store") -# TODO: Why is icon file moved to app bundle like this? -set_source_files_properties(${OSX_ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") -set_source_files_properties(${ELEM_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") -set_source_files_properties(${SPACEGROUP_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") +if(MOLOVOL_BUILD_GUI) + # Resource files for macOS Bundle (GUI only) + set(OSX_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/macOS") + set(OSX_ICON_FILE "${OSX_RES_DIR}/icon.icns") + set(OSX_LICENSE_RTF "${OSX_RES_DIR}/LICENSE.rtf") + set(OSX_DMG_BACKGROUND "${OSX_RES_DIR}/background.png") + set(OSX_DMG_DSSTORE "${OSX_RES_DIR}/DS_Store/.DS_Store") + + if(APPLE) + set_source_files_properties(${OSX_ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + set_source_files_properties(${ELEM_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + set_source_files_properties(${SPACEGROUP_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + + if(MOLOVOL_RENDERER) + set_source_files_properties(${TPL_VTK_COPY} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/licenses") + set(OSX_RESOURCE_FILES ${OSX_ICON_FILE} ${ELEM_FILE} ${SPACEGROUP_FILE} ${TPL_VTK_COPY}) + else() + set(OSX_RESOURCE_FILES ${OSX_ICON_FILE} ${ELEM_FILE} ${SPACEGROUP_FILE}) + endif() + endif() -if(MOLOVOL_RENDERER) - set_source_files_properties(${TPL_VTK_COPY} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/licenses") - set(OSX_RESOURCE_FILES ${OSX_ICON_FILE} ${ELEM_FILE} ${SPACEGROUP_FILE} ${TPL_VTK_COPY}) -else() - set(OSX_RESOURCE_FILES ${OSX_ICON_FILE} ${ELEM_FILE} ${SPACEGROUP_FILE}) + # GUI-specific Debian package resources + set(DEB_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/linux") + set(DEB_DESKTOP_FILE ${DEB_RES_DIR}/MoloVol.desktop) + set(DEB_ICON ${DEB_RES_DIR}/molovol.png) + + # Resource files for Windows + set(WIN_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/windows") + set(WIN_RESOURCE_FILES "${WIN_RES_DIR}/resource.rc") + set(WIN_ICON_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/windows/icon.ico") + set(WIN_LICENSE_RTF "${WIN_RES_DIR}/LICENSE.rtf") endif() -# Resource files for Debian package -set(DEB_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/linux") -set(DEB_COPYRIGHT_FILE "${DEB_RES_DIR}/copyright") -set(DEB_CHANGELOG_FILE "${DEB_RES_DIR}/changelog") -file(STRINGS ${DEB_RES_DIR}/MoloVol.desktop DEB_DESKTOP_FILE) -set(DEB_DESKTOP_FILE ${DEB_RES_DIR}/MoloVol.desktop) -set(DEB_MAN_FILE ${DEB_RES_DIR}/molovol.1) -set(DEB_ICON ${DEB_RES_DIR}/molovol.png) - -# Resource files for Windows -set(WIN_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/windows") -set(WIN_RESOURCE_FILES "${WIN_RES_DIR}/resource.rc") -set(WIN_ICON_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/windows/icon.ico") -set(WIN_LICENSE_RTF "${WIN_RES_DIR}/LICENSE.rtf") - -# Example files -set(INPUTDIR inputfile) -set(EXAMPLE_FILES ${INPUTDIR}/example_C60.cif ${INPUTDIR}/example_C60.xyz ${INPUTDIR}/example_C60.pdb) +# Debian package resources needed for both GUI and non-GUI +if(UNIX AND NOT APPLE) + set(DEB_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/linux") + set(DEB_COPYRIGHT_FILE "${DEB_RES_DIR}/copyright") + set(DEB_CHANGELOG_FILE "${DEB_RES_DIR}/changelog") + set(DEB_MAN_FILE "${DEB_RES_DIR}/molovol.1") +endif() \ No newline at end of file diff --git a/cmake/Sources.cmake b/cmake/Sources.cmake index fd263b0d..7efb796d 100644 --- a/cmake/Sources.cmake +++ b/cmake/Sources.cmake @@ -1,16 +1,8 @@ - -# List of source files -set(SOURCES +# Base sources (non-GUI) +set(LIB_SOURCES src/atom.cpp src/atomtree.cpp - src/base_guicontrol.cpp - src/base_cmdline.cpp - src/base_constr.cpp - src/base_event.cpp - src/base_guicontrol.cpp - src/base_init.cpp src/cavity.cpp - src/controller.cpp src/crystallographer.cpp src/griddata.cpp src/importmanager.cpp @@ -22,4 +14,35 @@ set(SOURCES src/special_chars.cpp src/vector.cpp src/voxel.cpp + src/controller.cpp ) +# Create the static library +add_library(molovol_lib STATIC ${LIB_SOURCES}) +target_include_directories(molovol_lib PUBLIC include) + +# Set the same compiler options for the library +target_compile_options(molovol_lib PRIVATE -Wall -Werror -Wno-unused-command-line-argument -Wno-invalid-source-encoding) +target_compile_options(molovol_lib PRIVATE "$<$>:-DDEBUG>") + +# Define MOLOVOL_GUI for the library when building with GUI +if(MOLOVOL_BUILD_GUI) + target_compile_definitions(molovol_lib PRIVATE MOLOVOL_GUI) +endif() + +# GUI-specific sources +set(GUI_SOURCES + src/base_guicontrol.cpp + src/base_constr.cpp + src/base_event.cpp + src/base_init.cpp +) + +set(CLI_SOURCES + src/base_cmdline.cpp +) + +if(MOLOVOL_BUILD_GUI) + set(SOURCES ${GUI_SOURCES}) +else() + set(SOURCES ${CLI_SOURCES}) +endif() diff --git a/cmake/Testing.cmake b/cmake/Testing.cmake index 451807cf..d7d04bc3 100644 --- a/cmake/Testing.cmake +++ b/cmake/Testing.cmake @@ -1,4 +1,4 @@ - +# Enable testing. This is crucial for CMake to manage and run tests. enable_testing() # Create a MoloVol library for the test sources to use @@ -23,16 +23,58 @@ set(TEST_NAMES ) set(MOLOVOL_TEST_DIR ${CMAKE_SOURCE_DIR}/test) +#separate and explicit to avoid having duplicate main function -foreach(TN IN ITEMS ${TEST_NAMES}) +# Create separate executables for each unit test +add_executable(vector_test ${MOLOVOL_TEST_DIR}/class_vector.cpp) +add_executable(atom_test ${MOLOVOL_TEST_DIR}/struct_atom.cpp) +add_executable(string_test ${MOLOVOL_TEST_DIR}/cut_off_string.cpp) +add_executable(atomtree_test ${MOLOVOL_TEST_DIR}/class_atomtree.cpp) +add_executable(benchmark_tests ${MOLOVOL_TEST_DIR}/performance_test.cpp) - set(TEST_SRC_NAME ${TN}.cpp) - set(TEST_EXE_NAME t_${TN}) - add_executable(${TEST_EXE_NAME} ${MOLOVOL_TEST_DIR}/${TEST_SRC_NAME}) - set_target_properties(${TEST_EXE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/testbin) - target_include_directories(${TEST_EXE_NAME} PUBLIC ${MOLOVOL_TEST_DIR}) - target_link_libraries(${TEST_EXE_NAME} mvl) - add_test(NAME ${TN} COMMAND ${TEST_EXE_NAME}) +# Set include directories for all tests +foreach(TEST_TARGET vector_test atom_test string_test atomtree_test benchmark_tests) + target_link_libraries(${TEST_TARGET} PRIVATE molovol_lib) + target_include_directories(${TEST_TARGET} PUBLIC + ${MOLOVOL_TEST_DIR} + ${CMAKE_SOURCE_DIR}/include + ) endforeach() +# Find the benchmark library. This is required if you have benchmark tests. +find_package(benchmark REQUIRED) +target_link_libraries(benchmark_tests PRIVATE + benchmark::benchmark_main +) +add_custom_command(TARGET benchmark_tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + $/inputfile + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_SOURCE_DIR}/inputfile/elements.txt + $/inputfile/ + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_SOURCE_DIR}/inputfile/example_C60.xyz + $/inputfile/ + COMMENT "Copying input files for benchmark tests" +) + +# Add all tests +add_test(NAME vector_test COMMAND vector_test) +add_test(NAME atom_test COMMAND atom_test) +add_test(NAME string_test COMMAND string_test) +add_test(NAME atomtree_test COMMAND atomtree_test) +add_test(NAME benchmark_tests COMMAND benchmark_tests) +target_compile_definitions(benchmark_tests PRIVATE + SOURCE_DIR="${CMAKE_SOURCE_DIR}" +) +# Add compiler warnings +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU") + foreach(TEST_TARGET vector_test atom_test string_test atomtree_test benchmark_tests) + target_compile_options(${TEST_TARGET} PRIVATE -Wall -Wextra -Werror) + endforeach() +endif() + +add_custom_target(build_tests ALL + DEPENDS vector_test atom_test string_test atomtree_test benchmark_tests) + \ No newline at end of file diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake new file mode 100644 index 00000000..448be264 --- /dev/null +++ b/cmake/wasm.cmake @@ -0,0 +1,74 @@ +# wasm.cmake + +# Ensure we're using Emscripten +if(NOT EMSCRIPTEN) + message(FATAL_ERROR "wasm.cmake should only be included when compiling with Emscripten") +endif() + +# Set C++ standard for WASM build +set(CMAKE_CXX_EXTENSIONS OFF) + +# Emscripten compiler flags +set(WASM_COMPILER_FLAGS + -s WASM=1 + -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS','allocate'] + -s ALLOW_MEMORY_GROWTH=1 + -s EXPORTED_FUNCTIONS=['_malloc','_free'] + -fexceptions + -s FORCE_FILESYSTEM=1 +) + +function(require_file FILE_PATH) + if(NOT EXISTS "${FILE_PATH}") + message(FATAL_ERROR "Required file not found: ${FILE_PATH}") + endif() +endfunction() + +# Verify required files exist +require_file("${CMAKE_CURRENT_SOURCE_DIR}/inputfile/elements.txt") +require_file("${CMAKE_CURRENT_SOURCE_DIR}/inputfile/space_groups.txt") + +# Link flags specific to WASM +set(WASM_LINK_FLAGS + -s ENVIRONMENT='web' + -s MODULARIZE=1 + -s EXPORT_NAME='createMoloVolModule' + -s NO_EXIT_RUNTIME=1 + -s ASSERTIONS=1 + --bind + -s EXPORT_ES6=0#disabled for safari + -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS'] + + # Preload resource files + --preload-file ${CMAKE_CURRENT_SOURCE_DIR}/inputfile/elements.txt@/inputfile/elements.txt + --preload-file ${CMAKE_CURRENT_SOURCE_DIR}/inputfile/space_groups.txt@/inputfile/space_groups.txt +) + +# Apply compiler flags +string(REPLACE ";" " " WASM_COMPILER_FLAGS_STR "${WASM_COMPILER_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_COMPILER_FLAGS_STR}") + +# Create WebAssembly target with all sources including bindings +add_executable(molovol_wasm + ${SOURCES} + src/wasm_bindings.cpp +) + +# Apply link flags +string(REPLACE ";" " " WASM_LINK_FLAGS_STR "${WASM_LINK_FLAGS}") +set_target_properties(molovol_wasm PROPERTIES + LINK_FLAGS "${WASM_LINK_FLAGS_STR}" +) + +# Copy output files to web directory +add_custom_command(TARGET molovol_wasm POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/molovol_wasm.js + ${CMAKE_CURRENT_SOURCE_DIR}/webserver/static/molovol_wasm.js + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/molovol_wasm.wasm + ${CMAKE_CURRENT_SOURCE_DIR}/webserver/static/molovol_wasm.wasm + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/molovol_wasm.data + ${CMAKE_CURRENT_SOURCE_DIR}/webserver/static/molovol_wasm.data +) \ No newline at end of file diff --git a/container/Dockerfile b/container/Dockerfile index 2e8855b6..72db2744 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -1,11 +1,11 @@ # builds molovol and installs the webapp -FROM bsvogler/wxwidgets -# hack to create a headless x server, cmake for building -RUN apt update && apt upgrade -y -RUN apt-get install xvfb cmake -y -# does not work when set in dockerfile? -ENV DISPLAY=:1.0 - +FROM alpine:latest +RUN apk add --no-cache \ + cmake \ + make \ + gcc \ + g++ \ + musl-dev # compile molovol, for some reason some less important files like the readme are needed for cmake WORKDIR /build COPY cmake/ cmake/ @@ -16,24 +16,26 @@ COPY include/ include/ COPY LICENSE ./ COPY README.md ./ COPY inputfile/ inputfile/ -RUN cmake . -DCMAKE_BUILD_TYPE=RELEASE -DMOLOVOL_ABS_RESOURCE_PATH=ON && make +RUN cmake . -DCMAKE_BUILD_TYPE=RELEASE -DMOLOVOL_ABS_RESOURCE_PATH=ON -DMOLOVOL_BUILD_GUI=OFF -DCMAKE_C_FLAGS="-fopenmp" -DCMAKE_CXX_FLAGS="-fopenmp" && make +RUN apk add --no-cache \ + file \ + dpkg RUN cpack -G DEB && dpkg -i MoloVol_*.deb WORKDIR / # add flask webserver -#RUN apt install software-properties-common -y -RUN apt install python3.11 curl -y && apt-get clean && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache python3 py3-pip curl + # install poetry -RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/usr/local/ python3 - --version 1.6.1 +RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/usr/local/ python3 - --version 1.8.1 RUN poetry config virtualenvs.create false RUN poetry config virtualenvs.options.system-site-packages true -ENV PYTHONPATH=$PYTHONPATH:/usr/local/lib/python3.12/site-packages/ + # install dependencies COPY webserver/ ./ RUN poetry install --no-root --only main + WORKDIR / -COPY launch_headless.sh /launch_headless.sh -RUN chmod +x launch_headless.sh ENV FLASK_APP=/webserver/app.py -CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"] +CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"] \ No newline at end of file diff --git a/container/alpine_wip.Dockerfile b/container/alpine_wip.Dockerfile deleted file mode 100644 index 5aee0f49..00000000 --- a/container/alpine_wip.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM alpine -RUN apk apk update -RUN apk add build-base -RUN apk add gtk+2.0-dev -RUN wget https://github.com/wxWidgets/wxWidgets/releases/download/v3.1.5/wxWidgets-3.1.5.tar.bz2 -RUN tar xvf wxWidgets-3.1.5.tar.bz2 -WORKDIR wxWidgets-3.1.5 -RUN ./configure --disable-shared --enable-unicode -RUN make install \ No newline at end of file diff --git a/container/headless.Dockerfile b/container/headless.Dockerfile deleted file mode 100644 index b63a9bef..00000000 --- a/container/headless.Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# buidls only headless version without webapp -FROM ubuntu AS compiler -COPY ./container/install_wxwidgets.sh ./ -RUN ./install_wxwidgets.sh -RUN rm install_wxwidgets.sh - -# hack to create a headless x server, does not work when set in dockerfile? -RUN apt-get install xvfb -y -ENV DISPLAY=:1.0 - -#compile molovol - -#FROM compiler AS builder -RUN apt update; apt install pip -y -RUN apt purge --auto-remove cmake -y; pip install cmake --upgrade -WORKDIR / -COPY src/ src/ -COPY include/ include/ -COPY CMakeLists.txt / -COPY inputfile/ inputfile/ -RUN mkdir cmake -WORKDIR cmake -RUN cmake .. -DCMAKE_BUILD_TYPE=RELEASE -RUN make -#launch.sh is expecting that molovol is residing in bin -RUN mkdir /build/ && mv MoloVol /bin/ && mv inputfile /bin/ - -WORKDIR / -COPY launch_headless.sh launch.sh -RUN chmod +x launch.sh -ENTRYPOINT ["./launch.sh"] -CMD ["-r", "1.2", "-g", "0.2", "-fs", "/inputfile/isobutane.xyz", "-q", "-o", "time,vol"] \ No newline at end of file diff --git a/external/VTK b/external/VTK deleted file mode 160000 index f0430e5f..00000000 --- a/external/VTK +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f0430e5f19a271e4510bb7df3dc4149beb727ada diff --git a/external/wxVTK24 b/external/wxVTK24 deleted file mode 160000 index e23b37b9..00000000 --- a/external/wxVTK24 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e23b37b920c3a0a790d1523da46c9abdeb810fdb diff --git a/external/wxWidgets b/external/wxWidgets deleted file mode 160000 index 9c0a8be1..00000000 --- a/external/wxWidgets +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9c0a8be1dc32063d91ed1901fd5fcd54f4f955a1 diff --git a/include/atom.h b/include/atom.h index c6a8dace..cd8c18b6 100644 --- a/include/atom.h +++ b/include/atom.h @@ -29,8 +29,8 @@ struct Atom { const pos_type getPos() const; const Vector getPosVec() const; - const num_type getRad() const; - const num_type getCoordinate(const char) const; + num_type getRad() const; + num_type getCoordinate(const char) const; void print() const; bool isValid() const; diff --git a/include/atomtree.h b/include/atomtree.h index 27e0422b..ff8e33e3 100644 --- a/include/atomtree.h +++ b/include/atomtree.h @@ -43,8 +43,7 @@ class AtomTree{ // TODO: Move and copy operators const AtomNode* getRoot() const; - - const double getMaxRad() const; + double getMaxRad() const; const std::vector& getAtomList() const; std::vector listAllWithin(Atom::pos_type, const double) const; diff --git a/include/base_cmdline.h b/include/base_cmdline.h new file mode 100644 index 00000000..b0b7b65b --- /dev/null +++ b/include/base_cmdline.h @@ -0,0 +1,79 @@ +#ifndef MOLOVOL_BASE_CMDLINE_H +#define MOLOVOL_BASE_CMDLINE_H + +#include +#include +#include +#include +#include "flags.h" + +struct CommandLineOption { + std::string shortName; + std::string longName; + std::string description; + bool isSwitch; // true for flags, false for options with values + bool isRequired; +}; + +class CommandLineParser { +public: + bool parse(int argc, char* argv[]); // Original method for native environment + bool parse(const std::vector& args); // New method for WASM + bool found(const std::string& name) const; + std::optional getValue(const std::string& name) const; + void displayHelp() const; + const std::vector& getOptions() const { return options; } + +private: + const std::vector options = { + {"h", "help", "Display help for command line interface", true, false}, + {"r", "radius", "Probe radius", false, true}, + {"g", "grid", "Spatial resolution of the underlying grid", false, true}, + {"fs", "file-structure", "Path to the structure file", false, true}, + {"fe", "file-elements", "Path to the elements file", false, false}, + {"do", "dir-output", "Path to the output directory", false, false}, + {"r2", "radius2", "Large probe radius (for two-probe mode)", false, false}, + {"d", "depth", "Octree depth", false, false}, + {"ht", "hetatm", "Include HETATM from pdb file", true, false}, + {"uc", "unitcell", "Evaluate unit cell", true, false}, + {"sf", "surface", "Calculate surfaces", true, false}, + {"xr", "export-report", "Export report (requires:-do)", true, false}, + {"xt", "export-total", "Export total surface map (requires:-do)", true, false}, + {"xc", "export-cavities", "Export surface maps for all cavities (requires:-do)", true, false}, + {"o", "output", "Control what parts of the output to display (default:all)", false, false}, + {"q", "quiet", "Silence progress reporting", true, false}, + {"un", "unicode", "Allow unicode output", true, false}, + {"v", "version", "Display the app version", true, false} + }; + + std::map parsedOptions; + std::map parsedFlags; + + std::optional findOption(const std::string& name) const; + bool validateRequiredOptions() const; +}; + +// Validation function declarations +bool validateProbes(double r1, double r2, bool pm); +bool validateExport(const std::string& out_dir, const std::vector& exp_options); +bool validatePdb(const std::string& file, bool hetatm, bool unitcell); + +// Display options constants - declare as extern +inline const std::map DISPLAY_OPTIONS = { + {"none", mvOUT_NONE}, {"inputfile", mvOUT_STRUCTURE}, {"resolution", mvOUT_RESOLUTION}, + {"depth", mvOUT_DEPTH}, {"radius_small", mvOUT_RADIUS_S}, {"radius_large", mvOUT_RADIUS_L}, + {"input", mvOUT_INP}, {"hetatm", mvOUT_OPT_HETATM}, {"unitcell", mvOUT_OPT_UNITCELL}, + {"probemode", mvOUT_OPT_PROBEMODE}, {"surface", mvOUT_OPT_SURFACE}, {"options", mvOUT_OPT}, + {"formula", mvOUT_FORMULA}, {"time", mvOUT_TIME}, {"vol_vdw", mvOUT_VOL_VDW}, + {"vol_inaccessible", mvOUT_VOL_INACCESSIBLE}, {"vol_core_s", mvOUT_VOL_CORE_S}, + {"vol_shell_s", mvOUT_VOL_SHELL_S}, {"vol_core_l", mvOUT_VOL_CORE_L}, + {"vol_shell_l", mvOUT_VOL_SHELL_L}, {"vol_mol", mvOUT_VOL_MOL}, {"vol", mvOUT_VOL}, + {"surf_vdw", mvOUT_SURF_VDW}, {"surf_mol", mvOUT_SURF_MOL}, + {"surf_excluded_s", mvOUT_SURF_EXCLUDED_S}, {"surf_accessible_s", mvOUT_SURF_ACCESSIBLE_S}, + {"surf", mvOUT_SURF}, {"cavities", mvOUT_CAVITIES}, {"all", mvOUT_ALL} +}; + +// Display options evaluation function +unsigned evalDisplayOptions(const std::string& output); + +#endif // MOLOVOL_BASE_CMDLINE_H \ No newline at end of file diff --git a/include/controller.h b/include/controller.h index 5a9b3b91..31615d72 100644 --- a/include/controller.h +++ b/include/controller.h @@ -1,22 +1,24 @@ #ifndef CONTROLLER_H - #define CONTROLLER_H #include "flags.h" #include #include +#include +#ifdef MOLOVOL_GUI #include +class MainFrame; +#endif struct CalcReportBundle; class Model; -class MainFrame; class AtomTree; struct Atom; template class Container3D; class Voxel; -class Ctrl{ +class Ctrl { public: static Ctrl* getInstance(); @@ -30,13 +32,19 @@ class Ctrl{ static std::string getDefaultElemPath(); static std::string getVersion(); +#ifdef MOLOVOL_GUI bool loadElementsFile(); bool loadAtomFile(); - bool runCalculation(); - bool runCalculation(const double, const double, const double, const std::string&, - const std::string&, const std::string&, const int, const bool, const bool, - const bool, const bool, const bool, const bool, const bool, const unsigned); + bool runCalculation(); // GUI version void registerView(MainFrame* inp_gui); +#endif + + // CLI version + bool runCalculation(const double, const double, const double, + const std::string&, const std::string&, const std::string&, + const int, const bool, const bool, const bool, const bool, + const bool, const bool, const bool, const unsigned); + void clearOutput(); void notifyUser(std::string); void notifyUser(std::wstring); @@ -61,24 +69,24 @@ class Ctrl{ void displayErrorMessage(const int, const std::vector& =std::vector()); private: - // consider making static pointer for model Model* _current_calculation; - // static attributes to ensure there is only one of each static Ctrl* s_instance; +#ifdef MOLOVOL_GUI static MainFrame* s_gui; +#endif - bool _abort_calculation; // variable for main thread to signal stopping the calculation + bool _abort_calculation; bool _calculation_finished; - bool _to_gui = true; // determines whether to print to console or to GUI - bool _quiet = true; // silences all non-result command line outputs + bool _to_gui = true; + bool _quiet = true; void displayInput(CalcReportBundle&, const unsigned=mvOUT_ALL); void displayResults(CalcReportBundle&, const unsigned=mvOUT_ALL); void displayCavityList(CalcReportBundle&, const unsigned=mvOUT_ALL); std::string getErrorMessage(const int); - inline static const std::string s_version = "1.2.0"; + inline static const std::string s_version = "1.2.1"; inline static const std::string s_elem_file = "elements.txt"; }; -#endif +#endif \ No newline at end of file diff --git a/include/special_chars.h b/include/special_chars.h index 1aea4487..b2653d26 100644 --- a/include/special_chars.h +++ b/include/special_chars.h @@ -3,8 +3,9 @@ #define SPECIAL_CHARS_H #include +#ifdef MOLOVOL_GUI #include - +#endif class Symbol{ public: static std::wstring angstrom(); diff --git a/src/atom.cpp b/src/atom.cpp index 1a533372..d98dcc6a 100644 --- a/src/atom.cpp +++ b/src/atom.cpp @@ -25,11 +25,11 @@ const Vector Atom::getPosVec() const { } // Not needed for struct member access -const double Atom::getRad() const { +double Atom::getRad() const { return rad; } -const double Atom::getCoordinate(const char dim) const { +double Atom::getCoordinate(const char dim) const { switch(dim){ case 0: return pos_x; case 1: return pos_y; diff --git a/src/atomtree.cpp b/src/atomtree.cpp index 831a935d..5e0f53db 100644 --- a/src/atomtree.cpp +++ b/src/atomtree.cpp @@ -187,7 +187,7 @@ std::vector& AtomTree::getAtomList() { return _atom_list; } -const double AtomTree::getMaxRad() const { +double AtomTree::getMaxRad() const { return _max_rad; } diff --git a/src/base_cmdline.cpp b/src/base_cmdline.cpp index be7605c2..5e9246f7 100644 --- a/src/base_cmdline.cpp +++ b/src/base_cmdline.cpp @@ -1,235 +1,240 @@ -#include - -#ifndef WX_PRECOMP -# include -#endif - -#include "base.h" +#include "base_cmdline.h" #include "controller.h" #include "misc.h" -#include "flags.h" #include "special_chars.h" -#include +#include #include -// contains all command line options -static const wxCmdLineEntryDesc s_cmd_line_desc[] = -{ - { wxCMD_LINE_SWITCH, "h", "help", "Display help for command line interface", wxCMD_LINE_VAL_NONE, wxCMD_LINE_OPTION_HELP}, - // required - { wxCMD_LINE_OPTION, "r", "radius", "Probe radius", wxCMD_LINE_VAL_DOUBLE}, - { wxCMD_LINE_OPTION, "g", "grid", "Spatial resolution of the underlying grid", wxCMD_LINE_VAL_DOUBLE}, - { wxCMD_LINE_OPTION, "fs", "file-structure", "Path to the structure file", wxCMD_LINE_VAL_STRING}, - // optional - { wxCMD_LINE_OPTION, "fe", "file-elements", "Path to the elements file", wxCMD_LINE_VAL_STRING}, - { wxCMD_LINE_OPTION, "do", "dir-output", "Path to the output directory", wxCMD_LINE_VAL_STRING}, - { wxCMD_LINE_OPTION, "r2", "radius2", "Large probe radius (for two-probe mode)", wxCMD_LINE_VAL_DOUBLE}, - { wxCMD_LINE_OPTION, "d", "depth", "Octree depth", wxCMD_LINE_VAL_NUMBER}, - { wxCMD_LINE_SWITCH, "ht", "hetatm", "Include HETATM from pdb file", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "uc", "unitcell", "Evaluate unit cell", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "sf", "surface", "Calculate surfaces", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "xr", "export-report", "Export report (requires:-do)", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "xt", "export-total", "Export total surface map (requires:-do)", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "xc", "export-cavities", "Export surface maps for all cavities (requires:-do)", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_OPTION, "o", "output", "Control what parts of the output to display (default:all)", wxCMD_LINE_VAL_STRING}, - { wxCMD_LINE_SWITCH, "q", "quiet", "Silence progress reporting", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "un", "unicode", "Allow unicode output", wxCMD_LINE_VAL_NONE}, - { wxCMD_LINE_SWITCH, "v", "version", "Display the app version", wxCMD_LINE_VAL_NONE}, - { wxCMD_LINE_NONE } -}; - -static const std::vector s_required_args = {"radius", "grid", "file-structure"}; - -bool validateProbes(const double, const double, const bool); -bool validateExport(const std::string, const std::vector); -bool validatePdb(const std::string, const bool, const bool); -unsigned evalDisplayOptions(const std::string); - -// return true to supress GUI, return false to open GUI -void MainApp::evalCmdLine(){ - // if there are no cmd line arguments, open app normally - silenceGUI(true); - wxCmdLineParser parser = wxCmdLineParser(argc,argv); - parser.SetDesc(s_cmd_line_desc); - // if something is wrong with the cmd line args, stop - if(parser.Parse() != 0){return;} - // ascii - if(parser.Found("un")){Symbol::allow_unicode();} - else{Symbol::limit2ascii();} - // version - if(parser.Found("v")){ - Ctrl::getInstance()->version(); - return; - } - - // Check if all required arguments are available - std::vector missing_args; - - for (const std::string& arg_name : s_required_args){ - if (!parser.Found(arg_name)){ - missing_args.push_back(arg_name); - } - } - - switch (missing_args.size()){ - case (0): - break; - case (1): - Ctrl::getInstance()->displayErrorMessage(911, missing_args); - return; - case (2): - Ctrl::getInstance()->displayErrorMessage(912, missing_args); - return; - case (3): - Ctrl::getInstance()->displayErrorMessage(913, missing_args); - return; - default: - Ctrl::getInstance()->displayErrorMessage(914, missing_args); - return; - } - // All required arguments are available - - Ctrl::getInstance()->hush(parser.Found("q")); - - // minimum required arguments for calculation - double probe_radius_s; - double grid_resolution; - wxString structure_file_path; - - parser.Found("r",&probe_radius_s); - parser.Found("g",&grid_resolution); - parser.Found("fs",&structure_file_path); - - // optional arguments with default values - wxString elements_file_path = Ctrl::getDefaultElemPath(); - wxString output_dir_path = ""; - wxString output = "all"; - double probe_radius_l = 0; - long tree_depth = 4; - bool opt_include_hetatm = false; - bool opt_unit_cell = false; - bool opt_surface_area = false; - bool opt_probe_mode = false; - bool exp_report = false; - bool exp_total_map = false; - bool exp_cavity_maps = false; - - parser.Found("fe",&elements_file_path); - parser.Found("do",&output_dir_path); - parser.Found("o",&output); - parser.Found("r2",&probe_radius_l); - parser.Found("d",&tree_depth); - opt_include_hetatm = parser.Found("ht"); - opt_unit_cell = parser.Found("uc"); - opt_surface_area = parser.Found("sf"); - opt_probe_mode = parser.Found("r") && parser.Found("r2"); - exp_report = parser.Found("xr"); - exp_total_map = parser.Found("xt"); - exp_cavity_maps = parser.Found("xc"); - - if(!validateProbes(probe_radius_s, probe_radius_l, opt_probe_mode) - || !validateExport(output_dir_path.ToStdString(), {exp_report, exp_total_map, exp_cavity_maps}) - || !validatePdb(structure_file_path.ToStdString(), opt_include_hetatm, opt_unit_cell)){ - return; - } - - unsigned display_flag = evalDisplayOptions(output.ToStdString()); - - // run calculation - Ctrl::getInstance()->runCalculation( - probe_radius_s, - probe_radius_l, - grid_resolution, - structure_file_path.ToStdString(), - elements_file_path.ToStdString(), - output_dir_path.ToStdString(), - (int)tree_depth, - opt_include_hetatm, - opt_unit_cell, - opt_surface_area, - opt_probe_mode, - exp_report, - exp_total_map, - exp_cavity_maps, - display_flag); +bool CommandLineParser::parse(const std::vector& args) { + try { + // Check for empty arguments + if (args.empty()) { + Ctrl::getInstance()->displayErrorMessage(901); + return false; + } + + // Process arguments directly without conversion to char* + for (size_t i = 1; i < args.size(); i++) { + const std::string& arg = args[i]; + if (arg.empty()) { + Ctrl::getInstance()->displayErrorMessage(901); + return false; + } + + if (arg[0] != '-') continue; + + std::string optName = arg.substr(arg[1] == '-' ? 2 : 1); + if (optName.empty()) { + Ctrl::getInstance()->displayErrorMessage(901); + return false; + } + + auto option = findOption(optName); + if (!option) { + std::cerr << "Unknown option: " << arg << std::endl; + return false; + } + + if (option->isSwitch) { + parsedFlags[option->longName] = true; + } else if (i + 1 < args.size()) { + const std::string& value = args[++i]; + if (value.empty() || value[0] == '-') { + std::cerr << "Invalid value for option: " << arg << std::endl; + return false; + } + parsedOptions[option->longName] = value; + } else { + std::cerr << "Missing value for option: " << arg << std::endl; + return false; + } + } + + // Don't validate required options for help or version flags + if (parsedFlags.count("help") > 0 || parsedFlags.count("version") > 0) { + return true; + } + + return validateRequiredOptions(); + } catch (const std::exception& e) { + std::cerr << "Error parsing arguments: " << e.what() << std::endl; + Ctrl::getInstance()->displayErrorMessage(901); + return false; + } +} + +bool CommandLineParser::parse(int argc, char* argv[]) { + std::vector args; + for (int i = 0; i < argc; ++i) { + args.push_back(argv[i]); + } + return parse(args); } -bool validateProbes(const double r1, const double r2, const bool pm){ - if(pm && r2 < r1){ - Ctrl::getInstance()->displayErrorMessage(104); - return false; - } - return true; +bool CommandLineParser::found(const std::string& name) const { + return parsedFlags.count(name) > 0 || parsedOptions.count(name) > 0; } -bool validateExport(const std::string out_dir, const std::vector exp_options){ - bool any_option_on = isIncluded(true,exp_options); - if (any_option_on && out_dir.empty()){ - Ctrl::getInstance()->displayErrorMessage(302); - return false; - } - return true; +std::optional CommandLineParser::getValue(const std::string& name) const { + auto it = parsedOptions.find(name); + if (it != parsedOptions.end()) { + return it->second; + } + return std::nullopt; } -bool validatePdb(const std::string file, const bool hetatm, const bool unitcell){ - if ((fileExtension(file) != "pdb" && fileExtension(file) != "cif") && (hetatm || unitcell)){ - Ctrl::getInstance()->displayErrorMessage(115); - return false; - } - return true; +void CommandLineParser::displayHelp() const { + std::cout << "Usage:\n"; + for (const auto& opt : options) { + std::cout << " -" << opt.shortName << ", --" << opt.longName + << (opt.isRequired ? " (required)" : "") << "\n " + << opt.description << "\n"; + } } -static std::map s_display_map { - {"none", mvOUT_NONE}, - {"inputfile", mvOUT_STRUCTURE}, - {"resolution", mvOUT_RESOLUTION}, - {"depth", mvOUT_DEPTH}, - {"radius_small", mvOUT_RADIUS_S}, - {"radius_large", mvOUT_RADIUS_L}, - {"input", mvOUT_INP}, - {"hetatm", mvOUT_OPT_HETATM}, - {"unitcell", mvOUT_OPT_UNITCELL}, - {"probemode", mvOUT_OPT_PROBEMODE}, - {"surface", mvOUT_OPT_SURFACE}, - {"options", mvOUT_OPT}, - {"formula", mvOUT_FORMULA}, - {"time", mvOUT_TIME}, - {"vol_vdw", mvOUT_VOL_VDW}, - {"vol_inaccessible", mvOUT_VOL_INACCESSIBLE}, - {"vol_core_s", mvOUT_VOL_CORE_S}, - {"vol_shell_s", mvOUT_VOL_SHELL_S}, - {"vol_core_l", mvOUT_VOL_CORE_L}, - {"vol_shell_l", mvOUT_VOL_SHELL_L}, - {"vol_mol", mvOUT_VOL_MOL}, - {"vol", mvOUT_VOL}, - {"surf_vdw", mvOUT_SURF_VDW}, - {"surf_mol", mvOUT_SURF_MOL}, - {"surf_excluded_s", mvOUT_SURF_EXCLUDED_S}, - {"surf_accessible_s", mvOUT_SURF_ACCESSIBLE_S}, - {"surf", mvOUT_SURF}, - {"cavities", mvOUT_CAVITIES}, - {"all", mvOUT_ALL} -}; - -unsigned evalDisplayOptions(const std::string output){ - std::stringstream ss(output); - std::vector display_options; - while(ss.good()){ - std::string substr; - getline(ss, substr, ','); - display_options.push_back(substr); - } - unsigned display_flag = 0; - bool unknown_flag = false; - for (std::string& elem : display_options){ - if (s_display_map.find(elem) == s_display_map.end()){ - unknown_flag = true; - } - else { - display_flag |= s_display_map.at(elem); - } - } - if (unknown_flag){Ctrl::getInstance()->displayErrorMessage(902);} - return display_flag; +std::optional CommandLineParser::findOption(const std::string& name) const { + for (const auto& opt : options) { + if (opt.shortName == name || opt.longName == name) { + return opt; + } + } + return std::nullopt; } +bool CommandLineParser::validateRequiredOptions() const { + std::vector missing; + for (const auto& opt : options) { + if (opt.isRequired && !found(opt.longName)) { + missing.push_back(opt.longName); + } + } + + if (!missing.empty()) { + Ctrl::getInstance()->displayErrorMessage(910 + missing.size(), missing); + return false; + } + return true; +} + +unsigned evalDisplayOptions(const std::string& output) { + std::stringstream ss(output); + std::string option; + unsigned display_flag = 0; + bool unknown_flag = false; + + while (std::getline(ss, option, ',')) { + auto it = DISPLAY_OPTIONS.find(option); + if (it != DISPLAY_OPTIONS.end()) { + display_flag |= it->second; + } else { + unknown_flag = true; + } + } + + if (unknown_flag) { + Ctrl::getInstance()->displayErrorMessage(902); + } + return display_flag; +} + +#ifndef EMSCRIPTEN +int main(int argc, char* argv[]) { + auto ctrl = Ctrl::getInstance(); + ctrl->disableGUI(); // Ensure we're in CLI mode + + CommandLineParser parser; + if (!parser.parse(argc, argv)) { + return 1; + } + + if (parser.found("help")) { + parser.displayHelp(); + return 0; + } + + if (parser.found("version")) { + ctrl->version(); + return 0; + } + + // Get required parameters with validation + auto probe_radius = parser.getValue("radius"); + if (!probe_radius) { + ctrl->displayErrorMessage(109); + return 1; + } + double probe_radius_s = std::stod(*probe_radius); + + auto grid = parser.getValue("grid"); + if (!grid) { + ctrl->displayErrorMessage(109); + return 1; + } + double grid_resolution = std::stod(*grid); + + auto structure_file = parser.getValue("file-structure"); + if (!structure_file) { + ctrl->displayErrorMessage(102); + return 1; + } + + // Optional parameters + std::string elements_file_path = parser.getValue("file-elements").value_or(Ctrl::getDefaultElemPath()); + std::string output_dir_path = parser.getValue("dir-output").value_or(""); + + double probe_radius_l = 0.0; + if (auto radius2 = parser.getValue("radius2")) { + probe_radius_l = std::stod(*radius2); + if (probe_radius_l < probe_radius_s) { + ctrl->displayErrorMessage(104); + return 1; + } + } + + int tree_depth = 3; // Default value + if (auto depth = parser.getValue("depth")) { + tree_depth = std::stoi(*depth); + } + + // Boolean flags + bool opt_include_hetatm = parser.found("hetatm"); + bool opt_unit_cell = parser.found("unitcell"); + bool opt_surface_area = parser.found("surface"); + bool opt_probe_mode = parser.getValue("radius2").has_value(); + bool exp_report = parser.found("export-report"); + bool exp_total_map = parser.found("export-total"); + bool exp_cavity_maps = parser.found("export-cavities"); + + // Set display options + unsigned display_flag = mvOUT_ALL; + if (auto output = parser.getValue("output")) { + display_flag = evalDisplayOptions(*output); + } + + // Handle quiet mode + if (parser.found("quiet")) { + ctrl->hush(true); + } + + // Run the calculation + bool success = ctrl->runCalculation( + probe_radius_s, + probe_radius_l, + grid_resolution, + *structure_file, + elements_file_path, + output_dir_path, + tree_depth, + opt_include_hetatm, + opt_unit_cell, + opt_surface_area, + opt_probe_mode, + exp_report, + exp_total_map, + exp_cavity_maps, + display_flag + ); + + return success ? 0 : 1; +} +#endif \ No newline at end of file diff --git a/src/controller.cpp b/src/controller.cpp index 67bf3202..800c6ca1 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1,7 +1,5 @@ - #include "controller.h" -#include "base.h" -#include "atom.h" // i don't know why +#include "atom.h" #include "model.h" #include "misc.h" #include "exception.h" @@ -13,12 +11,18 @@ #include #include +#ifdef MOLOVOL_GUI +#include "base.h" +#endif + /////////////////////// // STATIC ATTRIBUTES // /////////////////////// Ctrl* Ctrl::s_instance = NULL; +#ifdef MOLOVOL_GUI MainFrame* Ctrl::s_gui = NULL; +#endif /////////////////////////// // STATIC MEMBERS ACCESS // @@ -59,7 +63,11 @@ void Ctrl::disableGUI(){ } void Ctrl::enableGUI(){ +#ifdef MOLOVOL_GUI _to_gui = true; +#else + _to_gui = false; +#endif } bool Ctrl::isGUIEnabled(){ @@ -70,10 +78,6 @@ bool Ctrl::isGUIEnabled(){ // METHODS // ///////////// -void Ctrl::registerView(MainFrame* inp_gui){ - s_gui = inp_gui; -} - Ctrl* Ctrl::getInstance(){ if(s_instance == NULL){ s_instance = new Ctrl(); @@ -81,44 +85,39 @@ Ctrl* Ctrl::getInstance(){ return s_instance; } +#ifdef MOLOVOL_GUI +void Ctrl::registerView(MainFrame* inp_gui){ + s_gui = inp_gui; +} + bool Ctrl::loadElementsFile(){ - // create an instance of the model class - // ensures, that there is only ever one instance of the model class + // GUI-specific implementation... if(_current_calculation == NULL){ _current_calculation = new Model(); } std::string elements_filepath = s_gui->getElementsFilepath(); - // even if there is no valid radii file, the program can be used - // by manually setting radii in the GUI after loading a structure if(!_current_calculation->importElemFile(elements_filepath)){ displayErrorMessage(101); } - // refresh atom list using new radius map s_gui->displayAtomList(_current_calculation->generateAtomList()); return true; } bool Ctrl::loadAtomFile(){ - // create an instance of the model class - // ensures, that there is only ever one instance of the model class + // GUI-specific implementation... if(_current_calculation == NULL){ _current_calculation = new Model(); } - bool successful_import; - successful_import = _current_calculation->readAtomsFromFile(s_gui->getAtomFilepath(), s_gui->getIncludeHetatm()); - + bool successful_import = _current_calculation->readAtomsFromFile( + s_gui->getAtomFilepath(), + s_gui->getIncludeHetatm() + ); s_gui->displayAtomList(_current_calculation->generateAtomList()); - return successful_import; } -///////////////// -// CALCULATION // -///////////////// - -// default function call: transfer data from GUI to Model bool Ctrl::runCalculation(){ // reset abort flag setAbortFlag(false); @@ -177,8 +176,9 @@ bool Ctrl::runCalculation(){ return data.success; } +#endif -// for starting a calculation from the command line +// CLI version bool Ctrl::runCalculation( const double probe_radius_s, const double probe_radius_l, @@ -256,10 +256,12 @@ bool Ctrl::runCalculation( //////////////////////// void Ctrl::clearOutput(){ +#ifdef MOLOVOL_GUI if (_to_gui) { s_gui->extClearOutputText(); s_gui->extClearOutputGrid(); } +#endif } void Ctrl::displayInput(CalcReportBundle& data, const unsigned display_flag){ @@ -434,7 +436,7 @@ void Ctrl::displayResults(CalcReportBundle& data, const unsigned display_flag){ } } -void Ctrl::displayCavityList(CalcReportBundle& data, const unsigned display_flag){ +void Ctrl::displayCavityList(CalcReportBundle& data, [[maybe_unused]] const unsigned display_flag){ // store headers and units const std::wstring vol_unit = Symbol::angstrom() + Symbol::cubed(); const std::wstring surf_unit = Symbol::angstrom() + Symbol::squared(); @@ -466,8 +468,11 @@ void Ctrl::displayCavityList(CalcReportBundle& data, const unsigned display_flag } // display data + if (_to_gui){ + #ifdef MOLOVOL_GUI s_gui->extDisplayCavityList(table); + #endif } else{ table.print(); @@ -475,49 +480,65 @@ void Ctrl::displayCavityList(CalcReportBundle& data, const unsigned display_flag } void Ctrl::notifyUser(std::string str){ +#ifdef MOLOVOL_GUI if (_to_gui){ s_gui->extAppendOutput(str); + return; } - else { - std::cout << str; - } +#endif + std::cout << str; } void Ctrl::notifyUser(std::wstring wstr){ +#ifdef MOLOVOL_GUI if (_to_gui){ s_gui->extAppendOutputW(wstr); + return; } - else { - std::cout << wstr; - } +#endif + std::wcout << wstr; } void Ctrl::updateStatus(const std::string str){ +#ifdef MOLOVOL_GUI if (_to_gui) { s_gui->extSetStatus(str); + return; } - else if(_quiet) {} - else{ +#endif + if (!_quiet) { std::cout << str << std::endl; } } void Ctrl::updateProgressBar(const int percentage){ assert (percentage <= 100); +#ifdef MOLOVOL_GUI if (_to_gui) { s_gui->extSetProgressBar(percentage); + return; } - else if(_quiet) {} - else { - std::cout << std::to_string(percentage) + "\%" << std::endl; +#endif + if (!_quiet) { + std::cout << percentage << "%" << std::endl; } } void Ctrl::renderSurface(const Container3D& surf_data, const std::array origin, const double grid_step, const bool probe_mode, const unsigned char n_cavities, const std::vector& atomlist) { +#ifdef MOLOVOL_GUI if (_to_gui) { s_gui->extRenderSurface(surf_data, origin, grid_step, probe_mode, n_cavities, atomlist); } +#else + // In non-GUI mode, this function does nothing + (void)surf_data; + (void)origin; + (void)grid_step; + (void)probe_mode; + (void)n_cavities; + (void)atomlist; +#endif } const Container3D& Ctrl::getSurfaceData() const { @@ -587,9 +608,11 @@ bool Ctrl::getAbortFlag(){ // checks whether worker thread has received a signal to stop the calculation and // updates the progress of the calculation void Ctrl::updateCalculationStatus(){ +#ifdef MOLOVOL_GUI if (_to_gui){ setAbortFlag(s_gui->receivedAbortCommand()); } + #endif } //////////////////// @@ -646,14 +669,14 @@ void Ctrl::displayErrorMessage(const int error_code, const std::vectorextOpenErrorDialog(error_code, msg); + return; } - else{ - // Print to console - std::cout << error_code << ": " << msg << std::endl; - } +#endif + // Print to console + std::cout << error_code << ": " << msg << std::endl; } std::string Ctrl::getErrorMessage(const int error_code){ @@ -664,3 +687,4 @@ std::string Ctrl::getErrorMessage(const int error_code){ return s_error_codes.find(error_code)->second; } } + diff --git a/src/importmanager.cpp b/src/importmanager.cpp index 5df31bc2..0efe6864 100644 --- a/src/importmanager.cpp +++ b/src/importmanager.cpp @@ -1,7 +1,7 @@ #include "importmanager.h" #include "crystallographer.h" #include "misc.h" - +#include // This is a temporary fix so that we can wite unit tests for sections of the code #ifndef LIBRARY_BUILD #include "controller.h" diff --git a/src/model_filereading.cpp b/src/model_filereading.cpp index ef54e4b4..72e08432 100644 --- a/src/model_filereading.cpp +++ b/src/model_filereading.cpp @@ -13,7 +13,9 @@ #include #include #include - +#ifdef __EMSCRIPTEN__ +#include +#endif /////////////////// // IMPORT STRUCT // /////////////////// @@ -37,8 +39,7 @@ bool Model::importElemFile(const std::string& elem_path){ setRadiusMap(data.rad_map); _elem_weight = data.weight_map; _elem_Z = data.atomic_num_map; - - return (data.rad_map.size()); + return data.rad_map.size(); } // used for importing only the radius map from the radius file @@ -47,73 +48,92 @@ std::unordered_map Model::extractRadiusMap(const std::strin return extractDataFromElemFile(elem_path).rad_map; } -ElementsFileBundle extractDataFromElemFile(const std::string& elem_path){ - ElementsFileBundle data; +ElementsFileBundle extractDataFromElemFile(const std::string& elem_path) { + ElementsFileBundle data; - auto hasCorrectFormat = [](std::vector substrings){ - if (substrings.size() != 4){return false;} - if (substrings[0].find_first_not_of("0123456789") != std::string::npos){return false;} - for (char i : {2,3}){ - if (substrings[i].find_first_not_of("0123456789E.+e") != std::string::npos){return false;} + #ifdef __EMSCRIPTEN__ + char* content_ptr = (char*)EM_ASM_PTR({ + try { + var content = FS.readFile('/inputfile/elements.txt', {encoding: 'utf8'}); + var lengthBytes = lengthBytesUTF8(content) + 1; + var stringOnWasmHeap = _malloc(lengthBytes); + stringToUTF8(content, stringOnWasmHeap, lengthBytes); + return stringOnWasmHeap; + } catch(e) { + return 0; + } + }); + + if (!content_ptr) { + return data; } - return true; - }; - std::string line; - std::ifstream inp_file(elem_path); - bool invalid_symbol_detected = false; - bool invalid_radius_value = false; - bool invalid_weight_value = false; - while(getline(inp_file,line)){ - std::vector substrings = ImportMngr::splitLine(line); - // substrings[0]: Atomic Number - // substrings[1]: Element Symbol - // substrings[2]: Radius - // substrings[3]: Weight - if(hasCorrectFormat(substrings)){ - substrings[1] = ImportMngr::strToValidSymbol(substrings[1]); - // skip entry if element symbol invalid - if (substrings[1].empty()){ - invalid_symbol_detected = true; - } - else { - try{data.rad_map[substrings[1]] = std::stod(substrings[2]);} - catch (const std::invalid_argument& e){ - data.rad_map[substrings[1]] = 0; - invalid_radius_value = true; + std::string fileContent(content_ptr); + free(content_ptr); + + std::istringstream inp_stream(fileContent); + #else + std::ifstream inp_stream(elem_path); + #endif + + std::string line; + bool headerDone = false; + + while (getline(inp_stream, line)) { + if(line.empty()) { + continue; + } + + if(!headerDone) { + if(line[0] >= '0' && line[0] <= '9') { + headerDone = true; + } else { + continue; + } } - try{data.weight_map[substrings[1]] = std::stod(substrings[3]);} - catch (const std::invalid_argument& e){ - data.weight_map[substrings[1]] = 0; - invalid_weight_value = true; + + std::vector substrings = ImportMngr::splitLine(line); + + if(substrings.size() != 4) { + continue; + } + + try { + int atomic_num = std::stoi(substrings[0]); + std::string symbol = ImportMngr::strToValidSymbol(substrings[1]); + double radius = std::stod(substrings[2]); + double weight = std::stod(substrings[3]); + + if(!symbol.empty()) { + data.rad_map[symbol] = radius; + data.weight_map[symbol] = weight; + data.atomic_num_map[symbol] = atomic_num; + } } - try{data.atomic_num_map[substrings[1]] = std::stoi(substrings[0]);} - catch (const std::invalid_argument& e){ - data.atomic_num_map[substrings[1]] = 0; + catch(const std::exception&) { + continue; } - } } - } - if (invalid_symbol_detected) {Ctrl::getInstance()->displayErrorMessage(106);} - if (invalid_radius_value) {Ctrl::getInstance()->displayErrorMessage(107);} - if (invalid_weight_value) {Ctrl::getInstance()->displayErrorMessage(108);} - return data; -} + return data; +} ////////////////////// // ATOM FILE IMPORT // ////////////////////// bool Model::readAtomsFromFile(const std::string& filepath, bool include_hetatm){ clearAtomData(); - + + // Debug output for filename and extension + std::string extension = fileExtension(filepath); std::vector atom_list; + // XYZ file import - if (fileExtension(filepath) == "xyz"){ + if (extension == "xyz"){ atom_list = ImportMngr::readFileXYZ(filepath); } // PDB file import - else if (fileExtension(filepath) == "pdb"){ + else if (extension == "pdb"){ const std::pair,UnitCell> import_data = ImportMngr::readFilePDB(filepath, include_hetatm); atom_list = import_data.first; @@ -123,7 +143,7 @@ bool Model::readAtomsFromFile(const std::string& filepath, bool include_hetatm){ } } // CIF file import - else if (fileExtension(filepath) == "cif"){ + else if (extension == "cif"){ try{ const std::pair,UnitCell> import_data = ImportMngr::readFileCIF(filepath); atom_list = import_data.first; diff --git a/src/space.cpp b/src/space.cpp index 5c2a1a54..a95a6411 100644 --- a/src/space.cpp +++ b/src/space.cpp @@ -134,21 +134,35 @@ void Space::assignAtomVsCore(){ const std::array vxl_origin = getOrigin(); // calculate side length of top level voxel const double vxl_dist = _grid_size * pow(2,_max_depth); - std::array vxl_pos; - std::array top_lvl_index; - for(top_lvl_index[0] = 0; top_lvl_index[0] < getGridsteps()[0]; top_lvl_index[0]++){ - Ctrl::getInstance()->updateCalculationStatus(); - vxl_pos[0] = vxl_origin[0] + vxl_dist * (0.5 + top_lvl_index[0]); - for(top_lvl_index[1] = 0; top_lvl_index[1] < getGridsteps()[1]; top_lvl_index[1]++){ - vxl_pos[1] = vxl_origin[1] + vxl_dist * (0.5 + top_lvl_index[1]); - for(top_lvl_index[2] = 0; top_lvl_index[2] < getGridsteps()[2]; top_lvl_index[2]++){ - vxl_pos[2] = vxl_origin[2] + vxl_dist * (0.5 + top_lvl_index[2]); - // voxel position is deliberately not stored in voxel object to reduce memory cost - if (Ctrl::getInstance()->getAbortFlag()){return;} - getTopVxl(top_lvl_index).evalRelationToAtoms(top_lvl_index, vxl_pos, _max_depth); + const auto steps = getGridsteps(); + bool should_abort = false; + + #pragma omp parallel + { + std::array vxl_pos; + std::array top_lvl_index; + + #pragma omp for collapse(2) + for(unsigned int i = 0; i < steps[0]; i++){ + Ctrl::getInstance()->updateCalculationStatus(); + vxl_pos[0] = vxl_origin[0] + vxl_dist * (0.5 + i); + for(unsigned int j = 0; j < steps[1]; j++){ + vxl_pos[1] = vxl_origin[1] + vxl_dist * (0.5 + j); + for(unsigned int k = 0; k < steps[2]; k++){ + if (!should_abort) { + vxl_pos[2] = vxl_origin[2] + vxl_dist * (0.5 + k); + top_lvl_index = {i, j, k}; + // voxel position is deliberately not stored in voxel object to reduce memory cost + if (Ctrl::getInstance()->getAbortFlag()){ + should_abort = true; + } else { + getTopVxl(top_lvl_index).evalRelationToAtoms(top_lvl_index, vxl_pos, _max_depth); + } + } + } } + Ctrl::getInstance()->updateProgressBar(int(100*(double(i)+1)/double(getGridsteps()[0]))); } - Ctrl::getInstance()->updateProgressBar(int(100*(double(top_lvl_index[0])+1)/double(getGridsteps()[0]))); } } diff --git a/src/special_chars.cpp b/src/special_chars.cpp index 3720f87f..5ff4689f 100644 --- a/src/special_chars.cpp +++ b/src/special_chars.cpp @@ -1,52 +1,62 @@ - #include "special_chars.h" #include #include +#include +#include +#include + +#ifdef MOLOVOL_GUI +#include +#endif bool Symbol::s_ascii = false; -void Symbol::limit2ascii(){ +void Symbol::limit2ascii() { Symbol::s_ascii = true; } -void Symbol::allow_unicode(){ +void Symbol::allow_unicode() { Symbol::s_ascii = false; } -std::wstring Symbol::angstrom(){ - return Symbol::s_ascii? L"A" : L"\u212B"; +std::wstring Symbol::angstrom() { + return Symbol::s_ascii ? L"A" : L"\u212B"; } -std::wstring Symbol::squared(){ - return Symbol::s_ascii? L"^2" : L"\u00B2"; +std::wstring Symbol::squared() { + return Symbol::s_ascii ? L"^2" : L"\u00B2"; } -std::wstring Symbol::cubed(){ - return Symbol::s_ascii? L"^3": L"\u00B3"; +std::wstring Symbol::cubed() { + return Symbol::s_ascii ? L"^3" : L"\u00B3"; } // from a numeric char, return the unicode encoded subscripts -wchar_t Symbol::digitSubscript(char digit){ - if (Symbol::s_ascii){return digit;} +wchar_t Symbol::digitSubscript(char digit) { + if (Symbol::s_ascii) { + return digit; + } wchar_t subscript = 0x2080; // unicode encoding of subscript "0" int value = digit - '0'; // convert numeric char to corresponding int subscript = subscript + value; // adding a digit to subscript "0" return subscript of that digit return subscript; } -std::wstring Symbol::generateChemicalFormulaUnicode(std::string chemical_formula){ +std::wstring Symbol::generateChemicalFormulaUnicode(std::string chemical_formula) { std::wstring chemical_formula_unicode; - for (size_t i = 0; i < chemical_formula.size(); i++){ - if (isalpha(chemical_formula[i])){ - // it is much simpler to convert string to wxString to wstring than directly from string to wstring + for (size_t i = 0; i < chemical_formula.size(); i++) { + if (isalpha(chemical_formula[i])) { +#ifdef MOLOVOL_GUI + // Use wxString for GUI builds wxString buffer(chemical_formula[i]); chemical_formula_unicode.append(buffer.wxString::ToStdWstring()); - } - else { +#else + // Direct conversion for non-GUI builds + chemical_formula_unicode.push_back(static_cast(chemical_formula[i])); +#endif + } else { chemical_formula_unicode += digitSubscript(chemical_formula[i]); } } return chemical_formula_unicode; -} - - +} \ No newline at end of file diff --git a/src/wasm_bindings.cpp b/src/wasm_bindings.cpp new file mode 100644 index 00000000..84b8bb75 --- /dev/null +++ b/src/wasm_bindings.cpp @@ -0,0 +1,173 @@ +#include +#include +#include +#include +#include +#include +#include +#include "controller.h" +#include "flags.h" +#include "misc.h" +#include "base_cmdline.h" + +using namespace emscripten; + +// Structure to hold calculation parameters +struct CalculationParams { + // Required parameters + double probe_radius_small; + double grid_resolution; + std::string structure_content; + std::string filename; + // Optional parameters + double probe_radius_large = 0.0; + int tree_depth = 4; + + // Boolean flags + bool include_hetatm = false; + bool unit_cell = false; + bool surface_area = false; + bool export_report = false; + bool export_total_map = false; + bool export_cavity_maps = false; +}; + +// Initialize Controller for WASM environment +void init_controller() { + Ctrl::getInstance()->disableGUI(); + Ctrl::getInstance()->hush(false); // Enable output for debugging +} + +// Get version information +std::string get_version() { + init_controller(); + return Ctrl::getVersion(); +} + +// Helper function to write file content and verify it +bool writeAndVerifyFile(const std::string& filename, const std::string& content) { + std::ofstream file(filename); + if (!file) { + printf("Failed to open file for writing: %s\n", filename.c_str()); + return false; + } + + file << content; + file.close(); + + // Verify file was written + std::ifstream verify(filename); + if (!verify) { + printf("Failed to verify file existence: %s\n", filename.c_str()); + return false; + } + + return true; +} + +// Main calculation function using direct parameter passing +val calculate_volumes_direct(const CalculationParams& params) { + printf("\n=== Starting calculation with direct parameter binding ===\n"); + + // Initialize controller + init_controller(); + + // Validate parameters + if (params.probe_radius_small <= 0) { + val::global("Error").new_(std::string("Invalid probe radius")).throw_(); + return val::null(); + } + + if (params.grid_resolution <= 0 || params.grid_resolution < 0.1) { + val::global("Error").new_(std::string("Invalid grid resolution")).throw_(); + return val::null(); + } + + if (params.structure_content.empty()) { + val::global("Error").new_(std::string("No structure content provided")).throw_(); + return val::null(); + } + + + + // Set output flags for all relevant information + unsigned output_flags = mvOUT_RESOLUTION | mvOUT_DEPTH | mvOUT_RADIUS_S | mvOUT_RADIUS_L | + mvOUT_OPT | mvOUT_VOL | mvOUT_SURF | mvOUT_CAVITIES; + + // Write structure content to temporary file + std::string input_filepath = "/tmp/" + params.filename; + if (!writeAndVerifyFile(input_filepath, params.structure_content)) { + val::global("Error").new_(std::string("Failed to write structure file")).throw_(); + return val::null(); + } + // Run calculation + bool success = false; + try { + success = Ctrl::getInstance()->runCalculation( + params.probe_radius_small, + params.probe_radius_large, + params.grid_resolution, + input_filepath,//strucutre input file + "inputfile/elements.txt",//elements + "/tmp", // Temporary directory for any exports + params.tree_depth, + params.include_hetatm, + params.unit_cell, + params.surface_area, + params.probe_radius_large > 0, // probe mode + params.export_report, + params.export_total_map, + params.export_cavity_maps, + output_flags + ); + + printf("Calculation %s\n", success ? "succeeded" : "failed"); + + } catch (const std::exception& e) { + printf("Calculation failed with exception: %s\n", e.what()); + val::global("Error").new_(std::string("Calculation failed with exception: ") + e.what()).throw_(); + return val::null(); + } catch (...) { + printf("Calculation failed with unknown exception\n"); + val::global("Error").new_(std::string("Calculation failed with unknown exception")).throw_(); + return val::null(); + } + + if (!success) { + printf("Calculation failed - checking completion status\n"); + if (!Ctrl::getInstance()->isCalculationDone()) { + printf("Calculation was not completed\n"); + } + val::global("Error").new_(std::string("Calculation failed - Check input file format and parameters")).throw_(); + return val::null(); + } + + printf("Calculation completed successfully\n"); + + // Return success + val result = val::object(); + result.set("success", true); + result.set("version", Ctrl::getVersion()); + + return result; +} + +// Bind everything to JavaScript +EMSCRIPTEN_BINDINGS(molovol_module) { + value_object("CalculationParams") + .field("probe_radius_small", &CalculationParams::probe_radius_small) + .field("grid_resolution", &CalculationParams::grid_resolution) + .field("structure_content", &CalculationParams::structure_content) + .field("filename", &CalculationParams::filename) + .field("probe_radius_large", &CalculationParams::probe_radius_large) + .field("tree_depth", &CalculationParams::tree_depth) + .field("include_hetatm", &CalculationParams::include_hetatm) + .field("unit_cell", &CalculationParams::unit_cell) + .field("surface_area", &CalculationParams::surface_area) + .field("export_report", &CalculationParams::export_report) + .field("export_total_map", &CalculationParams::export_total_map) + .field("export_cavity_maps", &CalculationParams::export_cavity_maps); + + function("get_version", &get_version); + function("calculate_volumes", &calculate_volumes_direct); +} \ No newline at end of file diff --git a/test/class_atomtree.cpp b/test/class_atomtree.cpp index 92362659..89fcaf5f 100644 --- a/test/class_atomtree.cpp +++ b/test/class_atomtree.cpp @@ -85,7 +85,7 @@ int main() { for (size_t at_id = 0; at_id < all_atoms.size(); ++at_id) { const Atom& at = all_atoms[at_id]; std::vector closest = atomtree.listAllWithin(at.getPos(), 0); - if (!(closest.size()-1 == valence.at(at.symbol))) return -1; + if (!(closest.size()-1 == static_cast(valence.at(at.symbol)))) return -1; } } } diff --git a/test/performance_test.cpp b/test/performance_test.cpp new file mode 100644 index 00000000..1f9c3f97 --- /dev/null +++ b/test/performance_test.cpp @@ -0,0 +1,148 @@ +#include +#include "vector.h" +#include "atom.h" +#include "space.h" +#include "model.h" +#include "importmanager.h" +#include +#include +#include + +// Performance test for Vector class operations +static void BM_VectorOperations(benchmark::State& state) { + Vector vec1(1.2, 2.4, 3.6); + Vector vec2(6.0, 4.8, 3.0); + + for (auto _ : state) { + benchmark::DoNotOptimize(vec1 + vec2); + benchmark::DoNotOptimize(vec1 - vec2); + benchmark::DoNotOptimize(vec1 * 2.0); + benchmark::DoNotOptimize(vec1.length()); + benchmark::DoNotOptimize(vec1 * vec2); + benchmark::DoNotOptimize(distance(vec1, vec2)); + } +} +BENCHMARK(BM_VectorOperations); + +// Performance test for Atom class operations +static void BM_AtomOperations(benchmark::State& state) { + // Using the 6-argument constructor from atom.h + Atom atom1(1.0, 2.0, 3.0, "C", 1.7, 6); + Atom atom2(4.0, 5.0, 6.0, "O", 1.52, 8); + + for (auto _ : state) { + benchmark::DoNotOptimize(distance(atom1.getPosVec(), atom2.getPosVec())); + benchmark::DoNotOptimize(atom1.getRad() + atom2.getRad()); + benchmark::DoNotOptimize(atom1.number + atom2.number); + } +} +BENCHMARK(BM_AtomOperations); + +// Performance test for Space class operations +static void BM_SpaceOperations(benchmark::State& state) { + std::vector atoms = { + Atom(1.0, 2.0, 3.0, "C", 1.7, 6), + Atom(4.0, 5.0, 6.0, "O", 1.52, 8), + Atom(7.0, 8.0, 9.0, "H", 1.2, 1) + }; + + double grid_size = 0.2; + int depth = 4; + double r_probe = 1.4; + bool unit_cell_option = false; + std::array unit_cell_axes = {0, 0, 0}; + + Space space(atoms, grid_size, depth, r_probe, unit_cell_option, unit_cell_axes); + + for (auto _ : state) { + std::vector cavities; // Create a vector to pass by reference + bool output_flag = false; // Added missing boolean reference parameter + space.assignTypeInGrid(atoms, cavities, 1.4, 0, false, output_flag); + } +} +BENCHMARK(BM_SpaceOperations); + +// Performance test for Model class operations +static void BM_ModelOperations(benchmark::State& state) { + std::string elements_file_path = SOURCE_DIR "/inputfile/elements.txt"; + std::string structure_file_path = SOURCE_DIR "/inputfile/test_unit_cell_1.pdb"; + + Model model; + try { + if (!model.importElemFile(elements_file_path)) { + state.SkipWithError("Failed to import elements file"); + return; + } + if (!model.readAtomsFromFile(structure_file_path, true)) { + state.SkipWithError("Failed to read structure file"); + return; + } + + // Set parameters with unit cell analysis since we're using a PDB file + if (!model.setParameters( + structure_file_path, // file_path + "", // output_dir + true, // inc_hetatm + true, // analyze_unit_cell + true, // calc_surface_areas + false, // probe_mode + 1.4, // r_probe1 + 0.0, // r_probe2 + 0.2, // grid_step + 4, // max_depth + false, // make_report + false, // make_full_map + false, // make_cav_maps + model.getRadiusMap(), // rad_map + {} // included_elem (empty means include all) + )) { + state.SkipWithError("Failed to set parameters"); + return; + } + + for (auto _ : state) { + model.generateData(); + } + } catch (const std::exception& e) { + state.SkipWithError(e.what()); + } +} +BENCHMARK(BM_ModelOperations); + +// Performance test for Model::processUnitCell +static void BM_ImportElements(benchmark::State& state) { + std::string elements_file_path = SOURCE_DIR "/inputfile/elements.txt"; + Model model; + + for (auto _ : state) { + model.importElemFile(elements_file_path); + } +} +BENCHMARK(BM_ImportElements); + +// Benchmark PDB file reading +static void BM_ReadPDBFile(benchmark::State& state) { + std::string elements_file_path = SOURCE_DIR "/inputfile/elements.txt"; + std::string structure_file_path = SOURCE_DIR "/inputfile/test_unit_cell_1.pdb"; + Model model; + model.importElemFile(elements_file_path); + + for (auto _ : state) { + model.readAtomsFromFile(structure_file_path, true); + } +} +BENCHMARK(BM_ReadPDBFile); + +// Benchmark atom list generation +static void BM_GenerateAtomList(benchmark::State& state) { + std::string elements_file_path = SOURCE_DIR "/inputfile/elements.txt"; + std::string structure_file_path = SOURCE_DIR "/inputfile/test_unit_cell_1.pdb"; + Model model; + model.importElemFile(elements_file_path); + model.readAtomsFromFile(structure_file_path, true); + + for (auto _ : state) { + model.generateAtomList(); + } +} +BENCHMARK(BM_GenerateAtomList); \ No newline at end of file diff --git a/webserver/app.py b/webserver/app.py index d01cbb0b..f8affd1b 100644 --- a/webserver/app.py +++ b/webserver/app.py @@ -15,6 +15,7 @@ app = Flask(__name__) UPLOAD_FOLDER = './userupload/' +EXECUTABLE_CMD = "/build/molovol" app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # check if upload folder exists and create if missing @@ -334,7 +335,7 @@ def io(): print(f"Starting process with args: {args}\n") try: - mlvl_out = subprocess.check_output(["./launch_headless.sh"] + args, stderr=subprocess.STDOUT).decode( + mlvl_out = subprocess.check_output([EXECUTABLE_CMD] + args, stderr=subprocess.STDOUT).decode( "utf-8") except Exception as e: out = "Exception: " + str(e) @@ -422,7 +423,7 @@ def get_entry_size(entry_path): # Request the executable's version. If the executable is not found, then the web page crashes def app_version(): - return subprocess.check_output(["./launch_headless.sh", "-v"], stderr=subprocess.STDOUT).decode("utf-8") + return subprocess.check_output([EXECUTABLE_CMD, "-v"], stderr=subprocess.STDOUT).decode("utf-8") def is_nonzero_numeric(value): diff --git a/webserver/startserver.sh b/webserver/startserver.sh new file mode 100755 index 00000000..ddbe2717 --- /dev/null +++ b/webserver/startserver.sh @@ -0,0 +1 @@ +python3 -m http.server 8080 diff --git a/webserver/static/main.js b/webserver/static/main.js new file mode 100644 index 00000000..1b39b1a9 --- /dev/null +++ b/webserver/static/main.js @@ -0,0 +1,231 @@ +// Initialize output and worker +let currentActivity = ''; +let currentProgress = 0; +let animationFrameId = null; +let pendingCalcUpdates = []; + +// Create and initialize web worker +const worker = new Worker('static/worker.js'); +worker.onmessage = function(e) { + const { type, data } = e.data; + switch (type) { + case 'ready': + console.log('WASM worker ready'); + break; + case 'version': + // Update version display + const versionElement = document.getElementById('version'); + if (versionElement) { + versionElement.textContent = 'v' + data; + } + break; + case 'output': + // More precise detection of progress messages + const isProgress = + /^\d+%$/.test(data) || + data.includes('Searching inaccessible areas') || + data.includes('Probing space') || + data.includes('Blocking off cavities') || + data.includes('Identifying cavities'); + + appendOutput(data, isProgress); + break; + case 'error': + appendOutput(`Error: ${data}`, true); + break; + case 'result': + handleCalculationComplete(data); + break; + } +}; + +function updateProgressBar(activity, progress) { + const progressOutput = document.getElementById('progress-output'); + if (!progressOutput) return; + + // Create or update progress elements + if (!progressOutput.querySelector('.progress-activity')) { + progressOutput.innerHTML = ` + + + `; + } + + // Update progress content + const activityLabel = progressOutput.querySelector('.progress-activity'); + const percentageLabel = progressOutput.querySelector('.progress-percentage'); + + if (activity) { + currentActivity = activity; + currentProgress = 0; + activityLabel.textContent = activity; + percentageLabel.textContent = '0%'; + } + + if (progress !== undefined) { + currentProgress = progress; + percentageLabel.textContent = `${progress}%`; + } +} + +function appendOutput(text, isProgress = false) { + if (isProgress) { + // Check if text contains an activity name + const activityMatch = text.match(/(Probing space|Identifying cavities|Searching inaccessible areas|Blocking off cavities)\.{3}/); + if (activityMatch) { + updateProgressBar(activityMatch[1], 0); + } else { + // Check if text contains a percentage + const percentMatch = text.match(/(\d+)%/); + if (percentMatch) { + updateProgressBar(null, parseInt(percentMatch[1])); + } + } + } else { + const calcResultElement = document.getElementById('calculation-result'); + if (!calcResultElement) return; + + // Check if we already have the table container + let tableContainer = calcResultElement.querySelector('.cavities-container'); + if (!tableContainer) { + // Create the containers for both text and table + const textDiv = document.createElement('div'); + textDiv.className = 'calculation-text'; + tableContainer = document.createElement('div'); + tableContainer.className = 'cavities-container'; + + // Create table structure + const table = document.createElement('table'); + table.className = 'cavities'; + table.innerHTML = ` + + + + + `; + + tableContainer.appendChild(table); + calcResultElement.appendChild(textDiv); + calcResultElement.appendChild(tableContainer); + } + + if (detectTableContent(text)) { + const table = tableContainer.querySelector('table.cavities'); + + if (text.includes('Cavity ID')) { + // Handle header row + const headers = ['Cavity ID', 'Occupied Volume (A^3)', 'Cavity Type', 'Center Coord x, y, z (A)']; + const headerRow = table.querySelector('#table-header'); + if (headerRow) { + headerRow.innerHTML = headers.map(h => `${h}`).join(''); + table.style.display = 'table'; + } + } else { + // Handle data row + const tbody = table.querySelector('#table-body'); + if (tbody) { + // Split by whitespace but preserve content within parentheses + const parts = text.trim().split(/\s+/); + const cells = []; + let i = 0; + + // First three columns (ID, Volume, Type) + while (i < parts.length - 3) { // -3 for the coordinates + cells.push(parts[i]); + i++; + } + + // Last column (coordinates) + cells.push(parts.slice(i).join(' ')); + + const tr = document.createElement('tr'); + tr.innerHTML = cells.map(c => `${c}`).join(''); + tbody.appendChild(tr); + } + } + } else { + // Handle non-table text + const textDiv = calcResultElement.querySelector('.calculation-text'); + if (textDiv) { + const div = document.createElement('div'); + div.textContent = text; + textDiv.appendChild(div); + } + } + } +} + +function detectTableContent(text) { + return text.includes('Cavity ID') || + (text.match(/^\d+\s+\d+\.\d+\s+/) && text.includes('Isolated')); +} + +function handleCalculationComplete(result) { + // Hide progress bar when calculation is complete + const progressOutput = document.getElementById('progress-output'); + if (progressOutput) { + progressOutput.style.display = 'none'; + } + + setupDownloadButtons(result); + window.location.hash = 'output'; +} + +// Form submission handler +async function handleSubmit(event) { + event.preventDefault(); + + const fileInput = document.getElementById('structure'); + const file = fileInput.files[0]; + if (!file) { + alert('Please select a structure file.'); + return; + } + + try { + // Clear previous output and show results section + const outputElement = document.getElementById('calculation-result'); + outputElement.textContent = ''; + + // Show and reset progress output + const progressOutput = document.getElementById('progress-output'); + progressOutput.style.display = 'block'; + progressOutput.innerHTML = ''; + + document.getElementById('results').style.display = 'block'; + + // Read file content + const fileContent = await file.text(); + + // Create calculation parameters + const params = { + probe_radius_small: parseFloat(document.getElementById('radius').value), + probe_radius_large: document.getElementById('radius2').value ? + parseFloat(document.getElementById('radius2').value) : 0.0, + grid_resolution: parseFloat(document.getElementById('grid').value), + tree_depth: 4, + structure_content: fileContent, + filename: file.name, + include_hetatm: document.getElementById('hetatm').checked, + unit_cell: document.getElementById('unitcell').checked, + surface_area: document.getElementById('surface').checked, + export_report: true, + export_total_map: document.getElementById('export-total').checked, + export_cavity_maps: document.getElementById('export-cavities').checked + }; + + // Send calculation request to worker + worker.postMessage({ type: 'calculate', params }); + + } catch (error) { + console.error('Submission failed:', error); + appendOutput(`Error: ${error.message}`); + } +} + +// Initialize event listeners when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + const form = document.getElementById('inputs'); + form.addEventListener('submit', handleSubmit); + initializeUI(); +}); \ No newline at end of file diff --git a/webserver/static/style.css b/webserver/static/style.css index c7d757b8..8a9ac8af 100644 --- a/webserver/static/style.css +++ b/webserver/static/style.css @@ -1,428 +1,225 @@ -:root{ - --sans-font:-apple-system,BlinkMacSystemFont,"Avenir Next",Avenir,"Nimbus Sans L",Roboto,Noto,"Segoe UI",Arial,Helvetica,"Helvetica Neue",sans-serif; - --mono-font:Consolas,Menlo,Monaco,"Andale Mono","Ubuntu Mono",monospace; - --bg:#fff; - --accent-bg:#f5f7ff; - --text:#212121; - --text-light:#585858; - --border:#d8dae1; - --accent:#2380be; - --code:#d81b60; - --preformatted:#444; - --marked:#ffdd33; - --disabled:#efefef; -} -/* Dark mode -@media (prefers-color-scheme:dark){ - :root{ - --bg:#212121; - --accent-bg:#2b2b2b; - --text:#dcdcdc; - --text-light:#ababab; - --border:#666; - --accent:#ffb300; - --code:#f06292; - --preformatted:#ccc; - --disabled:#111} - img,video{opacity:.8} -}*/ -html{ - font-family:var(--sans-font); - scroll-behavior:smooth -} -body{ - color:var(--text); - background:var(--bg); - font-size:1rem; - line-height:1.5; - display:grid; - grid-template-columns: - 1fr min(45rem,90%) 1fr;margin:0} -body>*{grid-column:2} -body>header{ - background:var(--accent-bg); - border-bottom:1px solid var(--border); - text-align:center; - padding:0 .5rem 2rem .5rem; - grid-column:1/-1; - box-sizing:border-box -} -body>header h1{ - max-width:1200px; - margin:1rem auto} -body>header p{ - max-width:40rem; - margin:1rem auto -} -main{padding-top:1.5rem} -body>footer{ - margin-top:3rem; - padding:2rem 1rem 1.5rem 1rem; - color:var(--text-light); - font-size:.9rem; - text-align:center; - border-top:1.5px solid var(--border) -} -h1{font-size:3rem} -h2{ - font-size:2.6rem; - margin-top:2rem; -} -h3{font-size:2rem;margin-top:2rem} -h4{ - font-size:1.3rem; - margin-bottom:1rem; -} -h5{font-size:1.15rem} -h6{font-size:.96rem} -h1,h2,h3{line-height:1.1} -@media only screen and (max-width:720px){ - h1{font-size:2.5rem} - h2{font-size:2.1rem} - h3{font-size:1.75rem} - h4{font-size:1.25rem} -} -a,a:visited{color:var(--accent)} -a:hover{text-decoration:none} -[role=button],button,input[type=button],input[type=reset],input[type=submit]{ - border:none; - border-radius:5px; - background:var(--accent); - font-size:1rem; - color:var(--bg); - padding:.7rem .9rem; - margin:.5rem 0 -} -[role=button][aria-disabled=true],button[disabled],input[type=button][disabled],input[type=checkbox][disabled],input[type=radio][disabled],input[type=reset][disabled],input[type=submit][disabled],select[disabled]{ - opacity:.5; - cursor:not-allowed -} -input:disabled,select:disabled,textarea:disabled{ - cursor:not-allowed; - background-color:var(--disabled) -} -input[type=range]{padding:0} -abbr{cursor:help} -[role=button]:focus,[role=button]:not([aria-disabled=true]):hover,button:enabled:hover,button:focus,input[type=button]:enabled:hover,input[type=button]:focus,input[type=reset]:enabled:hover,input[type=reset]:focus,input[type=submit]:enabled:hover,input[type=submit]:focus{ - filter:brightness(1.4); - cursor:pointer -} -header>nav{ - font-size:1rem; - line-height:2; - padding:1rem 0 0 0 -} -header>nav ol,header>nav ul{ - align-content:space-around; - align-items:center; - display:flex; - flex-direction:row; - justify-content:center; - list-style-type:none; - margin:0;padding:0 -} -header>nav ol li,header>nav ul li{display:inline-block} -header>nav a,header>nav a:visited{ - margin:0 1rem 1rem 0; - border:1px solid var(--border); - border-radius:5px; - color:var(--text); - display:inline-block; - padding:.1rem 1rem; - text-decoration:none -} -header>nav a:hover{ - color:var(--accent); - border-color:var(--accent) -} -header>nav a:last-child{margin-right:0} -@media only screen and (max-width:720px){ - header>nav a{ - border:none; - padding:0; - color:var(--accent); - text-decoration:underline;line-height:1 - } -} -aside{ - width:30%; - padding:0 15px; - margin-left:15px; - float:right; - background:var(--accent-bg); - border:1px solid var(--border); - border-radius:5px -} -article{ - border:1px solid var(--border); - padding:1rem; - border-radius:5px -} -article h2:first-child,section h2:first-child{margin-top:1rem} -section{ - border-top:1px solid var(--border); - border-bottom:1px solid var(--border); - padding:2rem 1rem; - margin:3rem 0 -} -details{ - background:var(--accent-bg); - border:1px solid var(--border); - border-radius:5px;margin-bottom:1rem -} -summary{ - cursor:pointer; - font-weight:700; - padding:.6rem 1rem -} -details[open]{padding:.6rem 1rem .75rem 1rem} -details[open] summary+*{margin-top:0} -details[open] summary{ - margin-bottom:.5rem; - padding:0 -} -details[open]>:last-child{margin-bottom:0} -input,select,textarea{ - font-size:inherit; - font-family:inherit; - padding:.5rem; - margin-bottom:.5rem; - color:var(--text); - background:var(--bg); - border:1px solid var(--border); - border-radius:5px; - box-shadow:none; - box-sizing:border-box; - width:60%; - -moz-appearance:none; - -webkit-appearance:none; - appearance:none -} -select{ - background-image:linear-gradient(45deg,transparent 49%,var(--text) 51%),linear-gradient(135deg,var(--text) 51%,transparent 49%); - background-position:calc(100% - 20px),calc(100% - 15px); - background-size:5px 5px,5px 5px; - background-repeat:no-repeat -} -select[multiple]{ - background-image:none!important -} -input[type=checkbox],input[type=radio]{ - vertical-align:middle; - position:relative -} -input[type=radio]{border-radius:100%} -input[type=checkbox]:checked,input[type=radio]:checked{ - background:var(--accent) -} -input[type=checkbox]:checked::after{ - content:" "; - width:.1em; - height:.25em; - border-radius:0; - position:absolute; - top:.05em; - left:.18em; - background:0 0; - border-right:solid var(--bg) .08em; - border-bottom:solid var(--bg) .08em; - font-size:1.8em; - transform:rotate(45deg) -} -input[type=radio]:checked::after{ - content:" "; - width:.25em; - height:.25em; - border-radius:100%; - position:absolute; - top:.125em; - background:var(--bg); - left:.125em;font-size:32px -} -textarea{width:80%} -@media only screen and (max-width:720px){ - input,select,textarea{width:100%} -} -input[type=checkbox],input[type=radio]{ - width:auto -} -input[type=file]{border:0} -hr{ - color:var(--border); - border-top:1px; - margin:1rem auto -} -mark{ - padding:2px 5px; - border-radius:4px; - background:var(--marked) -} -main img,main video{ - max-width:100%; - height:auto; - border-radius:5px -} -figure{ - margin:0; - text-align:center -} -figcaption{ - font-size:.9rem; - color:var(--text-light); - margin-bottom:1rem -} -blockquote{ - margin:2rem 0 2rem 2rem; - padding:.4rem .8rem; - border-left:.35rem solid var(--accent); - color:var(--text-light); - font-style:italic -} -cite{ - font-size:.9rem; - color:var(--text-light); - font-style:normal -} -code,kbd,pre,pre span,samp{ - font-family:var(--mono-font); - color:var(--code) -} -kbd{ - color:var(--preformatted); - border:1px solid var(--preformatted); - border-bottom:3px solid var(--preformatted); - border-radius:5px; - padding:.1rem .4rem -} -pre{ - padding:1rem 1.4rem; - max-width:100%; - overflow:auto; - color:var(--preformatted); - background:var(--accent-bg); - border:1px solid var(--border); - border-radius:5px -} -pre code{ - color:var(--preformatted); - background:0 0; - margin:0; - padding:0 +/* Base variables */ +:root { + --sans-font: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir, "Nimbus Sans L", Roboto, Noto, "Segoe UI", Arial, Helvetica, sans-serif; + --mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + --bg: #fff; + --accent-bg: #f5f7ff; + --text: #212121; + --text-light: #585858; + --border: #d8dae1; + --accent: #2380be; + --code: #d81b60; + --preformatted: #444; +} + +/* Wordmark and header */ +#wordmark { + margin-top: 2rem; + margin-bottom: 1.5rem; } -input[type=checkbox],input[type=number] { - margin-right:0.6rem; +#wordmark p { + margin-top: 0; } -input[type=checkbox]:disabled + label { - color:var(--text-light); +#disclaimer { + border-top: 1.5px solid var(--border); + border-bottom: 1.5px solid var(--border); + margin-bottom: 2rem; } -body { - background-image: url('images/bg.png'); - background-repeat: no-repeat; - background-attachment: fixed; - background-size: cover; +.infotext { + font-size: 0.8rem; + margin: 0.6rem; + text-align: justify; } -/* Wordmark style */ - -#wordmark { - margin-top:2rem; - margin-bottom:1.5rem; +/* Base layout */ +html { + font-family: var(--sans-font); + scroll-behavior: smooth; } -#wordmark p { - margin-top:0rem; +body { + color: var(--text); + background: var(--bg) url('images/bg.png') no-repeat fixed; + background-size: cover; + font-size: 1rem; + line-height: 1.5; + display: grid; + grid-template-columns: 1fr min(45rem, 90%) 1fr; + margin: 0; } -/* Input form */ - -#inputs div { - margin-bottom:0.8rem; +body > * { + grid-column: 2; +} + +/* Typography */ +h1, h2, h3 { line-height: 1.1; } +h1 { font-size: 3rem; } +h2 { font-size: 2.6rem; margin-top: 2rem; } +h3 { font-size: 2rem; margin-top: 2rem; } +h4 { font-size: 1.3rem; margin-bottom: 1rem; } + +@media only screen and (max-width: 720px) { + h1 { font-size: 2.5rem; } + h2 { font-size: 2.1rem; } + h3 { font-size: 1.75rem; } + h4 { font-size: 1.25rem; } + input, select, textarea { width: 100%; } +} + +/* Links and buttons */ +a, a:visited { color: var(--accent); } +a:hover { text-decoration: none; } + +button, +input[type="button"], +input[type="reset"], +input[type="submit"], +[role="button"] { + border: none; + border-radius: 5px; + background: var(--accent); + font-size: 1rem; + color: var(--bg); + padding: 0.7rem 0.9rem; + margin: 0.5rem 0; + cursor: pointer; +} + +button:enabled:hover, +input[type="button"]:enabled:hover, +input[type="submit"]:enabled:hover, +[role="button"]:hover:not([aria-disabled="true"]) { + filter: brightness(1.4); +} + +/* Form elements */ +input, select, textarea { + font: inherit; + padding: 0.5rem; + margin-bottom: 0.5rem; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 5px; + width: 60%; + appearance: none; } -/* User communication */ - -#disclaimer { - border-top:1.5px solid var(--border); - border-bottom:1.5px solid var(--border); - margin-bottom:2rem; +input[type="checkbox"], +input[type="radio"] { + width: auto; + margin-right: 0.6rem; + vertical-align: middle; } -#consent{ - border-top:1.5px solid var(--border); +/* Disabled states */ +input:disabled, +select:disabled, +textarea:disabled, +button:disabled { + opacity: 0.5; + cursor: not-allowed; + background-color: var(--disabled); } -.infotext { - font-size:0.8rem; - margin:0.6rem; - text-align: justify; +/* Tables */ +table { + border-collapse: collapse; + width: 100%; + margin: 1.5rem 0; } -/* Output style */ -#outputbox { +td, th { + text-align: left; + padding: 0.6rem; border: 1px solid var(--border); - background-color:var(--accent-bg); - font-family:var(--mono-font); } -output p { - margin:0.4rem; - font-size:0.8rem +th { + background: var(--bg); + font-weight: 700; } -output p b { - font-size:0.9rem; -} +tr:nth-child(even) { background: var(--bg); } -output table { - font-size:0.8rem; - margin: 0.4rem; - width:98%; +/* Dropzone styles */ +.dropzone { + border: 2px dashed var(--accent); + border-radius: 8px; + padding: 20px; + text-align: center; + background: #f8fafc; + transition: all 0.3s ease; + cursor: pointer; + margin-bottom: 20px; } -.error { - color:red; +.dropzone.drag-over { + background: #e8f4ff; + border-color: #1d6697; } -/* Table style */ -table{ - border-collapse:collapse; - margin:1.5rem 0; - overflow:auto; - width:100%; +.dropzone.has-file { + background: #e8f4ff; + border-style: solid; } -td,th{ - text-align:left; - padding:0.6rem +.dropzone-input { display: none; } + +.file-info { + margin-top: 10px; + color: var(--accent); + display: none; } -th{ - background:var(--bg); - font-weight:700; - border-top:1.5px solid var(--border); - border-bottom:var(--border); +.dropzone.has-file .file-info { display: block; } + +/* Output styles */ +.output-box { + white-space: pre-wrap; + padding: 12px; + border: 1px solid var(--border); + border-radius: 4px; + background-color: var(--accent-bg); + font-family: var(--mono-font); } -tr:nth-child(even){ - background:var(--bg); +#calculation-result { + height: 400px; + overflow-y: auto; } -table caption{ - font-weight:700; - margin-bottom:.1rem; +#progress-output { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + max-height: 100px; + overflow-y: auto; } -/* Remove splin buttons from number input */ -/* Chrome, Safari, Edge, Opera */ -input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { +.progress-activity { flex-grow: 1; } +.progress-percentage { color: var(--accent); } + +/* Remove number input spinners */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } -/* Firefox */ -input[type=number] { - -moz-appearance: textfield; -} +input[type="number"] { -moz-appearance: textfield; } + +/* Footer */ +footer { + margin-top: 3rem; + padding: 2rem 1rem 1.5rem; + color: var(--text-light); + font-size: 0.9rem; + text-align: center; + border-top: 1.5px solid var(--border); +} \ No newline at end of file diff --git a/webserver/static/ui.js b/webserver/static/ui.js new file mode 100644 index 00000000..73acda3e --- /dev/null +++ b/webserver/static/ui.js @@ -0,0 +1,122 @@ +function initializeUI() { + initializeFileInput(); + initializeProbeValidation(); + initializeDragAndDrop(); +} + +function initializeFileInput() { + document.getElementById('structure').addEventListener('change', function(e) { + evalFilename(this.value, true); + }); +} + +function initializeProbeValidation() { + const radius = document.getElementById("radius"); + const radius2 = document.getElementById("radius2"); + radius2.addEventListener("input", (event) => { + if (radius2.value !== "" && parseFloat(radius2.value) < parseFloat(radius.value)) { + radius2.setCustomValidity("Must be larger than small probe radius"); + radius2.reportValidity(); + } else { + radius2.setCustomValidity(""); + } + }); +} + +function initializeDragAndDrop() { + const dropzone = document.getElementById('dropzone'); + const input = document.getElementById('structure'); + const fileNameDisplay = document.getElementById('file-name'); + + function handleFileSelect(file) { + if (file) { + fileNameDisplay.textContent = file.name; + dropzone.classList.add('has-file'); + evalFilename(file.name, true); + } else { + fileNameDisplay.textContent = 'No file selected'; + dropzone.classList.remove('has-file'); + } + } + + input.addEventListener('change', function(e) { + handleFileSelect(this.files[0]); + }); + + // Setup drag and drop handlers + dropzone.addEventListener('click', function(e) { + if (e.target !== input) { + e.preventDefault(); + input.click(); + } + }); + + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropzone.addEventListener(eventName, e => { + e.preventDefault(); + e.stopPropagation(); + }); + }); + + ['dragenter', 'dragover'].forEach(eventName => { + dropzone.addEventListener(eventName, () => { + dropzone.classList.add('drag-over'); + }); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropzone.addEventListener(eventName, () => { + dropzone.classList.remove('drag-over'); + }); + }); + + dropzone.addEventListener('drop', function(e) { + const file = e.dataTransfer.files[0]; + if (file) { + input.files = e.dataTransfer.files; + handleFileSelect(file); + } + }); +} + +function evalFilename(filename, newFile) { + const extension = filename.match(/\.\w+$/gm); + if (!extension) { + lockCheckboxes(false, false); + alert('File extension unknown.'); + return; + } + + switch (extension[0].toLowerCase()) { + case '.pdb': + if (newFile) { + document.getElementById('hetatm').checked = true; + } + lockCheckboxes(false, false); + break; + case '.xyz': + lockCheckboxes(true, true); + break; + case '.cif': + lockCheckboxes(true, false); + break; + default: + lockCheckboxes(false, false); + alert('File extension unknown.'); + } +} + +function lockCheckboxes(htDisable, ucDisable) { + document.getElementById('hetatm').disabled = htDisable; + document.getElementById('unitcell').disabled = ucDisable; +} + +function handleCalculationComplete(result) { + setupDownloadButtons(result); + window.location.hash = 'output'; +} + +function setupDownloadButtons(result) { + // Implementation of download button setup... + // This will depend on how your WASM module returns results +} diff --git a/webserver/static/worker.js b/webserver/static/worker.js new file mode 100644 index 00000000..88980bb3 --- /dev/null +++ b/webserver/static/worker.js @@ -0,0 +1,54 @@ +// Import WASM module - using importScripts for web worker compatibility +importScripts('molovol_wasm.js'); + +// Configure module +const moduleConfig = { + print: (text) => { + self.postMessage({ type: 'output', data: text }); + }, + printErr: (text) => { + self.postMessage({ type: 'error', data: text }); + } +}; + +// Initialize WASM module, name is defined in wasm compilation config in cmake +let wasmModule; +createMoloVolModule(moduleConfig).then(module => { + wasmModule = module; + + // Get and send version to main thread + try { + const version = wasmModule.get_version(); + self.postMessage({ + type: 'version', + data: version + }); + } catch (error) { + console.error('Failed to get version:', error); + } + + self.postMessage({ type: 'ready' }); +}).catch(error => { + self.postMessage({ type: 'error', data: `Failed to initialize WASM: ${error}` }); +}); + +// Handle messages from main thread +self.onmessage = async function(e) { + if (e.data.type !== 'calculate' || !wasmModule) return; + + try { + // Run calculation + const result = wasmModule.calculate_volumes(e.data.params); + + // Notify main thread of completion + self.postMessage({ + type: 'result', + data: result + }); + } catch (error) { + self.postMessage({ + type: 'error', + data: error.toString() + }); + } +}; \ No newline at end of file diff --git a/webserver/wasmform.html b/webserver/wasmform.html new file mode 100644 index 00000000..a63980c6 --- /dev/null +++ b/webserver/wasmform.html @@ -0,0 +1,131 @@ + + + + MoloVol Web + + + + + + + + + + + + + + + + +
+ MoloVol wordmark +

+
+ +
+

+ An equivalent desktop version that offers slightly more options is available for download + here + along with the user manual. + You can find a detailed ex­pla­na­tion of the underlying algorithms in + this publication. +

+
+ +
+
+
+ +
+
📄
+

Drag and drop your file here or click to select

+
+
+ Selected file: No file selected +
+ +
+
+ +
+ +
+ + +
+ + +
+
+ +
+ +
+ + +
+ + +
+
+ +
+

Export options

+
+
+ + +
+ +
+ +
+
+ + +
+ +
+
+ + + + + + + + \ No newline at end of file